mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-20 06:25:07 +00:00
327 lines
11 KiB
TypeScript
327 lines
11 KiB
TypeScript
// app/(tabs)/history/workoutHistory.tsx
|
|
import React, { useState, useEffect } from 'react';
|
|
import { View, ScrollView, ActivityIndicator, RefreshControl, Pressable } from 'react-native';
|
|
import { Text } from '@/components/ui/text';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useSQLiteContext } from 'expo-sqlite';
|
|
import { Workout } from '@/types/workout';
|
|
import { format } from 'date-fns';
|
|
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
|
|
import WorkoutCard from '@/components/workout/WorkoutCard';
|
|
import NostrLoginSheet from '@/components/sheets/NostrLoginSheet';
|
|
import { useWorkoutHistory } from '@/lib/hooks/useWorkoutHistory';
|
|
|
|
// Define colors for icons and buttons
|
|
const primaryColor = "#8b5cf6"; // Purple color
|
|
const mutedColor = "#9ca3af"; // Gray color
|
|
const primaryBgColor = "#8b5cf6"; // Purple background
|
|
const primaryTextColor = "#ffffff"; // White text for purple background
|
|
const mutedBgColor = "#f3f4f6"; // Light gray background
|
|
const mutedTextColor = "#6b7280"; // Dark gray text for light background
|
|
|
|
// Mock data for when database tables aren't yet created
|
|
const mockWorkouts: Workout[] = [
|
|
{
|
|
id: '1',
|
|
title: 'Push 1',
|
|
type: 'strength',
|
|
exercises: [
|
|
{
|
|
id: 'ex1',
|
|
exerciseId: 'bench-press',
|
|
title: 'Bench Press',
|
|
type: 'strength',
|
|
category: 'Push',
|
|
sets: [],
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-07T10:00:00').getTime(),
|
|
lastUpdated: new Date('2025-03-07T10:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
tags: ['compound', 'push']
|
|
},
|
|
{
|
|
id: 'ex2',
|
|
exerciseId: 'shoulder-press',
|
|
title: 'Shoulder Press',
|
|
type: 'strength',
|
|
category: 'Push',
|
|
sets: [],
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-07T10:00:00').getTime(),
|
|
lastUpdated: new Date('2025-03-07T10:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
tags: ['compound', 'push']
|
|
},
|
|
{
|
|
id: 'ex3',
|
|
exerciseId: 'tricep-extension',
|
|
title: 'Tricep Extension',
|
|
type: 'strength',
|
|
category: 'Push',
|
|
sets: [],
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-07T10:00:00').getTime(),
|
|
lastUpdated: new Date('2025-03-07T10:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
tags: ['isolation', 'push']
|
|
}
|
|
],
|
|
startTime: new Date('2025-03-07T10:00:00').getTime(),
|
|
endTime: new Date('2025-03-07T11:47:00').getTime(),
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-07T10:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
totalVolume: 9239
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Pull 1',
|
|
type: 'strength',
|
|
exercises: [
|
|
{
|
|
id: 'ex4',
|
|
exerciseId: 'pull-up',
|
|
title: 'Pull Up',
|
|
type: 'strength',
|
|
category: 'Pull',
|
|
sets: [],
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-05T14:00:00').getTime(),
|
|
lastUpdated: new Date('2025-03-05T14:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
tags: ['compound', 'pull']
|
|
},
|
|
{
|
|
id: 'ex5',
|
|
exerciseId: 'barbell-row',
|
|
title: 'Barbell Row',
|
|
type: 'strength',
|
|
category: 'Pull',
|
|
sets: [],
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-05T14:00:00').getTime(),
|
|
lastUpdated: new Date('2025-03-05T14:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
tags: ['compound', 'pull']
|
|
}
|
|
],
|
|
startTime: new Date('2025-03-05T14:00:00').getTime(),
|
|
endTime: new Date('2025-03-05T15:36:00').getTime(),
|
|
isCompleted: true,
|
|
created_at: new Date('2025-03-05T14:00:00').getTime(),
|
|
availability: { source: ['local'] },
|
|
totalVolume: 1396
|
|
}
|
|
];
|
|
|
|
// Group workouts by month
|
|
const groupWorkoutsByMonth = (workouts: Workout[]) => {
|
|
const grouped: Record<string, Workout[]> = {};
|
|
|
|
workouts.forEach(workout => {
|
|
const monthKey = format(workout.startTime, 'MMMM yyyy');
|
|
if (!grouped[monthKey]) {
|
|
grouped[monthKey] = [];
|
|
}
|
|
grouped[monthKey].push(workout);
|
|
});
|
|
|
|
return Object.entries(grouped);
|
|
};
|
|
|
|
export default function HistoryScreen() {
|
|
const db = useSQLiteContext();
|
|
const { isAuthenticated } = useNDKCurrentUser();
|
|
const [workouts, setWorkouts] = useState<Workout[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [useMockData, setUseMockData] = useState(false);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [includeNostr, setIncludeNostr] = useState(true);
|
|
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
|
|
|
|
// Create memoized filters to prevent recreation on every render
|
|
const filters = React.useMemo(() => {
|
|
if (includeNostr) {
|
|
return undefined;
|
|
} else {
|
|
// Explicitly type the array to match WorkoutFilters interface
|
|
return { source: ['local' as const] };
|
|
}
|
|
}, [includeNostr]);
|
|
|
|
// Use the unified workout history hook
|
|
const {
|
|
workouts: allWorkouts,
|
|
loading,
|
|
refreshing: hookRefreshing,
|
|
refresh,
|
|
error
|
|
} = useWorkoutHistory({
|
|
includeNostr,
|
|
filters,
|
|
realtime: true
|
|
});
|
|
|
|
// Set workouts from the hook with more careful state updates
|
|
useEffect(() => {
|
|
if (loading) {
|
|
setIsLoading(true);
|
|
return; // Exit early to avoid multiple state updates in one effect
|
|
}
|
|
|
|
setIsLoading(false);
|
|
setRefreshing(false);
|
|
|
|
// Check if we need to use mock data (empty workouts)
|
|
if (allWorkouts.length === 0 && !error) {
|
|
console.log('No workouts found, using mock data');
|
|
setWorkouts(mockWorkouts);
|
|
setUseMockData(true);
|
|
} else {
|
|
setWorkouts(allWorkouts);
|
|
setUseMockData(false);
|
|
}
|
|
}, [allWorkouts, loading, error, mockWorkouts]);
|
|
|
|
// Pull to refresh handler
|
|
const onRefresh = React.useCallback(() => {
|
|
setRefreshing(true);
|
|
refresh();
|
|
}, [refresh]);
|
|
|
|
// Group workouts by month
|
|
const groupedWorkouts = groupWorkoutsByMonth(workouts);
|
|
|
|
return (
|
|
<View className="flex-1 bg-background">
|
|
<ScrollView
|
|
className="flex-1"
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
>
|
|
{/* Nostr Login Prompt */}
|
|
{!isAuthenticated && (
|
|
<View className="mx-4 mt-4 p-4 bg-primary/5 rounded-lg border border-primary/20">
|
|
<Text className="text-foreground font-medium mb-2">
|
|
Connect with Nostr
|
|
</Text>
|
|
<Text className="text-muted-foreground mb-4">
|
|
Login with Nostr to see your workouts from other devices and back up your workout history.
|
|
</Text>
|
|
<Button
|
|
variant="purple"
|
|
onPress={() => setIsLoginSheetOpen(true)}
|
|
className="w-full"
|
|
>
|
|
<Text className="text-white">Login with Nostr</Text>
|
|
</Button>
|
|
</View>
|
|
)}
|
|
{isLoading && !refreshing ? (
|
|
<View className="items-center justify-center py-20">
|
|
<ActivityIndicator size="large" className="mb-4" />
|
|
<Text className="text-muted-foreground">Loading workout history...</Text>
|
|
</View>
|
|
) : workouts.length === 0 ? (
|
|
<View className="items-center justify-center py-20">
|
|
<Text className="text-muted-foreground">No workouts recorded yet.</Text>
|
|
<Text className="text-muted-foreground mt-2">
|
|
Complete a workout to see it here.
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
// Display grouped workouts by month
|
|
<View className="p-4">
|
|
{useMockData && (
|
|
<View className="bg-primary/5 rounded-lg p-4 mb-4 border border-border">
|
|
<Text className="text-muted-foreground text-sm">
|
|
Showing example data. Your completed workouts will appear here.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{isAuthenticated && (
|
|
<View className="mb-4">
|
|
<View className="flex-row justify-between items-center">
|
|
<Text className="text-foreground text-sm font-medium">
|
|
Workout Source
|
|
</Text>
|
|
|
|
<View className="flex-row border border-border rounded-full overflow-hidden">
|
|
<Pressable
|
|
onPress={() => setIncludeNostr(true)}
|
|
style={{
|
|
backgroundColor: includeNostr ? primaryBgColor : 'transparent',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
}}
|
|
>
|
|
<Text style={{
|
|
color: includeNostr ? primaryTextColor : mutedTextColor,
|
|
fontSize: 14,
|
|
fontWeight: includeNostr ? '600' : '400',
|
|
}}>
|
|
All Workouts
|
|
</Text>
|
|
</Pressable>
|
|
|
|
<Pressable
|
|
onPress={() => setIncludeNostr(false)}
|
|
style={{
|
|
backgroundColor: !includeNostr ? primaryBgColor : 'transparent',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
}}
|
|
>
|
|
<Text style={{
|
|
color: !includeNostr ? primaryTextColor : mutedTextColor,
|
|
fontSize: 14,
|
|
fontWeight: !includeNostr ? '600' : '400',
|
|
}}>
|
|
Local Only
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
|
|
<Text className="text-muted-foreground text-xs mt-1">
|
|
{includeNostr
|
|
? 'Showing all workouts from Nostr and your local device'
|
|
: 'Only showing workouts saved on this device'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{groupedWorkouts.map(([month, monthWorkouts]) => (
|
|
<View key={month} className="mb-6">
|
|
<Text className="text-foreground text-xl font-semibold mb-4">
|
|
{month.toUpperCase()}
|
|
</Text>
|
|
|
|
{monthWorkouts.map((workout) => (
|
|
<WorkoutCard
|
|
key={workout.id}
|
|
workout={workout}
|
|
showDate={true}
|
|
showExercises={true}
|
|
/>
|
|
))}
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{/* Add bottom padding for better scrolling experience */}
|
|
<View className="h-20" />
|
|
</ScrollView>
|
|
|
|
{/* Nostr Login Sheet */}
|
|
<NostrLoginSheet
|
|
open={isLoginSheetOpen}
|
|
onClose={() => setIsLoginSheetOpen(false)}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|