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)}
))}
)}
)
}