Unilateral exercises logging

This commit is contained in:
AG
2025-12-03 23:30:32 +02:00
parent 50f3d4d49b
commit a632de65ea
24 changed files with 1656 additions and 244 deletions

View File

@@ -227,51 +227,178 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
{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} />}
autoFocus={activePlan && !isPlanFinished && activePlan.steps[currentStepIndex]?.isWeighted && (selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STRENGTH)}
{/* Unilateral Exercise Toggle */}
{selectedExercise.isUnilateral && (
<div className="flex items-center gap-3 px-2 py-3 bg-surface-container rounded-xl">
<input
type="checkbox"
id="sameValuesBothSides"
checked={tracker.sameValuesBothSides}
onChange={(e) => tracker.handleToggleSameValues(e.target.checked)}
className="w-5 h-5 rounded border-2 border-outline bg-surface-container-high checked:bg-primary checked:border-primary cursor-pointer"
/>
)}
{(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>
<label htmlFor="sameValuesBothSides" className="text-sm text-on-surface cursor-pointer flex-1">
{t('same_values_both_sides', lang)}
</label>
</div>
)}
{/* Input Forms */}
{selectedExercise.isUnilateral && !tracker.sameValuesBothSides ? (
/* Separate Left/Right Inputs */
<div className="space-y-4">
{/* Left Side */}
<div className="space-y-2">
<div className="text-sm font-medium text-primary flex items-center gap-2 px-2">
<span className="w-6 h-6 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xs font-bold">L</span>
{t('left', lang)}
</div>
<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={tracker.weightLeft}
step="0.1"
onChange={(e: any) => tracker.setWeightLeft(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={tracker.repsLeft}
onChange={(e: any) => tracker.setRepsLeft(e.target.value)}
icon={<Activity size={10} />}
type="number"
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={t('time_sec', lang)}
value={tracker.durationLeft}
onChange={(e: any) => tracker.setDurationLeft(e.target.value)}
icon={<TimerIcon size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && (
<FilledInput
label={t('dist_m', lang)}
value={tracker.distanceLeft}
onChange={(e: any) => tracker.setDistanceLeft(e.target.value)}
icon={<ArrowRight size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.HIGH_JUMP) && (
<FilledInput
label={t('height_cm', lang)}
value={tracker.heightLeft}
onChange={(e: any) => tracker.setHeightLeft(e.target.value)}
icon={<ArrowUp size={10} />}
/>
)}
</div>
</div>
{/* Right Side */}
<div className="space-y-2">
<div className="text-sm font-medium text-secondary flex items-center gap-2 px-2">
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center text-xs font-bold">R</span>
{t('right', lang)}
</div>
<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={tracker.weightRight}
step="0.1"
onChange={(e: any) => tracker.setWeightRight(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={tracker.repsRight}
onChange={(e: any) => tracker.setRepsRight(e.target.value)}
icon={<Activity size={10} />}
type="number"
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={t('time_sec', lang)}
value={tracker.durationRight}
onChange={(e: any) => tracker.setDurationRight(e.target.value)}
icon={<TimerIcon size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && (
<FilledInput
label={t('dist_m', lang)}
value={tracker.distanceRight}
onChange={(e: any) => tracker.setDistanceRight(e.target.value)}
icon={<ArrowRight size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.HIGH_JUMP) && (
<FilledInput
label={t('height_cm', lang)}
value={tracker.heightRight}
onChange={(e: any) => tracker.setHeightRight(e.target.value)}
icon={<ArrowUp size={10} />}
/>
)}
</div>
</div>
</div>
) : (
/* Single Input Form (for bilateral or unilateral with same values) */
<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} />}
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) && (
<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={handleAddSet}

View File

@@ -143,50 +143,177 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang, sporadicSets
{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} />}
{/* Unilateral Exercise Toggle */}
{selectedExercise.isUnilateral && (
<div className="flex items-center gap-3 px-2 py-3 bg-surface-container rounded-xl">
<input
type="checkbox"
id="sameValuesBothSidesSporadic"
checked={tracker.sameValuesBothSides}
onChange={(e) => tracker.handleToggleSameValues(e.target.checked)}
className="w-5 h-5 rounded border-2 border-outline bg-surface-container-high checked:bg-primary checked:border-primary cursor-pointer"
/>
)}
{(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>
<label htmlFor="sameValuesBothSidesSporadic" className="text-sm text-on-surface cursor-pointer flex-1">
{t('same_values_both_sides', lang)}
</label>
</div>
)}
{/* Input Forms */}
{selectedExercise.isUnilateral && !tracker.sameValuesBothSides ? (
/* Separate Left/Right Inputs */
<div className="space-y-4">
{/* Left Side */}
<div className="space-y-2">
<div className="text-sm font-medium text-primary flex items-center gap-2 px-2">
<span className="w-6 h-6 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xs font-bold">L</span>
{t('left', lang)}
</div>
<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={tracker.weightLeft}
step="0.1"
onChange={(e: any) => tracker.setWeightLeft(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={tracker.repsLeft}
onChange={(e: any) => tracker.setRepsLeft(e.target.value)}
icon={<Activity size={10} />}
type="number"
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={t('time_sec', lang)}
value={tracker.durationLeft}
onChange={(e: any) => tracker.setDurationLeft(e.target.value)}
icon={<TimerIcon size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && (
<FilledInput
label={t('dist_m', lang)}
value={tracker.distanceLeft}
onChange={(e: any) => tracker.setDistanceLeft(e.target.value)}
icon={<ArrowRight size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.HIGH_JUMP) && (
<FilledInput
label={t('height_cm', lang)}
value={tracker.heightLeft}
onChange={(e: any) => tracker.setHeightLeft(e.target.value)}
icon={<ArrowUp size={10} />}
/>
)}
</div>
</div>
{/* Right Side */}
<div className="space-y-2">
<div className="text-sm font-medium text-secondary flex items-center gap-2 px-2">
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center text-xs font-bold">R</span>
{t('right', lang)}
</div>
<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={tracker.weightRight}
step="0.1"
onChange={(e: any) => tracker.setWeightRight(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={tracker.repsRight}
onChange={(e: any) => tracker.setRepsRight(e.target.value)}
icon={<Activity size={10} />}
type="number"
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && (
<FilledInput
label={t('time_sec', lang)}
value={tracker.durationRight}
onChange={(e: any) => tracker.setDurationRight(e.target.value)}
icon={<TimerIcon size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && (
<FilledInput
label={t('dist_m', lang)}
value={tracker.distanceRight}
onChange={(e: any) => tracker.setDistanceRight(e.target.value)}
icon={<ArrowRight size={10} />}
/>
)}
{(selectedExercise.type === ExerciseType.HIGH_JUMP) && (
<FilledInput
label={t('height_cm', lang)}
value={tracker.heightRight}
onChange={(e: any) => tracker.setHeightRight(e.target.value)}
icon={<ArrowUp size={10} />}
/>
)}
</div>
</div>
</div>
) : (
/* Single Input Form (for bilateral or unilateral with same values) */
<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}

View File

@@ -77,6 +77,36 @@ export const useTracker = ({
const [isSporadicMode, setIsSporadicMode] = useState(false);
const [sporadicSuccess, setSporadicSuccess] = useState(false);
// Unilateral Exercise State
const [sameValuesBothSides, setSameValuesBothSides] = useState(true);
const [weightLeft, setWeightLeft] = useState<string>('');
const [weightRight, setWeightRight] = useState<string>('');
const [repsLeft, setRepsLeft] = useState<string>('');
const [repsRight, setRepsRight] = useState<string>('');
const [durationLeft, setDurationLeft] = useState<string>('');
const [durationRight, setDurationRight] = useState<string>('');
const [distanceLeft, setDistanceLeft] = useState<string>('');
const [distanceRight, setDistanceRight] = useState<string>('');
const [heightLeft, setHeightLeft] = useState<string>('');
const [heightRight, setHeightRight] = useState<string>('');
const handleToggleSameValues = (checked: boolean) => {
setSameValuesBothSides(checked);
if (!checked) {
// Propagate values from single fields to left/right fields
setWeightLeft(weight);
setWeightRight(weight);
setRepsLeft(reps);
setRepsRight(reps);
setDurationLeft(duration);
setDurationRight(duration);
setDistanceLeft(distance);
setDistanceRight(distance);
setHeightLeft(height);
setHeightRight(height);
}
};
useEffect(() => {
const loadData = async () => {
const exList = await getExercises(userId);
@@ -216,100 +246,289 @@ export const useTracker = ({
const handleAddSet = async () => {
if (!activeSession || !selectedExercise) return;
const setData: Partial<WorkoutSet> = {
exerciseId: selectedExercise.id,
};
// For unilateral exercises, create two sets (LEFT and RIGHT)
if (selectedExercise.isUnilateral) {
const setsToCreate: Array<Partial<WorkoutSet> & { side: 'LEFT' | 'RIGHT' }> = [];
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;
}
if (sameValuesBothSides) {
// Create two identical sets with LEFT and RIGHT sides
const setData: Partial<WorkoutSet> = {
exerciseId: selectedExercise.id,
};
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);
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;
}
setsToCreate.push({ ...setData, side: 'LEFT' });
setsToCreate.push({ ...setData, side: 'RIGHT' });
} else {
// Create separate sets for LEFT and RIGHT with different values
const leftSetData: Partial<WorkoutSet> = {
exerciseId: selectedExercise.id,
};
const rightSetData: Partial<WorkoutSet> = {
exerciseId: selectedExercise.id,
};
switch (selectedExercise.type) {
case ExerciseType.STRENGTH:
if (weightLeft) leftSetData.weight = parseFloat(weightLeft);
if (repsLeft) leftSetData.reps = parseInt(repsLeft);
if (weightRight) rightSetData.weight = parseFloat(weightRight);
if (repsRight) rightSetData.reps = parseInt(repsRight);
break;
case ExerciseType.BODYWEIGHT:
if (weightLeft) leftSetData.weight = parseFloat(weightLeft);
if (repsLeft) leftSetData.reps = parseInt(repsLeft);
leftSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
if (weightRight) rightSetData.weight = parseFloat(weightRight);
if (repsRight) rightSetData.reps = parseInt(repsRight);
rightSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.CARDIO:
if (durationLeft) leftSetData.durationSeconds = parseInt(durationLeft);
if (distanceLeft) leftSetData.distanceMeters = parseFloat(distanceLeft);
if (durationRight) rightSetData.durationSeconds = parseInt(durationRight);
if (distanceRight) rightSetData.distanceMeters = parseFloat(distanceRight);
break;
case ExerciseType.STATIC:
if (durationLeft) leftSetData.durationSeconds = parseInt(durationLeft);
leftSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
if (durationRight) rightSetData.durationSeconds = parseInt(durationRight);
rightSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.HIGH_JUMP:
if (heightLeft) leftSetData.height = parseFloat(heightLeft);
if (heightRight) rightSetData.height = parseFloat(heightRight);
break;
case ExerciseType.LONG_JUMP:
if (distanceLeft) leftSetData.distanceMeters = parseFloat(distanceLeft);
if (distanceRight) rightSetData.distanceMeters = parseFloat(distanceRight);
break;
case ExerciseType.PLYOMETRIC:
if (repsLeft) leftSetData.reps = parseInt(repsLeft);
if (repsRight) rightSetData.reps = parseInt(repsRight);
break;
}
setsToCreate.push({ ...leftSetData, side: 'LEFT' });
setsToCreate.push({ ...rightSetData, side: 'RIGHT' });
}
// Log both sets
try {
for (const setData of setsToCreate) {
const response = await api.post('/sessions/active/log-set', setData);
if (response.success) {
const { newSet } = response;
onSetAdded(newSet);
}
}
// Update plan progress after logging both sets
if (activePlan) {
const response = await api.post('/sessions/active/log-set', { exerciseId: selectedExercise.id });
if (response.success && response.activeExerciseId) {
const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === response.activeExerciseId);
if (nextStepIndex !== -1) {
setCurrentStepIndex(nextStepIndex);
}
} else if (response.success && !response.activeExerciseId) {
setCurrentStepIndex(activePlan.steps.length);
}
}
} catch (error) {
console.error("Failed to log unilateral sets:", error);
}
} else {
// Regular bilateral exercise - single set
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);
}
} catch (error) {
console.error("Failed to log set:", error);
}
};
const handleLogSporadicSet = async () => {
if (!selectedExercise) return;
const set: any = {
exerciseId: selectedExercise.id,
timestamp: Date.now(),
};
// For unilateral exercises, create two sets (LEFT and RIGHT)
if (selectedExercise.isUnilateral) {
const setsToCreate: any[] = [];
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;
}
if (sameValuesBothSides) {
// Create two identical sets with LEFT and RIGHT sides
const set: any = {
exerciseId: selectedExercise.id,
timestamp: Date.now(),
};
try {
const result = await logSporadicSet(set);
if (result) {
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;
}
setsToCreate.push({ ...set, side: 'LEFT' });
setsToCreate.push({ ...set, side: 'RIGHT' });
} else {
// Create separate sets for LEFT and RIGHT with different values
const leftSet: any = {
exerciseId: selectedExercise.id,
timestamp: Date.now(),
};
const rightSet: any = {
exerciseId: selectedExercise.id,
timestamp: Date.now(),
};
switch (selectedExercise.type) {
case ExerciseType.STRENGTH:
if (weightLeft) leftSet.weight = parseFloat(weightLeft);
if (repsLeft) leftSet.reps = parseInt(repsLeft);
if (weightRight) rightSet.weight = parseFloat(weightRight);
if (repsRight) rightSet.reps = parseInt(repsRight);
break;
case ExerciseType.BODYWEIGHT:
if (weightLeft) leftSet.weight = parseFloat(weightLeft);
if (repsLeft) leftSet.reps = parseInt(repsLeft);
leftSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
if (weightRight) rightSet.weight = parseFloat(weightRight);
if (repsRight) rightSet.reps = parseInt(repsRight);
rightSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.CARDIO:
if (durationLeft) leftSet.durationSeconds = parseInt(durationLeft);
if (distanceLeft) leftSet.distanceMeters = parseFloat(distanceLeft);
if (durationRight) rightSet.durationSeconds = parseInt(durationRight);
if (distanceRight) rightSet.distanceMeters = parseFloat(distanceRight);
break;
case ExerciseType.STATIC:
if (durationLeft) leftSet.durationSeconds = parseInt(durationLeft);
leftSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
if (durationRight) rightSet.durationSeconds = parseInt(durationRight);
rightSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
break;
case ExerciseType.HIGH_JUMP:
if (heightLeft) leftSet.height = parseFloat(heightLeft);
if (heightRight) rightSet.height = parseFloat(heightRight);
break;
case ExerciseType.LONG_JUMP:
if (distanceLeft) leftSet.distanceMeters = parseFloat(distanceLeft);
if (distanceRight) rightSet.distanceMeters = parseFloat(distanceRight);
break;
case ExerciseType.PLYOMETRIC:
if (repsLeft) leftSet.reps = parseInt(repsLeft);
if (repsRight) rightSet.reps = parseInt(repsRight);
break;
}
setsToCreate.push({ ...leftSet, side: 'LEFT' });
setsToCreate.push({ ...rightSet, side: 'RIGHT' });
}
// Log both sets
try {
for (const set of setsToCreate) {
await logSporadicSet(set);
}
setSporadicSuccess(true);
setTimeout(() => setSporadicSuccess(false), 2000);
// Reset form
@@ -318,10 +537,72 @@ export const useTracker = ({
setDuration('');
setDistance('');
setHeight('');
setWeightLeft('');
setWeightRight('');
setRepsLeft('');
setRepsRight('');
setDurationLeft('');
setDurationRight('');
setDistanceLeft('');
setDistanceRight('');
setHeightLeft('');
setHeightRight('');
if (onSporadicSetAdded) onSporadicSetAdded();
} catch (error) {
console.error("Failed to log unilateral sporadic sets:", error);
}
} else {
// Regular bilateral exercise - single set
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(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);
}
} catch (error) {
console.error("Failed to log sporadic set:", error);
}
};
@@ -440,5 +721,29 @@ export const useTracker = ({
handleCancelEdit,
jumpToStep,
resetForm,
// Unilateral exercise state
sameValuesBothSides,
setSameValuesBothSides,
weightLeft,
setWeightLeft,
weightRight,
setWeightRight,
repsLeft,
setRepsLeft,
repsRight,
setRepsRight,
durationLeft,
setDurationLeft,
durationRight,
setDurationRight,
distanceLeft,
setDistanceLeft,
distanceRight,
setDistanceRight,
heightLeft,
setHeightLeft,
heightRight,
setHeightRight,
handleToggleSameValues,
};
};