feat: dashboard rest days widget + today highlighting
- 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:
parent
f87b93ce2f
commit
7a0b2097ae
120
frontend/src/components/RestDaysWidget.jsx
Normal file
120
frontend/src/components/RestDaysWidget.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}}>
|
||||
|
|
|
|||
|
|
@ -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,9 +404,23 @@ export default function RestDaysPage() {
|
|||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
}}>
|
||||
<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
|
||||
className="btn btn-secondary"
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user