export function renderTrainingView(deps) {
const {
state,
dom,
t,
escapeHtml,
formatLap,
formatRaceClock,
uid,
saveState,
renderView,
normalizeTrainingState,
syncTrainingLedger,
buildDriverTrainingStats,
buildDriverActivePracticeStats,
buildTrainingRecommendation,
getTrainingGoalProgress,
getNextTrainingMilestone,
getSelectedDriverId,
setSelectedDriverId,
} = deps;
state.training = normalizeTrainingState(state.training);
syncTrainingLedger();
const drivers = [...state.drivers].sort((left, right) => String(left.name).localeCompare(String(right.name)));
let selectedDriverId = String(getSelectedDriverId() || "");
if (!drivers.some((driver) => driver.id === selectedDriverId)) {
selectedDriverId = drivers[0]?.id || "";
setSelectedDriverId(selectedDriverId);
}
if (!selectedDriverId) {
dom.view.innerHTML = `
${escapeHtml(t("training.no_drivers_body"))}
`;
return;
}
const selectedDriver = drivers.find((driver) => driver.id === selectedDriverId);
const stats = buildDriverTrainingStats(state, selectedDriverId);
const activePractice = buildDriverActivePracticeStats(state, selectedDriverId);
const recommendation = buildTrainingRecommendation(stats);
const nextMilestone = getNextTrainingMilestone(stats);
const goals = state.training.goals.filter((goal) => goal.driverId === selectedDriverId);
const profileShare = (state.training.shares || []).find((share) => share.driverId === selectedDriverId && share.enabled);
const publicProfileUrl = profileShare ? buildPublicProfileUrl(profileShare.token, state.settings?.language) : "";
const driverOptions = drivers
.map((driver) => ``)
.join("");
const goalMarkup = goals.length
? goals.map((goal) => {
const progress = getTrainingGoalProgress(goal, stats);
const statusClass = goal.claimedAt ? "claimed" : progress.reached ? "reached" : "active";
const statusLabel = goal.claimedAt
? t("training.goal_claimed")
: progress.reached
? t("training.goal_reached")
: t("training.goal_remaining", { laps: progress.remainingLaps });
return `
${escapeHtml(statusLabel)}
${escapeHtml(goal.title || t("training.default_goal_title", { laps: goal.targetLaps }))}
${progress.currentLaps} / ${progress.targetLaps}
${goal.reward ? `${escapeHtml(t("training.reward"))}: ${escapeHtml(goal.reward)}
` : ""}
${progress.reached && !goal.claimedAt ? `` : ""}
`;
}).join("")
: `
${escapeHtml(t("training.no_goals"))}
`;
const targetText = recommendation.targetLapMs
? t("training.plan_target", { lap: formatLap(recommendation.targetLapMs) })
: t("training.plan_no_target");
const lastActivity = stats.lastLapAt
? new Date(stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
: t("training.no_activity");
const dailyRows = stats.dailySummaries.length
? stats.dailySummaries.slice(0, 10).map((day) => `
| ${escapeHtml(new Date(day.latestTimestamp).toLocaleDateString(undefined, { dateStyle: "medium" }))} |
${day.laps} |
${formatLap(day.bestLapMs)} |
${formatLap(day.averageLapMs)} |
`).join("")
: `| ${escapeHtml(t("training.no_history"))} |
`;
const clubRows = drivers
.map((driver) => ({ driver, stats: driver.id === selectedDriverId ? stats : buildDriverTrainingStats(state, driver.id) }))
.sort((left, right) => right.stats.totalLaps - left.stats.totalLaps || String(left.driver.name).localeCompare(String(right.driver.name)))
.map((entry, index) => {
const milestone = getNextTrainingMilestone(entry.stats);
return `
| ${index + 1} |
|
${entry.stats.totalLaps} |
${entry.stats.trainingDays} |
${formatLap(entry.stats.bestLapMs)} |
${milestone.targetLaps} |
`;
}).join("");
dom.view.innerHTML = `
${escapeHtml(t("training.eyebrow"))}
${escapeHtml(selectedDriver.name)}
${escapeHtml(t("training.intro"))}
${profileShare ? `
` : `
`}
${activePractice ? `
${escapeHtml(t("training.live_now"))}
${escapeHtml(activePractice.sessionName || t("training.active_practice"))}
${escapeHtml(t("training.shared_practice"))}
${trainingInsight(t("training.session_laps"), String(activePractice.laps))}
${trainingInsight(t("training.best_lap"), formatLap(activePractice.bestLapMs))}
${trainingInsight(t("training.average_lap"), formatLap(activePractice.averageLapMs))}
${trainingInsight(t("training.latest_lap"), formatLap(activePractice.lastLapMs))}
${escapeHtml(t("training.live_practice_hint"))}
` : ""}
${trainingStat(t("training.total_laps"), String(stats.totalLaps), t("training.valid_laps"))}
${trainingStat(t("training.training_days"), String(stats.trainingDays), t("training.sessions_count", { sessions: stats.totalSessions }))}
${trainingStat(t("training.best_lap"), formatLap(stats.bestLapMs), t("training.personal_best"))}
${trainingStat(t("training.consistency"), stats.recentLapCount ? `${stats.consistencySpreadPct.toFixed(1)}%` : "-", t("training.last_30_laps"))}
${escapeHtml(t("training.next_level"))}
${escapeHtml(t("training.next_level_target", { laps: nextMilestone.targetLaps }))}
${escapeHtml(t("training.goal_remaining", { laps: nextMilestone.remainingLaps }))}
${nextMilestone.currentLaps} / ${nextMilestone.targetLaps}
${escapeHtml(t(recommendation.titleKey))}
${escapeHtml(targetText)}
${escapeHtml(t(recommendation.bodyKey))}
10 min${escapeHtml(t("training.block_push"))}
10 min${escapeHtml(t("training.block_control"))}
${goals.length ? `
` : ""}
${escapeHtml(t("training.manage_goals_sharing"))}${escapeHtml(t("training.manage_goals_sharing_hint"))}
+
${escapeHtml(t("training.new_goal"))}
${escapeHtml(t("training.public_profile"))}
${escapeHtml(t(profileShare ? "training.public_profile_active" : "training.public_profile_off"))}
${escapeHtml(t("training.public_profile_help"))}
${profileShare ? `
` : `
`}
${escapeHtml(t("training.detailed_stats"))}${escapeHtml(t("training.detailed_stats_hint"))}
+
${trainingInsight(t("training.average_lap"), formatLap(stats.averageLapMs))}
${trainingInsight(t("training.recent_average"), formatLap(stats.recentAverageMs))}
${trainingInsight(t("training.controlled_streak"), t("training.lap_count", { laps: stats.controlledLapStreak }))}
${trainingInsight(t("training.last_activity"), lastActivity)}
${trainingInsight(t("training.stints"), String(stats.stintCount))}
${trainingInsight(t("training.best_three"), stats.bestThreeConsecutive ? `3/${formatRaceClock(stats.bestThreeConsecutive.totalMs)}` : "-")}
${trainingInsight(t("training.best_five"), stats.bestFiveMinuteResult ? `${stats.bestFiveMinuteResult.lapCount}/${formatRaceClock(stats.bestFiveMinuteResult.totalMs)}` : "-")}
${trainingInsight(t("training.five_avg_pace"), stats.bestFiveMinuteResult ? formatLap(stats.bestFiveMinuteResult.averageLapMs) : "-")}
${escapeHtml(t("training.history"))}
${stats.personalBestGainMs ? `${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}` : ""}
| ${escapeHtml(t("training.date"))} | ${escapeHtml(t("training.laps"))} | ${escapeHtml(t("training.best_lap"))} | ${escapeHtml(t("training.average_lap"))} |
${dailyRows}
${escapeHtml(t("training.club_progress"))}${escapeHtml(t("training.club_progress_hint"))}
+
| # | ${escapeHtml(t("training.driver"))} | ${escapeHtml(t("training.laps"))} | ${escapeHtml(t("training.training_days"))} | ${escapeHtml(t("training.best_lap"))} | ${escapeHtml(t("training.next_target"))} |
${clubRows}
`;
document.getElementById("training-driver")?.addEventListener("change", (event) => {
setSelectedDriverId(event.currentTarget.value);
renderView();
});
document.getElementById("trainingHeroEnableProfile")?.addEventListener("click", () => {
replacePublicProfileShare(state, selectedDriverId);
saveState();
renderView();
});
document.getElementById("trainingHeroOpenProfile")?.addEventListener("click", () => {
window.open(publicProfileUrl, "_blank", "noopener,noreferrer");
});
document.getElementById("trainingHeroCopyProfile")?.addEventListener("click", async () => {
await copyText(publicProfileUrl);
});
document.querySelectorAll(".training-preset").forEach((button) => {
button.addEventListener("click", () => {
const input = document.querySelector('#training-goal-form input[name="targetLaps"]');
if (input instanceof HTMLInputElement) input.value = button.dataset.laps || "500";
});
});
document.getElementById("training-goal-form")?.addEventListener("submit", (event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const targetLaps = Math.max(1, Math.round(Number(form.get("targetLaps") || 0) || 0));
state.training.goals.push({
id: uid("goal"),
driverId: selectedDriverId,
targetLaps,
title: String(form.get("title") || "").trim(),
reward: String(form.get("reward") || "").trim(),
createdAt: new Date().toISOString(),
claimedAt: "",
});
saveState();
renderView();
});
goals.forEach((goal) => {
document.getElementById(`training-claim-${goal.id}`)?.addEventListener("click", () => {
state.training.goals = state.training.goals.map((item) => item.id === goal.id ? { ...item, claimedAt: new Date().toISOString() } : item);
saveState();
renderView();
});
document.getElementById(`training-delete-${goal.id}`)?.addEventListener("click", () => {
state.training.goals = state.training.goals.filter((item) => item.id !== goal.id);
saveState();
renderView();
});
});
document.getElementById("trainingPublicProfileEnable")?.addEventListener("click", () => {
replacePublicProfileShare(state, selectedDriverId);
saveState();
renderView();
});
document.getElementById("trainingPublicProfileCopy")?.addEventListener("click", async () => {
await copyText(publicProfileUrl);
});
document.getElementById("trainingPublicProfileOpen")?.addEventListener("click", () => {
window.open(publicProfileUrl, "_blank", "noopener,noreferrer");
});
document.getElementById("trainingPublicProfileRotate")?.addEventListener("click", () => {
if (!confirm(t("training.replace_link_confirm"))) return;
replacePublicProfileShare(state, selectedDriverId);
saveState();
renderView();
});
document.getElementById("trainingPublicProfileDisable")?.addEventListener("click", () => {
state.training.shares = state.training.shares.map((share) => share.driverId === selectedDriverId ? { ...share, enabled: false } : share);
saveState();
renderView();
});
document.querySelectorAll(".training-club-driver").forEach((button) => {
button.addEventListener("click", () => {
setSelectedDriverId(button.dataset.driverId || "");
renderView();
});
});
}
function trainingStat(label, value, note) {
return `${label}
${value}
${note}`;
}
function trainingInsight(label, value) {
return `${label}${value}
`;
}
function buildPublicProfileUrl(token, language) {
const url = new URL(`/public-training/${encodeURIComponent(token)}`, window.location.origin);
url.searchParams.set("lang", language === "en" ? "en" : "sv");
return url.toString();
}
function generateShareToken() {
const bytes = new Uint8Array(24);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
function replacePublicProfileShare(state, driverId) {
const nextShare = {
driverId,
token: generateShareToken(),
enabled: true,
createdAt: new Date().toISOString(),
};
state.training.shares = [...state.training.shares.filter((share) => share.driverId !== driverId), nextShare];
}
async function copyText(value) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(value);
return;
} catch {
// HTTP LAN pages may not receive Clipboard API permission.
}
}
const input = document.createElement("textarea");
input.value = value;
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
}