29c6e399c0
Write the activity summary to sessionStorage on click in ActivityFeed, then read it synchronously at module init in ActivityDetailLoader so the page renders immediately without the "Loading activity…" blank screen or the 2 round-trip index fetch. Direct URL / bookmark / shared link falls through to the existing slow path unchanged.
133 lines
4.6 KiB
Svelte
133 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { loadIndexPaged } from '../lib/dataloader';
|
|
import ActivityDetail from './ActivityDetail.svelte';
|
|
import { isUnlisted } from '../lib/format';
|
|
import type { ActivitySummary, BASIndex } from '../lib/types';
|
|
|
|
export let base: string = '/';
|
|
|
|
// Synchronous init — check sessionStorage before first render so "Loading activity…"
|
|
// is never shown when navigating from the feed.
|
|
const _activityId = window.location.pathname.match(/\/activity\/([^/]+)/)?.[1] ?? null;
|
|
let _initial: ActivitySummary | null = null;
|
|
if (_activityId) {
|
|
try {
|
|
const c = sessionStorage.getItem(`bincio:activity:${_activityId}`);
|
|
if (c) _initial = JSON.parse(c);
|
|
} catch {}
|
|
}
|
|
|
|
let activity: ActivitySummary | null = _initial;
|
|
let notFound = _activityId === null;
|
|
let loading = _activityId !== null && _initial === null;
|
|
|
|
/**
|
|
* Build an ActivitySummary stub from a detail JSON object.
|
|
* Used when we fetch the detail file directly without going through the index.
|
|
*/
|
|
function summaryFromDetail(d: any, detailUrl: string, handle?: string): ActivitySummary {
|
|
return {
|
|
id: d.id,
|
|
title: d.title ?? d.id,
|
|
sport: d.sport ?? 'other',
|
|
sub_sport: d.sub_sport ?? null,
|
|
started_at: d.started_at ?? '',
|
|
distance_m: d.distance_m ?? null,
|
|
duration_s: d.duration_s ?? null,
|
|
moving_time_s: d.moving_time_s ?? null,
|
|
elevation_gain_m: d.elevation_gain_m ?? null,
|
|
avg_speed_kmh: d.avg_speed_kmh ?? null,
|
|
max_speed_kmh: d.max_speed_kmh ?? null,
|
|
avg_hr_bpm: d.avg_hr_bpm ?? null,
|
|
max_hr_bpm: d.max_hr_bpm ?? null,
|
|
avg_cadence_rpm: d.avg_cadence_rpm ?? null,
|
|
avg_power_w: d.avg_power_w ?? null,
|
|
mmp: d.mmp ?? null,
|
|
source: d.source ?? null,
|
|
privacy: d.privacy ?? 'public',
|
|
detail_url: detailUrl,
|
|
track_url: d.bbox && d.privacy !== 'no_gps'
|
|
? detailUrl.replace(/\.json$/, '.geojson')
|
|
: null,
|
|
preview_coords: null,
|
|
...(handle ? { handle } : {}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fallback: fetch the activity detail file directly without loading the index.
|
|
* Tries single-user path first, then each multi-user handle shard.
|
|
*/
|
|
async function fetchActivityDirect(id: string): Promise<ActivitySummary | null> {
|
|
// Single-user: public/data → _merged/, so activities/ resolves directly
|
|
try {
|
|
const url = `${base}data/activities/${id}.json`;
|
|
const r = await fetch(url);
|
|
if (r.ok) {
|
|
const d = await r.json();
|
|
if (d.id === id) return summaryFromDetail(d, `activities/${id}.json`);
|
|
}
|
|
} catch { /* fall through */ }
|
|
|
|
// Multi-user: try each handle shard
|
|
try {
|
|
const r = await fetch(`${base}data/index.json`);
|
|
if (!r.ok) return null;
|
|
const root: BASIndex = await r.json();
|
|
for (const shard of (root.shards ?? [])) {
|
|
if (!shard.handle) continue;
|
|
const url = `${base}data/${shard.handle}/_merged/activities/${id}.json`;
|
|
try {
|
|
const dr = await fetch(url);
|
|
if (!dr.ok) continue;
|
|
const d = await dr.json();
|
|
if (d.id === id) {
|
|
return summaryFromDetail(
|
|
d,
|
|
`${shard.handle}/_merged/activities/${id}.json`,
|
|
shard.handle,
|
|
);
|
|
}
|
|
} catch { /* try next */ }
|
|
}
|
|
} catch { /* ignore */ }
|
|
return null;
|
|
}
|
|
|
|
onMount(async () => {
|
|
if (!loading) return; // fast path: summary already from sessionStorage
|
|
|
|
const id = _activityId!;
|
|
try {
|
|
// Load only the most-recent year shard — avoids downloading all years just
|
|
// to look up one activity. Falls back to a direct file fetch if not found.
|
|
const { index } = await loadIndexPaged(base);
|
|
activity = index.activities.find(a => a.id === id) ?? null;
|
|
|
|
if (!activity) {
|
|
// Not in first year shard (old activity) or shard fetch failed —
|
|
// fetch the detail file directly to avoid loading all remaining shards.
|
|
activity = await fetchActivityDirect(id);
|
|
}
|
|
|
|
if (!activity) notFound = true;
|
|
} catch {
|
|
notFound = true;
|
|
}
|
|
loading = false;
|
|
});
|
|
</script>
|
|
|
|
{#if loading}
|
|
<p class="text-zinc-500 text-sm mt-8 text-center">Loading activity…</p>
|
|
{:else if notFound}
|
|
<div class="text-center mt-16">
|
|
<p class="text-zinc-400 text-sm mb-2">Activity not found.</p>
|
|
<p class="text-zinc-600 text-xs">It may still be processing — try refreshing in a moment.</p>
|
|
<a href={base} class="mt-4 inline-block text-blue-400 hover:text-blue-300 text-sm">← Back to feed</a>
|
|
</div>
|
|
{:else if activity}
|
|
<ActivityDetail {activity} {base} />
|
|
{/if}
|