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
+3 -154
View File
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { useAuth } from './context/AuthContext'
import Dashboard from './pages/Dashboard'
import ProfilePage from './pages/ProfilePage'
import ProgressPage from './pages/ProgressPage'
import WorkoutPage from './pages/WorkoutPage'
import './App.css'
const API_URL = '/api'
@@ -118,7 +119,7 @@ function App() {
// Workout view
if (view === 'workout' && selectedDay) {
return (
<WorkoutView
<WorkoutPage
day={selectedDay}
week={currentWeek}
logs={logs}
@@ -138,156 +139,4 @@ function App() {
)
}
function WorkoutView({ day, week, logs, onLogSet, onBack, fetchProgression }) {
const [progressions, setProgressions] = useState({})
const [expandedExercise, setExpandedExercise] = useState(null)
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) || []
return (
<div className="app workout-view">
<header className="header workout-header">
<button className="back-btn" onClick={onBack}> Tillbaka</button>
<div className="header-title">
<h1>{day.name}</h1>
<span className="header-subtitle">Vecka {week} Dag {day.day_number}</span>
</div>
</header>
<main className="main workout-main">
{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}
/>
))}
</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' : ''}`}>
<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 App