import React, { useState, useEffect } from 'react'; import { Plus, Activity, ChevronDown, ChevronUp, Dumbbell, PlayCircle, CheckCircle, User, Scale, X, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, CheckSquare, Trash2, Percent, MoreVertical, Edit } from 'lucide-react'; import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, WorkoutPlan, Language } from '../types'; import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../services/storage'; import { getCurrentUserProfile } from '../services/auth'; import { t } from '../services/i18n'; import { generateId } from '../utils/uuid'; import { api } from '../services/api'; interface TrackerProps { userId: string; userWeight?: number; activeSession: WorkoutSession | null; activePlan: WorkoutPlan | null; onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void; onSessionEnd: () => void; onSessionQuit: () => void; onSetAdded: (set: WorkoutSet) => void; onRemoveSet: (setId: string) => void; onUpdateSet: (set: WorkoutSet) => void; lang: Language; } import FilledInput from './FilledInput'; import ExerciseModal from './ExerciseModal'; const Tracker: React.FC = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSessionQuit, onSetAdded, onRemoveSet, onUpdateSet, lang }) => { const [exercises, setExercises] = useState([]); const [plans, setPlans] = useState([]); const [selectedExercise, setSelectedExercise] = useState(null); const [lastSet, setLastSet] = useState(undefined); // Timer State const [elapsedTime, setElapsedTime] = useState('00:00:00'); // Form State const [weight, setWeight] = useState(''); const [reps, setReps] = useState(''); const [duration, setDuration] = useState(''); const [distance, setDistance] = useState(''); const [height, setHeight] = useState(''); const [bwPercentage, setBwPercentage] = useState('100'); // User Weight State const [userBodyWeight, setUserBodyWeight] = useState(userWeight ? userWeight.toString() : '70'); // Create Exercise State const [isCreating, setIsCreating] = useState(false); // Plan Execution State const [currentStepIndex, setCurrentStepIndex] = useState(0); const [showPlanPrep, setShowPlanPrep] = useState(null); const [showPlanList, setShowPlanList] = useState(false); // Confirmation State const [showFinishConfirm, setShowFinishConfirm] = useState(false); const [showQuitConfirm, setShowQuitConfirm] = useState(false); const [showMenu, setShowMenu] = useState(false); // Edit Set State const [editingSetId, setEditingSetId] = useState(null); const [editWeight, setEditWeight] = useState(''); const [editReps, setEditReps] = useState(''); const [editDuration, setEditDuration] = useState(''); const [editDistance, setEditDistance] = useState(''); const [editHeight, setEditHeight] = useState(''); useEffect(() => { const loadData = async () => { const exList = await getExercises(userId); setExercises(exList); const planList = await getPlans(userId); setPlans(planList); if (activeSession?.userBodyWeight) { setUserBodyWeight(activeSession.userBodyWeight.toString()); } else if (userWeight) { setUserBodyWeight(userWeight.toString()); } }; loadData(); }, [activeSession, userId, userWeight, activePlan]); // Timer Logic useEffect(() => { let interval: number; if (activeSession) { const updateTimer = () => { const diff = Math.floor((Date.now() - activeSession.startTime) / 1000); const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); const s = diff % 60; setElapsedTime(`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`); }; updateTimer(); interval = window.setInterval(updateTimer, 1000); } return () => clearInterval(interval); }, [activeSession]); // Recalculate current step when sets change useEffect(() => { if (activeSession && activePlan) { const performedCounts = new Map(); for (const set of activeSession.sets) { performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1); } let nextStepIndex = activePlan.steps.length; // Default to finished const plannedCounts = new Map(); for (let i = 0; i < activePlan.steps.length; i++) { const step = activePlan.steps[i]; const exerciseId = step.exerciseId; plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1); const performedCount = performedCounts.get(exerciseId) || 0; if (performedCount < plannedCounts.get(exerciseId)!) { nextStepIndex = i; break; } } setCurrentStepIndex(nextStepIndex); } }, [activeSession, activePlan]); useEffect(() => { if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) { if (currentStepIndex < activePlan.steps.length) { const step = activePlan.steps[currentStepIndex]; if (step) { const exDef = exercises.find(e => e.id === step.exerciseId); if (exDef) { setSelectedExercise(exDef); } } } } }, [currentStepIndex, activePlan, exercises]); useEffect(() => { const updateSelection = async () => { if (selectedExercise) { setBwPercentage(selectedExercise.bodyWeightPercentage ? selectedExercise.bodyWeightPercentage.toString() : '100'); const set = await getLastSetForExercise(userId, selectedExercise.id); setLastSet(set); if (set) { setWeight(set.weight?.toString() || ''); setReps(set.reps?.toString() || ''); setDuration(set.durationSeconds?.toString() || ''); setDistance(set.distanceMeters?.toString() || ''); setHeight(set.height?.toString() || ''); } else { setWeight(''); setReps(''); setDuration(''); setDistance(''); setHeight(''); } // Clear fields not relevant to the selected exercise type if (selectedExercise.type !== ExerciseType.STRENGTH && selectedExercise.type !== ExerciseType.BODYWEIGHT) { setWeight(''); } if (selectedExercise.type !== ExerciseType.STRENGTH && selectedExercise.type !== ExerciseType.BODYWEIGHT && selectedExercise.type !== ExerciseType.PLYOMETRIC) { setReps(''); } if (selectedExercise.type !== ExerciseType.CARDIO && selectedExercise.type !== ExerciseType.STATIC) { setDuration(''); } if (selectedExercise.type !== ExerciseType.CARDIO && selectedExercise.type !== ExerciseType.LONG_JUMP) { setDistance(''); } if (selectedExercise.type !== ExerciseType.HIGH_JUMP) { setHeight(''); } } }; updateSelection(); }, [selectedExercise, userId]); const handleStart = (plan?: WorkoutPlan) => { if (plan && plan.description) { setShowPlanPrep(plan); } else { onSessionStart(plan, parseFloat(userBodyWeight)); } }; const confirmPlanStart = () => { if (showPlanPrep) { onSessionStart(showPlanPrep, parseFloat(userBodyWeight)); setShowPlanPrep(null); } } const handleAddSet = async () => { if (!activeSession || !selectedExercise) return; const setData: Partial = { exerciseId: selectedExercise.id, }; switch (selectedExercise.type) { case ExerciseType.STRENGTH: if (weight) setData.weight = parseFloat(weight); if (reps) setData.reps = parseInt(reps); break; case ExerciseType.BODYWEIGHT: if (weight) setData.weight = parseFloat(weight); if (reps) setData.reps = parseInt(reps); setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; break; case ExerciseType.CARDIO: if (duration) setData.durationSeconds = parseInt(duration); if (distance) setData.distanceMeters = parseFloat(distance); break; case ExerciseType.STATIC: if (duration) setData.durationSeconds = parseInt(duration); setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; break; case ExerciseType.HIGH_JUMP: if (height) setData.height = parseFloat(height); break; case ExerciseType.LONG_JUMP: if (distance) setData.distanceMeters = parseFloat(distance); break; case ExerciseType.PLYOMETRIC: if (reps) setData.reps = parseInt(reps); break; } try { const response = await api.post('/sessions/active/log-set', setData); if (response.success) { const { newSet, activeExerciseId } = response; onSetAdded(newSet); if (activePlan && activeExerciseId) { const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId); if (nextStepIndex !== -1) { setCurrentStepIndex(nextStepIndex); } } else if (activePlan && !activeExerciseId) { // Plan is finished setCurrentStepIndex(activePlan.steps.length); } } } catch (error) { console.error("Failed to log set:", error); // Optionally, show an error message to the user } }; const handleCreateExercise = async (newEx: ExerciseDef) => { await saveExercise(userId, newEx); const exList = await getExercises(userId); setExercises(exList.filter(e => !e.isArchived)); setSelectedExercise(newEx); setIsCreating(false); }; const handleEditSet = (set: WorkoutSet) => { setEditingSetId(set.id); setEditWeight(set.weight?.toString() || ''); setEditReps(set.reps?.toString() || ''); setEditDuration(set.durationSeconds?.toString() || ''); setEditDistance(set.distanceMeters?.toString() || ''); setEditHeight(set.height?.toString() || ''); }; const handleSaveEdit = (set: WorkoutSet) => { const updatedSet: WorkoutSet = { ...set, ...(editWeight && { weight: parseFloat(editWeight) }), ...(editReps && { reps: parseInt(editReps) }), ...(editDuration && { durationSeconds: parseInt(editDuration) }), ...(editDistance && { distanceMeters: parseFloat(editDistance) }), ...(editHeight && { height: parseFloat(editHeight) }) }; onUpdateSet(updatedSet); setEditingSetId(null); }; const handleCancelEdit = () => { setEditingSetId(null); }; const jumpToStep = (index: number) => { if (!activePlan) return; setCurrentStepIndex(index); setShowPlanList(false); }; const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length; const exerciseTypeLabels: Record = { [ExerciseType.STRENGTH]: t('type_strength', lang), [ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang), [ExerciseType.CARDIO]: t('type_cardio', lang), [ExerciseType.STATIC]: t('type_static', lang), [ExerciseType.HIGH_JUMP]: t('type_height', lang), [ExerciseType.LONG_JUMP]: t('type_dist', lang), [ExerciseType.PLYOMETRIC]: t('type_jump', lang), }; if (!activeSession) { return (

{t('ready_title', lang)}

{t('ready_subtitle', lang)}

setUserBodyWeight(e.target.value)} />

{t('change_in_profile', lang)}

{plans.length > 0 && (

{t('or_choose_plan', lang)}

{plans.map(plan => ( ))}
)}
{showPlanPrep && (

{showPlanPrep.name}

{t('prep_title', lang)}
{showPlanPrep.description || t('prep_no_instructions', lang)}
)}
); } return (

{activePlan ? activePlan.name : t('free_workout', lang)}

{elapsedTime} {activeSession.userBodyWeight ? ` • ${activeSession.userBodyWeight}kg` : ''}
{showMenu && ( <>
setShowMenu(false)} />
)}
{activePlan && (
{showPlanList && (
{activePlan.steps.map((step, idx) => ( ))}
)}
)}
{selectedExercise && (
{(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( setWeight(e.target.value)} icon={} autoFocus={activePlan && !isPlanFinished && activePlan.steps[currentStepIndex]?.isWeighted && (selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STRENGTH)} /> )} {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( setReps(e.target.value)} icon={} type="number" /> )} {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( setDuration(e.target.value)} icon={} /> )} {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( setDistance(e.target.value)} icon={} /> )} {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( setHeight(e.target.value)} icon={} /> )}
)} {activeSession.sets.length > 0 && (

{t('history_section', lang)}

{[...activeSession.sets].reverse().map((set, idx) => { const setNumber = activeSession.sets.length - idx; const isEditing = editingSetId === set.id; return (
{setNumber}
{isEditing ? (
{set.exerciseName}
{set.weight !== undefined && ( setEditWeight(e.target.value)} className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none" placeholder="Weight (kg)" /> )} {set.reps !== undefined && ( setEditReps(e.target.value)} className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none" placeholder="Reps" /> )} {set.durationSeconds !== undefined && ( setEditDuration(e.target.value)} className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none" placeholder="Duration (s)" /> )} {set.distanceMeters !== undefined && ( setEditDistance(e.target.value)} className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none" placeholder="Distance (m)" /> )} {set.height !== undefined && ( setEditHeight(e.target.value)} className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none" placeholder="Height (cm)" /> )}
) : (
{set.exerciseName}
{set.type === ExerciseType.STRENGTH && `${set.weight ? `${set.weight}kg` : ''} ${set.reps ? `x ${set.reps}` : ''}`.trim() } {set.type === ExerciseType.BODYWEIGHT && `${set.weight ? `${set.weight}kg` : ''} ${set.reps ? `x ${set.reps}` : ''}`.trim() } {set.type === ExerciseType.CARDIO && `${set.durationSeconds ? `${set.durationSeconds}s` : ''} ${set.distanceMeters ? `/ ${set.distanceMeters}m` : ''}`.trim() } {set.type === ExerciseType.STATIC && `${set.durationSeconds ? `${set.durationSeconds}s` : ''}`.trim() } {set.type === ExerciseType.HIGH_JUMP && `${set.height ? `${set.height}cm` : ''}`.trim() } {set.type === ExerciseType.LONG_JUMP && `${set.distanceMeters ? `${set.distanceMeters}m` : ''}`.trim() } {set.type === ExerciseType.PLYOMETRIC && `${set.reps ? `x ${set.reps}` : ''}`.trim() }
)}
{isEditing ? ( <> ) : ( <> )}
); })}
)}
{isCreating && ( setIsCreating(false)} onSave={handleCreateExercise} lang={lang} /> )} {/* Finish Confirmation Dialog */} {showFinishConfirm && (

{t('finish_confirm_title', lang)}

{t('finish_confirm_msg', lang)}

)} {/* Quit Without Saving Confirmation Dialog */} {showQuitConfirm && (

{t('quit_confirm_title', lang)}

{t('quit_confirm_msg', lang)}

)}
); }; export default Tracker;