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 843771e935
commit 368071ecae
8 changed files with 147 additions and 80 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."
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Gravl - Träning</title> <title>Gravl - Träning</title>
<script type="module" crossorigin src="/assets/index-hhKetRGz.js"></script> <script type="module" crossorigin src="/assets/index-DLV768U5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-my_lGtI5.css"> <link rel="stylesheet" crossorigin href="/assets/index-VYqTaBQ1.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+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>