map now working
This commit is contained in:
@@ -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}
|
||||
Reference in New Issue
Block a user