This commit is contained in:
2026-05-15 18:54:46 +02:00
parent 335f8069a0
commit 9cbab22580
2 changed files with 223 additions and 183 deletions

View File

@@ -8,14 +8,6 @@ import type {ActivityStreamPointDo} from '@/types/ActivityStreamPointDo'
import type {ActivitySummaryDo} from '@/types/ActivitySummaryDo'
import {formatDistance, formatDuration, formatPace, formatSpeed, sportIcon, summaryNum} from '@/utils/activity'
type PointInfo = {
speed: number
power: number
hr: number
grade: number
distance: number
} | null
function isBike(type: string): boolean {
const lowered = type.toLowerCase()
return lowered.includes('ride') || lowered.includes('cycling')
@@ -32,16 +24,27 @@ function tssColor(tss: number): string {
return 'text-red-600'
}
type PointInfo = {speed: number; power: number; hr: number; distance: number} | null
function HoverRow({label, value}: {label: string; value: string}) {
return (
<div>
<p className="text-[9px] font-semibold tracking-widest text-zinc-400 uppercase">{label}</p>
<p className="text-sm font-bold text-zinc-800 tabular-nums leading-tight">{value}</p>
</div>
)
}
function ActivityRouteMap({
points,
timeSeries,
isRun,
isBike,
isRunActivity,
isBikeActivity,
}: {
points: ActivityMapStreamPointDo[]
timeSeries: ActivityStreamPointDo[]
isRun: boolean
isBike: boolean
isRunActivity: boolean
isBikeActivity: boolean
}) {
const containerRef = useRef<HTMLDivElement>(null)
const mapRef = useRef<L.Map | null>(null)
@@ -103,7 +106,6 @@ function ActivityRouteMap({
speed: tsPoint.velocityMps ?? 0,
power: tsPoint.watts ?? 0,
hr: tsPoint.heartrateBpm ?? 0,
grade: tsPoint.gradeSmooth ?? 0,
distance: tsPoint.distanceM ?? 0,
} : null
@@ -125,14 +127,11 @@ function ActivityRouteMap({
</div>
<div className="px-4 pb-3 space-y-3">
<HoverRow
label={isRun ? 'Pace' : 'Speed'}
value={isRun ? formatPace(pointInfo.speed) : formatSpeed(pointInfo.speed)}
label={isRunActivity ? 'Pace' : 'Speed'}
value={isRunActivity ? formatPace(pointInfo.speed) : formatSpeed(pointInfo.speed)}
/>
<HoverRow
label="Distance"
value={formatDistance(pointInfo.distance)}
/>
{isBike && pointInfo.power > 0 && (
<HoverRow label="Distance" value={formatDistance(pointInfo.distance)}/>
{isBikeActivity && pointInfo.power > 0 && (
<HoverRow label="Power" value={`${Math.round(pointInfo.power)} W`}/>
)}
{pointInfo.hr > 0 && (
@@ -223,35 +222,11 @@ function ActivityRouteMap({
)
}
function HoverRow({label, value}: {label: string; value: string}) {
return (
<div>
<p className="text-[9px] font-semibold tracking-widest text-zinc-400 uppercase">{label}</p>
<p className="text-sm font-bold text-zinc-800 tabular-nums leading-tight">{value}</p>
</div>
)
}
type MetricItem = {
label: string
value: string
colorClass?: string
}
function MetricCell({label, value, colorClass = 'text-zinc-800'}: MetricItem) {
return (
<div>
<p className="text-[10px] font-semibold tracking-widest text-zinc-400 uppercase">{label}</p>
<p className={`text-sm font-semibold tabular-nums ${colorClass}`}>{value}</p>
</div>
)
}
export function ActivityOverviewCard({
activity,
mapStream,
timeSeries = [],
}: {
activity,
mapStream,
timeSeries = [],
}: {
activity: ActivitySummaryDo
mapStream: ActivityMapStreamPointDo[] | null
timeSeries?: ActivityStreamPointDo[]
@@ -277,16 +252,9 @@ export function ActivityOverviewCard({
const sportBadgeLabel = isBike(activity.activityType) ? 'Ride' : isRun(activity.activityType) ? 'Run' : null
const metrics: MetricItem[] = [
{label: speedPaceLabel, value: speedPaceValue},
{label: 'Avg HR', value: heartrate != null ? `${heartrate} bpm` : '—'},
{label: 'TSS', value: String(tssValue ?? '—'), colorClass: tssValue != null ? tssColor(tssValue) : 'text-zinc-800'},
...(isBike(activity.activityType) ? [{label: 'Avg Power', value: power != null ? `${power} W` : '—'}] : []),
{label: 'kCal', value: kcal != null ? kcal.toLocaleString() : '—'},
]
return (
<div className="border border-zinc-200 rounded-xl bg-white shadow-md hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 overflow-hidden">
<div
className="border border-zinc-200 rounded-xl bg-white shadow-md hover:shadow-xl hover:-translate-y-0.5 transition-all duration-200 overflow-hidden">
{/* Header – always full-width */}
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-100">
<div className="flex items-center gap-3 min-w-0">
@@ -297,61 +265,80 @@ export function ActivityOverviewCard({
</div>
</div>
{sportBadgeLabel != null && (
<span className="shrink-0 ml-3 px-3 py-1 text-xs font-bold text-white rounded-full bg-gradient-to-r from-red-500 to-orange-500 shadow-sm">
<span
className="shrink-0 ml-3 px-3 py-1 text-xs font-bold text-white rounded-full bg-gradient-to-r from-red-500 to-orange-500 shadow-sm">
{sportBadgeLabel}
</span>
)}
</div>
{/* Body – stacked on mobile, two-column on lg+ */}
<div className="grid grid-cols-1 lg:grid-cols-[65%_35%]">
{/* Left: map or indoor placeholder */}
<div className="h-[280px] lg:h-[420px]">
{hasMap ? (
<ActivityRouteMap
points={mapStream!}
timeSeries={timeSeries}
isRun={isRun(activity.activityType)}
isBike={isBike(activity.activityType)}
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-zinc-50 to-zinc-100 flex flex-col items-center justify-center gap-2">
<span className="text-6xl opacity-20">{sportIcon(activity.activityType)}</span>
<p className="text-sm font-semibold text-zinc-400 tracking-wide">Indoor Trainer Ride</p>
</div>
)}
</div>
{/* Right: metrics panel */}
<div className="flex flex-col justify-between p-6 border-t border-zinc-100 lg:border-t-0 lg:border-l lg:border-zinc-100">
{/* Hero row: Moving Time + Distance */}
<div>
<div className="mb-1">
<p className="text-[10px] font-semibold tracking-widest text-zinc-400 uppercase mb-1.5">Moving Time · Distance</p>
<div className="flex items-baseline gap-1.5">
<p className="text-2xl font-bold text-zinc-900 tabular-nums leading-none">{formatDuration(activity.movingTimeS)}</p>
<span className="text-zinc-300 text-lg font-light">•</span>
<p className="text-2xl font-bold text-zinc-900 tabular-nums leading-none">{formatDistance(summaryNum(activity.valuesForSummary, 'distanceM') ?? 0)}</p>
</div>
</div>
<div className="border-t border-zinc-100 my-5"/>
{/* 2-column metrics grid */}
<div className="grid grid-cols-2 gap-x-5 gap-y-5">
{metrics.map((metric, i) => (
<div key={metric.label} className={i >= 2 ? 'pt-4 border-t border-zinc-100' : ''}>
<MetricCell {...metric}/>
</div>
))}
</div>
{/* Body – full-width map + metrics below slider */}
<div className="aspect-square">
{hasMap ? (
<ActivityRouteMap
points={mapStream!}
timeSeries={timeSeries}
isRunActivity={isRun(activity.activityType)}
isBikeActivity={isBike(activity.activityType)}
/>
) : (
<div
className="w-full h-full bg-gradient-to-br from-zinc-50 to-zinc-100 flex flex-col items-center justify-center gap-2">
<span className="text-6xl opacity-20">{sportIcon(activity.activityType)}</span>
<p className="text-sm font-semibold text-zinc-400 tracking-wide">Indoor Trainer Ride</p>
</div>
)}
</div>
{/* Metrics – always below the map/slider */}
<div className="border-t border-zinc-100">
{/* Hero row: Time · Distance · TSS */}
<div className="grid grid-cols-3 divide-x divide-zinc-100">
<div className="px-4 py-3 text-center">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">Time</p>
<p className="text-base font-bold tabular-nums text-zinc-900 leading-none">{formatDuration(activity.movingTimeS)}</p>
</div>
<div className="px-4 py-3 text-center">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">Distance</p>
<p className="text-base font-bold tabular-nums text-zinc-900 leading-none">{formatDistance(activity.distanceM)}</p>
</div>
<div className="px-4 py-3 text-center">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">TSS</p>
<p className={`text-base font-bold tabular-nums leading-none ${tssValue != null ? tssColor(tssValue) : 'text-zinc-900'}`}>
{tssValue ?? '—'}
</p>
</div>
</div>
{/* Secondary metrics */}
<div className="grid grid-cols-2 sm:grid-cols-4 border-t border-zinc-100">
{[
{label: speedPaceLabel, value: speedPaceValue},
{label: 'Avg HR', value: heartrate != null ? `${heartrate} bpm` : '—'},
...(isBike(activity.activityType) ? [{
label: 'Avg Power',
value: power != null ? `${power} W` : '—'
}] : []),
{label: 'kCal', value: kcal != null ? kcal.toLocaleString() : '—'},
].map((metric, i) => (
<div
key={metric.label}
className={[
'px-4 py-2.5',
i % 2 === 0 ? 'border-r border-zinc-100' : '',
i >= 2 ? 'border-t border-zinc-100 sm:border-t-0' : '',
].join(' ')}
>
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-0.5">{metric.label}</p>
<p className="text-sm font-semibold tabular-nums text-zinc-800">{metric.value}</p>
</div>
))}
</div>
</div>
{/* Footer – always full-width */}
<div className="px-5 pb-5 pt-4">
<button className="w-full py-3 rounded-lg text-sm font-bold text-white bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 transition-all shadow-sm hover:shadow-md">
<button
className="w-full py-3 rounded-lg text-sm font-bold text-white bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 transition-all shadow-sm hover:shadow-md">
View Details →
</button>
</div>

View File

@@ -129,7 +129,7 @@ function GoalPeriodColumn({title, subtitle, goals, actuals}: {
actuals: Actuals
}) {
return (
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-5 flex flex-col gap-4">
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-4 flex flex-col gap-3">
<div>
<p className="text-sm font-bold text-zinc-900">{title}</p>
<p className="text-[11px] text-zinc-400 mt-0.5">{subtitle}</p>
@@ -154,14 +154,61 @@ function GoalPeriodColumn({title, subtitle, goals, actuals}: {
// ── DayCard ───────────────────────────────────────────────────────────────────
function DayCard({day, activities, selectedActivityId, onActivityClick}: {
function DayCard({day, activities, selectedActivityId, onActivityClick, expanded = false}: {
day: Date
activities: ActivitySummaryDo[]
selectedActivityId: number | null
onActivityClick: (activity: ActivitySummaryDo) => void
expanded?: boolean
}) {
const today = isToday(day)
const dayActivities = activities.filter((a) => isSameDay(new Date(a.startTime), day))
if (expanded) {
return (
<div className={[
'rounded-xl border p-4',
today ? 'border-orange-200 bg-orange-50/70' : 'bg-white border-zinc-100 shadow-sm',
].join(' ')}>
<div className="mb-3">
<p className={['text-sm font-bold uppercase tracking-wide', today ? 'gradient-text' : 'text-zinc-700'].join(' ')}>
{format(day, 'EEEE')}
</p>
<p className="text-xs text-zinc-400">{format(day, 'd MMMM')}</p>
</div>
{dayActivities.length === 0 ? (
<p className="text-sm text-zinc-300">Rest day</p>
) : (
<div className="flex flex-col gap-2">
{dayActivities.map((a) => {
const isSelected = a.id === selectedActivityId
return (
<div
key={String(a.id)}
onClick={() => onActivityClick(a)}
className={[
'rounded-lg p-2.5 cursor-pointer transition-colors',
isSelected ? 'bg-orange-100' : 'bg-zinc-50 hover:bg-zinc-100',
].join(' ')}
>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg leading-none">{sportIcon(a.activityType)}</span>
<p className={['text-sm font-semibold truncate', isSelected ? 'text-orange-600' : 'text-zinc-800'].join(' ')}>
{a.name}
</p>
</div>
<p className="text-xs text-zinc-400 tabular-nums">
{formatDistance(a.distanceM)} · {formatDuration(a.movingTimeS)}
</p>
</div>
)
})}
</div>
)}
</div>
)
}
return (
<div className={[
'rounded-xl border flex flex-col p-2.5 min-h-27.5',
@@ -238,9 +285,9 @@ function DailyBreakdown({weekDays, activities, selectedActivityId, onActivityCli
<button
onClick={() => setMobileDayIndex((i) => Math.max(0, i - 1))}
disabled={mobileDayIndex === 0}
className="p-2 rounded-xl bg-white border border-zinc-100 shadow-sm text-zinc-400 hover:text-zinc-700 disabled:opacity-25 disabled:cursor-not-allowed transition-colors shrink-0"
className="w-11 h-11 flex items-center justify-center rounded-xl bg-white border border-zinc-100 shadow-sm text-zinc-400 hover:text-zinc-700 disabled:opacity-25 disabled:cursor-not-allowed transition-colors shrink-0"
>
<ChevronLeft size={18}/>
<ChevronLeft size={20}/>
</button>
<div className="flex-1">
<DayCard
@@ -248,14 +295,15 @@ function DailyBreakdown({weekDays, activities, selectedActivityId, onActivityCli
activities={activities}
selectedActivityId={selectedActivityId}
onActivityClick={onActivityClick}
expanded
/>
</div>
<button
onClick={() => setMobileDayIndex((i) => Math.min(weekDays.length - 1, i + 1))}
disabled={mobileDayIndex === weekDays.length - 1}
className="p-2 rounded-xl bg-white border border-zinc-100 shadow-sm text-zinc-400 hover:text-zinc-700 disabled:opacity-25 disabled:cursor-not-allowed transition-colors shrink-0"
className="w-11 h-11 flex items-center justify-center rounded-xl bg-white border border-zinc-100 shadow-sm text-zinc-400 hover:text-zinc-700 disabled:opacity-25 disabled:cursor-not-allowed transition-colors shrink-0"
>
<ChevronRight size={18}/>
<ChevronRight size={20}/>
</button>
</div>
<div className="flex items-center gap-1.5">
@@ -287,29 +335,6 @@ function TrendArrow({delta}: {delta: number}) {
)
}
// ── MetricCard ────────────────────────────────────────────────────────────────
function MetricCard({label, value, unit, sub, valueColor, trend}: {
label: string
value: string | number
unit?: string
sub?: string
valueColor?: string
trend?: number | null
}) {
return (
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-5">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-3">{label}</p>
<p className="text-3xl font-bold leading-none" style={{color: valueColor ?? '#18181b'}}>
{value}
{unit && <span className="text-sm font-normal text-zinc-400 ml-1">{unit}</span>}
{trend != null && <TrendArrow delta={trend}/>}
</p>
{sub && <p className="text-xs text-zinc-400 mt-1">{sub}</p>}
</div>
)
}
// ── Load chart helpers ────────────────────────────────────────────────────────
type LoadPoint = {
@@ -589,68 +614,96 @@ export function Dashboard({athleteId}: { athleteId: number }) {
<section className="flex flex-col gap-4">
<p className="text-[11px] font-bold tracking-widest text-zinc-400 uppercase">Training Analytics</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<MetricCard label="Fitness (CTL)" value={ctl} unit="TSS/day" sub="Chronic Training Load" valueColor="#ef4444" trend={ctlTrend}/>
<MetricCard label="Fatigue (ATL)" value={atl} unit="TSS/day" sub="Acute Training Load" valueColor="#fdba74" trend={atlTrend}/>
<MetricCard
label="Form (TSB)"
value={tsb}
sub={Number(tsb) > 0 ? 'Fresh — ready to race' : 'Fatigued — building fitness'}
valueColor="#52525b"
trend={tsbTrend}
/>
{/* CTL / ATL / TSB — single compact card */}
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-4">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-3">Fitness State</p>
<div className="grid grid-cols-3 divide-x divide-zinc-100">
<div className="pr-4">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">Fitness</p>
<p className="text-2xl font-bold tabular-nums leading-none" style={{color: '#ef4444'}}>
{ctl}
<span className="text-xs font-normal text-zinc-400 ml-1">CTL</span>
{ctlTrend != null && <TrendArrow delta={ctlTrend}/>}
</p>
<p className="text-[10px] text-zinc-400 mt-1">TSS/day</p>
</div>
<div className="px-4">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">Fatigue</p>
<p className="text-2xl font-bold tabular-nums leading-none" style={{color: '#fdba74'}}>
{atl}
<span className="text-xs font-normal text-zinc-400 ml-1">ATL</span>
{atlTrend != null && <TrendArrow delta={atlTrend}/>}
</p>
<p className="text-[10px] text-zinc-400 mt-1">TSS/day</p>
</div>
<div className="pl-4">
<p className="text-[9px] font-bold tracking-widest text-zinc-400 uppercase mb-1">Form</p>
<p className="text-2xl font-bold tabular-nums leading-none" style={{color: '#52525b'}}>
{tsb}
<span className="text-xs font-normal text-zinc-400 ml-1">TSB</span>
{tsbTrend != null && <TrendArrow delta={tsbTrend}/>}
</p>
<p className="text-[10px] text-zinc-400 mt-1">
{Number(tsb) > 0 ? 'Fresh' : 'Fatigued'}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-5">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-5">Weekly Training Load (TSS)</p>
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-4">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-4">Weekly Training Load (TSS)</p>
{loadData.some((d) => d.tss > 0) ? (
<ResponsiveContainer width="100%" height={200}>
<ComposedChart data={loadData} margin={{left: -20, right: 10}} onClick={handleChartClick(loadData)} style={{cursor: 'pointer'}}>
<defs>
<linearGradient id="tssGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#ef4444"/>
<stop offset="100%" stopColor="#f97316"/>
</linearGradient>
<linearGradient id="tssAreaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.12}/>
<stop offset="100%" stopColor="#f97316" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="week" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<YAxis tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<Tooltip content={<LoadTooltip/>}/>
<Area type="monotone" dataKey="tss" fill="url(#tssAreaGrad)" stroke="none"/>
<Line type="monotone" dataKey="tss" stroke="url(#tssGrad)" strokeWidth={2} dot={false} name="TSS"/>
</ComposedChart>
</ResponsiveContainer>
<div className="h-32 md:h-50">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={loadData} margin={{left: -20, right: 10}} onClick={handleChartClick(loadData)} style={{cursor: 'pointer'}}>
<defs>
<linearGradient id="tssGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#ef4444"/>
<stop offset="100%" stopColor="#f97316"/>
</linearGradient>
<linearGradient id="tssAreaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.12}/>
<stop offset="100%" stopColor="#f97316" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="week" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<YAxis tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<Tooltip content={<LoadTooltip/>}/>
<Area type="monotone" dataKey="tss" fill="url(#tssAreaGrad)" stroke="none"/>
<Line type="monotone" dataKey="tss" stroke="url(#tssGrad)" strokeWidth={2} dot={false} name="TSS"/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-50 flex items-center justify-center text-zinc-400 text-sm">No training data yet</div>
<div className="h-32 md:h-50 flex items-center justify-center text-zinc-400 text-sm">No training data yet</div>
)}
</div>
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-5">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-5">Fitness, Fatigue &amp; Form (CTL / ATL / TSB)</p>
<div className="bg-white rounded-2xl shadow-sm border border-zinc-100 p-4">
<p className="text-[10px] font-bold tracking-widest text-zinc-400 uppercase mb-4">Fitness, Fatigue &amp; Form (CTL / ATL / TSB)</p>
{ctlLoadData.some((d) => d.ctl !== null) ? (
<ResponsiveContainer width="100%" height={200}>
<ComposedChart data={ctlLoadData} margin={{left: -20, right: 0}} onClick={handleChartClick(ctlLoadData)} style={{cursor: 'pointer'}}>
<defs>
<linearGradient id="tssAreaGrad2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.12}/>
<stop offset="100%" stopColor="#f97316" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="week" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false} padding={{left: 20, right: 20}}/>
<YAxis yAxisId="left" domain={ctlDomain} tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<YAxis yAxisId="right" orientation="right" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<Tooltip content={<LoadTooltip/>}/>
<Area yAxisId="left" type="monotone" dataKey="ctl" fill="url(#tssAreaGrad2)" stroke="none" connectNulls/>
<Line yAxisId="left" type="monotone" dataKey="ctl" stroke="#ef4444" strokeWidth={2} dot={false} name="CTL" connectNulls/>
<Line yAxisId="left" type="monotone" dataKey="atl" stroke="#fdba74" strokeWidth={1.5} strokeDasharray="5 3" dot={false} name="ATL" connectNulls/>
<Line yAxisId="right" type="monotone" dataKey="tsb" stroke="#52525b" strokeWidth={1.5} strokeDasharray="3 3" dot={false} name="TSB" connectNulls/>
</ComposedChart>
</ResponsiveContainer>
<div className="h-32 md:h-50">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={ctlLoadData} margin={{left: -20, right: 0}} onClick={handleChartClick(ctlLoadData)} style={{cursor: 'pointer'}}>
<defs>
<linearGradient id="tssAreaGrad2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ef4444" stopOpacity={0.12}/>
<stop offset="100%" stopColor="#f97316" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="week" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false} padding={{left: 20, right: 20}}/>
<YAxis yAxisId="left" domain={ctlDomain} tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<YAxis yAxisId="right" orientation="right" tick={{fontSize: 10, fill: '#a1a1aa'}} axisLine={false} tickLine={false}/>
<Tooltip content={<LoadTooltip/>}/>
<Area yAxisId="left" type="monotone" dataKey="ctl" fill="url(#tssAreaGrad2)" stroke="none" connectNulls/>
<Line yAxisId="left" type="monotone" dataKey="ctl" stroke="#ef4444" strokeWidth={2} dot={false} name="CTL" connectNulls/>
<Line yAxisId="left" type="monotone" dataKey="atl" stroke="#fdba74" strokeWidth={1.5} strokeDasharray="5 3" dot={false} name="ATL" connectNulls/>
<Line yAxisId="right" type="monotone" dataKey="tsb" stroke="#52525b" strokeWidth={1.5} strokeDasharray="3 3" dot={false} name="TSB" connectNulls/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-50 flex items-center justify-center text-zinc-400 text-sm">No fitness data yet</div>
<div className="h-32 md:h-50 flex items-center justify-center text-zinc-400 text-sm">No fitness data yet</div>
)}
</div>
</section>