Add segments, YTD stats, route matching fixes, body battery layout, pace fix
- Segments page: new /segments route with auto-generate (1km splits, turn detection, hill detection), manual segment creation, per-segment performance times across matched activities; fixed auth on existing segment endpoints - YTD distance: new /activities/stats/ytd endpoint; Dashboard replaces 'Total distance' with 'Running this year' + 'Cycling this year'; Activities page shows YTD stats row - Weekly chart click: clicking a Dashboard bar navigates to Activities filtered to that week; Activities reads from/to query params with dismissable chip - Route matching: add ±2.5% distance gate + 3% relative DTW threshold (was flat 80m); tighten candidate pre-filter from 80/120% to 95/105% - Body battery layout: HR chart and body battery now side-by-side at same height on large screens instead of stacked full-width - Pace display fix: MetricTimeline clamps GPS speed outliers before computing Y-axis domain; tick formatter guards against v<=0 or v>25 m/s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { format } from 'date-fns'
|
||||
import api from '../utils/api'
|
||||
import {
|
||||
formatDuration, formatDistance, formatPace, formatHeartRate,
|
||||
@@ -10,24 +11,38 @@ import {
|
||||
const SPORTS = ['all', 'running', 'cycling', 'hiking', 'walking']
|
||||
|
||||
export default function ActivitiesPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const [sport, setSport] = useState('all')
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const fromParam = searchParams.get('from')
|
||||
const toParam = searchParams.get('to')
|
||||
|
||||
const { data: activities, isLoading } = useQuery({
|
||||
queryKey: ['activities', sport, page],
|
||||
queryKey: ['activities', sport, page, fromParam, toParam],
|
||||
queryFn: () =>
|
||||
api.get('/activities/', {
|
||||
params: {
|
||||
sport_type: sport === 'all' ? undefined : sport,
|
||||
page,
|
||||
per_page: 20,
|
||||
from_date: fromParam ? new Date(fromParam).toISOString() : undefined,
|
||||
to_date: toParam ? new Date(toParam + 'T23:59:59').toISOString() : undefined,
|
||||
},
|
||||
}).then(r => r.data),
|
||||
})
|
||||
|
||||
const { data: ytdStats } = useQuery({
|
||||
queryKey: ['ytd-stats'],
|
||||
queryFn: () => api.get('/activities/stats/ytd').then(r => r.data),
|
||||
})
|
||||
|
||||
const clearDateFilter = () => navigate('/activities')
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-bold text-white">Activities</h1>
|
||||
<Link
|
||||
to="/upload"
|
||||
@@ -37,6 +52,28 @@ export default function ActivitiesPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* YTD stats */}
|
||||
{ytdStats && (
|
||||
<div className="flex gap-4 mb-4 text-sm">
|
||||
{ytdStats.running_km > 0 && (
|
||||
<span className="text-blue-400">🏃 {ytdStats.running_km.toFixed(0)} km this year</span>
|
||||
)}
|
||||
{ytdStats.cycling_km > 0 && (
|
||||
<span className="text-orange-400">🚴 {ytdStats.cycling_km.toFixed(0)} km this year</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date filter chip */}
|
||||
{fromParam && (
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-xs bg-blue-600/20 text-blue-300 border border-blue-500/30 px-3 py-1 rounded-full">
|
||||
Week of {format(new Date(fromParam), 'MMM d, yyyy')}
|
||||
</span>
|
||||
<button onClick={clearDateFilter} className="text-xs text-gray-500 hover:text-gray-300 transition-colors">✕ Clear</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sport filter */}
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
{SPORTS.map(s => (
|
||||
|
||||
Reference in New Issue
Block a user