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:
+3
-154
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user