feat(dash) very complete
This commit is contained in:
+91
-33
@@ -149,17 +149,28 @@
|
||||
</select>
|
||||
|
||||
<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)"
|
||||
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/*"
|
||||
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3"/>
|
||||
<div class="mb-2 text-xs font-semibold text-zinc-400 uppercase tracking-wider">File Source</div>
|
||||
|
||||
<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"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
|
||||
Upload
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold text-white">
|
||||
Save Media
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -234,6 +245,7 @@
|
||||
|
||||
let allCampaigns = [];
|
||||
let showInactive = false;
|
||||
let campaignsWithMedia = new Set();
|
||||
|
||||
// =============================
|
||||
// UI HELPERS
|
||||
@@ -335,10 +347,16 @@
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const { data: mediaData } = await supabaseClient
|
||||
.from("media")
|
||||
.select("campaign_id");
|
||||
|
||||
campaignsWithMedia = new Set(mediaData?.map(m => m.campaign_id) || []);
|
||||
|
||||
allCampaigns = data || [];
|
||||
|
||||
// Populate filters
|
||||
@@ -361,6 +379,7 @@
|
||||
});
|
||||
|
||||
renderCampaigns();
|
||||
await loadMedia();
|
||||
}
|
||||
|
||||
function renderCampaigns() {
|
||||
@@ -391,6 +410,11 @@
|
||||
</div>
|
||||
|
||||
<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"
|
||||
data-id="${c.id}">
|
||||
Edit
|
||||
@@ -590,6 +614,7 @@
|
||||
const category = document.getElementById("mediaCategory").value.trim();
|
||||
const type = document.getElementById("mediaType").value.trim();
|
||||
const fileInput = document.getElementById("mediaFile");
|
||||
const urlInput = document.getElementById("mediaUrl").value.trim();
|
||||
const msg = document.getElementById("uploadMsg");
|
||||
|
||||
if (!campaignId) {
|
||||
@@ -597,46 +622,63 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileInput.files || fileInput.files.length === 0) {
|
||||
setMessage(msg, "Select a file first.", true);
|
||||
const hasFile = fileInput.files && fileInput.files.length > 0;
|
||||
const hasUrl = !!urlInput;
|
||||
|
||||
if (!hasFile && !hasUrl) {
|
||||
setMessage(msg, "Please select a file OR paste a URL.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
if (hasFile && hasUrl) {
|
||||
setMessage(msg, "Please select EITHER a file OR a URL, not both.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage(msg, "Uploading...");
|
||||
let finalUrl = "";
|
||||
|
||||
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 filePath = `${campaignId}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
|
||||
|
||||
// Upload to storage
|
||||
const { data: uploadData, error: uploadError } = await supabaseClient
|
||||
const { error: uploadError } = await supabaseClient
|
||||
.storage
|
||||
.from(STORAGE_BUCKET)
|
||||
.upload(filePath, file, {
|
||||
cacheControl: "3600",
|
||||
upsert: false
|
||||
});
|
||||
.upload(filePath, file, { cacheControl: "3600", upsert: false });
|
||||
|
||||
if (uploadError) {
|
||||
setMessage(msg, "Upload error: " + uploadError.message, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get public URL (bucket must be public OR use signed urls)
|
||||
const { data: publicUrlData } = supabaseClient
|
||||
.storage
|
||||
.from(STORAGE_BUCKET)
|
||||
.getPublicUrl(filePath);
|
||||
|
||||
const publicUrl = publicUrlData.publicUrl;
|
||||
finalUrl = publicUrlData.publicUrl;
|
||||
}
|
||||
|
||||
setMessage(msg, "Saving in database...");
|
||||
|
||||
// Insert into media table
|
||||
const { error: insertError } = await supabaseClient
|
||||
.from("media")
|
||||
.insert([{
|
||||
campaign_id: campaignId,
|
||||
url: publicUrl,
|
||||
url: finalUrl,
|
||||
category,
|
||||
type
|
||||
}]);
|
||||
@@ -647,22 +689,26 @@
|
||||
}
|
||||
|
||||
fileInput.value = "";
|
||||
document.getElementById("mediaUrl").value = "";
|
||||
document.getElementById("mediaCategory").value = "";
|
||||
document.getElementById("mediaType").value = "";
|
||||
|
||||
setMessage(msg, "Upload complete!");
|
||||
setMessage(msg, "Media saved successfully!");
|
||||
|
||||
campaignsWithMedia.add(campaignId);
|
||||
renderCampaigns();
|
||||
await loadMedia();
|
||||
}
|
||||
|
||||
async function loadMedia() {
|
||||
const list = document.getElementById("mediaList");
|
||||
list.innerHTML = "";
|
||||
|
||||
const { data, error } = await supabaseClient
|
||||
.from("media")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(50);
|
||||
.limit(100);
|
||||
|
||||
const list = document.getElementById("mediaList");
|
||||
list.innerHTML = "";
|
||||
|
||||
if (error) {
|
||||
list.innerHTML = `<div class="text-red-400 text-sm">Error loading media: ${error.message}</div>`;
|
||||
@@ -674,16 +720,28 @@
|
||||
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");
|
||||
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 = `
|
||||
<img src="${m.url}" class="w-full h-auto" />
|
||||
<div class="p-3">
|
||||
<div class="text-sm font-semibold">${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 text-zinc-500 mt-1">${formatDate(m.created_at)}</div>
|
||||
<img src="${m.url}" class="w-full h-auto block" loading="lazy" />
|
||||
<div class="p-2.5">
|
||||
<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 font-semibold text-zinc-200 truncate">${campaignName}</div>
|
||||
<div class="text-[10px] text-zinc-500 mt-1">${formatDate(m.created_at)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user