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_title"))}

${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("training.recommendation"))}

20 min
${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.goals"))}

${goals.length}
${goalMarkup}
` : ""}
${escapeHtml(t("training.manage_goals_sharing"))}${escapeHtml(t("training.manage_goals_sharing_hint"))} +

${escapeHtml(t("training.new_goal"))}

${[500, 1000, 1500].map((value) => ``).join("")}

${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) }))}` : ""}
${dailyRows}
${escapeHtml(t("training.date"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.average_lap"))}
${escapeHtml(t("training.club_progress"))}${escapeHtml(t("training.club_progress_hint"))} +
${clubRows}
#${escapeHtml(t("training.driver"))}${escapeHtml(t("training.laps"))}${escapeHtml(t("training.training_days"))}${escapeHtml(t("training.best_lap"))}${escapeHtml(t("training.next_target"))}
`; 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(); }