import { useParams } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { useState, useMemo } from 'react' import api from '../utils/api' import ActivityMap from '../components/activity/ActivityMap' import MetricTimeline from '../components/activity/MetricTimeline' import HRZoneBar from '../components/activity/HRZoneBar' import LapTable from '../components/activity/LapTable' import StatCard from '../components/ui/StatCard' import { formatDuration, formatDistance, formatPace, formatElevation, formatHeartRate, formatDateTime, formatCadence, sportIcon, } from '../utils/format' const METRICS = [ { key: 'heart_rate', label: 'Heart Rate', unit: 'bpm', color: '#f43f5e' }, { key: 'speed_ms', label: 'Pace / Speed', unit: '', color: '#3b82f6' }, { key: 'altitude_m', label: 'Elevation', unit: 'm', color: '#84cc16' }, { key: 'cadence', label: 'Cadence', unit: '', color: '#f97316' }, { key: 'power', label: 'Power', unit: 'W', color: '#a855f7' }, { key: 'temperature_c', label: 'Temperature', unit: '°C', color: '#06b6d4' }, ] export default function ActivityDetailPage() { const { id } = useParams() const [activeMetrics, setActiveMetrics] = useState(['heart_rate', 'speed_ms', 'altitude_m']) const [hoveredDistance, setHoveredDistance] = useState(null) const [mapHeight, setMapHeight] = useState(420) const [mapType, setMapType] = useState('dark') const { data: activity, isLoading } = useQuery({ queryKey: ['activity', id], queryFn: () => api.get(`/activities/${id}`).then(r => r.data), }) const { data: dataPoints } = useQuery({ queryKey: ['activity-points', id], queryFn: () => api.get(`/activities/${id}/data-points?downsample=3`).then(r => r.data), enabled: !!activity, }) const { data: laps } = useQuery({ queryKey: ['activity-laps', id], queryFn: () => api.get(`/activities/${id}/laps`).then(r => r.data), enabled: !!activity, }) const toggleMetric = (key) => { setActiveMetrics(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key] ) } // Check which metrics have actual data const availableMetrics = useMemo(() => { if (!dataPoints?.length) return new Set() return new Set( METRICS .filter(m => dataPoints.some(p => p[m.key] != null && p[m.key] !== 0)) .map(m => m.key) ) }, [dataPoints]) if (isLoading) { return
Loading activity…
} if (!activity) return null return (
{/* Header */}
{sportIcon(activity.sport_type)}

{activity.name}

{formatDateTime(activity.start_time)}

{/* Primary stats */}
{/* Secondary stats */}
{/* Map with controls */}
{/* Map toolbar */}
Map style: {['dark', 'street', 'satellite'].map(t => ( ))}
Height: {[280, 420, 560].map(h => ( ))}
{/* HR Zones */} {activity.hr_zones && Object.values(activity.hr_zones).some(v => v > 0) && (

Heart Rate Zones

)} {/* Metric timeline */}

Activity Timeline

{METRICS.filter(m => availableMetrics.has(m.key)).map(({ key, label, color }) => ( ))}
{dataPoints && dataPoints.length > 0 ? ( availableMetrics.has(m))} metrics={METRICS} onHoverDistance={setHoveredDistance} sportType={activity.sport_type} /> ) : (

No timeline data available for this activity

)}
{/* Laps */} {laps && laps.length > 0 && (

Laps

)}
) }