athlete page first draft

This commit is contained in:
Davide Scaini
2026-03-30 09:05:18 +02:00
parent 2a1493a3e5
commit ec6175b143
8 changed files with 594 additions and 3 deletions
+111
View File
@@ -0,0 +1,111 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { AthleteJson, BASIndex, ActivitySummary } from '../lib/types';
import MmpChart from './MmpChart.svelte';
let athlete: AthleteJson | null = null;
let activities: ActivitySummary[] = [];
let loading = true;
let error: string | null = null;
onMount(async () => {
try {
const [athleteRes, indexRes] = await Promise.all([
fetch(`${import.meta.env.BASE_URL}data/athlete.json`),
fetch(`${import.meta.env.BASE_URL}data/index.json`),
]);
if (!athleteRes.ok) throw new Error('athlete.json not found — run bincio extract first');
athlete = await athleteRes.json();
const index: BASIndex = await indexRes.json();
// Only activities with power data contribute to the curve
activities = index.activities.filter(a => a.mmp && a.privacy !== 'private');
} catch (e: any) {
error = e.message;
} finally {
loading = false;
}
});
function fmtZone(zones: [number, number][], i: number): string {
const [lo, hi] = zones[i];
return hi >= 9000 ? `${lo}+ W` : `${lo}${hi} W`;
}
function fmtHrZone(zones: [number, number][], i: number): string {
const [lo, hi] = zones[i];
return hi >= 900 ? `${lo}+ bpm` : `${lo}${hi} bpm`;
}
</script>
{#if loading}
<p class="text-zinc-400 text-sm">Loading…</p>
{:else if error}
<p class="text-red-400 text-sm">{error}</p>
{:else if athlete}
<!-- Power curve section -->
<section class="mb-10">
<h2 class="text-lg font-semibold text-white mb-4">Power Curve</h2>
{#if athlete.power_curve.all_time}
<div class="bg-zinc-900 rounded-xl p-4 border border-zinc-800">
<MmpChart {athlete} {activities} />
</div>
{:else}
<p class="text-zinc-500 text-sm">No power data found. Make sure your activities include power meter data and re-run <code class="text-zinc-300">bincio extract</code>.</p>
{/if}
</section>
<!-- Profile section -->
<section>
<h2 class="text-lg font-semibold text-white mb-4">Profile</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Key numbers -->
<div class="bg-zinc-900 rounded-xl p-4 border border-zinc-800 space-y-3">
<h3 class="text-sm font-medium text-zinc-400 uppercase tracking-wide">Key numbers</h3>
{#if athlete.max_hr}
<div class="flex justify-between text-sm">
<span class="text-zinc-400">Max HR</span>
<span class="text-white font-medium">{athlete.max_hr} bpm</span>
</div>
{/if}
{#if athlete.ftp_w}
<div class="flex justify-between text-sm">
<span class="text-zinc-400">FTP</span>
<span class="text-white font-medium">{athlete.ftp_w} W</span>
</div>
{/if}
{#if !athlete.max_hr && !athlete.ftp_w}
<p class="text-zinc-500 text-sm">Set <code>athlete.max_hr</code> and <code>athlete.ftp_w</code> in your config.</p>
{/if}
</div>
<!-- HR zones -->
{#if athlete.hr_zones}
<div class="bg-zinc-900 rounded-xl p-4 border border-zinc-800 space-y-2">
<h3 class="text-sm font-medium text-zinc-400 uppercase tracking-wide">HR Zones</h3>
{#each athlete.hr_zones as zone, i}
<div class="flex justify-between items-center text-sm">
<span class="text-zinc-400">Z{i + 1}</span>
<span class="text-white">{fmtHrZone(athlete.hr_zones, i)}</span>
</div>
{/each}
</div>
{/if}
<!-- Power zones -->
{#if athlete.power_zones}
<div class="bg-zinc-900 rounded-xl p-4 border border-zinc-800 space-y-2">
<h3 class="text-sm font-medium text-zinc-400 uppercase tracking-wide">Power Zones</h3>
{#each athlete.power_zones as zone, i}
<div class="flex justify-between items-center text-sm">
<span class="text-zinc-400">Z{i + 1}</span>
<span class="text-white">{fmtZone(athlete.power_zones, i)}</span>
</div>
{/each}
</div>
{/if}
</div>
</section>
{/if}