all repos — go-lift @ 092e7440440b8459d82fb90c05c005f9f38e3c51

Lightweight workout tracker prototype..

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;