From ea0f8f2e27359bb72cca1fd48aae8ce613bc2b96 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Wed, 4 Feb 2026 21:59:26 +0100 Subject: [PATCH] Lock suggestions after reveal and move per-phase navigation --- Endpoints/StateEndpoints.cs | 11 ++- Endpoints/SuggestEndpoints.cs | 14 ++-- wwwroot/app.js | 98 +++++++++++++++--------- wwwroot/css/components.css | 21 ++++++ wwwroot/css/forms-and-auth.css | 6 ++ wwwroot/index.html | 31 +++++++- wwwroot/js/i18n.js | 12 +++ wwwroot/js/ui.js | 132 ++++++++++++++++++++++++++------- 8 files changed, 252 insertions(+), 73 deletions(-) diff --git a/Endpoints/StateEndpoints.cs b/Endpoints/StateEndpoints.cs index e74c1a4..843cb42 100644 --- a/Endpoints/StateEndpoints.cs +++ b/Endpoints/StateEndpoints.cs @@ -35,10 +35,11 @@ public static class StateEndpoints return Results.Ok(new { player.Id, player.DisplayName, player.Username, player.IsAdmin, player.CurrentPhase }); }); - app.MapPost("/api/me/phase/next", async (HttpContext ctx, AppDbContext db) => + app.MapPost("/api/me/phase/next", async (HttpContext ctx, AppDbContext db, IConfiguration config) => { var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); if (player is null) return Results.Unauthorized(); + var isAdmin = await EndpointHelpers.IsAdmin(ctx, db, config); var next = NextPhase(player.CurrentPhase); var appState = await db.AppState.FirstAsync(); @@ -48,15 +49,21 @@ public static class StateEndpoints return Results.BadRequest(new { error = "Results are locked until the admin enables them." }); } + // Non-admins can only move forward player.CurrentPhase = next; await db.SaveChangesAsync(); return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen }); }); - app.MapPost("/api/me/phase/prev", async (HttpContext ctx, AppDbContext db) => + app.MapPost("/api/me/phase/prev", async (HttpContext ctx, AppDbContext db, IConfiguration config) => { var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); if (player is null) return Results.Unauthorized(); + var isAdmin = await EndpointHelpers.IsAdmin(ctx, db, config); + if (!isAdmin) + { + return Results.BadRequest(new { error = "Only admins can move backward." }); + } player.CurrentPhase = PrevPhase(player.CurrentPhase); await db.SaveChangesAsync(); diff --git a/Endpoints/SuggestEndpoints.cs b/Endpoints/SuggestEndpoints.cs index e34a863..fe4fa64 100644 --- a/Endpoints/SuggestEndpoints.cs +++ b/Endpoints/SuggestEndpoints.cs @@ -14,9 +14,6 @@ public static class SuggestEndpoints { var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db); if (player is null) return Results.Unauthorized(); - var phase = await EndpointHelpers.GetPhase(db, player.Id); - if (phase != Phase.Suggest) - return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); var mine = await db.Suggestions.AsNoTracking() .Where(s => s.PlayerId == player.Id) .Select(s => new @@ -104,7 +101,7 @@ public static class SuggestEndpoints var phase = await EndpointHelpers.GetPhase(db, player.Id); if (!isAdmin && phase != Phase.Suggest) - return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); + return Results.BadRequest(new { error = "Suggestions are frozen; you can no longer delete them." }); var suggestion = isAdmin ? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id) @@ -127,8 +124,7 @@ public static class SuggestEndpoints if (player is null) return Results.Unauthorized(); var phase = await EndpointHelpers.GetPhase(db, player.Id); - if (phase != Phase.Suggest) - return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase); + // Non-admins can edit optional fields after Suggest, but not the name } if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100) @@ -157,7 +153,11 @@ public static class SuggestEndpoints return Results.Unauthorized(); } - suggestion.Name = request.Name.Trim(); + var isSuggestPhase = isAdmin ? true : await EndpointHelpers.GetPhase(db, player?.Id ?? Guid.Empty) == Phase.Suggest; + if (isSuggestPhase || isAdmin) + { + suggestion.Name = request.Name.Trim(); + } suggestion.Genre = EndpointHelpers.TrimTo(request.Genre, 50); suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500); suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048); diff --git a/wwwroot/app.js b/wwwroot/app.js index feb28b9..a21dcef 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -16,6 +16,8 @@ import { renderResults, renderPhaseTitles, openNewSuggestionModal, + updatePhaseNav, + openConfirmModal, } from "./js/ui.js"; import { loadState, @@ -63,6 +65,7 @@ function setupHandlers() { if (state.phase === "Results") { renderResults(); } + updatePhaseNav(); }); const loginForm = $("login-form"); @@ -122,39 +125,7 @@ function setupHandlers() { }); } - 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); - } - }); - } - - 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); - } - }); - } + bindNavButtons(); $("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone"))); $("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone"))); @@ -272,3 +243,64 @@ function setupLanguageSwitchers() { updateLanguageButtons(); } + +function bindNavButtons() { + const makeForward = (id, before) => { + const btn = $(id); + if (!btn) return; + btn.addEventListener("click", async () => { + try { + if (before) { + const proceed = await before(); + if (!proceed) return; + } + 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); + } + }); + }; + + const makeBack = (id) => { + const btn = $(id); + if (!btn) return; + btn.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); + } + }); + }; + + makeForward("nav-suggest-next", async () => { + return await new Promise((resolve) => { + openConfirmModal({ + title: t("nav.freezeModalTitle"), + body: t("nav.freezeModalBody"), + confirmLabel: t("nav.next"), + onConfirm: (close) => { + close(); + resolve(true); + }, + }); + }); + }); + makeForward("nav-reveal-next"); + makeForward("nav-vote-next"); + + makeBack("nav-reveal-prev"); + makeBack("nav-vote-prev"); +} diff --git a/wwwroot/css/components.css b/wwwroot/css/components.css index 20e573c..d4a5eb5 100644 --- a/wwwroot/css/components.css +++ b/wwwroot/css/components.css @@ -135,6 +135,27 @@ button .chip { align-items: center; font-weight: 600; } + +.phase-nav { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.phase-nav .nav-text { + display: flex; + flex-direction: column; + gap: 6px; + color: #4b3a26; +} + +.phase-nav .nav-actions { + display: flex; + gap: 8px; + align-items: center; +} .vote-controls { display: flex; gap: 10px; diff --git a/wwwroot/css/forms-and-auth.css b/wwwroot/css/forms-and-auth.css index 8a12ad4..b841d39 100644 --- a/wwwroot/css/forms-and-auth.css +++ b/wwwroot/css/forms-and-auth.css @@ -22,3 +22,9 @@ justify-content: space-between; gap: 12px; } + +input[readonly].readonly { + background: #f7f3eb; + color: #6f6353; + cursor: not-allowed; +} diff --git a/wwwroot/index.html b/wwwroot/index.html index efbf914..e218f20 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -71,10 +71,7 @@ Logout
- Loading… - -
@@ -102,6 +99,15 @@

Your suggestions

+
+
+