Polish edit modals across app

This commit is contained in:
larssand
2026-03-15 08:52:20 +01:00
parent d65e4881e0
commit 7c3603f14d
2 changed files with 393 additions and 76 deletions

View File

@@ -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 = `
<div class="panel-row">
<section class="panel">
@@ -2008,6 +2066,29 @@ function renderClasses() {
)}
</div>
</section>
${
editingClass
? `
<div class="modal-overlay" id="classEditModalOverlay">
<div class="modal-card">
<div class="panel-header">
<h3>${t("common.edit")}</h3>
<button class="btn" id="classEditCancel">${t("common.cancel")}</button>
</div>
<form id="classEditForm" class="panel-body form-grid cols-2">
<input name="name" required value="${escapeHtml(editingClass.name)}" placeholder="${t("classes.placeholder")}" />
<p class="form-error" id="classEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
<button class="btn" id="classEditCancelFooter" type="button">${t("common.cancel")}</button>
</div>
</form>
</div>
</div>
`
: ""
}
`;
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("")}
</select>
<input name="transponder" value="${escapeHtml(editingDriver.transponder || "")}" placeholder="${t("drivers.transponder_placeholder")}" />
<p class="form-error" id="driverEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
<button class="btn" id="driverEditCancelFooter" type="button">${t("common.cancel")}</button>
@@ -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 = `
<section class="panel">
<div class="panel-header"><h3>${t("cars.create")}</h3></div>
@@ -2213,6 +2335,35 @@ function renderCars() {
)}
</div>
</section>
${
editingCar
? `
<div class="modal-overlay" id="carEditModalOverlay">
<div class="modal-card">
<div class="panel-header">
<h3>${t("common.edit")}</h3>
<button class="btn" id="carEditCancel">${t("common.cancel")}</button>
</div>
<form id="carEditForm" class="panel-body form-grid cols-3">
<input name="name" required value="${escapeHtml(editingCar.name)}" placeholder="${t("cars.name_placeholder")}" />
<input
name="transponder"
required
value="${escapeHtml(editingCar.transponder || "")}"
placeholder="${t("cars.transponder_placeholder")}"
/>
<p class="form-error" id="carEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
<button class="btn" id="carEditCancelFooter" type="button">${t("common.cancel")}</button>
</div>
</form>
</div>
</div>
`
: ""
}
`;
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) => `<option value="${c.id}">${escapeHtml(c.name)}</option>`)
.join("");
const editingEvent = filteredEvents.find((event) => event.id === selectedEventEditId) || null;
dom.view.innerHTML = `
<section class="panel">
@@ -2315,6 +2498,38 @@ function renderEventWorkspace(mode) {
</section>
<section id="eventManageArea"></section>
${
editingEvent
? `
<div class="modal-overlay" id="eventEditModalOverlay">
<div class="modal-card">
<div class="panel-header">
<h3>${t("common.edit")}</h3>
<button class="btn" id="eventEditCancel">${t("common.cancel")}</button>
</div>
<form id="eventEditForm" class="panel-body form-grid cols-3">
<input name="name" required value="${escapeHtml(editingEvent.name)}" placeholder="${t("events.name_placeholder")}" />
<input name="date" required type="date" value="${escapeHtml(editingEvent.date || "")}" />
<select name="classId">
${state.classes
.map(
(item) =>
`<option value="${item.id}" ${item.id === editingEvent.classId ? "selected" : ""}>${escapeHtml(item.name)}</option>`
)
.join("")}
</select>
<p class="form-error" id="eventEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
<button class="btn" id="eventEditCancelFooter" type="button">${t("common.cancel")}</button>
</div>
</form>
</div>
</div>
`
: ""
}
`;
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) => `<option value="${c.id}">${escapeHtml(c.name)} (${escapeHtml(c.transponder)})</option>`)
.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
? `
<div class="modal-overlay" id="sessionEditModalOverlay">
<div class="modal-card">
<div class="panel-header">
<h3>${t("events.edit_session")}</h3>
<button class="btn" id="sessionEditCancel">${t("common.cancel")}</button>
</div>
<form id="sessionEditForm" class="panel-body form-grid cols-5">
<input name="name" required value="${escapeHtml(editingSession.name)}" placeholder="${t("events.session_name")}" />
<select name="type">
${SESSION_TYPES.map(
(item) => `<option value="${item}" ${item === editingSession.type ? "selected" : ""}>${getSessionTypeLabel(item)}</option>`
).join("")}
</select>
<input name="durationMin" required type="number" min="1" value="${editingSession.durationMin || 5}" />
<select name="startMode">
<option value="mass" ${normalizeStartMode(editingSession.startMode) === "mass" ? "selected" : ""}>${t("events.start_mode_mass")}</option>
<option value="position" ${normalizeStartMode(editingSession.startMode) === "position" ? "selected" : ""}>${t("events.start_mode_position")}</option>
<option value="staggered" ${normalizeStartMode(editingSession.startMode) === "staggered" ? "selected" : ""}>${t("events.start_mode_staggered")}</option>
</select>
<input name="seedBestLapCount" type="number" min="0" step="1" value="${editingSession.seedBestLapCount || 0}" />
<input name="staggerGapSec" type="number" min="0" step="1" value="${editingSession.staggerGapSec || 0}" />
<p class="form-error" id="sessionEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
<button class="btn" id="sessionEditCancelFooter" type="button">${t("common.cancel")}</button>
</div>
</form>
</div>
</div>
`
: ""
}
`;
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();