Fix follow-ups: lap bests, segments, charts, dashboard health
- Lap bests: compare against OTHER activities on the route (exclude self), so single-activity routes no longer show every lap as "best" - Segment create: POST to trailing-slash URL (was a 307 that dropped the body); surface errors in the UI - PR splits: scale GPS distance stream to the activity's official distance so over-measured GPS no longer yields bogus split PRs - Speed route colours: red->orange->green->blue->purple (slow->fast) with smooth interpolation + a Slow/Fast gradient key under the map - Health body battery: snap activity highlight to the categorical axis; white tooltip text + % suffix - Health weight: y-min = lowest weight - 20kg; st/lb hover shows total lbs too - Health sleep: move 8h/avg reference labels into the right margin - Dashboard: Health-today pulls latest non-null values (sleep score, VO2 max); body battery tile renders a condensed colour-graded intraday graph Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useParams } from 'react-router-dom'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState, useMemo } from 'react'
|
||||
import api from '../utils/api'
|
||||
import ActivityMap from '../components/activity/ActivityMap'
|
||||
import ActivityMap, { SPEED_GRADIENT } from '../components/activity/ActivityMap'
|
||||
import MetricTimeline from '../components/activity/MetricTimeline'
|
||||
import HRZoneBar from '../components/activity/HRZoneBar'
|
||||
import LapTable from '../components/activity/LapTable'
|
||||
@@ -81,16 +81,22 @@ export default function ActivityDetailPage() {
|
||||
setSegPoints(prev => (prev.length >= 2 ? [{ distance_m: dist }] : [...prev, { distance_m: dist }]))
|
||||
}
|
||||
|
||||
const [segError, setSegError] = useState('')
|
||||
const createSegment = async () => {
|
||||
const [a, b] = segPoints
|
||||
await api.post('/segments', {
|
||||
name: segName.trim() || 'Segment',
|
||||
activity_id: Number(id),
|
||||
start_distance_m: a.distance_m,
|
||||
end_distance_m: b.distance_m,
|
||||
})
|
||||
setSegCreate(false); setSegPoints([]); setSegName('')
|
||||
qc.invalidateQueries({ queryKey: ['activity-segments', id] })
|
||||
setSegError('')
|
||||
try {
|
||||
await api.post('/segments/', {
|
||||
name: segName.trim() || 'Segment',
|
||||
activity_id: Number(id),
|
||||
start_distance_m: a.distance_m,
|
||||
end_distance_m: b.distance_m,
|
||||
})
|
||||
setSegCreate(false); setSegPoints([]); setSegName('')
|
||||
qc.invalidateQueries({ queryKey: ['activity-segments', id] })
|
||||
} catch (e) {
|
||||
setSegError(e.response?.data?.detail || 'Failed to create segment')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMetric = (key) => {
|
||||
@@ -230,6 +236,7 @@ export default function ActivityDetailPage() {
|
||||
{segPoints.length > 0 && (
|
||||
<button onClick={() => setSegPoints([])} className="text-gray-400 hover:text-white">Reset</button>
|
||||
)}
|
||||
{segError && <span className="text-red-400">{segError}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ height: mapHeight }}>
|
||||
@@ -243,6 +250,13 @@ export default function ActivityDetailPage() {
|
||||
onMapClick={segCreate ? handleMapClick : undefined}
|
||||
/>
|
||||
</div>
|
||||
{colorMode === 'speed' && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-t border-gray-800">
|
||||
<span className="text-xs text-gray-500">Slow</span>
|
||||
<div className="h-2 flex-1 max-w-xs rounded-full" style={{ background: SPEED_GRADIENT }} />
|
||||
<span className="text-xs text-gray-500">Fast</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Metric timeline */}
|
||||
|
||||
Reference in New Issue
Block a user