From 7c3603f14d3a6899b3f979d9b327d431a86c6779 Mon Sep 17 00:00:00 2001 From: larssand Date: Sun, 15 Mar 2026 08:52:20 +0100 Subject: [PATCH] Polish edit modals across app --- src/app.js | 462 +++++++++++++++++++++++++++++++++++++++++-------- src/styles.css | 7 + 2 files changed, 393 insertions(+), 76 deletions(-) diff --git a/src/app.js b/src/app.js index 04a25e9..25cf731 100644 --- a/src/app.js +++ b/src/app.js @@ -395,6 +395,11 @@ const TRANSLATIONS = { "validation.missing_tp": "En eller flera tilldelade bilar saknar transponder-ID.", "validation.duplicate_tp": "Dubblett-transponder i session: {ids}.", "validation.invalid_date": "Datum måste vara i format YYYY-MM-DD.", + "validation.invalid_selection": "Välj ett giltigt alternativ.", + "validation.required_name": "Namn måste fyllas i.", + "validation.required_transponder": "Transponder måste fyllas i.", + "validation.required_date": "Datum måste fyllas i.", + "validation.required_duration": "Duration måste vara minst 1 minut.", "edit.class_name": "Redigera klassnamn", "edit.driver_name": "Redigera förarnamn", "edit.driver_class": "Redigera förarklass", @@ -867,6 +872,11 @@ const TRANSLATIONS = { "validation.missing_tp": "One or more assigned cars are missing transponder ID.", "validation.duplicate_tp": "Duplicate transponder(s) in session: {ids}.", "validation.invalid_date": "Date must be in YYYY-MM-DD format.", + "validation.invalid_selection": "Select a valid option.", + "validation.required_name": "Name is required.", + "validation.required_transponder": "Transponder is required.", + "validation.required_date": "Date is required.", + "validation.required_duration": "Duration must be at least 1 minute.", "edit.class_name": "Edit class name", "edit.driver_name": "Edit driver name", "edit.driver_class": "Edit driver class", @@ -974,9 +984,13 @@ let reconnectTimer = null; let backendSyncTimer = null; let appVersionPollTimer = null; let baselineAppVersion = ""; +let selectedClassEditId = null; let selectedLeaderboardKey = null; let selectedGridSessionId = null; let selectedDriverEditId = null; +let selectedCarEditId = null; +let selectedEventEditId = null; +let selectedSessionEditId = null; let quickAddDraft = null; let overlaySyncTimer = null; let overlayRotationTimer = null; @@ -985,6 +999,7 @@ let overlayEvents = []; let lastOverlayLeaderKeyBySession = {}; let lastOverlayTop3BySession = {}; let lastOverlayBestLapByKey = {}; +let activeModalEscapeHandler = null; const backend = { available: false, lastSyncAt: null, @@ -1608,6 +1623,7 @@ function renderNav() { } function renderView() { + clearModalEscapeHandler(); const navMeta = NAV_ITEMS.find((x) => x.id === currentView); dom.pageTitle.textContent = navMeta ? t(navMeta.titleKey) : ""; dom.pageSubtitle.textContent = navMeta ? t(navMeta.subtitleKey) : ""; @@ -1654,6 +1670,47 @@ function renderView() { updateHeaderState(); } +function clearModalEscapeHandler() { + if (activeModalEscapeHandler) { + document.removeEventListener("keydown", activeModalEscapeHandler); + activeModalEscapeHandler = null; + } +} + +function bindModalShell(overlayId, onClose, focusSelector = 'input, select, textarea, button') { + const overlay = document.getElementById(overlayId); + if (!overlay) { + clearModalEscapeHandler(); + return; + } + const focusTarget = overlay.querySelector(focusSelector); + window.setTimeout(() => { + if (focusTarget instanceof HTMLElement) { + focusTarget.focus(); + if (focusTarget instanceof HTMLInputElement) { + focusTarget.select(); + } + } + }, 0); + clearModalEscapeHandler(); + activeModalEscapeHandler = (event) => { + if (event.key === "Escape") { + event.preventDefault(); + onClose(); + } + }; + document.addEventListener("keydown", activeModalEscapeHandler); +} + +function setFormError(errorId, message) { + const errorNode = document.getElementById(errorId); + if (!errorNode) { + return; + } + errorNode.textContent = message || ""; + errorNode.hidden = !message; +} + function updateHeaderState() { const session = getActiveSession(); if (!session) { @@ -1978,6 +2035,7 @@ function statCard(label, value, note) { } function renderClasses() { + const editingClass = state.classes.find((item) => item.id === selectedClassEditId) || null; dom.view.innerHTML = `
@@ -2008,6 +2066,29 @@ function renderClasses() { )}
+ + ${ + editingClass + ? ` + + ` + : "" + } `; document.getElementById("classForm")?.addEventListener("submit", (e) => { @@ -2020,16 +2101,7 @@ function renderClasses() { state.classes.forEach((item) => { document.getElementById(`class-edit-${item.id}`)?.addEventListener("click", () => { - const nextName = prompt(t("edit.class_name"), item.name); - if (nextName === null) { - return; - } - const cleaned = nextName.trim(); - if (!cleaned) { - return; - } - item.name = cleaned; - saveState(); + selectedClassEditId = item.id; renderView(); }); @@ -2039,6 +2111,46 @@ function renderClasses() { renderView(); }); }); + + document.getElementById("classEditCancel")?.addEventListener("click", () => { + selectedClassEditId = null; + renderView(); + }); + + document.getElementById("classEditCancelFooter")?.addEventListener("click", () => { + selectedClassEditId = null; + renderView(); + }); + + document.getElementById("classEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "classEditModalOverlay") { + selectedClassEditId = null; + renderView(); + } + }); + + bindModalShell("classEditModalOverlay", () => { + selectedClassEditId = null; + renderView(); + }); + + document.getElementById("classEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + if (!editingClass) { + return; + } + const form = new FormData(event.currentTarget); + const cleaned = String(form.get("name") || "").trim(); + if (!cleaned) { + setFormError("classEditError", t("validation.required_name")); + return; + } + setFormError("classEditError", ""); + editingClass.name = cleaned; + selectedClassEditId = null; + saveState(); + renderView(); + }); } function renderDrivers() { @@ -2100,6 +2212,7 @@ function renderDrivers() { .join("")} +
@@ -2158,6 +2271,11 @@ function renderDrivers() { } }); + bindModalShell("driverEditModalOverlay", () => { + selectedDriverEditId = null; + renderView(); + }); + document.getElementById("driverEditForm")?.addEventListener("submit", (event) => { event.preventDefault(); if (!editingDriver) { @@ -2168,11 +2286,14 @@ function renderDrivers() { const cleanedClassId = String(form.get("classId") || "").trim(); const cleanedTp = String(form.get("transponder") || "").trim(); if (!cleanedName) { + setFormError("driverEditError", t("validation.required_name")); return; } if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) { + setFormError("driverEditError", t("validation.invalid_selection")); return; } + setFormError("driverEditError", ""); editingDriver.name = cleanedName; editingDriver.classId = cleanedClassId || editingDriver.classId; editingDriver.transponder = cleanedTp; @@ -2183,6 +2304,7 @@ function renderDrivers() { } function renderCars() { + const editingCar = state.cars.find((car) => car.id === selectedCarEditId) || null; dom.view.innerHTML = `

${t("cars.create")}

@@ -2213,6 +2335,35 @@ function renderCars() { )}
+ + ${ + editingCar + ? ` + + ` + : "" + } `; document.getElementById("carForm")?.addEventListener("submit", (e) => { @@ -2229,22 +2380,7 @@ function renderCars() { state.cars.forEach((c) => { document.getElementById(`car-edit-${c.id}`)?.addEventListener("click", () => { - const nextName = prompt(t("edit.car_name"), c.name); - if (nextName === null) { - return; - } - const nextTp = prompt(t("edit.car_transponder"), c.transponder || ""); - if (nextTp === null) { - return; - } - const cleanedName = nextName.trim(); - const cleanedTp = nextTp.trim(); - if (!cleanedName || !cleanedTp) { - return; - } - c.name = cleanedName; - c.transponder = cleanedTp; - saveState(); + selectedCarEditId = c.id; renderView(); }); @@ -2257,6 +2393,52 @@ function renderCars() { renderView(); }); }); + + document.getElementById("carEditCancel")?.addEventListener("click", () => { + selectedCarEditId = null; + renderView(); + }); + + document.getElementById("carEditCancelFooter")?.addEventListener("click", () => { + selectedCarEditId = null; + renderView(); + }); + + document.getElementById("carEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "carEditModalOverlay") { + selectedCarEditId = null; + renderView(); + } + }); + + bindModalShell("carEditModalOverlay", () => { + selectedCarEditId = null; + renderView(); + }); + + document.getElementById("carEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + if (!editingCar) { + return; + } + const form = new FormData(event.currentTarget); + const cleanedName = String(form.get("name") || "").trim(); + const cleanedTp = String(form.get("transponder") || "").trim(); + if (!cleanedName) { + setFormError("carEditError", t("validation.required_name")); + return; + } + if (!cleanedTp) { + setFormError("carEditError", t("validation.required_transponder")); + return; + } + setFormError("carEditError", ""); + editingCar.name = cleanedName; + editingCar.transponder = cleanedTp; + selectedCarEditId = null; + saveState(); + renderView(); + }); } function renderEvents() { @@ -2273,6 +2455,7 @@ function renderEventWorkspace(mode) { const classOptions = state.classes .map((c) => ``) .join(""); + const editingEvent = filteredEvents.find((event) => event.id === selectedEventEditId) || null; dom.view.innerHTML = `
@@ -2315,6 +2498,38 @@ function renderEventWorkspace(mode) {
+ + ${ + editingEvent + ? ` + + ` + : "" + } `; document.getElementById("eventForm")?.addEventListener("submit", (e) => { @@ -2334,26 +2549,7 @@ function renderEventWorkspace(mode) { filteredEvents.forEach((e) => { document.getElementById(`event-edit-${e.id}`)?.addEventListener("click", () => { - const nextName = prompt(t("edit.event_name"), e.name); - if (nextName === null) { - return; - } - const nextDate = prompt(t("edit.event_date"), e.date); - if (nextDate === null) { - return; - } - const cleanedName = nextName.trim(); - const cleanedDate = nextDate.trim(); - if (!cleanedName) { - return; - } - if (!isValidIsoDate(cleanedDate)) { - alert(t("validation.invalid_date")); - return; - } - e.name = cleanedName; - e.date = cleanedDate; - saveState(); + selectedEventEditId = e.id; renderView(); }); @@ -2373,6 +2569,62 @@ function renderEventWorkspace(mode) { renderEventManager(e.id); }); }); + + document.getElementById("eventEditCancel")?.addEventListener("click", () => { + selectedEventEditId = null; + renderView(); + }); + + document.getElementById("eventEditCancelFooter")?.addEventListener("click", () => { + selectedEventEditId = null; + renderView(); + }); + + document.getElementById("eventEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "eventEditModalOverlay") { + selectedEventEditId = null; + renderView(); + } + }); + + bindModalShell("eventEditModalOverlay", () => { + selectedEventEditId = null; + renderView(); + }); + + document.getElementById("eventEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + if (!editingEvent) { + return; + } + const form = new FormData(event.currentTarget); + const cleanedName = String(form.get("name") || "").trim(); + const cleanedDate = String(form.get("date") || "").trim(); + const cleanedClassId = String(form.get("classId") || "").trim(); + if (!cleanedName) { + setFormError("eventEditError", t("validation.required_name")); + return; + } + if (!cleanedDate) { + setFormError("eventEditError", t("validation.required_date")); + return; + } + if (!isValidIsoDate(cleanedDate)) { + setFormError("eventEditError", t("validation.invalid_date")); + return; + } + if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) { + setFormError("eventEditError", t("validation.invalid_selection")); + return; + } + setFormError("eventEditError", ""); + editingEvent.name = cleanedName; + editingEvent.date = cleanedDate; + editingEvent.classId = cleanedClassId || editingEvent.classId; + selectedEventEditId = null; + saveState(); + renderView(); + }); } function renderEventManager(eventId) { @@ -2400,6 +2652,7 @@ function renderEventManager(eventId) { .map((c) => ``) .join(""); const branding = normalizeBrandingConfig(event.branding); + const editingSession = sessions.find((session) => session.id === selectedSessionEditId) || null; const gridSessions = event.mode === "race" ? sessions.filter((session) => normalizeStartMode(session.startMode) === "position") : []; if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) { selectedGridSessionId = ""; @@ -2722,6 +2975,42 @@ function renderEventManager(eventId) { ` : "" } + + ${ + editingSession + ? ` + + ` + : "" + } `; document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => { @@ -2792,36 +3081,7 @@ function renderEventManager(eventId) { sessions.forEach((s) => { document.getElementById(`session-edit-${s.id}`)?.addEventListener("click", () => { - const nextName = prompt(t("events.session_name"), s.name); - if (nextName === null) { - return; - } - const nextDuration = prompt(t("events.duration_placeholder"), String(s.durationMin || 5)); - if (nextDuration === null) { - return; - } - const nextStartMode = prompt( - `${t("events.start_mode")} (mass|position|staggered)`, - String(s.startMode || "mass") - ); - if (nextStartMode === null) { - return; - } - const nextSeedBestLaps = prompt(t("events.seed_best_laps"), String(s.seedBestLapCount || 0)); - if (nextSeedBestLaps === null) { - return; - } - const nextStaggerGapSec = prompt(t("events.stagger_gap_sec"), String(s.staggerGapSec || 0)); - if (nextStaggerGapSec === null) { - return; - } - - s.name = nextName.trim() || s.name; - s.durationMin = Math.max(1, Number(nextDuration) || s.durationMin || 5); - s.startMode = normalizeStartMode(nextStartMode); - s.seedBestLapCount = Math.max(0, Number(nextSeedBestLaps) || 0); - s.staggerGapSec = Math.max(0, Number(nextStaggerGapSec) || 0); - saveState(); + selectedSessionEditId = s.id; renderEventManager(eventId); }); @@ -2863,6 +3123,56 @@ function renderEventManager(eventId) { }); }); + document.getElementById("sessionEditCancel")?.addEventListener("click", () => { + selectedSessionEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("sessionEditCancelFooter")?.addEventListener("click", () => { + selectedSessionEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("sessionEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "sessionEditModalOverlay") { + selectedSessionEditId = null; + renderEventManager(eventId); + } + }); + + bindModalShell("sessionEditModalOverlay", () => { + selectedSessionEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("sessionEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + if (!editingSession) { + return; + } + const form = new FormData(event.currentTarget); + const cleanedName = String(form.get("name") || "").trim(); + const cleanedDuration = Number(form.get("durationMin") || editingSession.durationMin || 5) || 0; + if (!cleanedName) { + setFormError("sessionEditError", t("validation.required_name")); + return; + } + if (cleanedDuration < 1) { + setFormError("sessionEditError", t("validation.required_duration")); + return; + } + setFormError("sessionEditError", ""); + editingSession.name = cleanedName; + editingSession.type = String(form.get("type") || editingSession.type); + editingSession.durationMin = Math.max(1, cleanedDuration); + editingSession.startMode = normalizeStartMode(String(form.get("startMode") || editingSession.startMode || "mass")); + editingSession.seedBestLapCount = Math.max(0, Number(form.get("seedBestLapCount") || 0) || 0); + editingSession.staggerGapSec = Math.max(0, Number(form.get("staggerGapSec") || 0) || 0); + selectedSessionEditId = null; + saveState(); + renderEventManager(eventId); + }); + if (event.mode === "track") { document.getElementById("sponsorRoundsForm")?.addEventListener("submit", (e) => { e.preventDefault(); diff --git a/src/styles.css b/src/styles.css index 8c28819..01ed8b3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -439,6 +439,13 @@ select:focus { flex-wrap: wrap; } +.form-error { + grid-column: 1 / -1; + margin: 0; + color: #ff9b9b; + font-size: 0.9rem; +} + .quick-add-actions { justify-content: flex-end; }