Add medals, HRV status dots, smooth segment hover, side-by-side map/timeline, HR zone times
Build and push images / validate (push) Successful in 3s
Build and push images / build-frontend (push) Successful in 10s
Build and push images / build-backend (push) Successful in 6s
Build and push images / build-worker (push) Successful in 5s

- Silver/bronze medals (not just gold) on route & segment leaderboards
- Colour HRV nightly-avg trend dots: orange unbalanced, red low
- Project segment-hover dot smoothly along the track line (interpolated)
- Show map and activity timeline side by side, half width each
- Show time spent in each HR zone next to the percentage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:44:20 +01:00
parent ec87f68729
commit af32a0bb7f
7 changed files with 83 additions and 35 deletions
+8 -13
View File
@@ -14,16 +14,7 @@ import {
formatHeartRate, formatDateTime, formatCadence, sportIcon,
} from '../utils/format'
// Find the cumulative distance along the track nearest a clicked lat/lng.
function nearestDistance(points, lat, lng) {
let best = null, bestD = Infinity
for (const p of points) {
if (p.latitude == null || p.longitude == null || p.distance_m == null) continue
const d = (p.latitude - lat) ** 2 + (p.longitude - lng) ** 2
if (d < bestD) { bestD = d; best = p.distance_m }
}
return best
}
import { projectToTrack } from '../utils/track'
const METRICS = [
{ key: 'heart_rate', label: 'Heart Rate', unit: 'bpm', color: '#f43f5e' },
@@ -83,8 +74,9 @@ export default function ActivityDetailPage() {
const handleMapClick = ({ lat, lng }) => {
if (!segCreate || !dataPoints) return
const dist = nearestDistance(dataPoints, lat, lng)
if (dist == null) return
const proj = projectToTrack(dataPoints, lat, lng)
if (proj?.distance_m == null) return
const dist = proj.distance_m
setSegPoints(prev => (prev.length >= 2 ? [{ distance_m: dist }] : [...prev, { distance_m: dist }]))
}
@@ -162,10 +154,12 @@ export default function ActivityDetailPage() {
{activity.hr_zones && Object.values(activity.hr_zones).some(v => v > 0) && (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<h3 className="text-sm font-medium text-gray-300 mb-3">Heart Rate Zones</h3>
<HRZoneBar zones={activity.hr_zones} />
<HRZoneBar zones={activity.hr_zones} totalSeconds={activity.moving_time_s ?? activity.duration_s} />
</div>
)}
{/* Map and activity timeline side by side, each ~half width on large screens */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Map with controls — only when the activity has a GPS track */}
{activity.polyline && activity.distance_m > 0 ? (
<div className="bg-gray-900 rounded-xl overflow-hidden border border-gray-800">
@@ -309,6 +303,7 @@ export default function ActivityDetailPage() {
<p className="text-gray-600 text-sm text-center py-8">No timeline data available for this activity</p>
)}
</div>
</div>
{/* Laps · Routes · Segments — on one row, each shrinking to fit and
expanding to fill the width when fewer are present. */}