import { api, adminApi } from "./js/api.js"; import { t, setLanguage, getLanguage, initI18n, onLanguageChange } 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, } 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 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.length > 24) return toast("Username must be 24 characters or fewer.", true); if (!username || !password) return toast(t("auth.needCredentials"), true); try { await api.login({ username, password }); 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.length > 24) return toast("Username must be 24 characters or fewer.", true); if (displayName.length > 16) return toast("Display name must be 16 characters or fewer.", true); if (!username || !password) return toast(t("auth.needCredentials"), true); try { await api.register({ username, password, displayName, adminKey }); 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 (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)); } const resultsToggle = $("results-open"); if (resultsToggle) { resultsToggle.addEventListener("change", async (e) => { const desired = !!e.target.checked; try { const resp = await adminApi.setResultsOpen(desired); state.resultsOpen = resp.resultsOpen; renderPhasePill(); toast(t("admin.resultsUpdated")); } 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) { finalizeBtn.addEventListener("click", async () => { try { const desired = !state.votesFinal; await api.finalizeVotes(desired); state.votesFinal = desired; renderPhasePill(); renderVotes(); toast(desired ? t("vote.finalize") : t("vote.unfinalize")); } catch (err) { toast(err.message, true); } }); } }