diff --git a/app/(tabs)/history.tsx b/app/(tabs)/history.tsx deleted file mode 100644 index 34aeee1..0000000 --- a/app/(tabs)/history.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// app/(tabs)/social.tsx -import { View } from 'react-native'; -import { Text } from '@/components/ui/text'; -import { Button } from '@/components/ui/button'; -import { Bell } from 'lucide-react-native'; -import Header from '@/components/Header'; -import { TabScreen } from '@/components/layout/TabScreen'; - -export default function SocialScreen() { - return ( - -
console.log('Open notifications')} - > - - - - - - } - /> - - Social Screen - - - ); -} \ No newline at end of file diff --git a/app/(tabs)/history/_layout.tsx b/app/(tabs)/history/_layout.tsx new file mode 100644 index 0000000..a05eb92 --- /dev/null +++ b/app/(tabs)/history/_layout.tsx @@ -0,0 +1,56 @@ +// app/(tabs)/history/_layout.tsx +import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; +import HistoryScreen from '@/app/(tabs)/history/workoutHistory'; +import CalendarScreen from '@/app/(tabs)/history/calendar'; +import Header from '@/components/Header'; +import { useTheme } from '@react-navigation/native'; +import type { CustomTheme } from '@/lib/theme'; +import { TabScreen } from '@/components/layout/TabScreen'; + +const Tab = createMaterialTopTabNavigator(); + +export default function HistoryLayout() { + const theme = useTheme() as CustomTheme; + + return ( + +
+ + + + + + + ); +} \ No newline at end of file diff --git a/app/(tabs)/history/calendar.tsx b/app/(tabs)/history/calendar.tsx new file mode 100644 index 0000000..0afc910 --- /dev/null +++ b/app/(tabs)/history/calendar.tsx @@ -0,0 +1,381 @@ +// app/(tabs)/history/calendar.tsx +import React, { useState, useEffect } from 'react'; +import { View, ScrollView, ActivityIndicator, TouchableOpacity, Pressable, RefreshControl } from 'react-native'; +import { Text } from '@/components/ui/text'; +import { useSQLiteContext } from 'expo-sqlite'; +import { Workout } from '@/types/workout'; +import { format, isSameDay } from 'date-fns'; +import { Card, CardContent } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; +import { WorkoutHistoryService } from '@/lib/db/services/WorkoutHIstoryService'; +import WorkoutCard from '@/components/workout/WorkoutCard'; +import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react-native'; + +// Add custom styles for 1/7 width (for calendar days) +const styles = { + calendarDay: "w-[14.28%]" +}; + +// Week days for the calendar view - Fixed the duplicate key issue +const WEEK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + +// Mock data for when database tables aren't yet created +const mockWorkouts: Workout[] = [ + { + id: '1', + title: 'Push 1', + type: 'strength', + exercises: [], + 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: [], + 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 + } +]; + +export default function CalendarScreen() { + const db = useSQLiteContext(); + const [workouts, setWorkouts] = useState([]); + const [selectedMonth, setSelectedMonth] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [isLoading, setIsLoading] = useState(true); + const [workoutDates, setWorkoutDates] = useState>(new Set()); + const [useMockData, setUseMockData] = useState(false); + const [refreshing, setRefreshing] = useState(false); + + // Initialize workout history service + const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]); + + // Load workouts function + const loadWorkouts = async () => { + try { + setIsLoading(true); + const allWorkouts = await workoutHistoryService.getAllWorkouts(); + setWorkouts(allWorkouts); + setUseMockData(false); + } catch (error) { + console.error('Error loading workouts:', error); + + // Check if the error is about missing tables + const errorMsg = error instanceof Error ? error.message : String(error); + if (errorMsg.includes('no such table')) { + console.log('Using mock data because workout tables not yet created'); + setWorkouts(mockWorkouts); + setUseMockData(true); + } else { + // For other errors, just show empty state + setWorkouts([]); + setUseMockData(false); + } + } finally { + setIsLoading(false); + setRefreshing(false); + } + }; + + // Initial load workouts + useEffect(() => { + loadWorkouts(); + }, [workoutHistoryService]); + + // Pull to refresh handler + const onRefresh = React.useCallback(() => { + setRefreshing(true); + loadWorkouts(); + }, []); + + // Load workout dates for selected month + useEffect(() => { + const getWorkoutDatesForMonth = async () => { + try { + const year = selectedMonth.getFullYear(); + const month = selectedMonth.getMonth(); + + let dates: Date[] = []; + + // If we're using mock data, filter from mock workouts + if (useMockData) { + dates = workouts + .filter(workout => { + const date = new Date(workout.startTime); + return date.getFullYear() === year && date.getMonth() === month; + }) + .map(workout => new Date(workout.startTime)); + } else { + // Try to use the service method if it exists and table exists + try { + if (typeof workoutHistoryService.getWorkoutDatesInMonth === 'function') { + dates = await workoutHistoryService.getWorkoutDatesInMonth(year, month); + } else { + // Otherwise filter from loaded workouts + dates = workouts + .filter(workout => { + const date = new Date(workout.startTime); + return date.getFullYear() === year && date.getMonth() === month; + }) + .map(workout => new Date(workout.startTime)); + } + } catch (error) { + console.error('Error getting workout dates:', error); + // If table doesn't exist, use empty array + dates = []; + } + } + + // Convert to strings for the Set + const dateStrings = dates.map(date => format(date, 'yyyy-MM-dd')); + setWorkoutDates(new Set(dateStrings)); + } catch (error) { + console.error('Error getting workout dates:', error); + setWorkoutDates(new Set()); + } + }; + + getWorkoutDatesForMonth(); + }, [selectedMonth, workouts, workoutHistoryService, useMockData]); + + // Get dates for current month's calendar + const getDaysInMonth = (year: number, month: number) => { + const date = new Date(year, month, 1); + const days: (Date | null)[] = []; + let day = 1; + + // Get the day of week for the first day (0 = Sunday, 1 = Monday, etc.) + const firstDayOfWeek = date.getDay() === 0 ? 6 : date.getDay() - 1; // Convert to Monday-based + + // Add empty days for the beginning of the month + for (let i = 0; i < firstDayOfWeek; i++) { + days.push(null); + } + + // Add all days in the month + while (date.getMonth() === month) { + days.push(new Date(year, month, day)); + day++; + date.setDate(day); + } + + return days; + }; + + const daysInMonth = getDaysInMonth( + selectedMonth.getFullYear(), + selectedMonth.getMonth() + ); + + const goToPreviousMonth = () => { + setSelectedMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1, 1)); + }; + + const goToNextMonth = () => { + setSelectedMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1, 1)); + }; + + // Check if a date has workouts + const hasWorkout = (date: Date | null) => { + if (!date) return false; + return workoutDates.has(format(date, 'yyyy-MM-dd')); + }; + + // Handle date selection in calendar + const handleDateSelect = (date: Date | null) => { + if (!date) return; + setSelectedDate(date); + }; + + // Get workouts for selected date + const [selectedDateWorkouts, setSelectedDateWorkouts] = useState([]); + const [loadingDateWorkouts, setLoadingDateWorkouts] = useState(false); + + useEffect(() => { + const loadWorkoutsForDate = async () => { + try { + setLoadingDateWorkouts(true); + + if (useMockData) { + // Use mock data filtering + const filtered = workouts.filter(workout => + isSameDay(new Date(workout.startTime), selectedDate) + ); + setSelectedDateWorkouts(filtered); + } else { + try { + if (typeof workoutHistoryService.getWorkoutsByDate === 'function') { + // Use the service method if available + const dateWorkouts = await workoutHistoryService.getWorkoutsByDate(selectedDate); + setSelectedDateWorkouts(dateWorkouts); + } else { + // Fall back to filtering the already loaded workouts + const filtered = workouts.filter(workout => + isSameDay(new Date(workout.startTime), selectedDate) + ); + setSelectedDateWorkouts(filtered); + } + } catch (error) { + // Handle the case where the workout table doesn't exist + console.error('Error loading workouts for date:', error); + setSelectedDateWorkouts([]); + } + } + } catch (error) { + console.error('Error loading workouts for date:', error); + setSelectedDateWorkouts([]); + } finally { + setLoadingDateWorkouts(false); + } + }; + + loadWorkoutsForDate(); + }, [selectedDate, workouts, workoutHistoryService, useMockData]); + + return ( + + + } + > + + {useMockData && ( + + + Showing example data. Your completed workouts will appear here. + + + )} + + {/* Calendar section */} + + + + Workout Calendar + + + + + {/* Month navigation */} + + + + + + + {format(selectedMonth, 'MMMM yyyy')} + + + + + + + + {/* Week days header - Fixed with unique keys */} + + {WEEK_DAYS.map(day => ( + + + {day.charAt(0)} + + + ))} + + + {/* Calendar grid */} + + {daysInMonth.map((date, index) => ( + date && handleDateSelect(date)} + > + + {date ? ( + + + {date.getDate()} + + + ) : ( + + )} + + + ))} + + + + + + {/* Selected date workouts */} + + + {format(selectedDate, 'MMMM d, yyyy')} + + + {isLoading || loadingDateWorkouts ? ( + + + Loading workouts... + + ) : selectedDateWorkouts.length === 0 ? ( + + No workouts on this date + {!useMockData && ( + + Complete a workout on this day to see it here + + )} + + ) : ( + + {selectedDateWorkouts.map(workout => ( + + ))} + + )} + + + + {/* Add bottom padding for better scrolling experience */} + + + + ); +} \ No newline at end of file diff --git a/app/(tabs)/history/workoutHistory.tsx b/app/(tabs)/history/workoutHistory.tsx new file mode 100644 index 0000000..b52ca2e --- /dev/null +++ b/app/(tabs)/history/workoutHistory.tsx @@ -0,0 +1,160 @@ +// app/(tabs)/history/workoutHistory.tsx +import React, { useState, useEffect } from 'react'; +import { View, ScrollView, ActivityIndicator, RefreshControl } from 'react-native'; +import { Text } from '@/components/ui/text'; +import { useSQLiteContext } from 'expo-sqlite'; +import { Workout } from '@/types/workout'; +import { format } from 'date-fns'; +import { WorkoutHistoryService } from '@/lib/db/services/WorkoutHIstoryService'; +import WorkoutCard from '@/components/workout/WorkoutCard'; + +// Mock data for when database tables aren't yet created +const mockWorkouts: Workout[] = [ + { + id: '1', + title: 'Push 1', + type: 'strength', + exercises: [], + 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: [], + 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 = {}; + + 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 [workouts, setWorkouts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [useMockData, setUseMockData] = useState(false); + const [refreshing, setRefreshing] = useState(false); + + // Initialize workout history service + const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]); + + // Load workouts + const loadWorkouts = async () => { + try { + setIsLoading(true); + const allWorkouts = await workoutHistoryService.getAllWorkouts(); + setWorkouts(allWorkouts); + setUseMockData(false); + } catch (error) { + console.error('Error loading workouts:', error); + + // Check if the error is about missing tables + const errorMsg = error instanceof Error ? error.message : String(error); + if (errorMsg.includes('no such table')) { + console.log('Using mock data because workout tables not yet created'); + setWorkouts(mockWorkouts); + setUseMockData(true); + } else { + // For other errors, just show empty state + setWorkouts([]); + setUseMockData(false); + } + } finally { + setIsLoading(false); + setRefreshing(false); + } + }; + + // Initial load + useEffect(() => { + loadWorkouts(); + }, [workoutHistoryService]); + + // Pull to refresh handler + const onRefresh = React.useCallback(() => { + setRefreshing(true); + loadWorkouts(); + }, []); + + // Group workouts by month + const groupedWorkouts = groupWorkoutsByMonth(workouts); + + return ( + + + } + > + {isLoading && !refreshing ? ( + + + Loading workout history... + + ) : workouts.length === 0 ? ( + + No workouts recorded yet. + + Complete a workout to see it here. + + + ) : ( + // Display grouped workouts by month + + {useMockData && ( + + + Showing example data. Your completed workouts will appear here. + + + )} + + {groupedWorkouts.map(([month, monthWorkouts]) => ( + + + {month.toUpperCase()} + + + {monthWorkouts.map((workout) => ( + + ))} + + ))} + + )} + + {/* Add bottom padding for better scrolling experience */} + + + + ); +} \ No newline at end of file diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index bc3dafc..94aa780 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ // app/(tabs)/index.tsx import React, { useState, useEffect, useCallback } from 'react'; -import { ScrollView, View } from 'react-native' +import { ScrollView, View, TouchableOpacity } from 'react-native' import { useFocusEffect } from '@react-navigation/native'; import { router } from 'expo-router' import { @@ -20,7 +20,7 @@ import FavoriteTemplate from '@/components/workout/FavoriteTemplate' import { useWorkoutStore } from '@/stores/workoutStore' import { Text } from '@/components/ui/text' import { getRandomWorkoutTitle } from '@/utils/workoutTitles' -import { Bell } from 'lucide-react-native'; +import { Bell, Star, Clock, Dumbbell } from 'lucide-react-native'; import { Button } from '@/components/ui/button'; interface FavoriteTemplateData { @@ -44,6 +44,8 @@ type PendingWorkoutAction = | { type: 'quick-start' } | { type: 'template', templateId: string } | { type: 'template-select' }; + +const purpleColor = 'hsl(261, 90%, 66%)'; export default function WorkoutScreen() { const { startWorkout } = useWorkoutStore.getState(); @@ -204,6 +206,30 @@ export default function WorkoutScreen() { } }; + // Helper function to format time ago + const getTimeAgo = (timestamp: number) => { + const now = Date.now(); + const diffInSeconds = Math.floor((now - timestamp) / 1000); + + if (diffInSeconds < 60) return 'Just now'; + + const diffInMinutes = Math.floor(diffInSeconds / 60); + if (diffInMinutes < 60) return `${diffInMinutes} min ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours} hr ago`; + + const diffInDays = Math.floor(diffInHours / 24); + if (diffInDays === 1) return 'Yesterday'; + if (diffInDays < 30) return `${diffInDays} days ago`; + + const diffInMonths = Math.floor(diffInDays / 30); + if (diffInMonths === 1) return '1 month ago'; + if (diffInMonths < 12) return `${diffInMonths} months ago`; + + return 'Over a year ago'; + }; + return (
- + + {/* Start a Workout section - without the outer box */} + + Start a Workout + + Begin a new workout or choose from one of your templates. + - {/* Favorites section */} - - - Favorites - - - {isLoadingFavorites ? ( - - - Loading favorites... - - - ) : favoriteWorkouts.length === 0 ? ( - - - Star workouts from your library to see them here - - - ) : ( - - {favoriteWorkouts.map(template => ( - handleStartWorkout(template.id)} - onFavoritePress={() => handleFavoritePress(template.id)} - /> - ))} - - )} - - + {/* Buttons from HomeWorkout but directly here */} + + + + + + + + {/* Favorites section with adjusted grid layout */} + + Favorites + + {isLoadingFavorites ? ( + + + Loading favorites... + + + ) : favoriteWorkouts.length === 0 ? ( + + + Star workouts from your library to see them here + + + ) : ( + + {favoriteWorkouts.map(template => ( + handleStartWorkout(template.id)} + className="w-[48%] mb-3" + style={{ aspectRatio: 1 / 0.85 }} // Slightly less tall than a square + > + + + {/* Top section with title and star */} + + + + {template.title} + + + { + e.stopPropagation(); + handleFavoritePress(template.id); + }} + > + + + + + {/* First 2 exercises */} + + {template.exercises.slice(0, 2).map((exercise, index) => ( + + • {exercise.title} + + ))} + {template.exercises.length > 2 && ( + + +{template.exercises.length - 2} more + + )} + + + + {/* Stats row */} + + + + + + {template.exerciseCount} + + + + {template.duration && ( + + + + {Math.round(template.duration / 60)} min + + + )} + + + {/* Last performed info */} + {template.lastUsed && ( + + + + Last: {getTimeAgo(template.lastUsed)} + + + )} + + + + + ))} + + )} + diff --git a/components/workout/WorkoutCard.tsx b/components/workout/WorkoutCard.tsx new file mode 100644 index 0000000..4362cd6 --- /dev/null +++ b/components/workout/WorkoutCard.tsx @@ -0,0 +1,116 @@ +// components/workout/WorkoutCard.tsx +import React from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { ChevronRight } from 'lucide-react-native'; +import { Card, CardContent } from '@/components/ui/card'; +import { Workout } from '@/types/workout'; +import { format } from 'date-fns'; +import { useRouter } from 'expo-router'; + +interface WorkoutCardProps { + workout: Workout; + showDate?: boolean; + showExercises?: boolean; +} + +// Calculate duration in hours and minutes +const formatDuration = (startTime: number, endTime: number) => { + const durationMs = endTime - startTime; + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } + return `${minutes}m`; +}; + +export const WorkoutCard: React.FC = ({ + workout, + showDate = true, + showExercises = true +}) => { + const router = useRouter(); + + const handlePress = () => { + // Navigate to workout details + console.log(`Navigate to workout ${workout.id}`); + // Implement navigation when endpoint is available + // router.push(`/workout/${workout.id}`); + }; + + return ( + + + + {workout.title} + + + + + + {showDate && ( + + {format(workout.startTime, 'EEEE, MMM d')} + + )} + + + + + ⏱️ + + + {formatDuration(workout.startTime, workout.endTime || Date.now())} + + + + + + ⚖️ + + + {workout.totalVolume ? `${workout.totalVolume} lb` : '0 lb'} + + + + {workout.totalReps && ( + + + 🔄 + + + {workout.totalReps} reps + + + )} + + + {/* Show exercises if requested */} + {showExercises && ( + + Exercise + {/* In a real implementation, you would map through actual exercises */} + {workout.exercises && workout.exercises.length > 0 ? ( + workout.exercises.slice(0, 3).map((exercise, idx) => ( + + {exercise.title} + + )) + ) : ( + No exercises recorded + )} + + {workout.exercises && workout.exercises.length > 3 && ( + + +{workout.exercises.length - 3} more exercises + + )} + + )} + + + ); +}; + +export default WorkoutCard; \ No newline at end of file diff --git a/lib/db/services/WorkoutHIstoryService.ts b/lib/db/services/WorkoutHIstoryService.ts new file mode 100644 index 0000000..ce4003e --- /dev/null +++ b/lib/db/services/WorkoutHIstoryService.ts @@ -0,0 +1,198 @@ +// lib/db/services/WorkoutHistoryService.ts +import { SQLiteDatabase } from 'expo-sqlite'; +import { Workout } from '@/types/workout'; +import { format } from 'date-fns'; +import { DbService } from '../db-service'; + +export class WorkoutHistoryService { + private db: DbService; + + constructor(database: SQLiteDatabase) { + this.db = new DbService(database); + } + + /** + * Get all workouts, sorted by date in descending order + */ + async getAllWorkouts(): Promise { + try { + const workouts = await this.db.getAllAsync<{ + id: string; + title: string; + type: string; + start_time: number; + end_time: number; + is_completed: number; + created_at: number; + last_updated: number; + template_id: string | null; + total_volume: number | null; + total_reps: number | null; + source: string; + }>( + `SELECT * FROM workouts + ORDER BY start_time DESC` + ); + + // Transform database records to Workout objects + return workouts.map(row => this.mapRowToWorkout(row)); + } catch (error) { + console.error('Error getting workouts:', error); + throw error; + } + } + + /** + * Get workouts for a specific date + */ + async getWorkoutsByDate(date: Date): Promise { + try { + const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); + const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999).getTime(); + + const workouts = await this.db.getAllAsync<{ + id: string; + title: string; + type: string; + start_time: number; + end_time: number; + is_completed: number; + created_at: number; + last_updated: number; + template_id: string | null; + total_volume: number | null; + total_reps: number | null; + source: string; + }>( + `SELECT * FROM workouts + WHERE start_time >= ? AND start_time <= ? + ORDER BY start_time DESC`, + [startOfDay, endOfDay] + ); + + return workouts.map(row => this.mapRowToWorkout(row)); + } catch (error) { + console.error('Error getting workouts by date:', error); + throw error; + } + } + + /** + * Get all dates that have workouts within a month + */ + async getWorkoutDatesInMonth(year: number, month: number): Promise { + try { + const startOfMonth = new Date(year, month, 1).getTime(); + const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59, 999).getTime(); + + const result = await this.db.getAllAsync<{ + start_time: number; + }>( + `SELECT DISTINCT start_time FROM workouts + WHERE start_time >= ? AND start_time <= ?`, + [startOfMonth, endOfMonth] + ); + + return result.map(row => new Date(row.start_time)); + } catch (error) { + console.error('Error getting workout dates:', error); + return []; + } + } + + /** + * Get workout details including exercises + */ + async getWorkoutDetails(workoutId: string): Promise { + try { + const workout = await this.db.getFirstAsync<{ + id: string; + title: string; + type: string; + start_time: number; + end_time: number; + is_completed: number; + created_at: number; + last_updated: number; + template_id: string | null; + total_volume: number | null; + total_reps: number | null; + source: string; + }>( + `SELECT * FROM workouts WHERE id = ?`, + [workoutId] + ); + + if (!workout) return null; + + // Get exercises for this workout + // This is just a skeleton - you'll need to implement the actual query + // based on your database schema + const exercises = await this.db.getAllAsync( + `SELECT * FROM workout_exercises WHERE workout_id = ?`, + [workoutId] + ); + + const workoutObj = this.mapRowToWorkout(workout); + // You would set the exercises property here based on your schema + // workoutObj.exercises = exercises.map(...); + + return workoutObj; + } catch (error) { + console.error('Error getting workout details:', error); + throw error; + } + } + + /** + * Get the total number of workouts + */ + async getWorkoutCount(): Promise { + try { + const result = await this.db.getFirstAsync<{ count: number }>( + `SELECT COUNT(*) as count FROM workouts` + ); + + return result?.count || 0; + } catch (error) { + console.error('Error getting workout count:', error); + return 0; + } + } + + /** + * Helper method to map a database row to a Workout object + */ + private mapRowToWorkout(row: { + id: string; + title: string; + type: string; + start_time: number; + end_time: number; + is_completed: number; + created_at: number; + last_updated?: number; + template_id?: string | null; + total_volume?: number | null; + total_reps?: number | null; + source: string; + }): Workout { + return { + id: row.id, + title: row.title, + type: row.type as any, // Cast to TemplateType + startTime: row.start_time, + endTime: row.end_time, + isCompleted: row.is_completed === 1, + created_at: row.created_at, + lastUpdated: row.last_updated, + templateId: row.template_id || undefined, + totalVolume: row.total_volume || undefined, + totalReps: row.total_reps || undefined, + availability: { + source: [row.source as any] // Cast to StorageSource + }, + exercises: [] // Exercises would be loaded separately + }; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 38fbe4e..adfcf8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@rn-primitives/types": "~1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "date-fns": "^4.1.0", "expo": "^52.0.35", "expo-crypto": "~14.0.2", "expo-dev-client": "~5.0.12", @@ -11292,6 +11293,16 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 6a9bc55..50dd759 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@rn-primitives/types": "~1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "date-fns": "^4.1.0", "expo": "^52.0.35", "expo-crypto": "~14.0.2", "expo-dev-client": "~5.0.12",