ui/src/pages/NewWorkoutPage.tsx (view raw)
1import { useState, useEffect } from 'react';
2import { useNavigate, useLocation } from 'react-router-dom';
3import {
4 FaArrowLeft,
5 FaCheck,
6 FaSave,
7 FaPlay,
8 FaStop,
9 FaStar,
10 FaRegStar,
11 FaForward
12} from 'react-icons/fa';
13import type {
14 Routine,
15 Exercise,
16 RecordRoutine,
17 RecordExercise,
18 RecordSet,
19 Set,
20 RecordRoutineItem
21} from '../types/models';
22import { RoutineService, WorkoutService } from '../services/api';
23
24interface SetForWorkout {
25 id?: number;
26 setId: number;
27 actualReps: number;
28 actualWeight: number;
29 actualDuration: number;
30 originalSet: Set;
31 completed: boolean;
32}
33
34interface ExerciseForWorkout {
35 id?: number;
36 exerciseId: number;
37 exercise: Exercise;
38 sets: SetForWorkout[];
39 startedAt?: string;
40 endedAt?: string;
41 actualRestTime: number; // in seconds
42 notes: string;
43}
44
45const NewWorkoutPage = () => {
46 const navigate = useNavigate();
47 const location = useLocation();
48
49 // Routines state
50 const [routines, setRoutines] = useState<Routine[]>([]);
51 const [selectedRoutine, setSelectedRoutine] = useState<Routine | null>(null);
52 const [isLoading, setIsLoading] = useState<boolean>(true);
53 const [error, setError] = useState<string | null>(null);
54
55 // Workout tracking state
56 const [workoutStarted, setWorkoutStarted] = useState<boolean>(false);
57 const [workoutCompleted, setWorkoutCompleted] = useState<boolean>(false);
58 const [startTime, setStartTime] = useState<string>('');
59 const [endTime, setEndTime] = useState<string | null>(null);
60 const [elapsedSeconds, setElapsedSeconds] = useState<number>(0);
61 const [intervalId, setIntervalId] = useState<number | null>(null);
62
63 // Exercise tracking state
64 const [currentExerciseIndex, setCurrentExerciseIndex] = useState<number>(0);
65 const [workoutExercises, setWorkoutExercises] = useState<ExerciseForWorkout[]>([]);
66
67 // Workout notes and rating
68 const [workoutNotes, setWorkoutNotes] = useState<string>('');
69 const [feelingRating, setFeelingRating] = useState<number>(3);
70
71 // Success message state
72 const [successMessage, setSuccessMessage] = useState<string | null>(null);
73
74 // Load routines and check for pre-selected routine
75 useEffect(() => {
76 const fetchRoutines = async () => {
77 try {
78 setIsLoading(true);
79 const data = await RoutineService.getAll();
80 setRoutines(data);
81
82 // Check if a routine was pre-selected (from workouts page)
83 if (location.state && location.state.routineId) {
84 const routineId = location.state.routineId;
85 const routine = data.find(r => r.id === routineId);
86
87 if (routine) {
88 handleSelectRoutine(routine);
89 }
90 }
91
92 setError(null);
93 } catch (err) {
94 console.error('Failed to fetch routines:', err);
95 setError('Could not load workout routines. Please try again later.');
96 } finally {
97 setIsLoading(false);
98 }
99 };
100
101 fetchRoutines();
102 }, [location]);
103
104 // Setup the workout when a routine is selected
105 const handleSelectRoutine = (routine: Routine) => {
106 setSelectedRoutine(routine);
107
108 // Initialize workout exercises from routine items
109 const exercises: ExerciseForWorkout[] = [];
110
111 // Process routine items into exercises for the workout
112 routine.routineItems.forEach(item => {
113 if (item.exercise && item.exerciseId) {
114 // This is a regular exercise item
115 const exercise = item.exercise;
116
117 // Get the sets from the exercise or create default ones
118 const exerciseSets = exercise.sets || [];
119 const setsForWorkout: SetForWorkout[] = exerciseSets.map(set => ({
120 setId: set.id || 0,
121 originalSet: set,
122 actualReps: set.reps,
123 actualWeight: set.weight,
124 actualDuration: set.duration,
125 completed: false
126 }));
127
128 // If there are no sets defined, create a default set
129 if (setsForWorkout.length === 0) {
130 setsForWorkout.push({
131 setId: 0,
132 originalSet: {
133 id: 0,
134 exerciseId: exercise.id || 0,
135 reps: 10,
136 weight: 0,
137 duration: 0,
138 orderIndex: 0
139 },
140 actualReps: 10,
141 actualWeight: 0,
142 actualDuration: 0,
143 completed: false
144 });
145 }
146
147 exercises.push({
148 exerciseId: exercise.id || 0,
149 exercise: exercise,
150 sets: setsForWorkout,
151 actualRestTime: item.restTime,
152 notes: ''
153 });
154 }
155 // We could handle supersets here if needed
156 });
157
158 setWorkoutExercises(exercises);
159 setCurrentExerciseIndex(0);
160 };
161
162 // Start the workout
163 const startWorkout = () => {
164 if (!selectedRoutine) return;
165
166 const now = new Date().toISOString();
167 setStartTime(now);
168 setWorkoutStarted(true);
169
170 // Mark first exercise as started
171 if (workoutExercises.length > 0) {
172 const updatedExercises = [...workoutExercises];
173 updatedExercises[0].startedAt = now;
174 setWorkoutExercises(updatedExercises);
175 }
176
177 // Start the timer
178 const id = window.setInterval(() => {
179 setElapsedSeconds(prev => prev + 1);
180 }, 1000);
181
182 setIntervalId(id);
183 };
184
185 // Complete the workout
186 const completeWorkout = () => {
187 if (intervalId) {
188 clearInterval(intervalId);
189 setIntervalId(null);
190 }
191
192 const now = new Date().toISOString();
193 setEndTime(now);
194
195 // Mark current exercise as completed if not already
196 if (workoutExercises.length > 0 && currentExerciseIndex < workoutExercises.length) {
197 const updatedExercises = [...workoutExercises];
198 const currentExercise = updatedExercises[currentExerciseIndex];
199
200 if (currentExercise.startedAt && !currentExercise.endedAt) {
201 currentExercise.endedAt = now;
202 }
203
204 setWorkoutExercises(updatedExercises);
205 }
206
207 setWorkoutCompleted(true);
208 };
209
210 // Format timer display
211 const formatTime = (seconds: number) => {
212 const hrs = Math.floor(seconds / 3600);
213 const mins = Math.floor((seconds % 3600) / 60);
214 const secs = seconds % 60;
215
216 return `${hrs > 0 ? hrs + ':' : ''}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
217 };
218
219 // Handle set completion toggle
220 const toggleSetCompleted = (exerciseIndex: number, setIndex: number) => {
221 const updatedExercises = [...workoutExercises];
222 const currentSet = updatedExercises[exerciseIndex].sets[setIndex];
223 currentSet.completed = !currentSet.completed;
224
225 setWorkoutExercises(updatedExercises);
226 };
227
228 // Handle weight, reps or duration change
229 const handleSetDataChange = (
230 exerciseIndex: number,
231 setIndex: number,
232 field: 'actualReps' | 'actualWeight' | 'actualDuration',
233 value: number
234 ) => {
235 const updatedExercises = [...workoutExercises];
236 const currentSet = updatedExercises[exerciseIndex].sets[setIndex];
237 currentSet[field] = value;
238
239 setWorkoutExercises(updatedExercises);
240 };
241
242 // Move to next exercise
243 const nextExercise = () => {
244 if (currentExerciseIndex >= workoutExercises.length - 1) return;
245
246 const now = new Date().toISOString();
247 const updatedExercises = [...workoutExercises];
248
249 // Complete current exercise
250 const currentExercise = updatedExercises[currentExerciseIndex];
251 if (currentExercise.startedAt && !currentExercise.endedAt) {
252 currentExercise.endedAt = now;
253 }
254
255 // Start next exercise
256 const nextIndex = currentExerciseIndex + 1;
257 const nextExercise = updatedExercises[nextIndex];
258 nextExercise.startedAt = now;
259
260 setWorkoutExercises(updatedExercises);
261 setCurrentExerciseIndex(nextIndex);
262 };
263
264 // Handle notes for an exercise
265 const handleExerciseNotes = (exerciseIndex: number, notes: string) => {
266 const updatedExercises = [...workoutExercises];
267 updatedExercises[exerciseIndex].notes = notes;
268
269 setWorkoutExercises(updatedExercises);
270 };
271
272 // Create RecordSets from workout exercise sets
273 const createRecordSets = (exercise: ExerciseForWorkout): RecordSet[] => {
274 return exercise.sets.map((set, index) => ({
275 recordExerciseId: 0, // Will be filled in by backend
276 setId: set.setId,
277 actualReps: set.actualReps,
278 actualWeight: set.actualWeight,
279 actualDuration: set.actualDuration,
280 completedAt: exercise.endedAt || new Date().toISOString(),
281 orderIndex: index,
282 set: set.originalSet
283 }));
284 };
285
286 // Save workout record
287 const saveWorkout = async () => {
288 if (!selectedRoutine || !startTime) return;
289
290 try {
291 const now = new Date().toISOString();
292
293 // Ensure all exercises have start/end times
294 const completedExercises = workoutExercises.map((ex) => {
295 if (!ex.startedAt) {
296 ex.startedAt = startTime;
297 }
298 if (!ex.endedAt) {
299 ex.endedAt = endTime || now;
300 }
301 return ex;
302 });
303
304 // Create RecordExercises from completed exercises
305 const recordExercises: RecordExercise[] = completedExercises.map((ex, index) => ({
306 id: undefined,
307 recordRoutineId: 0, // Will be filled in by backend
308 exerciseId: ex.exerciseId,
309 startedAt: ex.startedAt || startTime,
310 endedAt: ex.endedAt || now,
311 actualRestTime: ex.actualRestTime,
312 orderIndex: index,
313 recordSets: createRecordSets(ex),
314 exercise: ex.exercise
315 }));
316
317 // Create RecordRoutineItems from recordExercises
318 const recordRoutineItems: RecordRoutineItem[] = recordExercises.map((ex, index) => ({
319 recordRoutineId: 0, // Will be filled in by backend
320 recordExerciseId: undefined, // Will be filled in after recordExercise is created
321 recordSuperSetId: null,
322 actualRestTime: workoutExercises[index].actualRestTime,
323 orderIndex: index,
324 recordExercise: ex,
325 recordSuperSet: null
326 }));
327
328 const workoutRecord: RecordRoutine = {
329 routineId: selectedRoutine.id!,
330 startedAt: startTime,
331 endedAt: endTime || now,
332 routine: selectedRoutine,
333 recordRoutineItems: recordRoutineItems
334 };
335
336 await WorkoutService.create(workoutRecord);
337 setSuccessMessage('Workout saved successfully!');
338
339 // Redirect after a brief delay
340 setTimeout(() => {
341 navigate('/home');
342 }, 1500);
343 } catch (err) {
344 console.error('Failed to save workout:', err);
345 setError('Failed to save your workout. Please try again.');
346 }
347 };
348
349 // Check if all sets in current exercise are completed
350 const isCurrentExerciseComplete = () => {
351 if (currentExerciseIndex >= workoutExercises.length) return false;
352
353 const currentExercise = workoutExercises[currentExerciseIndex];
354 return currentExercise.sets.every(set => set.completed);
355 };
356
357 // Progress status percentage
358 const calculateProgress = () => {
359 if (workoutExercises.length === 0) return 0;
360
361 const totalSets = workoutExercises.reduce((total, ex) => total + ex.sets.length, 0);
362 const completedSets = workoutExercises.reduce((total, ex) => {
363 return total + ex.sets.filter(set => set.completed).length;
364 }, 0);
365
366 return Math.round((completedSets / totalSets) * 100);
367 };
368
369 return (
370 <div className="page new-workout-page">
371 <div className="page-header">
372 <button
373 onClick={() => navigate(-1)}
374 className="btn btn-secondary back-button"
375 >
376 <FaArrowLeft /> Back
377 </button>
378 <h1>New Workout</h1>
379 </div>
380
381 {error && <div className="error-message">{error}</div>}
382 {successMessage && <div className="success-message">{successMessage}</div>}
383
384 {!selectedRoutine ? (
385 // Routine selection view
386 <div className="routine-selection card">
387 <h2>Select a Routine</h2>
388
389 {isLoading ? (
390 <div className="loading">Loading routines...</div>
391 ) : routines.length === 0 ? (
392 <div className="empty-state">
393 <p>No routines found.</p>
394 <p>Create a routine to start working out.</p>
395 <button
396 onClick={() => navigate('/new-routine')}
397 className="btn btn-primary mt-md"
398 >
399 Create Routine
400 </button>
401 </div>
402 ) : (
403 <div className="routines-list">
404 {routines.map(routine => (
405 <div key={routine.id} className="routine-item" onClick={() => handleSelectRoutine(routine)}>
406 <h3>{routine.name}</h3>
407 {routine.description && <p>{routine.description}</p>}
408 <div className="routine-meta">
409 <span>{routine.routineItems.length} exercises</span>
410 </div>
411 </div>
412 ))}
413 </div>
414 )}
415 </div>
416 ) : !workoutStarted ? (
417 // Workout ready view
418 <div className="workout-ready card">
419 <h2>{selectedRoutine.name}</h2>
420 {selectedRoutine.description && <p className="routine-description">{selectedRoutine.description}</p>}
421
422 <div className="workout-details">
423 <div className="detail-item">
424 <span className="detail-label">Exercises:</span>
425 <span className="detail-value">{workoutExercises.length}</span>
426 </div>
427 <div className="detail-item">
428 <span className="detail-label">Sets:</span>
429 <span className="detail-value">
430 {workoutExercises.reduce((total, ex) => total + ex.sets.length, 0)}
431 </span>
432 </div>
433 </div>
434
435 <div className="exercise-preview">
436 <h3>Exercises</h3>
437 <ul className="exercise-list">
438 {workoutExercises.map((exercise, index) => (
439 <li key={`${exercise.exerciseId}-${index}`}>
440 <div className="exercise-name">{exercise.exercise.name}</div>
441 <div className="exercise-sets">{exercise.sets.length} sets</div>
442 </li>
443 ))}
444 </ul>
445 </div>
446
447 <div className="action-buttons">
448 <button
449 className="btn btn-primary btn-lg btn-block"
450 onClick={startWorkout}
451 >
452 <FaPlay /> Start Workout
453 </button>
454 <button
455 className="btn btn-secondary btn-block mt-md"
456 onClick={() => setSelectedRoutine(null)}
457 >
458 Select Different Routine
459 </button>
460 </div>
461 </div>
462 ) : workoutCompleted ? (
463 // Workout complete view
464 <div className="workout-complete card">
465 <div className="workout-summary">
466 <h2>Workout Complete!</h2>
467
468 <div className="summary-stats">
469 <div className="stat">
470 <span className="stat-label">Duration</span>
471 <span className="stat-value">{formatTime(elapsedSeconds)}</span>
472 </div>
473
474 <div className="stat">
475 <span className="stat-label">Completed</span>
476 <span className="stat-value">{calculateProgress()}%</span>
477 </div>
478 </div>
479
480 <div className="feeling-rating">
481 <p>How was your workout?</p>
482 <div className="stars">
483 {[1, 2, 3, 4, 5].map(rating => (
484 <button
485 key={rating}
486 onClick={() => setFeelingRating(rating)}
487 className="star-btn"
488 >
489 {rating <= feelingRating ? <FaStar /> : <FaRegStar />}
490 </button>
491 ))}
492 </div>
493 </div>
494
495 <div className="workout-notes form-group">
496 <label htmlFor="workout-notes">Workout Notes</label>
497 <textarea
498 id="workout-notes"
499 value={workoutNotes}
500 onChange={e => setWorkoutNotes(e.target.value)}
501 placeholder="Add notes about the overall workout..."
502 rows={3}
503 />
504 </div>
505
506 <div className="action-buttons">
507 <button
508 onClick={saveWorkout}
509 className="btn btn-primary btn-lg btn-block"
510 >
511 <FaSave /> Save Workout
512 </button>
513 </div>
514 </div>
515 </div>
516 ) : (
517 // Active workout view
518 <div className="active-workout">
519 <div className="workout-header card">
520 <h2>{selectedRoutine.name}</h2>
521
522 <div className="workout-timer">
523 <div className="timer-value">{formatTime(elapsedSeconds)}</div>
524 <div className="progress-bar">
525 <div
526 className="progress"
527 style={{ width: `${calculateProgress()}%` }}
528 ></div>
529 </div>
530 </div>
531 </div>
532
533 <div className="current-exercise card">
534 {currentExerciseIndex < workoutExercises.length ? (
535 <>
536 <h3 className="exercise-name">
537 {workoutExercises[currentExerciseIndex].exercise.name}
538 </h3>
539
540 <div className="exercise-sets">
541 <table className="sets-table">
542 <thead>
543 <tr>
544 <th>Set</th>
545 <th>Weight</th>
546 <th>Reps</th>
547 {workoutExercises[currentExerciseIndex].sets.some(s => s.originalSet.duration > 0) && (
548 <th>Time</th>
549 )}
550 <th>Done</th>
551 </tr>
552 </thead>
553 <tbody>
554 {workoutExercises[currentExerciseIndex].sets.map((set, setIndex) => (
555 <tr key={setIndex} className={set.completed ? 'completed' : ''}>
556 <td>{setIndex + 1}</td>
557 <td>
558 <input
559 type="number"
560 min="0"
561 step="1"
562 value={set.actualWeight}
563 onChange={e => handleSetDataChange(
564 currentExerciseIndex,
565 setIndex,
566 'actualWeight',
567 parseFloat(e.target.value) || 0
568 )}
569 />
570 </td>
571 <td>
572 <input
573 type="number"
574 min="0"
575 step="1"
576 value={set.actualReps}
577 onChange={e => handleSetDataChange(
578 currentExerciseIndex,
579 setIndex,
580 'actualReps',
581 parseInt(e.target.value) || 0
582 )}
583 />
584 </td>
585 {workoutExercises[currentExerciseIndex].sets.some(s => s.originalSet.duration > 0) && (
586 <td>
587 <input
588 type="number"
589 min="0"
590 step="1"
591 value={set.actualDuration}
592 onChange={e => handleSetDataChange(
593 currentExerciseIndex,
594 setIndex,
595 'actualDuration',
596 parseInt(e.target.value) || 0
597 )}
598 />
599 </td>
600 )}
601 <td>
602 <button
603 className={`btn-check ${set.completed ? 'completed' : ''}`}
604 onClick={() => toggleSetCompleted(currentExerciseIndex, setIndex)}
605 >
606 {set.completed && <FaCheck />}
607 </button>
608 </td>
609 </tr>
610 ))}
611 </tbody>
612 </table>
613 </div>
614
615 <div className="exercise-notes form-group">
616 <label htmlFor="exercise-notes">Exercise Notes</label>
617 <textarea
618 id="exercise-notes"
619 value={workoutExercises[currentExerciseIndex].notes}
620 onChange={e => handleExerciseNotes(currentExerciseIndex, e.target.value)}
621 placeholder="Add notes for this exercise..."
622 rows={2}
623 />
624 </div>
625
626 <div className="exercise-navigation">
627 {currentExerciseIndex < workoutExercises.length - 1 && (
628 <button
629 onClick={nextExercise}
630 className={`btn btn-primary ${isCurrentExerciseComplete() ? 'pulse' : ''}`}
631 disabled={!isCurrentExerciseComplete() && workoutExercises[currentExerciseIndex].sets.length > 0}
632 >
633 <FaForward /> Next Exercise
634 </button>
635 )}
636
637 {currentExerciseIndex === workoutExercises.length - 1 && isCurrentExerciseComplete() && (
638 <button
639 onClick={completeWorkout}
640 className="btn btn-primary pulse"
641 >
642 <FaStop /> Finish Workout
643 </button>
644 )}
645 </div>
646 </>
647 ) : (
648 <div>
649 <p>All exercises completed!</p>
650 <button
651 onClick={completeWorkout}
652 className="btn btn-primary"
653 >
654 <FaStop /> Finish Workout
655 </button>
656 </div>
657 )}
658 </div>
659
660 <div className="workout-nav">
661 <div className="exercises-list card">
662 <h3>Progress</h3>
663 <ul>
664 {workoutExercises.map((ex, index) => (
665 <li
666 key={`${ex.exerciseId}-${index}`}
667 className={`
668 ${index === currentExerciseIndex ? 'active' : ''}
669 ${ex.sets.every(s => s.completed) ? 'completed' : ''}
670 `}
671 onClick={() => setCurrentExerciseIndex(index)}
672 >
673 <span className="exercise-number">{index + 1}</span>
674 <span className="exercise-list-name">{ex.exercise.name}</span>
675 <span className="exercise-progress">
676 {ex.sets.filter(s => s.completed).length}/{ex.sets.length}
677 </span>
678 </li>
679 ))}
680 </ul>
681 </div>
682
683 <div className="workout-actions card">
684 <button
685 onClick={completeWorkout}
686 className="btn btn-danger btn-block"
687 >
688 <FaStop /> End Workout
689 </button>
690 </div>
691 </div>
692 </div>
693 )}
694
695 <style>{`
696 .page-header {
697 display: flex;
698 align-items: center;
699 margin-bottom: var(--spacing-lg);
700 }
701
702 .back-button {
703 margin-right: var(--spacing-md);
704 padding: var(--spacing-sm) var(--spacing-md);
705 }
706
707 /* Routine Selection */
708 .routines-list {
709 display: grid;
710 gap: var(--spacing-md);
711 }
712
713 .routine-item {
714 padding: var(--spacing-md);
715 border: 1px solid var(--border-color);
716 border-radius: var(--border-radius);
717 cursor: pointer;
718 transition: all 0.2s;
719 }
720
721 .routine-item:hover {
722 background-color: rgba(0, 122, 255, 0.05);
723 border-color: var(--primary-color);
724 }
725
726 .routine-item h3 {
727 margin: 0;
728 margin-bottom: var(--spacing-xs);
729 }
730
731 .routine-item p {
732 margin: 0;
733 margin-bottom: var(--spacing-sm);
734 color: var(--text-muted);
735 }
736
737 .routine-meta {
738 font-size: 0.9rem;
739 color: var(--text-muted);
740 }
741
742 /* Workout Ready */
743 .routine-description {
744 color: var(--text-muted);
745 margin-bottom: var(--spacing-lg);
746 }
747
748 .workout-details {
749 display: flex;
750 gap: var(--spacing-lg);
751 margin-bottom: var(--spacing-lg);
752 }
753
754 .detail-item {
755 display: flex;
756 flex-direction: column;
757 align-items: center;
758 background-color: rgba(0, 122, 255, 0.1);
759 padding: var(--spacing-md) var(--spacing-lg);
760 border-radius: var(--border-radius);
761 }
762
763 .detail-label {
764 color: var(--text-muted);
765 font-size: 0.9rem;
766 }
767
768 .detail-value {
769 font-size: 1.2rem;
770 font-weight: bold;
771 }
772
773 .exercise-preview {
774 margin-bottom: var(--spacing-lg);
775 }
776
777 .exercise-list {
778 list-style: none;
779 padding: 0;
780 margin: 0;
781 }
782
783 .exercise-list li {
784 padding: var(--spacing-sm) 0;
785 border-bottom: 1px solid var(--border-color);
786 display: flex;
787 justify-content: space-between;
788 }
789
790 .exercise-list li:last-child {
791 border-bottom: none;
792 }
793
794 .exercise-sets {
795 color: var(--text-muted);
796 font-size: 0.9rem;
797 }
798
799 .btn-lg {
800 padding: var(--spacing-md);
801 font-size: 1.1rem;
802 }
803
804 /* Active Workout */
805 .workout-header {
806 margin-bottom: var(--spacing-md);
807 padding: var(--spacing-md);
808 }
809
810 .workout-header h2 {
811 margin-bottom: var(--spacing-sm);
812 }
813
814 .workout-timer {
815 text-align: center;
816 }
817
818 .timer-value {
819 font-size: 1.5rem;
820 font-weight: bold;
821 margin-bottom: var(--spacing-xs);
822 }
823
824 .progress-bar {
825 height: 8px;
826 background-color: var(--light-gray);
827 border-radius: 4px;
828 overflow: hidden;
829 }
830
831 .progress {
832 height: 100%;
833 background-color: var(--primary-color);
834 transition: width 0.3s ease;
835 }
836
837 .current-exercise {
838 margin-bottom: var(--spacing-md);
839 padding: var(--spacing-md);
840 }
841
842 .exercise-name {
843 margin-bottom: var(--spacing-md);
844 font-size: 1.2rem;
845 }
846
847 /* Sets table */
848 .sets-table {
849 width: 100%;
850 border-collapse: collapse;
851 margin-bottom: var(--spacing-md);
852 }
853
854 .sets-table th {
855 padding: var(--spacing-sm);
856 text-align: center;
857 border-bottom: 1px solid var(--border-color);
858 font-weight: 600;
859 }
860
861 .sets-table td {
862 padding: var(--spacing-sm);
863 text-align: center;
864 border-bottom: 1px solid var(--border-color);
865 }
866
867 .sets-table tr.completed {
868 background-color: rgba(52, 199, 89, 0.1);
869 }
870
871 .sets-table input {
872 width: 60px;
873 padding: 4px;
874 text-align: center;
875 }
876
877 .btn-check {
878 width: 30px;
879 height: 30px;
880 border-radius: 50%;
881 border: 1px solid var(--border-color);
882 background: white;
883 display: flex;
884 align-items: center;
885 justify-content: center;
886 cursor: pointer;
887 }
888
889 .btn-check.completed {
890 background-color: var(--success-color);
891 color: white;
892 border-color: var(--success-color);
893 }
894
895 .exercise-notes {
896 margin-bottom: var(--spacing-md);
897 }
898
899 .exercise-navigation {
900 display: flex;
901 justify-content: flex-end;
902 }
903
904 /* Pulse animation for the next button */
905 .pulse {
906 animation: pulse 1.5s infinite;
907 }
908
909 @keyframes pulse {
910 0% {
911 box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.4);
912 }
913 70% {
914 box-shadow: 0 0 0 10px rgba(0, 122, 255, 0);
915 }
916 100% {
917 box-shadow: 0 0 0 0 rgba(0, 122, 255, 0);
918 }
919 }
920
921 /* Progress list */
922 .workout-nav {
923 margin-bottom: var(--spacing-lg);
924 }
925
926 .exercises-list ul {
927 list-style: none;
928 padding: 0;
929 margin: 0;
930 }
931
932 .exercises-list li {
933 display: flex;
934 align-items: center;
935 padding: var(--spacing-sm);
936 border-bottom: 1px solid var(--border-color);
937 cursor: pointer;
938 }
939
940 .exercises-list li.active {
941 background-color: rgba(0, 122, 255, 0.1);
942 }
943
944 .exercises-list li.completed {
945 color: var(--text-muted);
946 }
947
948 .exercise-number {
949 width: 24px;
950 height: 24px;
951 border-radius: 50%;
952 background-color: var(--light-gray);
953 display: flex;
954 align-items: center;
955 justify-content: center;
956 margin-right: var(--spacing-sm);
957 font-size: 0.8rem;
958 }
959
960 .exercises-list li.completed .exercise-number {
961 background-color: var(--success-color);
962 color: white;
963 }
964
965 .exercise-list-name {
966 flex: 1;
967 }
968
969 .exercise-progress {
970 font-size: 0.8rem;
971 color: var(--text-muted);
972 }
973
974 .workout-actions {
975 margin-top: var(--spacing-md);
976 padding: var(--spacing-md);
977 }
978
979 /* Workout Complete */
980 .workout-complete {
981 text-align: center;
982 padding: var(--spacing-lg);
983 }
984
985 .workout-summary h2 {
986 margin-bottom: var(--spacing-lg);
987 }
988
989 .summary-stats {
990 display: flex;
991 justify-content: center;
992 gap: var(--spacing-xl);
993 margin-bottom: var(--spacing-lg);
994 }
995
996 .stat {
997 display: flex;
998 flex-direction: column;
999 }
1000
1001 .stat-label {
1002 font-size: 0.9rem;
1003 color: var(--text-muted);
1004 }
1005
1006 .stat-value {
1007 font-size: 1.5rem;
1008 font-weight: bold;
1009 }
1010
1011 .feeling-rating {
1012 margin-bottom: var(--spacing-lg);
1013 }
1014
1015 .stars {
1016 display: flex;
1017 justify-content: center;
1018 gap: var(--spacing-sm);
1019 }
1020
1021 .star-btn {
1022 background: none;
1023 border: none;
1024 font-size: 1.5rem;
1025 color: #ffb700;
1026 cursor: pointer;
1027 }
1028
1029 /* Shared */
1030 .loading {
1031 text-align: center;
1032 padding: var(--spacing-lg);
1033 color: var(--text-muted);
1034 }
1035
1036 .empty-state {
1037 text-align: center;
1038 padding: var(--spacing-lg);
1039 color: var(--text-muted);
1040 }
1041
1042 .error-message {
1043 background-color: rgba(255, 59, 48, 0.1);
1044 color: var(--danger-color);
1045 padding: var(--spacing-md);
1046 border-radius: var(--border-radius);
1047 margin-bottom: var(--spacing-lg);
1048 }
1049
1050 .success-message {
1051 background-color: rgba(52, 199, 89, 0.1);
1052 color: var(--success-color);
1053 padding: var(--spacing-md);
1054 border-radius: var(--border-radius);
1055 margin-bottom: var(--spacing-lg);
1056 }
1057
1058 .mt-md {
1059 margin-top: var(--spacing-md);
1060 }
1061
1062 .card {
1063 background-color: white;
1064 border-radius: var(--border-radius);
1065 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1066 padding: var(--spacing-lg);
1067 }
1068
1069 @media (min-width: 768px) {
1070 .active-workout {
1071 display: grid;
1072 grid-template-columns: 2fr 1fr;
1073 gap: var(--spacing-md);
1074 }
1075
1076 .workout-header {
1077 grid-column: 1 / -1;
1078 }
1079
1080 .workout-nav {
1081 grid-column: 2;
1082 grid-row: 2;
1083 }
1084 }
1085 `}</style>
1086 </div>
1087 );
1088};
1089
1090export default NewWorkoutPage;