"Add Windows installer and driver training rewards"
This commit is contained in:
181
src/training_views.js
Normal file
181
src/training_views.js
Normal file
@@ -0,0 +1,181 @@
|
||||
export function renderTrainingView(deps) {
|
||||
const {
|
||||
state,
|
||||
dom,
|
||||
t,
|
||||
escapeHtml,
|
||||
formatLap,
|
||||
uid,
|
||||
saveState,
|
||||
renderView,
|
||||
normalizeTrainingState,
|
||||
buildDriverTrainingStats,
|
||||
buildTrainingRecommendation,
|
||||
getTrainingGoalProgress,
|
||||
getSelectedDriverId,
|
||||
setSelectedDriverId,
|
||||
} = deps;
|
||||
|
||||
state.training = normalizeTrainingState(state.training);
|
||||
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 = `
|
||||
<section class="panel training-empty">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.no_drivers_title"))}</h3></div>
|
||||
<div class="panel-body"><p>${escapeHtml(t("training.no_drivers_body"))}</p></div>
|
||||
</section>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDriver = drivers.find((driver) => driver.id === selectedDriverId);
|
||||
const stats = buildDriverTrainingStats(state, selectedDriverId);
|
||||
const recommendation = buildTrainingRecommendation(stats);
|
||||
const goals = state.training.goals.filter((goal) => goal.driverId === selectedDriverId);
|
||||
const driverOptions = drivers
|
||||
.map((driver) => `<option value="${escapeHtml(driver.id)}" ${driver.id === selectedDriverId ? "selected" : ""}>${escapeHtml(driver.name)}</option>`)
|
||||
.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 `
|
||||
<article class="training-goal ${statusClass}">
|
||||
<div class="training-goal-head">
|
||||
<div>
|
||||
<span class="training-kicker">${escapeHtml(statusLabel)}</span>
|
||||
<h4>${escapeHtml(goal.title || t("training.default_goal_title", { laps: goal.targetLaps }))}</h4>
|
||||
</div>
|
||||
<strong>${progress.currentLaps} / ${progress.targetLaps}</strong>
|
||||
</div>
|
||||
<div class="training-progress" aria-label="${escapeHtml(t("training.goal_progress"))}">
|
||||
<span style="width:${progress.percent.toFixed(2)}%"></span>
|
||||
</div>
|
||||
${goal.reward ? `<p><b>${escapeHtml(t("training.reward"))}:</b> ${escapeHtml(goal.reward)}</p>` : ""}
|
||||
<div class="actions">
|
||||
${progress.reached && !goal.claimedAt ? `<button class="btn btn-primary btn-mini" id="training-claim-${escapeHtml(goal.id)}">${escapeHtml(t("training.claim_reward"))}</button>` : ""}
|
||||
<button class="btn btn-danger btn-mini" id="training-delete-${escapeHtml(goal.id)}">${escapeHtml(t("common.delete"))}</button>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).join("")
|
||||
: `<p class="hint">${escapeHtml(t("training.no_goals"))}</p>`;
|
||||
|
||||
const targetText = recommendation.targetLapMs
|
||||
? t("training.plan_target", { lap: formatLap(recommendation.targetLapMs) })
|
||||
: t("training.plan_no_target");
|
||||
|
||||
dom.view.innerHTML = `
|
||||
<section class="training-hero">
|
||||
<div>
|
||||
<span class="training-kicker">${escapeHtml(t("training.eyebrow"))}</span>
|
||||
<h3>${escapeHtml(selectedDriver.name)}</h3>
|
||||
<p>${escapeHtml(t("training.intro"))}</p>
|
||||
</div>
|
||||
<label class="training-driver-select">
|
||||
<span>${escapeHtml(t("training.driver"))}</span>
|
||||
<select id="training-driver">${driverOptions}</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="grid cols-4 training-stats">
|
||||
${trainingStat(t("training.total_laps"), String(stats.totalLaps), t("training.valid_laps"))}
|
||||
${trainingStat(t("training.sessions"), String(stats.totalSessions), t("training.recorded_sessions"))}
|
||||
${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"))}
|
||||
</div>
|
||||
|
||||
<div class="training-layout">
|
||||
<section class="panel training-plan">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.recommendation"))}</h3><span class="badge">20 min</span></div>
|
||||
<div class="panel-body">
|
||||
<span class="training-kicker">${escapeHtml(t(recommendation.titleKey))}</span>
|
||||
<h3>${escapeHtml(targetText)}</h3>
|
||||
<p>${escapeHtml(t(recommendation.bodyKey))}</p>
|
||||
<div class="training-blocks">
|
||||
<div><strong>10 min</strong><span>${escapeHtml(t("training.block_push"))}</span></div>
|
||||
<div><strong>10 min</strong><span>${escapeHtml(t("training.block_control"))}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.new_goal"))}</h3></div>
|
||||
<div class="panel-body">
|
||||
<form id="training-goal-form" class="form-grid training-goal-form">
|
||||
<label>${escapeHtml(t("training.target_laps"))}<input type="number" name="targetLaps" min="1" step="1" value="500" required /></label>
|
||||
<div class="training-presets" aria-label="${escapeHtml(t("training.quick_targets"))}">
|
||||
${[500, 1000, 1500].map((value) => `<button type="button" class="btn btn-mini training-preset" data-laps="${value}">${value}</button>`).join("")}
|
||||
</div>
|
||||
<label>${escapeHtml(t("training.goal_name"))}<input name="title" placeholder="${escapeHtml(t("training.goal_name_placeholder"))}" /></label>
|
||||
<label>${escapeHtml(t("training.reward"))}<input name="reward" placeholder="${escapeHtml(t("training.reward_placeholder"))}" /></label>
|
||||
<button class="btn btn-primary" type="submit">${escapeHtml(t("training.add_goal"))}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.goals"))}</h3><span class="badge">${goals.length}</span></div>
|
||||
<div class="panel-body training-goals">${goalMarkup}</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
document.getElementById("training-driver")?.addEventListener("change", (event) => {
|
||||
setSelectedDriverId(event.currentTarget.value);
|
||||
renderView();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function trainingStat(label, value, note) {
|
||||
return `<article class="stat-card"><p>${label}</p><h3>${value}</h3><small>${note}</small></article>`;
|
||||
}
|
||||
Reference in New Issue
Block a user