feat(dash) very complete

This commit is contained in:
2026-05-16 01:23:03 -06:00
parent 0b4440969c
commit 47edaacae7
+103 -45
View File
@@ -149,17 +149,28 @@
</select> </select>
<input id="mediaCategory" placeholder="Category (e.g. kitchen, front, bathroom)" <input id="mediaCategory" placeholder="Category (e.g. kitchen, front, bathroom)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3"/> class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3 focus:ring-2 focus:ring-indigo-500"/>
<input id="mediaType" placeholder="Type (e.g. photo, video)" <input id="mediaType" placeholder="Type (e.g. photo, video)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3"/> class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-4 focus:ring-2 focus:ring-indigo-500"/>
<input id="mediaFile" type="file" accept="image/*" <div class="mb-2 text-xs font-semibold text-zinc-400 uppercase tracking-wider">File Source</div>
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3"/>
<input id="mediaFile" type="file" accept="image/*,video/*"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
<div class="py-2 flex items-center justify-center">
<div class="h-px bg-zinc-800 w-full"></div>
<span class="px-3 text-xs text-zinc-500 font-bold mb-1">OR</span>
<div class="h-px bg-zinc-800 w-full"></div>
</div>
<input id="mediaUrl" type="url" placeholder="Paste remote URL (e.g. Google Drive, Imgur)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-5 focus:ring-2 focus:ring-indigo-500"/>
<button id="uploadMediaBtn" <button id="uploadMediaBtn"
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold"> class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold text-white">
Upload Save Media
</button> </button>
<div id="uploadMsg" class="text-sm text-zinc-400 mt-2"></div> <div id="uploadMsg" class="text-sm text-zinc-400 mt-2"></div>
@@ -167,7 +178,7 @@
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4"> <div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<h3 class="font-bold text-lg mb-3">Media List</h3> <h3 class="font-bold text-lg mb-3">Media List</h3>
<div id="mediaList" class="space-y-3"></div> <div id="mediaList" class="columns-2 md:columns-3 lg:columns-4 gap-3 space-y-3"></div>
</div> </div>
</div> </div>
@@ -234,6 +245,7 @@
let allCampaigns = []; let allCampaigns = [];
let showInactive = false; let showInactive = false;
let campaignsWithMedia = new Set();
// ============================= // =============================
// UI HELPERS // UI HELPERS
@@ -335,10 +347,16 @@
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
if (error) { if (error) {
list.innerHTML = `<div class="text-red-400 text-sm">Error loading campaigns: ${error.message}</div>`; document.getElementById("campaignList").innerHTML = `<div class="text-red-400 text-sm">Error loading campaigns: ${error.message}</div>`;
return; return;
} }
const { data: mediaData } = await supabaseClient
.from("media")
.select("campaign_id");
campaignsWithMedia = new Set(mediaData?.map(m => m.campaign_id) || []);
allCampaigns = data || []; allCampaigns = data || [];
// Populate filters // Populate filters
@@ -361,6 +379,7 @@
}); });
renderCampaigns(); renderCampaigns();
await loadMedia();
} }
function renderCampaigns() { function renderCampaigns() {
@@ -391,6 +410,11 @@
</div> </div>
<div class="flex flex-col gap-2 shrink-0"> <div class="flex flex-col gap-2 shrink-0">
${campaignsWithMedia.has(c.id) ? `
<a href="/gallery?campaign_id=${c.id}" target="_blank"
class="bg-indigo-600 hover:bg-indigo-500 text-center text-xs px-3 py-2 rounded-xl transition text-white font-semibold">
Gallery
</a>` : ''}
<button class="editBtn bg-zinc-800 hover:bg-zinc-700 text-xs px-3 py-2 rounded-xl transition" <button class="editBtn bg-zinc-800 hover:bg-zinc-700 text-xs px-3 py-2 rounded-xl transition"
data-id="${c.id}"> data-id="${c.id}">
Edit Edit
@@ -590,6 +614,7 @@
const category = document.getElementById("mediaCategory").value.trim(); const category = document.getElementById("mediaCategory").value.trim();
const type = document.getElementById("mediaType").value.trim(); const type = document.getElementById("mediaType").value.trim();
const fileInput = document.getElementById("mediaFile"); const fileInput = document.getElementById("mediaFile");
const urlInput = document.getElementById("mediaUrl").value.trim();
const msg = document.getElementById("uploadMsg"); const msg = document.getElementById("uploadMsg");
if (!campaignId) { if (!campaignId) {
@@ -597,46 +622,63 @@
return; return;
} }
if (!fileInput.files || fileInput.files.length === 0) { const hasFile = fileInput.files && fileInput.files.length > 0;
setMessage(msg, "Select a file first.", true); const hasUrl = !!urlInput;
if (!hasFile && !hasUrl) {
setMessage(msg, "Please select a file OR paste a URL.", true);
return;
}
if (hasFile && hasUrl) {
setMessage(msg, "Please select EITHER a file OR a URL, not both.", true);
return; return;
} }
const file = fileInput.files[0]; let finalUrl = "";
setMessage(msg, "Uploading..."); if (hasUrl) {
// Validate URL
try {
new URL(urlInput);
finalUrl = urlInput;
} catch (_) {
setMessage(msg, "Please provide a valid URL (include http/https).", true);
return;
}
} else {
const file = fileInput.files[0];
setMessage(msg, "Uploading to bucket...");
const ext = file.name.split(".").pop(); const ext = file.name.split(".").pop();
const filePath = `${campaignId}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`; const filePath = `${campaignId}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
// Upload to storage const { error: uploadError } = await supabaseClient
const { data: uploadData, error: uploadError } = await supabaseClient .storage
.storage .from(STORAGE_BUCKET)
.from(STORAGE_BUCKET) .upload(filePath, file, { cacheControl: "3600", upsert: false });
.upload(filePath, file, {
cacheControl: "3600",
upsert: false
});
if (uploadError) { if (uploadError) {
setMessage(msg, "Upload error: " + uploadError.message, true); setMessage(msg, "Upload error: " + uploadError.message, true);
return; return;
}
const { data: publicUrlData } = supabaseClient
.storage
.from(STORAGE_BUCKET)
.getPublicUrl(filePath);
finalUrl = publicUrlData.publicUrl;
} }
// Get public URL (bucket must be public OR use signed urls) setMessage(msg, "Saving in database...");
const { data: publicUrlData } = supabaseClient
.storage
.from(STORAGE_BUCKET)
.getPublicUrl(filePath);
const publicUrl = publicUrlData.publicUrl;
// Insert into media table // Insert into media table
const { error: insertError } = await supabaseClient const { error: insertError } = await supabaseClient
.from("media") .from("media")
.insert([{ .insert([{
campaign_id: campaignId, campaign_id: campaignId,
url: publicUrl, url: finalUrl,
category, category,
type type
}]); }]);
@@ -647,22 +689,26 @@
} }
fileInput.value = ""; fileInput.value = "";
document.getElementById("mediaUrl").value = "";
document.getElementById("mediaCategory").value = ""; document.getElementById("mediaCategory").value = "";
document.getElementById("mediaType").value = ""; document.getElementById("mediaType").value = "";
setMessage(msg, "Upload complete!"); setMessage(msg, "Media saved successfully!");
campaignsWithMedia.add(campaignId);
renderCampaigns();
await loadMedia(); await loadMedia();
} }
async function loadMedia() { async function loadMedia() {
const list = document.getElementById("mediaList");
list.innerHTML = "";
const { data, error } = await supabaseClient const { data, error } = await supabaseClient
.from("media") .from("media")
.select("*") .select("*")
.order("created_at", { ascending: false }) .order("created_at", { ascending: false })
.limit(50); .limit(100);
const list = document.getElementById("mediaList");
list.innerHTML = "";
if (error) { if (error) {
list.innerHTML = `<div class="text-red-400 text-sm">Error loading media: ${error.message}</div>`; list.innerHTML = `<div class="text-red-400 text-sm">Error loading media: ${error.message}</div>`;
@@ -674,16 +720,28 @@
return; return;
} }
data.forEach(m => { // Filter media from active campaigns
const activeCampaignIds = new Set(allCampaigns.filter(c => c.active !== false).map(c => c.id));
const filteredMedia = data.filter(m => activeCampaignIds.has(m.campaign_id));
if (filteredMedia.length === 0) {
list.innerHTML = `<div class="text-zinc-400 text-sm">No media from active campaigns found.</div>`;
return;
}
filteredMedia.forEach(m => {
const campaign = allCampaigns.find(c => c.id === m.campaign_id);
const campaignName = campaign ? campaign.name : "Unknown Campaign";
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden"; card.className = "bg-zinc-950 border border-zinc-800 rounded-xl overflow-hidden break-inside-avoid mb-3";
card.innerHTML = ` card.innerHTML = `
<img src="${m.url}" class="w-full h-auto" /> <img src="${m.url}" class="w-full h-auto block" loading="lazy" />
<div class="p-3"> <div class="p-2.5">
<div class="text-sm font-semibold">${m.category || "photo"}${m.type || "unknown"}</div> <div class="text-[10px] font-bold text-indigo-400 uppercase tracking-tight mb-0.5">${m.category || "photo"}${m.type || "unknown"}</div>
<div class="text-xs text-zinc-400 mt-1">Campaign: ${m.campaign_id}</div> <div class="text-xs font-semibold text-zinc-200 truncate">${campaignName}</div>
<div class="text-xs text-zinc-500 mt-1">${formatDate(m.created_at)}</div> <div class="text-[10px] text-zinc-500 mt-1">${formatDate(m.created_at)}</div>
</div> </div>
`; `;