From abd072082124b8d0f0e5eec2cfec81d2c643edb0 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Wed, 28 Jan 2026 15:37:11 +0100 Subject: [PATCH] UX overhaul: card layout, admin overlay, status bar, and phase-safe inputs --- wwwroot/app.js | 93 +++++++++++++++++----------- wwwroot/index.html | 97 +++++++++++++++-------------- wwwroot/styles.css | 148 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 230 insertions(+), 108 deletions(-) diff --git a/wwwroot/app.js b/wwwroot/app.js index f979e44..a6fd26c 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -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 = `${s.name}${s.genre ? ` · ${s.genre}` : ""}
${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 = `${s.name} by ${s.author || "Anonymous"}${s.genre ? ` · ${s.genre}` : ""}
${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 = ` -
-
- ${s.name} by ${s.author || "Anonymous"} -
-
- - ${current} -
-
`; + const footer = document.createElement("div"); + footer.className = "vote-controls"; + footer.innerHTML = ` + + ${current}`; + 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 = `${r.name} — ${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 = ` +
+
+
+

${s.name}

+ ${showAuthor && s.author ? `${s.author}` : ""} +
+ ${s.genre ? `

${s.genre}

` : ""} + ${s.description ? `

${s.description}

` : ""} + ${s.youtubeUrl ? `YouTube ↗` : ""} +
+ `; + return card; +} + async function main() { setupHandlers(); try { diff --git a/wwwroot/index.html b/wwwroot/index.html index c6ae4dd..f4b942d 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -7,34 +7,29 @@ -
-
-

CoopGameChooser

-

Blind suggestions, blind votes, quick decision.

-
-
Loading…
-
+
+ + Loading… + +
-
-

Your Name

- - -

-
- -
-

Current Phase

-

Loading…

-

-
-
-
    +
    -
    - -
    -

    Admin

    - -
    - - -
    -
    - - +
    + + + diff --git a/wwwroot/styles.css b/wwwroot/styles.css index e1be2bb..5716469 100644 --- a/wwwroot/styles.css +++ b/wwwroot/styles.css @@ -1,43 +1,42 @@ :root { font-family: "Segoe UI", system-ui, -apple-system, sans-serif; - background: radial-gradient(circle at 20% 20%, #1f2937, #0b1224); + background: radial-gradient(circle at 20% 20%, #0f172a, #050816); color: #e5e7eb; } body { margin: 0; - padding: 24px; + padding: 20px; } -.hero { +.status-bar { display: flex; - justify-content: space-between; align-items: center; - background: linear-gradient(135deg, #111827, #0f172a); + gap: 10px; + padding: 10px 12px; + background: rgba(15, 23, 42, 0.8); border: 1px solid #1f2937; - border-radius: 12px; - padding: 16px 20px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); + border-radius: 10px; + box-shadow: 0 10px 24px rgba(0,0,0,0.25); + max-width: 540px; } -.subtitle { - margin: 4px 0 0; +.status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 10px #22c55e; +} + +.counts { color: #9ca3af; -} - -.phase-pill { - padding: 8px 12px; - background: #1d4ed8; - color: white; - border-radius: 999px; - font-weight: 600; - min-width: 96px; - text-align: center; + font-size: 13px; } .grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + grid-template-columns: 1fr; gap: 16px; margin-top: 16px; } @@ -55,6 +54,23 @@ body { margin-bottom: 8px; } +.split { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + flex-wrap: wrap; +} + +.name-box { + min-width: 220px; + max-width: 260px; + background: #0b1224; + border: 1px solid #1f2937; + padding: 10px; + border-radius: 10px; +} + .stack { display: flex; flex-direction: column; @@ -92,16 +108,70 @@ button.danger { border-color: #b91c1c; } +button.ghost { + background: transparent; + border-color: #374151; +} + .label { color: #9ca3af; font-size: 12px; } .hint { color: #9ca3af; font-size: 12px; margin: 8px 0 0; } -.list { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-direction: column; gap: 10px; } -.list li { padding: 12px; border: 1px solid #1f2937; border-radius: 8px; background: #0b1224; } +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 12px; + margin-top: 12px; +} -.vote-row { display: flex; justify-content: space-between; gap: 12px; align-items: center; } -.vote-controls { display: flex; gap: 10px; align-items: center; min-width: 180px; } +.game-card { + background: #0b1224; + border: 1px solid #1f2937; + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 220px; +} + +.card-visual { + height: 140px; + background: linear-gradient(135deg, #1d4ed8, #22c55e); + background-size: cover; + background-position: center; +} + +.card-body { + padding: 12px; + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; +} + +.card-title-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.card-title-row h3 { margin: 0; font-size: 18px; } +.muted { color: #9ca3af; margin: 0; } +.link { color: #93c5fd; text-decoration: none; font-weight: 600; } +.link:hover { text-decoration: underline; } +.chip { + background: #1f2937; + color: #e5e7eb; + padding: 4px 8px; + border-radius: 999px; + font-size: 12px; +} + +.vote-controls { display: flex; gap: 10px; align-items: center; margin-top: 6px; } .score { font-weight: 700; } +.results-grid .game-card { border-color: #2563eb44; } + .hidden { display: none !important; } .toast { @@ -116,3 +186,31 @@ button.danger { max-width: 320px; } .toast.error { background: #dc2626; } + +.admin-toggle { + position: fixed; + bottom: 18px; + right: 18px; + width: 44px; + height: 44px; + border-radius: 50%; + border: 1px solid #1f2937; + background: #0f172a; + color: #9ca3af; + font-weight: 700; + box-shadow: 0 8px 20px rgba(0,0,0,0.35); +} + +.admin-panel { + position: fixed; + bottom: 70px; + right: 18px; + width: 320px; + z-index: 20; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; +}