Add WorkoutPage with warmup exercises (Claude Code)

- Dedicated workout page with progress tracking
- Warmup section with general + muscle-specific exercises
- Preparatory sets (2x10 @ 50% of first exercise)
- Checkbox tracking for warmup completion
- Progress bar showing completed exercises
- Animated 'Finish workout' button when done
- Mobile-first CSS with responsive design

Built by Claude Code 2.1.29
This commit is contained in:
2026-02-01 14:20:00 +01:00
parent 66812f9db2
commit 73d1f39ea9
3 changed files with 658 additions and 154 deletions
+353
View File
@@ -0,0 +1,353 @@
import { useState, useEffect } from 'react'
// Uppvärmningsövningar baserat på muskelgrupp
const warmupExercises = {
general: [
{ name: 'Cykel eller roddmaskin', duration: '5 min', icon: '🚴' },
{ name: 'Armcirklar', duration: '30 sek/riktning', icon: '🔄' },
{ name: 'Bensvingar (framåt/bakåt)', duration: '10 per ben', icon: '🦵' },
{ name: 'Bensvingar (sidled)', duration: '10 per ben', icon: '🦵' },
{ name: 'Höftcirklar', duration: '10 per riktning', icon: '⭕' },
],
specific: {
'Bröst': [
{ name: 'Lätta armhävningar', reps: '10-15' },
{ name: 'Band pull-aparts', reps: '15-20' },
],
'Rygg': [
{ name: 'Lat stretch', reps: '30 sek/sida' },
{ name: 'Lätta rodd-drag', reps: '15-20' },
],
'Ben': [
{ name: 'Bodyweight squats', reps: '15-20' },
{ name: 'Utfallssteg', reps: '10/ben' },
],
'Axlar': [
{ name: 'Axelrotationer', reps: '10/riktning' },
{ name: 'Band dislocates', reps: '10-15' },
],
'Armar': [
{ name: 'Handledscirklar', reps: '10/riktning' },
{ name: 'Lätta bicepscurls', reps: '15-20' },
],
}
}
// Mappa övningar till muskelgrupper
function getMuscleGroups(exercises) {
const groups = new Set()
exercises.forEach(ex => {
if (ex.muscle_group) {
groups.add(ex.muscle_group)
}
})
return Array.from(groups)
}
function WorkoutPage({ day, week, logs, onLogSet, onBack, fetchProgression }) {
const [progressions, setProgressions] = useState({})
const [expandedExercise, setExpandedExercise] = useState(null)
const [warmupDone, setWarmupDone] = useState(false)
const [warmupExpanded, setWarmupExpanded] = useState(true)
const [completedWarmups, setCompletedWarmups] = useState(new Set())
useEffect(() => {
loadProgressions()
}, [day])
const loadProgressions = async () => {
const progs = {}
for (const exercise of day.exercises) {
if (exercise.id) {
progs[exercise.id] = await fetchProgression(exercise.id)
}
}
setProgressions(progs)
}
const exercises = day.exercises?.filter(e => e.name) || []
const muscleGroups = getMuscleGroups(exercises)
// Beräkna progress
const completedExercises = exercises.filter(ex => {
const exLogs = logs[ex.id] || []
const completedSets = exLogs.filter(l => l.completed).length
return completedSets >= ex.sets
}).length
const toggleWarmup = (idx) => {
const newCompleted = new Set(completedWarmups)
if (newCompleted.has(idx)) {
newCompleted.delete(idx)
} else {
newCompleted.add(idx)
}
setCompletedWarmups(newCompleted)
}
// Kombinera generell och specifik uppvärmning
const generalWarmups = warmupExercises.general
const specificWarmups = muscleGroups.flatMap(group =>
warmupExercises.specific[group] || []
)
const totalWarmups = generalWarmups.length + specificWarmups.length
const warmupProgress = completedWarmups.size
return (
<div className="workout-page">
<header className="page-header">
<button className="back-btn" onClick={onBack}> Tillbaka</button>
<div className="header-center">
<h1>{day.name}</h1>
<span className="header-subtitle">Vecka {week} Dag {day.day_number}</span>
</div>
<div className="header-progress">
<span className="progress-text">{completedExercises}/{exercises.length}</span>
</div>
</header>
<main className="page-main workout-main">
{/* Progress Bar */}
<div className="workout-progress-bar">
<div
className="workout-progress-fill"
style={{ width: `${(completedExercises / exercises.length) * 100}%` }}
/>
</div>
{/* Uppvärmningssektion */}
<section className={`warmup-section ${warmupDone ? 'completed' : ''}`}>
<div
className="warmup-header"
onClick={() => setWarmupExpanded(!warmupExpanded)}
>
<div className="warmup-title">
<span className="warmup-icon">🔥</span>
<h2>Uppvärmning</h2>
<span className="warmup-progress">{warmupProgress}/{totalWarmups}</span>
</div>
<span className={`expand-icon ${warmupExpanded ? 'expanded' : ''}`}></span>
</div>
{warmupExpanded && (
<div className="warmup-content">
{/* Generell uppvärmning */}
<div className="warmup-category">
<h3>Generell uppvärmning (5-10 min)</h3>
<div className="warmup-list">
{generalWarmups.map((warmup, idx) => (
<div
key={idx}
className={`warmup-item ${completedWarmups.has(idx) ? 'done' : ''}`}
onClick={() => toggleWarmup(idx)}
>
<span className="warmup-check">
{completedWarmups.has(idx) ? '✓' : '○'}
</span>
<span className="warmup-item-icon">{warmup.icon}</span>
<span className="warmup-name">{warmup.name}</span>
<span className="warmup-duration">{warmup.duration || warmup.reps}</span>
</div>
))}
</div>
</div>
{/* Specifik uppvärmning */}
{specificWarmups.length > 0 && (
<div className="warmup-category">
<h3>Specifik för {muscleGroups.join(', ')}</h3>
<div className="warmup-list">
{specificWarmups.map((warmup, idx) => {
const globalIdx = generalWarmups.length + idx
return (
<div
key={globalIdx}
className={`warmup-item ${completedWarmups.has(globalIdx) ? 'done' : ''}`}
onClick={() => toggleWarmup(globalIdx)}
>
<span className="warmup-check">
{completedWarmups.has(globalIdx) ? '✓' : '○'}
</span>
<span className="warmup-name">{warmup.name}</span>
<span className="warmup-duration">{warmup.reps}</span>
</div>
)
})}
</div>
</div>
)}
{/* Lätt set av första övningen */}
{exercises[0] && (
<div className="warmup-category">
<h3>Förberedande set</h3>
<div className="warmup-list">
<div
className={`warmup-item ${completedWarmups.has('prep') ? 'done' : ''}`}
onClick={() => {
const newCompleted = new Set(completedWarmups)
if (newCompleted.has('prep')) {
newCompleted.delete('prep')
} else {
newCompleted.add('prep')
}
setCompletedWarmups(newCompleted)
}}
>
<span className="warmup-check">
{completedWarmups.has('prep') ? '✓' : '○'}
</span>
<span className="warmup-name">Lätta set {exercises[0].name}</span>
<span className="warmup-duration">2×10 @ 50%</span>
</div>
</div>
</div>
)}
<button
className={`warmup-done-btn ${warmupDone ? 'completed' : ''}`}
onClick={() => setWarmupDone(!warmupDone)}
>
{warmupDone ? '✓ Uppvärmning klar!' : 'Markera uppvärmning som klar'}
</button>
</div>
)}
</section>
{/* Övningslista */}
<section className="exercises-section">
<h2>Övningar</h2>
{exercises.map((exercise, idx) => (
<ExerciseCard
key={exercise.id || idx}
exercise={exercise}
logs={logs[exercise.id] || []}
progression={progressions[exercise.id]}
expanded={expandedExercise === exercise.id}
onToggle={() => setExpandedExercise(
expandedExercise === exercise.id ? null : exercise.id
)}
onLogSet={onLogSet}
/>
))}
</section>
{/* Avsluta pass */}
<button
className={`finish-workout-btn ${completedExercises === exercises.length ? 'ready' : ''}`}
onClick={onBack}
>
{completedExercises === exercises.length
? '🎉 Avsluta pass'
: `Avsluta pass (${completedExercises}/${exercises.length} klara)`}
</button>
</main>
</div>
)
}
function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSet }) {
const [setInputs, setSetInputs] = useState({})
useEffect(() => {
// Initialize with suggested weight or last logged
const initial = {}
for (let i = 1; i <= exercise.sets; i++) {
const existingLog = logs.find(l => l.set_number === i)
initial[i] = {
weight: existingLog?.weight || progression?.suggestedWeight || '',
reps: existingLog?.reps || '',
completed: existingLog?.completed || false
}
}
setSetInputs(initial)
}, [exercise, logs, progression])
const handleInputChange = (setNum, field, value) => {
setSetInputs(prev => ({
...prev,
[setNum]: { ...prev[setNum], [field]: value }
}))
}
const handleComplete = (setNum) => {
const input = setInputs[setNum]
const newCompleted = !input.completed
setSetInputs(prev => ({
...prev,
[setNum]: { ...prev[setNum], completed: newCompleted }
}))
onLogSet(exercise.id, setNum, input.weight, input.reps, newCompleted)
}
const completedSets = Object.values(setInputs).filter(s => s.completed).length
return (
<div className={`exercise-card ${expanded ? 'expanded' : ''} ${completedSets === exercise.sets ? 'all-done' : ''}`}>
<div className="exercise-header" onClick={onToggle}>
<div className="exercise-info">
<h3>{exercise.name}</h3>
<span className="muscle-group">{exercise.muscle_group}</span>
</div>
<div className="exercise-meta">
<span className="sets-info">{exercise.sets}×{exercise.reps_min}-{exercise.reps_max}</span>
<span className={`progress-badge ${completedSets === exercise.sets ? 'complete' : ''}`}>
{completedSets}/{exercise.sets}
</span>
</div>
</div>
{expanded && (
<div className="exercise-body">
{progression && (
<div className="progression-hint">
💡 {progression.reason}
{progression.suggestedWeight && (
<strong> {progression.suggestedWeight} kg</strong>
)}
</div>
)}
<div className="sets-list">
{Array.from({ length: exercise.sets }, (_, i) => i + 1).map(setNum => {
const input = setInputs[setNum] || { weight: '', reps: '', completed: false }
return (
<div key={setNum} className={`set-row ${input.completed ? 'completed' : ''}`}>
<span className="set-number">Set {setNum}</span>
<div className="set-inputs">
<input
type="number"
placeholder="kg"
value={input.weight}
onChange={(e) => handleInputChange(setNum, 'weight', e.target.value)}
className="weight-input"
inputMode="decimal"
/>
<span className="input-separator">×</span>
<input
type="number"
placeholder="reps"
value={input.reps}
onChange={(e) => handleInputChange(setNum, 'reps', e.target.value)}
className="reps-input"
inputMode="numeric"
/>
</div>
<button
className={`complete-btn ${input.completed ? 'done' : ''}`}
onClick={() => handleComplete(setNum)}
>
{input.completed ? '✓' : '○'}
</button>
</div>
)
})}
</div>
</div>
)}
</div>
)
}
export default WorkoutPage