Lock suggestions after reveal and move per-phase navigation
This commit is contained in:
@@ -34,6 +34,12 @@ const translations = {
|
||||
"nav.prev": "Back",
|
||||
"nav.next": "Next",
|
||||
"nav.waitingForResults": "Waiting…",
|
||||
"nav.freezeTitle": "Ready to reveal?",
|
||||
"nav.freezeHint": "Moving forward will freeze your suggestions. Titles become locked; only extra details stay editable.",
|
||||
"nav.freezeModalTitle": "Freeze suggestions?",
|
||||
"nav.freezeModalBody": "Once you leave Suggest, your games are locked: titles cannot be changed or deleted. Only optional details (description, links, players, artwork) remain editable. Continue?",
|
||||
"nav.revealHint": "Review all games, then advance to voting when ready.",
|
||||
"nav.voteHint": "Cast votes for every game to unlock results.",
|
||||
|
||||
"suggest.title": "Suggest games (up to 5)",
|
||||
"suggest.new": "Add new suggestion",
|
||||
@@ -146,6 +152,12 @@ const translations = {
|
||||
"nav.prev": "Zurück",
|
||||
"nav.next": "Weiter",
|
||||
"nav.waitingForResults": "Warten…",
|
||||
"nav.freezeTitle": "Bereit zum Aufdecken?",
|
||||
"nav.freezeHint": "Beim Weitergehen werden deine Vorschläge eingefroren. Titel bleiben gesperrt; nur Zusatzinfos bleiben bearbeitbar.",
|
||||
"nav.freezeModalTitle": "Vorschläge einfrieren?",
|
||||
"nav.freezeModalBody": "Sobald du die Vorschlagsphase verlässt, sind deine Spiele gesperrt: Titel können nicht mehr geändert oder gelöscht werden. Nur optionale Angaben (Beschreibung, Links, Spielerzahlen, Bilder) bleiben bearbeitbar. Fortfahren?",
|
||||
"nav.revealHint": "Sieh dir alle Spiele an und gehe dann zur Abstimmung weiter.",
|
||||
"nav.voteHint": "Bewerte alle Spiele, um die Ergebnisse freizuschalten.",
|
||||
|
||||
"suggest.title": "Schlage Spiele vor (bis zu 5)",
|
||||
"suggest.new": "Neuen Vorschlag hinzufügen",
|
||||
|
||||
132
wwwroot/js/ui.js
132
wwwroot/js/ui.js
@@ -79,27 +79,7 @@ export function renderPhasePill() {
|
||||
const id = viewMap[state.phase];
|
||||
if (id) $(id).classList.remove("hidden");
|
||||
|
||||
const prevBtn = $("prev-phase");
|
||||
if (prevBtn) prevBtn.disabled = state.phase === "Suggest";
|
||||
|
||||
const nextBtn = $("next-phase");
|
||||
if (nextBtn) {
|
||||
const atResults = state.phase === "Results";
|
||||
const locked = !state.resultsOpen && state.phase === "Vote";
|
||||
nextBtn.disabled = atResults || locked;
|
||||
nextBtn.textContent = locked ? t("nav.waitingForResults") : t("nav.next");
|
||||
}
|
||||
|
||||
const resultsLock = $("results-lock");
|
||||
if (resultsLock) {
|
||||
resultsLock.classList.toggle("hidden", state.resultsOpen);
|
||||
resultsLock.textContent = t("admin.resultsLocked");
|
||||
}
|
||||
|
||||
const adminResultsToggle = $("results-open");
|
||||
if (adminResultsToggle) {
|
||||
adminResultsToggle.checked = !!state.resultsOpen;
|
||||
}
|
||||
updatePhaseNav();
|
||||
}
|
||||
|
||||
export function renderCounts() {
|
||||
@@ -125,10 +105,12 @@ export function renderMySuggestions() {
|
||||
const wrap = $("my-suggestions");
|
||||
if (!wrap) return;
|
||||
wrap.innerHTML = "";
|
||||
const allowEdit = state.phase === "Suggest" || state.me?.isAdmin;
|
||||
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: true, allowEdit }),
|
||||
buildCard(s, { showAuthor: false, allowDelete, allowEdit, lockTitle }),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -306,7 +288,7 @@ export function renderPhaseTitles() {
|
||||
|
||||
export function buildCard(
|
||||
s,
|
||||
{ showAuthor = false, allowDelete = false, allowEdit = false },
|
||||
{ showAuthor = false, allowDelete = false, allowEdit = false, lockTitle = false },
|
||||
) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "game-card";
|
||||
@@ -366,6 +348,7 @@ export function buildCard(
|
||||
close();
|
||||
await window.refreshPhaseData();
|
||||
},
|
||||
lockTitle,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -378,7 +361,7 @@ export function buildCard(
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildSuggestionForm(initial = {}) {
|
||||
function buildSuggestionForm(initial = {}, lockTitle = false) {
|
||||
const form = document.createElement("form");
|
||||
form.className = "stack suggestion-form";
|
||||
form.innerHTML = `
|
||||
@@ -441,7 +424,13 @@ function buildSuggestionForm(initial = {}) {
|
||||
|
||||
const setVal = (name, value) => {
|
||||
const input = form.querySelector(`[name="${name}"]`);
|
||||
if (input) input.value = value ?? "";
|
||||
if (input) {
|
||||
input.value = value ?? "";
|
||||
if (name === "name" && lockTitle) {
|
||||
input.readOnly = true;
|
||||
input.classList.add("readonly");
|
||||
}
|
||||
}
|
||||
};
|
||||
setVal("name", initial.name ?? "");
|
||||
setVal("genre", initial.genre ?? "");
|
||||
@@ -472,7 +461,7 @@ function buildSuggestionForm(initial = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit }) {
|
||||
function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit, lockTitle = false }) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "edit-modal";
|
||||
const panel = document.createElement("div");
|
||||
@@ -485,7 +474,7 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit }) {
|
||||
<div class="edit-body"></div>
|
||||
`;
|
||||
|
||||
const form = buildSuggestionForm(initial);
|
||||
const form = buildSuggestionForm(initial, lockTitle);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "stack horizontal";
|
||||
const submitBtn = document.createElement("button");
|
||||
@@ -565,6 +554,52 @@ export function openLightbox(url, title) {
|
||||
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 = `
|
||||
<div class="edit-header">
|
||||
<h3>${title}</h3>
|
||||
<button class="lightbox-close" aria-label="${t("modal.close")}">x</button>
|
||||
</div>
|
||||
<div class="edit-body">
|
||||
<p>${body}</p>
|
||||
</div>
|
||||
`;
|
||||
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) => {
|
||||
@@ -681,3 +716,44 @@ function isValidImageUrl(url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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-reveal", phase === "Reveal");
|
||||
showNav("nav-vote", phase === "Vote");
|
||||
|
||||
const lockBadge = $("results-lock");
|
||||
if (lockBadge) {
|
||||
const locked = !state.resultsOpen && phase === "Vote";
|
||||
lockBadge.classList.toggle("hidden", !locked);
|
||||
lockBadge.textContent = t("admin.resultsLocked");
|
||||
}
|
||||
|
||||
// Toggle admin-only back buttons
|
||||
const backButtons = ["nav-reveal-prev", "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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user