all repos — go-lift @ 092e7440440b8459d82fb90c05c005f9f38e3c51

Lightweight workout tracker prototype..

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;