import { api } from "./api.js"; import { t } from "./i18n.js"; import { state } from "./state.js"; import { $, toast } from "./dom.js"; import { renderAdminLinker, renderAdminVoteStatus } from "./admin-ui.js"; import { getUiRuntime } from "./ui-runtime.js"; import { linkedPeerIds, linkRootId, sortByName } from "./ui-utils.js"; import { buildCard } from "./suggestions-ui.js"; export function renderVotes() { const list = $("vote-list"); if (!list) return; const prevScroll = list.scrollTop; list.innerHTML = ""; const votesMap = Object.fromEntries( state.myVotes.map((v) => [v.suggestionId, v.score]), ); sortByName(state.allSuggestions).forEach((s) => { const canEdit = !!state.me?.isAdmin || s.isOwner; const lockTitle = state.phase !== "Suggest" && !state.me?.isAdmin; const li = buildCard(s, { showAuthor: true, allowEdit: canEdit, allowDelete: !!state.me?.isAdmin, lockTitle, }); const hasVote = Object.prototype.hasOwnProperty.call(votesMap, s.id); const current = hasVote ? votesMap[s.id] : 5; 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.scrollTop = prevScroll; list.querySelectorAll("input[type=range]").forEach((input) => { input.addEventListener("input", (e) => { if (state.votesFinal) return; const val = Number(e.target.value); const id = e.target.dataset.id; $("score-" + id).textContent = val; const emojiEl = $("emoji-" + id); if (emojiEl) emojiEl.textContent = scoreToEmoji(val); const warn = $("warn-" + id); if (warn) warn.classList.remove("hidden"); e.target.dataset.pending = "1"; 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); const prevScore = votesMap[suggestionId]; const linkedIds = (e.target.dataset.linked || "") .split(",") .filter(Boolean) .map((x) => Number(x)); const resetUi = () => { const label = $("score-" + suggestionId); const emoji = $("emoji-" + suggestionId); const warn = $("warn-" + suggestionId); const fallbackValue = prevScore ?? 5; const fallbackDisplay = prevScore ?? "—"; const fallbackEmoji = prevScore != null ? scoreToEmoji(prevScore) : "⚠️"; e.target.value = fallbackValue; if (label) label.textContent = fallbackDisplay; if (emoji) emoji.textContent = fallbackEmoji; if (warn) warn.classList.remove("hidden"); }; try { await api.vote(suggestionId, score); toast(t("vote.saved")); delete e.target.dataset.pending; const warn = $("warn-" + suggestionId); if (warn) warn.classList.add("hidden"); linkedIds.forEach((id) => { const peerWarn = $("warn-" + id); if (peerWarn) peerWarn.classList.add("hidden"); const peerSlider = document.querySelector( `input[type=range][data-id="${id}"]`, ); if (peerSlider) delete peerSlider.dataset.pending; }); await getUiRuntime().loadVoteData(); updateMissingBadgeFromDom(); } catch (err) { delete e.target.dataset.pending; resetUi(); 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); const warn = $("warn-" + id); if (slider && score != null) { slider.value = score; if (scoreLabel) scoreLabel.textContent = score; if (emoji) emoji.textContent = scoreToEmoji(score); if (warn) warn.classList.add("hidden"); delete slider.dataset.pending; } }); 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); const warn = $("warn-" + id); if (scoreLabel) scoreLabel.textContent = "—"; if (emoji) emoji.textContent = neutralEmoji(); if (warn) warn.classList.remove("hidden"); }); } 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 missingVotesCount() { const total = state.allSuggestions?.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 missing = missingVotesCount(); badge.classList.toggle("hidden", missing === 0); } 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.remove("hidden"); slider.dataset.pending = "1"; }); } 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 jokerBtn = $("open-joker-modal"); if (jokerBtn) { const showJoker = phase === "Vote" && state.hasJoker; jokerBtn.classList.toggle("hidden", !showJoker); jokerBtn.disabled = !showJoker; } 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(); const backButtons = ["nav-vote-prev"]; backButtons.forEach((id) => { const btn = $(id); if (btn) btn.classList.toggle("hidden", !isAdmin); }); const suggestNext = $("nav-suggest-next"); const suggestHint = $("nav-suggest-hint"); if (suggestNext) { const hasSuggestions = (state.mySuggestions?.length ?? 0) > 0; const needsSuggestion = phase === "Suggest" && !hasSuggestions; suggestNext.classList.toggle("hidden", needsSuggestion); suggestNext.textContent = t("nav.next"); if (suggestHint) { suggestHint.classList.toggle("hidden", !needsSuggestion); suggestHint.textContent = t("nav.addSuggestionFirst"); } } const voteNext = $("nav-vote-next"); if (voteNext) { const locked = !state.resultsOpen && !isAdmin; voteNext.disabled = locked; voteNext.textContent = locked ? t("nav.waitingForResults") : t("nav.next"); } const adminResultsToggle = $("results-open"); if (adminResultsToggle) { adminResultsToggle.textContent = state.resultsOpen ? t("admin.resultsOpenDisable") : t("admin.resultsOpenEnable"); } }