import { t, setLanguage, getLanguage, initI18n, onLanguageChange, faqMarkdown } from "./js/i18n.js"; import { state, clearUserState } from "./js/state.js"; import { toast } from "./js/dom.js"; import { handleAuthError, renderWelcome, renderPhasePill, renderCounts, renderMySuggestions, renderAllSuggestions, renderVotes, syncVoteScores, renderResults, renderPhaseTitles, updatePhaseNav, configureUiRuntime, } from "./js/ui.js"; import { loadSuggestData, loadVoteData, refreshPhaseData, } from "./js/data.js"; import { setupAuthHandlers } from "./js/app-auth-handlers.js"; import { setupAdminHandlers } from "./js/app-admin-handlers.js"; import { setupVoteNavigationHandlers } from "./js/app-vote-nav-handlers.js"; const REFRESH_INTERVAL_MS = 4000; let refreshInFlight = null; let refreshTimerId = null; let refreshSchedulerStarted = false; async function runSerializedRefresh() { if (refreshInFlight) return refreshInFlight; refreshInFlight = refreshPhaseData().finally(() => { refreshInFlight = null; }); return refreshInFlight; } async function refreshWithUiErrorHandling() { try { await runSerializedRefresh(); } catch (err) { if (!handleAuthError(err, clearUserState)) toast(err.message, true); } } function scheduleNextRefresh() { refreshTimerId = window.setTimeout(async () => { if (!document.hidden && !state.adminStatusMenuOpen) { await refreshWithUiErrorHandling(); } scheduleNextRefresh(); }, REFRESH_INTERVAL_MS); } function startRefreshScheduler() { if (refreshSchedulerStarted) return; refreshSchedulerStarted = true; document.addEventListener("visibilitychange", () => { if (!document.hidden && !state.adminStatusMenuOpen) { refreshWithUiErrorHandling(); } }); if (refreshTimerId !== null) { window.clearTimeout(refreshTimerId); } scheduleNextRefresh(); } configureUiRuntime({ refreshPhaseData: runSerializedRefresh, loadSuggestData, loadVoteData, handleAuthError: (err) => handleAuthError(err, clearUserState), }); function setupHandlers() { setupAuthHandlers({ runSerializedRefresh }); setupAdminHandlers({ runSerializedRefresh }); setupVoteNavigationHandlers({ runSerializedRefresh }); setupLanguageSwitchers(); onLanguageChange(() => { updateLanguageButtons(); renderWelcome(); renderPhasePill(); renderCounts(); renderPhaseTitles(); renderMySuggestions(); renderAllSuggestions(); if (state.phase === "Vote") { renderVotes(); state.votesRendered = true; syncVoteScores(); } if (state.phase === "Results") { renderResults(); } updatePhaseNav(); }); document.querySelectorAll(".help-chip").forEach((chip) => { chip.addEventListener("click", () => openFaqModal()); }); } async function main() { await initI18n(); setupHandlers(); await refreshWithUiErrorHandling(); startRefreshScheduler(); } 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 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("