import { api, adminApi } from "./js/api.js"; const state = { isAuthenticated: false, authMode: "login", me: null, phase: null, counts: null, mySuggestions: [], allSuggestions: [], myVotes: [], results: [] }; const $ = (id) => document.getElementById(id); const toastEl = $("toast"); function toast(msg, isError = false) { if (!toastEl) return; toastEl.textContent = msg; toastEl.classList.remove("hidden"); toastEl.classList.toggle("error", isError); setTimeout(() => toastEl.classList.add("hidden"), 2000); } function setAuthUI(isAuthed) { const main = document.querySelector("main"); const statusBar = document.querySelector(".status-bar"); const authCard = $("auth-card"); [main, statusBar].forEach(el => el?.classList.toggle("hidden", !isAuthed)); if (authCard) authCard.classList.toggle("hidden", isAuthed); const adminToggle = $("admin-toggle"); if (adminToggle) adminToggle.classList.toggle("hidden", !isAuthed || !state.me?.isAdmin); } function setAuthMode(mode) { state.authMode = mode; document.querySelectorAll(".auth-form").forEach(form => { form.classList.toggle("hidden", form.dataset.mode !== mode); }); document.querySelectorAll("[data-auth-tab]").forEach(btn => { btn.classList.toggle("active", btn.dataset.authTab === mode); }); } function clearUserState() { state.me = null; state.phase = null; state.counts = null; state.mySuggestions = []; state.allSuggestions = []; state.myVotes = []; state.results = []; } function handleAuthError(err) { if (err?.status === 401) { clearUserState(); state.isAuthenticated = false; setAuthUI(false); return true; } toast(err?.message || "Unexpected error", true); return false; } async function loadState() { const [me, stateData] = await Promise.all([api.me(), api.state()]); state.isAuthenticated = true; state.me = me; state.phase = stateData.currentPhase; state.counts = stateData; setAuthUI(true); renderWelcome(); renderPhasePill(); renderCounts(); } async function loadSuggestData() { if (state.phase !== "Suggest") return; state.mySuggestions = await api.mySuggestions(); renderMySuggestions(); } async function loadRevealData() { if (state.phase === "Reveal" || state.phase === "Vote" || state.phase === "Results") { state.allSuggestions = await api.allSuggestions(); renderAllSuggestions(); } } async function loadVoteData() { if (state.phase !== "Vote") return; state.myVotes = await api.myVotes(); renderVotes(); } async function loadResults() { if (state.phase !== "Results") return; state.results = await api.results(); renderResults(); } function renderPhasePill() { $("phase-pill").textContent = state.phase || "Loading…"; document.querySelectorAll(".phase-view").forEach((el) => el.classList.add("hidden")); const viewMap = { Suggest: "suggest-view", Reveal: "reveal-view", Vote: "vote-view", Results: "results-view" }; const id = viewMap[state.phase]; if (id) $(id).classList.remove("hidden"); const phaseSelect = $("phase-select"); if (phaseSelect && !phaseSelect.dataset.userEditing) { phaseSelect.value = state.phase || "Suggest"; } } function renderCounts() { if (!state.counts) return; $("counts").textContent = `Players: ${state.counts.players} • Suggestions: ${state.counts.suggestions} • Votes: ${state.counts.votes}`; } function renderWelcome() { const el = $("welcome-text"); if (!el) return; const name = state.me?.displayName?.trim() || state.me?.username || "Player"; el.textContent = `Welcome, ${name}!`; } function renderMySuggestions() { const wrap = $("my-suggestions"); if (!wrap) return; wrap.innerHTML = ""; state.mySuggestions.forEach((s) => wrap.appendChild(buildCard(s, { showAuthor: false, allowDelete: true }))); } function renderAllSuggestions() { const list = $("all-suggestions"); if (!list) return; list.innerHTML = ""; state.allSuggestions.forEach((s) => list.appendChild(buildCard(s, { showAuthor: true }))); } function renderVotes() { const list = $("vote-list"); if (!list) return; list.innerHTML = ""; const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score])); state.allSuggestions.forEach((s) => { const li = buildCard(s, { showAuthor: true }); const current = votesMap[s.id] ?? 0; const footer = document.createElement("div"); footer.className = "vote-controls"; footer.innerHTML = ` ${current}`; li.querySelector(".card-body").appendChild(footer); list.appendChild(li); }); list.querySelectorAll("input[type=range]").forEach((input) => { input.addEventListener("input", (e) => { const val = Number(e.target.value); $("score-" + e.target.dataset.id).textContent = val; }); input.addEventListener("change", async (e) => { const suggestionId = Number(e.target.dataset.id); const score = Number(e.target.value); try { await api.vote(suggestionId, score); toast("Saved vote"); await loadVoteData(); } catch (err) { toast(err.message, true); } }); }); } function renderResults() { const container = $("results-list"); container.innerHTML = ""; const table = document.createElement("table"); table.className = "results-table"; table.innerHTML = ` Rank Game Author Votes Avg Total Links `; const tbody = table.querySelector("tbody"); state.results.forEach((r, idx) => { const row = document.createElement("tr"); row.innerHTML = ` ${idx + 1} ${r.screenshotUrl ? `${r.name}` : ''}
${r.name}
${r.genre ? `
${r.genre}
` : ''}
${r.author ?? "—"} ${r.count} ${r.average.toFixed(1)} ${r.total} ${r.gameUrl ? `Site ↗
` : ''} ${r.youtubeUrl ? `YouTube ↗` : ''} `; tbody.appendChild(row); }); container.appendChild(table); container.querySelectorAll(".clickable-thumb").forEach(img => { img.addEventListener("click", () => openLightbox(img.src, img.alt)); }); } function setupHandlers() { document.querySelectorAll("[data-auth-tab]").forEach(btn => { btn.addEventListener("click", () => setAuthMode(btn.dataset.authTab)); }); setAuthMode(state.authMode); const loginForm = $("login-form"); if (loginForm) { loginForm.addEventListener("submit", async (e) => { e.preventDefault(); const username = $("login-username").value.trim(); const password = $("login-password").value; if (!username || !password) return toast("Username and password required", true); try { await api.login({ username, password }); state.isAuthenticated = true; setAuthUI(true); await refreshPhaseData(); toast("Logged in"); } catch (err) { if (err?.status === 401) return toast("Invalid username or password", true); if (handleAuthError(err)) return; } }); } const registerForm = $("register-form"); if (registerForm) { registerForm.addEventListener("submit", async (e) => { e.preventDefault(); const username = $("register-username").value.trim(); const password = $("register-password").value; const displayName = $("register-displayName").value.trim(); const adminKey = $("register-adminkey").value.trim(); if (!username || !password) return toast("Username and password required", true); try { await api.register({ username, password, displayName, adminKey }); state.isAuthenticated = true; setAuthUI(true); await refreshPhaseData(); toast("Registered"); } catch (err) { if (handleAuthError(err)) return; toast(err.message, true); } }); } $("suggest-form").addEventListener("submit", async (e) => { e.preventDefault(); const form = e.target; const data = Object.fromEntries(new FormData(form).entries()); if (!data.name) return toast("Name required", true); try { await api.createSuggestion(data); form.reset(); toast("Suggestion added"); await loadSuggestData(); } catch (err) { toast(err.message, true); } }); $("set-phase").addEventListener("click", async () => { const phase = $("phase-select").value; try { await adminApi.setPhase(phase); toast("Phase updated"); state.phase = phase; $("phase-select").dataset.userEditing = ""; await refreshPhaseData(); } catch (err) { toast(err.message, true); } }); const phaseSelect = $("phase-select"); ["focus", "input", "click"].forEach(evt => { phaseSelect.addEventListener(evt, () => { phaseSelect.dataset.userEditing = "1"; }); }); phaseSelect.addEventListener("blur", () => { phaseSelect.dataset.userEditing = ""; }); $("reset").addEventListener("click", () => adminAction(adminApi.reset, "Reset complete")); $("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, "Factory reset complete")); const logoutBtn = $("logout"); if (logoutBtn) { logoutBtn.addEventListener("click", async (e) => { e.preventDefault(); try { await api.logout(); } catch (err) { toast(err.message, true); } clearUserState(); state.isAuthenticated = false; setAuthUI(false); }); } const adminToggle = $("admin-toggle"); const adminCard = $("admin-card"); const adminClose = $("admin-close"); if (adminToggle && adminCard && adminClose) { const togglePanel = (show) => adminCard.classList.toggle("hidden", !show); adminToggle.addEventListener("click", () => togglePanel(adminCard.classList.contains("hidden"))); adminClose.addEventListener("click", () => togglePanel(false)); } } async function adminAction(fn, successMessage) { try { await fn(); toast(successMessage); await refreshPhaseData(); } catch (err) { toast(err.message, true); } } async function refreshPhaseData() { try { await loadState(); await Promise.all([loadSuggestData(), loadRevealData(), loadVoteData(), loadResults()]); } catch (err) { if (handleAuthError(err)) return; throw err; } } function buildCard(s, { showAuthor = false, allowDelete = false }) { const card = document.createElement("article"); card.className = "game-card"; const hasImage = !!s.screenshotUrl; const visual = hasImage ? `` : `
`; card.innerHTML = ` ${visual}

${s.name}

${s.gameUrl ? `Site ↗` : ""} ${s.youtubeUrl ? `YouTube ↗` : ""} ${showAuthor && s.author ? `${s.author}` : ""} ${allowDelete ? `` : ""}
${s.genre ? `

${s.genre}

` : ""} ${s.description ? `

${s.description}

` : ""}
`; if (hasImage) { const btn = card.querySelector(".card-visual"); btn.addEventListener("click", () => openLightbox(s.screenshotUrl, s.name)); } if (allowDelete) { const del = card.querySelector("[data-delete]"); del.addEventListener("click", async () => { try { await api.deleteSuggestion(s.id); toast("Suggestion deleted"); await loadSuggestData(); } catch (err) { toast(err.message, true); } }); } return card; } function openLightbox(url, title) { const overlay = document.createElement("div"); overlay.className = "lightbox"; overlay.innerHTML = ` `; overlay.addEventListener("click", (e) => { if (e.target.classList.contains("lightbox") || e.target.classList.contains("lightbox-close")) { overlay.remove(); } }); document.body.appendChild(overlay); } async function main() { setupHandlers(); try { await refreshPhaseData(); } catch (err) { toast(err.message, true); } setInterval(() => { refreshPhaseData().catch(err => { if (!handleAuthError(err)) toast(err.message, true); }); }, 4000); } main();