ui/src/pages/WorkoutsPage.tsx (view raw)
1import { useEffect, useState } from 'react';
2import { Link, useNavigate } from 'react-router-dom';
3import { FaPlus, FaPlay, FaEdit, FaTrash, FaFilter } from 'react-icons/fa';
4import type { Routine } from '../types/models';
5import { RoutineService } from '../services/api';
6
7const WorkoutsPage = () => {
8 const [routines, setRoutines] = useState<Routine[]>([]);
9 const [loading, setLoading] = useState<boolean>(true);
10 const [error, setError] = useState<string | null>(null);
11 const [searchTerm, setSearchTerm] = useState<string>('');
12 const navigate = useNavigate();
13
14 useEffect(() => {
15 const fetchRoutines = async () => {
16 try {
17 setLoading(true);
18 const data = await RoutineService.getAll();
19 setRoutines(data);
20 setError(null);
21 } catch (err) {
22 console.error('Failed to fetch routines:', err);
23 setError('Could not load workout routines. Please try again later.');
24 } finally {
25 setLoading(false);
26 }
27 };
28
29 fetchRoutines();
30 }, []);
31
32 const handleStartWorkout = (routine: Routine) => {
33 // Navigate to new workout page with the selected routine
34 navigate('/new-workout', { state: { routineId: routine.id } });
35 };
36
37 const handleDeleteRoutine = async (id: number) => {
38 if (window.confirm('Are you sure you want to delete this routine?')) {
39 try {
40 await RoutineService.delete(id);
41 setRoutines(routines.filter(routine => routine.id !== id));
42 } catch (err) {
43 console.error('Failed to delete routine:', err);
44 alert('Failed to delete routine. Please try again.');
45 }
46 }
47 };
48
49 const filteredRoutines = routines.filter(routine =>
50 routine.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
51 routine.description.toLowerCase().includes(searchTerm.toLowerCase())
52 );
53
54 if (loading) {
55 return (
56 <div className="page workouts-page">
57 <h1>Workouts</h1>
58 <div className="loading">Loading routines...</div>
59 </div>
60 );
61 }
62
63 if (error) {
64 return (
65 <div className="page workouts-page">
66 <h1>Workouts</h1>
67 <div className="error-message">{error}</div>
68 <div className="mt-lg">
69 <Link to="/new-routine" className="btn btn-primary">
70 <FaPlus /> Create New Routine
71 </Link>
72 </div>
73 </div>
74 );
75 }
76
77 return (
78 <div className="page workouts-page">
79 <h1>Workout Routines</h1>
80
81 {/* Search and Filter */}
82 <div className="search-bar">
83 <div className="search-input-container">
84 <FaFilter />
85 <input
86 type="text"
87 placeholder="Search routines..."
88 value={searchTerm}
89 onChange={(e) => setSearchTerm(e.target.value)}
90 className="search-input"
91 />
92 </div>
93 </div>
94
95 {/* Create New Button */}
96 <div className="action-buttons mb-lg">
97 <Link to="/new-routine" className="btn btn-primary">
98 <FaPlus /> Create New Routine
99 </Link>
100 </div>
101
102 {/* Routines List */}
103 {filteredRoutines.length > 0 ? (
104 <div className="routines-list">
105 {filteredRoutines.map(routine => (
106 <div key={routine.id} className="card routine-card">
107 <div className="routine-info">
108 <h3>{routine.name}</h3>
109 <p className="routine-description">{routine.description}</p>
110 <div className="routine-stats"></div>
111 </div>
112
113 <div className="routine-actions">
114 <button
115 className="btn btn-primary"
116 onClick={() => handleStartWorkout(routine)}
117 >
118 <FaPlay /> Start
119 </button>
120 <div className="routine-action-buttons">
121 <Link
122 to={`/new-routine`}
123 state={{ editRoutine: routine }}
124 className="btn btn-secondary action-btn"
125 >
126 <FaEdit />
127 </Link>
128 <button
129 className="btn btn-danger action-btn"
130 onClick={() => routine.id && handleDeleteRoutine(routine.id)}
131 >
132 <FaTrash />
133 </button>
134 </div>
135 </div>
136 </div>
137 ))}
138 </div>
139 ) : (
140 <div className="empty-state">
141 {searchTerm ? (
142 <p>No routines found matching "{searchTerm}"</p>
143 ) : (
144 <>
145 <p>You haven't created any workout routines yet.</p>
146 <p className="mt-sm">Create your first routine to get started!</p>
147 <Link to="/new-routine" className="btn btn-primary mt-md">
148 <FaPlus /> Create Routine
149 </Link>
150 </>
151 )}
152 </div>
153 )}
154
155 <style>{`
156 .search-bar {
157 margin-bottom: var(--spacing-md);
158 }
159
160 .search-input-container {
161 display: flex;
162 align-items: center;
163 background-color: white;
164 border-radius: var(--border-radius);
165 padding: 0 var(--spacing-md);
166 border: 1px solid var(--light-gray);
167 }
168
169 .search-input {
170 border: none;
171 padding: var(--spacing-sm) var(--spacing-sm);
172 flex: 1;
173 }
174
175 .search-input:focus {
176 outline: none;
177 }
178
179 .action-buttons {
180 display: flex;
181 justify-content: flex-end;
182 margin: var(--spacing-md) 0;
183 }
184
185 .routines-list {
186 display: grid;
187 gap: var(--spacing-md);
188 }
189
190 .routine-card {
191 display: flex;
192 flex-direction: column;
193 }
194
195 .routine-info {
196 flex: 1;
197 margin-bottom: var(--spacing-md);
198 }
199
200 .routine-description {
201 color: var(--dark-gray);
202 margin: var(--spacing-sm) 0;
203 }
204
205 .routine-stats {
206 display: flex;
207 gap: var(--spacing-md);
208 color: var(--dark-gray);
209 font-size: 0.9rem;
210 }
211
212 .routine-actions {
213 display: flex;
214 justify-content: space-between;
215 align-items: center;
216 }
217
218 .routine-action-buttons {
219 display: flex;
220 gap: var(--spacing-sm);
221 }
222
223 .action-btn {
224 padding: 8px;
225 min-width: 40px;
226 }
227
228 .empty-state {
229 text-align: center;
230 padding: var(--spacing-xl) var(--spacing-md);
231 background-color: white;
232 border-radius: var(--border-radius);
233 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
234 }
235
236 @media (min-width: 768px) {
237 .routine-card {
238 flex-direction: row;
239 }
240
241 .routine-info {
242 margin-bottom: 0;
243 margin-right: var(--spacing-lg);
244 }
245
246 .routine-actions {
247 flex-direction: column;
248 align-items: flex-end;
249 justify-content: space-between;
250 }
251 }
252 `}</style>
253 </div>
254 );
255};
256
257export default WorkoutsPage;