feat(dash) very complete
This commit is contained in:
+103
-45
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user