dashboard start
This commit is contained in:
+605
@@ -0,0 +1,605 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Campaign Dashboard</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Supabase JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-zinc-950 text-white min-h-screen">
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- LOGIN SCREEN -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="loginScreen" class="flex items-center justify-center min-h-screen p-6">
|
||||||
|
<div class="w-full max-w-md bg-zinc-900 border border-zinc-800 rounded-2xl shadow-lg p-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-2">🏠 Campaign Dashboard</h1>
|
||||||
|
<p class="text-zinc-400 text-sm mb-6">
|
||||||
|
Login to manage campaigns, sessions, and media uploads.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<input id="loginEmail" type="email" placeholder="Email"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
|
||||||
|
|
||||||
|
<input id="loginPassword" type="password" placeholder="Password"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
|
||||||
|
|
||||||
|
<button id="loginBtn"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="loginError" class="text-red-400 text-sm"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- DASHBOARD -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="dashboard" class="hidden min-h-screen">
|
||||||
|
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-zinc-800 bg-zinc-950 sticky top-0 z-50">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-bold text-lg">Dashboard</h2>
|
||||||
|
<p id="userEmail" class="text-xs text-zinc-400"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="logoutBtn"
|
||||||
|
class="bg-zinc-800 hover:bg-zinc-700 px-4 py-2 rounded-xl text-sm font-semibold">
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="grid grid-cols-3 gap-2 bg-zinc-900 border border-zinc-800 rounded-2xl p-2">
|
||||||
|
<button class="tabBtn bg-indigo-600 rounded-xl py-2 text-sm font-semibold" data-tab="campaignsTab">
|
||||||
|
Campaigns
|
||||||
|
</button>
|
||||||
|
<button class="tabBtn bg-zinc-800 rounded-xl py-2 text-sm font-semibold" data-tab="sessionsTab">
|
||||||
|
Sessions
|
||||||
|
</button>
|
||||||
|
<button class="tabBtn bg-zinc-800 rounded-xl py-2 text-sm font-semibold" data-tab="mediaTab">
|
||||||
|
Media
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- CAMPAIGNS TAB -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="campaignsTab" class="tabContent p-4 space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
|
<h3 class="font-bold text-lg mb-3">Create Campaign</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<input id="newCampaignName" placeholder="Campaign name"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none"/>
|
||||||
|
|
||||||
|
<textarea id="newCampaignNotes" placeholder="Notes (optional)"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none"></textarea>
|
||||||
|
|
||||||
|
<button id="createCampaignBtn"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
|
||||||
|
Create Campaign
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="campaignCreateMsg" class="text-sm text-zinc-400"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
|
<h3 class="font-bold text-lg mb-3">Campaign List</h3>
|
||||||
|
|
||||||
|
<div id="campaignList" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- SESSIONS TAB -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="sessionsTab" class="tabContent hidden p-4 space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
|
<h3 class="font-bold text-lg mb-3">Sessions</h3>
|
||||||
|
|
||||||
|
<select id="sessionsCampaignFilter"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3">
|
||||||
|
<option value="">All campaigns</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button id="refreshSessionsBtn"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
|
||||||
|
Refresh Sessions
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
|
<h3 class="font-bold text-lg mb-3">Session List</h3>
|
||||||
|
<div id="sessionList" class="space-y-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- MEDIA TAB -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="mediaTab" class="tabContent hidden p-4 space-y-4">
|
||||||
|
|
||||||
|
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
|
||||||
|
<h3 class="font-bold text-lg mb-3">Upload Media</h3>
|
||||||
|
|
||||||
|
<select id="mediaCampaignSelect"
|
||||||
|
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3">
|
||||||
|
<option value="">Select campaign</option>
|
||||||
|
</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"/>
|
||||||
|
|
||||||
|
<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"/>
|
||||||
|
|
||||||
|
<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"/>
|
||||||
|
|
||||||
|
<button id="uploadMediaBtn"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="uploadMsg" class="text-sm text-zinc-400 mt-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// =============================
|
||||||
|
// CONFIG
|
||||||
|
// =============================
|
||||||
|
const SUPABASE_URL = "http://144.217.160.51:8000";
|
||||||
|
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NTU4OTc2LCJleHAiOjE5MzQyMzg5NzZ9.eFvvIQUstht8TxNffqAcqfmgS7W2JMZsDxkV41XBPOA";
|
||||||
|
|
||||||
|
// This bucket must exist in Supabase Storage
|
||||||
|
const STORAGE_BUCKET = "campaign-media";
|
||||||
|
|
||||||
|
const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// UI HELPERS
|
||||||
|
// =============================
|
||||||
|
function show(el) { el.classList.remove("hidden"); }
|
||||||
|
function hide(el) { el.classList.add("hidden"); }
|
||||||
|
|
||||||
|
function setMessage(el, msg, isError=false) {
|
||||||
|
el.innerText = msg;
|
||||||
|
el.className = isError ? "text-sm text-red-400" : "text-sm text-zinc-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d) {
|
||||||
|
try {
|
||||||
|
return new Date(d).toLocaleString();
|
||||||
|
} catch {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// AUTH
|
||||||
|
// =============================
|
||||||
|
async function login() {
|
||||||
|
const email = document.getElementById("loginEmail").value.trim();
|
||||||
|
const password = document.getElementById("loginPassword").value.trim();
|
||||||
|
const loginError = document.getElementById("loginError");
|
||||||
|
|
||||||
|
loginError.innerText = "";
|
||||||
|
|
||||||
|
const { data, error } = await supabaseClient.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
loginError.innerText = error.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await supabaseClient.auth.signOut();
|
||||||
|
await refreshUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshUI() {
|
||||||
|
const { data: { session } } = await supabaseClient.auth.getSession();
|
||||||
|
|
||||||
|
const loginScreen = document.getElementById("loginScreen");
|
||||||
|
const dashboard = document.getElementById("dashboard");
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
show(loginScreen);
|
||||||
|
hide(dashboard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(loginScreen);
|
||||||
|
show(dashboard);
|
||||||
|
|
||||||
|
document.getElementById("userEmail").innerText = session.user.email;
|
||||||
|
|
||||||
|
await loadCampaigns();
|
||||||
|
await loadSessions();
|
||||||
|
await loadMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// TABS
|
||||||
|
// =============================
|
||||||
|
function setupTabs() {
|
||||||
|
const tabButtons = document.querySelectorAll(".tabBtn");
|
||||||
|
const tabContents = document.querySelectorAll(".tabContent");
|
||||||
|
|
||||||
|
tabButtons.forEach(btn => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
tabButtons.forEach(b => b.classList.remove("bg-indigo-600"));
|
||||||
|
tabButtons.forEach(b => b.classList.add("bg-zinc-800"));
|
||||||
|
|
||||||
|
btn.classList.remove("bg-zinc-800");
|
||||||
|
btn.classList.add("bg-indigo-600");
|
||||||
|
|
||||||
|
tabContents.forEach(tab => hide(tab));
|
||||||
|
show(document.getElementById(btn.dataset.tab));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// CAMPAIGNS CRUD
|
||||||
|
// campaigns table must exist
|
||||||
|
// columns: id (uuid), name (text), notes (text), created_at
|
||||||
|
// =============================
|
||||||
|
async function loadCampaigns() {
|
||||||
|
const list = document.getElementById("campaignList");
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
const { data, error } = await supabaseClient
|
||||||
|
.from("campaigns")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
list.innerHTML = `<div class="text-red-400 text-sm">Error loading campaigns: ${error.message}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate filters
|
||||||
|
const sessionsFilter = document.getElementById("sessionsCampaignFilter");
|
||||||
|
const mediaSelect = document.getElementById("mediaCampaignSelect");
|
||||||
|
|
||||||
|
sessionsFilter.innerHTML = `<option value="">All campaigns</option>`;
|
||||||
|
mediaSelect.innerHTML = `<option value="">Select campaign</option>`;
|
||||||
|
|
||||||
|
data.forEach(c => {
|
||||||
|
const opt1 = document.createElement("option");
|
||||||
|
opt1.value = c.id;
|
||||||
|
opt1.innerText = c.name;
|
||||||
|
sessionsFilter.appendChild(opt1);
|
||||||
|
|
||||||
|
const opt2 = document.createElement("option");
|
||||||
|
opt2.value = c.id;
|
||||||
|
opt2.innerText = c.name;
|
||||||
|
mediaSelect.appendChild(opt2);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forEach(c => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl p-4";
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="flex justify-between items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="font-bold text-lg">${c.name}</div>
|
||||||
|
<div class="text-xs text-zinc-400 mt-1">${c.notes || ""}</div>
|
||||||
|
<div class="text-xs text-zinc-500 mt-2">Created: ${formatDate(c.created_at)}</div>
|
||||||
|
<div class="text-xs text-zinc-500 mt-1">ID: ${c.id}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<button class="editBtn bg-zinc-800 hover:bg-zinc-700 text-xs px-3 py-2 rounded-xl"
|
||||||
|
data-id="${c.id}" data-name="${c.name}" data-notes="${c.notes || ""}">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button class="deleteBtn bg-red-600 hover:bg-red-500 text-xs px-3 py-2 rounded-xl"
|
||||||
|
data-id="${c.id}">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".deleteBtn").forEach(btn => {
|
||||||
|
btn.addEventListener("click", async () => {
|
||||||
|
const id = btn.dataset.id;
|
||||||
|
if (!confirm("Delete this campaign?")) return;
|
||||||
|
await deleteCampaign(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".editBtn").forEach(btn => {
|
||||||
|
btn.addEventListener("click", async () => {
|
||||||
|
const id = btn.dataset.id;
|
||||||
|
const oldName = btn.dataset.name;
|
||||||
|
const oldNotes = btn.dataset.notes;
|
||||||
|
|
||||||
|
const name = prompt("Edit campaign name:", oldName);
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
const notes = prompt("Edit notes:", oldNotes);
|
||||||
|
|
||||||
|
await updateCampaign(id, name, notes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCampaign() {
|
||||||
|
const name = document.getElementById("newCampaignName").value.trim();
|
||||||
|
const notes = document.getElementById("newCampaignNotes").value.trim();
|
||||||
|
const msg = document.getElementById("campaignCreateMsg");
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
setMessage(msg, "Campaign name is required.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabaseClient
|
||||||
|
.from("campaigns")
|
||||||
|
.insert([{ name, notes }]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setMessage(msg, "Error: " + error.message, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("newCampaignName").value = "";
|
||||||
|
document.getElementById("newCampaignNotes").value = "";
|
||||||
|
|
||||||
|
setMessage(msg, "Campaign created!");
|
||||||
|
await loadCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCampaign(id) {
|
||||||
|
const { error } = await supabaseClient
|
||||||
|
.from("campaigns")
|
||||||
|
.delete()
|
||||||
|
.eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert("Error deleting campaign: " + error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCampaign(id, name, notes) {
|
||||||
|
const { error } = await supabaseClient
|
||||||
|
.from("campaigns")
|
||||||
|
.update({ name, notes })
|
||||||
|
.eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert("Error updating campaign: " + error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadCampaigns();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// SESSIONS VIEWER
|
||||||
|
// sessions table must exist
|
||||||
|
// columns: id, campaign_id, created_at, ip, user_agent, etc
|
||||||
|
// =============================
|
||||||
|
async function loadSessions() {
|
||||||
|
const list = document.getElementById("sessionList");
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
const campaignFilter = document.getElementById("sessionsCampaignFilter").value;
|
||||||
|
|
||||||
|
let query = supabaseClient
|
||||||
|
.from("sessions")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(100);
|
||||||
|
|
||||||
|
if (campaignFilter) {
|
||||||
|
query = query.eq("campaign_id", campaignFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
list.innerHTML = `<div class="text-red-400 text-sm">Error loading sessions: ${error.message}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
list.innerHTML = `<div class="text-zinc-400 text-sm">No sessions found.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(s => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl p-4";
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">Session ID: ${s.id}</div>
|
||||||
|
<div class="text-xs text-zinc-400 mt-1">Campaign: ${s.campaign_id || "N/A"}</div>
|
||||||
|
<div class="text-xs text-zinc-500 mt-2">Created: ${formatDate(s.created_at)}</div>
|
||||||
|
<div class="text-xs text-zinc-500 mt-1">IP: ${s.ip || "unknown"}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// MEDIA UPLOAD
|
||||||
|
// media table must exist
|
||||||
|
// columns: id, campaign_id, url, category, type, created_at
|
||||||
|
// =============================
|
||||||
|
async function uploadMedia() {
|
||||||
|
const campaignId = document.getElementById("mediaCampaignSelect").value;
|
||||||
|
const category = document.getElementById("mediaCategory").value.trim();
|
||||||
|
const type = document.getElementById("mediaType").value.trim();
|
||||||
|
const fileInput = document.getElementById("mediaFile");
|
||||||
|
const msg = document.getElementById("uploadMsg");
|
||||||
|
|
||||||
|
if (!campaignId) {
|
||||||
|
setMessage(msg, "Select a campaign first.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
setMessage(msg, "Select a file first.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
setMessage(msg, "Uploading...");
|
||||||
|
|
||||||
|
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
|
||||||
|
.storage
|
||||||
|
.from(STORAGE_BUCKET)
|
||||||
|
.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;
|
||||||
|
|
||||||
|
// Insert into media table
|
||||||
|
const { error: insertError } = await supabaseClient
|
||||||
|
.from("media")
|
||||||
|
.insert([{
|
||||||
|
campaign_id: campaignId,
|
||||||
|
url: publicUrl,
|
||||||
|
category,
|
||||||
|
type
|
||||||
|
}]);
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
setMessage(msg, "DB insert error: " + insertError.message, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput.value = "";
|
||||||
|
document.getElementById("mediaCategory").value = "";
|
||||||
|
document.getElementById("mediaType").value = "";
|
||||||
|
|
||||||
|
setMessage(msg, "Upload complete!");
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
list.innerHTML = `<div class="text-red-400 text-sm">Error loading media: ${error.message}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
list.innerHTML = `<div class="text-zinc-400 text-sm">No media uploaded yet.</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(m => {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden";
|
||||||
|
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// INIT
|
||||||
|
// =============================
|
||||||
|
document.getElementById("loginBtn").addEventListener("click", login);
|
||||||
|
document.getElementById("logoutBtn").addEventListener("click", logout);
|
||||||
|
|
||||||
|
document.getElementById("createCampaignBtn").addEventListener("click", createCampaign);
|
||||||
|
|
||||||
|
document.getElementById("refreshSessionsBtn").addEventListener("click", loadSessions);
|
||||||
|
document.getElementById("sessionsCampaignFilter").addEventListener("change", loadSessions);
|
||||||
|
|
||||||
|
document.getElementById("uploadMediaBtn").addEventListener("click", uploadMedia);
|
||||||
|
|
||||||
|
setupTabs();
|
||||||
|
|
||||||
|
// Keep session state updated
|
||||||
|
supabaseClient.auth.onAuthStateChange(() => {
|
||||||
|
refreshUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshUI();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user