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:
@@ -1150,3 +1150,305 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
WORKOUT PAGE STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.workout-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .header-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .header-center h1 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .header-subtitle {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .header-progress {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .workout-main {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar */
|
||||||
|
.workout-progress-bar {
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--accent), var(--success));
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Uppvärmningssektion */
|
||||||
|
.warmup-section {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-section.completed {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: rgba(78, 204, 163, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-title h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-progress {
|
||||||
|
background: var(--bg);
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-content {
|
||||||
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-category {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-category:last-of-type {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-category h3 {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item:hover {
|
||||||
|
background: var(--bg-card-hover, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item.done {
|
||||||
|
background: rgba(78, 204, 163, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item.done .warmup-name {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-check {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item.done .warmup-check {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-duration {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-done-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-done-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-done-btn.completed {
|
||||||
|
background: var(--success);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Övningssektion */
|
||||||
|
.exercises-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exercises-section h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exercise Card utökad */
|
||||||
|
.exercise-card.all-done {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: rgba(78, 204, 163, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exercise-card.all-done .exercise-info h3::after {
|
||||||
|
content: ' ✓';
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avsluta pass knapp */
|
||||||
|
.finish-workout-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finish-workout-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.finish-workout-btn.ready {
|
||||||
|
background: linear-gradient(135deg, var(--accent) 0%, #6366f1 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
animation: pulse-glow 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(233, 69, 96, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px 5px rgba(233, 69, 96, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimeringar för WorkoutPage */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.workout-page .page-header {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workout-page .header-center h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-item {
|
||||||
|
padding: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warmup-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-154
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState } from 'react'
|
||||||
import { useAuth } from './context/AuthContext'
|
import { useAuth } from './context/AuthContext'
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import ProfilePage from './pages/ProfilePage'
|
import ProfilePage from './pages/ProfilePage'
|
||||||
import ProgressPage from './pages/ProgressPage'
|
import ProgressPage from './pages/ProgressPage'
|
||||||
|
import WorkoutPage from './pages/WorkoutPage'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
const API_URL = '/api'
|
const API_URL = '/api'
|
||||||
@@ -118,7 +119,7 @@ function App() {
|
|||||||
// Workout view
|
// Workout view
|
||||||
if (view === 'workout' && selectedDay) {
|
if (view === 'workout' && selectedDay) {
|
||||||
return (
|
return (
|
||||||
<WorkoutView
|
<WorkoutPage
|
||||||
day={selectedDay}
|
day={selectedDay}
|
||||||
week={currentWeek}
|
week={currentWeek}
|
||||||
logs={logs}
|
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
|
export default App
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user