import { Link, useNavigate } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts' import { startOfWeek, format, subWeeks, eachWeekOfInterval, subDays, addDays } from 'date-fns' import api from '../utils/api' import StatCard from '../components/ui/StatCard' import { formatDuration, formatDistance, formatPace, formatHeartRate, formatDate, sportIcon, formatSleep, } from '../utils/format' function bbLevelColor(level) { if (level == null) return '#6b7280' if (level >= 75) return '#3b82f6' if (level >= 50) return '#22c55e' if (level >= 25) return '#f59e0b' return '#ef4444' } function MiniBodyBattery({ bb }) { if (!bb?.end_level && !bb?.charged) return null const { charged, drained, start_level, end_level, values } = bb const color = bbLevelColor(end_level) const sparkData = Array.isArray(values) ? values.map(([ts, level]) => ({ ts, level })) : [] return (

Body Battery

View →
{end_level != null && ( {Math.round(end_level)} )} {charged != null && ( +{charged} )} {drained != null && ( -{drained} )}
{start_level != null && end_level != null && (

{start_level} → {end_level} today

)} {sparkData.length >= 2 && (
format(new Date(ts), 'HH:mm')} formatter={v => [`${Math.round(v)}`, 'Battery']} />
)}
) } function WeeklyChart({ activities }) { const navigate = useNavigate() if (!activities?.length) return (
No activities yet
) // Build last 8 weeks in chronological order const now = new Date() const weeks = eachWeekOfInterval({ start: subWeeks(startOfWeek(now), 7), end: startOfWeek(now), }) const data = weeks.map(weekStart => { const weekKey = format(weekStart, 'MMM d') const weekEnd = addDays(weekStart, 7) const km = activities .filter(a => { const t = new Date(a.start_time) return t >= weekStart && t < weekEnd }) .reduce((s, a) => s + (a.distance_m || 0) / 1000, 0) return { week: weekKey, km: parseFloat(km.toFixed(2)), weekStartISO: format(weekStart, 'yyyy-MM-dd'), weekEndISO: format(weekEnd, 'yyyy-MM-dd'), } }) const handleBarClick = (entry) => { if (entry?.activePayload?.[0]?.payload) { const { weekStartISO, weekEndISO } = entry.activePayload[0].payload navigate(`/activities?from=${weekStartISO}&to=${weekEndISO}`) } } return ( `${v.toFixed(0)}`} /> [`${v.toFixed(1)} km`, 'Distance']} cursor={{ fill: 'rgba(59,130,246,0.1)' }} /> ) } export default function DashboardPage() { const { data: recentActivities } = useQuery({ queryKey: ['activities-recent'], queryFn: () => api.get('/activities/', { params: { per_page: 10 } }).then(r => r.data), }) const { data: allActivities } = useQuery({ queryKey: ['activities-all-chart'], queryFn: () => api.get('/activities/', { params: { per_page: 100, from_date: subDays(new Date(), 60).toISOString() }, }).then(r => r.data), }) const { data: healthSummary } = useQuery({ queryKey: ['health-summary'], queryFn: () => api.get('/health-metrics/summary').then(r => r.data), }) const { data: records } = useQuery({ queryKey: ['records-running'], queryFn: () => api.get('/records/', { params: { sport_type: 'running' } }).then(r => r.data), }) const { data: ytdStats } = useQuery({ queryKey: ['ytd-stats'], queryFn: () => api.get('/activities/stats/ytd').then(r => r.data), }) const latest = healthSummary?.latest return (

Dashboard

+ Import data

Weekly distance (km)

Health today

{latest ? ( <> {[ ['HRV', latest.hrv_nightly_avg ? `${Math.round(latest.hrv_nightly_avg)} ms` : '--'], ['Sleep score', latest.sleep_score ? Math.round(latest.sleep_score) : '--'], ['Steps', latest.steps?.toLocaleString() ?? '--'], ['VO2 Max', latest.vo2max ? latest.vo2max.toFixed(1) : '--'], ['Stress', latest.avg_stress ? Math.round(latest.avg_stress) : '--'], ].map(([label, val]) => (
{label} {val}
))} View full health dashboard → ) : (

No health data. Import a Garmin export.

)}
{/* Recent activities */}

Recent activities

View all →
{recentActivities?.slice(0, 5).map(activity => ( {sportIcon(activity.sport_type)}

{activity.name}

{formatDate(activity.start_time)}

{formatDistance(activity.distance_m)}

dist

{formatDuration(activity.duration_s)}

time

{formatHeartRate(activity.avg_heart_rate)}

HR

))} {!recentActivities?.length && (

No activities yet — import some data

)}
{records?.length > 0 && (

Running PRs

View all →
{records.slice(0, 5).map(rec => (

{rec.distance_label}

{formatDuration(rec.duration_s)}

))}
)}
) }