Revert "Implement admin back-pass flow and guarded admin actions"

This reverts commit 5595bfd3b1.
This commit is contained in:
2026-02-08 14:43:26 +01:00
parent 5595bfd3b1
commit 5ec18d20ea
25 changed files with 108 additions and 571 deletions

View File

@@ -47,7 +47,7 @@ async function refreshWithUiErrorHandling() {
function scheduleNextRefresh() {
refreshTimerId = window.setTimeout(async () => {
if (!document.hidden && !state.adminStatusMenuOpen) {
if (!document.hidden) {
await refreshWithUiErrorHandling();
}
scheduleNextRefresh();
@@ -59,7 +59,7 @@ function startRefreshScheduler() {
refreshSchedulerStarted = true;
document.addEventListener("visibilitychange", () => {
if (!document.hidden && !state.adminStatusMenuOpen) {
if (!document.hidden) {
refreshWithUiErrorHandling();
}
});

View File

@@ -30,11 +30,6 @@
font-size: 12px;
letter-spacing: 0.3px;
}
.admin-status-select {
width: 100%;
min-width: 140px;
padding: 6px 8px;
}
.admin-panel {
position: fixed;

View File

@@ -108,7 +108,7 @@ Wenn ein Admin doppelte Spiele verknüpft:
Mit **„Finalisieren"** werden deine Bewertungen gesperrt. Deaktiviere es, um erneut zu bearbeiten.
„Finalisieren" ist nur während der Abstimmungsphase verfügbar und wird automatisch zurückgesetzt, wenn:
- Du mit einem Zurück-Pass zurück in die Vorschlagsphase wechselst
- Ein Joker ein neues Spiel hinzufügt
- Ein Admin Spiele verknüpft oder trennt
### Abstimmen nach Änderungen
@@ -119,26 +119,26 @@ Wenn neue Spiele hinzugefügt oder Verknüpfungen geändert werden:
Überprüfe deine Liste und bewerte erneut, bevor du wieder finalisierst.
## Zurück-Pass (Einmalige Rückkehr)
## Joker (Späte Ergänzungen)
### Was ist ein Zurück-Pass?
### Was ist ein Joker?
Ein **Zurück-Pass** ist eine einmalige Berechtigung, mit der du von der **Abstimmungsphase** zurück in die **Vorschlagsphase** wechseln kannst. Ein Admin muss ihn dir während der Abstimmung geben.
Ein **Joker** ist ein einmaliger zusätzlicher Vorschlags-Slot, der nur während der **Abstimmungsphase** verfügbar ist. Ein Admin muss ihn dir gewähren.
### So funktioniert es
Wenn du einen Zurück-Pass erhältst:
- Erscheint ein **Zurück**-Button in der Abstimmungsphase für dein Konto
- Bei Nutzung wechselst du einmal in die Vorschlagsphase zurück und der Pass wird verbraucht
- Deine Finalisierung wird beim Zurückwechseln aufgehoben
Wenn du einen Joker erhältst:
- Erscheint ein Button in der oberen Leiste, mit dem du ein weiteres Spiel hinzufügen kannst
- Nach der Nutzung wird der Joker sofort verbraucht
- Die Finalisierung aller Abstimmungen werden automatisch zurückgesetzt, damit das neue Spiel bewertet werden kann
Admins können bei Bedarf später einen weiteren Pass vergeben.
Admins können bei Bedarf zusätzliche Joker vergeben.
## Ergebnisse
### Wann sind die Ergebnisse sichtbar?
Die Ergebnisse bleiben verborgen, bis ein Admin sie freigibt. Danach werden alle Spieler automatisch in die **Ergebnisphase** verschoben. Falls nötig, kann ein Admin die Ergebnisse wieder schließen: Spieler mit mindestens einem Vorschlag kehren in die Abstimmungsphase zurück, Spieler ohne Vorschlag in die Vorschlagsphase, und Finalisierungen werden zurückgesetzt.
Die Ergebnisse bleiben verborgen, bis ein Admin sie freigibt. Danach werden alle Spieler automatisch in die **Ergebnisphase** verschoben. Falls nötig, kann ein Admin die Ergebnisse wieder schließen: Alle kehren in die Abstimmungsphase zurück und alle Abstimmungen werden zur Anpassung zurückgesetzt.
### Kann ich in der Ergebnisphase etwas bearbeiten?
@@ -148,15 +148,13 @@ Nein. Vorschläge und Bewertungen sind schreibgeschützt. Wende dich bei Bedarf
### Was können Admin-Konten tun?
- Zurück-Pässe während der Abstimmung vergeben
- Joker während der Abstimmung vergeben
- Doppelte Vorschläge verknüpfen oder trennen
- Vorschläge löschen
- Abstimmungsstatus einsehen (wer finalisiert hat)
- Einen Spieler löschen (inklusive dessen Vorschläge und Stimmen)
- Spieler über den Status-Dropdown von Abstimmung zurück auf Vorschlag setzen
- Die Datenbank auf Werkseinstellungen zurücksetzen
- Zu vorherigen Phasen zurückkehren
- Reset-/Löschaktionen mit dem eigenen Admin-Passwort bestätigen
### Was können Admin-Konten nicht tun?
@@ -176,7 +174,7 @@ Stelle sicher:
### „Du hast das Limit von 5 Vorschlägen erreicht."
Bitte einen Admin um einen Zurück-Pass, wenn du wieder in die Vorschlagsphase wechseln und deine Liste anpassen musst.
Warte auf die Abstimmungsphase und bitte bei Bedarf um einen Joker.
### „Füge mindestens einen Vorschlag hinzu, bevor du in die Abstimmungsphase wechselst."

View File

@@ -82,20 +82,21 @@ Common reasons:
Check the bottom-right corner of the screen for error messages.
## Back Pass (One-Time Return)
## Jokers (Late Additions)
### What is a back pass?
### What is a joker?
A **back pass** is a one-time permission that lets you move from **Vote** back to **Suggest**. An admin must grant it to you during Vote.
A **joker** is a one-time extra suggestion slot available only during the **Vote phase**. An admin must grant it to you.
### How it works
If you receive a back pass:
- A **Back** button appears in Vote for your account.
- Using it moves you to Suggest once and consumes the pass.
- Your finalized flag is cleared when you move back.
If you receive a joker:
- A button appears in the top bar allowing you to add one more game.
- Once used, the joker is consumed immediately.
- Your ballot becomes unfinalized.
- All players are unfinalized so the new game can be scored.
Admins may grant another pass later if needed.
Admins may grant additional jokers if necessary.
## Voting
@@ -125,7 +126,7 @@ If an admin links duplicate games:
Toggling **"Finalize"** locks your scores. Toggle it off to edit again.
Finalize is only available during the Vote phase and will automatically reset if:
- You move back to Suggest with a granted back pass
- A joker adds a new game
- An admin links or unlinks games
### Voting after changes
@@ -141,7 +142,7 @@ Review your list and rescore before finalizing again.
### When are results visible?
Results are hidden until an admin opens them. When opened, all players are automatically moved to the **Results phase**.
If needed, an admin can close the Results: players with at least one suggestion return to Vote, players without suggestions return to Suggest, and finalized ballots are cleared.
If needed, an admin can close the Results: everyone returns to the Vote phase, and all ballots are unfinalized for adjustments.
### Can I edit anything in Results?
@@ -151,15 +152,13 @@ No. Suggestions and votes are read-only. Contact an admin for assistance.
### What can admin accounts do?
- Grant jokers during Vote
- Link or unlink duplicate suggestions
- Delete suggestions
- View vote readiness (who has finalized)
- Delete a player (removes their suggestions and votes)
- Move players from Vote back to Suggest from the status dropdown
- Grant one-time back passes
- Reset the database to factory defaults
- Move backward to previous phases
- Confirm reset/delete actions with their own admin password
### What can't admin accounts do?
@@ -179,7 +178,7 @@ Make sure:
### "You have reached the 5 suggestion limit."
Ask an admin to grant a back pass if you need to return to Suggest and adjust your list.
Wait for the Vote phase and request a joker if needed.
### "Add at least one suggestion before entering the Vote phase."

View File

@@ -26,7 +26,6 @@
"counts.format": "Players: {players} • Suggestions: {suggestions} • Votes: {votes}",
"nav.prev": "Back",
"nav.next": "Next",
"nav.backToSuggestOnce": "Use pass: back to suggest",
"nav.addSuggestionFirst": "Add a game first",
"nav.waitingForResults": "Waiting…",
"nav.freezeTitle": "Ready to reveal?",
@@ -103,16 +102,11 @@
"vote.listUpdatedConfirm": "OK",
"admin.title": "Admin",
"admin.tools": "Admin tools",
"admin.resultsOpenButtonEnable": "Allow results phase",
"admin.resultsOpenButtonDisable": "Lock results phase",
"admin.resultsOpenToggle": "Allow results phase",
"admin.resultsLocked": "Results locked by admin",
"admin.resultsUpdated": "Results availability updated",
"admin.reset": "Reset (keep players)",
"admin.factoryReset": "Factory reset",
"admin.resetConfirmBody": "Enter your admin password to reset all games and votes while keeping player accounts.",
"admin.factoryResetConfirmBody": "Enter your admin password to permanently delete all accounts, games, votes, and state.",
"admin.passwordLabel": "Admin password",
"admin.passwordRequired": "Admin password is required.",
"admin.resetDone": "Reset complete",
"admin.factoryResetDone": "Factory reset complete",
"admin.readyForResults": "Ready for results",
@@ -121,10 +115,9 @@
"admin.playerUsername": "Username",
"admin.playerStatus": "Status",
"admin.playerGames": "Games",
"admin.playerJoker": "Back pass",
"admin.playerJoker": "Joker",
"admin.playerDelete": "Delete",
"admin.grantJokerChip": "Grant back",
"admin.statusUpdated": "Player status updated",
"admin.grantJokerChip": "Grant",
"admin.statusSuggesting": "Suggesting",
"admin.statusVoting": "Voting",
"admin.statusFinished": "Finished",
@@ -132,7 +125,7 @@
"admin.deleteBody": "Delete player \"{name}\" and all their games and votes? This cannot be undone.",
"admin.deleteConfirm": "Delete",
"admin.deleteDone": "Player deleted",
"admin.jokerGranted": "Back pass granted",
"admin.jokerGranted": "Joker granted",
"admin.linkTitle": "Link games",
"admin.linkSource": "Game to link",
"admin.linkTarget": "Link to (parent)",
@@ -193,7 +186,6 @@
"counts.format": "Spieler: {players} • Vorschläge: {suggestions} • Stimmen: {votes}",
"nav.prev": "Zurück",
"nav.next": "Weiter",
"nav.backToSuggestOnce": "Pass nutzen: zurück zu Vorschlag",
"nav.addSuggestionFirst": "Zuerst ein Spiel vorschlagen",
"nav.waitingForResults": "Warten…",
"nav.freezeTitle": "Bereit zum Aufdecken?",
@@ -270,16 +262,11 @@
"vote.listUpdatedConfirm": "OK",
"admin.title": "Admin",
"admin.tools": "Admin-Werkzeuge",
"admin.resultsOpenButtonEnable": "Ergebnisse freigeben",
"admin.resultsOpenButtonDisable": "Ergebnisse sperren",
"admin.resultsOpenToggle": "Ergebnisse freigeben",
"admin.resultsLocked": "Ergebnisse vom Admin gesperrt",
"admin.resultsUpdated": "Ergebnisfreigabe aktualisiert",
"admin.reset": "Zurücksetzen (Spieler behalten)",
"admin.factoryReset": "Werkseinstellung",
"admin.resetConfirmBody": "Gib dein Admin-Passwort ein, um alle Spiele und Stimmen zurückzusetzen, aber die Konten zu behalten.",
"admin.factoryResetConfirmBody": "Gib dein Admin-Passwort ein, um alle Konten, Spiele, Stimmen und den Zustand dauerhaft zu löschen.",
"admin.passwordLabel": "Admin-Passwort",
"admin.passwordRequired": "Admin-Passwort ist erforderlich.",
"admin.resetDone": "Zurücksetzen abgeschlossen",
"admin.factoryResetDone": "Werkseinstellung abgeschlossen",
"admin.readyForResults": "Bereit für Ergebnisse",
@@ -288,10 +275,9 @@
"admin.playerUsername": "Benutzername",
"admin.playerStatus": "Status",
"admin.playerGames": "Spiele",
"admin.playerJoker": "Zurück-Pass",
"admin.playerJoker": "Joker",
"admin.playerDelete": "Löschen",
"admin.grantJokerChip": "Pass geben",
"admin.statusUpdated": "Status aktualisiert",
"admin.grantJokerChip": "Joker",
"admin.statusSuggesting": "Vorschlagen",
"admin.statusVoting": "Bewerten",
"admin.statusFinished": "Fertig",
@@ -299,7 +285,7 @@
"admin.deleteBody": "Spieler \"{name}\" samt Spielen und Stimmen löschen? Dies kann nicht rückgängig gemacht werden.",
"admin.deleteConfirm": "Löschen",
"admin.deleteDone": "Spieler gelöscht",
"admin.jokerGranted": "Zurück-Pass vergeben",
"admin.jokerGranted": "Joker vergeben",
"admin.linkTitle": "Spiele verknüpfen",
"admin.linkSource": "Spiel verknüpfen",
"admin.linkTarget": "Verknüpfen mit",

View File

@@ -170,7 +170,7 @@
<th data-i18n="admin.playerUsername">Username</th>
<th data-i18n="admin.playerStatus">Status</th>
<th data-i18n="admin.playerGames">Games</th>
<th data-i18n="admin.playerJoker">Back pass</th>
<th data-i18n="admin.playerJoker">Joker</th>
<th data-i18n="admin.playerDelete">Delete</th>
</tr>
</thead>
@@ -178,7 +178,10 @@
</table>
</div>
</div>
<button id="results-open-toggle" class="secondary" type="button" data-i18n="admin.resultsOpenButtonEnable">Allow results phase</button>
<label class="stack toggle-row">
<input type="checkbox" id="results-open" />
<span data-i18n="admin.resultsOpenToggle">Allow results phase</span>
</label>
<div class="stack hidden" id="admin-linker">
<h4 data-i18n="admin.linkTitle">Link games</h4>
<label class="stack">

View File

@@ -15,27 +15,6 @@ function displayPlayerStatus(player) {
return phase;
}
function renderStatusSelect(player) {
const statusText = displayPlayerStatus(player);
const safeStatusText = escapeHtml(statusText);
const playerId = escapeHtml(player.playerId);
if (player.phase === "Vote") {
return `
<select class="admin-status-select" data-player-phase="${playerId}" data-current-phase="Vote">
<option value="Vote" selected>${safeStatusText}</option>
<option value="Suggest">${escapeHtml(t("admin.statusSuggesting"))}</option>
</select>
`;
}
return `
<select class="admin-status-select" disabled data-player-phase="${playerId}" data-current-phase="${escapeHtml(player.phase)}">
<option value="${escapeHtml(player.phase)}" selected>${safeStatusText}</option>
</select>
`;
}
export function renderAdminVoteStatus() {
if (!state.me?.isAdmin) return;
const statusBadge = $("admin-ready-status");
@@ -45,13 +24,14 @@ export function renderAdminVoteStatus() {
table.innerHTML = "";
state.adminVoteStatus.voters.forEach((v) => {
const tr = document.createElement("tr");
const statusText = displayPlayerStatus(v);
const gamesTooltip = escapeHtml((v.suggestionTitles || []).join(", "));
const nameText = escapeHtml(truncate(v.name, 28));
const userText = escapeHtml(truncate(v.username, 24));
tr.innerHTML = `
<td title="${escapeHtml(v.name)}">${nameText}</td>
<td class="muted small" title="${escapeHtml(v.username)}">${userText}</td>
<td>${renderStatusSelect(v)}</td>
<td>${statusText}</td>
<td title="${gamesTooltip}">${v.suggestionCount ?? 0}</td>
<td><button class="chip" data-grant-joker="${v.playerId}" type="button">${v.hasJoker ? "🎟" : t("admin.grantJokerChip")}</button></td>
<td><button class="chip danger-chip" data-delete-player="${v.playerId}" data-name="${v.name}" type="button">✕</button></td>

View File

@@ -56,11 +56,10 @@ export const api = {
export const adminApi = {
setResultsOpen: (resultsOpen) => request("/api/admin/results", { method: "POST", body: { resultsOpen } }),
voteStatus: () => request("/api/admin/vote-status"),
reset: (adminPassword) => request("/api/admin/reset", { method: "POST", body: { adminPassword } }),
factoryReset: (adminPassword) => request("/api/admin/factory-reset", { method: "POST", body: { adminPassword } }),
reset: () => request("/api/admin/reset", { method: "POST" }),
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
grantJoker: (playerId) => request("/api/admin/joker", { method: "POST", body: { playerId } }),
deletePlayer: (playerId, adminPassword) => request(`/api/admin/players/${playerId}`, { method: "DELETE", body: { adminPassword } }),
setPlayerPhase: (playerId, phase) => request(`/api/admin/players/${playerId}/phase`, { method: "POST", body: { phase } }),
deletePlayer: (playerId) => request(`/api/admin/players/${playerId}`, { method: "DELETE" }),
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
unlinkSuggestions: (suggestionId) =>

View File

@@ -3,7 +3,7 @@ import { t } from "./i18n.js";
import { state } from "./state.js";
import { $, toast } from "./dom.js";
import {
openPasswordConfirmModal,
openConfirmModal,
openResultsRelockModal,
renderPhasePill,
} from "./ui.js";
@@ -13,10 +13,8 @@ async function adminAction(fn, successMessage, runSerializedRefresh) {
await fn();
toast(successMessage);
await runSerializedRefresh();
return true;
} catch (err) {
toast(err.message, true);
return false;
}
}
@@ -34,56 +32,24 @@ function setupAdminPanelToggle() {
}
function setupResetButtons(runSerializedRefresh) {
const askPasswordThenRun = ({
title,
body,
confirmLabel,
action,
done,
}) => {
openPasswordConfirmModal({
title,
body,
confirmLabel,
onConfirm: async (password, close) => {
const success = await adminAction(
() => action(password),
done,
runSerializedRefresh,
);
if (success) close();
},
});
};
$("reset").addEventListener("click", () =>
askPasswordThenRun({
title: t("admin.reset"),
body: t("admin.resetConfirmBody"),
confirmLabel: t("admin.reset"),
action: (password) => adminApi.reset(password),
done: t("admin.resetDone"),
}),
adminAction(adminApi.reset, t("admin.resetDone"), runSerializedRefresh),
);
$("factory-reset").addEventListener("click", () =>
askPasswordThenRun({
title: t("admin.factoryReset"),
body: t("admin.factoryResetConfirmBody"),
confirmLabel: t("admin.factoryReset"),
action: (password) => adminApi.factoryReset(password),
done: t("admin.factoryResetDone"),
}),
adminAction(
adminApi.factoryReset,
t("admin.factoryResetDone"),
runSerializedRefresh,
),
);
}
function setupResultsToggle(runSerializedRefresh) {
const resultsToggle = $("results-open-toggle");
const resultsToggle = $("results-open");
if (!resultsToggle) return;
resultsToggle.addEventListener("click", async () => {
const desired = !state.resultsOpen;
resultsToggle.disabled = true;
resultsToggle.addEventListener("change", async (e) => {
const desired = !!e.target.checked;
try {
const resp = await adminApi.setResultsOpen(desired);
const wasResultsOpen = state.resultsOpen;
@@ -96,9 +62,8 @@ function setupResultsToggle(runSerializedRefresh) {
toast(t("admin.resultsUpdated"));
await runSerializedRefresh();
} catch (err) {
e.target.checked = !desired;
toast(err.message, true);
} finally {
resultsToggle.disabled = false;
}
});
}
@@ -127,45 +92,6 @@ function setupPlayerTableActions(runSerializedRefresh) {
const playerTable = $("admin-player-table");
if (!playerTable) return;
const syncSelectFocusState = () => {
state.adminStatusMenuOpen = !!playerTable.querySelector(
".admin-status-select:focus",
);
};
playerTable.addEventListener("focusin", (e) => {
if (e.target.closest(".admin-status-select")) {
state.adminStatusMenuOpen = true;
}
});
playerTable.addEventListener("focusout", () => {
window.setTimeout(syncSelectFocusState, 0);
});
playerTable.addEventListener("change", async (e) => {
const statusSelect = e.target.closest(".admin-status-select");
if (!statusSelect || statusSelect.disabled) return;
const playerId = statusSelect.dataset.playerPhase;
const currentPhase = statusSelect.dataset.currentPhase;
const desiredPhase = statusSelect.value;
if (!playerId || !desiredPhase || desiredPhase === currentPhase) return;
statusSelect.disabled = true;
try {
await adminApi.setPlayerPhase(playerId, desiredPhase);
toast(t("admin.statusUpdated"));
await runSerializedRefresh();
} catch (err) {
statusSelect.value = currentPhase ?? statusSelect.value;
toast(err.message, true);
} finally {
statusSelect.disabled = false;
state.adminStatusMenuOpen = false;
}
});
playerTable.addEventListener("click", async (e) => {
const grantBtn = e.target.closest("[data-grant-joker]");
const deleteBtn = e.target.closest("[data-delete-player]");
@@ -181,13 +107,13 @@ function setupPlayerTableActions(runSerializedRefresh) {
} else if (deleteBtn) {
const playerId = deleteBtn.dataset.deletePlayer;
const name = deleteBtn.dataset.name || "";
openPasswordConfirmModal({
openConfirmModal({
title: t("admin.deleteTitle"),
body: t("admin.deleteBody", { name }),
confirmLabel: t("admin.deleteConfirm"),
onConfirm: async (password, close) => {
onConfirm: async (close) => {
try {
await adminApi.deletePlayer(playerId, password);
await adminApi.deletePlayer(playerId);
toast(t("admin.deleteDone"));
close();
await runSerializedRefresh();

View File

@@ -1,11 +1,6 @@
import { api } from "./api.js";
import { t } from "./i18n.js";
import {
state,
clearUserState,
clearSavedUsername,
setSavedUsername,
} from "./state.js";
import { state, clearUserState, setSavedUsername } from "./state.js";
import { $, toast } from "./dom.js";
import {
handleAuthError,
@@ -144,38 +139,24 @@ function setupLogoutHandler() {
const logoutBtn = $("logout");
if (!logoutBtn) return;
const clearAuthFormFields = () => {
[
"login-username",
"login-password",
"register-username",
"register-password",
"register-displayName",
"register-adminkey",
].forEach((id) => {
const input = $(id);
if (input) input.value = "";
});
["login-consent", "register-consent"].forEach((id) => {
const box = $(id);
if (box) box.checked = false;
});
};
logoutBtn.addEventListener("click", async (e) => {
e.preventDefault();
const lastUser = state.me?.username;
try {
await api.logout();
} catch (err) {
toast(err.message, true);
}
clearUserState();
clearSavedUsername();
state.isAuthenticated = false;
setAuthMode("login");
setAuthUI(false);
clearAuthFormFields();
if (lastUser) {
setSavedUsername(lastUser);
const loginUser = $("login-username");
if (loginUser) loginUser.value = lastUser;
const loginPass = $("login-password");
if (loginPass) loginPass.value = "";
}
});
}

View File

@@ -34,15 +34,7 @@ export async function loadSuggestionsData() {
const latest = await api.allSuggestions();
const latestSig = signatureSuggestions(latest);
const changed = latestSig !== state.allSuggestionsSig;
const canCompareWithDisplayedVoteList =
state.phase === "Vote" &&
state.votesRendered &&
!!state.displayedVoteSuggestionsSig;
if (
changed &&
canCompareWithDisplayedVoteList &&
latestSig !== state.displayedVoteSuggestionsSig
) {
if (changed && state.phase === "Vote" && state.allSuggestionsSig) {
const added = latest
.filter((s) => !prevById[s.id])
.map((s) => s.name);

View File

@@ -80,84 +80,6 @@ export function openConfirmModal({
document.body.appendChild(overlay);
}
export function openPasswordConfirmModal({
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 close = () => overlay.remove();
const bodyWrap = panel.querySelector(".edit-body");
const fieldWrap = document.createElement("label");
fieldWrap.className = "stack";
fieldWrap.innerHTML = `
<span class="label">${t("admin.passwordLabel")}</span>
<input type="password" autocomplete="current-password" />
`;
bodyWrap?.appendChild(fieldWrap);
const passwordInput = fieldWrap.querySelector("input");
const actions = document.createElement("div");
actions.className = "stack horizontal";
const confirmBtn = document.createElement("button");
confirmBtn.className = "danger";
confirmBtn.textContent = confirmLabel ?? t("modal.confirm");
actions.append(confirmBtn);
if (cancelLabel !== null && cancelLabel !== undefined) {
const cancelBtn = document.createElement("button");
cancelBtn.className = "ghost";
cancelBtn.type = "button";
cancelBtn.textContent = cancelLabel;
actions.append(cancelBtn);
cancelBtn.addEventListener("click", close);
}
bodyWrap?.appendChild(actions);
overlay.addEventListener("click", (e) => {
if (
e.target.classList.contains("edit-modal") ||
e.target.classList.contains("lightbox-close")
) {
close();
}
});
confirmBtn.addEventListener("click", async () => {
const password = passwordInput?.value ?? "";
if (!password.trim()) {
toast(t("admin.passwordRequired"), true);
passwordInput?.focus();
return;
}
try {
await onConfirm?.(password, close);
} catch (err) {
toast(err.message, true);
}
});
overlay.appendChild(panel);
document.body.appendChild(overlay);
passwordInput?.focus();
}
export function openResultsRelockModal() {
openConfirmModal({
title: t("results.relockedTitle"),

View File

@@ -11,12 +11,10 @@ export const state = {
mySuggestions: [],
allSuggestions: [],
allSuggestionsSig: null,
displayedVoteSuggestionsSig: null,
myVotes: [],
results: [],
votesRendered: false,
adminVoteStatus: null,
adminStatusMenuOpen: false,
};
export function clearUserState() {
@@ -29,13 +27,9 @@ export function clearUserState() {
state.counts = null;
state.mySuggestions = [];
state.allSuggestions = [];
state.allSuggestionsSig = null;
state.displayedVoteSuggestionsSig = null;
state.myVotes = [];
state.results = [];
state.votesRendered = false;
state.adminVoteStatus = null;
state.adminStatusMenuOpen = false;
const adminCard = document.getElementById("admin-card");
if (adminCard) adminCard.classList.add("hidden");
}
@@ -44,5 +38,3 @@ export const getSavedUsername = () =>
localStorage.getItem("last_username") || "";
export const setSavedUsername = (name) =>
localStorage.setItem("last_username", name);
export const clearSavedUsername = () =>
localStorage.removeItem("last_username");

View File

@@ -27,7 +27,6 @@ import { renderResults } from "./results-ui.js";
import {
openConfirmModal,
openLightbox,
openPasswordConfirmModal,
openResultsRelockModal,
openSuggestionsChangedModal,
} from "./modals-ui.js";
@@ -65,7 +64,6 @@ export {
openConfirmModal,
openLightbox,
openNewSuggestionModal,
openPasswordConfirmModal,
openResultsRelockModal,
openSuggestionsChangedModal,
renderAllSuggestions,

View File

@@ -42,7 +42,6 @@ export function renderVotes() {
li.querySelector(".card-body").appendChild(footer);
list.appendChild(li);
});
state.displayedVoteSuggestionsSig = state.allSuggestionsSig;
updatePhaseNav();
updateMissingBadgeFromDom();
list.scrollTop = prevScroll;
@@ -203,11 +202,9 @@ export function updatePhaseNav() {
showNav("nav-suggest", phase === "Suggest");
showNav("nav-vote", phase === "Vote");
const playerCanMoveBackToSuggest =
!isAdmin && phase === "Vote" && state.hasJoker;
const jokerBtn = $("open-joker-modal");
if (jokerBtn) {
const showJoker = false;
const showJoker = phase === "Vote" && state.hasJoker;
jokerBtn.classList.toggle("hidden", !showJoker);
jokerBtn.disabled = !showJoker;
}
@@ -245,14 +242,11 @@ export function updatePhaseNav() {
renderAdminLinker();
updateMissingBadgeFromDom();
const votePrev = $("nav-vote-prev");
if (votePrev) {
const canUseBack = isAdmin || playerCanMoveBackToSuggest;
votePrev.classList.toggle("hidden", !canUseBack);
votePrev.textContent = playerCanMoveBackToSuggest
? t("nav.backToSuggestOnce")
: t("nav.prev");
}
const backButtons = ["nav-vote-prev"];
backButtons.forEach((id) => {
const btn = $(id);
if (btn) btn.classList.toggle("hidden", !isAdmin);
});
const suggestNext = $("nav-suggest-next");
if (suggestNext) {
@@ -274,10 +268,8 @@ export function updatePhaseNav() {
: t("nav.next");
}
const adminResultsToggle = $("results-open-toggle");
const adminResultsToggle = $("results-open");
if (adminResultsToggle) {
adminResultsToggle.textContent = state.resultsOpen
? t("admin.resultsOpenButtonDisable")
: t("admin.resultsOpenButtonEnable");
adminResultsToggle.checked = !!state.resultsOpen;
}
}