import { useState, useEffect } from 'react' import { Link, useSearchParams } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import api from '../utils/api' import { formatDuration, formatDistance, formatPace, formatHeartRate, formatElevation, formatDate, sportColor, distanceUnitLabel, } from '../utils/format' import { useUnit } from '../hooks/useUnits' import SportIcon from '../components/ui/SportIcon' const KM_PER_MI = 1.609344 const FALLBACK_SPORTS = ['running', 'cycling', 'hiking', 'walking'] export default function ActivitiesPage() { const [searchParams] = useSearchParams() const unit = useUnit() const distLabel = distanceUnitLabel(unit) const [page, setPage] = useState(1) // Filters const [sport, setSport] = useState('all') const [year, setYear] = useState('all') const [minDist, setMinDist] = useState('') const [maxDist, setMaxDist] = useState('') const [fromDate, setFromDate] = useState(searchParams.get('from') || '') const [toDate, setToDate] = useState(searchParams.get('to') || '') // Deep link from the dashboard weekly chart arrives as ?from&to. useEffect(() => { const f = searchParams.get('from'); const t = searchParams.get('to') if (f) setFromDate(f) if (t) setToDate(t) }, [searchParams]) // Reset to page 1 whenever a filter changes. useEffect(() => { setPage(1) }, [sport, year, minDist, maxDist, fromDate, toDate]) const { data: filterOpts } = useQuery({ queryKey: ['activity-filters'], queryFn: () => api.get('/activities/stats/filters').then(r => r.data), }) const sportTypes = filterOpts?.sport_types?.length ? filterOpts.sport_types : FALLBACK_SPORTS const years = filterOpts?.years || [] const toKm = v => { const n = parseFloat(v) if (isNaN(n)) return undefined return unit === 'mi' ? n * KM_PER_MI : n } const { data: activities, isLoading } = useQuery({ queryKey: ['activities', sport, year, minDist, maxDist, fromDate, toDate, page, unit], queryFn: () => api.get('/activities/', { params: { sport_type: sport === 'all' ? undefined : sport, year: year === 'all' ? undefined : year, min_distance_km: toKm(minDist), max_distance_km: toKm(maxDist), from_date: fromDate ? new Date(fromDate).toISOString() : undefined, to_date: toDate ? new Date(toDate + 'T23:59:59').toISOString() : undefined, page, per_page: 20, }, }).then(r => r.data), }) const anyFilter = sport !== 'all' || year !== 'all' || minDist || maxDist || fromDate || toDate const clearFilters = () => { setSport('all'); setYear('all'); setMinDist(''); setMaxDist(''); setFromDate(''); setToDate('') } // Totals for the visible results — only meaningful (complete) when everything // fits on one page; otherwise they'd be a misleading partial sum. const singlePage = (activities?.length ?? 0) < 20 const totalDistanceM = activities?.reduce((sum, a) => sum + (a.distance_m || 0), 0) || 0 const totalDurationS = activities?.reduce((sum, a) => sum + (a.duration_s || 0), 0) || 0 return (

Activities

📈 Summary + Import
{/* Sport filter chips */}
{['all', ...sportTypes].map(s => ( ))}
{/* Year / distance / date filters */}
{anyFilter && ( )}
{/* Filtered totals (only when the result set is a single page) */} {anyFilter && activities?.length > 0 && singlePage && (
{formatDistance(totalDistanceM, unit)} ·{' '} {formatDuration(totalDurationS)} ·{' '} {activities.length} {activities.length === 1 ? 'activity' : 'activities'}
)} {/* Activity list */} {isLoading ? (
Loading…
) : (
{activities?.map(activity => ( {/* Sport indicator */}
{/* Name + date */}

{activity.name}

{activity.original_name && (

orig. {activity.original_name}

)}

{formatDate(activity.start_time)}

{/* Compact metrics line — the full metrics column is hidden below sm */}

{formatDistance(activity.distance_m, unit)} · {formatDuration(activity.duration_s)} · {formatPace(activity.avg_speed_ms, activity.sport_type, unit)}

{/* Metrics */}

{formatDistance(activity.distance_m, unit)}

distance

{formatDuration(activity.duration_s)}

time

{formatPace(activity.avg_speed_ms, activity.sport_type, unit)}

pace

{formatHeartRate(activity.avg_heart_rate)}

avg HR

{activity.elevation_gain_m ? `↑ ${formatElevation(activity.elevation_gain_m, unit)}` : '--'}

elev

))} {activities?.length === 0 && (

No activities yet

Import your Garmin or Strava data to get started

)}
)} {/* Pagination */} {activities?.length === 20 && (
Page {page}
)}
) }