UX overhaul: card layout, admin overlay, status bar, and phase-safe inputs
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user