Add fullscreen suggestion edit UX with admin overrides

This commit is contained in:
2026-01-29 01:28:39 +01:00
parent 1ba7fd5c37
commit 637451b485
5 changed files with 149 additions and 4 deletions

View File

@@ -140,14 +140,16 @@ function renderMySuggestions() {
const wrap = $("my-suggestions");
if (!wrap) return;
wrap.innerHTML = "";
state.mySuggestions.forEach((s) => wrap.appendChild(buildCard(s, { showAuthor: false, allowDelete: true })));
const allowEdit = state.phase === "Suggest" || state.me?.isAdmin;
state.mySuggestions.forEach((s) => wrap.appendChild(buildCard(s, { showAuthor: false, allowDelete: true, allowEdit })));
}
function renderAllSuggestions() {
const list = $("all-suggestions");
if (!list) return;
list.innerHTML = "";
state.allSuggestions.forEach((s) => list.appendChild(buildCard(s, { showAuthor: true })));
const allowEdit = !!state.me?.isAdmin;
state.allSuggestions.forEach((s) => list.appendChild(buildCard(s, { showAuthor: true, allowEdit })));
}
function renderVotes() {
@@ -156,7 +158,7 @@ function renderVotes() {
list.innerHTML = "";
const votesMap = Object.fromEntries(state.myVotes.map((v) => [v.suggestionId, v.score]));
state.allSuggestions.forEach((s) => {
const li = buildCard(s, { showAuthor: true });
const li = buildCard(s, { showAuthor: true, allowEdit: !!state.me?.isAdmin });
const current = votesMap[s.id] ?? 0;
const footer = document.createElement("div");
footer.className = "vote-controls";
@@ -363,7 +365,7 @@ async function refreshPhaseData() {
}
}
function buildCard(s, { showAuthor = false, allowDelete = false }) {
function buildCard(s, { showAuthor = false, allowDelete = false, allowEdit = false }) {
const card = document.createElement("article");
card.className = "game-card";
const hasImage = !!s.screenshotUrl;
@@ -379,6 +381,7 @@ function buildCard(s, { showAuthor = false, allowDelete = false }) {
${s.gameUrl ? `<a class="link compact" href="${s.gameUrl}" target="_blank" rel="noopener">Site ↗</a>` : ""}
${s.youtubeUrl ? `<a class="link compact" href="${s.youtubeUrl}" target="_blank" rel="noopener">YouTube ↗</a>` : ""}
${showAuthor && s.author ? `<span class="chip">${s.author}</span>` : ""}
${allowEdit ? `<button class="chip" data-edit="${s.id}" type="button">Edit</button>` : ""}
${allowDelete ? `<button class="chip danger-chip" data-delete="${s.id}" type="button">Delete</button>` : ""}
</div>
</div>
@@ -390,6 +393,10 @@ function buildCard(s, { showAuthor = false, allowDelete = false }) {
const btn = card.querySelector(".card-visual");
btn.addEventListener("click", () => openLightbox(s.screenshotUrl, s.name));
}
if (allowEdit) {
const editBtn = card.querySelector("[data-edit]");
editBtn?.addEventListener("click", () => openEditModal(s));
}
if (allowDelete) {
const del = card.querySelector("[data-delete]");
del.addEventListener("click", async () => {
@@ -405,6 +412,61 @@ function buildCard(s, { showAuthor = false, allowDelete = false }) {
return card;
}
function openEditModal(s) {
const overlay = document.createElement("div");
overlay.className = "edit-modal";
overlay.innerHTML = `
<div class="edit-panel">
<div class="edit-header">
<h3>Edit game</h3>
<button class="lightbox-close" aria-label="Close">×</button>
</div>
<div class="edit-body">
<form class="stack" id="edit-form">
<input name="name" required maxlength="100" placeholder="Game name *" value="${s.name ?? ""}" />
<input name="genre" maxlength="50" placeholder="Genre" value="${s.genre ?? ""}" />
<textarea name="description" maxlength="500" placeholder="Short description">${s.description ?? ""}</textarea>
<div class="stack horizontal">
<input name="screenshotUrl" maxlength="2048" placeholder="Screenshot URL" value="${s.screenshotUrl ?? ""}" />
<input name="youtubeUrl" maxlength="2048" placeholder="YouTube URL" value="${s.youtubeUrl ?? ""}" />
<input name="gameUrl" maxlength="2048" placeholder="Game website URL" value="${s.gameUrl ?? ""}" />
</div>
<div class="stack horizontal">
<button type="submit">Save changes</button>
<button type="button" class="ghost" id="edit-cancel">Cancel</button>
</div>
</form>
</div>
</div>
`;
const close = () => overlay.remove();
overlay.addEventListener("click", (e) => {
if (e.target.classList.contains("edit-modal") || e.target.classList.contains("lightbox-close")) close();
});
const cancelBtn = overlay.querySelector("#edit-cancel");
cancelBtn?.addEventListener("click", close);
const form = overlay.querySelector("#edit-form");
form?.addEventListener("submit", async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
if (!data.name?.trim()) return toast("Name required", true);
try {
await api.updateSuggestion(s.id, data);
toast("Saved changes");
close();
await refreshPhaseData();
} catch (err) {
if (handleAuthError(err)) return;
toast(err.message, true);
}
});
document.body.appendChild(overlay);
}
function openLightbox(url, title) {
const overlay = document.createElement("div");
overlay.className = "lightbox";

View File

@@ -38,6 +38,7 @@ export const api = {
mySuggestions: () => request("/api/suggestions/mine"),
createSuggestion: (payload) => request("/api/suggestions", { method: "POST", body: payload }),
deleteSuggestion: (id) => request(`/api/suggestions/${id}`, { method: "DELETE" }),
updateSuggestion: (id, payload) => request(`/api/suggestions/${id}`, { method: "PUT", body: payload }),
allSuggestions: () => request("/api/suggestions/all"),
myVotes: () => request("/api/votes/mine"),

View File

@@ -305,6 +305,38 @@ input[type="range"].full-slider::-moz-range-track {
cursor: pointer;
}
.edit-modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 110;
}
.edit-modal .edit-panel {
background: #0b1224;
border: 1px solid #1f2937;
border-radius: 12px;
width: min(960px, 94vw);
max-height: 92vh;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
box-shadow: 0 20px 48px rgba(0,0,0,0.45);
}
.edit-modal .edit-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.edit-modal .edit-body {
overflow: auto;
max-height: 70vh;
}
.panel-header {
display: flex;
justify-content: space-between;