From b2b3996d78b257a8c0aeb16b1e6dbc036fc73f57 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Fri, 6 Feb 2026 23:19:34 +0100 Subject: [PATCH] Notify voters when suggestions change during refresh --- wwwroot/js/data.js | 20 +++++++++++++++++++- wwwroot/js/i18n.js | 6 ++++++ wwwroot/js/ui.js | 14 ++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/wwwroot/js/data.js b/wwwroot/js/data.js index db97d7a..e34202e 100644 --- a/wwwroot/js/data.js +++ b/wwwroot/js/data.js @@ -1,5 +1,5 @@ import { api, adminApi } from "./api.js"; -import { handleAuthError, renderAllSuggestions, renderCounts, renderMySuggestions, renderPhasePill, renderPhaseTitles, renderResults, renderVotes, renderWelcome, setAuthUI, syncVoteScores, updatePhaseNav, openResultsRelockModal } from "./ui.js"; +import { handleAuthError, renderAllSuggestions, renderCounts, renderMySuggestions, renderPhasePill, renderPhaseTitles, renderResults, renderVotes, renderWelcome, setAuthUI, syncVoteScores, updatePhaseNav, openResultsRelockModal, openSuggestionsChangedModal } from "./ui.js"; import { state, clearUserState } from "./state.js"; export async function loadState() { @@ -29,9 +29,27 @@ export async function loadSuggestData() { export async function loadRevealData() { if (state.phase === "Vote" || state.phase === "Results") { + const prev = state.allSuggestions ?? []; + const prevById = Object.fromEntries(prev.map((s) => [s.id, s])); const latest = await api.allSuggestions(); const latestSig = signatureSuggestions(latest); const changed = latestSig !== state.allSuggestionsSig; + if (changed && state.phase === "Vote" && state.allSuggestionsSig) { + const added = latest + .filter((s) => !prevById[s.id]) + .map((s) => s.name); + const relinked = latest + .filter((s) => { + const prevItem = prevById[s.id]; + return ( + prevItem && + prevItem.parentSuggestionId !== s.parentSuggestionId + ); + }) + .map((s) => s.name); + const names = [...added, ...relinked]; + openSuggestionsChangedModal(names); + } state.allSuggestions = latest; state.allSuggestionsSig = latestSig; renderAllSuggestions(); diff --git a/wwwroot/js/i18n.js b/wwwroot/js/i18n.js index 55f7d6c..cc42559 100644 --- a/wwwroot/js/i18n.js +++ b/wwwroot/js/i18n.js @@ -108,6 +108,9 @@ const translations = { "results.relockedTitle": "Results closed", "results.relockedBody": "Results have been locked again. You’re back in the voting phase and your finalized status was cleared. Adjust scores and re-finalize when ready.", "results.relockedConfirm": "Got it", + "vote.listUpdatedTitle": "Vote list updated", + "vote.listUpdatedBody": "New or linked games: {names}", + "vote.listUpdatedConfirm": "OK", "admin.title": "Admin", "admin.tools": "Admin tools", @@ -280,6 +283,9 @@ const translations = { "results.relockedTitle": "Ergebnisse geschlossen", "results.relockedBody": "Die Ergebnisse wurden wieder gesperrt. Du bist zurück in der Bewertungsphase und deine Finalisierung wurde zurückgesetzt. Passe deine Bewertungen an und schließe erneut ab, wenn du bereit bist.", "results.relockedConfirm": "Verstanden", + "vote.listUpdatedTitle": "Liste aktualisiert", + "vote.listUpdatedBody": "Neue oder verknüpfte Spiele: {names}", + "vote.listUpdatedConfirm": "OK", "admin.title": "Admin", "admin.tools": "Admin-Werkzeuge", diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 3cbab11..6347140 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -172,6 +172,7 @@ export function renderAllSuggestions() { 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]), @@ -205,6 +206,7 @@ export function renderVotes() { }); updatePhaseNav(); updateMissingBadgeFromDom(); + list.scrollTop = prevScroll; list.querySelectorAll("input[type=range]").forEach((input) => { input.addEventListener("input", (e) => { if (state.votesFinal) return; @@ -734,6 +736,18 @@ export function openResultsRelockModal() { }); } +export function openSuggestionsChangedModal(names) { + const uniq = Array.from(new Set(names)).filter(Boolean); + if (uniq.length === 0) return; + openConfirmModal({ + title: t("vote.listUpdatedTitle"), + body: t("vote.listUpdatedBody", { names: uniq.join(", ") }), + confirmLabel: t("vote.listUpdatedConfirm"), + cancelLabel: null, + onConfirm: (close) => close(), + }); +} + export function openConfirmModal({ title, body, confirmLabel, cancelLabel = t("modal.cancel"), onConfirm }) { const overlay = document.createElement("div"); overlay.className = "edit-modal";