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",