From fb9dcf1b9ce6e82a8ed1647ba5a238a14215de60 Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 19 Mar 2026 20:38:58 +0100 Subject: [PATCH] Add broader search, brand exports, and denser overlay --- README.md | 3 ++- README.sv.md | 3 ++- src/app.js | 73 +++++++++++++++++++++++++++++++++----------------- src/styles.css | 68 +++++++++++++++++++++++----------------------- 4 files changed, 86 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index f6f3fc7..1202d2d 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ JMK RB RaceController is an RC timing and race-control system with support for s - Editing in UI: - classes, event names/dates, drivers, and cars can be edited directly - drivers and cars support optional `brand` fields for team, sponsor, manufacturer, or model data - - `Drivers` and `Cars` can be filtered by brand directly in the UI + - `Drivers` and `Cars` can be searched by name, transponder, or brand directly in the UI + - race start lists and heat sheets include driver brand in print, CSV, and PDF exports - Live race control: - countdown during timed sessions - auto-finish at end of session time with `Race is finished` diff --git a/README.sv.md b/README.sv.md index 2b94087..9004cc6 100644 --- a/README.sv.md +++ b/README.sv.md @@ -81,7 +81,8 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he - Redigering i UI: - Klasser, eventnamn/datum, förare och bilar kan redigeras direkt - förare och bilar har valfria brandfält för team, sponsor, märke eller modell - - `Förare` och `Bilar` kan filtreras direkt på brand i UI + - `Förare` och `Bilar` kan sökas på namn, transponder eller brand direkt i UI + - startlistor och heatsheets tar med förarens brand i print, CSV och PDF-export - Live race-kontroll: - Nedräkning under pågående session - Auto-finish vid tidslut med status `Race is finished` diff --git a/src/app.js b/src/app.js index 0bb3f64..3f53a02 100644 --- a/src/app.js +++ b/src/app.js @@ -87,14 +87,14 @@ const TRANSLATIONS = { "drivers.create": "Skapa förare", "drivers.name_placeholder": "Förarnamn", "drivers.brand_placeholder": "Team / märke (valfritt)", - "drivers.brand_filter_placeholder": "Filtrera på team / märke", + "drivers.brand_filter_placeholder": "Sök namn / transponder / brand", "drivers.transponder_placeholder": "Personlig transponder (valfritt)", "drivers.add": "Lägg till förare", "drivers.title": "Förare", "cars.create": "Skapa bil", "cars.name_placeholder": "Bilnamn eller nummer", "cars.brand_placeholder": "Märke / modell (valfritt)", - "cars.brand_filter_placeholder": "Filtrera på märke / modell", + "cars.brand_filter_placeholder": "Sök namn / transponder / brand", "cars.transponder_placeholder": "Bilens transponder", "cars.add": "Lägg till bil", "cars.title": "Bilar", @@ -729,14 +729,14 @@ const TRANSLATIONS = { "drivers.create": "Create Driver", "drivers.name_placeholder": "Driver name", "drivers.brand_placeholder": "Team / brand (optional)", - "drivers.brand_filter_placeholder": "Filter by team / brand", + "drivers.brand_filter_placeholder": "Search name / transponder / brand", "drivers.transponder_placeholder": "Personal transponder (optional)", "drivers.add": "Add Driver", "drivers.title": "Drivers", "cars.create": "Create Track Car", "cars.name_placeholder": "Car name or number", "cars.brand_placeholder": "Brand / model (optional)", - "cars.brand_filter_placeholder": "Filter by brand / model", + "cars.brand_filter_placeholder": "Search name / transponder / brand", "cars.transponder_placeholder": "Car transponder", "cars.add": "Add Car", "cars.title": "Cars", @@ -2841,10 +2841,12 @@ function renderDrivers() { const classOptions = state.classes .map((c) => ``) .join(""); + const driverSearch = driverBrandFilter.trim().toLowerCase(); const filteredDrivers = state.drivers.filter((driver) => - String(driver.brand || "") - .toLowerCase() - .includes(driverBrandFilter.trim().toLowerCase()) + !driverSearch || + [driver.name, driver.transponder, driver.brand] + .map((value) => String(value || "").toLowerCase()) + .some((value) => value.includes(driverSearch)) ); const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId) || null; @@ -3013,10 +3015,12 @@ function renderDrivers() { } function renderCars() { + const carSearch = carBrandFilter.trim().toLowerCase(); const filteredCars = state.cars.filter((car) => - String(car.brand || "") - .toLowerCase() - .includes(carBrandFilter.trim().toLowerCase()) + !carSearch || + [car.name, car.transponder, car.brand] + .map((value) => String(value || "").toLowerCase()) + .some((value) => value.includes(carSearch)) ); const editingCar = state.cars.find((car) => car.id === selectedCarEditId) || null; dom.view.innerHTML = ` @@ -5910,10 +5914,18 @@ function renderOverlayLeaderboard(rows) { +
+ + ${row.laps ?? 0} +
${escapeHtml(row.resultDisplay)}
+
+ + ${escapeHtml(row.gapDisplay || row.gapAhead || "-")} +
${escapeHtml(row.gapAhead || "-")} @@ -8463,16 +8475,18 @@ function buildRaceStartListsHtml(event) { ${ entries.length ? renderTable( - [t("events.slot"), t("table.driver"), t("table.transponder")], - entries.map( - (entry) => ` + [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")], + entries.map((entry) => { + const driver = state.drivers.find((item) => item.id === entry.id); + return ` ${entry.slot} ${escapeHtml(entry.name)} + ${escapeHtml(driver?.brand || "-")} ${escapeHtml(entry.meta || "-")} - ` - ) + `; + }) ) : `

${t("common.no_entries")}

` } @@ -8684,8 +8698,11 @@ async function exportRaceStartListsPdf(event) { const sections = sessions.map((session) => buildPdfSection( `${session.name} • ${getSessionTypeLabel(session.type)}`, - [t("events.slot"), t("table.driver"), t("table.transponder")], - getSessionGridEntries(session).map((entry) => [String(entry.slot), entry.name, entry.meta || "-"]) + [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")], + getSessionGridEntries(session).map((entry) => { + const driver = state.drivers.find((item) => item.id === entry.id); + return [String(entry.slot), entry.name, driver?.brand || "-", entry.meta || "-"]; + }) ) ); @@ -8815,8 +8832,11 @@ async function exportSessionHeatSheetPdf(session) { sections: [ buildPdfSection( `${session.name} • ${getSessionTypeLabel(session.type)}`, - [t("events.slot"), t("table.driver"), t("table.transponder")], - getSessionGridEntries(session).map((entry) => [String(entry.slot), entry.name, entry.meta || "-"]) + [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")], + getSessionGridEntries(session).map((entry) => { + const driver = state.drivers.find((item) => item.id === entry.id); + return [String(entry.slot), entry.name, driver?.brand || "-", entry.meta || "-"]; + }) ), ], }); @@ -8848,16 +8868,18 @@ function buildSessionHeatSheetHtml(session) { ${ entries.length ? renderTable( - [t("events.slot"), t("table.driver"), t("table.transponder")], - entries.map( - (entry) => ` + [t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")], + entries.map((entry) => { + const driver = state.drivers.find((item) => item.id === entry.id); + return ` ${entry.slot} ${escapeHtml(entry.name)} + ${escapeHtml(driver?.brand || "-")} ${escapeHtml(entry.meta || "-")} - ` - ) + `; + }) ) : `

${t("common.no_entries")}

` } @@ -8868,7 +8890,7 @@ function exportSessionHeatSheet(session) { const event = state.events.find((item) => item.id === session.eventId); const entries = getSessionGridEntries(session); const rows = [ - ["event", "class", "session", "type", "start_mode", "duration_min", "slot", "driver", "transponder"], + ["event", "class", "session", "type", "start_mode", "duration_min", "slot", "driver", "brand", "transponder"], ...entries.map((entry) => [ event?.name || "", getClassName(event?.classId || ""), @@ -8878,6 +8900,7 @@ function exportSessionHeatSheet(session) { String(session.durationMin || ""), String(entry.slot), entry.name, + state.drivers.find((item) => item.id === entry.id)?.brand || "", entry.meta || "", ]), ]; diff --git a/src/styles.css b/src/styles.css index 9e2c61f..fd27589 100644 --- a/src/styles.css +++ b/src/styles.css @@ -879,8 +879,8 @@ select:focus { .overlay-board { display: grid; - grid-template-columns: minmax(0, 1.92fr) minmax(180px, 0.31fr); - gap: 8px; + grid-template-columns: minmax(0, 1.98fr) minmax(156px, 0.27fr); + gap: 6px; } .overlay-board-tv { @@ -982,7 +982,7 @@ select:focus { } .overlay-table-wrap { - padding: 4px 6px 6px; + padding: 3px 5px 5px; } .overlay-display-wrap { @@ -1002,8 +1002,8 @@ select:focus { display: flex; justify-content: space-between; align-items: end; - gap: 8px; - padding: 7px 10px; + gap: 6px; + padding: 6px 8px; background: linear-gradient(135deg, rgba(225, 6, 0, 0.18), rgba(225, 6, 0, 0.04)), rgba(7, 12, 20, 0.92); @@ -1018,13 +1018,13 @@ select:focus { .overlay-fastest-banner strong { display: block; - margin-top: 2px; + margin-top: 1px; font-family: Orbitron, sans-serif; - font-size: clamp(1.02rem, 1.7vw, 1.45rem); + font-size: clamp(0.94rem, 1.45vw, 1.26rem); } .overlay-leaderboard-card { - padding: 5px; + padding: 4px; } .overlay-leaderboard-card-tv { @@ -1045,13 +1045,13 @@ select:focus { } .overlay-fastest-driver { - font-size: 0.76rem; + font-size: 0.68rem; text-align: right; } .overlay-fastest-meta { color: var(--muted); - font-size: 0.58rem; + font-size: 0.52rem; text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; @@ -1063,14 +1063,14 @@ select:focus { .overlay-race-metric strong, .overlay-race-best strong { - font-size: clamp(0.78rem, 0.98vw, 0.92rem); - line-height: 1.05; + font-size: clamp(0.72rem, 0.88vw, 0.84rem); + line-height: 1.02; } .overlay-race-pos .pos-pill { - min-width: 26px; - height: 26px; - font-size: 0.76rem; + min-width: 22px; + height: 22px; + font-size: 0.68rem; } .overlay-shell-dense .pill { @@ -1123,12 +1123,12 @@ select:focus { .overlay-side { display: grid; - gap: 6px; - font-size: 0.78rem; + gap: 4px; + font-size: 0.72rem; } .overlay-side-card { - padding: 6px; + padding: 5px; } .overlay-rotating-card { @@ -1136,15 +1136,15 @@ select:focus { } .overlay-side-card h3 { - margin: 0 0 3px; - font-size: 0.78rem; + margin: 0 0 2px; + font-size: 0.7rem; } .overlay-passing { display: flex; justify-content: space-between; - gap: 5px; - padding: 3px 0; + gap: 4px; + padding: 2px 0; border-bottom: 1px solid var(--line); } @@ -1153,13 +1153,13 @@ select:focus { } .overlay-side-card .overlay-passing strong { - font-size: 0.72rem; + font-size: 0.66rem; line-height: 1.15; } .overlay-side-card .overlay-passing span { font-family: Orbitron, sans-serif; - font-size: 0.64rem; + font-size: 0.58rem; color: var(--text); white-space: nowrap; } @@ -1171,15 +1171,15 @@ select:focus { .overlay-race-list { display: grid; - gap: 4px; + gap: 3px; } .overlay-race-row { display: grid; - grid-template-columns: 38px minmax(170px, 1.72fr) repeat(3, minmax(90px, 0.62fr)) minmax(95px, 0.66fr); - gap: 6px; + grid-template-columns: 34px minmax(160px, 1.7fr) repeat(4, minmax(78px, 0.54fr)) minmax(86px, 0.58fr); + gap: 5px; align-items: center; - padding: 5px 7px; + padding: 4px 6px; border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 8px; background: rgba(255, 255, 255, 0.03); @@ -1202,15 +1202,15 @@ select:focus { } .overlay-race-driver strong { - font-size: clamp(0.82rem, 1.04vw, 0.96rem); - line-height: 1.05; + font-size: clamp(0.75rem, 0.92vw, 0.88rem); + line-height: 1.02; } .overlay-race-driver span, .overlay-race-metric label, .overlay-race-best label { color: var(--muted); - font-size: 0.54rem; + font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.06em; } @@ -1225,14 +1225,14 @@ select:focus { } .overlay-prediction { - margin-top: 2px; + margin-top: 1px; } .overlay-prediction-meta { display: flex; justify-content: space-between; - gap: 8px; - margin-bottom: 2px; + gap: 6px; + margin-bottom: 1px; } .overlay-prediction-meta label,