Lock suggestions after reveal and move per-phase navigation
This commit is contained in:
@@ -35,10 +35,11 @@ public static class StateEndpoints
|
|||||||
return Results.Ok(new { player.Id, player.DisplayName, player.Username, player.IsAdmin, player.CurrentPhase });
|
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);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null) return Results.Unauthorized();
|
if (player is null) return Results.Unauthorized();
|
||||||
|
var isAdmin = await EndpointHelpers.IsAdmin(ctx, db, config);
|
||||||
|
|
||||||
var next = NextPhase(player.CurrentPhase);
|
var next = NextPhase(player.CurrentPhase);
|
||||||
var appState = await db.AppState.FirstAsync();
|
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." });
|
return Results.BadRequest(new { error = "Results are locked until the admin enables them." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-admins can only move forward
|
||||||
player.CurrentPhase = next;
|
player.CurrentPhase = next;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return Results.Ok(new { player.CurrentPhase, appState.ResultsOpen });
|
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);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null) return Results.Unauthorized();
|
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);
|
player.CurrentPhase = PrevPhase(player.CurrentPhase);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ public static class SuggestEndpoints
|
|||||||
{
|
{
|
||||||
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
||||||
if (player is null) return Results.Unauthorized();
|
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()
|
var mine = await db.Suggestions.AsNoTracking()
|
||||||
.Where(s => s.PlayerId == player.Id)
|
.Where(s => s.PlayerId == player.Id)
|
||||||
.Select(s => new
|
.Select(s => new
|
||||||
@@ -104,7 +101,7 @@ public static class SuggestEndpoints
|
|||||||
|
|
||||||
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
||||||
if (!isAdmin && phase != Phase.Suggest)
|
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
|
var suggestion = isAdmin
|
||||||
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id)
|
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == id)
|
||||||
@@ -127,8 +124,7 @@ public static class SuggestEndpoints
|
|||||||
if (player is null) return Results.Unauthorized();
|
if (player is null) return Results.Unauthorized();
|
||||||
|
|
||||||
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
var phase = await EndpointHelpers.GetPhase(db, player.Id);
|
||||||
if (phase != Phase.Suggest)
|
// Non-admins can edit optional fields after Suggest, but not the name
|
||||||
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
|
if (string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 100)
|
||||||
@@ -157,7 +153,11 @@ public static class SuggestEndpoints
|
|||||||
return Results.Unauthorized();
|
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.Genre = EndpointHelpers.TrimTo(request.Genre, 50);
|
||||||
suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500);
|
suggestion.Description = EndpointHelpers.TrimTo(request.Description, 500);
|
||||||
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048);
|
suggestion.ScreenshotUrl = EndpointHelpers.TrimTo(request.ScreenshotUrl, 2048);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
renderResults,
|
renderResults,
|
||||||
renderPhaseTitles,
|
renderPhaseTitles,
|
||||||
openNewSuggestionModal,
|
openNewSuggestionModal,
|
||||||
|
updatePhaseNav,
|
||||||
|
openConfirmModal,
|
||||||
} from "./js/ui.js";
|
} from "./js/ui.js";
|
||||||
import {
|
import {
|
||||||
loadState,
|
loadState,
|
||||||
@@ -63,6 +65,7 @@ function setupHandlers() {
|
|||||||
if (state.phase === "Results") {
|
if (state.phase === "Results") {
|
||||||
renderResults();
|
renderResults();
|
||||||
}
|
}
|
||||||
|
updatePhaseNav();
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginForm = $("login-form");
|
const loginForm = $("login-form");
|
||||||
@@ -122,39 +125,7 @@ function setupHandlers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevPhaseBtn = $("prev-phase");
|
bindNavButtons();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone")));
|
$("reset").addEventListener("click", () => adminAction(adminApi.reset, t("admin.resetDone")));
|
||||||
$("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone")));
|
$("factory-reset").addEventListener("click", () => adminAction(adminApi.factoryReset, t("admin.factoryResetDone")));
|
||||||
@@ -272,3 +243,64 @@ function setupLanguageSwitchers() {
|
|||||||
|
|
||||||
updateLanguageButtons();
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,6 +135,27 @@ button .chip {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 600;
|
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 {
|
.vote-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -22,3 +22,9 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[readonly].readonly {
|
||||||
|
background: #f7f3eb;
|
||||||
|
color: #6f6353;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,10 +71,7 @@
|
|||||||
<a id="logout" href="#" class="link inline-link" data-i18n="auth.logout">Logout</a>
|
<a id="logout" href="#" class="link inline-link" data-i18n="auth.logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-center">
|
<div class="status-center">
|
||||||
<button id="prev-phase" class="chip nav-btn" type="button" data-i18n="nav.prev">Back</button>
|
|
||||||
<span id="phase-pill" data-i18n="phase.loading">Loading…</span>
|
<span id="phase-pill" data-i18n="phase.loading">Loading…</span>
|
||||||
<button id="next-phase" class="chip nav-btn" type="button" data-i18n="nav.next">Next</button>
|
|
||||||
<span class="badge warning hidden" id="results-lock" data-i18n="admin.resultsLocked">Results locked by admin</span>
|
|
||||||
<span class="counts" id="counts">—</span>
|
<span class="counts" id="counts">—</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-right">
|
<div class="status-right">
|
||||||
@@ -102,6 +99,15 @@
|
|||||||
<h3 data-i18n="section.mySuggestions">Your suggestions</h3>
|
<h3 data-i18n="section.mySuggestions">Your suggestions</h3>
|
||||||
<div id="my-suggestions" class="card-grid"></div>
|
<div id="my-suggestions" class="card-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card subcard phase-nav" id="nav-suggest">
|
||||||
|
<div class="nav-text">
|
||||||
|
<strong data-i18n="nav.freezeTitle">Ready to reveal?</strong>
|
||||||
|
<p data-i18n="nav.freezeHint">Moving forward will freeze your suggestions. Titles become locked; only extra details stay editable.</p>
|
||||||
|
</div>
|
||||||
|
<div class="nav-actions">
|
||||||
|
<button id="nav-suggest-next" class="primary" data-i18n="nav.next">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="reveal-view" class="phase-view hidden">
|
<div id="reveal-view" class="phase-view hidden">
|
||||||
@@ -109,6 +115,15 @@
|
|||||||
<h2 id="reveal-title" data-i18n="section.allSuggestions">All Suggestions</h2>
|
<h2 id="reveal-title" data-i18n="section.allSuggestions">All Suggestions</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="all-suggestions" class="card-grid"></div>
|
<div id="all-suggestions" class="card-grid"></div>
|
||||||
|
<div class="card subcard phase-nav" id="nav-reveal">
|
||||||
|
<div class="nav-text">
|
||||||
|
<p data-i18n="nav.revealHint">Review all games, then advance to voting when ready.</p>
|
||||||
|
</div>
|
||||||
|
<div class="nav-actions">
|
||||||
|
<button id="nav-reveal-prev" class="ghost" data-i18n="nav.prev">Back</button>
|
||||||
|
<button id="nav-reveal-next" class="primary" data-i18n="nav.next">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="vote-view" class="phase-view hidden">
|
<div id="vote-view" class="phase-view hidden">
|
||||||
@@ -116,6 +131,16 @@
|
|||||||
<h2 id="vote-title" data-i18n="section.vote">Vote 0–10</h2>
|
<h2 id="vote-title" data-i18n="section.vote">Vote 0–10</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="vote-list" class="card-grid"></div>
|
<div id="vote-list" class="card-grid"></div>
|
||||||
|
<div class="card subcard phase-nav" id="nav-vote">
|
||||||
|
<div class="nav-text">
|
||||||
|
<p data-i18n="nav.voteHint">Cast votes for every game to unlock results.</p>
|
||||||
|
<span class="badge warning hidden" id="results-lock" data-i18n="admin.resultsLocked">Results locked by admin</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-actions">
|
||||||
|
<button id="nav-vote-prev" class="ghost" data-i18n="nav.prev">Back</button>
|
||||||
|
<button id="nav-vote-next" class="primary" data-i18n="nav.next">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="results-view" class="phase-view hidden">
|
<div id="results-view" class="phase-view hidden">
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ const translations = {
|
|||||||
"nav.prev": "Back",
|
"nav.prev": "Back",
|
||||||
"nav.next": "Next",
|
"nav.next": "Next",
|
||||||
"nav.waitingForResults": "Waiting…",
|
"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.title": "Suggest games (up to 5)",
|
||||||
"suggest.new": "Add new suggestion",
|
"suggest.new": "Add new suggestion",
|
||||||
@@ -146,6 +152,12 @@ const translations = {
|
|||||||
"nav.prev": "Zurück",
|
"nav.prev": "Zurück",
|
||||||
"nav.next": "Weiter",
|
"nav.next": "Weiter",
|
||||||
"nav.waitingForResults": "Warten…",
|
"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.title": "Schlage Spiele vor (bis zu 5)",
|
||||||
"suggest.new": "Neuen Vorschlag hinzufügen",
|
"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];
|
const id = viewMap[state.phase];
|
||||||
if (id) $(id).classList.remove("hidden");
|
if (id) $(id).classList.remove("hidden");
|
||||||
|
|
||||||
const prevBtn = $("prev-phase");
|
updatePhaseNav();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCounts() {
|
export function renderCounts() {
|
||||||
@@ -125,10 +105,12 @@ export function renderMySuggestions() {
|
|||||||
const wrap = $("my-suggestions");
|
const wrap = $("my-suggestions");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
wrap.innerHTML = "";
|
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) =>
|
state.mySuggestions.forEach((s) =>
|
||||||
wrap.appendChild(
|
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(
|
export function buildCard(
|
||||||
s,
|
s,
|
||||||
{ showAuthor = false, allowDelete = false, allowEdit = false },
|
{ showAuthor = false, allowDelete = false, allowEdit = false, lockTitle = false },
|
||||||
) {
|
) {
|
||||||
const card = document.createElement("article");
|
const card = document.createElement("article");
|
||||||
card.className = "game-card";
|
card.className = "game-card";
|
||||||
@@ -366,6 +348,7 @@ export function buildCard(
|
|||||||
close();
|
close();
|
||||||
await window.refreshPhaseData();
|
await window.refreshPhaseData();
|
||||||
},
|
},
|
||||||
|
lockTitle,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -378,7 +361,7 @@ export function buildCard(
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSuggestionForm(initial = {}) {
|
function buildSuggestionForm(initial = {}, lockTitle = false) {
|
||||||
const form = document.createElement("form");
|
const form = document.createElement("form");
|
||||||
form.className = "stack suggestion-form";
|
form.className = "stack suggestion-form";
|
||||||
form.innerHTML = `
|
form.innerHTML = `
|
||||||
@@ -441,7 +424,13 @@ function buildSuggestionForm(initial = {}) {
|
|||||||
|
|
||||||
const setVal = (name, value) => {
|
const setVal = (name, value) => {
|
||||||
const input = form.querySelector(`[name="${name}"]`);
|
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("name", initial.name ?? "");
|
||||||
setVal("genre", initial.genre ?? "");
|
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");
|
const overlay = document.createElement("div");
|
||||||
overlay.className = "edit-modal";
|
overlay.className = "edit-modal";
|
||||||
const panel = document.createElement("div");
|
const panel = document.createElement("div");
|
||||||
@@ -485,7 +474,7 @@ function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit }) {
|
|||||||
<div class="edit-body"></div>
|
<div class="edit-body"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const form = buildSuggestionForm(initial);
|
const form = buildSuggestionForm(initial, lockTitle);
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "stack horizontal";
|
actions.className = "stack horizontal";
|
||||||
const submitBtn = document.createElement("button");
|
const submitBtn = document.createElement("button");
|
||||||
@@ -565,6 +554,52 @@ export function openLightbox(url, title) {
|
|||||||
document.body.appendChild(overlay);
|
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) {
|
export function normalizeSuggestionForm(formData) {
|
||||||
const obj = Object.fromEntries(formData.entries());
|
const obj = Object.fromEntries(formData.entries());
|
||||||
const parseNum = (v) => {
|
const parseNum = (v) => {
|
||||||
@@ -681,3 +716,44 @@ function isValidImageUrl(url) {
|
|||||||
return false;
|
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