Group the session picker by event instead of one long flat list

The "select session" dropdown in Timing, and the session replay
picker, listed every session across every event in one long flat
list - unwieldy once a club has generated several races' worth of
practice/qualifying/finals sessions. Both now group into <optgroup>s
by event, most recently created event first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 22:20:34 +02:00
parent e356e30eda
commit a449bec07d
6 changed files with 53 additions and 8 deletions

View File

@@ -9,6 +9,23 @@ import {
setSessionReplaySpeed,
} from "./session_replay.js";
// With several races each generating a practice + multiple qualifying heats + finals, a
// flat list of every session quickly becomes long and hard to scan. Groups sessions by
// their event, most-recently-created event first (state.events is already append-order),
// keeping each event's own sessions in their existing (already chronological) order.
function groupSessionsByEventRecency(sessions, state) {
const knownEventIds = new Set(state.events.map((event) => event.id));
const orderedEventIds = [...state.events].map((event) => event.id).reverse();
const groups = new Map();
sessions.forEach((session) => {
const key = knownEventIds.has(session.eventId) ? session.eventId : "__unknown__";
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(session);
});
const orderedKeys = [...orderedEventIds.filter((id) => groups.has(id)), ...(groups.has("__unknown__") ? ["__unknown__"] : [])];
return orderedKeys.map((key) => ({ eventId: key === "__unknown__" ? null : key, sessions: groups.get(key) }));
}
function stopSessionReplayTimer(runtime) {
if (runtime?.timer) clearInterval(runtime.timer);
if (runtime) runtime.timer = null;
@@ -63,7 +80,15 @@ function renderSessionReplayPanel(runtime, deps) {
<div class="session-replay-controls">
<select id="sessionReplaySelect">
<option value="">${t("timing.replay_select")}</option>
${replayable.map((session) => `<option value="${escapeHtml(session.id)}" ${session.id === selectedId ? "selected" : ""}>${escapeHtml(getEventName(session.eventId))} · ${escapeHtml(session.name)} · ${state.resultsBySession[session.id].passings.length}</option>`).join("")}
${groupSessionsByEventRecency(replayable, state)
.map(
({ eventId, sessions }) => `<optgroup label="${escapeHtml(eventId ? getEventName(eventId) : t("timing.other_sessions"))}">
${sessions
.map((session) => `<option value="${escapeHtml(session.id)}" ${session.id === selectedId ? "selected" : ""}>${escapeHtml(session.name)} · ${state.resultsBySession[session.id].passings.length}</option>`)
.join("")}
</optgroup>`
)
.join("")}
</select>
<button id="sessionReplayLoad" class="btn" type="button" ${selectedId ? "" : "disabled"}>${t("timing.replay_load")}</button>
<button id="sessionReplayPlay" class="btn btn-primary" type="button" ${runtime.passings.length ? "" : "disabled"}>${t("timing.replay_play")}</button>
@@ -427,11 +452,15 @@ export function renderTimingView(deps) {
<label class="timing-compact-label" for="activeSessionSelect">${t("timing.select_session")}</label>
<select id="activeSessionSelect">
<option value="">${t("timing.select_session")}</option>
${state.sessions
${groupSessionsByEventRecency(state.sessions, state)
.map(
(s) => `<option value="${s.id}" ${state.activeSessionId === s.id ? "selected" : ""}>${escapeHtml(
getEventName(s.eventId)
)} • ${escapeHtml(s.name)} • ${escapeHtml(getSessionTypeLabel(s.type))}</option>`
({ eventId, sessions }) => `<optgroup label="${escapeHtml(eventId ? getEventName(eventId) : t("timing.other_sessions"))}">
${sessions
.map(
(s) => `<option value="${s.id}" ${state.activeSessionId === s.id ? "selected" : ""}>${escapeHtml(s.name)} • ${escapeHtml(getSessionTypeLabel(s.type))}</option>`
)
.join("")}
</optgroup>`
)
.join("")}
</select>