From 5fea907f4c43dd47cea50aa811201402cdb63fef Mon Sep 17 00:00:00 2001 From: Clawd Agent Date: Sun, 1 Mar 2026 19:41:54 +0100 Subject: [PATCH] feat(04-04-visual-distinction): Add custom vs program workout badges on WorkoutSelectPage - Fetch custom workouts for authenticated user - Display 'Anpassad' (custom) or 'Program' badge on each workout card - Add badge component with orange accent for custom, muted color for program - Badge positioned bottom-right of workout icon - Responsive styling consistent with Gravl dark theme - All build checks pass --- .pm-checkpoint.json | 17 +++++----- frontend/src/App.css | 40 ++++++++++++++++++++++++ frontend/src/pages/WorkoutSelectPage.jsx | 30 ++++++++++++++++-- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/.pm-checkpoint.json b/.pm-checkpoint.json index a564a8c..c42c6aa 100644 --- a/.pm-checkpoint.json +++ b/.pm-checkpoint.json @@ -1,12 +1,13 @@ { - "lastRun": "2026-03-01T11:31:00+01:00", - "status": "in_progress", + "lastRun": "2026-03-01T17:38:00+01:00", + "status": "completed", "phase": "04-workout-modification", "activeTask": "04-03-frontend-workout-edit", - "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api"], - "nextTask": "04-03-frontend-workout-edit", - "agentSession": "swift-trail", - "agentType": "claude-coding-agent", - "spawnTime": "2026-03-01T11:31:00+01:00", - "notes": "Spawned Claude coding agent (swift-trail) with exec+pty in background. Task: Build Edit Workout button, ExercisePicker modal, swap/add exercise flows, fork confirmation dialog, and save to custom_workouts API. Monitoring progress." + "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api", "04-03-frontend-workout-edit"], + "nextTask": "04-04-visual-distinction", + "agentSession": "claude-code-frontend", + "agentType": "claude-code-local-exec", + "spawnTime": "2026-03-01T17:38:00+01:00", + "result": "Phase 04-03 complete. Edit workflow implemented: ExercisePicker modal, swap/add/remove exercise flows, fork confirmation dialog, API integration (POST/PUT custom-workouts). All success criteria met. Ready for 04-04.", + "notes": "Previous attempt hit Gemini quota limit. Recovered at 17:38. Advancing to 04-04: Add visual distinction badges (custom vs program) on WorkoutSelectPage." } diff --git a/frontend/src/App.css b/frontend/src/App.css index 0ae13cd..1893ffc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2958,3 +2958,43 @@ border: 2px solid var(--border); display: flex; align-items: center; justify-content: center; } .warmup-item.done .warmup-check { background: var(--success); border-color: var(--success); color: white; } + +/* Workout badge styling */ +.workout-badge-container { + position: relative; + display: flex; + align-items: flex-end; +} + +.workout-badge { + position: absolute; + bottom: -6px; + right: -6px; + font-size: var(--font-xs); + font-weight: 600; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-sm); + border: 1px solid transparent; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + white-space: nowrap; + color: white; +} + +.workout-badge.custom { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +.workout-badge.program { + background: var(--text-muted); + color: white; + border-color: var(--text-muted); + opacity: 0.7; +} + +.workout-select-card:hover .workout-badge { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); +} diff --git a/frontend/src/pages/WorkoutSelectPage.jsx b/frontend/src/pages/WorkoutSelectPage.jsx index fcd7597..564c804 100644 --- a/frontend/src/pages/WorkoutSelectPage.jsx +++ b/frontend/src/pages/WorkoutSelectPage.jsx @@ -17,11 +17,13 @@ const getWorkoutColor = (name) => { function WorkoutSelectPage({ onBack, onSelectWorkout }) { const [program, setProgram] = useState(null) + const [customWorkouts, setCustomWorkouts] = useState([]) const [loading, setLoading] = useState(true) const [selectedWorkout, setSelectedWorkout] = useState(null) useEffect(() => { fetchProgram() + fetchCustomWorkouts() }, []) const fetchProgram = async () => { @@ -36,6 +38,24 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { } } + const fetchCustomWorkouts = async () => { + try { + const token = localStorage.getItem('token') + if (!token) return + const res = await fetch(`${API_URL}/custom-workouts`, { + headers: { 'Authorization': `Bearer ${token}` } + }) + const data = await res.json() + setCustomWorkouts(data || []) + } catch (err) { + console.error('Failed to fetch custom workouts:', err) + } + } + + const isWorkoutCustom = (programDayId) => { + return customWorkouts.some(cw => cw.source_program_day_id === programDayId) + } + const handleSelect = (workout) => { setSelectedWorkout(workout) } @@ -76,6 +96,7 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { const color = getWorkoutColor(workout.name) const isSelected = selectedWorkout?.id === workout.id const exerciseCount = workout.exercises?.filter(e => e.name).length || 0 + const isCustom = isWorkoutCustom(workout.id) return (
handleSelect(workout)} > -
- +
+
+ +
+ + {isCustom ? 'Anpassad' : 'Program'} +

{workout.name}