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 { renderSettings } from "./settings.js";
|
||||
@@ -3377,6 +3378,7 @@ const dom = {
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
installListSorting(document.body);
|
||||
document.body.classList.toggle("overlay-mode", overlayMode);
|
||||
["focusin", "input", "change"].forEach((eventName) => {
|
||||
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 }) {
|
||||
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"),
|
||||
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(
|
||||
(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>
|
||||
<span>${passing.lapMs == null ? "-" : formatLapLocal(passing.lapMs)}</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
.join("")}</div>`
|
||||
: `<p>${t("timing.no_passings")}</p>`,
|
||||
},
|
||||
{
|
||||
@@ -60,12 +61,12 @@ export function renderOverlayLeaderboard(rows, { t, escapeHtml, formatTeamActive
|
||||
}
|
||||
|
||||
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
|
||||
.map((row, idx) => {
|
||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||
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">
|
||||
<span class="pos-pill ${posClass}">${idx + 1}</span>
|
||||
</div>
|
||||
|
||||
@@ -3987,3 +3987,31 @@ select:focus {
|
||||
.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 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 {
|
||||
advanceSessionReplay,
|
||||
buildSessionReplaySnapshot,
|
||||
@@ -267,7 +268,7 @@ function renderLeaderboard(rows, deps) {
|
||||
return `<p>${t("timing.no_laps")}</p>`;
|
||||
}
|
||||
|
||||
return renderTable(
|
||||
return sortableTable(renderTable(
|
||||
[
|
||||
t("table.pos"),
|
||||
t("table.driver"),
|
||||
@@ -287,7 +288,7 @@ function renderLeaderboard(rows, deps) {
|
||||
rows.map((row, idx) => {
|
||||
const posClass = idx === 0 ? "pos-1" : idx === 1 ? "pos-2" : idx === 2 ? "pos-3" : "";
|
||||
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>
|
||||
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
||||
@@ -310,7 +311,7 @@ function renderLeaderboard(rows, deps) {
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
);
|
||||
), "timing-leaderboard");
|
||||
}
|
||||
|
||||
function renderRecentPassings(session, deps) {
|
||||
@@ -324,11 +325,11 @@ function renderRecentPassings(session, deps) {
|
||||
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"), ""],
|
||||
items.map((p, index) => {
|
||||
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>${escapeHtml(p.transponder)}</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>
|
||||
`;
|
||||
})
|
||||
);
|
||||
), "timing-passings");
|
||||
}
|
||||
|
||||
export function renderTimingView(deps) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sortValuesAttribute } from "./list_sorting.js";
|
||||
import { getDriverProfileGroups, getDriverProfileReferences } from "./driver_profile_logic.js";
|
||||
|
||||
function renderStintRow(row, { t, escapeHtml, formatLap, formatRaceClock }) {
|
||||
@@ -12,7 +13,7 @@ function renderStintRow(row, { t, escapeHtml, formatLap, formatRaceClock }) {
|
||||
? `${paceRetention} · ${t("training.strong_finish")}`
|
||||
: paceRetention;
|
||||
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>${escapeHtml(new Date(row.startedAt).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }))}</td>
|
||||
<td>${row.lapCount}</td>
|
||||
@@ -182,7 +183,7 @@ export function renderTrainingView(deps) {
|
||||
const dailyRows = stats.dailySummaries.length
|
||||
? stats.dailySummaries.slice(0, 10).map((day) => `
|
||||
<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>${formatLap(day.bestLapMs)}</td>
|
||||
<td>${formatLap(day.averageLapMs)}</td>
|
||||
@@ -237,7 +238,7 @@ export function renderTrainingView(deps) {
|
||||
.map((entry) => {
|
||||
const milestone = getNextTrainingMilestone(entry.stats);
|
||||
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>${entry.stats.totalLaps}</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>` : ""}
|
||||
</div>
|
||||
<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>
|
||||
${stintAnalysis.rows.length > featuredStints.length ? `
|
||||
<details class="training-stint-all">
|
||||
<summary>${escapeHtml(t("training.show_all_stints", { count: stintAnalysis.rows.length }))}</summary>
|
||||
<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>
|
||||
</details>` : ""}
|
||||
<p class="hint training-method-note">${escapeHtml(t("training.stats_method"))}</p>
|
||||
@@ -580,11 +581,11 @@ export function renderTrainingView(deps) {
|
||||
${channelStats.recentPitStops.length ? `
|
||||
<div class="training-section-title"><h3>${escapeHtml(t("training.recent_pit_stops"))}</h3></div>
|
||||
<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>
|
||||
<tbody>${channelStats.recentPitStops.map((stop) => `
|
||||
<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.sessionName || "-")}</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>` : ""}
|
||||
</div>
|
||||
<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>
|
||||
<tbody>${dailyRows}</tbody>
|
||||
</table>
|
||||
@@ -613,7 +614,7 @@ export function renderTrainingView(deps) {
|
||||
<span class="training-disclosure-arrow">+</span>
|
||||
</summary>
|
||||
<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>
|
||||
<tbody>${clubRows}</tbody>
|
||||
</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" })
|
||||
: "-";
|
||||
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>${escapeHtml(row.className)}</td>
|
||||
<td>${row.stats.totalLaps}</td>
|
||||
@@ -941,7 +942,7 @@ function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, uid, sav
|
||||
<div class="panel-body">
|
||||
<p class="hint">${escapeHtml(t("training.overview_class_hint"))}</p>
|
||||
<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>
|
||||
<tbody>${tableRows}</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user