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
This commit is contained in:
2026-03-01 19:41:54 +01:00
parent a24199e56c
commit b5c9250a10
3 changed files with 77 additions and 10 deletions
+9 -8
View File
@@ -1,12 +1,13 @@
{ {
"lastRun": "2026-03-01T11:31:00+01:00", "lastRun": "2026-03-01T17:38:00+01:00",
"status": "in_progress", "status": "completed",
"phase": "04-workout-modification", "phase": "04-workout-modification",
"activeTask": "04-03-frontend-workout-edit", "activeTask": "04-03-frontend-workout-edit",
"tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api"], "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-03-frontend-workout-edit", "nextTask": "04-04-visual-distinction",
"agentSession": "swift-trail", "agentSession": "claude-code-frontend",
"agentType": "claude-coding-agent", "agentType": "claude-code-local-exec",
"spawnTime": "2026-03-01T11:31:00+01:00", "spawnTime": "2026-03-01T17:38: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." "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."
} }
+40
View File
@@ -2958,3 +2958,43 @@
border: 2px solid var(--border); display: flex; align-items: center; justify-content: center; 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; } .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);
}
+28 -2
View File
@@ -17,11 +17,13 @@ const getWorkoutColor = (name) => {
function WorkoutSelectPage({ onBack, onSelectWorkout }) { function WorkoutSelectPage({ onBack, onSelectWorkout }) {
const [program, setProgram] = useState(null) const [program, setProgram] = useState(null)
const [customWorkouts, setCustomWorkouts] = useState([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selectedWorkout, setSelectedWorkout] = useState(null) const [selectedWorkout, setSelectedWorkout] = useState(null)
useEffect(() => { useEffect(() => {
fetchProgram() fetchProgram()
fetchCustomWorkouts()
}, []) }, [])
const fetchProgram = async () => { 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) => { const handleSelect = (workout) => {
setSelectedWorkout(workout) setSelectedWorkout(workout)
} }
@@ -76,6 +96,7 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) {
const color = getWorkoutColor(workout.name) const color = getWorkoutColor(workout.name)
const isSelected = selectedWorkout?.id === workout.id const isSelected = selectedWorkout?.id === workout.id
const exerciseCount = workout.exercises?.filter(e => e.name).length || 0 const exerciseCount = workout.exercises?.filter(e => e.name).length || 0
const isCustom = isWorkoutCustom(workout.id)
return ( return (
<div <div
@@ -84,8 +105,13 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) {
style={{ '--workout-color': color }} style={{ '--workout-color': color }}
onClick={() => handleSelect(workout)} onClick={() => handleSelect(workout)}
> >
<div className="workout-icon" style={{ background: color }}> <div className="workout-badge-container">
<Icon name={iconName} size={28} /> <div className="workout-icon" style={{ background: color }}>
<Icon name={iconName} size={28} />
</div>
<span className={`workout-badge ${isCustom ? 'custom' : 'program'}`}>
{isCustom ? 'Anpassad' : 'Program'}
</span>
</div> </div>
<div className="workout-details"> <div className="workout-details">
<h3>{workout.name}</h3> <h3>{workout.name}</h3>