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 = `
@@ -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;
}