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 7167f89e54
commit 8314ac2f1c
8 changed files with 147 additions and 80 deletions
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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Gravl - Träning</title>
<script type="module" crossorigin src="/assets/index-hhKetRGz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-my_lGtI5.css">
<script type="module" crossorigin src="/assets/index-DLV768U5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-VYqTaBQ1.css">
</head>
<body>
<div id="root"></div>
+40
View File
@@ -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);
}
+28 -2
View File
@@ -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 (
<div
@@ -84,8 +105,13 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) {
style={{ '--workout-color': color }}
onClick={() => handleSelect(workout)}
>
<div className="workout-icon" style={{ background: color }}>
<Icon name={iconName} size={28} />
<div className="workout-badge-container">
<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 className="workout-details">
<h3>{workout.name}</h3>