ui/src/pages/NewExercisePage.tsx (view raw)
1import { useState, useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3import { FaSave, FaArrowLeft, FaTrash, FaTimes } from 'react-icons/fa';
4import type { Exercise, Equipment, MuscleGroup } from '../types/models';
5import { ExerciseService, EquipmentService, MuscleGroupService } from '../services/api';
6
7const NewExercisePage = () => {
8 const navigate = useNavigate();
9 const location = useLocation();
10 const [isLoading, setIsLoading] = useState<boolean>(false);
11 const [error, setError] = useState<string | null>(null);
12 const [successMessage, setSuccessMessage] = useState<string | null>(null);
13
14 const [exerciseToEdit, setExerciseToEdit] = useState<Exercise | null>(null);
15 const [isEditMode, setIsEditMode] = useState<boolean>(false);
16
17 // Available equipment and muscle groups from API
18 const [availableEquipment, setAvailableEquipment] = useState<Equipment[]>([]);
19 const [availableMuscleGroups, setAvailableMuscleGroups] = useState<MuscleGroup[]>([]);
20
21 // Selected items
22 const [selectedEquipment, setSelectedEquipment] = useState<Equipment[]>([]);
23 const [selectedMuscleGroups, setSelectedMuscleGroups] = useState<MuscleGroup[]>([]);
24
25 // Form state
26 const [formData, setFormData] = useState<Exercise>({
27 name: '',
28 description: '',
29 equipment: [],
30 muscleGroups: [],
31 sets: [],
32 });
33
34 // Fetch equipment and muscle groups from API
35 useEffect(() => {
36 const fetchData = async () => {
37 setIsLoading(true);
38 try {
39 const [equipmentData, muscleGroupsData] = await Promise.all([
40 EquipmentService.getAll(),
41 MuscleGroupService.getAll()
42 ]);
43 setAvailableEquipment(equipmentData);
44 setAvailableMuscleGroups(muscleGroupsData);
45 } catch (err) {
46 console.error('Failed to fetch data:', err);
47 setError('Failed to load equipment and muscle groups');
48 } finally {
49 setIsLoading(false);
50 }
51 };
52
53 fetchData();
54 }, []);
55
56 // Check if we're editing an existing exercise
57 useEffect(() => {
58 if (location.state && location.state.editExercise) {
59 const exercise = location.state.editExercise as Exercise;
60 setFormData(exercise);
61 setSelectedEquipment(exercise.equipment || []);
62 setSelectedMuscleGroups(exercise.muscleGroups || []);
63 setExerciseToEdit(exercise);
64 setIsEditMode(true);
65 }
66 }, [location]);
67
68 const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
69 const { name, value } = e.target;
70
71 setFormData(prev => ({
72 ...prev,
73 [name]: value,
74 }));
75 };
76
77 const handleEquipmentSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
78 const equipmentId = parseInt(e.target.value);
79 const equipment = availableEquipment.find(eq => eq.id === equipmentId);
80
81 if (equipment && !selectedEquipment.some(e => e.id === equipment.id)) {
82 const updatedEquipment = [...selectedEquipment, equipment];
83 setSelectedEquipment(updatedEquipment);
84 setFormData(prev => ({
85 ...prev,
86 equipment: updatedEquipment
87 }));
88 }
89
90 // Reset select to default
91 e.target.value = '';
92 };
93
94 const removeEquipment = (equipmentId?: number) => {
95 if (!equipmentId) return;
96 const updatedEquipment = selectedEquipment.filter(e => e.id !== equipmentId);
97 setSelectedEquipment(updatedEquipment);
98 setFormData(prev => ({
99 ...prev,
100 equipment: updatedEquipment
101 }));
102 };
103
104 const handleMuscleGroupSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
105 const muscleGroupId = parseInt(e.target.value);
106 const muscleGroup = availableMuscleGroups.find(mg => mg.id === muscleGroupId);
107
108 if (muscleGroup && !selectedMuscleGroups.some(mg => mg.id === muscleGroup.id)) {
109 const updatedMuscleGroups = [...selectedMuscleGroups, muscleGroup];
110 setSelectedMuscleGroups(updatedMuscleGroups);
111 setFormData(prev => ({
112 ...prev,
113 muscleGroups: updatedMuscleGroups
114 }));
115 }
116
117 // Reset select to default
118 e.target.value = '';
119 };
120
121 const removeMuscleGroup = (muscleGroupId?: number) => {
122 if (!muscleGroupId) return;
123 const updatedMuscleGroups = selectedMuscleGroups.filter(mg => mg.id !== muscleGroupId);
124 setSelectedMuscleGroups(updatedMuscleGroups);
125 setFormData(prev => ({
126 ...prev,
127 muscleGroups: updatedMuscleGroups
128 }));
129 };
130
131 const handleSubmit = async (e: React.FormEvent) => {
132 e.preventDefault();
133 setIsLoading(true);
134 setError(null);
135
136 try {
137 if (formData.muscleGroups.length === 0) {
138 throw new Error('Please select at least one muscle group');
139 }
140
141 if (isEditMode && exerciseToEdit?.id) {
142 // Update existing exercise
143 await ExerciseService.update(exerciseToEdit.id, formData);
144 setSuccessMessage('Exercise updated successfully!');
145 } else {
146 // Create new exercise
147 await ExerciseService.create(formData);
148 setSuccessMessage('Exercise created successfully!');
149
150 // Reset form if creating new
151 if (!isEditMode) {
152 setFormData({
153 name: '',
154 description: '',
155 equipment: [],
156 muscleGroups: [],
157 sets: [],
158 });
159 setSelectedEquipment([]);
160 setSelectedMuscleGroups([]);
161 }
162 }
163
164 // Show success message briefly then redirect
165 setTimeout(() => {
166 navigate('/workouts');
167 }, 1500);
168 } catch (err: unknown) {
169 console.error('Failed to save exercise:', err);
170 setError('Failed to save exercise. Please try again.');
171 } finally {
172 setIsLoading(false);
173 }
174 };
175
176 const handleDelete = async () => {
177 if (!exerciseToEdit?.id || !confirm('Are you sure you want to delete this exercise?')) {
178 return;
179 }
180
181 setIsLoading(true);
182 try {
183 await ExerciseService.delete(exerciseToEdit.id);
184 setSuccessMessage('Exercise deleted successfully!');
185
186 // Redirect after deletion
187 setTimeout(() => {
188 navigate('/workouts');
189 }, 1500);
190 } catch (err) {
191 console.error('Failed to delete exercise:', err);
192 setError('Failed to delete exercise. It might be used in one or more routines.');
193 } finally {
194 setIsLoading(false);
195 }
196 };
197
198 return (
199 <div className="page new-exercise-page">
200 <div className="page-header">
201 <button
202 onClick={() => navigate(-1)}
203 className="btn btn-secondary back-button"
204 >
205 <FaArrowLeft /> Back
206 </button>
207 <h1>{isEditMode ? 'Edit Exercise' : 'New Exercise'}</h1>
208 </div>
209
210 {error && <div className="error-message">{error}</div>}
211 {successMessage && <div className="success-message">{successMessage}</div>}
212
213 <div className="card">
214 <form onSubmit={handleSubmit}>
215 <div className="form-group">
216 <label htmlFor="name">Exercise Name*</label>
217 <input
218 type="text"
219 id="name"
220 name="name"
221 value={formData.name}
222 onChange={handleInputChange}
223 placeholder="e.g. Bench Press"
224 required
225 disabled={isLoading}
226 />
227 </div>
228
229 <div className="form-group">
230 <label htmlFor="description">Description</label>
231 <textarea
232 id="description"
233 name="description"
234 value={formData.description}
235 onChange={handleInputChange}
236 placeholder="Describe how to perform this exercise properly..."
237 rows={4}
238 disabled={isLoading}
239 />
240 </div>
241
242 <div className="form-group">
243 <label htmlFor="muscleGroups">Muscle Groups*</label>
244 <select
245 id="muscleGroups"
246 name="muscleGroups"
247 onChange={handleMuscleGroupSelect}
248 disabled={isLoading}
249 >
250 <option value="">Select muscle group...</option>
251 {availableMuscleGroups.map(group => (
252 <option key={group.id} value={group.id}>{group.name}</option>
253 ))}
254 </select>
255
256 <div className="tags-container">
257 {selectedMuscleGroups.map(group => (
258 <div key={group.id} className="tag">
259 {group.name}
260 <button
261 type="button"
262 className="tag-remove"
263 onClick={() => removeMuscleGroup(group.id)}
264 disabled={isLoading}
265 >
266 <FaTimes />
267 </button>
268 </div>
269 ))}
270 </div>
271 </div>
272
273 <div className="form-group">
274 <label htmlFor="equipment">Equipment</label>
275 <select
276 id="equipment"
277 name="equipment"
278 onChange={handleEquipmentSelect}
279 disabled={isLoading}
280 >
281 <option value="">Select equipment...</option>
282 {availableEquipment.map(eq => (
283 <option key={eq.id} value={eq.id}>{eq.name}</option>
284 ))}
285 </select>
286
287 <div className="tags-container">
288 {selectedEquipment.map(eq => (
289 <div key={eq.id} className="tag">
290 {eq.name}
291 <button
292 type="button"
293 className="tag-remove"
294 onClick={() => removeEquipment(eq.id)}
295 disabled={isLoading}
296 >
297 <FaTimes />
298 </button>
299 </div>
300 ))}
301 </div>
302 </div>
303
304 <div className="form-actions">
305 <button
306 type="submit"
307 className="btn btn-primary btn-block"
308 disabled={isLoading}
309 >
310 <FaSave /> {isEditMode ? 'Update Exercise' : 'Save Exercise'}
311 </button>
312
313 {isEditMode && exerciseToEdit?.id && (
314 <button
315 type="button"
316 className="btn btn-danger btn-block mt-md"
317 onClick={handleDelete}
318 disabled={isLoading}
319 >
320 <FaTrash /> Delete Exercise
321 </button>
322 )}
323 </div>
324 </form>
325 </div>
326
327 <style>{`
328 .page-header {
329 display: flex;
330 align-items: center;
331 margin-bottom: var(--spacing-lg);
332 }
333
334 .back-button {
335 margin-right: var(--spacing-md);
336 padding: var(--spacing-sm) var(--spacing-md);
337 }
338
339 .form-actions {
340 margin-top: var(--spacing-xl);
341 }
342
343 textarea {
344 resize: vertical;
345 }
346
347 .error-message {
348 background-color: rgba(255, 59, 48, 0.1);
349 color: var(--danger-color);
350 padding: var(--spacing-md);
351 border-radius: var(--border-radius);
352 margin-bottom: var(--spacing-lg);
353 }
354
355 .success-message {
356 background-color: rgba(52, 199, 89, 0.1);
357 color: var(--success-color);
358 padding: var(--spacing-md);
359 border-radius: var(--border-radius);
360 margin-bottom: var(--spacing-lg);
361 }
362
363 .tags-container {
364 display: flex;
365 flex-wrap: wrap;
366 gap: var(--spacing-sm);
367 margin-top: var(--spacing-sm);
368 }
369
370 .tag {
371 display: flex;
372 align-items: center;
373 background-color: var(--primary-color-light);
374 color: var(--primary-color-dark);
375 padding: var(--spacing-xs) var(--spacing-sm);
376 border-radius: var(--border-radius);
377 font-size: 0.9rem;
378 }
379
380 .tag-remove {
381 border: none;
382 background: none;
383 color: var(--primary-color-dark);
384 margin-left: var(--spacing-xs);
385 padding: 0;
386 font-size: 0.8rem;
387 display: flex;
388 align-items: center;
389 cursor: pointer;
390 }
391
392 .mt-md {
393 margin-top: var(--spacing-md);
394 }
395 `}</style>
396 </div>
397 );
398};
399
400export default NewExercisePage;