Timer implemented. No working tests.

This commit is contained in:
AG
2025-12-10 23:07:31 +02:00
parent 3df4abba47
commit b86664816d
24 changed files with 806 additions and 116 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical, Scale } from 'lucide-react';
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical, Scale, Search } from 'lucide-react';
import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types';
import { getExercises, saveExercise } from '../services/storage';
import { t } from '../services/i18n';
@@ -12,6 +12,7 @@ import FilledInput from './FilledInput';
import { toTitleCase } from '../utils/text';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
interface PlansProps {
lang: Language;
@@ -93,7 +94,10 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
exerciseId: ex.id,
exerciseName: ex.name,
exerciseType: ex.type,
isWeighted: false
isWeighted: false,
restTimeSeconds: 120 // Default new step to 120s? Or leave undefined to use profile default?
// Requirement: "fallback to user default". So maybe undefined/null is better for "inherit".
// But UI needs a value. Let's start with 120 or empty.
};
setSteps([...steps, newStep]);
setShowExerciseSelector(false);
@@ -134,6 +138,10 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
setSteps(steps.map(s => s.id === stepId ? { ...s, isWeighted: !s.isWeighted } : s));
};
const updateRestTime = (stepId: string, seconds: number | undefined) => {
setSteps(steps.map(s => s.id === stepId ? { ...s, restTimeSeconds: seconds } : s));
};
const removeStep = (stepId: string) => {
setSteps(steps.filter(s => s.id !== stepId));
};
@@ -222,18 +230,35 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<div className="flex-1">
<div className="text-base font-medium text-on-surface">{step.exerciseName}</div>
<label className="flex items-center gap-2 mt-1 cursor-pointer w-fit">
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
{step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
<div className="flex items-center gap-4 mt-1">
<label className="flex items-center gap-2 cursor-pointer w-fit">
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
{step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
</div>
<input
type="checkbox"
checked={step.isWeighted}
onChange={() => toggleWeighted(step.id)}
className="hidden"
/>
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
</label>
<div className="flex items-center gap-2">
<TimerIcon size={14} className="text-on-surface-variant" />
<input
type="number"
placeholder="Rest (s)"
className="w-16 bg-transparent border-b border-outline-variant text-xs text-on-surface focus:border-primary focus:outline-none text-center"
value={step.restTimeSeconds || ''}
onChange={(e) => {
const val = parseInt(e.target.value);
updateRestTime(step.id, isNaN(val) ? undefined : val);
}}
/>
<span className="text-xs text-on-surface-variant">s</span>
</div>
<input
type="checkbox"
checked={step.isWeighted}
onChange={() => toggleWeighted(step.id)}
className="hidden"
/>
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
</label>
</div>
</div>
<Button onClick={() => removeStep(step.id)} variant="ghost" size="icon" className="text-on-surface-variant hover:text-error hover:bg-error/10">
<X size={20} />
@@ -254,20 +279,19 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
</div>
</div>
{showExerciseSelector && (
<div className="fixed inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200">
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
<div className="flex gap-2">
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" size="icon" className="text-primary hover:bg-primary-container/20">
<Plus size={20} />
</Button>
<Button onClick={() => setShowExerciseSelector(false)} variant="ghost" size="icon">
<X size={20} />
</Button>
</div>
<Modal
isOpen={showExerciseSelector}
onClose={() => setShowExerciseSelector(false)}
title={t('select_exercise', lang)}
maxWidth="md"
>
<div className="flex flex-col h-[60vh]">
<div className="flex justify-end mb-2">
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" className="text-primary hover:bg-primary-container/20 flex gap-2">
<Plus size={18} /> {t('create_exercise', lang)}
</Button>
</div>
<div className="flex-1 overflow-y-auto p-2">
<div className="flex-1 overflow-y-auto -mx-6 px-6">
{availableExercises
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
@@ -275,84 +299,80 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
<button
key={ex.id}
onClick={() => addStep(ex)}
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between"
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between group"
>
<span>{ex.name}</span>
<span className="group-hover:text-primary transition-colors">{ex.name}</span>
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
</button>
))}
</div>
{isCreatingExercise && (
<div className="fixed inset-0 bg-surface z-[60] flex flex-col animate-in slide-in-from-bottom-full duration-200">
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
<h3 className="text-title-medium font-medium text-on-surface">{t('create_exercise', lang)}</h3>
<Button onClick={() => setIsCreatingExercise(false)} variant="ghost" size="icon" className="text-on-surface-variant hover:bg-white/5">
<X size={20} />
</Button>
</div>
<div className="p-4 space-y-6 overflow-y-auto flex-1">
<FilledInput
label={t('ex_name', lang)}
value={newExName}
onChange={(e: any) => setNewExName(e.target.value)}
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewExName(toTitleCase(newExName))}
/>
<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={() => setNewExType(type.id)}
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newExType === 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>
{newExType === ExerciseType.BODYWEIGHT && (
<FilledInput
label={t('body_weight_percent', lang)}
value={newExBwPercentage}
onChange={(e: any) => setNewExBwPercentage(e.target.value)}
icon={<Percent size={12} />}
/>
)}
<div className="flex justify-end mt-4">
<Button
onClick={handleCreateExercise}
fullWidth
size="lg"
>
<CheckCircle size={20} className="mr-2" />
{t('create_btn', lang)}
</Button>
</div>
</div>
</div>
)}
</div>
)}
</Modal>
<Modal
isOpen={isCreatingExercise}
onClose={() => setIsCreatingExercise(false)}
title={t('create_exercise', lang)}
maxWidth="md"
>
<div className="space-y-6 pt-2">
<FilledInput
label={t('ex_name', lang)}
value={newExName}
onChange={(e: any) => setNewExName(e.target.value)}
type="text"
autoFocus
autocapitalize="words"
onBlur={() => setNewExName(toTitleCase(newExName))}
/>
<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={() => setNewExType(type.id)}
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newExType === 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>
{newExType === ExerciseType.BODYWEIGHT && (
<FilledInput
label={t('body_weight_percent', lang)}
value={newExBwPercentage}
onChange={(e: any) => setNewExBwPercentage(e.target.value)}
icon={<Percent size={12} />}
/>
)}
<div className="flex justify-end mt-6">
<Button
onClick={handleCreateExercise}
fullWidth
size="lg"
>
<CheckCircle size={20} className="mr-2" />
{t('create_btn', lang)}
</Button>
</div>
</div>
</Modal>
</div>
);
}