From a6e1f4c89c622cb2b35b876b355a43b744b96af4 Mon Sep 17 00:00:00 2001 From: larssand Date: Sun, 15 Mar 2026 14:38:51 +0100 Subject: [PATCH] fix team lag --- src/app.js | 206 +++++++++++++++++++++++++++++++++++++++++++++++-- src/styles.css | 57 ++++++++++++++ 2 files changed, 258 insertions(+), 5 deletions(-) diff --git a/src/app.js b/src/app.js index ca4a92f..60831b9 100644 --- a/src/app.js +++ b/src/app.js @@ -220,6 +220,7 @@ const TRANSLATIONS = { "events.no_teams": "Inga lag skapade ännu.", "events.team_standings": "Lagställning", "events.no_team_results": "Inga teamresultat ännu.", + "events.edit_team": "Redigera lag", "events.add_session": "Lägg till session", "events.set_active": "Sätt aktiv", "events.assignments": "Tilldelningar", @@ -277,6 +278,7 @@ const TRANSLATIONS = { "timing.open_speaker_overlay": "Speaker overlay", "timing.open_results_overlay": "Result overlay", "timing.open_tv_overlay": "TV overlay", + "timing.open_team_overlay": "Team overlay", "timing.close_details": "Stang", "timing.detail_title": "Leaderboard-detaljer", "timing.lap_history": "Varvhistorik", @@ -479,12 +481,16 @@ const TRANSLATIONS = { "overlay.mode_speaker": "Speaker", "overlay.mode_results": "Resultat", "overlay.mode_tv": "TV", + "overlay.mode_team": "Team", "overlay.fastest_lap": "Snabbaste varv", "overlay.fullscreen": "Fullscreen", "overlay.leaderboard_live": "Live leaderboard", "overlay.rotating_panel": "Displaypanel", "overlay.next_predicted_lap": "Nästa varv", "overlay.event_markers": "Eventmarkörer", + "overlay.team_battle": "Lagkamp", + "overlay.active_member": "Aktiv förare/bil", + "overlay.top_three": "Topp 3", "guide.host_title": "Hur Managed AMMC körs", "guide.host_1": "1. AMMC körs alltid på samma maskin som `npm start` eller `node server.js` körs på.", "guide.host_2": "2. Om du bara surfar in från en laptop/webbläsare startas ingen process där. Webbläsaren styr bara backend via HTTP.", @@ -722,6 +728,7 @@ const TRANSLATIONS = { "events.no_teams": "No teams created yet.", "events.team_standings": "Team standings", "events.no_team_results": "No team results yet.", + "events.edit_team": "Edit team", "events.add_session": "Add Session", "events.set_active": "Set Active", "events.assignments": "Assignments", @@ -779,6 +786,7 @@ const TRANSLATIONS = { "timing.open_speaker_overlay": "Speaker overlay", "timing.open_results_overlay": "Results overlay", "timing.open_tv_overlay": "TV overlay", + "timing.open_team_overlay": "Team overlay", "timing.close_details": "Close", "timing.detail_title": "Leaderboard details", "timing.lap_history": "Lap history", @@ -981,12 +989,16 @@ const TRANSLATIONS = { "overlay.mode_speaker": "Speaker", "overlay.mode_results": "Results", "overlay.mode_tv": "TV", + "overlay.mode_team": "Team", "overlay.fastest_lap": "Fastest Lap", "overlay.fullscreen": "Fullscreen", "overlay.leaderboard_live": "Live leaderboard", "overlay.rotating_panel": "Display panel", "overlay.next_predicted_lap": "Next lap", "overlay.event_markers": "Event markers", + "overlay.team_battle": "Team battle", + "overlay.active_member": "Active driver/car", + "overlay.top_three": "Top 3", "guide.host_title": "How Managed AMMC Runs", "guide.host_1": "1. AMMC always runs on the same machine where `npm start` or `node server.js` is running.", "guide.host_2": "2. If you only browse from a laptop/browser, no process is started there. The browser only controls the backend over HTTP.", @@ -1024,7 +1036,7 @@ const TRANSLATIONS = { const urlParams = new URLSearchParams(window.location.search); const overlayMode = urlParams.get("view") === "overlay"; -const overlayViewMode = ["leaderboard", "speaker", "results", "tv"].includes(String(urlParams.get("overlayMode") || "").toLowerCase()) +const overlayViewMode = ["leaderboard", "speaker", "results", "tv", "team"].includes(String(urlParams.get("overlayMode") || "").toLowerCase()) ? String(urlParams.get("overlayMode")).toLowerCase() : "leaderboard"; const state = loadState(); @@ -1041,6 +1053,7 @@ let selectedDriverEditId = null; let selectedCarEditId = null; let selectedEventEditId = null; let selectedSessionEditId = null; +let selectedTeamEditId = null; let quickAddDraft = null; let overlaySyncTimer = null; let overlayRotationTimer = null; @@ -2749,6 +2762,10 @@ function renderEventManager(eventId) { .join(""); const raceDrivers = event.mode === "race" ? state.drivers.filter((driver) => !event.classId || driver.classId === event.classId) : []; const raceTeams = event.mode === "race" ? getEventTeams(event) : []; + if (selectedTeamEditId && !raceTeams.some((team) => team.id === selectedTeamEditId)) { + selectedTeamEditId = null; + } + const editingTeam = event.mode === "race" ? raceTeams.find((team) => team.id === selectedTeamEditId) || null : null; const carOptions = state.cars .map((c) => ``) .join(""); @@ -2992,7 +3009,10 @@ function renderEventManager(eventId) { .join(", ") || "-" )} - +
+ + +
` ) @@ -3163,6 +3183,59 @@ function renderEventManager(eventId) { : "" } + ${ + editingTeam + ? ` + + ` + : "" + } + ${ editingSession ? ` @@ -3490,13 +3563,69 @@ function renderEventManager(eventId) { }); raceTeams.forEach((team) => { + document.getElementById(`team-edit-${team.id}`)?.addEventListener("click", () => { + selectedTeamEditId = team.id; + renderEventManager(eventId); + }); + document.getElementById(`team-delete-${team.id}`)?.addEventListener("click", () => { event.raceConfig.teams = getEventTeams(event).filter((item) => item.id !== team.id); + if (selectedTeamEditId === team.id) { + selectedTeamEditId = null; + } saveState(); renderEventManager(eventId); }); }); + document.getElementById("teamEditCancel")?.addEventListener("click", () => { + selectedTeamEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("teamEditCancelFooter")?.addEventListener("click", () => { + selectedTeamEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("teamEditModalOverlay")?.addEventListener("click", (modalEvent) => { + if (modalEvent.target?.id === "teamEditModalOverlay") { + selectedTeamEditId = null; + renderEventManager(eventId); + } + }); + + bindModalShell("teamEditModalOverlay", () => { + selectedTeamEditId = null; + renderEventManager(eventId); + }); + + document.getElementById("teamEditForm")?.addEventListener("submit", (submitEvent) => { + submitEvent.preventDefault(); + if (!editingTeam) { + return; + } + const form = new FormData(submitEvent.currentTarget); + const name = String(form.get("teamName") || "").trim(); + const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean); + const carIds = form.getAll("teamCarIds").map(String).filter(Boolean); + if (!name) { + setFormError("teamEditError", t("validation.required_name")); + return; + } + if (!driverIds.length && !carIds.length) { + setFormError("teamEditError", t("validation.invalid_selection")); + return; + } + setFormError("teamEditError", ""); + event.raceConfig.teams = getEventTeams(event).map((team) => + team.id === editingTeam.id ? normalizeRaceTeam({ ...team, name, driverIds, carIds }) : team + ); + selectedTeamEditId = null; + saveState(); + renderEventManager(eventId); + }); + document.getElementById("raceFormatForm")?.addEventListener("submit", (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); @@ -3764,6 +3893,7 @@ function renderTiming() { + @@ -4004,6 +4134,7 @@ function renderTiming() { document.getElementById("openSpeakerOverlay")?.addEventListener("click", () => openOverlayWindow("speaker")); document.getElementById("openResultsOverlay")?.addEventListener("click", () => openOverlayWindow("results")); document.getElementById("openTvOverlay")?.addEventListener("click", () => openOverlayWindow("tv")); + document.getElementById("openTeamOverlay")?.addEventListener("click", () => openOverlayWindow("team")); document.querySelectorAll("[data-speaker-setting]").forEach((node) => { node.addEventListener("change", (event) => { const input = event.currentTarget; @@ -4318,6 +4449,8 @@ function renderOverlay() { ` + : overlayViewMode === "team" + ? renderTeamOverlay(leaderboard, result, sessionTiming) : overlayViewMode === "tv" ? `
@@ -4592,7 +4725,10 @@ function renderLeaderboard(rows) { return ` ${idx + 1} - ${escapeHtml(row.displayName || row.driverName)} + +
${escapeHtml(row.displayName || row.driverName)}
+ ${row.teamId && row.subLabel ? `
${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}
` : ""} + ${escapeHtml(row.subLabel || row.carName)} ${escapeHtml(row.transponder)} ${row.laps} @@ -4626,7 +4762,7 @@ function renderOverlayLeaderboard(rows) {
${escapeHtml(row.displayName || row.driverName)} - ${escapeHtml(row.subLabel || row.transponder || "-")} + ${escapeHtml(row.teamId && row.subLabel ? `${t("overlay.active_member")}: ${row.subLabel}` : row.subLabel || row.transponder || "-")}
@@ -4661,6 +4797,63 @@ function renderOverlayLeaderboard(rows) { `; } +function renderTeamOverlay(rows, result, sessionTiming) { + const topThree = rows.slice(0, 3); + return ` +
+
+
+

${t("overlay.top_three")}

+ ${t("overlay.team_battle")} +
+
+ ${topThree + .map( + (row, index) => ` +
+ ${index + 1} + ${escapeHtml(row.displayName || row.driverName)} +

${escapeHtml(row.resultDisplay || "-")}

+ ${t("overlay.active_member")}: ${escapeHtml(row.subLabel || "-")} +
+ ` + ) + .join("")} +
+
+
+
+
+
+ ${t("table.laps")} + ${rows[0]?.laps || 0} + ${escapeHtml(rows[0]?.displayName || rows[0]?.driverName || "-")} +
+
+ ${t("timing.total_passings")} + ${result?.passings.length || 0} + ${sessionTiming?.untimed ? t("timing.elapsed") : t("timing.remaining")} +
+
+ ${t("overlay.fastest_lap")} + ${formatLap([...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.bestLapMs)} + ${escapeHtml( + [...rows].filter((row) => Number.isFinite(row.bestLapMs)).sort((a, b) => a.bestLapMs - b.bestLapMs)[0]?.displayName || "-" + )} +
+
+
+
+

${t("events.team_standings")}

+
+ ${renderOverlayLeaderboard(rows)} +
+
+
+
+ `; +} + function renderRecentPassings(session) { if (!session) { return `

${t("timing.no_session_selected")}

`; @@ -5913,7 +6106,10 @@ function renderTeamRaceStandings(event) { (row, index) => ` ${index + 1} - ${escapeHtml(row.displayName || row.driverName)} + +
${escapeHtml(row.displayName || row.driverName)}
+ ${row.subLabel ? `
${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}
` : ""} + ${row.laps} ${escapeHtml(row.resultDisplay)} ${formatLap(row.bestLapMs)} diff --git a/src/styles.css b/src/styles.css index 1f6e006..a89a8b5 100644 --- a/src/styles.css +++ b/src/styles.css @@ -609,6 +609,16 @@ select:focus { margin: 0 0 10px; } +.table-primary { + font-weight: 700; +} + +.table-subnote { + margin-top: 4px; + color: var(--muted); + font-size: 0.8rem; +} + .grid-editor-toolbar { display: flex; justify-content: space-between; @@ -881,6 +891,49 @@ select:focus { gap: 16px; } +.overlay-team-layout { + display: grid; + gap: 18px; +} + +.overlay-team-podium { + border: 1px solid var(--line); + border-radius: 18px; + padding: 16px; + background: rgba(7, 12, 20, 0.82); + box-shadow: var(--shadow); +} + +.overlay-team-podium-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} + +.overlay-team-card { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.03); + display: grid; + gap: 10px; +} + +.overlay-team-card strong { + font-family: Orbitron, sans-serif; + font-size: clamp(1.2rem, 2vw, 1.9rem); +} + +.overlay-team-card p, +.overlay-team-card small { + margin: 0; +} + +.overlay-team-card-1 { + border-color: rgba(225, 6, 0, 0.45); + background: linear-gradient(135deg, rgba(225, 6, 0, 0.12), rgba(255, 255, 255, 0.03)); +} + .overlay-table-wrap, .overlay-side-card, .overlay-empty { @@ -1268,6 +1321,10 @@ select:focus { grid-template-columns: 1fr; } + .overlay-team-podium-grid { + grid-template-columns: 1fr; + } + .overlay-header { align-items: flex-start; flex-direction: column;