bc437cce92
Fixes:
- Dashboard: featured most-recent activity card with map + stats
- Maps default to Street; preferCanvas + larger tile buffer for smoother pan/zoom
- Running cadence as colour-banded dots + 165 spm guide line
- Routes: inline row expansion, rename (PATCH /routes/{id}), podium + deltas, tiled map
- Records: remove reversed pace Y-axis
- Profile: remove resting HR; add goal weight
- Health: snapshot weight carry-forward; VO2 trend axis 30-70;
weight goal line + kg/st-lb toggle + axis max; sleep 8h/avg lines
- Garmin sync progress moved to global store with persistent floating bar
Features:
- Speed-coloured activity route (default) with Speed/Solid toggle
- GPS-geometry segments: draw on map, match across all activities,
1st/2nd/3rd leaderboard + podium badges (replaces old distance segments)
- Lap bests: best time per lap across a route + delta column
- Body Battery: highlight activity time windows
Schema: users.goal_weight_kg ALTER; new segments/segment_efforts tables.
Removes RouteSegment, the Segments page, and segment-bests endpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
3.2 KiB
React
64 lines
3.2 KiB
React
import { formatDuration, formatDistance, formatPace, formatHeartRate, formatCadence } from '../../utils/format'
|
||
|
||
const RUNNING_TYPES = new Set(['running', 'hiking', 'walking'])
|
||
|
||
export default function LapTable({ laps, sportType, lapBests }) {
|
||
const showPower = !RUNNING_TYPES.has(sportType?.toLowerCase())
|
||
const hasBests = lapBests && Object.keys(lapBests).length > 0
|
||
return (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="text-xs text-gray-500 border-b border-gray-800">
|
||
<th className="text-left pb-2 font-medium">Lap</th>
|
||
<th className="text-right pb-2 font-medium">Distance</th>
|
||
<th className="text-right pb-2 font-medium">Time</th>
|
||
{hasBests && <th className="text-right pb-2 font-medium">Best</th>}
|
||
{hasBests && <th className="text-right pb-2 font-medium">Δ</th>}
|
||
<th className="text-right pb-2 font-medium">Pace</th>
|
||
<th className="text-right pb-2 font-medium">Avg HR</th>
|
||
<th className="text-right pb-2 font-medium">Cadence</th>
|
||
{showPower && <th className="text-right pb-2 font-medium">Power</th>}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{laps.map((lap) => {
|
||
const best = hasBests ? lapBests[String(lap.lap_number)] : null
|
||
const delta = best != null && lap.duration_s != null ? lap.duration_s - best : null
|
||
const isBest = delta != null && delta <= 0.5
|
||
return (
|
||
<tr key={lap.lap_number} className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
|
||
<td className="py-2 text-gray-400">{lap.lap_number}</td>
|
||
<td className="py-2 text-right text-gray-200">{formatDistance(lap.distance_m)}</td>
|
||
<td className={`py-2 text-right ${isBest ? 'text-yellow-400 font-semibold' : 'text-gray-200'}`}>{formatDuration(lap.duration_s)}</td>
|
||
{hasBests && (
|
||
<td className="py-2 text-right font-mono text-gray-500">{best != null ? formatDuration(best) : '--'}</td>
|
||
)}
|
||
{hasBests && (
|
||
<td className={`py-2 text-right font-mono ${
|
||
delta == null ? 'text-gray-700' : isBest ? 'text-yellow-400' : delta < 0 ? 'text-green-400' : 'text-red-400'
|
||
}`}>
|
||
{delta == null ? '--' : isBest ? '🏆' : `${delta > 0 ? '+' : '−'}${formatDuration(Math.abs(delta))}`}
|
||
</td>
|
||
)}
|
||
<td className="py-2 text-right text-gray-200">{formatPace(lap.avg_speed_ms, sportType)}</td>
|
||
<td className="py-2 text-right">
|
||
<span className="text-red-400">{formatHeartRate(lap.avg_heart_rate)}</span>
|
||
</td>
|
||
<td className="py-2 text-right text-gray-400">
|
||
{lap.avg_cadence ? formatCadence(lap.avg_cadence, sportType) : '--'}
|
||
</td>
|
||
{showPower && (
|
||
<td className="py-2 text-right text-gray-400">
|
||
{lap.avg_power ? `${Math.round(lap.avg_power)} W` : '--'}
|
||
</td>
|
||
)}
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)
|
||
}
|