add sorting
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { installListSorting } from "./list_sorting.js";
|
||||||
import { getOverlayModeLabel, buildOverlayPanels, renderOverlaySidePanel, renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } from "./overlays.js";
|
import { getOverlayModeLabel, buildOverlayPanels, renderOverlaySidePanel, renderOverlayLeaderboard, renderTeamOverlay, renderObsOverlay } from "./overlays.js";
|
||||||
|
|
||||||
import { renderSettings } from "./settings.js";
|
import { renderSettings } from "./settings.js";
|
||||||
@@ -3377,6 +3378,7 @@ const dom = {
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
installListSorting(document.body);
|
||||||
document.body.classList.toggle("overlay-mode", overlayMode);
|
document.body.classList.toggle("overlay-mode", overlayMode);
|
||||||
["focusin", "input", "change"].forEach((eventName) => {
|
["focusin", "input", "change"].forEach((eventName) => {
|
||||||
dom.view.addEventListener(eventName, markSettingsDraftActive);
|
dom.view.addEventListener(eventName, markSettingsDraftActive);
|
||||||
|
|||||||
138
src/list_sorting.js
Normal file
138
src/list_sorting.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
const storageKey = "racecore.listSorting.v1";
|
||||||
|
const preferences = new Map();
|
||||||
|
let loaded = false;
|
||||||
|
|
||||||
|
function loadPreferences() {
|
||||||
|
if (loaded) return;
|
||||||
|
loaded = true;
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(storageKey) || "{}");
|
||||||
|
for (const [key, value] of Object.entries(saved)) {
|
||||||
|
if (Number.isInteger(value?.column) && ["asc", "desc"].includes(value.direction)) preferences.set(key, value);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSortValue(value) {
|
||||||
|
const text = String(value ?? "").trim();
|
||||||
|
if (!text || /^[\s—–-]+$/.test(text)) return null;
|
||||||
|
const normalized = text.replace(/\u2212/g, "-").replace(/,/g, ".");
|
||||||
|
if (/^[+-]?\d+(?:\.\d+)?\s*(?:%|s|L)?$/.test(normalized)) return parseFloat(normalized);
|
||||||
|
if (/^[+-]?\d+(?::\d+){1,2}(?:\.\d+)?$/.test(normalized)) {
|
||||||
|
const sign = normalized.startsWith("-") ? -1 : 1;
|
||||||
|
return sign * normalized.replace(/^[+-]/, "").split(":").reduce((total, part) => total * 60 + Number(part), 0);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareSortValues(left, right, direction = "asc") {
|
||||||
|
if (left == null || left === "" || (typeof left === "number" && !Number.isFinite(left))) return right == null || right === "" || (typeof right === "number" && !Number.isFinite(right)) ? 0 : 1;
|
||||||
|
if (right == null || right === "" || (typeof right === "number" && !Number.isFinite(right))) return -1;
|
||||||
|
if (Array.isArray(left) && Array.isArray(right)) {
|
||||||
|
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
||||||
|
const result = compareSortValues(left[i], right[i], direction);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const comparison = typeof left === "number" && typeof right === "number"
|
||||||
|
? left - right
|
||||||
|
: String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: "base" });
|
||||||
|
return direction === "desc" ? -comparison : comparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortValuesAttribute(values) {
|
||||||
|
return JSON.stringify(values).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortableTable(markup, id) {
|
||||||
|
return markup.replace("<table", `<table data-sort-id="${id}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installListSorting(root) {
|
||||||
|
loadPreferences();
|
||||||
|
const initialized = new WeakSet();
|
||||||
|
const originalRows = new WeakMap();
|
||||||
|
const selector = "[data-sort-id], .training-history-table";
|
||||||
|
|
||||||
|
function rowsFor(list) {
|
||||||
|
return list.tagName === "TABLE" ? [...(list.tBodies[0]?.rows || [])] : [...list.children].filter((child) => child.hasAttribute("data-sort-values"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueFor(row, column) {
|
||||||
|
if (row.dataset.sortValues) {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(row.dataset.sortValues)[column] ?? null;
|
||||||
|
return typeof value === "string" ? parseSortValue(value) : value;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
const cell = row.cells?.[column];
|
||||||
|
return parseSortValue(cell?.dataset.sortValue ?? cell?.querySelector(".table-primary")?.textContent ?? cell?.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(list) {
|
||||||
|
const order = preferences.get(list.dataset.sortId);
|
||||||
|
const original = originalRows.get(list) || [];
|
||||||
|
const rows = order ? [...original].sort((a, b) => compareSortValues(valueFor(a, order.column), valueFor(b, order.column), order.direction)) : original;
|
||||||
|
const parent = list.tagName === "TABLE" ? list.tBodies[0] : list;
|
||||||
|
const currentRows = rowsFor(list);
|
||||||
|
if (parent && rows.some((row, index) => currentRows[index] !== row)) parent.append(...rows);
|
||||||
|
list.querySelectorAll("[data-sort-column]").forEach((button) => {
|
||||||
|
const active = order?.column === Number(button.dataset.sortColumn);
|
||||||
|
button.querySelector(".list-sort-arrow").textContent = active ? (order.direction === "asc" ? " ▲" : " ▼") : " ↕";
|
||||||
|
if (button.parentElement.tagName === "TH") button.parentElement.setAttribute("aria-sort", active ? (order.direction === "asc" ? "ascending" : "descending") : "none");
|
||||||
|
else button.setAttribute("aria-pressed", String(active));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enhance(list) {
|
||||||
|
if (initialized.has(list)) return;
|
||||||
|
initialized.add(list);
|
||||||
|
if (!list.dataset.sortId) {
|
||||||
|
const tables = [...root.querySelectorAll(".training-history-table")];
|
||||||
|
const view = new URLSearchParams(location.search).get("view") || "training";
|
||||||
|
list.dataset.sortId = `${view}:${list.className}:${tables.indexOf(list)}`;
|
||||||
|
}
|
||||||
|
originalRows.set(list, rowsFor(list));
|
||||||
|
const headers = list.tagName === "TABLE" ? [...(list.tHead?.rows[0]?.cells || [])] : [];
|
||||||
|
let labels = [];
|
||||||
|
try { labels = JSON.parse(list.dataset.sortLabels || "[]"); } catch {}
|
||||||
|
const toolbar = headers.length ? null : document.createElement("div");
|
||||||
|
if (toolbar) { toolbar.className = "list-sort-toolbar"; list.prepend(toolbar); }
|
||||||
|
(headers.length ? headers.map((header) => header.textContent.trim()) : labels).forEach((label, column) => {
|
||||||
|
if (!label) return;
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "list-sort-button";
|
||||||
|
button.dataset.sortColumn = column;
|
||||||
|
button.append(document.createTextNode(label));
|
||||||
|
const arrow = document.createElement("span");
|
||||||
|
arrow.className = "list-sort-arrow";
|
||||||
|
arrow.setAttribute("aria-hidden", "true");
|
||||||
|
button.append(arrow);
|
||||||
|
if (headers[column]) headers[column].replaceChildren(button);
|
||||||
|
else toolbar.append(button);
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const id = list.dataset.sortId;
|
||||||
|
const previous = preferences.get(id);
|
||||||
|
if (previous?.column === column && previous.direction === "desc") preferences.delete(id);
|
||||||
|
else preferences.set(id, { column, direction: previous?.column === column ? "desc" : "asc" });
|
||||||
|
try { localStorage.setItem(storageKey, JSON.stringify(Object.fromEntries(preferences))); } catch {}
|
||||||
|
apply(list);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
apply(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scan(node) {
|
||||||
|
if (node.nodeType !== 1) return;
|
||||||
|
if (node.matches(selector)) enhance(node);
|
||||||
|
node.querySelectorAll(selector).forEach(enhance);
|
||||||
|
}
|
||||||
|
scan(root);
|
||||||
|
const observer = new MutationObserver((records) => {
|
||||||
|
for (const record of records) for (const node of record.addedNodes) scan(node);
|
||||||
|
});
|
||||||
|
observer.observe(root, { childList: true, subtree: true });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sortValuesAttribute } from "./list_sorting.js";
|
||||||
export function getOverlayModeLabel(mode, { t }) {
|
export function getOverlayModeLabel(mode, { t }) {
|
||||||
return t(`overlay.mode_${String(mode || "leaderboard").toLowerCase()}`);
|
return t(`overlay.mode_${String(mode || "leaderboard").toLowerCase()}`);
|
||||||
}
|
}
|
||||||
@@ -7,16 +8,16 @@ export function buildOverlayPanels(active, recent, { t, overlayEvents, normalize
|
|||||||
{
|
{
|
||||||
title: t("overlay.last_passings"),
|
title: t("overlay.last_passings"),
|
||||||
content: recent.length
|
content: recent.length
|
||||||
? recent
|
? `<div data-sort-id="overlay-passings" data-sort-labels="${sortValuesAttribute([t("table.time"), t("table.driver"), t("table.last_lap")])}">${recent
|
||||||
.map(
|
.map(
|
||||||
(passing) => `
|
(passing) => `
|
||||||
<div class="overlay-passing">
|
<div class="overlay-passing" data-sort-values="${sortValuesAttribute([passing.timestamp, passing.displayName || passing.teamName || passing.driverName || passing.transponder, passing.lapMs])}">
|
||||||
<strong>${escapeHtmlLocal(passing.displayName || passing.teamName || passing.driverName || passing.transponder || t("common.unknown_driver"))}</strong>
|
<strong>${escapeHtmlLocal(passing.displayName || passing.teamName || passing.driverName || passing.transponder || t("common.unknown_driver"))}</strong>
|
||||||
<span>${passing.lapMs == null ? "-" : formatLapLocal(passing.lapMs)}</span>
|
<span>${passing.lapMs == null ? "-" : formatLapLocal(passing.lapMs)}</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")}</div>`
|
||||||
: `<p>${t("timing.no_passings")}</p>`,
|
: `<p>${t("timing.no_passings")}</p>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,12 +61,12 @@ export function renderOverlayLeaderboard(rows, { t, escapeHtml, formatTeamActive
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="overlay-race-list">
|
<div class="overlay-race-list" data-sort-id="overlay-leaderboard" data-sort-labels="${sortValuesAttribute([t("table.pos"), t("table.driver"), t("table.laps"), t("table.result"), t("table.gap"), t("table.ahead_gap"), t("table.own_delta"), t("table.best_lap")])}">
|
||||||
${rows
|
${rows
|
||||||
.map((row, idx) => {
|
.map((row, idx) => {
|
||||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||||
return `
|
return `
|
||||||
<article class="overlay-race-row ${idx === 0 ? "overlay-race-row-leader" : ""} ${row.invalidPending ? "overlay-race-row-invalid" : ""}" data-overlay-competitor="${escapeHtml(row.key)}">
|
<article class="overlay-race-row ${idx === 0 ? "overlay-race-row-leader" : ""} ${row.invalidPending ? "overlay-race-row-invalid" : ""}" data-sort-values="${sortValuesAttribute([idx + 1, row.displayName || row.driverName, row.laps ?? 0, row.resultDisplay, row.gapDisplay || row.gapAhead || null, row.gapAhead || null, row.lapDelta || null, row.bestLapMs || null])}" data-overlay-competitor="${escapeHtml(row.key)}">
|
||||||
<div class="overlay-race-pos">
|
<div class="overlay-race-pos">
|
||||||
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3987,3 +3987,31 @@ select:focus {
|
|||||||
.training-share-sections legend { padding: 0 6px; font-weight: 700; }
|
.training-share-sections legend { padding: 0 6px; font-weight: 700; }
|
||||||
.training-share-sections .hint { grid-column: 1 / -1; margin: 0 0 4px; }
|
.training-share-sections .hint { grid-column: 1 / -1; margin: 0 0 4px; }
|
||||||
.training-share-sections label { display: flex; align-items: center; gap: 8px; }
|
.training-share-sections label { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
|
.list-sort-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 3px 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.list-sort-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.list-sort-arrow { opacity: 0.65; white-space: nowrap; }
|
||||||
|
[aria-sort="ascending"] .list-sort-button,
|
||||||
|
[aria-sort="descending"] .list-sort-button,
|
||||||
|
.list-sort-button[aria-pressed="true"] { color: var(--accent); }
|
||||||
|
.list-sort-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
.overlay-shell-obs .list-sort-toolbar { display: none; }
|
||||||
|
@media print { .list-sort-toolbar, .list-sort-arrow { display: none; } }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sortableTable, sortValuesAttribute } from "./list_sorting.js";
|
||||||
import {
|
import {
|
||||||
advanceSessionReplay,
|
advanceSessionReplay,
|
||||||
buildSessionReplaySnapshot,
|
buildSessionReplaySnapshot,
|
||||||
@@ -267,7 +268,7 @@ function renderLeaderboard(rows, deps) {
|
|||||||
return `<p>${t("timing.no_laps")}</p>`;
|
return `<p>${t("timing.no_laps")}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderTable(
|
return sortableTable(renderTable(
|
||||||
[
|
[
|
||||||
t("table.pos"),
|
t("table.pos"),
|
||||||
t("table.driver"),
|
t("table.driver"),
|
||||||
@@ -287,7 +288,7 @@ function renderLeaderboard(rows, deps) {
|
|||||||
rows.map((row, idx) => {
|
rows.map((row, idx) => {
|
||||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||||
return `
|
return `
|
||||||
<tr class="${row.invalidPending ? "leaderboard-invalid" : ""}">
|
<tr data-sort-values="${sortValuesAttribute([idx + 1, row.displayName || row.driverName, row.subLabel || row.carName, row.transponder, row.laps, row.resultDisplay, row.lastLapMs || null, row.bestLapMs || null, row.leaderGap || row.gap || null, row.gapAhead || null, row.lapDelta || null, row.lastSplitMs || null, [Number(row.pitStops || 0), Number(row.totalPitMs || 0)]])}" class="${row.invalidPending ? "leaderboard-invalid" : ""}">
|
||||||
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
|
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
||||||
@@ -310,7 +311,7 @@ function renderLeaderboard(rows, deps) {
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
);
|
), "timing-leaderboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecentPassings(session, deps) {
|
function renderRecentPassings(session, deps) {
|
||||||
@@ -324,11 +325,11 @@ function renderRecentPassings(session, deps) {
|
|||||||
return `<p>${t("timing.no_passings")}</p>`;
|
return `<p>${t("timing.no_passings")}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderTable(
|
return sortableTable(renderTable(
|
||||||
[t("table.time"), t("table.transponder"), t("table.driver"), t("table.car"), t("table.last_lap"), t("table.status"), ""],
|
[t("table.time"), t("table.transponder"), t("table.driver"), t("table.car"), t("table.last_lap"), t("table.status"), ""],
|
||||||
items.map((p, index) => {
|
items.map((p, index) => {
|
||||||
return `
|
return `
|
||||||
<tr class="${isCountedPassing(p) ? "" : "passing-invalid"}">
|
<tr data-sort-values="${sortValuesAttribute([p.timestamp, p.transponder, p.teamName || p.driverName, p.carName || p.subLabel, p.lapMs, getPassingValidationLabel(p)])}" class="${isCountedPassing(p) ? "" : "passing-invalid"}">
|
||||||
<td>${new Date(p.timestamp).toLocaleTimeString()}</td>
|
<td>${new Date(p.timestamp).toLocaleTimeString()}</td>
|
||||||
<td>${escapeHtml(p.transponder)}</td>
|
<td>${escapeHtml(p.transponder)}</td>
|
||||||
<td>${escapeHtml(p.teamName ? `${p.teamName} • ${p.driverName || t("common.unknown_driver")}` : p.driverName || t("common.unknown_driver"))}</td>
|
<td>${escapeHtml(p.teamName ? `${p.teamName} • ${p.driverName || t("common.unknown_driver")}` : p.driverName || t("common.unknown_driver"))}</td>
|
||||||
@@ -339,7 +340,7 @@ function renderRecentPassings(session, deps) {
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
);
|
), "timing-passings");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderTimingView(deps) {
|
export function renderTimingView(deps) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sortValuesAttribute } from "./list_sorting.js";
|
||||||
import { getDriverProfileGroups, getDriverProfileReferences } from "./driver_profile_logic.js";
|
import { getDriverProfileGroups, getDriverProfileReferences } from "./driver_profile_logic.js";
|
||||||
|
|
||||||
function renderStintRow(row, { t, escapeHtml, formatLap, formatRaceClock }) {
|
function renderStintRow(row, { t, escapeHtml, formatLap, formatRaceClock }) {
|
||||||
@@ -12,7 +13,7 @@ function renderStintRow(row, { t, escapeHtml, formatLap, formatRaceClock }) {
|
|||||||
? `${paceRetention} · ${t("training.strong_finish")}`
|
? `${paceRetention} · ${t("training.strong_finish")}`
|
||||||
: paceRetention;
|
: paceRetention;
|
||||||
return `
|
return `
|
||||||
<tr class="${row.insufficient ? "is-muted" : ""}">
|
<tr data-sort-values="${sortValuesAttribute([row.number, row.startedAt, row.lapCount, row.bestLapMs || null, row.paceMs || null, row.racePace10Ms || null, row.mistakes, row.endChangeMs, row.consistencyPct, row.fiveMinuteResult ? [row.fiveMinuteResult.lapCount, row.fiveMinuteResult.totalMs] : null, row.durationMs])}" class="${row.insufficient ? "is-muted" : ""}">
|
||||||
<td>${row.number}</td>
|
<td>${row.number}</td>
|
||||||
<td>${escapeHtml(new Date(row.startedAt).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }))}</td>
|
<td>${escapeHtml(new Date(row.startedAt).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }))}</td>
|
||||||
<td>${row.lapCount}</td>
|
<td>${row.lapCount}</td>
|
||||||
@@ -182,7 +183,7 @@ export function renderTrainingView(deps) {
|
|||||||
const dailyRows = stats.dailySummaries.length
|
const dailyRows = stats.dailySummaries.length
|
||||||
? stats.dailySummaries.slice(0, 10).map((day) => `
|
? stats.dailySummaries.slice(0, 10).map((day) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(new Date(day.latestTimestamp).toLocaleDateString(undefined, { dateStyle: "medium" }))}</td>
|
<td data-sort-value="${day.latestTimestamp}">${escapeHtml(new Date(day.latestTimestamp).toLocaleDateString(undefined, { dateStyle: "medium" }))}</td>
|
||||||
<td>${day.laps}</td>
|
<td>${day.laps}</td>
|
||||||
<td>${formatLap(day.bestLapMs)}</td>
|
<td>${formatLap(day.bestLapMs)}</td>
|
||||||
<td>${formatLap(day.averageLapMs)}</td>
|
<td>${formatLap(day.averageLapMs)}</td>
|
||||||
@@ -237,7 +238,7 @@ export function renderTrainingView(deps) {
|
|||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const milestone = getNextTrainingMilestone(entry.stats);
|
const milestone = getNextTrainingMilestone(entry.stats);
|
||||||
return `
|
return `
|
||||||
<tr class="${entry.profile.id === selectedProfileId ? "is-selected" : ""}">
|
<tr data-sort-values="${sortValuesAttribute([entry.driver?.name, entry.stats.totalLaps, entry.stats.trainingDays, entry.stats.bestLapMs || null, milestone.targetLaps])}" class="${entry.profile.id === selectedProfileId ? "is-selected" : ""}">
|
||||||
<td><button class="training-club-driver" data-driver-id="${escapeHtml(entry.profile.id)}" data-class-id="${escapeHtml(selectedClassId)}" type="button">${escapeHtml(entry.driver?.name || "-")}</button></td>
|
<td><button class="training-club-driver" data-driver-id="${escapeHtml(entry.profile.id)}" data-class-id="${escapeHtml(selectedClassId)}" type="button">${escapeHtml(entry.driver?.name || "-")}</button></td>
|
||||||
<td>${entry.stats.totalLaps}</td>
|
<td>${entry.stats.totalLaps}</td>
|
||||||
<td>${entry.stats.trainingDays}</td>
|
<td>${entry.stats.trainingDays}</td>
|
||||||
@@ -546,13 +547,13 @@ export function renderTrainingView(deps) {
|
|||||||
${stintAnalysis.best ? `<span class="badge success">${escapeHtml(t("training.best_stint", { stint: stintAnalysis.best.number, laps: stintAnalysis.best.lapCount, pace: formatLap(stintAnalysis.best.paceMs) }))}</span>` : ""}
|
${stintAnalysis.best ? `<span class="badge success">${escapeHtml(t("training.best_stint", { stint: stintAnalysis.best.number, laps: stintAnalysis.best.lapCount, pace: formatLap(stintAnalysis.best.paceMs) }))}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap training-history-wrap">
|
<div class="table-wrap training-history-wrap">
|
||||||
<table class="training-history-table training-stint-table"><thead>${stintColumns}</thead><tbody>${featuredStintRows}</tbody></table>
|
<table data-sort-id="training-stints-featured" class="training-history-table training-stint-table"><thead>${stintColumns}</thead><tbody>${featuredStintRows}</tbody></table>
|
||||||
</div>
|
</div>
|
||||||
${stintAnalysis.rows.length > featuredStints.length ? `
|
${stintAnalysis.rows.length > featuredStints.length ? `
|
||||||
<details class="training-stint-all">
|
<details class="training-stint-all">
|
||||||
<summary>${escapeHtml(t("training.show_all_stints", { count: stintAnalysis.rows.length }))}</summary>
|
<summary>${escapeHtml(t("training.show_all_stints", { count: stintAnalysis.rows.length }))}</summary>
|
||||||
<div class="table-wrap training-history-wrap">
|
<div class="table-wrap training-history-wrap">
|
||||||
<table class="training-history-table training-stint-table"><thead>${stintColumns}</thead><tbody>${allStintRows}</tbody></table>
|
<table data-sort-id="training-stints-all" class="training-history-table training-stint-table"><thead>${stintColumns}</thead><tbody>${allStintRows}</tbody></table>
|
||||||
</div>
|
</div>
|
||||||
</details>` : ""}
|
</details>` : ""}
|
||||||
<p class="hint training-method-note">${escapeHtml(t("training.stats_method"))}</p>
|
<p class="hint training-method-note">${escapeHtml(t("training.stats_method"))}</p>
|
||||||
@@ -580,11 +581,11 @@ export function renderTrainingView(deps) {
|
|||||||
${channelStats.recentPitStops.length ? `
|
${channelStats.recentPitStops.length ? `
|
||||||
<div class="training-section-title"><h3>${escapeHtml(t("training.recent_pit_stops"))}</h3></div>
|
<div class="training-section-title"><h3>${escapeHtml(t("training.recent_pit_stops"))}</h3></div>
|
||||||
<div class="table-wrap training-history-wrap">
|
<div class="table-wrap training-history-wrap">
|
||||||
<table class="training-history-table">
|
<table data-sort-id="training-pit-stops" class="training-history-table">
|
||||||
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("table.event"))}</th><th>${escapeHtml(t("table.session"))}</th><th>${escapeHtml(t("table.car"))}</th><th>${escapeHtml(t("table.duration"))}</th></tr></thead>
|
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("table.event"))}</th><th>${escapeHtml(t("table.session"))}</th><th>${escapeHtml(t("table.car"))}</th><th>${escapeHtml(t("table.duration"))}</th></tr></thead>
|
||||||
<tbody>${channelStats.recentPitStops.map((stop) => `
|
<tbody>${channelStats.recentPitStops.map((stop) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(new Date(stop.enteredAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }))}</td>
|
<td data-sort-value="${stop.enteredAt}">${escapeHtml(new Date(stop.enteredAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }))}</td>
|
||||||
<td>${escapeHtml(stop.eventName || "-")}</td>
|
<td>${escapeHtml(stop.eventName || "-")}</td>
|
||||||
<td>${escapeHtml(stop.sessionName || "-")}</td>
|
<td>${escapeHtml(stop.sessionName || "-")}</td>
|
||||||
<td>${escapeHtml(stop.carName || "-")}</td>
|
<td>${escapeHtml(stop.carName || "-")}</td>
|
||||||
@@ -599,7 +600,7 @@ export function renderTrainingView(deps) {
|
|||||||
${stats.personalBestGainMs ? `<span class="badge success">${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}</span>` : ""}
|
${stats.personalBestGainMs ? `<span class="badge success">${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap training-history-wrap">
|
<div class="table-wrap training-history-wrap">
|
||||||
<table class="training-history-table">
|
<table data-sort-id="training-history" class="training-history-table">
|
||||||
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.average_lap"))}</th></tr></thead>
|
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.average_lap"))}</th></tr></thead>
|
||||||
<tbody>${dailyRows}</tbody>
|
<tbody>${dailyRows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -613,7 +614,7 @@ export function renderTrainingView(deps) {
|
|||||||
<span class="training-disclosure-arrow">+</span>
|
<span class="training-disclosure-arrow">+</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="training-disclosure-body table-wrap training-history-wrap">
|
<div class="training-disclosure-body table-wrap training-history-wrap">
|
||||||
<table class="training-history-table training-club-table">
|
<table data-sort-id="training-class-drivers" class="training-history-table training-club-table">
|
||||||
<thead><tr><th>${escapeHtml(t("training.driver"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
|
<thead><tr><th>${escapeHtml(t("training.driver"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
|
||||||
<tbody>${clubRows}</tbody>
|
<tbody>${clubRows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -859,7 +860,7 @@ function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, uid, sav
|
|||||||
? new Date(row.stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
|
? new Date(row.stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
|
||||||
: "-";
|
: "-";
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr data-sort-values="${sortValuesAttribute([row.driver?.name, row.className, row.stats.totalLaps, row.stats.trainingDays, row.stats.bestLapMs || null, row.stats.lastLapAt || null, milestone.targetLaps])}">
|
||||||
<td><button class="training-club-driver" data-driver-id="${escapeHtml(row.profile.id)}" data-class-id="${escapeHtml(row.classId)}" type="button">${escapeHtml(row.driver?.name || "-")}</button></td>
|
<td><button class="training-club-driver" data-driver-id="${escapeHtml(row.profile.id)}" data-class-id="${escapeHtml(row.classId)}" type="button">${escapeHtml(row.driver?.name || "-")}</button></td>
|
||||||
<td>${escapeHtml(row.className)}</td>
|
<td>${escapeHtml(row.className)}</td>
|
||||||
<td>${row.stats.totalLaps}</td>
|
<td>${row.stats.totalLaps}</td>
|
||||||
@@ -941,7 +942,7 @@ function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, uid, sav
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p class="hint">${escapeHtml(t("training.overview_class_hint"))}</p>
|
<p class="hint">${escapeHtml(t("training.overview_class_hint"))}</p>
|
||||||
<div class="table-wrap training-history-wrap">
|
<div class="table-wrap training-history-wrap">
|
||||||
<table class="training-history-table training-club-table">
|
<table data-sort-id="training-overview" class="training-history-table training-club-table">
|
||||||
<thead><tr><th>${escapeHtml(t("training.driver"))}</th><th>${escapeHtml(t("training.class"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.last_activity"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
|
<thead><tr><th>${escapeHtml(t("training.driver"))}</th><th>${escapeHtml(t("training.class"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.last_activity"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
|
||||||
<tbody>${tableRows}</tbody>
|
<tbody>${tableRows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user