const state = { me: null, phase: null, counts: null, mySuggestions: [], allSuggestions: [], myVotes: [], results: [], adminKey: "" }; const $ = (id) => document.getElementById(id); const toastEl = $("toast"); function toast(msg, isError = false) { toastEl.textContent = msg; toastEl.classList.remove("hidden"); toastEl.classList.toggle("error", isError); setTimeout(() => toastEl.classList.add("hidden"), 2400); } async function api(path, options = {}) { const res = await fetch(path, { headers: { "Content-Type": "application/json", ...(options.adminKey ? { "X-Admin-Key": options.adminKey } : {}) }, ...options }); if (!res.ok) { let msg = `${res.status}`; try { const body = await res.json(); msg = body.error || JSON.stringify(body); } catch (_) { /* ignore */ } throw new Error(msg); } return res.status === 204 ? null : res.json(); } async function loadState() { const [me, stateData] = await Promise.all([ api("/api/me"), api("/api/state") ]); state.me = me; state.phase = stateData.currentPhase; state.counts = stateData; renderPhasePill(); renderCounts(); const nameInput = $("name-input"); if (!nameInput.dataset.userEditing) { nameInput.value = me.displayName || ""; } $("player-id").textContent = `Player ID: ${me.id}`; } async function loadSuggestData() { if (state.phase !== "Suggest") return; state.mySuggestions = await api("/api/suggestions/mine"); renderMySuggestions(); } async function loadRevealData() { if (state.phase === "Reveal" || state.phase === "Vote" || state.phase === "Results") { state.allSuggestions = await api("/api/suggestions/all"); renderAllSuggestions(); } } async function loadVoteData() { if (state.phase !== "Vote") return; state.myVotes = await api("/api/votes/mine"); renderVotes(); } async function loadResults() { if (state.phase !== "Results") return; state.results = await api("/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.dataset.userEditing) { phaseSelect.value = state.phase || "Suggest"; } } function renderCounts() { if (!state.counts) return; $("phase-description").textContent = `Phase: ${state.phase}`; $("counts").textContent = `Players: ${state.counts.players} • Suggestions: ${state.counts.suggestions} • Votes: ${state.counts.votes}`; } function renderMySuggestions() { const list = $("my-suggestions"); list.innerHTML = ""; state.mySuggestions.forEach((s) => { const li = document.createElement("li"); li.innerHTML = `${s.name}${s.genre ? ` · ${s.genre}` : ""}
${s.description || ""}`; list.appendChild(li); }); } function renderAllSuggestions() { const list = $("all-suggestions"); list.innerHTML = ""; state.allSuggestions.forEach((s) => { const li = document.createElement("li"); li.innerHTML = `${s.name} by ${s.author || "Anonymous"}${s.genre ? ` · ${s.genre}` : ""}
${s.description || ""}`; list.appendChild(li); }); } function renderVotes() { const list = $("vote-list"); list.innerHTML = ""; const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score])); state.allSuggestions.forEach((s) => { const li = document.createElement("li"); const current = votesMap[s.id] ?? 0; li.innerHTML = `
${s.name} by ${s.author || "Anonymous"}
${current}
`; 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("/api/votes", { method: "POST", body: JSON.stringify({ suggestionId, score }) }); toast("Saved vote"); await loadVoteData(); } catch (err) { toast(err.message, true); } }); }); } function renderResults() { const list = $("results-list"); list.innerHTML = ""; state.results.forEach((r) => { const li = document.createElement("li"); li.innerHTML = `${r.name} — ${r.total} pts (${r.count} votes, avg ${r.average.toFixed(1)})${r.author ? ` · ${r.author}` : ""}`; list.appendChild(li); }); } function setupHandlers() { const nameInput = $("name-input"); ["focus", "input"].forEach(evt => { nameInput.addEventListener(evt, () => { nameInput.dataset.userEditing = "1"; }); }); nameInput.addEventListener("blur", () => { nameInput.dataset.userEditing = ""; }); $("save-name").addEventListener("click", async () => { const name = nameInput.value.trim(); if (!name) return toast("Name required", true); try { const me = await api("/api/me/name", { method: "POST", body: JSON.stringify({ name }) }); state.me = me; nameInput.dataset.userEditing = ""; toast("Saved name"); } catch (err) { 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("/api/suggestions", { method: "POST", body: JSON.stringify(data) }); form.reset(); toast("Suggestion added"); await loadSuggestData(); } catch (err) { toast(err.message, true); } }); $("set-phase").addEventListener("click", async () => { const phase = $("phase-select").value; const adminKey = $("admin-key").value; try { await api("/api/admin/phase", { method: "POST", body: JSON.stringify({ phase }), adminKey }); 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("/api/admin/reset", "Reset complete")); $("factory-reset").addEventListener("click", () => adminAction("/api/admin/factory-reset", "Factory reset complete")); } async function adminAction(path, successMessage) { const adminKey = $("admin-key").value; try { await api(path, { method: "POST", adminKey }); toast(successMessage); await refreshPhaseData(); } catch (err) { toast(err.message, true); } } async function refreshPhaseData() { await loadState(); await Promise.all([loadSuggestData(), loadRevealData(), loadVoteData(), loadResults()]); } async function main() { setupHandlers(); try { await refreshPhaseData(); } catch (err) { toast(err.message, true); } setInterval(refreshPhaseData, 4000); } main();