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
+20 -3
View File
@@ -649,7 +649,17 @@ function DailySnapshot({ day, snapshotWeight, avg30, intradayHr, bodyBattery, bb
// ── Trend Charts ────────────────────────────────────────────────────────────
function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDate, onDayClick, connectNulls = false, showDots = false, domain, referenceLines }) {
// Highlight problem days on a trend line by colouring the dot from a status field
// (e.g. HRV status): orange = unbalanced, red = low/poor. Other days get no dot.
const STATUS_DOT_COLORS = { unbalanced: '#f97316', low: '#ef4444', poor: '#ef4444' }
const statusDot = (statusKey) => (props) => {
const { cx, cy, payload } = props
const color = STATUS_DOT_COLORS[String(payload?.[statusKey] || '').toLowerCase()]
if (cx == null || cy == null || !color) return null
return <circle cx={cx} cy={cy} r={3.5} fill={color} stroke="#111827" strokeWidth={1} />
}
function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDate, onDayClick, connectNulls = false, showDots = false, domain, referenceLines, statusDotKey }) {
const vals = data.filter(d => d[dataKey] != null)
if (!vals.length) return (
<div className="flex items-center justify-center text-gray-600 text-xs" style={{ height }}>No data</div>
@@ -686,7 +696,7 @@ function MetricChart({ data, dataKey, color, formatter, height = 140, selectedDa
))}
<Area type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2}
fill={`url(#grad-${dataKey})`}
dot={showDots ? { fill: color, r: 3, strokeWidth: 0 } : false}
dot={statusDotKey ? statusDot(statusDotKey) : (showDots ? { fill: color, r: 3, strokeWidth: 0 } : false)}
connectNulls={connectNulls} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
@@ -1017,10 +1027,17 @@ export default function HealthPage() {
</div>
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<h3 className="text-sm font-medium text-gray-300 mb-3">HRV (nightly avg)</h3>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-300">HRV (nightly avg)</h3>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#f97316' }} /> Unbalanced</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full" style={{ background: '#ef4444' }} /> Low</span>
</div>
</div>
<MetricChart data={metrics} dataKey="hrv_nightly_avg" color="#8b5cf6"
formatter={v => `${Math.round(v)} ms`}
selectedDate={selDateForCharts} onDayClick={handleDayClick}
statusDotKey="hrv_status"
referenceLines={[
{ y: 20, stroke: '#f59e0b', strokeDasharray: '3 3', label: { value: 'Low', position: 'insideTopRight', fill: '#f59e0b', fontSize: 9 } },
{ y: 60, stroke: '#22c55e', strokeDasharray: '3 3', label: { value: 'Good', position: 'insideTopRight', fill: '#22c55e', fontSize: 9 } },