Explore: personal GPS heatmap tab under Athlete page
- bincio/explore.py: bake_tracks() simplifies GPS coords (RDP ε=0.0001), strips to [lng,lat], groups by sport type, writes per-handle tracks.json - bake-tracks CLI command; render CLI calls _bake_tracks() after each build; strava_zip runs it once at end of batch - /api/me/tracks endpoint serves the baked file; wipe_user cleans it up - Explore.svelte: MapLibre full-screen map with sidebar — type pills, year/month date filter, Lines / Heatmap (global or by-type) view modes - AthleteView: Explore tab visible only to profile owner (checks __bincioMe) - Base.astro: fullscreen prop + Planner nav link
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import MmpChart from './MmpChart.svelte';
|
||||
import RecordsView from './RecordsView.svelte';
|
||||
import AthleteDrawer from './AthleteDrawer.svelte';
|
||||
import Explore from './Explore.svelte';
|
||||
import { isUnlisted, formatElapsed, formatDistance, sportIcon } from '../lib/format';
|
||||
import { loadIndex, loadAthlete } from '../lib/dataloader';
|
||||
|
||||
@@ -21,9 +22,10 @@
|
||||
let error: string | null = null;
|
||||
let drawerOpen = false;
|
||||
|
||||
type Tab = 'power' | 'records' | 'segments' | 'profile';
|
||||
type Tab = 'power' | 'records' | 'segments' | 'profile' | 'explore';
|
||||
let activeTab: Tab = 'power';
|
||||
let mounted = false;
|
||||
let isOwner = false;
|
||||
|
||||
interface SegmentSummaryItem {
|
||||
segment: { id: string; name: string; sport: string | null; distance_m: number };
|
||||
@@ -80,9 +82,11 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const TABS: Tab[] = ['power', 'records', 'segments', 'profile'];
|
||||
isOwner = (window as any).__bincioMe === handle;
|
||||
const TABS: Tab[] = ['power', 'records', 'segments', 'profile', 'explore'];
|
||||
const rawTab = new URLSearchParams(window.location.search).get('tab');
|
||||
activeTab = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : 'power';
|
||||
const resolved = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : 'power';
|
||||
activeTab = (resolved === 'explore' && !isOwner) ? 'power' : resolved;
|
||||
mounted = true;
|
||||
|
||||
// Resolve handle for the segments endpoint
|
||||
@@ -140,12 +144,14 @@
|
||||
return hi >= 900 ? `${lo}+ bpm` : `${lo}–${hi} bpm`;
|
||||
}
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean }[] = [
|
||||
{ key: 'power', label: 'Power Curve' },
|
||||
{ key: 'records', label: 'Records' },
|
||||
{ key: 'segments', label: 'Segments' },
|
||||
{ key: 'profile', label: 'Profile' },
|
||||
{ key: 'explore', label: 'Explore', ownerOnly: true },
|
||||
];
|
||||
$: TABS = ALL_TABS.filter(t => !t.ownerOnly || isOwner);
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -325,6 +331,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Explore tab -->
|
||||
{:else if activeTab === 'explore'}
|
||||
<div style="height: calc(100vh - 200px); margin: 0 -1rem -1.5rem;">
|
||||
<Explore {handle} {base} embedded={true} />
|
||||
</div>
|
||||
|
||||
<!-- Profile tab -->
|
||||
{:else if activeTab === 'profile'}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
Reference in New Issue
Block a user