()
| 20 | const [loading, setLoading] = useState(false) |
| 21 | const [error, setError] = useState('') |
| 22 | const [pickerExercises, setPickerExercises] = useState<Record<number, types.Exercise>>({}) |
| 23 | const [formData, setFormData] = useState<ProgramFormData>({ name: '', notes: '', days: [] }) |
| 24 | |
| 25 | useEffect(() => { if (error) window.scrollTo({ top: 0, behavior: 'smooth' }) }, [error]) |
| 26 | |
| 27 | const cacheExercise = (ex: types.Exercise) => setPickerExercises(prev => ({ ...prev, [ex.id]: ex })) |
| 28 | |
| 29 | const handleSubmit = async (e: React.FormEvent) => { |
| 30 | e.preventDefault() |
| 31 | if (!formData.name.trim()) { setError('Program name required'); return } |
| 32 | if (formData.days.length === 0) { setError('Add at least one day'); return } |
| 33 | const hasAnyExercise = hasWorkoutExercises(formData.days) |
| 34 | if (!hasAnyExercise) { setError('Add at least one exercise to a workout day'); return } |
| 35 | setLoading(true) |
| 36 | try { |
| 37 | const payload = { |
| 38 | name: formData.name, |
| 39 | notes: formData.notes, |
| 40 | days: formData.days.map(d => ({ |
| 41 | ...d, |
| 42 | exercises: d.exercises.map(ex => ({ |
| 43 | ...ex, |
| 44 | sets: ex.sets.map(s => ({ ...s, target_weight: displayToLbs(s.target_weight, settings.weight_unit) })), |
| 45 | })), |
| 46 | })), |
| 47 | } |
| 48 | await programAPI.create(payload) |
| 49 | navigate('/programs') |
| 50 | } catch (err: any) { |
| 51 | setError(err.response?.data?.error || 'Failed to create program') |
| 52 | } finally { |
| 53 | setLoading(false) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | const totalExercises = formData.days.reduce((s, d) => s + d.exercises.length, 0) |
| 58 | const totalSets = formData.days.reduce((s, d) => s + d.exercises.reduce((s2, ex) => s2 + ex.sets.length, 0), 0) |
| 59 | |
| 60 | return ( |
| 61 | <div className="space-y-6 animate-slide-up pb-10"> |
| 62 | <div className="flex items-center gap-3"> |
| 63 | <button aria-label="Go back" onClick={() => navigate(-1)} className="p-2 hover:bg-surface-muted rounded-lg transition-colors"> |
| 64 | <ArrowLeft className="w-5 h-5 text-tx-muted" /> |
| 65 | </button> |
| 66 | <div> |
| 67 | <h1 className="font-display font-bold text-2xl text-tx-primary">New Program</h1> |
| 68 | <p className="text-xs text-tx-muted"> |
| 69 | {formData.days.length} day{formData.days.length === 1 ? '' : 's'} • {totalExercises} exercise{totalExercises === 1 ? '' : 's'} • {totalSets} set{totalSets === 1 ? '' : 's'} |
| 70 | </p> |
| 71 | </div> |
| 72 | </div> |
| 73 | |
| 74 | <form onSubmit={handleSubmit} className="space-y-6"> |
| 75 | {error && ( |
| 76 | <div className="alert-error"> |
| 77 | <AlertCircle className="w-4 h-4 flex-shrink-0" /> |
| 78 | <span>{error}</span> |
| 79 | </div> |
nothing calls this directly
no test coverage detected