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 { triggerCelebration } from "./js/effects.js"; import { setAuthUI, setAuthMode, handleAuthError, renderWelcome, renderPhasePill, renderCounts, renderMySuggestions, renderAllSuggestions, renderVotes, syncVoteScores, renderResults, renderPhaseTitles, normalizeSuggestionForm, } 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; }); } const langSelects = Array.from(document.querySelectorAll(".lang-select")); const syncLanguageSelects = () => langSelects.forEach((sel) => (sel.value = getLanguage())); syncLanguageSelects(); langSelects.forEach((sel) => sel.addEventListener("change", () => setLanguage(sel.value))); onLanguageChange(() => { syncLanguageSelects(); renderWelcome(); renderPhasePill(); renderCounts(); renderPhaseTitles(); renderMySuggestions(); renderAllSuggestions(); if (state.phase === "Vote") { renderVotes(); state.votesRendered = true; syncVoteScores(); } if (state.phase === "Results") { renderResults(); } }); 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); } }); } $("suggest-form").addEventListener("submit", async (e) => { e.preventDefault(); const form = e.target; const data = normalizeSuggestionForm(new FormData(form)); if (!data.name) return toast(t("toast.nameRequired"), true); if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) { return toast(t("toast.invalidImageUrl"), true); } try { await api.createSuggestion(data); form.reset(); toast(t("toast.suggestionAdded")); triggerCelebration(form.querySelector("button[type=submit]")); await loadSuggestData(); } catch (err) { toast(err.message, true); } }); $("set-phase").addEventListener("click", async () => { const phase = $("phase-select").value; try { await adminApi.setPhase(phase); toast(t("admin.phaseUpdated")); state.prevPhase = state.phase; state.phase = phase; state.votesRendered = false; renderPhasePill(); $("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(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)); } } 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 isValidImageUrl(url) { if (!url) return true; try { const u = new URL(url); const allowed = ["http:", "https:"]; if (!allowed.includes(u.protocol)) return false; const path = u.pathname.toLowerCase(); return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) => path.endsWith(ext)); } catch { return false; } }