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 EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||||||
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
|
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
|
||||||
import SleepWidget from '../components/SleepWidget'
|
import SleepWidget from '../components/SleepWidget'
|
||||||
|
import RestDaysWidget from '../components/RestDaysWidget'
|
||||||
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
||||||
import Markdown from '../utils/Markdown'
|
import Markdown from '../utils/Markdown'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
@ -477,6 +478,11 @@ export default function Dashboard() {
|
||||||
<SleepWidget/>
|
<SleepWidget/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Rest Days Widget */}
|
||||||
|
<div style={{marginBottom:16}}>
|
||||||
|
<RestDaysWidget/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Training Type Distribution */}
|
{/* Training Type Distribution */}
|
||||||
{activities.length > 0 && (
|
{activities.length > 0 && (
|
||||||
<div className="card section-gap" style={{marginBottom:16}}>
|
<div className="card section-gap" style={{marginBottom:16}}>
|
||||||
|
|
|
||||||
|
|
@ -356,9 +356,14 @@ export default function RestDaysPage() {
|
||||||
const isEditing = editing?.id === day.id
|
const isEditing = editing?.id === day.id
|
||||||
const focus = day.rest_config?.focus || 'mental_rest'
|
const focus = day.rest_config?.focus || 'mental_rest'
|
||||||
const preset = PRESETS.find(p => p.id === focus)
|
const preset = PRESETS.find(p => p.id === focus)
|
||||||
|
const isToday = day.date === dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
return (
|
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 ? (
|
{isEditing ? (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>
|
<div style={{ fontWeight: 600, fontSize: 14, marginBottom: 8 }}>
|
||||||
|
|
@ -399,9 +404,23 @@ export default function RestDaysPage() {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}>
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
<div style={{ fontWeight: 600, fontSize: 14 }}>
|
||||||
{dayjs(day.date).format('DD. MMMM YYYY')}
|
{dayjs(day.date).format('DD. MMMM YYYY')}
|
||||||
</div>
|
</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 }}>
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,16 @@ function hdrs(extra={}) {
|
||||||
|
|
||||||
async function req(path, opts={}) {
|
async function req(path, opts={}) {
|
||||||
const res = await fetch(BASE+path, {...opts, headers:hdrs(opts.headers||{})})
|
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()
|
return res.json()
|
||||||
}
|
}
|
||||||
const json=(d)=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
|
const json=(d)=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user