import { api } from "./api.js"; import { t } from "./i18n.js"; import { state, getSavedUsername, setSavedUsername } from "./state.js"; import { $, toast } from "./dom.js"; import { setupCardVisualHover, triggerCelebration } from "./effects.js"; import { adminApi } from "./api.js"; export function setAuthUI(isAuthed) { const main = document.querySelector("main"); const statusBar = document.querySelector(".status-bar"); const authCard = $("auth-card"); [main, statusBar].forEach((el) => el?.classList.toggle("hidden", !isAuthed), ); if (authCard) authCard.classList.toggle("hidden", isAuthed); const adminToggle = $("admin-toggle"); if (adminToggle) adminToggle.classList.toggle("hidden", !isAuthed || !state.me?.isAdmin); if (!isAuthed) { const adminCard = $("admin-card"); if (adminCard) adminCard.classList.add("hidden"); const loginUser = $("login-username"); const cachedUser = getSavedUsername(); if ( loginUser && cachedUser && !loginUser.dataset.userEditing && !loginUser.value ) { loginUser.value = cachedUser; } } } export function setAuthMode(mode) { state.authMode = mode; document.querySelectorAll(".auth-form").forEach((form) => { form.classList.toggle("hidden", form.dataset.mode !== mode); }); const title = $("auth-title"); const toggleBtn = $("auth-toggle"); if (title) { title.textContent = mode === "login" ? t("auth.loginHeading") : t("auth.registerHeading"); } if (toggleBtn) { toggleBtn.textContent = mode === "login" ? t("auth.switchToRegister") : t("auth.switchToLogin"); } } export function handleAuthError(err, clearUserState) { if (err?.status === 401) { clearUserState(); state.isAuthenticated = false; setAuthUI(false); return true; } toast(err?.message || t("toast.unexpected"), true); return false; } export function renderPhasePill() { const phaseKey = typeof state.phase === "string" ? state.phase.toLowerCase() : null; const pill = $("phase-pill"); if (pill) pill.textContent = phaseKey ? t(`phase.${phaseKey}`) : t("phase.loading"); document.querySelectorAll(".phase-view").forEach((el) => el.classList.add("hidden"), ); const viewMap = { Suggest: "suggest-view", Vote: "vote-view", Results: "results-view", }; const id = viewMap[state.phase]; if (id) $(id).classList.remove("hidden"); updatePhaseNav(); } export function renderCounts() { if (!state.counts) return; $("counts").textContent = t("counts.format", { players: state.counts.players, suggestions: state.counts.suggestions, votes: state.counts.votes, }); } export function renderWelcome() { const el = $("welcome-text"); if (!el) return; const name = state.me?.displayName?.trim() || state.me?.username || t("auth.defaultName"); el.textContent = t("auth.welcome", { name }); } export function renderMySuggestions() { const wrap = $("my-suggestions"); if (!wrap) return; wrap.innerHTML = ""; const allowEdit = true; // own suggestions can always adjust optional data const lockTitle = state.phase !== "Suggest" && !state.me?.isAdmin; const allowDelete = state.phase === "Suggest" || state.me?.isAdmin; state.mySuggestions.forEach((s) => wrap.appendChild( buildCard(s, { showAuthor: false, allowDelete, allowEdit, lockTitle }), ), ); } export function renderAllSuggestions() { renderAdminLinker(); const list = $("all-suggestions"); if (!list) return; list.innerHTML = ""; const allowEdit = true; // allow own edits (optional fields) in vote const allowDelete = !!state.me?.isAdmin; state.allSuggestions.forEach((s) => list.appendChild( buildCard(s, { showAuthor: true, allowEdit, allowDelete }), ), ); renderPhaseTitles(); } export 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) => { const canEdit = !!state.me?.isAdmin || s.playerId === state.me?.id; const lockTitle = state.phase !== "Suggest" && !state.me?.isAdmin; const li = buildCard(s, { showAuthor: true, allowEdit: canEdit, lockTitle, }); const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id); const current = hasVote ? votesMap[s.id] : 5; // start neutral when no prior vote const displayScore = hasVote ? current : "—"; const displayEmoji = hasVote ? scoreToEmoji(current) : "⚠️"; const linkedIds = linkedPeerIds(s); const rootId = linkRootId(s); const footer = document.createElement("div"); footer.className = "vote-controls"; footer.innerHTML = `
${state.votesFinal ? t("vote.missingFinalWarn") : t("vote.missingWarn")}
${displayScore} ${displayEmoji}
`; li.querySelector(".card-body").appendChild(footer); list.appendChild(li); }); updatePhaseNav(); updateMissingBadgeFromDom(); list.querySelectorAll("input[type=range]").forEach((input) => { input.addEventListener("input", (e) => { if (state.votesFinal) return; const val = Number(e.target.value); $("score-" + e.target.dataset.id).textContent = val; const emojiEl = $("emoji-" + e.target.dataset.id); if (emojiEl) emojiEl.textContent = scoreToEmoji(val); const warn = $("warn-" + e.target.dataset.id); if (warn) warn.classList.add("hidden"); syncLinkedSliders(e.target, val); updateMissingBadgeFromDom(); }); input.addEventListener("change", async (e) => { if (state.votesFinal) return; const suggestionId = Number(e.target.dataset.id); const score = Number(e.target.value); try { await api.vote(suggestionId, score); toast(t("vote.saved")); await window.loadVoteData(); } catch (err) { toast(err.message, true); } }); }); } export function syncVoteScores() { const votesMap = Object.fromEntries( state.myVotes.map((v) => [v.suggestionId, v.score]), ); Object.entries(votesMap).forEach(([id, score]) => { const slider = document.querySelector( `input[type=range][data-id="${id}"]`, ); const scoreLabel = $("score-" + id); const emoji = $("emoji-" + id); if (slider && score != null) { slider.value = score; if (scoreLabel) scoreLabel.textContent = score; if (emoji) emoji.textContent = scoreToEmoji(score); } }); document .querySelectorAll("input[type=range][data-id]") .forEach((slider) => { const id = slider.dataset.id; if (Object.prototype.hasOwnProperty.call(votesMap, Number(id))) return; const scoreLabel = $("score-" + id); const emoji = $("emoji-" + id); if (scoreLabel) scoreLabel.textContent = "—"; if (emoji) emoji.textContent = neutralEmoji(); }); } export function renderResults() { const container = $("results-list"); container.innerHTML = ""; const table = document.createElement("table"); table.className = "results-table"; table.innerHTML = ` ${t("results.rank")} ${t("results.game")} ${t("results.author")} ${t("results.average")} ${t("results.votesList")} ${t("results.myVote")} ${t("results.links")} `; const tbody = table.querySelector("tbody"); state.results.forEach((r, idx) => { const row = document.createElement("tr"); const podiumClass = idx === 0 ? "podium podium-1" : idx === 1 ? "podium podium-2" : idx === 2 ? "podium podium-3" : ""; row.className = podiumClass; const medal = idx === 0 ? "🥇" : idx === 1 ? "🥈" : idx === 2 ? "🥉" : `${idx + 1}`; row.innerHTML = ` ${medal} ${r.screenshotUrl ? `${r.name}` : ''}
${r.name} ${renderLinkBadge(r)}
${buildResultMeta(r)}
${r.author ?? "—"} ${r.average?.toFixed ? r.average.toFixed(1) : r.average} ${formatVotes(r.votes)} ${formatMyVote(r.myVote)} ${r.gameUrl ? `${t("results.link.site")}
` : ''} ${r.youtubeUrl ? `${t("results.link.youtube")}` : ''} `; tbody.appendChild(row); }); const frame = document.createElement("div"); frame.className = "results-frame"; frame.appendChild(table); container.appendChild(frame); container.querySelectorAll(".clickable-thumb").forEach((img) => { img.addEventListener("click", () => openLightbox(img.src, img.alt)); }); } function buildResultMeta(r) { const hasPlayers = r.minPlayers || r.maxPlayers; const players = hasPlayers ? t("card.players", { min: r.minPlayers ?? "?", max: r.maxPlayers ?? "?" }) : null; const bits = [r.genre, players].filter(Boolean); if (bits.length === 0) return ""; return `
${bits.join(" • ")}
`; } export function renderPhaseTitles() { const voteTitle = $("vote-title"); const totalGames = state.allSuggestions?.length ?? 0; if (voteTitle) { voteTitle.textContent = totalGames > 0 ? t("section.vote.count", { count: totalGames }) : t("section.vote"); } } export function buildCard( s, { showAuthor = false, allowDelete = false, allowEdit = false, lockTitle = false }, ) { const card = document.createElement("article"); card.className = "game-card"; const hasImage = !!s.screenshotUrl; const linkedTitles = linkedPeerTitles(s); const linked = isLinked(s); const linkTooltip = linked ? linkedTitles.length > 0 ? t("card.linkedWith", { names: linkedTitles.join(", ") }) : t("card.linked") : ""; const linkChip = linked ? `` : ""; const visual = hasImage ? `` : `
`; const hasPlayers = s.minPlayers || s.maxPlayers; const players = hasPlayers ? `${t("card.players", { min: s.minPlayers ?? "?", max: s.maxPlayers ?? "?", })}` : ""; const genreAndPlayers = s.genre ? hasPlayers ? `${s.genre} • ${players}` : s.genre : hasPlayers ? players : undefined; const hasExtraInfo = genreAndPlayers || s.gameUrl || s.youtubeUrl; card.innerHTML = ` ${visual}

${s.name}

${linkChip} ${showAuthor && s.author ? `${s.author}` : ""} ${allowEdit ? `` : ""} ${allowDelete ? `` : ""}
${hasExtraInfo ? `

` : ""} ${genreAndPlayers ? genreAndPlayers : ""} ${s.gameUrl ? `${t("card.site")}` : ""} ${s.youtubeUrl ? `${t("card.youtube")}` : ""} ${hasExtraInfo ? `

` : ""} ${s.description ? `

${s.description}

` : ""}
`; if (hasImage) { const btn = card.querySelector(".card-visual"); setupCardVisualHover(btn, s.screenshotUrl); btn.addEventListener("click", () => openLightbox(s.screenshotUrl, s.name)); } if (linked && state.me?.isAdmin) { const unlinkBtn = card.querySelector("[data-unlink]"); unlinkBtn?.addEventListener("click", () => openUnlinkConfirm(s)); } if (allowEdit) { const editBtn = card.querySelector("[data-edit]"); editBtn?.addEventListener("click", () => openSuggestionModal({ title: t("modal.editTitle"), submitLabel: t("modal.save"), initial: s, onSubmit: async (data, close) => { await api.updateSuggestion(s.id, data); toast(t("toast.savedChanges")); close(); await window.refreshPhaseData(); }, lockTitle, }), ); } if (allowDelete) { const del = card.querySelector("[data-delete]"); del.addEventListener("click", async () => { openDeleteConfirmModal(s); }); } return card; } function buildSuggestionForm(initial = {}, lockTitle = false) { const form = document.createElement("form"); form.className = "stack suggestion-form"; form.innerHTML = `
${t("form.players")}
`; const setVal = (name, value) => { const input = form.querySelector(`[name="${name}"]`); if (input) { input.value = value ?? ""; if (name === "name" && lockTitle) { input.readOnly = true; input.classList.add("readonly"); } } }; setVal("name", initial.name ?? ""); setVal("genre", initial.genre ?? ""); const desc = form.querySelector("textarea[name=description]"); if (desc) desc.value = initial.description ?? ""; setVal("minPlayers", initial.minPlayers ?? ""); setVal("maxPlayers", initial.maxPlayers ?? ""); setVal("screenshotUrl", initial.screenshotUrl ?? ""); setVal("youtubeUrl", initial.youtubeUrl ?? ""); setVal("gameUrl", initial.gameUrl ?? ""); initCharCounters(form); return form; function initCharCounters(formEl) { const inputs = formEl.querySelectorAll("input[maxlength], textarea[maxlength]"); inputs.forEach((input) => { const counter = formEl.querySelector(`.char-counter[data-for="${input.name}"]`); if (!counter) return; const update = () => { const max = input.maxLength; if (!max || max < 0) return; const used = input.value?.length ?? 0; counter.textContent = `${used}/${max}`; }; input.addEventListener("input", update); update(); }); } } function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockTitle = false }) { const overlay = document.createElement("div"); overlay.className = "edit-modal"; const panel = document.createElement("div"); panel.className = "edit-panel"; panel.innerHTML = `

${title}

`; const form = buildSuggestionForm(initial, lockTitle); const actions = document.createElement("div"); actions.className = "stack horizontal"; const submitBtn = document.createElement("button"); submitBtn.type = "submit"; submitBtn.textContent = submitLabel; const cancelBtn = document.createElement("button"); cancelBtn.type = "button"; cancelBtn.className = "ghost"; cancelBtn.textContent = t("modal.cancel"); actions.append(submitBtn, cancelBtn); form.appendChild(actions); const close = () => overlay.remove(); overlay.addEventListener("click", (e) => { if ( e.target.classList.contains("edit-modal") || e.target.classList.contains("lightbox-close") ) close(); }); cancelBtn.addEventListener("click", close); form.addEventListener("submit", async (e) => { e.preventDefault(); const data = normalizeSuggestionForm(new FormData(form)); if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) { return toast(t("toast.invalidImageUrl"), true); } if (!data.name?.trim()) return toast(t("toast.nameRequired"), true); try { await onSubmit(data, close, submitBtn); } catch (err) { if (window.handleAuthError?.(err)) return; toast(err.message, true); } }); panel.querySelector(".edit-body")?.appendChild(form); overlay.appendChild(panel); document.body.appendChild(overlay); return overlay; } export function openNewSuggestionModal() { openSuggestionModal({ title: t("modal.addTitle") || t("suggest.new"), submitLabel: t("form.submit"), initial: {}, onSubmit: async (data, close, submitBtn) => { await api.createSuggestion(data); toast(t("toast.suggestionAdded")); if (submitBtn) triggerCelebration(submitBtn); close(); await window.loadSuggestData(); }, }); } export function openLightbox(url, title) { const overlay = document.createElement("div"); overlay.className = "lightbox"; overlay.innerHTML = ` `; overlay.addEventListener("click", (e) => { if ( e.target.classList.contains("lightbox") || e.target.classList.contains("lightbox-close") ) { overlay.remove(); } }); document.body.appendChild(overlay); } export function openConfirmModal({ title, body, confirmLabel, cancelLabel = t("modal.cancel"), onConfirm }) { const overlay = document.createElement("div"); overlay.className = "edit-modal"; const panel = document.createElement("div"); panel.className = "edit-panel"; panel.innerHTML = `

${title}

${body}

`; const actions = document.createElement("div"); actions.className = "stack horizontal"; const confirmBtn = document.createElement("button"); confirmBtn.textContent = confirmLabel ?? t("modal.confirm"); const cancelBtn = document.createElement("button"); cancelBtn.className = "ghost"; cancelBtn.type = "button"; cancelBtn.textContent = cancelLabel; actions.append(confirmBtn, cancelBtn); panel.querySelector(".edit-body")?.appendChild(actions); const close = () => overlay.remove(); overlay.addEventListener("click", (e) => { if ( e.target.classList.contains("edit-modal") || e.target.classList.contains("lightbox-close") ) close(); }); cancelBtn.addEventListener("click", close); confirmBtn.addEventListener("click", async () => { try { await onConfirm?.(close); } catch (err) { toast(err.message, true); } }); overlay.appendChild(panel); document.body.appendChild(overlay); } export function normalizeSuggestionForm(formData) { const obj = Object.fromEntries(formData.entries()); const parseNum = (v) => { if (v === undefined || v === null || v === "") return null; const n = Number(v); return Number.isFinite(n) ? n : null; }; return { name: obj.name?.trim(), genre: obj.genre?.trim() || null, description: obj.description?.trim() || null, screenshotUrl: obj.screenshotUrl?.trim() || null, youtubeUrl: obj.youtubeUrl?.trim() || null, gameUrl: obj.gameUrl?.trim() || null, minPlayers: parseNum(obj.minPlayers), maxPlayers: parseNum(obj.maxPlayers), }; } export function scoreToEmoji(score) { if (score == null || Number.isNaN(score)) return neutralEmoji(); if (score < 1) return "😡"; if (score <= 3) return "😠"; if (score <= 6) return "😐"; if (score <= 8) return "🙂"; if (score <= 9) return "😃"; return "🤩"; } export function neutralEmoji() { return "😐"; } function formatVotes(votes) { if (!Array.isArray(votes) || votes.length === 0) return "⚠️"; const sorted = [...votes].sort((a, b) => a - b); return sorted.map((v) => scoreToEmoji(v)).join(""); } function formatMyVote(score) { if (score == null || Number.isNaN(score)) return "—"; return `${score} ${scoreToEmoji(score)}`; } function missingVotesCount() { const total = state.allSuggestions?.length ?? 0; const mine = state.myVotes?.length ?? 0; const votedIds = new Set(state.myVotes?.map((v) => v.suggestionId)); const missing = total - votedIds.size; return missing < 0 ? 0 : missing; } function updateMissingBadgeFromDom() { const badge = $("vote-missing"); if (!badge) return; if (state.votesFinal || state.phase !== "Vote") { badge.classList.add("hidden"); return; } const sliders = document.querySelectorAll("#vote-list input[type=range]"); const missing = Array.from(sliders).some((slider) => { const scoreLabel = $("score-" + slider.dataset.id); return !scoreLabel || scoreLabel.textContent === "—"; }); badge.classList.toggle("hidden", !missing); } function renderAdminVoteStatus() { if (!state.me?.isAdmin) return; const list = $("admin-voter-list"); const status = $("admin-ready-status"); if (!state.adminVoteStatus || !list || !status) return; list.innerHTML = ""; state.adminVoteStatus.voters.forEach((v) => { const li = document.createElement("li"); const name = v.name?.length > 24 ? `${v.name.slice(0, 21)}…` : v.name; li.textContent = `${name} — ${v.finalized ? "✅" : "⏳"}`; li.title = v.name; list.appendChild(li); }); const waiting = state.adminVoteStatus.waiting; const ready = waiting.length === 0; const waitingDisplay = waiting.map((name) => name?.length > 24 ? `${name.slice(0, 21)}…` : name, ); status.textContent = ready ? t("admin.readyForResults") : t("admin.waitingForPlayers", { names: waitingDisplay.join(", ") }); status.className = ready ? "badge" : "badge warning"; } function renderAdminLinker() { const wrap = $("admin-linker"); const source = $("link-source"); const target = $("link-target"); if (!wrap || !source || !target) return; const visible = state.me?.isAdmin && state.phase === "Vote"; wrap.classList.toggle("hidden", !visible); if (!visible) return; const previousSource = source.value; const previousTarget = target.value; const options = (state.allSuggestions ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)); const fillSelect = (select, placeholderKey) => { select.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = t(placeholderKey); placeholder.disabled = true; placeholder.selected = true; select.appendChild(placeholder); options.forEach((s) => { const opt = document.createElement("option"); opt.value = s.id; opt.textContent = buildLinkOptionLabel(s); opt.dataset.root = linkRootId(s); select.appendChild(opt); }); }; fillSelect(source, "admin.linkSourcePlaceholder"); fillSelect(target, "admin.linkTargetPlaceholder"); if (previousSource && options.some((s) => String(s.id) === previousSource)) source.value = previousSource; if (previousTarget && options.some((s) => String(s.id) === previousTarget)) target.value = previousTarget; const preventSameSelection = () => { const sourceVal = source.value; const targetVal = target.value; Array.from(target.options).forEach((opt) => { if (!opt.value) return; opt.disabled = opt.value === sourceVal; }); Array.from(source.options).forEach((opt) => { if (!opt.value) return; opt.disabled = opt.value === targetVal; }); }; source.onchange = preventSameSelection; target.onchange = preventSameSelection; preventSameSelection(); } function openDeleteConfirmModal(s) { const overlay = document.createElement("div"); overlay.className = "edit-modal"; const panel = document.createElement("div"); panel.className = "edit-panel"; panel.innerHTML = `

${t("modal.confirmDeleteTitle")}

`; const preview = buildCard( { ...s, id: s.id }, // shallow copy to avoid mutations { showAuthor: true, allowDelete: false, allowEdit: false }, ); preview.classList.add("preview-card"); const actions = document.createElement("div"); actions.className = "stack horizontal"; const confirmBtn = document.createElement("button"); confirmBtn.className = "danger"; confirmBtn.textContent = t("modal.confirmDelete"); const cancelBtn = document.createElement("button"); cancelBtn.type = "button"; cancelBtn.className = "ghost"; cancelBtn.textContent = t("modal.cancel"); actions.append(confirmBtn, cancelBtn); const body = panel.querySelector(".delete-body"); body?.append(preview, actions); const close = () => overlay.remove(); overlay.addEventListener("click", (e) => { if ( e.target.classList.contains("edit-modal") || e.target.classList.contains("lightbox-close") ) close(); }); cancelBtn.addEventListener("click", close); confirmBtn.addEventListener("click", async () => { try { await api.deleteSuggestion(s.id); toast(t("toast.suggestionDeleted")); close(); await window.loadSuggestData(); } catch (err) { toast(err.message, true); } }); overlay.appendChild(panel); document.body.appendChild(overlay); } 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; } } function linkRootId(s) { return s?.parentSuggestionId ?? s?.id; } function linkedPeerIds(s) { if (!s) return []; if (Array.isArray(s.linkedIds) && s.linkedIds.length > 0) { return s.linkedIds.filter((id) => id !== s.id); } if (!state.allSuggestions?.length) return []; const root = linkRootId(s); return state.allSuggestions .filter((other) => linkRootId(other) === root && other.id !== s.id) .map((other) => other.id); } function linkedPeerTitles(s) { if (!s) return []; if (Array.isArray(s.linkedTitles) && s.linkedTitles.length > 0) { return s.linkedTitles; } if (!state.allSuggestions?.length) return []; const root = linkRootId(s); return state.allSuggestions .filter((other) => linkRootId(other) === root && other.id !== s.id) .map((other) => other.name); } function isLinked(s) { return !!s?.parentSuggestionId || linkedPeerIds(s).length > 0; } function linkTooltip(s) { const peers = linkedPeerTitles(s); if (peers.length === 0) return t("card.linked"); return t("card.linkedWith", { names: peers.join(", ") }); } function renderLinkBadge(s) { if (!isLinked(s)) return ""; return `🔗`; } function buildLinkOptionLabel(s) { const author = s.author ? ` — ${s.author}` : ""; const linked = isLinked(s) ? " 🔗" : ""; return `${s.name}${author}${linked}`; } function syncLinkedSliders(sourceEl, value) { const linkedAttr = sourceEl?.dataset?.linked; if (!linkedAttr) return; const ids = linkedAttr.split(",").filter(Boolean); ids.forEach((id) => { const slider = document.querySelector(`input[type=range][data-id="${id}"]`); if (!slider || slider === sourceEl) return; slider.value = value; const scoreLabel = $("score-" + id); if (scoreLabel) scoreLabel.textContent = value; const emojiEl = $("emoji-" + id); if (emojiEl) emojiEl.textContent = scoreToEmoji(Number(value)); const warn = $("warn-" + id); if (warn) warn.classList.add("hidden"); }); } function openUnlinkConfirm(s) { const peers = linkedPeerTitles(s); const names = peers.length ? peers.join(", ") : t("admin.unlinkUnknownPeers"); openConfirmModal({ title: t("admin.unlinkTitle"), body: t("admin.unlinkBody", { name: s.name, peers: names }), confirmLabel: t("admin.unlinkConfirm"), cancelLabel: t("modal.cancel"), onConfirm: async (close) => { try { await adminApi.unlinkSuggestions(s.id); toast(t("admin.unlinkDone")); close(); await window.refreshPhaseData(); } catch (err) { toast(err.message, true); } } }); } export function updatePhaseNav() { const isAdmin = !!state.me?.isAdmin; const phase = state.phase; const showNav = (id, visible) => { const el = $(id); if (el) el.classList.toggle("hidden", !visible); }; showNav("nav-suggest", phase === "Suggest"); showNav("nav-vote", phase === "Vote"); const finalizeBtn = $("finalize-votes"); if (finalizeBtn) { finalizeBtn.textContent = state.votesFinal ? t("vote.unfinalize") : t("vote.finalize"); } const voteMissingBadge = $("vote-missing"); if (voteMissingBadge) { const missing = missingVotesCount(); const showMissing = !state.votesFinal && missing > 0; voteMissingBadge.classList.toggle("hidden", !showMissing); voteMissingBadge.textContent = t("vote.missingFooter"); } const waitAdmin = $("vote-wait-admin"); if (waitAdmin) { const show = state.votesFinal && phase === "Vote" && !state.resultsOpen; waitAdmin.classList.toggle("hidden", !show); waitAdmin.textContent = t("vote.waitAdmin"); } const voteStatusText = $("vote-status-text"); if (voteStatusText) { voteStatusText.textContent = state.votesFinal ? t("nav.voteFinalized") : t("nav.voteHint"); } renderAdminVoteStatus(); renderAdminLinker(); updateMissingBadgeFromDom(); // Toggle admin-only back buttons const backButtons = ["nav-vote-prev"]; backButtons.forEach((id) => { const btn = $(id); if (btn) btn.classList.toggle("hidden", !isAdmin); }); // Disable vote->results next if locked (for non-admins) const voteNext = $("nav-vote-next"); if (voteNext) { const locked = !state.resultsOpen && !isAdmin; voteNext.disabled = locked; voteNext.textContent = locked ? t("nav.waitingForResults") : t("nav.next"); } // Admin toggle state const adminResultsToggle = $("results-open"); if (adminResultsToggle) { adminResultsToggle.checked = !!state.resultsOpen; } }