Sporadic set logging added

This commit is contained in:
AG
2025-11-29 19:03:42 +02:00
parent d86abd6b1b
commit b5c8e8ac43
15 changed files with 1491 additions and 396 deletions

View File

@@ -1,19 +1,24 @@
import React, { useState } from 'react';
import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react';
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
import { WorkoutSession, ExerciseType, WorkoutSet, Language, SporadicSet } from '../types';
import { t } from '../services/i18n';
interface HistoryProps {
sessions: WorkoutSession[];
sporadicSets?: SporadicSet[];
onUpdateSession?: (session: WorkoutSession) => void;
onDeleteSession?: (sessionId: string) => void;
onUpdateSporadicSet?: (set: SporadicSet) => void;
onDeleteSporadicSet?: (setId: string) => void;
lang: Language;
}
const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSession, lang }) => {
const History: React.FC<HistoryProps> = ({ sessions, sporadicSets, onUpdateSession, onDeleteSession, onUpdateSporadicSet, onDeleteSporadicSet, lang }) => {
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [editingSporadicSet, setEditingSporadicSet] = useState<SporadicSet | null>(null);
const [deletingSporadicId, setDeletingSporadicId] = useState<string | null>(null);
const calculateSessionWork = (session: WorkoutSession) => {
const bw = session.userBodyWeight || 70;
@@ -88,6 +93,25 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
}
}
const handleSaveSporadicEdit = () => {
if (editingSporadicSet && onUpdateSporadicSet) {
onUpdateSporadicSet(editingSporadicSet);
setEditingSporadicSet(null);
}
};
const handleUpdateSporadicField = (field: keyof SporadicSet, value: number) => {
if (!editingSporadicSet) return;
setEditingSporadicSet({ ...editingSporadicSet, [field]: value });
};
const handleConfirmDeleteSporadic = () => {
if (deletingSporadicId && onDeleteSporadicSet) {
onDeleteSporadicSet(deletingSporadicId);
setDeletingSporadicId(null);
}
};
if (sessions.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full text-on-surface-variant p-8 text-center">
@@ -175,6 +199,65 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
})}
</div>
{/* Sporadic Sets Section */}
{sporadicSets && sporadicSets.length > 0 && (
<div className="mt-8">
<h3 className="text-xl font-medium text-on-surface mb-4 px-2">{t('sporadic_sets_title', lang)}</h3>
{Object.entries(
sporadicSets.reduce((groups: Record<string, SporadicSet[]>, set) => {
const date = new Date(set.timestamp).toISOString().split('T')[0];
if (!groups[date]) groups[date] = [];
groups[date].push(set);
return groups;
}, {})
)
.sort(([a], [b]) => b.localeCompare(a))
.map(([date, sets]) => (
<div key={date} className="mb-4">
<div className="text-sm text-on-surface-variant px-2 mb-2 font-medium">{date}</div>
<div className="space-y-2">
{sets.map(set => (
<div
key={set.id}
className="bg-surface-container-low rounded-xl p-4 border border-outline-variant/10 flex justify-between items-center"
>
<div className="flex-1">
<div className="font-medium text-on-surface">{set.exerciseName}</div>
<div className="text-sm text-on-surface-variant mt-1">
{set.type === ExerciseType.STRENGTH && `${set.weight || 0}kg x ${set.reps || 0}`}
{set.type === ExerciseType.BODYWEIGHT && `${set.weight ? `+${set.weight}kg` : 'BW'} x ${set.reps || 0}`}
{set.type === ExerciseType.CARDIO && `${set.durationSeconds || 0}s ${set.distanceMeters ? `/ ${set.distanceMeters}m` : ''}`}
{set.type === ExerciseType.STATIC && `${set.durationSeconds || 0}s`}
{set.type === ExerciseType.HIGH_JUMP && `${set.height || 0}cm`}
{set.type === ExerciseType.LONG_JUMP && `${set.distanceMeters || 0}m`}
{set.type === ExerciseType.PLYOMETRIC && `x ${set.reps || 0}`}
</div>
<div className="text-xs text-on-surface-variant mt-1">
{new Date(set.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<div className="flex gap-1">
<button
onClick={() => setEditingSporadicSet(JSON.parse(JSON.stringify(set)))}
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors"
>
<Pencil size={18} />
</button>
<button
onClick={() => setDeletingSporadicId(set.id)}
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
>
<Trash2 size={18} />
</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
{/* DELETE CONFIRMATION DIALOG (MD3) */}
{deletingId && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
@@ -338,6 +421,30 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
</div>
</div>
)}
{/* Sporadic Set Delete Confirmation */}
{deletingSporadicId && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-surface-container w-full max-w-xs rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-xl font-normal text-on-surface mb-2">{t('delete', lang)}</h3>
<p className="text-sm text-on-surface-variant mb-8">{t('delete_confirm', lang)}</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setDeletingSporadicId(null)}
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
>
{t('cancel', lang)}
</button>
<button
onClick={handleConfirmDeleteSporadic}
className="px-4 py-2 rounded-full bg-error-container text-on-error-container font-medium"
>
{t('delete', lang)}
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -1,402 +1,85 @@
import React from 'react';
import { MoreVertical, X, CheckSquare, ChevronUp, ChevronDown, Scale, Dumbbell, Plus, Activity, Timer as TimerIcon, ArrowRight, ArrowUp, CheckCircle, Edit, Trash2 } from 'lucide-react';
import { ExerciseType, Language, WorkoutSet } from '../../types';
import { t } from '../../services/i18n';
import FilledInput from '../FilledInput';
import ExerciseModal from '../ExerciseModal';
import { useTracker } from './useTracker';
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;
interface ActiveSessionViewProps {
tracker: ReturnType<typeof useTracker>;
activeSession: any; // Using any to avoid strict type issues with the complex session object for now, but ideally should be WorkoutSession
lang: Language;
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 ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet }) => {
const {
elapsedTime,
showFinishConfirm,
setShowFinishConfirm,
showQuitConfirm,
setShowQuitConfirm,
showMenu,
setShowMenu,
activePlan, // This comes from useTracker props but we might need to pass it explicitly if not in hook return
currentStepIndex,
showPlanList,
setShowPlanList,
jumpToStep,
searchQuery,
setSearchQuery,
setShowSuggestions,
showSuggestions,
filteredExercises,
setSelectedExercise,
selectedExercise,
weight,
setWeight,
reps,
setReps,
duration,
setDuration,
distance,
setDistance,
height,
setHeight,
handleAddSet,
editingSetId,
editWeight,
setEditWeight,
editReps,
setEditReps,
editDuration,
setEditDuration,
editDistance,
setEditDistance,
editHeight,
setEditHeight,
handleCancelEdit,
handleSaveEdit,
handleEditSet,
isCreating,
setIsCreating,
handleCreateExercise,
exercises
} = tracker;
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSessionQuit, onSetAdded, onRemoveSet, onUpdateSet, lang }) => {
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [selectedExercise, setSelectedExercise] = useState<ExerciseDef | null>(null);
const [lastSet, setLastSet] = useState<WorkoutSet | undefined>(undefined);
const [searchQuery, setSearchQuery] = useState<string>('');
const [showSuggestions, setShowSuggestions] = useState(false);
// Timer State
const [elapsedTime, setElapsedTime] = useState<string>('00:00:00');
// Form State
const [weight, setWeight] = useState<string>('');
const [reps, setReps] = useState<string>('');
const [duration, setDuration] = useState<string>('');
const [distance, setDistance] = useState<string>('');
const [height, setHeight] = useState<string>('');
const [bwPercentage, setBwPercentage] = useState<string>('100');
// User Weight State
const [userBodyWeight, setUserBodyWeight] = useState<string>(userWeight ? userWeight.toString() : '70');
// Create Exercise State
const [isCreating, setIsCreating] = useState(false);
// Plan Execution State
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [showPlanPrep, setShowPlanPrep] = useState<WorkoutPlan | null>(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<string | null>(null);
const [editWeight, setEditWeight] = useState<string>('');
const [editReps, setEditReps] = useState<string>('');
const [editDuration, setEditDuration] = useState<string>('');
const [editDistance, setEditDistance] = useState<string>('');
const [editHeight, setEditHeight] = useState<string>('');
useEffect(() => {
const loadData = async () => {
const exList = await getExercises(userId);
exList.sort((a, b) => a.name.localeCompare(b.name));
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<string, number>();
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<string, number>();
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('');
}
} else {
setSearchQuery(''); // Clear search query if no exercise is selected
}
};
updateSelection();
}, [selectedExercise, userId]);
const filteredExercises = searchQuery === ''
? exercises
: exercises.filter(ex =>
ex.name.toLowerCase().includes(searchQuery.toLowerCase())
);
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<WorkoutSet> = {
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);
setExercises(prev => [...prev, newEx].sort((a, b) => a.name.localeCompare(b.name)));
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);
};
// We need activePlan from the hook or props. The hook returns 'plans' but not 'activePlan'.
// Actually useTracker takes activePlan as prop but doesn't return it.
// We should probably pass activePlan as a prop to this component directly from the parent.
// Let's assume the parent passes it or we modify the hook.
// For now, let's use the activePlan passed to the hook if possible, but the hook doesn't expose it.
// I will modify the hook to return activePlan or just accept it as prop here.
// The hook accepts activePlan as argument, so I can return it.
// Let's modify useTracker to return activePlan in the next step if needed, or just pass it here.
// Wait, I can't modify useTracker easily now without rewriting it.
// I'll pass activePlan as a prop to ActiveSessionView.
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
const exerciseTypeLabels: Record<ExerciseType, string> = {
[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 (
<div className="flex flex-col h-full p-4 md:p-8 overflow-y-auto relative">
<div className="flex-1 flex flex-col items-center justify-center space-y-12">
<div className="flex flex-col items-center gap-4">
<div className="w-24 h-24 rounded-full bg-surface-container-high flex items-center justify-center text-primary shadow-elevation-1">
<Dumbbell size={40} />
</div>
<div className="text-center">
<h1 className="text-3xl font-normal text-on-surface">{t('ready_title', lang)}</h1>
<p className="text-on-surface-variant text-sm">{t('ready_subtitle', lang)}</p>
</div>
</div>
<div className="w-full max-w-sm bg-surface-container rounded-2xl p-6 flex flex-col items-center gap-4 shadow-elevation-1">
<label className="text-xs text-on-surface-variant font-bold tracking-wide flex items-center gap-2">
<User size={14} />
{t('my_weight', lang)}
</label>
<div className="flex items-center gap-4">
<input
type="number"
step="0.1"
className="text-5xl font-normal text-on-surface tabular-nums bg-transparent text-center w-full focus:outline-none"
value={userBodyWeight}
onChange={(e) => setUserBodyWeight(e.target.value)}
/>
</div>
<p className="text-[10px] text-on-surface-variant">{t('change_in_profile', lang)}</p>
</div>
<div className="w-full max-w-xs">
<button
onClick={() => handleStart()}
className="w-full h-16 rounded-full bg-primary text-on-primary font-medium text-lg shadow-elevation-2 hover:shadow-elevation-3 active:shadow-elevation-1 transition-all flex items-center justify-center gap-2"
>
<PlayCircle size={24} />
{t('free_workout', lang)}
</button>
</div>
{plans.length > 0 && (
<div className="w-full max-w-md mt-8">
<h3 className="text-sm text-on-surface-variant font-medium px-4 mb-3">{t('or_choose_plan', lang)}</h3>
<div className="grid grid-cols-1 gap-3">
{plans.map(plan => (
<button
key={plan.id}
onClick={() => handleStart(plan)}
className="flex items-center justify-between p-4 bg-surface-container rounded-xl hover:bg-surface-container-high transition-colors border border-outline-variant/20"
>
<div className="text-left">
<div className="text-base font-medium text-on-surface">{plan.name}</div>
<div className="text-xs text-on-surface-variant">{plan.steps.length} {t('exercises_count', lang)}</div>
</div>
<div className="w-10 h-10 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center">
<ArrowRight size={20} />
</div>
</button>
))}
</div>
</div>
)}
</div>
{showPlanPrep && (
<div className="absolute inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm">
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-2xl font-normal text-on-surface mb-4">{showPlanPrep.name}</h3>
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm mb-8">
<div className="text-xs font-bold text-primary mb-2">{t('prep_title', lang)}</div>
{showPlanPrep.description || t('prep_no_instructions', lang)}
</div>
<div className="flex justify-end gap-2">
<button onClick={() => setShowPlanPrep(null)} className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
<button onClick={confirmPlanStart} className="px-6 py-2.5 rounded-full bg-primary text-on-primary font-medium">{t('start', lang)}</button>
</div>
</div>
</div>
)}
</div>
);
}
return (
<div className="flex flex-col h-full max-h-full overflow-hidden relative bg-surface">
<div className="px-4 py-3 bg-surface-container shadow-elevation-1 z-20 flex justify-between items-center">
@@ -600,7 +283,7 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
<div className="pt-4">
<h3 className="text-sm text-primary font-medium px-2 mb-3 tracking-wide">{t('history_section', lang)}</h3>
<div className="flex flex-col gap-2">
{[...activeSession.sets].reverse().map((set, idx) => {
{[...activeSession.sets].reverse().map((set: WorkoutSet, idx: number) => {
const setNumber = activeSession.sets.length - idx;
const isEditing = editingSetId === set.id;
return (
@@ -800,4 +483,4 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
);
};
export default Tracker;
export default ActiveSessionView;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { Dumbbell, User, PlayCircle, Plus, ArrowRight } from 'lucide-react';
import { Language } from '../../types';
import { t } from '../../services/i18n';
import { useTracker } from './useTracker';
interface IdleViewProps {
tracker: ReturnType<typeof useTracker>;
lang: Language;
}
const IdleView: React.FC<IdleViewProps> = ({ tracker, lang }) => {
const {
userBodyWeight,
setUserBodyWeight,
handleStart,
setIsSporadicMode,
plans,
showPlanPrep,
setShowPlanPrep,
confirmPlanStart
} = tracker;
return (
<div className="flex flex-col h-full p-4 md:p-8 overflow-y-auto relative">
<div className="flex-1 flex flex-col items-center justify-center space-y-12">
<div className="flex flex-col items-center gap-4">
<div className="w-24 h-24 rounded-full bg-surface-container-high flex items-center justify-center text-primary shadow-elevation-1">
<Dumbbell size={40} />
</div>
<div className="text-center">
<h1 className="text-3xl font-normal text-on-surface">{t('ready_title', lang)}</h1>
<p className="text-on-surface-variant text-sm">{t('ready_subtitle', lang)}</p>
</div>
</div>
<div className="w-full max-w-sm bg-surface-container rounded-2xl p-6 flex flex-col items-center gap-4 shadow-elevation-1">
<label className="text-xs text-on-surface-variant font-bold tracking-wide flex items-center gap-2">
<User size={14} />
{t('my_weight', lang)}
</label>
<div className="flex items-center gap-4">
<input
type="number"
step="0.1"
className="text-5xl font-normal text-on-surface tabular-nums bg-transparent text-center w-full focus:outline-none"
value={userBodyWeight}
onChange={(e) => setUserBodyWeight(e.target.value)}
/>
</div>
<p className="text-[10px] text-on-surface-variant">{t('change_in_profile', lang)}</p>
</div>
<div className="w-full max-w-xs space-y-3">
<button
onClick={() => handleStart()}
className="w-full h-16 rounded-full bg-primary text-on-primary font-medium text-lg shadow-elevation-2 hover:shadow-elevation-3 active:shadow-elevation-1 transition-all flex items-center justify-center gap-2"
>
<PlayCircle size={24} />
{t('free_workout', lang)}
</button>
<button
onClick={() => setIsSporadicMode(true)}
className="w-full h-12 rounded-full bg-surface-container-high text-on-surface font-medium text-base hover:bg-surface-container-highest transition-all flex items-center justify-center gap-2"
>
<Plus size={20} />
{t('quick_log', lang)}
</button>
</div>
{plans.length > 0 && (
<div className="w-full max-w-md mt-8">
<h3 className="text-sm text-on-surface-variant font-medium px-4 mb-3">{t('or_choose_plan', lang)}</h3>
<div className="grid grid-cols-1 gap-3">
{plans.map(plan => (
<button
key={plan.id}
onClick={() => handleStart(plan)}
className="flex items-center justify-between p-4 bg-surface-container rounded-xl hover:bg-surface-container-high transition-colors border border-outline-variant/20"
>
<div className="text-left">
<div className="text-base font-medium text-on-surface">{plan.name}</div>
<div className="text-xs text-on-surface-variant">{plan.steps.length} {t('exercises_count', lang)}</div>
</div>
<div className="w-10 h-10 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center">
<ArrowRight size={20} />
</div>
</button>
))}
</div>
</div>
)}
</div>
{showPlanPrep && (
<div className="absolute inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm">
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-2xl font-normal text-on-surface mb-4">{showPlanPrep.name}</h3>
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm mb-8">
<div className="text-xs font-bold text-primary mb-2">{t('prep_title', lang)}</div>
{showPlanPrep.description || t('prep_no_instructions', lang)}
</div>
<div className="flex justify-end gap-2">
<button onClick={() => setShowPlanPrep(null)} className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
<button onClick={confirmPlanStart} className="px-6 py-2.5 rounded-full bg-primary text-on-primary font-medium">{t('start', lang)}</button>
</div>
</div>
</div>
)}
</div>
);
};
export default IdleView;

View File

@@ -0,0 +1,157 @@
import React from 'react';
import { Dumbbell, Scale, Activity, Timer as TimerIcon, ArrowRight, ArrowUp, Plus, CheckCircle } from 'lucide-react';
import { ExerciseType, Language } from '../../types';
import { t } from '../../services/i18n';
import FilledInput from '../FilledInput';
import { useTracker } from './useTracker';
interface SporadicViewProps {
tracker: ReturnType<typeof useTracker>;
lang: Language;
}
const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
const {
searchQuery,
setSearchQuery,
setShowSuggestions,
showSuggestions,
filteredExercises,
setSelectedExercise,
selectedExercise,
weight,
setWeight,
reps,
setReps,
duration,
setDuration,
distance,
setDistance,
height,
setHeight,
handleLogSporadicSet,
sporadicSuccess,
setIsSporadicMode
} = tracker;
return (
<div className="flex flex-col h-full max-h-full overflow-hidden relative bg-surface">
<div className="px-4 py-3 bg-surface-container shadow-elevation-1 z-20 flex justify-between items-center">
<div className="flex flex-col">
<h2 className="text-title-medium text-on-surface flex items-center gap-2 font-medium">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse" />
{t('quick_log', lang)}
</h2>
</div>
<button
onClick={() => setIsSporadicMode(false)}
className="px-5 py-2 rounded-full bg-primary-container text-on-primary-container text-sm font-medium hover:opacity-90 transition-opacity"
>
{t('done', lang)}
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 pb-32 space-y-6">
{/* Exercise Selection */}
<div className="relative">
<FilledInput
label={t('select_exercise', lang)}
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setShowSuggestions(true);
}}
onFocus={() => setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 100)}
icon={<Dumbbell size={10} />}
autoComplete="off"
type="text"
/>
{showSuggestions && (
<div className="absolute top-full left-0 w-full bg-surface-container rounded-xl shadow-elevation-3 overflow-hidden z-20 mt-1 max-h-60 overflow-y-auto animate-in fade-in slide-in-from-top-2">
{filteredExercises.length > 0 ? (
filteredExercises.map(ex => (
<button
key={ex.id}
onClick={() => {
setSelectedExercise(ex);
setSearchQuery(ex.name);
setShowSuggestions(false);
}}
className="w-full text-left px-4 py-3 text-on-surface hover:bg-surface-container-high transition-colors text-lg"
>
{ex.name}
</button>
))
) : (
<div className="px-4 py-3 text-on-surface-variant text-lg">{t('no_exercises_found', lang)}</div>
)}
</div>
)}
</div>
{selectedExercise && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-300 space-y-6">
<div className="grid grid-cols-2 gap-4">
{(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={selectedExercise.type === ExerciseType.BODYWEIGHT ? t('add_weight', lang) : t('weight_kg', lang)}
value={weight}
step="0.1"
onChange={(e: any) => setWeight(e.target.value)}
icon={<Scale size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && (
<FilledInput
label={t('reps', lang)}
value={reps}
onChange={(e: any) => setReps(e.target.value)}
icon={<Activity size={10} />}
type="number"
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={t('time_sec', lang)}
value={duration}
onChange={(e: any) => setDuration(e.target.value)}
icon={<TimerIcon size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && (
<FilledInput
label={t('dist_m', lang)}
value={distance}
onChange={(e: any) => setDistance(e.target.value)}
icon={<ArrowRight size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.HIGH_JUMP) && (
<FilledInput
label={t('height_cm', lang)}
value={height}
onChange={(e: any) => setHeight(e.target.value)}
icon={<ArrowUp size={10} />}
/>
)}
</div>
<button
onClick={handleLogSporadicSet}
className={`w-full h-14 font-medium text-lg rounded-full shadow-elevation-2 hover:shadow-elevation-3 active:scale-[0.98] transition-all flex items-center justify-center gap-2 ${sporadicSuccess
? 'bg-green-500 text-white'
: 'bg-primary-container text-on-primary-container'
}`}
>
{sporadicSuccess ? <CheckCircle size={24} /> : <Plus size={24} />}
<span>{sporadicSuccess ? t('saved', lang) : t('log_set', lang)}</span>
</button>
</div>
)}
</div>
</div>
);
};
export default SporadicView;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { WorkoutSession, WorkoutSet, WorkoutPlan, Language } from '../../types';
import { useTracker } from './useTracker';
import IdleView from './IdleView';
import SporadicView from './SporadicView';
import ActiveSessionView from './ActiveSessionView';
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;
onSporadicSetAdded?: () => void;
lang: Language;
}
const Tracker: React.FC<TrackerProps> = (props) => {
const tracker = useTracker(props);
const { isSporadicMode } = tracker;
const { activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet } = props;
if (activeSession) {
return (
<ActiveSessionView
tracker={tracker}
activeSession={activeSession}
lang={lang}
onSessionEnd={onSessionEnd}
onSessionQuit={onSessionQuit}
onRemoveSet={onRemoveSet}
/>
);
}
if (isSporadicMode) {
return <SporadicView tracker={tracker} lang={lang} />;
}
return <IdleView tracker={tracker} lang={lang} />;
};
export default Tracker;

View File

@@ -0,0 +1,430 @@
import { useState, useEffect } from 'react';
import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, WorkoutPlan, Language } from '../../types';
import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../../services/storage';
import { api } from '../../services/api';
import { logSporadicSet } from '../../services/sporadicSets';
interface UseTrackerProps {
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;
onSporadicSetAdded?: () => void;
}
export const useTracker = ({
userId,
userWeight,
activeSession,
activePlan,
onSessionStart,
onSessionEnd,
onSessionQuit,
onSetAdded,
onRemoveSet,
onUpdateSet,
onSporadicSetAdded
}: UseTrackerProps) => {
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
const [selectedExercise, setSelectedExercise] = useState<ExerciseDef | null>(null);
const [lastSet, setLastSet] = useState<WorkoutSet | undefined>(undefined);
const [searchQuery, setSearchQuery] = useState<string>('');
const [showSuggestions, setShowSuggestions] = useState(false);
// Timer State
const [elapsedTime, setElapsedTime] = useState<string>('00:00:00');
// Form State
const [weight, setWeight] = useState<string>('');
const [reps, setReps] = useState<string>('');
const [duration, setDuration] = useState<string>('');
const [distance, setDistance] = useState<string>('');
const [height, setHeight] = useState<string>('');
const [bwPercentage, setBwPercentage] = useState<string>('100');
// User Weight State
const [userBodyWeight, setUserBodyWeight] = useState<string>(userWeight ? userWeight.toString() : '70');
// Create Exercise State
const [isCreating, setIsCreating] = useState(false);
// Plan Execution State
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [showPlanPrep, setShowPlanPrep] = useState<WorkoutPlan | null>(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<string | null>(null);
const [editWeight, setEditWeight] = useState<string>('');
const [editReps, setEditReps] = useState<string>('');
const [editDuration, setEditDuration] = useState<string>('');
const [editDistance, setEditDistance] = useState<string>('');
const [editHeight, setEditHeight] = useState<string>('');
// Sporadic Set State
const [isSporadicMode, setIsSporadicMode] = useState(false);
const [sporadicSuccess, setSporadicSuccess] = useState(false);
useEffect(() => {
const loadData = async () => {
const exList = await getExercises(userId);
exList.sort((a, b) => a.name.localeCompare(b.name));
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<string, number>();
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<string, number>();
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('');
}
} else {
setSearchQuery(''); // Clear search query if no exercise is selected
}
};
updateSelection();
}, [selectedExercise, userId]);
const filteredExercises = searchQuery === ''
? exercises
: exercises.filter(ex =>
ex.name.toLowerCase().includes(searchQuery.toLowerCase())
);
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<WorkoutSet> = {
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);
}
};
const handleLogSporadicSet = async () => {
if (!selectedExercise) return;
const set: any = {
exerciseId: selectedExercise.id,
timestamp: Date.now(),
};
switch (selectedExercise.type) {
case ExerciseType.STRENGTH:
if (weight) set.weight = parseFloat(weight);
if (reps) set.reps = parseInt(reps);
break;
case ExerciseType.BODYWEIGHT:
if (weight) set.weight = parseFloat(weight);
if (reps) set.reps = parseInt(reps);
set.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.CARDIO:
if (duration) set.durationSeconds = parseInt(duration);
if (distance) set.distanceMeters = parseFloat(distance);
break;
case ExerciseType.STATIC:
if (duration) set.durationSeconds = parseInt(duration);
set.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.HIGH_JUMP:
if (height) set.height = parseFloat(height);
break;
case ExerciseType.LONG_JUMP:
if (distance) set.distanceMeters = parseFloat(distance);
break;
case ExerciseType.PLYOMETRIC:
if (reps) set.reps = parseInt(reps);
break;
}
try {
const result = await logSporadicSet(userId, set);
if (result) {
setSporadicSuccess(true);
setTimeout(() => setSporadicSuccess(false), 2000);
// Reset form
setWeight('');
setReps('');
setDuration('');
setDistance('');
setHeight('');
if (onSporadicSetAdded) onSporadicSetAdded();
}
} catch (error) {
console.error("Failed to log sporadic set:", error);
}
};
const handleCreateExercise = async (newEx: ExerciseDef) => {
await saveExercise(userId, newEx);
setExercises(prev => [...prev, newEx].sort((a, b) => a.name.localeCompare(b.name)));
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);
};
return {
exercises,
plans,
selectedExercise,
setSelectedExercise,
lastSet,
searchQuery,
setSearchQuery,
showSuggestions,
setShowSuggestions,
elapsedTime,
weight,
setWeight,
reps,
setReps,
duration,
setDuration,
distance,
setDistance,
height,
setHeight,
bwPercentage,
setBwPercentage,
userBodyWeight,
setUserBodyWeight,
isCreating,
setIsCreating,
currentStepIndex,
showPlanPrep,
setShowPlanPrep,
showPlanList,
setShowPlanList,
showFinishConfirm,
setShowFinishConfirm,
showQuitConfirm,
setShowQuitConfirm,
showMenu,
setShowMenu,
editingSetId,
editWeight,
setEditWeight,
editReps,
setEditReps,
editDuration,
setEditDuration,
editDistance,
setEditDistance,
editHeight,
setEditHeight,
isSporadicMode,
setIsSporadicMode,
sporadicSuccess,
filteredExercises,
handleStart,
confirmPlanStart,
handleAddSet,
handleLogSporadicSet,
handleCreateExercise,
handleEditSet,
handleSaveEdit,
handleCancelEdit,
jumpToStep,
};
};