Modularize frontend: API helper module and cleaner app.js
This commit is contained in:
@@ -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 }) {
|
||||
<div class="card-body">
|
||||
<div class="card-title-row">
|
||||
<h3>${s.name}</h3>
|
||||
${s.youtubeUrl ? `<a class="link compact" href="${s.youtubeUrl}" target="_blank" rel="noopener">YouTube ↗</a>` : ""}
|
||||
<div class="title-meta">
|
||||
${s.youtubeUrl ? `<a class="link compact" href="${s.youtubeUrl}" target="_blank" rel="noopener">YouTube ↗</a>` : ""}
|
||||
${showAuthor && s.author ? `<span class="chip">${s.author}</span>` : ""}
|
||||
${allowDelete ? `<button class="chip danger-chip" data-delete="${s.id}" type="button">Delete</button>` : ""}
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,6 @@
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
44
wwwroot/js/api.js
Normal file
44
wwwroot/js/api.js
Normal file
@@ -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 }),
|
||||
};
|
||||
Reference in New Issue
Block a user