From c265ab12459c857024845494daf926f0ddf89942 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 22 Mar 2026 16:33:32 +0100 Subject: [PATCH] feat: RestDaysPage UI with Quick Mode presets (v9d Phase 2a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick Mode with 4 presets: - 💪 Kraft-Ruhetag (strength/hiit pause, cardio allowed, max 60%) - 🏃 Cardio-Ruhetag (cardio pause, strength/mobility allowed, max 70%) - 🧘 Entspannungstag (all pause, only meditation/walk, max 40%) - 📉 Deload (all allowed, max 70% intensity) Features: - Preset selection with visual cards - Date picker - Optional note field - List view with inline editing - Delete with confirmation - Toast notifications - Detail view (shows rest_from, allows, intensity_max) Integration: - Route: /rest-days - CaptureHub entry: 🛌 Ruhetage Next Phase: - Custom Mode (full control) - Activity conflict warnings - Weekly planning integration Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.jsx | 2 + frontend/src/pages/CaptureHub.jsx | 7 + frontend/src/pages/RestDaysPage.jsx | 487 ++++++++++++++++++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 frontend/src/pages/RestDaysPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3df5521..a83ecf1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -31,6 +31,7 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage' import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage' import SubscriptionPage from './pages/SubscriptionPage' import SleepPage from './pages/SleepPage' +import RestDaysPage from './pages/RestDaysPage' import './app.css' function Nav() { @@ -166,6 +167,7 @@ function AppShell() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/frontend/src/pages/CaptureHub.jsx b/frontend/src/pages/CaptureHub.jsx index 1ce7dcd..cdfb45f 100644 --- a/frontend/src/pages/CaptureHub.jsx +++ b/frontend/src/pages/CaptureHub.jsx @@ -52,6 +52,13 @@ const ENTRIES = [ to: '/sleep', color: '#7B68EE', }, + { + icon: '🛌', + label: 'Ruhetage', + sub: 'Kraft-, Cardio-, oder Entspannungs-Ruhetag erfassen', + to: '/rest-days', + color: '#9B59B6', + }, { icon: '📖', label: 'Messanleitung', diff --git a/frontend/src/pages/RestDaysPage.jsx b/frontend/src/pages/RestDaysPage.jsx new file mode 100644 index 0000000..01728ab --- /dev/null +++ b/frontend/src/pages/RestDaysPage.jsx @@ -0,0 +1,487 @@ +import { useState, useEffect } from 'react' +import { Pencil, Trash2, Check, X } from 'lucide-react' +import { api } from '../utils/api' +import dayjs from 'dayjs' +import 'dayjs/locale/de' +dayjs.locale('de') + +// Quick Mode Presets +const PRESETS = [ + { + id: 'muscle_recovery', + icon: '💪', + label: 'Kraft-Ruhetag', + description: 'Muskelregeneration – Kraft & HIIT pausieren, Cardio erlaubt', + color: '#D85A30', + config: { + focus: 'muscle_recovery', + rest_from: ['strength', 'hiit'], + allows: ['cardio_low', 'meditation', 'mobility', 'walk'], + intensity_max: 60, + } + }, + { + id: 'cardio_recovery', + icon: '🏃', + label: 'Cardio-Ruhetag', + description: 'Ausdauererholung – Cardio pausieren, Kraft & Mobility erlaubt', + color: '#378ADD', + config: { + focus: 'cardio_recovery', + rest_from: ['cardio', 'cardio_low', 'cardio_high'], + allows: ['strength', 'mobility', 'meditation'], + intensity_max: 70, + } + }, + { + id: 'mental_rest', + icon: '🧘', + label: 'Entspannungstag', + description: 'Mentale Erholung – Nur Meditation & Spaziergang', + color: '#7B68EE', + config: { + focus: 'mental_rest', + rest_from: ['strength', 'cardio', 'hiit', 'power'], + allows: ['meditation', 'walk'], + intensity_max: 40, + } + }, + { + id: 'deload', + icon: '📉', + label: 'Deload', + description: 'Reduzierte Intensität – Alles erlaubt, max 70% Last', + color: '#E67E22', + config: { + focus: 'deload', + rest_from: [], + allows: ['strength', 'cardio', 'mobility', 'meditation'], + intensity_max: 70, + } + }, +] + +const FOCUS_LABELS = { + muscle_recovery: '💪 Muskelregeneration', + cardio_recovery: '🏃 Cardio-Erholung', + mental_rest: '🧘 Mentale Erholung', + deload: '📉 Deload', + injury: '🩹 Verletzungspause', +} + +export default function RestDaysPage() { + const [restDays, setRestDays] = useState([]) + const [showForm, setShowForm] = useState(false) + const [formData, setFormData] = useState({ + date: dayjs().format('YYYY-MM-DD'), + preset: null, + note: '', + }) + const [editing, setEditing] = useState(null) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [toast, setToast] = useState(null) + + useEffect(() => { + loadRestDays() + }, []) + + const loadRestDays = async () => { + try { + const data = await api.listRestDays(90) + setRestDays(data) + } catch (err) { + console.error('Failed to load rest days:', err) + setError('Fehler beim Laden der Ruhetage') + } + } + + const showToast = (message, duration = 2000) => { + setToast(message) + setTimeout(() => setToast(null), duration) + } + + const handlePresetSelect = (preset) => { + setFormData(f => ({ ...f, preset: preset.id })) + } + + const handleSave = async () => { + if (!formData.preset) { + setError('Bitte wähle einen Ruhetag-Typ') + return + } + + setSaving(true) + setError(null) + + try { + const preset = PRESETS.find(p => p.id === formData.preset) + await api.createRestDay({ + date: formData.date, + rest_config: preset.config, + note: formData.note, + }) + + showToast('✓ Ruhetag gespeichert') + await loadRestDays() + setShowForm(false) + setFormData({ + date: dayjs().format('YYYY-MM-DD'), + preset: null, + note: '', + }) + } catch (err) { + console.error('Save failed:', err) + setError(err.message || 'Fehler beim Speichern') + } finally { + setSaving(false) + } + } + + const handleDelete = async (id) => { + if (!confirm('Ruhetag löschen?')) return + + try { + await api.deleteRestDay(id) + showToast('✓ Gelöscht') + await loadRestDays() + } catch (err) { + console.error('Delete failed:', err) + setError(err.message || 'Fehler beim Löschen') + } + } + + const startEdit = (day) => { + setEditing({ + ...day, + note: day.note || '', + }) + } + + const cancelEdit = () => { + setEditing(null) + } + + const handleUpdate = async () => { + try { + await api.updateRestDay(editing.id, { + note: editing.note, + }) + showToast('✓ Aktualisiert') + setEditing(null) + await loadRestDays() + } catch (err) { + console.error('Update failed:', err) + setError(err.message || 'Fehler beim Aktualisieren') + } + } + + return ( +
+

Ruhetage

+ + {/* Toast Notification */} + {toast && ( +
+ {toast} +
+ )} + + {/* New Entry Form */} + {!showForm ? ( + + ) : ( +
+
Neuer Ruhetag
+ + {/* Date */} +
+ + setFormData(f => ({ ...f, date: e.target.value }))} + /> + +
+ + {/* Presets */} +
+ +
+ {PRESETS.map(preset => ( + + ))} +
+
+ + {/* Note */} +
+ + setFormData(f => ({ ...f, note: e.target.value }))} + /> + +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Actions */} +
+ + +
+
+ )} + + {/* List */} +
+
+ Verlauf ({restDays.length}) +
+ + {restDays.length === 0 && ( +

Noch keine Ruhetage erfasst.

+ )} + + {restDays.map(day => { + const isEditing = editing?.id === day.id + const focus = day.rest_config?.focus || 'mental_rest' + const preset = PRESETS.find(p => p.id === focus) + + return ( +
+ {isEditing ? ( +
+
+ {dayjs(day.date).format('DD. MMMM YYYY')} +
+
+ + setEditing(d => ({ ...d, note: e.target.value }))} + /> + +
+
+ + +
+
+ ) : ( +
+
+
+ {dayjs(day.date).format('DD. MMMM YYYY')} +
+
+ + +
+
+ + {/* Preset Badge */} +
+ {preset?.icon || '📅'} {FOCUS_LABELS[focus] || focus} +
+ + {/* Note */} + {day.note && ( +

+ "{day.note}" +

+ )} + + {/* Details (collapsible, optional for later) */} + {day.rest_config && ( +
+ {day.rest_config.rest_from?.length > 0 && ( +
+ Pausiert: {day.rest_config.rest_from.join(', ')} +
+ )} + {day.rest_config.allows?.length > 0 && ( +
+ Erlaubt: {day.rest_config.allows.join(', ')} +
+ )} + {day.rest_config.intensity_max && ( +
+ Max Intensität: {day.rest_config.intensity_max}% HFmax +
+ )} +
+ )} +
+ )} +
+ ) + })} +
+
+ ) +}