diff --git a/wwwroot/app.js b/wwwroot/app.js
index 97b0d78..feb28b9 100644
--- a/wwwroot/app.js
+++ b/wwwroot/app.js
@@ -122,31 +122,39 @@ function setupHandlers() {
});
}
- $("set-phase").addEventListener("click", async () => {
- const phase = $("phase-select").value;
- try {
- await adminApi.setPhase(phase);
- toast(t("admin.phaseUpdated"));
- state.prevPhase = state.phase;
- state.phase = phase;
- state.votesRendered = false;
- renderPhasePill();
- $("phase-select").dataset.userEditing = "";
- await refreshPhaseData();
- } catch (err) {
- toast(err.message, true);
- }
- });
-
- const phaseSelect = $("phase-select");
- ["focus", "input", "click"].forEach((evt) => {
- phaseSelect.addEventListener(evt, () => {
- phaseSelect.dataset.userEditing = "1";
+ const prevPhaseBtn = $("prev-phase");
+ if (prevPhaseBtn) {
+ prevPhaseBtn.addEventListener("click", async () => {
+ try {
+ const resp = await api.prevPhase();
+ state.prevPhase = state.phase;
+ state.phase = resp.currentPhase;
+ state.resultsOpen = resp.resultsOpen ?? state.resultsOpen;
+ state.votesRendered = false;
+ renderPhasePill();
+ await refreshPhaseData();
+ } catch (err) {
+ toast(err.message, true);
+ }
});
- });
- phaseSelect.addEventListener("blur", () => {
- phaseSelect.dataset.userEditing = "";
- });
+ }
+
+ const nextPhaseBtn = $("next-phase");
+ if (nextPhaseBtn) {
+ nextPhaseBtn.addEventListener("click", async () => {
+ try {
+ const resp = await api.nextPhase();
+ state.prevPhase = state.phase;
+ state.phase = resp.currentPhase;
+ state.resultsOpen = resp.resultsOpen ?? state.resultsOpen;
+ state.votesRendered = false;
+ renderPhasePill();
+ await refreshPhaseData();
+ } catch (err) {
+ toast(err.message, true);
+ }
+ });
+ }
$("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone")));
$("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone")));
@@ -182,6 +190,22 @@ function setupHandlers() {
adminToggle.addEventListener("click", () => togglePanel(adminCard.classList.contains("hidden")));
adminClose.addEventListener("click", () => togglePanel(false));
}
+
+ const resultsToggle = $("results-open");
+ if (resultsToggle) {
+ resultsToggle.addEventListener("change", async (e) => {
+ const desired = !!e.target.checked;
+ try {
+ const resp = await adminApi.setResultsOpen(desired);
+ state.resultsOpen = resp.resultsOpen;
+ renderPhasePill();
+ toast(t("admin.resultsUpdated"));
+ } catch (err) {
+ e.target.checked = !desired;
+ toast(err.message, true);
+ }
+ });
+ }
}
async function adminAction(fn, successMessage) {
diff --git a/wwwroot/css/components.css b/wwwroot/css/components.css
index 68d28fb..20e573c 100644
--- a/wwwroot/css/components.css
+++ b/wwwroot/css/components.css
@@ -108,6 +108,33 @@ button .chip {
border-color: #a83a35;
}
+.nav-btn {
+ min-width: 64px;
+ font-weight: 700;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ border-radius: 999px;
+ font-size: 12px;
+ border: 1px solid transparent;
+}
+
+.badge.warning {
+ background: #fff0d6;
+ color: #7a4a00;
+ border-color: #f0c66b;
+}
+
+.toggle-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ font-weight: 600;
+}
.vote-controls {
display: flex;
gap: 10px;
diff --git a/wwwroot/css/layout.css b/wwwroot/css/layout.css
index 5a12bcb..9867fb9 100644
--- a/wwwroot/css/layout.css
+++ b/wwwroot/css/layout.css
@@ -83,6 +83,10 @@
align-items: center;
gap: 10px;
}
+.status-center {
+ flex-wrap: wrap;
+ justify-content: center;
+}
.logo-mark {
height: 65px;
margin: -10px;
diff --git a/wwwroot/index.html b/wwwroot/index.html
index 10e9492..efbf914 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -71,7 +71,10 @@
Logout
+
Loading…
+
+ Results locked by admin
—
@@ -130,15 +133,10 @@
Admin
-
-
-
-
+
diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js
index b078dcf..f0596ba 100644
--- a/wwwroot/js/api.js
+++ b/wwwroot/js/api.js
@@ -46,10 +46,12 @@ export const api = {
vote: (suggestionId, score) => request("/api/votes", { method: "POST", body: { suggestionId, score } }),
results: () => request("/api/results"),
+ nextPhase: () => request("/api/me/phase/next", { method: "POST" }),
+ prevPhase: () => request("/api/me/phase/prev", { method: "POST" }),
};
export const adminApi = {
- setPhase: (phase) => request("/api/admin/phase", { method: "POST", body: { phase } }),
+ setResultsOpen: (resultsOpen) => request("/api/admin/results", { method: "POST", body: { resultsOpen } }),
reset: () => request("/api/admin/reset", { method: "POST" }),
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
};
diff --git a/wwwroot/js/data.js b/wwwroot/js/data.js
index 8229723..55f53c5 100644
--- a/wwwroot/js/data.js
+++ b/wwwroot/js/data.js
@@ -8,6 +8,7 @@ export async function loadState() {
state.me = me;
state.prevPhase = state.phase;
state.phase = stateData.currentPhase;
+ state.resultsOpen = stateData.resultsOpen;
state.counts = stateData;
if (state.prevPhase !== state.phase && state.phase === "Vote") {
state.votesRendered = false;
@@ -52,7 +53,7 @@ export async function loadVoteData() {
}
export async function loadResults() {
- if (state.phase !== "Results") return;
+ if (state.phase !== "Results" || !state.resultsOpen) return;
state.results = await api.results();
renderResults();
}
diff --git a/wwwroot/js/i18n.js b/wwwroot/js/i18n.js
index 8e01ad2..d35cbe4 100644
--- a/wwwroot/js/i18n.js
+++ b/wwwroot/js/i18n.js
@@ -31,6 +31,10 @@ const translations = {
"counts.format": "Players: {players} • Suggestions: {suggestions} • Votes: {votes}",
+ "nav.prev": "Back",
+ "nav.next": "Next",
+ "nav.waitingForResults": "Waiting…",
+
"suggest.title": "Suggest games (up to 5)",
"suggest.new": "Add new suggestion",
"suggest.addButton": "Suggest a game",
@@ -79,10 +83,11 @@ const translations = {
"admin.title": "Admin",
"admin.tools": "Admin tools",
- "admin.setPhase": "Set 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.phaseUpdated": "Phase updated",
"admin.resetDone": "Reset complete",
"admin.factoryResetDone": "Factory reset complete",
@@ -138,6 +143,10 @@ const translations = {
"counts.format": "Spieler: {players} • Vorschläge: {suggestions} • Stimmen: {votes}",
+ "nav.prev": "Zurück",
+ "nav.next": "Weiter",
+ "nav.waitingForResults": "Warten…",
+
"suggest.title": "Schlage Spiele vor (bis zu 5)",
"suggest.new": "Neuen Vorschlag hinzufügen",
"suggest.addButton": "Spiel vorschlagen",
@@ -186,10 +195,11 @@ const translations = {
"admin.title": "Admin",
"admin.tools": "Admin-Werkzeuge",
- "admin.setPhase": "Phase setzen",
+ "admin.resultsOpenToggle": "Ergebnisse freigeben",
+ "admin.resultsLocked": "Ergebnisse vom Admin gesperrt",
+ "admin.resultsUpdated": "Ergebnisfreigabe aktualisiert",
"admin.reset": "Zurücksetzen (Spieler behalten)",
"admin.factoryReset": "Werkseinstellung",
- "admin.phaseUpdated": "Phase aktualisiert",
"admin.resetDone": "Zurücksetzen abgeschlossen",
"admin.factoryResetDone": "Werkseinstellung abgeschlossen",
diff --git a/wwwroot/js/state.js b/wwwroot/js/state.js
index 4e654fe..34ee56f 100644
--- a/wwwroot/js/state.js
+++ b/wwwroot/js/state.js
@@ -4,6 +4,7 @@ export const state = {
me: null,
phase: null,
prevPhase: null,
+ resultsOpen: false,
counts: null,
mySuggestions: [],
allSuggestions: [],
@@ -17,6 +18,7 @@ export function clearUserState() {
state.me = null;
state.phase = null;
state.prevPhase = null;
+ state.resultsOpen = false;
state.counts = null;
state.mySuggestions = [];
state.allSuggestions = [];
diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js
index d632cb5..6cf53b4 100644
--- a/wwwroot/js/ui.js
+++ b/wwwroot/js/ui.js
@@ -65,7 +65,8 @@ export function handleAuthError(err, clearUserState) {
export function renderPhasePill() {
const phaseKey = typeof state.phase === "string" ? state.phase.toLowerCase() : null;
- $("phase-pill").textContent = phaseKey ? "" : t("phase.loading");
+ 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"),
);
@@ -77,9 +78,27 @@ export function renderPhasePill() {
};
const id = viewMap[state.phase];
if (id) $(id).classList.remove("hidden");
- const phaseSelect = $("phase-select");
- if (phaseSelect && !phaseSelect.dataset.userEditing) {
- phaseSelect.value = state.phase || "Suggest";
+
+ 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;
}
}