Skip to content

Commit 38c0494

Browse files
authored
Merge pull request #3 from KushK04/historytabandprofilefieldchanges
Historytabandprofilefieldchanges
2 parents 9ff783c + 2442a40 commit 38c0494

File tree

12 files changed

+1403
-4
lines changed

12 files changed

+1403
-4
lines changed

KonditionExpo/app/(tabs)/_layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export default function TabLayout() {
4040
tabBarIcon: ({ color }) => <IconSymbol size={28} name="chart.line.uptrend.xyaxis" color={color} />,
4141
}}
4242
/>
43+
<Tabs.Screen
44+
name="history"
45+
options={{
46+
title: 'History',
47+
tabBarIcon: ({ color }) => <IconSymbol size={28} name="clock.fill" color={color} />,
48+
}}
49+
/>
4350
<Tabs.Screen
4451
name="profile"
4552
options={{
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
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

Comments
 (0)