map now working

This commit is contained in:
Davide Scaini
2026-03-28 19:34:22 +01:00
parent 5d58126d2f
commit 3441079913
18 changed files with 1489 additions and 10 deletions
+140
View File
@@ -0,0 +1,140 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { ActivitySummary, ActivityDetail } from '../lib/types';
import { formatDistance, formatDuration, formatElevation, formatSpeed, formatDate, formatTime, sportIcon, sportLabel, sportColor } from '../lib/format';
import ActivityMap from './ActivityMap.svelte';
import ActivityCharts from './ActivityCharts.svelte';
export let activity: ActivitySummary;
export let base: string = '/';
let detail: ActivityDetail | null = null;
let error = '';
// Linked hover index shared between map and charts
let hoveredIdx: number | null = null;
onMount(async () => {
if (!activity.detail_url) return;
try {
const res = await fetch(`${base}data/${activity.detail_url}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
detail = await res.json();
} catch (e: any) {
error = e.message;
}
});
$: trackUrl = activity.track_url ? `${base}data/${activity.track_url}` : null;
$: color = sportColor(activity.sport);
const stat = (label: string, value: string) => ({ label, value });
$: stats = [
stat('Distance', formatDistance(activity.distance_m)),
stat('Moving time', formatDuration(activity.moving_time_s ?? activity.duration_s)),
stat('Elevation ↑', formatElevation(activity.elevation_gain_m)),
stat('Avg speed', formatSpeed(activity.avg_speed_kmh)),
stat('Max speed', formatSpeed(activity.max_speed_kmh)),
stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—'),
stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—'),
stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—'),
];
</script>
<!-- Header -->
<div class="flex items-start gap-4 mb-6">
<a href={`${base}`} class="text-zinc-500 hover:text-white transition-colors mt-1 shrink-0">
← Back
</a>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span
class="text-xs font-medium px-2 py-0.5 rounded-full"
style="background:{color}22;color:{color}"
>
{sportIcon(activity.sport)} {sportLabel(activity.sport, activity.sub_sport)}
</span>
<span class="text-xs text-zinc-500">
{formatDate(activity.started_at)} · {formatTime(activity.started_at)}
</span>
</div>
<h1 class="text-2xl font-bold text-white">{activity.title}</h1>
{#if detail?.description}
<p class="text-zinc-400 mt-1 text-sm">{detail.description}</p>
{/if}
</div>
</div>
<!-- Map + Stats split -->
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-4 mb-4">
<!-- Map -->
<div class="h-[400px] lg:h-[420px] rounded-xl overflow-hidden bg-zinc-800">
{#if trackUrl}
<ActivityMap
{trackUrl}
timeseries={detail?.timeseries ?? null}
bbox={detail?.bbox ?? null}
accentColor={color}
bind:hoveredIdx
/>
{:else}
<div class="w-full h-full flex items-center justify-center text-zinc-600 text-sm">
No GPS track
</div>
{/if}
</div>
<!-- Stats panel -->
<div class="grid grid-cols-2 lg:grid-cols-1 gap-px bg-zinc-800 rounded-xl overflow-hidden">
{#each stats as s}
<div class="bg-zinc-900 px-4 py-3">
<p class="text-2xl font-bold text-white">{s.value}</p>
<p class="text-xs text-zinc-500">{s.label}</p>
</div>
{/each}
{#if detail?.gear}
<div class="bg-zinc-900 px-4 py-3 col-span-2 lg:col-span-1">
<p class="text-sm font-medium text-zinc-300">{detail.gear}</p>
<p class="text-xs text-zinc-500">Gear</p>
</div>
{/if}
</div>
</div>
<!-- Charts -->
{#if error}
<p class="text-red-400 text-sm mt-4">{error}</p>
{:else if detail?.timeseries && detail.timeseries.t.length > 0}
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
<ActivityCharts timeseries={detail.timeseries} bind:hoveredIdx />
</div>
{:else if !detail}
<div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4 h-32 animate-pulse" />
{/if}
<!-- Laps -->
{#if detail?.laps?.length}
<div class="mt-4 bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
<table class="w-full text-sm">
<thead class="border-b border-zinc-800">
<tr class="text-left text-zinc-500 text-xs">
<th class="px-4 py-2">Lap</th>
<th class="px-4 py-2">Distance</th>
<th class="px-4 py-2">Time</th>
<th class="px-4 py-2">Avg speed</th>
<th class="px-4 py-2">Avg HR</th>
</tr>
</thead>
<tbody>
{#each detail.laps as lap}
<tr class="border-b border-zinc-800/50 hover:bg-zinc-800/50">
<td class="px-4 py-2 text-zinc-400">#{lap.index + 1}</td>
<td class="px-4 py-2 text-white">{formatDistance(lap.distance_m)}</td>
<td class="px-4 py-2 text-white">{formatDuration(lap.duration_s)}</td>
<td class="px-4 py-2 text-white">{formatSpeed(lap.avg_speed_kmh)}</td>
<td class="px-4 py-2 text-white">{lap.avg_hr_bpm ? `${lap.avg_hr_bpm} bpm` : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}