UX overhaul: card layout, admin overlay, status bar, and phase-safe inputs

This commit is contained in:
2026-01-28 15:37:11 +01:00
parent 16e7067902
commit abd0720821
3 changed files with 230 additions and 108 deletions

View File

@@ -49,10 +49,12 @@ async function loadState() {
renderPhasePill();
renderCounts();
const nameInput = $("name-input");
if (!nameInput.dataset.userEditing) {
if (nameInput && !nameInput.dataset.userEditing) {
nameInput.value = me.displayName || "";
}
$("player-id").textContent = `Player ID: ${me.id}`;
if ($("player-id")) {
$("player-id").textContent = `Player ID: ${me.id}`;
}
}
async function loadSuggestData() {
@@ -92,35 +94,28 @@ function renderPhasePill() {
const id = viewMap[state.phase];
if (id) $(id).classList.remove("hidden");
const phaseSelect = $("phase-select");
if (!phaseSelect.dataset.userEditing) {
if (phaseSelect && !phaseSelect.dataset.userEditing) {
phaseSelect.value = state.phase || "Suggest";
}
}
function renderCounts() {
if (!state.counts) return;
$("phase-description").textContent = `Phase: ${state.phase}`;
$("counts").textContent = `Players: ${state.counts.players} • Suggestions: ${state.counts.suggestions} • Votes: ${state.counts.votes}`;
}
function renderMySuggestions() {
const list = $("my-suggestions");
list.innerHTML = "";
state.mySuggestions.forEach((s) => {
const li = document.createElement("li");
li.innerHTML = `<strong>${s.name}</strong>${s.genre ? ` · ${s.genre}` : ""}<br>${s.description || ""}`;
list.appendChild(li);
});
const wrap = $("my-suggestions");
if (!wrap) return;
wrap.innerHTML = "";
state.mySuggestions.forEach((s) => wrap.appendChild(buildCard(s, { showAuthor: false })));
}
function renderAllSuggestions() {
const list = $("all-suggestions");
if (!list) return;
list.innerHTML = "";
state.allSuggestions.forEach((s) => {
const li = document.createElement("li");
li.innerHTML = `<strong>${s.name}</strong> by ${s.author || "Anonymous"}${s.genre ? ` · ${s.genre}` : ""}<br>${s.description || ""}`;
list.appendChild(li);
});
state.allSuggestions.forEach((s) => list.appendChild(buildCard(s, { showAuthor: true })));
}
function renderVotes() {
@@ -128,18 +123,14 @@ function renderVotes() {
list.innerHTML = "";
const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
state.allSuggestions.forEach((s) => {
const li = document.createElement("li");
const li = buildCard(s, { showAuthor: true });
const current = votesMap[s.id] ?? 0;
li.innerHTML = `
<div class="vote-row">
<div>
<strong>${s.name}</strong> by ${s.author || "Anonymous"}
</div>
<div class="vote-controls">
<input type="range" min="0" max="10" value="${current}" data-id="${s.id}">
<span class="score" id="score-${s.id}">${current}</span>
</div>
</div>`;
const footer = document.createElement("div");
footer.className = "vote-controls";
footer.innerHTML = `
<input type="range" min="0" max="10" value="${current}" data-id="${s.id}">
<span class="score" id="score-${s.id}">${current}</span>`;
li.querySelector(".card-body").appendChild(footer);
list.appendChild(li);
});
list.querySelectorAll("input[type=range]").forEach((input) => {
@@ -165,18 +156,26 @@ function renderResults() {
const list = $("results-list");
list.innerHTML = "";
state.results.forEach((r) => {
const li = document.createElement("li");
li.innerHTML = `<strong>${r.name}</strong> — ${r.total} pts (${r.count} votes, avg ${r.average.toFixed(1)})${r.author ? ` · ${r.author}` : ""}`;
list.appendChild(li);
const card = buildCard({
id: r.id,
name: r.name,
genre: `${r.total} pts • ${r.count} votes • avg ${r.average.toFixed(1)}`,
description: r.author ? `By ${r.author}` : "",
screenshotUrl: r.screenshotUrl,
youtubeUrl: r.youtubeUrl
}, { showAuthor: false });
list.appendChild(card);
});
}
function setupHandlers() {
const nameInput = $("name-input");
["focus", "input"].forEach(evt => {
nameInput.addEventListener(evt, () => { nameInput.dataset.userEditing = "1"; });
});
nameInput.addEventListener("blur", () => { nameInput.dataset.userEditing = ""; });
if (nameInput) {
["focus", "input"].forEach(evt => {
nameInput.addEventListener(evt, () => { nameInput.dataset.userEditing = "1"; });
});
nameInput.addEventListener("blur", () => { nameInput.dataset.userEditing = ""; });
}
$("save-name").addEventListener("click", async () => {
const name = nameInput.value.trim();
@@ -232,6 +231,13 @@ function setupHandlers() {
$("reset").addEventListener("click", () => adminAction("/api/admin/reset", "Reset complete"));
$("factory-reset").addEventListener("click", () => adminAction("/api/admin/factory-reset", "Factory reset complete"));
const adminToggle = $("admin-toggle");
const adminCard = $("admin-card");
const adminClose = $("admin-close");
const togglePanel = (show) => adminCard.classList.toggle("hidden", !show);
adminToggle.addEventListener("click", () => togglePanel(!adminCard.classList.contains("hidden")));
adminClose.addEventListener("click", () => togglePanel(false));
}
async function adminAction(path, successMessage) {
@@ -250,6 +256,25 @@ async function refreshPhaseData() {
await Promise.all([loadSuggestData(), loadRevealData(), loadVoteData(), loadResults()]);
}
function buildCard(s, { showAuthor }) {
const card = document.createElement("article");
card.className = "game-card";
const hasImage = !!s.screenshotUrl;
card.innerHTML = `
<div class="card-visual" style="${hasImage ? `background-image:url('${s.screenshotUrl}')` : ''}"></div>
<div class="card-body">
<div class="card-title-row">
<h3>${s.name}</h3>
${showAuthor && s.author ? `<span class="chip">${s.author}</span>` : ""}
</div>
${s.genre ? `<p class="muted">${s.genre}</p>` : ""}
${s.description ? `<p>${s.description}</p>` : ""}
${s.youtubeUrl ? `<a class="link" href="${s.youtubeUrl}" target="_blank" rel="noopener">YouTube ↗</a>` : ""}
</div>
`;
return card;
}
async function main() {
setupHandlers();
try {