Move suggest form into modal

This commit is contained in:
2026-02-04 14:50:49 +01:00
parent bf1ceba156
commit 0cece5f7a3
5 changed files with 140 additions and 146 deletions

View File

@@ -2,7 +2,6 @@ import { api, adminApi } from "./js/api.js";
import { t, setLanguage, getLanguage, initI18n, onLanguageChange } from "./js/i18n.js";
import { state, clearUserState, getSavedUsername, setSavedUsername } from "./js/state.js";
import { $, toast } from "./js/dom.js";
import { triggerCelebration } from "./js/effects.js";
import {
setAuthUI,
setAuthMode,
@@ -16,7 +15,7 @@ import {
syncVoteScores,
renderResults,
renderPhaseTitles,
normalizeSuggestionForm,
openNewSuggestionModal,
} from "./js/ui.js";
import {
loadState,
@@ -117,24 +116,14 @@ function setupHandlers() {
});
}
$("suggest-form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.target;
const data = normalizeSuggestionForm(new FormData(form));
if (!data.name) return toast(t("toast.nameRequired"), true);
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
return toast(t("toast.invalidImageUrl"), true);
}
try {
await api.createSuggestion(data);
form.reset();
toast(t("toast.suggestionAdded"));
triggerCelebration(form.querySelector("button[type=submit]"));
await loadSuggestData();
} catch (err) {
toast(err.message, true);
}
});
const openSuggestBtn = $("open-suggest-modal");
if (openSuggestBtn) {
openSuggestBtn.addEventListener("click", (e) => {
e.preventDefault();
if (state.phase !== "Suggest") return;
openNewSuggestionModal();
});
}
$("set-phase").addEventListener("click", async () => {
const phase = $("phase-select").value;
@@ -223,16 +212,3 @@ async function main() {
}
main();
function isValidImageUrl(url) {
if (!url) return true;
try {
const u = new URL(url);
const allowed = ["http:", "https:"];
if (!allowed.includes(u.protocol)) return false;
const path = u.pathname.toLowerCase();
return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif"].some((ext) => path.endsWith(ext));
} catch {
return false;
}
}

View File

@@ -86,57 +86,15 @@
<section id="actions-card">
<div id="suggest-view" class="phase-view hidden">
<div class="phase-header">
<h2 data-i18n="suggest.title">Suggest (up to 5)</h2>
<p class="hint" data-i18n="suggest.hint">Only you can see your suggestions until Reveal.</p>
<div class="phase-text">
<h2 data-i18n="suggest.title">Suggest (up to 5)</h2>
<p class="hint" data-i18n="suggest.hint">Only you can see your suggestions until Reveal.</p>
</div>
<button id="open-suggest-modal" class="ghost" data-i18n="suggest.addButton">Suggest a game</button>
</div>
<div class="suggest-grid">
<div class="card">
<h3 data-i18n="suggest.new">Add new suggestion</h2>
<form id="suggest-form" class="stack">
<label class="stack">
<span class="label" data-i18n="form.gameName">Game name *</span>
<input name="name" required maxlength="100" />
</label>
<label class="stack">
<span class="label" data-i18n="form.genre">Genre</span>
<input name="genre" maxlength="50" />
</label>
<label class="stack">
<span class="label" data-i18n="form.description">Description</span>
<textarea name="description" maxlength="500"></textarea>
</label>
<div class="stack">
<span class="label" data-i18n="form.players">Players</span>
<div class="stack horizontal">
<label class="stack">
<span class="label" data-i18n="form.min">Min</span>
<input name="minPlayers" type="number" min="1" max="32" inputmode="numeric" />
</label>
<label class="stack">
<span class="label" data-i18n="form.max">Max</span>
<input name="maxPlayers" type="number" min="1" max="32" inputmode="numeric" />
</label>
</div>
</div>
<label class="stack">
<span class="label" data-i18n="form.screenshot">Screenshot URL</span>
<input name="screenshotUrl" maxlength="2048" />
</label>
<label class="stack">
<span class="label" data-i18n="form.youtube">YouTube URL</span>
<input name="youtubeUrl" maxlength="2048" />
</label>
<label class="stack">
<span class="label" data-i18n="form.gameUrl">Game website URL</span>
<input name="gameUrl" maxlength="2048" />
</label>
<button type="submit" data-i18n="form.submit">Submit</button>
</form>
</div>
<div class="card subcard">
<h3 data-i18n="section.mySuggestions">Your suggestions</h3>
<div id="my-suggestions" class="card-grid"></div>
</div>
<div class="card subcard">
<h3 data-i18n="section.mySuggestions">Your suggestions</h3>
<div id="my-suggestions" class="card-grid"></div>
</div>
</div>

View File

@@ -33,6 +33,7 @@ const translations = {
"suggest.title": "Suggest games (up to 5)",
"suggest.new": "Add new suggestion",
"suggest.addButton": "Suggest a game",
"suggest.hint": "Only you can see your suggestions until Reveal.",
"form.gameName": "Game name *",
"form.genre": "Genre",
@@ -97,6 +98,7 @@ const translations = {
"toast.invalidImageUrl": "Screenshot URL must be http(s) and end with an image file.",
"modal.editTitle": "Edit game",
"modal.addTitle": "Suggest a game",
"modal.save": "Save changes",
"modal.cancel": "Cancel",
"modal.close": "Close",
@@ -137,6 +139,7 @@ const translations = {
"suggest.title": "Schlage Spiele vor (bis zu 5)",
"suggest.new": "Neuen Vorschlag hinzufügen",
"suggest.addButton": "Spiel vorschlagen",
"suggest.hint": "Nur du siehst deine Vorschläge bis zur Enthüllung.",
"form.gameName": "Spielname *",
"form.genre": "Genre",
@@ -201,6 +204,7 @@ const translations = {
"toast.invalidImageUrl": "Screenshot-URL muss mit http(s) beginnen und auf eine Bilddatei enden.",
"modal.editTitle": "Spiel bearbeiten",
"modal.addTitle": "Spiel vorschlagen",
"modal.save": "Änderungen speichern",
"modal.cancel": "Abbrechen",
"modal.close": "Schließen",

View File

@@ -338,7 +338,19 @@ export function buildCard(
}
if (allowEdit) {
const editBtn = card.querySelector("[data-edit]");
editBtn?.addEventListener("click", () => openEditModal(s));
editBtn?.addEventListener("click", () =>
openSuggestionModal({
title: t("modal.editTitle"),
submitLabel: t("modal.save"),
initial: s,
onSubmit: async (data, close) => {
await api.updateSuggestion(s.id, data);
toast(t("toast.savedChanges"));
close();
await window.refreshPhaseData();
},
}),
);
}
if (allowDelete) {
const del = card.querySelector("[data-delete]");
@@ -355,62 +367,90 @@ export function buildCard(
return card;
}
function openEditModal(s) {
function buildSuggestionForm(initial = {}) {
const form = document.createElement("form");
form.className = "stack suggestion-form";
form.innerHTML = `
<label class="stack">
<span class="label" data-i18n="form.gameName">${t("form.gameName")}</span>
<input name="name" required maxlength="100" />
</label>
<label class="stack">
<span class="label" data-i18n="form.genre">${t("form.genre")}</span>
<input name="genre" maxlength="50" />
</label>
<label class="stack">
<span class="label" data-i18n="form.description">${t("form.description")}</span>
<textarea name="description" maxlength="500"></textarea>
</label>
<div class="stack">
<span class="label">${t("form.players")}</span>
<div class="stack horizontal">
<label class="stack">
<span class="label">${t("form.min")}</span>
<input name="minPlayers" type="number" min="1" max="32" inputmode="numeric" />
</label>
<label class="stack">
<span class="label">${t("form.max")}</span>
<input name="maxPlayers" type="number" min="1" max="32" inputmode="numeric" />
</label>
</div>
</div>
<label class="stack">
<span class="label" data-i18n="form.screenshot">${t("form.screenshot")}</span>
<input name="screenshotUrl" maxlength="2048" />
</label>
<label class="stack">
<span class="label" data-i18n="form.youtube">${t("form.youtube")}</span>
<input name="youtubeUrl" maxlength="2048" />
</label>
<label class="stack">
<span class="label" data-i18n="form.gameUrl">${t("form.gameUrl")}</span>
<input name="gameUrl" maxlength="2048" />
</label>
`;
const setVal = (name, value) => {
const input = form.querySelector(`[name="${name}"]`);
if (input) input.value = value ?? "";
};
setVal("name", initial.name ?? "");
setVal("genre", initial.genre ?? "");
const desc = form.querySelector("textarea[name=description]");
if (desc) desc.value = initial.description ?? "";
setVal("minPlayers", initial.minPlayers ?? "");
setVal("maxPlayers", initial.maxPlayers ?? "");
setVal("screenshotUrl", initial.screenshotUrl ?? "");
setVal("youtubeUrl", initial.youtubeUrl ?? "");
setVal("gameUrl", initial.gameUrl ?? "");
return form;
}
function openSuggestionModal({ title, submitLabel, initial = {}, onSubmit }) {
const overlay = document.createElement("div");
overlay.className = "edit-modal";
overlay.innerHTML = `
<div class="edit-panel">
const panel = document.createElement("div");
panel.className = "edit-panel";
panel.innerHTML = `
<div class="edit-header">
<h3>${t("modal.editTitle")}</h3>
<button class="lightbox-close" aria-label="${t("modal.close")}">×</button>
<h3>${title}</h3>
<button class="lightbox-close" aria-label="${t("modal.close")}">x</button>
</div>
<div class="edit-body">
<form class="stack" id="edit-form">
<label class="stack">
<span class="label" data-i18n="form.gameName">${t("form.gameName")}</span>
<input name="name" required maxlength="100" value="${s.name ?? ""}" />
</label>
<label class="stack">
<span class="label" data-i18n="form.genre">${t("form.genre")}</span>
<input name="genre" maxlength="50" value="${s.genre ?? ""}" />
</label>
<label class="stack">
<span class="label" data-i18n="form.description">${t("form.description")}</span>
<textarea name="description" maxlength="500">${s.description ?? ""}</textarea>
</label>
<div class="stack">
<span class="label">${t("form.players")}</span>
<div class="stack horizontal">
<label class="stack">
<span class="label">${t("form.min")}</span>
<input name="minPlayers" type="number" min="1" max="32" inputmode="numeric" value="${s.minPlayers ?? ""}" />
</label>
<label class="stack">
<span class="label">${t("form.max")}</span>
<input name="maxPlayers" type="number" min="1" max="32" inputmode="numeric" value="${s.maxPlayers ?? ""}" />
</label>
</div>
</div>
<label class="stack">
<span class="label" data-i18n="form.screenshot">${t("form.screenshot")}</span>
<input name="screenshotUrl" maxlength="2048" value="${s.screenshotUrl ?? ""}" />
</label>
<label class="stack">
<span class="label" data-i18n="form.youtube">${t("form.youtube")}</span>
<input name="youtubeUrl" maxlength="2048" value="${s.youtubeUrl ?? ""}" />
</label>
<label class="stack">
<span class="label" data-i18n="form.gameUrl">${t("form.gameUrl")}</span>
<input name="gameUrl" maxlength="2048" value="${s.gameUrl ?? ""}" />
</label>
<div class="stack horizontal">
<button type="submit">${t("modal.save")}</button>
<button type="button" class="ghost" id="edit-cancel">${t("modal.cancel")}</button>
</div>
</form>
</div>
</div>
`;
<div class="edit-body"></div>
`;
const form = buildSuggestionForm(initial);
const actions = document.createElement("div");
actions.className = "stack horizontal";
const submitBtn = document.createElement("button");
submitBtn.type = "submit";
submitBtn.textContent = submitLabel;
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "ghost";
cancelBtn.textContent = t("modal.cancel");
actions.append(submitBtn, cancelBtn);
form.appendChild(actions);
const close = () => overlay.remove();
overlay.addEventListener("click", (e) => {
@@ -420,12 +460,9 @@ function openEditModal(s) {
)
close();
});
cancelBtn.addEventListener("click", close);
const cancelBtn = overlay.querySelector("#edit-cancel");
cancelBtn?.addEventListener("click", close);
const form = overlay.querySelector("#edit-form");
form?.addEventListener("submit", async (e) => {
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = normalizeSuggestionForm(new FormData(form));
if (data.screenshotUrl && !isValidImageUrl(data.screenshotUrl)) {
@@ -433,17 +470,31 @@ function openEditModal(s) {
}
if (!data.name?.trim()) return toast(t("toast.nameRequired"), true);
try {
await api.updateSuggestion(s.id, data);
toast(t("toast.savedChanges"));
close();
await window.refreshPhaseData();
await onSubmit(data, close);
} catch (err) {
if (window.handleAuthError(err)) return;
if (window.handleAuthError?.(err)) return;
toast(err.message, true);
}
});
panel.querySelector(".edit-body")?.appendChild(form);
overlay.appendChild(panel);
document.body.appendChild(overlay);
return overlay;
}
export function openNewSuggestionModal() {
openSuggestionModal({
title: t("modal.addTitle") || t("suggest.new"),
submitLabel: t("form.submit"),
initial: {},
onSubmit: async (data, close) => {
await api.createSuggestion(data);
toast(t("toast.suggestionAdded"));
close();
await window.loadSuggestData();
},
});
}
export function openLightbox(url, title) {

View File

@@ -122,6 +122,11 @@ html {
justify-content: space-between;
gap: 12px;
}
.phase-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.phase-header h2 {
margin: 0;
}