590 lines
33 KiB
TypeScript
590 lines
33 KiB
TypeScript
|
||
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 } 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';
|
||
|
||
interface TrackerProps {
|
||
userId: string;
|
||
userWeight?: number;
|
||
activeSession: WorkoutSession | null;
|
||
activePlan: WorkoutPlan | null;
|
||
onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void;
|
||
onSessionEnd: () => void;
|
||
onSetAdded: (set: WorkoutSet) => void;
|
||
onRemoveSet: (setId: string) => void;
|
||
lang: Language;
|
||
}
|
||
|
||
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSetAdded, onRemoveSet, 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);
|
||
|
||
// 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);
|
||
const [newName, setNewName] = useState('');
|
||
const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
||
const [newBwPercentage, setNewBwPercentage] = useState<string>('100');
|
||
|
||
// Plan Execution State
|
||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||
const [showPlanPrep, setShowPlanPrep] = useState<WorkoutPlan | null>(null);
|
||
const [showPlanList, setShowPlanList] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const loadData = async () => {
|
||
const exList = await getExercises(userId);
|
||
setExercises(exList.filter(e => !e.isArchived));
|
||
const planList = await getPlans(userId);
|
||
setPlans(planList);
|
||
|
||
if (activeSession?.userBodyWeight) {
|
||
setUserBodyWeight(activeSession.userBodyWeight.toString());
|
||
} else if (userWeight) {
|
||
setUserBodyWeight(userWeight.toString());
|
||
}
|
||
};
|
||
loadData();
|
||
}, [activeSession, userId, userWeight]);
|
||
|
||
// 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]);
|
||
|
||
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) {
|
||
if (!selectedExercise || selectedExercise.id !== exDef.id) {
|
||
setSelectedExercise(exDef);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, [activeSession, activePlan, currentStepIndex, 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) {
|
||
if (set.weight !== undefined) setWeight(set.weight.toString());
|
||
if (set.reps !== undefined) setReps(set.reps.toString());
|
||
if (set.durationSeconds !== undefined) setDuration(set.durationSeconds.toString());
|
||
if (set.distanceMeters !== undefined) setDistance(set.distanceMeters.toString());
|
||
if (set.height !== undefined) setHeight(set.height.toString());
|
||
} else {
|
||
setWeight(''); setReps(''); setDuration(''); setDistance(''); 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);
|
||
setShowPlanPrep(null);
|
||
}
|
||
}
|
||
|
||
const handleAddSet = () => {
|
||
if (!activeSession || !selectedExercise) return;
|
||
|
||
const newSet: WorkoutSet = {
|
||
id: crypto.randomUUID(),
|
||
exerciseId: selectedExercise.id,
|
||
exerciseName: selectedExercise.name,
|
||
type: selectedExercise.type,
|
||
timestamp: Date.now(),
|
||
...(weight && { weight: parseFloat(weight) }),
|
||
...(reps && { reps: parseInt(reps) }),
|
||
...(duration && { durationSeconds: parseInt(duration) }),
|
||
...(distance && { distanceMeters: parseFloat(distance) }),
|
||
...(height && { height: parseFloat(height) }),
|
||
...((selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && { bodyWeightPercentage: parseFloat(bwPercentage) || 100 })
|
||
};
|
||
|
||
onSetAdded(newSet);
|
||
|
||
if (activePlan) {
|
||
const currentStep = activePlan.steps[currentStepIndex];
|
||
if (currentStep && currentStep.exerciseId === selectedExercise.id) {
|
||
const nextIndex = currentStepIndex + 1;
|
||
setCurrentStepIndex(nextIndex);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleCreateExercise = async () => {
|
||
if (!newName.trim()) return;
|
||
const newEx: ExerciseDef = {
|
||
id: crypto.randomUUID(),
|
||
name: newName.trim(),
|
||
type: newType,
|
||
...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 })
|
||
};
|
||
await saveExercise(userId, newEx);
|
||
const exList = await getExercises(userId);
|
||
setExercises(exList.filter(e => !e.isArchived));
|
||
setSelectedExercise(newEx);
|
||
setNewName('');
|
||
setNewType(ExerciseType.STRENGTH);
|
||
setNewBwPercentage('100');
|
||
setIsCreating(false);
|
||
};
|
||
|
||
const jumpToStep = (index: number) => {
|
||
if (!activePlan) return;
|
||
setCurrentStepIndex(index);
|
||
setShowPlanList(false);
|
||
};
|
||
|
||
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
|
||
|
||
const FilledInput = ({ label, value, onChange, type = "number", icon, autoFocus, step }: any) => (
|
||
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
|
||
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
|
||
{icon} {label}
|
||
</label>
|
||
<input
|
||
type={type}
|
||
step={step}
|
||
inputMode="decimal"
|
||
autoFocus={autoFocus}
|
||
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
|
||
placeholder="0"
|
||
value={value}
|
||
onChange={onChange}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
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">
|
||
<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-error animate-pulse" />
|
||
{activePlan ? activePlan.name : t('free_workout', lang)}
|
||
</h2>
|
||
<span className="text-xs text-on-surface-variant font-mono mt-0.5 flex items-center gap-2">
|
||
<span className="bg-surface-container-high px-1.5 py-0.5 rounded text-on-surface font-bold">{elapsedTime}</span>
|
||
{activeSession.userBodyWeight ? ` • ${activeSession.userBodyWeight}kg` : ''}
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={onSessionEnd}
|
||
className="px-5 py-2 rounded-full bg-error-container text-on-error-container text-sm font-medium hover:opacity-90 transition-opacity"
|
||
>
|
||
{t('finish', lang)}
|
||
</button>
|
||
</div>
|
||
|
||
{activePlan && (
|
||
<div className="bg-surface-container-low border-b border-outline-variant">
|
||
<button
|
||
onClick={() => setShowPlanList(!showPlanList)}
|
||
className="w-full px-4 py-3 flex justify-between items-center"
|
||
>
|
||
<div className="flex flex-col items-start">
|
||
{isPlanFinished ? (
|
||
<div className="flex items-center gap-2 text-primary">
|
||
<CheckSquare size={18} />
|
||
<span className="font-bold">{t('plan_completed', lang)}</span>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<span className="text-[10px] text-primary font-medium tracking-wider">{t('step', lang)} {currentStepIndex + 1} {t('of', lang)} {activePlan.steps.length}</span>
|
||
<div className="font-medium text-on-surface flex items-center gap-2">
|
||
{activePlan.steps[currentStepIndex].exerciseName}
|
||
{activePlan.steps[currentStepIndex].isWeighted && <Scale size={12} className="text-primary" />}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
{showPlanList ? <ChevronUp size={20} className="text-on-surface-variant" /> : <ChevronDown size={20} className="text-on-surface-variant" />}
|
||
</button>
|
||
|
||
{showPlanList && (
|
||
<div className="max-h-48 overflow-y-auto bg-surface-container-high p-2 space-y-1 animate-in slide-in-from-top-2">
|
||
{activePlan.steps.map((step, idx) => (
|
||
<button
|
||
key={step.id}
|
||
onClick={() => jumpToStep(idx)}
|
||
className={`w-full text-left px-4 py-3 rounded-full text-sm flex items-center justify-between transition-colors ${idx === currentStepIndex
|
||
? 'bg-primary-container text-on-primary-container font-medium'
|
||
: idx < currentStepIndex
|
||
? 'text-on-surface-variant opacity-50'
|
||
: 'text-on-surface hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<span>{idx + 1}. {step.exerciseName}</span>
|
||
{step.isWeighted && <Scale size={14} />}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 pb-32 space-y-6">
|
||
|
||
<div className="relative">
|
||
<select
|
||
className="w-full p-4 pr-12 bg-transparent border border-outline rounded-lg text-on-surface appearance-none focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary text-lg font-normal"
|
||
value={selectedExercise?.id || ''}
|
||
onChange={(e) => setSelectedExercise(exercises.find(ex => ex.id === e.target.value) || null)}
|
||
>
|
||
<option value="" disabled>{t('select_exercise', lang)}</option>
|
||
{exercises.map(ex => (
|
||
<option key={ex.id} value={ex.id} className="bg-surface-container text-on-surface">{ex.name}</option>
|
||
))}
|
||
</select>
|
||
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 text-on-surface-variant pointer-events-none" size={24} />
|
||
|
||
<button
|
||
onClick={() => setIsCreating(true)}
|
||
className="absolute right-12 top-1/2 -translate-y-1/2 p-2 text-primary hover:bg-primary-container/20 rounded-full"
|
||
>
|
||
<Plus size={24} />
|
||
</button>
|
||
</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} />}
|
||
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>
|
||
|
||
{(selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && (
|
||
<div className="flex items-center gap-4 px-2">
|
||
<div className="flex items-center gap-2 text-on-surface-variant">
|
||
<Percent size={16} />
|
||
<span className="text-xs font-medium">{t('body_weight_percent', lang)}</span>
|
||
</div>
|
||
<input
|
||
type="number"
|
||
className="w-20 border-b border-outline-variant bg-transparent text-center text-on-surface focus:border-primary focus:outline-none"
|
||
value={bwPercentage}
|
||
onChange={(e) => setBwPercentage(e.target.value)}
|
||
/>
|
||
<span className="text-on-surface-variant text-sm">%</span>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={handleAddSet}
|
||
className="w-full h-14 bg-primary-container text-on-primary-container 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"
|
||
>
|
||
<CheckCircle size={24} />
|
||
<span>{t('log_set', lang)}</span>
|
||
</button>
|
||
|
||
<div className="flex justify-center">
|
||
<div className="bg-surface-container px-4 py-2 rounded-full border border-outline-variant/20 text-xs text-on-surface-variant">
|
||
{t('prev', lang)}: <span className="text-on-surface font-medium ml-1">
|
||
{lastSet ? (
|
||
<>
|
||
{lastSet?.weight ? `${lastSet?.weight}kg × ` : ''}
|
||
{lastSet?.reps ? `${lastSet?.reps}` : ''}
|
||
{lastSet?.distanceMeters ? `${lastSet?.distanceMeters}m` : ''}
|
||
{lastSet?.height ? `${lastSet?.height}cm` : ''}
|
||
{lastSet?.durationSeconds ? `${lastSet?.durationSeconds}s` : ''}
|
||
</>
|
||
) : '—'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeSession.sets.length > 0 && (
|
||
<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) => {
|
||
const setNumber = activeSession.sets.length - idx;
|
||
return (
|
||
<div key={set.id} className="flex justify-between items-center p-4 bg-surface-container rounded-xl shadow-elevation-1 animate-in fade-in slide-in-from-bottom-2">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-8 h-8 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center text-xs font-bold">
|
||
{setNumber}
|
||
</div>
|
||
<div>
|
||
<div className="text-base font-medium text-on-surface">{set.exerciseName}</div>
|
||
<div className="text-sm text-on-surface-variant">
|
||
{set.weight !== undefined && `${set.weight}kg `}
|
||
{set.reps !== undefined && `x ${set.reps}`}
|
||
{set.distanceMeters !== undefined && `${set.distanceMeters}m`}
|
||
{set.durationSeconds !== undefined && `${set.durationSeconds}s`}
|
||
{set.height !== undefined && `${set.height}cm`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => onRemoveSet(set.id)}
|
||
className="p-2 text-on-surface-variant hover:text-error hover:bg-error-container/10 rounded-full transition-colors"
|
||
>
|
||
<Trash2 size={20} />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{isCreating && (
|
||
<div className="fixed inset-0 bg-black/60 z-[60] flex items-end sm:items-center justify-center p-4 backdrop-blur-sm">
|
||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3 animate-in slide-in-from-bottom-10 duration-200">
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h3 className="text-2xl font-normal text-on-surface">{t('create_exercise', lang)}</h3>
|
||
<button onClick={() => setIsCreating(false)} className="p-2 bg-surface-container-high rounded-full hover:bg-outline-variant/20"><X size={20} /></button>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
<FilledInput
|
||
label={t('ex_name', lang)}
|
||
value={newName}
|
||
onChange={(e: any) => setNewName(e.target.value)}
|
||
type="text"
|
||
autoFocus
|
||
/>
|
||
|
||
<div>
|
||
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{[
|
||
{ id: ExerciseType.STRENGTH, label: exerciseTypeLabels[ExerciseType.STRENGTH], icon: Dumbbell },
|
||
{ id: ExerciseType.BODYWEIGHT, label: exerciseTypeLabels[ExerciseType.BODYWEIGHT], icon: User },
|
||
{ id: ExerciseType.CARDIO, label: exerciseTypeLabels[ExerciseType.CARDIO], icon: Flame },
|
||
{ id: ExerciseType.STATIC, label: exerciseTypeLabels[ExerciseType.STATIC], icon: TimerIcon },
|
||
{ id: ExerciseType.HIGH_JUMP, label: exerciseTypeLabels[ExerciseType.HIGH_JUMP], icon: ArrowUp },
|
||
{ id: ExerciseType.LONG_JUMP, label: exerciseTypeLabels[ExerciseType.LONG_JUMP], icon: Ruler },
|
||
{ id: ExerciseType.PLYOMETRIC, label: exerciseTypeLabels[ExerciseType.PLYOMETRIC], icon: Footprints },
|
||
].map((type) => (
|
||
<button
|
||
key={type.id}
|
||
onClick={() => setNewType(type.id)}
|
||
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newType === type.id
|
||
? 'bg-secondary-container text-on-secondary-container border-transparent'
|
||
: 'bg-transparent text-on-surface-variant border-outline hover:border-on-surface-variant'
|
||
}`}
|
||
>
|
||
<type.icon size={14} /> {type.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{newType === ExerciseType.BODYWEIGHT && (
|
||
<FilledInput
|
||
label={t('body_weight_percent', lang)}
|
||
value={newBwPercentage}
|
||
onChange={(e: any) => setNewBwPercentage(e.target.value)}
|
||
icon={<Percent size={12} />}
|
||
/>
|
||
)}
|
||
|
||
<div className="flex justify-end mt-4">
|
||
<button
|
||
onClick={handleCreateExercise}
|
||
className="px-8 py-3 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1"
|
||
>
|
||
{t('create_btn', lang)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Tracker; |