From 63f8ca1104aff1db1d97526ab8de8f911244e4ac Mon Sep 17 00:00:00 2001 From: Ingo Schnabel Date: Mon, 20 Apr 2026 07:30:48 +0200 Subject: [PATCH] Dashboard --- .../components/dashboard/LastWorkoutCard.tsx | 55 +++++++-- peakup-frontend/src/pages/Dashboard.tsx | 112 +++++++++++------- .../maven/TypeScriptClientGeneratorMojo.java | 1 + 3 files changed, 113 insertions(+), 55 deletions(-) diff --git a/peakup-frontend/src/components/dashboard/LastWorkoutCard.tsx b/peakup-frontend/src/components/dashboard/LastWorkoutCard.tsx index 4e6bf31..fbea625 100644 --- a/peakup-frontend/src/components/dashboard/LastWorkoutCard.tsx +++ b/peakup-frontend/src/components/dashboard/LastWorkoutCard.tsx @@ -49,6 +49,7 @@ function ActivityRouteMap({ const sliderMax = Math.max(points.length - 1, 0) const boundedIdx = Math.min(sliderIdx, sliderMax) + const totalDistanceM = timeSeries.length > 0 ? (timeSeries[timeSeries.length - 1]?.distanceM ?? 0) : 0 useEffect(() => { if (!containerRef.current || points.length === 0) return @@ -111,11 +112,12 @@ function ActivityRouteMap({
{pointInfo && (
-
-
-

At Point

+
+
+

At Point

+
-
+
{sliderMax > 0 && ( -
+
+
+ + {formatDistance(tsPoint?.distanceM ?? 0)} + + + {formatDistance(totalDistanceM)} + +
setSliderIdx(Number(e.target.value))} - className="w-full accent-orange-500 cursor-pointer" + className="w-full cursor-pointer accent-orange-500 + [&::-webkit-slider-runnable-track]:h-2 + [&::-webkit-slider-runnable-track]:rounded-full + [&::-webkit-slider-runnable-track]:bg-zinc-200 + [&::-webkit-slider-thumb]:appearance-none + [&::-webkit-slider-thumb]:h-5 + [&::-webkit-slider-thumb]:w-5 + [&::-webkit-slider-thumb]:rounded-full + [&::-webkit-slider-thumb]:bg-orange-500 + [&::-webkit-slider-thumb]:shadow-md + [&::-webkit-slider-thumb]:-mt-1.5 + [&::-moz-range-track]:h-2 + [&::-moz-range-track]:rounded-full + [&::-moz-range-thumb]:h-5 + [&::-moz-range-thumb]:w-5 + [&::-moz-range-thumb]:rounded-full + [&::-moz-range-thumb]:border-0 + [&::-moz-range-thumb]:bg-orange-500" />
)} @@ -246,11 +273,11 @@ export function LastWorkoutCard({
{/* Right: metrics panel */} -
+
{/* Hero row: Moving Time + Distance */}
-

Moving Time · Distance

+

Moving Time · Distance

{formatDuration(activity.movingTimeS)}

• @@ -258,12 +285,14 @@ export function LastWorkoutCard({
-
+
- {/* 2-column metrics grid – easy to extend by adding to the metrics array */} -
- {metrics.map((metric) => ( - + {/* 2-column metrics grid */} +
+ {metrics.map((metric, i) => ( +
= 2 ? 'pt-4 border-t border-zinc-100' : ''}> + +
))}
diff --git a/peakup-frontend/src/pages/Dashboard.tsx b/peakup-frontend/src/pages/Dashboard.tsx index 216c89f..543175b 100644 --- a/peakup-frontend/src/pages/Dashboard.tsx +++ b/peakup-frontend/src/pages/Dashboard.tsx @@ -33,27 +33,42 @@ function weekStart(year: number, week: number): Date { // ── MetricCard ──────────────────────────────────────────────────────────────── +function TrendArrow({delta}: { delta: number }) { + if (delta === 0) return null + const up = delta > 0 + return ( + + {up ? '▲' : '▼'} {Math.abs(delta)} + + ) +} + function MetricCard({ label, value, unit, sub, valueColor, + className, + trend, }: { label: string value: string | number unit?: string sub?: string valueColor?: string + className?: string + trend?: number | null }) { return ( -
+

{label}

{value} {unit && {unit}} + {trend != null && }

{sub &&

{sub}

}
@@ -164,7 +179,7 @@ function GoalPeriodColumn({ actuals: Actuals }) { return ( -
+

{title}

{subtitle}

@@ -253,14 +268,14 @@ function DayCard({ className={[ 'border rounded-sm p-3 min-h-[90px]', today - ? 'bg-zinc-50 border-l-2 border-l-orange-500 border-t-zinc-200 border-r-zinc-200 border-b-zinc-200' - : 'bg-white border-zinc-200', + ? 'bg-orange-50 shadow-md border-t-zinc-200 border-r-zinc-200 border-b-zinc-200 border-l-4 border-l-orange-400' + : 'bg-white border-zinc-200 shadow-sm', ].join(' ')} >

{format(day, 'EEE d')} @@ -430,13 +445,24 @@ export function Dashboard({athleteId}: { athleteId: number }) { const ctlLoadData = loadData.slice(loadData.findIndex(d => d.ctl !== null)) + const nonNullCtlData = loadData.filter((d) => d.ctl !== null) + const prevCtlEntry = nonNullCtlData.length >= 2 ? nonNullCtlData[nonNullCtlData.length - 2] : null + const currentCtlEntry = nonNullCtlData.length >= 1 ? nonNullCtlData[nonNullCtlData.length - 1] : null + const ctlTrend = currentCtlEntry?.ctl != null && prevCtlEntry?.ctl != null + ? currentCtlEntry.ctl - prevCtlEntry.ctl + : null + const atlTrend = currentCtlEntry?.atl != null && prevCtlEntry?.atl != null + ? currentCtlEntry.atl - prevCtlEntry.atl + : null + const tsbTrend = ctlTrend != null && atlTrend != null ? ctlTrend - atlTrend : null + const ctlValues = ctlLoadData.map(d => d.ctl).filter((v): v is number => v !== null) const ctlDomain: [number, number] | ['auto', 'auto'] = ctlValues.length ? [Math.floor(Math.min(...ctlValues) * 0.9), Math.ceil(Math.max(...ctlValues) * 1.1)] : ['auto', 'auto'] return ( -

+
{/* Page header */}

Overview

@@ -444,7 +470,7 @@ export function Dashboard({athleteId}: { athleteId: number }) {
{/* ── Section 1: Training Goals ───────────────────────────────────────── */} -
+

Training Goals @@ -475,7 +501,7 @@ export function Dashboard({athleteId}: { athleteId: number }) {

-
+
+

{selectedActivityId !== null && selectedActivity !== lastWorkout @@ -520,45 +546,47 @@ export function Dashboard({athleteId}: { athleteId: number }) { )} {/* ── Section 3: This Week's Training ────────────────────────────────── */} -

-

- This Week's Training -

- -

+

+

Daily Breakdown

-
- {weekDays.map((day) => ( - setSelectedActivityId(activity.id ?? null)} - /> - ))} +
+
+ {weekDays.map((day) => ( + setSelectedActivityId(activity.id ?? null)} + /> + ))} +
{/* Fitness State */} -

- Fitness State -

-
- - - 0 ? 'Fresh — ready to race' : 'Fatigued — building fitness'} - valueColor="#52525b" - /> +
+

+ Fitness State +

+
+ + + 0 ? 'Fresh — ready to race' : 'Fatigued — building fitness'} + valueColor="#52525b" + className="border-zinc-100 bg-zinc-50/60" + trend={tsbTrend} + /> +
{/* Weekly TSS chart */} -
+

Weekly Training Load (TSS)

@@ -592,7 +620,7 @@ export function Dashboard({athleteId}: { athleteId: number }) {
{/* CTL / ATL / TSB chart */} -
+

Fitness, Fatigue & Form (CTL / ATL / TSB)

diff --git a/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java index c7515a8..bbd2b08 100644 --- a/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java +++ b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java @@ -57,6 +57,7 @@ import java.util.stream.Stream; *
  • {@code hooks.ts} — TanStack Query hooks
  • * */ +@SuppressWarnings("unused") @Mojo( name = "generate-typescript", defaultPhase = LifecyclePhase.PROCESS_CLASSES,