From bb09c37b3d04a2497aa6a38e78134e158fdb0e40 Mon Sep 17 00:00:00 2001 From: owain Date: Thu, 11 Jun 2026 23:31:04 +0100 Subject: [PATCH] Dashboard polish: bounded drag push, equal-height stats, dark featured map, sport-coloured weekly bars - Prevent widgets flying down the page when dragging (preventCollision) - Stat widgets fill their cell height so cards with/without sub-text align - Featured-activity map defaults to dark tiles - Weekly distance bars stacked and coloured by activity type, with a legend Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/ui/StatCard.jsx | 2 +- frontend/src/pages/DashboardPage.jsx | 69 +++++++++++++++++-------- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/ui/StatCard.jsx b/frontend/src/components/ui/StatCard.jsx index 927dfd5..973c0ac 100644 --- a/frontend/src/components/ui/StatCard.jsx +++ b/frontend/src/components/ui/StatCard.jsx @@ -9,7 +9,7 @@ const accentColors = { export default function StatCard({ label, value, accent = 'default', sub }) { return ( -
+

{label}

{value}

{sub &&

{sub}

} diff --git a/frontend/src/pages/DashboardPage.jsx b/frontend/src/pages/DashboardPage.jsx index 14cb395..3347d77 100644 --- a/frontend/src/pages/DashboardPage.jsx +++ b/frontend/src/pages/DashboardPage.jsx @@ -13,7 +13,7 @@ import StatCard from '../components/ui/StatCard' import ActivityMap from '../components/activity/ActivityMap' import { formatDuration, formatDistance, formatHeartRate, formatElevation, - formatDate, sportIcon, formatSleep, + formatDate, sportIcon, sportColor, formatSleep, } from '../utils/format' import { BB_INFERRED_COLOR, BB_INFERRED_LABEL, bbLevelColor, inferBBType } from '../utils/bodyBattery' @@ -278,35 +278,62 @@ function SleepDetail({ health }) { ) } +const sportLabel = s => (s ? s.charAt(0).toUpperCase() + s.slice(1) : 'Other') + function WeeklyChart({ activities }) { const navigate = useNavigate() - const data = useMemo(() => { - if (!activities?.length) return [] + const { data, sports } = useMemo(() => { + if (!activities?.length) return { data: [], sports: [] } + // Sports present, ordered by total distance (largest stacks at the bottom). + const totals = {} + for (const a of activities) totals[a.sport_type] = (totals[a.sport_type] || 0) + (a.distance_m || 0) + const sports = Object.keys(totals).sort((x, y) => totals[y] - totals[x]) const now = new Date() const weeks = eachWeekOfInterval({ start: subWeeks(startOfWeek(now), 7), end: startOfWeek(now) }) - return weeks.map(weekStart => { + const data = weeks.map(weekStart => { const weekEnd = addDays(weekStart, 7) - const km = activities - .filter(a => { const t = new Date(a.start_time); return t >= weekStart && t < weekEnd }) - .reduce((s, a) => s + (a.distance_m || 0) / 1000, 0) - return { week: format(weekStart, 'MMM d'), km: parseFloat(km.toFixed(2)), weekStartISO: format(weekStart, 'yyyy-MM-dd'), weekEndISO: format(weekEnd, 'yyyy-MM-dd') } + const row = { week: format(weekStart, 'MMM d'), weekStartISO: format(weekStart, 'yyyy-MM-dd'), weekEndISO: format(weekEnd, 'yyyy-MM-dd') } + for (const s of sports) row[s] = 0 + for (const a of activities) { + const t = new Date(a.start_time) + if (t >= weekStart && t < weekEnd) row[a.sport_type] += (a.distance_m || 0) / 1000 + } + for (const s of sports) row[s] = +row[s].toFixed(2) + return row }) + return { data, sports } }, [activities]) return ( {data.length ? ( - - { const p = e?.activePayload?.[0]?.payload; if (p) navigate(`/activities?from=${p.weekStartISO}&to=${p.weekEndISO}`) }} - style={{ cursor: 'pointer' }}> - - - `${v.toFixed(0)}`} /> - [`${v.toFixed(1)} km`, 'Distance']} cursor={{ fill: 'rgba(59,130,246,0.1)' }} /> - - - +
+
+ + { const p = e?.activePayload?.[0]?.payload; if (p) navigate(`/activities?from=${p.weekStartISO}&to=${p.weekEndISO}`) }} + style={{ cursor: 'pointer' }}> + + + `${v.toFixed(0)}`} /> + [`${(+v).toFixed(1)} km`, sportLabel(name)]} /> + {sports.map((s, i) => ( + + ))} + + +
+
+ {sports.map(s => ( +
+
+ {sportLabel(s)} +
+ ))} +
+
) : (
No activities yet
)} @@ -333,7 +360,7 @@ function FeaturedActivity({ activity, segments }) {
{activity.polyline - ? + ? :
No GPS track
}
@@ -603,7 +630,7 @@ export default function DashboardPage() { isResizable={editMode} onLayoutChange={handleLayoutChange} compactType={null} - preventCollision={false} + preventCollision draggableCancel=".widget-delete" > {layout.filter(l => WIDGETS[l.i]).map(l => (