import { api, adminApi } from "./js/api.js"; import { t, setLanguage, getLanguage, initI18n, onLanguageChange, faqMarkdown } from "./js/i18n.js"; import { state, clearUserState, getSavedUsername, setSavedUsername } from "./js/state.js"; import { $, toast } from "./js/dom.js"; import { setAuthUI, setAuthMode, handleAuthError, renderWelcome, renderPhasePill, renderCounts, renderMySuggestions, renderAllSuggestions, renderVotes, syncVoteScores, renderResults, renderPhaseTitles, openNewSuggestionModal, updatePhaseNav, openConfirmModal, openResultsRelockModal, } from "./js/ui.js"; import { loadState, loadSuggestData, loadRevealData, loadVoteData, loadResults, refreshPhaseData, } from "./js/data.js"; initI18n(); function setupHandlers() { const toggleAuth = $("auth-toggle"); if (toggleAuth) { toggleAuth.addEventListener("click", (e) => { e.preventDefault(); setAuthMode(state.authMode === "login" ? "register" : "login"); }); } setAuthMode(state.authMode); const hasConsent = () => document.cookie.split(";").some((c) => c.trim().startsWith("cookie_consent=1")); const setConsent = () => { document.cookie = "cookie_consent=1; path=/; max-age=31536000; SameSite=Lax"; }; const consentRows = document.querySelectorAll(".consent-row"); const toggleConsentRows = () => { const hide = hasConsent(); consentRows.forEach((row) => row.classList.toggle("hidden", hide)); }; toggleConsentRows(); ["login-consent", "register-consent"].forEach((id) => { const box = $(id); if (box) { box.checked = hasConsent(); } }); const loginUser = $("login-username"); if (loginUser) { const markEditing = () => { loginUser.dataset.userEditing = "1"; }; ["focus", "input", "keydown"].forEach((evt) => loginUser.addEventListener(evt, markEditing)); loginUser.addEventListener("blur", () => { delete loginUser.dataset.userEditing; }); } setupLanguageSwitchers(); onLanguageChange(() => { updateLanguageButtons(); renderWelcome(); renderPhasePill(); renderCounts(); renderPhaseTitles(); renderMySuggestions(); renderAllSuggestions(); if (state.phase === "Vote") { renderVotes(); state.votesRendered = true; syncVoteScores(); } if (state.phase === "Results") { renderResults(); } updatePhaseNav(); }); 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(t("auth.needCredentials"), true); if (!hasConsent() && !$("login-consent")?.checked) return toast(t("auth.cookieRequired"), true); try { await api.login({ username, password }); setConsent(); toggleConsentRows(); setSavedUsername(username); state.isAuthenticated = true; setAuthUI(true); await refreshPhaseData(); toast(t("toast.loggedIn")); } catch (err) { if (err?.status === 401) return toast(t("auth.invalidCredentials"), true); if (handleAuthError(err, clearUserState)) 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 (!displayName) return toast(t("toast.displayNameRequired") || "Display name is required.", true); if (!username || !password) return toast(t("auth.needCredentials"), true); if (!hasConsent() && !$("register-consent")?.checked) return toast(t("auth.cookieRequired"), true); try { await api.register({ username, password, displayName, adminKey }); setConsent(); toggleConsentRows(); setSavedUsername(username); state.isAuthenticated = true; setAuthUI(true); await refreshPhaseData(); toast(t("toast.registered")); } catch (err) { if (handleAuthError(err, clearUserState)) return; toast(err.message, true); } }); } const openSuggestBtn = $("open-suggest-modal"); if (openSuggestBtn) { openSuggestBtn.addEventListener("click", (e) => { e.preventDefault(); if (openSuggestBtn.disabled) return; if (state.phase !== "Suggest") return; openNewSuggestionModal(); }); } const openJokerBtn = $("open-joker-modal"); if (openJokerBtn) { openJokerBtn.addEventListener("click", (e) => { e.preventDefault(); if (state.phase !== "Vote" || !state.hasJoker) return; openNewSuggestionModal(); }); } bindNavButtons(); $("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone"))); $("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone"))); const logoutBtn = $("logout"); if (logoutBtn) { logoutBtn.addEventListener("click", async (e) => { e.preventDefault(); const lastUser = state.me?.username; try { await api.logout(); } catch (err) { toast(err.message, true); } clearUserState(); state.isAuthenticated = false; setAuthUI(false); if (lastUser) { setSavedUsername(lastUser); const loginUser = $("login-username"); if (loginUser) loginUser.value = lastUser; const loginPass = $("login-password"); if (loginPass) loginPass.value = ""; } }); } 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)); } document.querySelectorAll(".help-chip").forEach((chip) => { chip.addEventListener("click", () => openFaqModal()); }); const resultsToggle = $("results-open"); if (resultsToggle) { resultsToggle.addEventListener("change", async (e) => { const desired = !!e.target.checked; try { const resp = await adminApi.setResultsOpen(desired); const wasResultsOpen = state.resultsOpen; const wasPhase = state.phase; state.resultsOpen = resp.resultsOpen; if (wasResultsOpen && !resp.resultsOpen && wasPhase === "Results") { openResultsRelockModal(); } renderPhasePill(); toast(t("admin.resultsUpdated")); await refreshPhaseData(); } catch (err) { e.target.checked = !desired; toast(err.message, true); } }); } const linkApply = $("link-apply"); if (linkApply) { linkApply.addEventListener("click", async () => { const source = Number($("link-source")?.value); const target = Number($("link-target")?.value); if (!source || !target || source === target) { return toast(t("admin.linkValidation"), true); } try { await adminApi.linkSuggestions(source, target); toast(t("admin.linkDone")); await refreshPhaseData(); } catch (err) { toast(err.message, true); } }); } const playerTable = $("admin-player-table"); if (playerTable) { playerTable.addEventListener("click", async (e) => { const grantBtn = e.target.closest("[data-grant-joker]"); const deleteBtn = e.target.closest("[data-delete-player]"); if (grantBtn) { const playerId = grantBtn.dataset.grantJoker; try { await adminApi.grantJoker(playerId); toast(t("admin.jokerGranted")); await refreshPhaseData(); } catch (err) { toast(err.message, true); } } else if (deleteBtn) { const playerId = deleteBtn.dataset.deletePlayer; const name = deleteBtn.dataset.name || ""; openConfirmModal({ title: t("admin.deleteTitle"), body: t("admin.deleteBody", { name }), confirmLabel: t("admin.deleteConfirm"), onConfirm: async (close) => { try { await adminApi.deletePlayer(playerId); toast(t("admin.deleteDone")); close(); await refreshPhaseData(); } catch (err) { toast(err.message, true); } }, }); } }); } } async function adminAction(fn, successMessage) { try { await fn(); toast(successMessage); await refreshPhaseData(); } catch (err) { toast(err.message, true); } } async function main() { setupHandlers(); try { await refreshPhaseData(); } catch (err) { toast(err.message, true); } setInterval(() => { refreshPhaseData().catch((err) => { if (!handleAuthError(err, clearUserState)) toast(err.message, true); }); }, 4000); } main(); function updateLanguageButtons() { document.querySelectorAll(".lang-button").forEach((btn) => { btn.textContent = "🌐"; btn.title = t("lang.label"); btn.setAttribute("aria-label", t("lang.label")); }); } function setupLanguageSwitchers() { const switches = document.querySelectorAll(".lang-switch"); const closeAll = () => switches.forEach((wrap) => wrap.querySelector(".lang-menu")?.classList.add("hidden")); switches.forEach((wrap) => { const btn = wrap.querySelector(".lang-button"); const menu = wrap.querySelector(".lang-menu"); if (!btn || !menu) return; btn.addEventListener("click", (e) => { e.preventDefault(); const isHidden = menu.classList.contains("hidden"); closeAll(); if (isHidden) menu.classList.remove("hidden"); }); menu.querySelectorAll("[data-lang]").forEach((item) => item.addEventListener("click", () => { const lang = item.dataset.lang; if (lang) setLanguage(lang); closeAll(); }), ); }); document.addEventListener("click", (e) => { if (!e.target.closest(".lang-switch")) closeAll(); }); updateLanguageButtons(); } function bindNavButtons() { const makeForward = (id, before) => { const btn = $(id); if (!btn) return; btn.addEventListener("click", async () => { try { if (before) { const proceed = await before(); if (!proceed) return; } const resp = await api.nextPhase(); state.prevPhase = state.phase; state.phase = resp.currentPhase; state.resultsOpen = resp.resultsOpen ?? state.resultsOpen; state.votesRendered = false; renderPhasePill(); await refreshPhaseData(); } catch (err) { toast(err.message, true); } }); }; const makeBack = (id) => { const btn = $(id); if (!btn) return; btn.addEventListener("click", async () => { try { const resp = await api.prevPhase(); state.prevPhase = state.phase; state.phase = resp.currentPhase; state.resultsOpen = resp.resultsOpen ?? state.resultsOpen; state.votesRendered = false; renderPhasePill(); await refreshPhaseData(); } catch (err) { toast(err.message, true); } }); }; makeForward("nav-suggest-next", async () => { return await new Promise((resolve) => { openConfirmModal({ title: t("nav.freezeModalTitle"), body: t("nav.freezeModalBody"), confirmLabel: t("nav.next"), onConfirm: (close) => { close(); resolve(true); }, }); }); }); makeBack("nav-vote-prev"); const finalizeBtn = $("finalize-votes"); if (finalizeBtn) { const finalizeVotes = async (desired) => { await api.finalizeVotes(desired); state.votesFinal = desired; renderPhasePill(); renderVotes(); toast(desired ? t("vote.finalize") : t("vote.unfinalize")); }; const missingVotes = () => { const votedIds = new Set((state.myVotes ?? []).map((v) => v.suggestionId)); return (state.allSuggestions ?? []).filter((s) => !votedIds.has(s.id)); }; finalizeBtn.addEventListener("click", async () => { try { const desired = !state.votesFinal; if (desired) { const missing = missingVotes(); if (missing.length > 0) { openConfirmModal({ title: t("vote.finalizeMissingTitle"), body: t("vote.finalizeMissingBody", { count: missing.length }), confirmLabel: t("vote.finalizeMissingConfirm"), onConfirm: async (close) => { await finalizeVotes(desired); close(); }, }); return; } } await finalizeVotes(desired); } catch (err) { toast(err.message, true); } }); } } function markdownToHtml(md) { const lines = md.trim().split(/\r?\n/); const html = []; let inList = false; let inParagraph = false; const escapeHtml = (text) => text .replace(/&/g, "&") .replace(//g, ">"); const formatInline = (text) => escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/`([^`]+)`/g, "$1"); const closeParagraph = () => { if (inParagraph) { html.push("

"); inParagraph = false; } }; const closeList = () => { if (inList) { html.push(""); inList = false; } }; lines.forEach((rawLine) => { const line = rawLine.trimEnd(); const trimmed = line.trim(); if (!trimmed) { closeParagraph(); closeList(); return; } if (/^-{5,}$/.test(trimmed)) { closeParagraph(); closeList(); html.push('
'); return; } const heading = trimmed.match(/^(#{1,3})\s+(.*)$/); if (heading) { closeParagraph(); closeList(); const level = heading[1].length; const tag = level === 1 ? "h2" : level === 2 ? "h3" : "h4"; html.push(`<${tag}>${formatInline(heading[2].trim())}`); return; } if (/^[*-]\s+/.test(trimmed)) { closeParagraph(); if (!inList) { html.push("