Timer implemented. No working tests.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user