diff --git a/wwwroot/app.js b/wwwroot/app.js index 399c33a..cb97f3c 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -1,3 +1,5 @@ +import { api, adminApi } from "./js/api.js"; + const state = { me: null, phase: null, @@ -5,44 +7,22 @@ const state = { mySuggestions: [], allSuggestions: [], myVotes: [], - results: [], - adminKey: "" + 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"), 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(); + setTimeout(() => toastEl.classList.add("hidden"), 2000); } async function loadState() { - const [me, stateData] = await Promise.all([ - api("/api/me"), - api("/api/state") - ]); + const [me, stateData] = await Promise.all([api.me(), api.state()]); state.me = me; state.phase = stateData.currentPhase; state.counts = stateData; @@ -57,26 +37,26 @@ async function loadState() { async function loadSuggestData() { if (state.phase !== "Suggest") return; - state.mySuggestions = await api("/api/suggestions/mine"); + state.mySuggestions = await api.mySuggestions(); renderMySuggestions(); } async function loadRevealData() { if (state.phase === "Reveal" || state.phase === "Vote" || state.phase === "Results") { - state.allSuggestions = await api("/api/suggestions/all"); + state.allSuggestions = await api.allSuggestions(); renderAllSuggestions(); } } async function loadVoteData() { if (state.phase !== "Vote") return; - state.myVotes = await api("/api/votes/mine"); + state.myVotes = await api.myVotes(); renderVotes(); } async function loadResults() { if (state.phase !== "Results") return; - state.results = await api("/api/results"); + state.results = await api.results(); renderResults(); } @@ -119,6 +99,7 @@ function renderAllSuggestions() { 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) => { @@ -141,7 +122,7 @@ function renderVotes() { 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 }) }); + await api.vote(suggestionId, score); toast("Saved vote"); await loadVoteData(); } catch (err) { @@ -211,7 +192,7 @@ function setupHandlers() { 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 }) }); + const me = await api.setName(name); state.me = me; nameInput.dataset.userEditing = ""; toast("Saved name"); @@ -227,7 +208,7 @@ function setupHandlers() { 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) }); + await api.createSuggestion(data); form.reset(); toast("Suggestion added"); await loadSuggestData(); @@ -240,11 +221,7 @@ function setupHandlers() { const phase = $("phase-select").value; const adminKey = $("admin-key").value; try { - await api("/api/admin/phase", { - method: "POST", - body: JSON.stringify({ phase }), - adminKey - }); + await adminApi.setPhase(phase, adminKey); toast("Phase updated"); state.phase = phase; $("phase-select").dataset.userEditing = ""; @@ -260,8 +237,8 @@ function setupHandlers() { }); 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")); + $("reset").addEventListener("click", () => adminAction(adminApi.reset, "Reset complete")); + $("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, "Factory reset complete")); const adminToggle = $("admin-toggle"); const adminCard = $("admin-card"); @@ -273,10 +250,10 @@ function setupHandlers() { } } -async function adminAction(path, successMessage) { +async function adminAction(fn, successMessage) { const adminKey = $("admin-key").value; try { - await api(path, { method: "POST", adminKey }); + await fn(adminKey); toast(successMessage); await refreshPhaseData(); } catch (err) { @@ -301,8 +278,8 @@ function buildCard(s, { showAuthor = false, allowDelete = false }) {

${s.name}

- ${s.youtubeUrl ? `YouTube ↗` : ""}
+ ${s.youtubeUrl ? `YouTube ↗` : ""} ${showAuthor && s.author ? `${s.author}` : ""} ${allowDelete ? `` : ""}
@@ -315,18 +292,18 @@ function buildCard(s, { showAuthor = false, allowDelete = false }) { 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(`/api/suggestions/${s.id}`, { method: "DELETE" }); - toast("Suggestion deleted"); - await loadSuggestData(); - } catch (err) { - toast(err.message, true); - } - }); - } + 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; } diff --git a/wwwroot/index.html b/wwwroot/index.html index 78fd38a..6d54154 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -88,6 +88,6 @@ - + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js new file mode 100644 index 0000000..018e7e6 --- /dev/null +++ b/wwwroot/js/api.js @@ -0,0 +1,44 @@ +const defaultHeaders = { "Content-Type": "application/json" }; + +async function request(path, { method = "GET", body, adminKey } = {}) { + const res = await fetch(path, { + method, + headers: { + ...defaultHeaders, + ...(adminKey ? { "X-Admin-Key": adminKey } : {}) + }, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!res.ok) { + let msg = `${res.status}`; + try { + const data = await res.json(); + msg = data.error || JSON.stringify(data); + } catch { /* ignore */ } + throw new Error(msg); + } + return res.status === 204 ? null : res.json(); +} + +export const api = { + state: () => request("/api/state"), + me: () => request("/api/me"), + setName: (name) => request("/api/me/name", { method: "POST", body: { name } }), + + mySuggestions: () => request("/api/suggestions/mine"), + createSuggestion: (payload) => request("/api/suggestions", { method: "POST", body: payload }), + deleteSuggestion: (id) => request(`/api/suggestions/${id}`, { method: "DELETE" }), + allSuggestions: () => request("/api/suggestions/all"), + + myVotes: () => request("/api/votes/mine"), + vote: (suggestionId, score) => request("/api/votes", { method: "POST", body: { suggestionId, score } }), + + results: () => request("/api/results"), +}; + +export const adminApi = { + setPhase: (phase, adminKey) => request("/api/admin/phase", { method: "POST", body: { phase }, adminKey }), + reset: (adminKey) => request("/api/admin/reset", { method: "POST", adminKey }), + factoryReset: (adminKey) => request("/api/admin/factory-reset", { method: "POST", adminKey }), +};