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 (
{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)}
{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
No activities yet
Import your Garmin or Strava data to get started