Files
Live_RC/src/race_render_helpers.js

427 lines
16 KiB
JavaScript

export function renderTeamStintLog(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock }) {
if (!rows.length) {
return `<p>${t("events.no_team_results")}</p>`;
}
return `
<div class="team-log-grid">
${rows
.map((row) => {
const stints = buildTeamStintLog(session, row);
return `
<article class="team-log-card">
<h5>${escapeHtml(row.displayName || row.driverName)}</h5>
<div class="hint">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
${
stints.length
? renderTable(
[t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")],
stints.map(
(stint) => `
<tr>
<td>${stint.index}</td>
<td>${escapeHtml(stint.driverName || "-")}</td>
<td>${escapeHtml(stint.carName || "-")}</td>
<td>${new Date(stint.startTs).toLocaleTimeString()}</td>
<td>${formatRaceClock(stint.durationMs)}</td>
<td>${stint.laps}</td>
</tr>
`
)
)
: `<p>${t("timing.no_passings")}</p>`
}
</article>
`;
})
.join("")}
</div>
`;
}
export function renderTeamRaceStandings(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, renderTeamStintLog }) {
const groups = buildTeamRaceStandings(event);
if (!groups.length) {
return `<p>${t("events.no_team_results")}</p>`;
}
return groups
.map(
({ session, rows }) => `
<section class="team-standings-block">
<h4>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h4>
${
rows.length
? renderTable(
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
rows.map(
(row, index) => `
<tr>
<td>${index + 1}</td>
<td>
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
</td>
<td>${row.laps}</td>
<td>${escapeHtml(row.resultDisplay)}</td>
<td>${formatLap(row.bestLapMs)}</td>
</tr>
`
)
)
: `<p>${t("events.no_team_results")}</p>`
}
<div class="mt-16">
<h5>${t("events.team_stint_log")}</h5>
${rows.length ? renderTeamStintLog(session, rows) : `<p>${t("events.no_team_results")}</p>`}
</div>
</section>
`
)
.join("");
}
export function getSessionSortWeight(session) {
const order = {
open_practice: 0,
free_practice: 1,
practice: 2,
qualification: 3,
heat: 4,
final: 5,
team_race: 6,
};
return order[String(session?.type || "").toLowerCase()] || 99;
}
export function getDriverDisplayById(driverId, { state, t }) {
const driver = state.drivers.find((item) => item.id === driverId);
if (!driver) {
return t("common.unknown_driver");
}
return driver.transponder ? `${driver.name} (${driver.transponder})` : driver.name;
}
export function renderPositionGrid(session, { t, escapeHtml, getSessionGridEntries }) {
const entries = getSessionGridEntries(session);
if (!entries.length) {
return "";
}
return `
<div class="position-grid mt-16">
<h4>${t("events.position_grid")}</h4>
<p class="hint">${t("timing.position_grid_hint")}</p>
<div class="position-grid-list">
${entries
.map(
(entry) => `
<div class="position-grid-item">
<span class="pos-pill">${entry.slot}</span>
<div>
<strong>${escapeHtml(entry.name)}</strong>
${entry.meta ? `<div class="hint">${escapeHtml(entry.meta)}</div>` : ""}
</div>
</div>
`
)
.join("")}
</div>
</div>
`;
}
export function renderGridEditor(session, { t, escapeHtml, ensureSessionDriverOrder, state }) {
if (!session) {
return `<p>${t("events.grid_empty")}</p>`;
}
const driverIds = ensureSessionDriverOrder(session);
return `
<div class="grid-editor-toolbar">
<div>
<strong>${escapeHtml(session.name)}</strong>
<div class="hint">${t("events.grid_editor_hint")}</div>
<div class="hint">${t(session.gridCustomized ? "events.grid_locked" : "events.grid_unlocked")}</div>
</div>
<div class="actions">
<button id="gridToggleLock" class="btn" type="button">${t(session.gridCustomized ? "events.grid_unlock" : "events.grid_lock")}</button>
<button id="gridResetOrder" class="btn" type="button">${t("events.grid_reset")}</button>
</div>
</div>
<div class="drag-list mt-16" id="gridDragList">
${driverIds
.map((driverId, index) => {
const driver = state.drivers.find((item) => item.id === driverId);
return `
<div class="drag-item" draggable="true" data-index="${index}">
<span class="pos-pill">${index + 1}</span>
<div>
<strong>${escapeHtml(driver?.name || t("common.unknown_driver"))}</strong>
<div class="hint">${escapeHtml(driver?.transponder || "-")}</div>
</div>
</div>
`;
})
.join("")}
</div>
`;
}
export function getFinalMainLayouts(event, { getSessionsForEvent, getSessionGridOrder, getDriverDisplayById, t }) {
const finals = getSessionsForEvent(event.id)
.filter((session) => session.type === "final")
.sort((left, right) => {
const leftMain = String(left.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z";
const rightMain = String(right.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z";
if (leftMain !== rightMain) {
return leftMain.localeCompare(rightMain);
}
return String(left.name || "").localeCompare(String(right.name || ""));
});
const grouped = new Map();
finals.forEach((session) => {
const mainKey = String(session.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "A";
if (!grouped.has(mainKey)) {
grouped.set(mainKey, []);
}
grouped.get(mainKey).push(session);
});
return [...grouped.entries()].map(([mainKey, sessions]) => {
const sortedSessions = [...sessions].sort((left, right) => String(left.name || "").localeCompare(String(right.name || "")));
const baseSession = sortedSessions[0];
const baseDriverIds = [...getSessionGridOrder(baseSession)];
const reservedSlots = Math.max(0, Number(baseSession?.reservedBumpSlots || 0) || 0);
const capacity = Math.max(
Number(baseSession?.maxCars || event.raceConfig?.carsPerFinal || 0) || 0,
baseDriverIds.length + reservedSlots
);
const slots = [];
for (let index = 0; index < capacity; index += 1) {
const driverId = baseDriverIds[index];
if (driverId) {
slots.push({
slot: index + 1,
label: getDriverDisplayById(driverId),
reserved: false,
});
} else if (index < baseDriverIds.length + reservedSlots) {
slots.push({
slot: index + 1,
label: t("events.reserved_slot"),
reserved: true,
});
} else {
slots.push({
slot: index + 1,
label: "-",
reserved: false,
});
}
}
return {
mainKey,
sessions: sortedSessions,
slots,
};
});
}
export function renderFinalMatrix(event, { t, escapeHtml, getFinalMainLayouts, getStatusLabel }) {
const mains = getFinalMainLayouts(event);
if (!mains.length) {
return `<p>${t("events.no_final_matrix")}</p>`;
}
return `
<div class="final-matrix">
${mains
.map(
(main) => `
<section class="final-card">
<div class="final-card-header">
<h4>${t("events.main")} ${escapeHtml(main.mainKey)}</h4>
<span class="pill">${main.sessions.length} ${escapeHtml(t("events.leg_status").toLowerCase())}</span>
</div>
<div class="matrix-slots">
${main.slots
.map(
(slot) => `
<div class="matrix-slot ${slot.reserved ? "matrix-slot-reserved" : ""}">
<span class="pill">${t("events.slot")} ${slot.slot}</span>
<strong>${escapeHtml(slot.label)}</strong>
</div>
`
)
.join("")}
</div>
<div class="matrix-session-list">
${main.sessions
.map(
(session) => `
<div class="matrix-session-row">
<span>${escapeHtml(session.name)}</span>
<span class="pill ${session.status === "finished" ? "pill-green" : ""}">${escapeHtml(getStatusLabel(session.status))}</span>
</div>
`
)
.join("")}
</div>
</section>
`
)
.join("")}
</div>
`;
}
export function buildPrintBrandBlock(branding, { escapeHtml }) {
return `
<div class="print-brand-block">
${branding.logoDataUrl ? `<img class="print-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
<div>
<strong>${escapeHtml(branding.brandName)}</strong>
<p>${escapeHtml(branding.brandTagline)}</p>
</div>
</div>
`;
}
export function buildRaceStartListsHtml(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable }) {
const branding = resolveEventBranding(event);
const sessions = getSessionsForEvent(event.id)
.filter((session) => session.mode === "race")
.sort((left, right) => {
const weightDiff = getSessionSortWeight(left) - getSessionSortWeight(right);
if (weightDiff !== 0) {
return weightDiff;
}
return String(left.name || "").localeCompare(String(right.name || ""));
});
return `
<header class="print-header">
<div>
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
<h1>${escapeHtml(event.name)}</h1>
<p>${escapeHtml(event.date || "-")}</p>
</div>
<div class="print-meta">
${buildPrintBrandBlock(branding)}
</div>
</header>
<h2>${t("events.start_lists")}</h2>
${sessions
.map((session) => {
const entries = getSessionGridEntries(session);
return `
<section class="print-block">
<h3>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h3>
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(session.startMode))} • ${t("table.duration")}: ${session.durationMin} min</p>
${
entries.length
? renderTable(
[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 `
<tr>
<td>${entry.slot}</td>
<td>${escapeHtml(entry.name)}</td>
<td>${escapeHtml(driver?.brand || "-")}</td>
<td>${escapeHtml(entry.meta || "-")}</td>
</tr>
`;
})
)
: `<p>${t("common.no_entries")}</p>`
}
</section>
`;
})
.join("")}
`;
}
export function buildRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings }) {
const branding = resolveEventBranding(event);
return `
<header class="print-header">
<div>
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
<h1>${escapeHtml(event.name)}</h1>
<p>${escapeHtml(event.date || "-")}</p>
</div>
<div class="print-meta">
${buildPrintBrandBlock(branding)}
</div>
</header>
<section class="print-block">
<h2>${t("events.practice_standings")}</h2>
${renderRaceStandingsTableView(buildPracticeStandings(event), t("events.no_practice_results"))}
</section>
<section class="print-block">
<h2>${t("events.qualifying_standings")}</h2>
${renderRaceStandingsTableView(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
</section>
<section class="print-block">
<h2>${t("events.final_standings")}</h2>
${renderRaceStandingsTableView(buildFinalStandings(event), t("events.no_final_results"))}
</section>
<section class="print-block">
<h2>${t("events.team_standings")}</h2>
${renderTeamRaceStandings(event)}
</section>
`;
}
export function buildTeamRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog }) {
const branding = resolveEventBranding(event);
const groups = buildTeamRaceStandings(event);
return `
<header class="print-header">
<div>
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
<h1>${escapeHtml(event.name)}</h1>
<p>${escapeHtml(event.date || "-")}</p>
</div>
<div class="print-meta">
${buildPrintBrandBlock(branding)}
</div>
</header>
${groups
.map(
({ session, rows }) => `
<section class="print-block">
<h2>${t("events.team_report")} • ${escapeHtml(session.name)}</h2>
${
rows.length
? renderTable(
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
rows.map(
(row, index) => `
<tr>
<td>${index + 1}</td>
<td>${escapeHtml(row.displayName || row.driverName)}</td>
<td>${row.laps}</td>
<td>${escapeHtml(row.resultDisplay)}</td>
<td>${formatLap(row.bestLapMs)}</td>
</tr>
`
)
)
: `<p>${t("events.no_team_results")}</p>`
}
<h3>${t("events.team_stint_log")}</h3>
${renderTeamStintLog(session, rows)}
</section>
`
)
.join("")}
`;
}