1+ import React , { useState , useEffect } from 'react' ;
2+ import {
3+ SafeAreaView ,
4+ View ,
5+ Text ,
6+ StyleSheet ,
7+ ScrollView ,
8+ TouchableOpacity ,
9+ RefreshControl ,
10+ ActivityIndicator ,
11+ Alert ,
12+ } from 'react-native' ;
13+ import { router } from 'expo-router' ;
14+ import { apiService , WorkoutResponse } from '@/services/api' ;
15+ import { useAuth } from '@/contexts/AuthContext' ;
16+
17+ const WorkoutHistoryScreen = ( ) => {
18+ const { user } = useAuth ( ) ;
19+ const [ workouts , setWorkouts ] = useState < WorkoutResponse [ ] > ( [ ] ) ;
20+ const [ loading , setLoading ] = useState ( true ) ;
21+ const [ refreshing , setRefreshing ] = useState ( false ) ;
22+ const [ error , setError ] = useState < string | null > ( null ) ;
23+
24+ const fetchWorkouts = async ( isRefresh = false ) => {
25+ try {
26+ if ( isRefresh ) {
27+ setRefreshing ( true ) ;
28+ } else {
29+ setLoading ( true ) ;
30+ }
31+ setError ( null ) ;
32+
33+ const workoutData = await apiService . getWorkouts ( ) ;
34+ // Ensure workoutData is an array
35+ if ( Array . isArray ( workoutData ) ) {
36+ setWorkouts ( workoutData ) ;
37+ } else {
38+ console . warn ( 'API returned non-array data:' , workoutData ) ;
39+ setWorkouts ( [ ] ) ;
40+ setError ( 'Invalid data format received from server' ) ;
41+ }
42+ } catch ( err ) {
43+ console . error ( 'Error fetching workouts:' , err ) ;
44+ setError ( err instanceof Error ? err . message : 'Failed to load workouts' ) ;
45+ setWorkouts ( [ ] ) ; // Ensure workouts is always an array
46+ } finally {
47+ setLoading ( false ) ;
48+ setRefreshing ( false ) ;
49+ }
50+ } ;
51+
52+ useEffect ( ( ) => {
53+ fetchWorkouts ( ) ;
54+ } , [ ] ) ;
55+
56+ const onRefresh = ( ) => {
57+ fetchWorkouts ( true ) ;
58+ } ;
59+
60+ const formatDate = ( dateString : string ) => {
61+ const date = new Date ( dateString ) ;
62+ return date . toLocaleDateString ( 'en-US' , {
63+ year : 'numeric' ,
64+ month : 'short' ,
65+ day : 'numeric' ,
66+ } ) ;
67+ } ;
68+
69+ const formatDuration = ( minutes ?: number ) => {
70+ if ( ! minutes ) return 'N/A' ;
71+ const hours = Math . floor ( minutes / 60 ) ;
72+ const mins = minutes % 60 ;
73+ if ( hours > 0 ) {
74+ return `${ hours } h ${ mins } m` ;
75+ }
76+ return `${ mins } m` ;
77+ } ;
78+
79+ const getCompletionStatus = ( workout : WorkoutResponse ) => {
80+ return workout . is_completed ? 'Completed' : 'Incomplete' ;
81+ } ;
82+
83+ const getStatusColor = ( workout : WorkoutResponse ) => {
84+ return workout . is_completed ? '#4CAF50' : '#FF9800' ;
85+ } ;
86+
87+ const handleWorkoutPress = ( workout : WorkoutResponse ) => {
88+ // Navigate to the detailed workout view
89+ router . push ( {
90+ pathname : '/workout-detail' ,
91+ params : { workoutId : workout . id }
92+ } ) ;
93+ } ;
94+
95+ const renderWorkoutItem = ( workout : WorkoutResponse ) => (
96+ < TouchableOpacity
97+ key = { workout . id }
98+ style = { styles . workoutCard }
99+ onPress = { ( ) => handleWorkoutPress ( workout ) }
100+ >
101+ < View style = { styles . workoutHeader } >
102+ < Text style = { styles . workoutName } > { workout . name } </ Text >
103+ < View style = { [ styles . statusBadge , { backgroundColor : getStatusColor ( workout ) } ] } >
104+ < Text style = { styles . statusText } > { getCompletionStatus ( workout ) } </ Text >
105+ </ View >
106+ </ View >
107+
108+ < Text style = { styles . workoutDate } >
109+ { formatDate ( workout . completed_date || workout . created_at ) }
110+ </ Text >
111+
112+ < View style = { styles . workoutDetails } >
113+ < View style = { styles . detailItem } >
114+ < Text style = { styles . detailLabel } > Duration</ Text >
115+ < Text style = { styles . detailValue } > { formatDuration ( workout . duration_minutes ) } </ Text >
116+ </ View >
117+
118+ < View style = { styles . detailItem } >
119+ < Text style = { styles . detailLabel } > Exercises</ Text >
120+ < Text style = { styles . detailValue } > { workout . exercise_count || 0 } </ Text >
121+ </ View >
122+ </ View >
123+
124+ { workout . description && (
125+ < Text style = { styles . workoutDescription } numberOfLines = { 2 } >
126+ { workout . description }
127+ </ Text >
128+ ) }
129+
130+ < View style = { styles . viewDetailsContainer } >
131+ < Text style = { styles . viewDetailsText } > Tap to view exercises →</ Text >
132+ </ View >
133+ </ TouchableOpacity >
134+ ) ;
135+
136+ const renderEmptyState = ( ) => (
137+ < View style = { styles . emptyState } >
138+ < Text style = { styles . emptyTitle } > No Workouts Yet</ Text >
139+ < Text style = { styles . emptyMessage } >
140+ Start your fitness journey by completing your first workout!
141+ </ Text >
142+ </ View >
143+ ) ;
144+
145+ const renderErrorState = ( ) => (
146+ < View style = { styles . errorState } >
147+ < Text style = { styles . errorTitle } > Unable to Load Workouts</ Text >
148+ < Text style = { styles . errorMessage } > { error } </ Text >
149+ < TouchableOpacity style = { styles . retryButton } onPress = { ( ) => fetchWorkouts ( ) } >
150+ < Text style = { styles . retryText } > Try Again</ Text >
151+ </ TouchableOpacity >
152+ </ View >
153+ ) ;
154+
155+ if ( loading && ! refreshing ) {
156+ return (
157+ < SafeAreaView style = { styles . container } >
158+ < View style = { styles . loadingContainer } >
159+ < ActivityIndicator size = "large" color = "#70A1FF" />
160+ < Text style = { styles . loadingText } > Loading workout history...</ Text >
161+ </ View >
162+ </ SafeAreaView >
163+ ) ;
164+ }
165+
166+ return (
167+ < SafeAreaView style = { styles . container } >
168+ < View style = { styles . header } >
169+ < Text style = { styles . headerTitle } > Workout History</ Text >
170+ < Text style = { styles . headerSubtitle } >
171+ { workouts . length } { workouts . length === 1 ? 'workout' : 'workouts' } completed
172+ </ Text >
173+ </ View >
174+
175+ < ScrollView
176+ style = { styles . scrollView }
177+ contentContainerStyle = { styles . scrollContent }
178+ refreshControl = {
179+ < RefreshControl refreshing = { refreshing } onRefresh = { onRefresh } />
180+ }
181+ >
182+ { error ? (
183+ renderErrorState ( )
184+ ) : ! Array . isArray ( workouts ) || workouts . length === 0 ? (
185+ renderEmptyState ( )
186+ ) : (
187+ workouts . map ( renderWorkoutItem )
188+ ) }
189+ </ ScrollView >
190+ </ SafeAreaView >
191+ ) ;
192+ } ;
193+
194+ const styles = StyleSheet . create ( {
195+ container : {
196+ flex : 1 ,
197+ backgroundColor : '#FFFFFF' ,
198+ } ,
199+ header : {
200+ padding : 20 ,
201+ backgroundColor : '#F8F9FA' ,
202+ borderBottomWidth : 1 ,
203+ borderBottomColor : '#E9ECEF' ,
204+ } ,
205+ headerTitle : {
206+ fontSize : 28 ,
207+ fontWeight : 'bold' ,
208+ color : '#333' ,
209+ marginBottom : 4 ,
210+ } ,
211+ headerSubtitle : {
212+ fontSize : 16 ,
213+ color : '#666' ,
214+ } ,
215+ scrollView : {
216+ flex : 1 ,
217+ } ,
218+ scrollContent : {
219+ padding : 16 ,
220+ paddingBottom : 80 ,
221+ } ,
222+ loadingContainer : {
223+ flex : 1 ,
224+ justifyContent : 'center' ,
225+ alignItems : 'center' ,
226+ } ,
227+ loadingText : {
228+ marginTop : 16 ,
229+ fontSize : 16 ,
230+ color : '#666' ,
231+ } ,
232+ workoutCard : {
233+ backgroundColor : '#FFFFFF' ,
234+ borderRadius : 16 ,
235+ padding : 16 ,
236+ marginBottom : 16 ,
237+ shadowColor : '#000' ,
238+ shadowOffset : {
239+ width : 0 ,
240+ height : 2 ,
241+ } ,
242+ shadowOpacity : 0.1 ,
243+ shadowRadius : 4 ,
244+ elevation : 3 ,
245+ borderWidth : 1 ,
246+ borderColor : '#F0F0F0' ,
247+ } ,
248+ workoutHeader : {
249+ flexDirection : 'row' ,
250+ justifyContent : 'space-between' ,
251+ alignItems : 'center' ,
252+ marginBottom : 8 ,
253+ } ,
254+ workoutName : {
255+ fontSize : 18 ,
256+ fontWeight : 'bold' ,
257+ color : '#333' ,
258+ flex : 1 ,
259+ marginRight : 12 ,
260+ } ,
261+ statusBadge : {
262+ paddingHorizontal : 8 ,
263+ paddingVertical : 4 ,
264+ borderRadius : 12 ,
265+ } ,
266+ statusText : {
267+ color : '#FFFFFF' ,
268+ fontSize : 12 ,
269+ fontWeight : '600' ,
270+ } ,
271+ workoutDate : {
272+ fontSize : 14 ,
273+ color : '#666' ,
274+ marginBottom : 12 ,
275+ } ,
276+ workoutDetails : {
277+ flexDirection : 'row' ,
278+ justifyContent : 'space-between' ,
279+ marginBottom : 8 ,
280+ } ,
281+ detailItem : {
282+ flex : 1 ,
283+ } ,
284+ detailLabel : {
285+ fontSize : 12 ,
286+ color : '#999' ,
287+ marginBottom : 2 ,
288+ } ,
289+ detailValue : {
290+ fontSize : 16 ,
291+ fontWeight : '600' ,
292+ color : '#333' ,
293+ } ,
294+ workoutDescription : {
295+ fontSize : 14 ,
296+ color : '#666' ,
297+ fontStyle : 'italic' ,
298+ marginTop : 8 ,
299+ } ,
300+ emptyState : {
301+ flex : 1 ,
302+ justifyContent : 'center' ,
303+ alignItems : 'center' ,
304+ paddingVertical : 60 ,
305+ } ,
306+ emptyTitle : {
307+ fontSize : 24 ,
308+ fontWeight : 'bold' ,
309+ color : '#333' ,
310+ marginBottom : 8 ,
311+ } ,
312+ emptyMessage : {
313+ fontSize : 16 ,
314+ color : '#666' ,
315+ textAlign : 'center' ,
316+ lineHeight : 24 ,
317+ } ,
318+ errorState : {
319+ flex : 1 ,
320+ justifyContent : 'center' ,
321+ alignItems : 'center' ,
322+ paddingVertical : 60 ,
323+ } ,
324+ errorTitle : {
325+ fontSize : 20 ,
326+ fontWeight : 'bold' ,
327+ color : '#FF6B6B' ,
328+ marginBottom : 8 ,
329+ } ,
330+ errorMessage : {
331+ fontSize : 16 ,
332+ color : '#666' ,
333+ textAlign : 'center' ,
334+ marginBottom : 20 ,
335+ lineHeight : 24 ,
336+ } ,
337+ retryButton : {
338+ backgroundColor : '#70A1FF' ,
339+ paddingHorizontal : 24 ,
340+ paddingVertical : 12 ,
341+ borderRadius : 12 ,
342+ } ,
343+ retryText : {
344+ color : '#FFFFFF' ,
345+ fontSize : 16 ,
346+ fontWeight : '600' ,
347+ } ,
348+ viewDetailsContainer : {
349+ marginTop : 8 ,
350+ paddingTop : 8 ,
351+ borderTopWidth : 1 ,
352+ borderTopColor : '#F0F0F0' ,
353+ alignItems : 'center' ,
354+ } ,
355+ viewDetailsText : {
356+ fontSize : 14 ,
357+ color : '#70A1FF' ,
358+ fontWeight : '600' ,
359+ } ,
360+ } ) ;
361+
362+ export default WorkoutHistoryScreen ;
0 commit comments