Files
gallery/dashboard.html
T
2026-05-16 00:28:32 -06:00

606 lines
19 KiB
HTML

<!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>