feat: dashboard rest days widget + today highlighting
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

- Add RestDaysWidget component showing today's rest days with icons & colors
- Integrate widget into Dashboard (above training distribution)
- Highlight current day in RestDaysPage (accent border + HEUTE badge)
- Fix: Improve error handling in api.js (parse JSON detail field)

Part of v9d Phase 2 (Vitals & Recovery)
This commit is contained in:
Lars 2026-03-23 08:38:57 +01:00
parent f87b93ce2f
commit 7a0b2097ae
4 changed files with 158 additions and 4 deletions

View File

@ -0,0 +1,120 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { api } from '../utils/api'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
const FOCUS_ICONS = {
muscle_recovery: '💪',
cardio_recovery: '🏃',
mental_rest: '🧘',
deload: '📉',
injury: '🩹',
}
const FOCUS_LABELS = {
muscle_recovery: 'Muskelregeneration',
cardio_recovery: 'Cardio-Erholung',
mental_rest: 'Mentale Erholung',
deload: 'Deload',
injury: 'Verletzungspause',
}
const FOCUS_COLORS = {
muscle_recovery: '#D85A30',
cardio_recovery: '#378ADD',
mental_rest: '#7B68EE',
deload: '#E67E22',
injury: '#E74C3C',
}
export default function RestDaysWidget() {
const [todayRestDays, setTodayRestDays] = useState([])
const [loading, setLoading] = useState(true)
const nav = useNavigate()
useEffect(() => {
loadTodayRestDays()
}, [])
const loadTodayRestDays = async () => {
try {
const today = dayjs().format('YYYY-MM-DD')
const allRestDays = await api.listRestDays(7) // Last 7 days
const todayOnly = allRestDays.filter(d => d.date === today)
setTodayRestDays(todayOnly)
} catch (err) {
console.error('Failed to load rest days:', err)
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="card">
<div className="card-title">🛌 Ruhetage</div>
<div style={{ padding: '20px 0', textAlign: 'center', color: 'var(--text3)' }}>
Lädt...
</div>
</div>
)
}
return (
<div className="card">
<div className="card-title">🛌 Ruhetage</div>
{todayRestDays.length === 0 ? (
<div style={{ padding: '12px 0', color: 'var(--text3)', fontSize: 13 }}>
Heute kein Ruhetag geplant
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{todayRestDays.map(day => {
const focus = day.rest_config?.focus || 'mental_rest'
const icon = FOCUS_ICONS[focus] || '📅'
const label = FOCUS_LABELS[focus] || focus
const color = FOCUS_COLORS[focus] || '#888'
return (
<div
key={day.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: '8px 10px',
borderRadius: 8,
background: `${color}14`,
border: `1px solid ${color}`,
}}
>
<div style={{ fontSize: 20 }}>{icon}</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 600, color }}>
{label}
</div>
{day.note && (
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>
{day.note}
</div>
)}
</div>
</div>
)
})}
</div>
)}
<button
className="btn btn-secondary btn-full"
style={{ marginTop: 12, fontSize: 13 }}
onClick={() => nav('/rest-days')}
>
Ruhetage verwalten
</button>
</div>
)
}

View File

@ -12,6 +12,7 @@ import TrialBanner from '../components/TrialBanner'
import EmailVerificationBanner from '../components/EmailVerificationBanner'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import SleepWidget from '../components/SleepWidget'
import RestDaysWidget from '../components/RestDaysWidget'
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import dayjs from 'dayjs'
@ -477,6 +478,11 @@ export default function Dashboard() {
<SleepWidget/>
</div>
{/* Rest Days Widget */}
<div style={{marginBottom:16}}>
<RestDaysWidget/>
</div>
{/* Training Type Distribution */}
{activities.length > 0 && (
<div className="card section-gap" style={{marginBottom:16}}>

View File

@ -356,9 +356,14 @@ export default function RestDaysPage() {
const isEditing = editing?.id === day.id
const focus = day.rest_config?.focus || 'mental_rest'
const preset = PRESETS.find(p => p.id === focus)
const isToday = day.date === dayjs().format('YYYY-MM-DD')
return (
<div key={day.id} className="card" style={{ marginBottom: 8 }}>
<div key={day.id} className="card" style={{
marginBottom: 8,
border: isToday ? '2px solid var(--accent)' : undefined,
background: isToday ? 'var(--accent)08' : undefined,
}}>
{isEditing ? (
<div>
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>
@ -399,8 +404,22 @@ export default function RestDaysPage() {
alignItems: 'center',
marginBottom: 8,
}}>
<div style={{ fontWeight: 600, fontSize: 14 }}>
{dayjs(day.date).format('DD. MMMM YYYY')}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ fontWeight: 600, fontSize: 14 }}>
{dayjs(day.date).format('DD. MMMM YYYY')}
</div>
{isToday && (
<span style={{
padding: '2px 8px',
borderRadius: 4,
background: 'var(--accent)',
color: 'white',
fontSize: 11,
fontWeight: 600,
}}>
HEUTE
</span>
)}
</div>
<div style={{ display: 'flex', gap: 6 }}>
<button

View File

@ -15,7 +15,16 @@ function hdrs(extra={}) {
async function req(path, opts={}) {
const res = await fetch(BASE+path, {...opts, headers:hdrs(opts.headers||{})})
if (!res.ok) { const err=await res.text(); throw new Error(err) }
if (!res.ok) {
const err = await res.text()
// Try to parse JSON error with detail field
try {
const parsed = JSON.parse(err)
throw new Error(parsed.detail || err)
} catch {
throw new Error(err)
}
}
return res.json()
}
const json=(d)=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})