diff --git a/CHANGELOG.md b/CHANGELOG.md
index 570a3c4..6e57fe2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
+- Zustand workout store for state management
+ - Created comprehensive workout state store with Zustand
+ - Implemented selectors for efficient state access
+ - Added workout persistence and recovery
+ - Built automatic timer management with background support
+ - Developed minimization and maximization functionality
+- Workout tracking implementation with real-time tracking
+ - Added workout timer with proper background handling
+ - Implemented rest timer functionality
+ - Added exercise set tracking with weight and reps
+ - Created workout minimization and maximization system
+ - Implemented active workout bar for minimized workouts
- SQLite database implementation with development seeding
- Successfully integrated SQLite with proper transaction handling
- Added mock exercise library with 10 initial exercises
@@ -38,8 +50,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Favorite template functionality
- Template categories and filtering
- Quick-start template actions
+- Full-screen template details with tab navigation
+ - Replaced bottom sheet with dedicated full-screen layout
+ - Implemented material top tabs for content organization
+ - Added Overview, History, and Social tabs
+ - Improved template information hierarchy
+ - Added contextual action buttons based on template source
+ - Enhanced social sharing capabilities
+ - Improved workout history visualization
### Changed
+- Improved workout screen navigation consistency
+ - Standardized screen transitions and gestures
+ - Added back buttons for clearer navigation
+ - Implemented proper workout state persistence
+- Enhanced exercise selection interface
+ - Updated add-exercises screen with cleaner UI
+ - Added multi-select functionality for bulk exercise addition
+ - Implemented exercise search and filtering
- Improved exercise library interface
- Removed "Recent Exercises" section for cleaner UI
- Added alphabetical section organization
@@ -62,8 +90,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added category pills for filtering
- Improved spacing and layout
- Better visual hierarchy for favorites
+- Migrated from React Context to Zustand for state management
+ - Improved performance with fine-grained rendering
+ - Enhanced developer experience with simpler API
+ - Better type safety with TypeScript integration
+ - Added persistent workout state for recovery
+- Redesigned template details experience
+ - Migrated from bottom sheet to full-screen layout
+ - Restructured content with tab-based navigation
+ - Added dedicated header with prominent action buttons
+ - Improved attribution and source indication
+ - Enhanced visual separation between template metadata and content
### Fixed
+- Workout navigation gesture handling issues
+- Workout timer inconsistency during app background state
- Exercise deletion functionality
- Keyboard overlap issues in exercise creation form
- SQLite transaction nesting issues
@@ -73,6 +114,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Template category spacing issues
- Exercise list rendering on iOS
- Database reset and reseeding behavior
+- Template details UI overflow issues
+- Navigation inconsistencies between template screens
+- Content rendering issues in bottom sheet components
### Technical Details
1. Database Schema Enforcement:
@@ -99,11 +143,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added DevSeederService for development data
- Enhanced error handling and logging
+5. Workout State Management with Zustand:
+ - Implemented selector pattern for performance optimization
+ - Added module-level timer references for background operation
+ - Created workout persistence with auto-save functionality
+ - Developed state recovery for crash protection
+ - Added support for future Nostr integration
+ - Implemented workout minimization for multi-tasking
+
+6. Template Details UI Architecture:
+ - Implemented MaterialTopTabNavigator for content organization
+ - Created screen-specific components for each tab
+ - Developed conditional rendering based on template source
+ - Implemented context-aware action buttons
+ - Added proper navigation state handling
+
### Migration Notes
- Exercise creation now enforces schema constraints
- Input validation prevents invalid data entry
- Enhanced error messages provide better debugging information
- Template management requires updated type definitions
+- Workout state now persists across app restarts
+- Component access to workout state requires new selector pattern
+- Template details navigation has changed from modal to screen-based approach
## [0.1.0] - 2024-02-09
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 6d4df20..5182a81 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import { Platform, View } from 'react-native';
import { Tabs, useNavigation } from 'expo-router';
import { useTheme } from '@react-navigation/native';
-import { Dumbbell, Library, Users, History, User } from 'lucide-react-native';
+import { Dumbbell, Library, Users, History, User, Home } from 'lucide-react-native';
import type { CustomTheme } from '@/lib/theme';
import ActiveWorkoutBar from '@/components/workout/ActiveWorkoutBar';
import { useWorkoutStore } from '@/stores/workoutStore';
@@ -51,7 +51,7 @@ export default function TabLayout() {
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
-
+
),
}}
/>
@@ -85,7 +85,7 @@ export default function TabLayout() {
(
),
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 59a2d4d..27da755 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -1,6 +1,7 @@
// app/(tabs)/index.tsx
-import { useState, useEffect } from 'react'
+import React, { useState, useEffect, useCallback } from 'react';
import { ScrollView, View } from 'react-native'
+import { useFocusEffect } from '@react-navigation/native';
import { router } from 'expo-router'
import {
AlertDialog,
@@ -17,7 +18,6 @@ import Header from '@/components/Header'
import HomeWorkout from '@/components/workout/HomeWorkout'
import FavoriteTemplate from '@/components/workout/FavoriteTemplate'
import { useWorkoutStore } from '@/stores/workoutStore'
-import type { WorkoutTemplate } from '@/types/templates'
import { Text } from '@/components/ui/text'
import { getRandomWorkoutTitle } from '@/utils/workoutTitles'
@@ -59,39 +59,40 @@ export default function WorkoutScreen() {
endWorkout
} = useWorkoutStore();
- useEffect(() => {
- loadFavorites();
- }, []);
+ useFocusEffect(
+ useCallback(() => {
+ loadFavorites();
+ return () => {}; // Cleanup function
+ }, [])
+ );
const loadFavorites = async () => {
setIsLoadingFavorites(true);
try {
const favorites = await getFavorites();
- const workoutTemplates = favorites
- .filter(f => f.content && f.content.id && checkFavoriteStatus(f.content.id))
- .map(f => {
- const content = f.content;
- return {
- id: content.id,
- title: content.title || 'Untitled Workout',
- description: content.description || '',
- exercises: content.exercises.map(ex => ({
- title: ex.exercise.title,
- sets: ex.targetSets,
- reps: ex.targetReps
- })),
- exerciseCount: content.exercises.length,
- duration: content.metadata?.averageDuration,
- isFavorited: true,
- lastUsed: content.metadata?.lastUsed,
- source: content.availability.source.includes('nostr')
- ? 'nostr'
- : content.availability.source.includes('powr')
- ? 'powr'
- : 'local'
- } as FavoriteTemplateData;
- });
+ const workoutTemplates = favorites.map(f => {
+ const content = f.content;
+ return {
+ id: content.id,
+ title: content.title || 'Untitled Workout',
+ description: content.description || '',
+ exercises: content.exercises.map(ex => ({
+ title: ex.exercise.title,
+ sets: ex.targetSets,
+ reps: ex.targetReps
+ })),
+ exerciseCount: content.exercises.length,
+ duration: content.metadata?.averageDuration,
+ isFavorited: true,
+ lastUsed: content.metadata?.lastUsed,
+ source: content.availability.source.includes('nostr')
+ ? 'nostr'
+ : content.availability.source.includes('powr')
+ ? 'powr'
+ : 'local'
+ } as FavoriteTemplateData;
+ });
setFavoriteWorkouts(workoutTemplates);
} catch (error) {
diff --git a/app/(tabs)/library/_layout.tsx b/app/(tabs)/library/_layout.tsx
index 47b5c46..8c1f807 100644
--- a/app/(tabs)/library/_layout.tsx
+++ b/app/(tabs)/library/_layout.tsx
@@ -1,7 +1,6 @@
// app/(tabs)/library/_layout.tsx
import { View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
-import { Text } from '@/components/ui/text';
import { ThemeToggle } from '@/components/ThemeToggle';
import { SearchPopover } from '@/components/library/SearchPopover';
import { FilterPopover } from '@/components/library/FilterPopover';
diff --git a/app/(tabs)/library/exercises.tsx b/app/(tabs)/library/exercises.tsx
index 5b525bc..2ca3b1c 100644
--- a/app/(tabs)/library/exercises.tsx
+++ b/app/(tabs)/library/exercises.tsx
@@ -107,50 +107,6 @@ export default function ExercisesScreen() {
- {/* Filter buttons */}
-
-
-
-
-
-
-
{/* Exercises list */}
(null);
- const [selectedTemplate, setSelectedTemplate] = useState(null);
+ // Remove selectedTemplate state since we're not using it anymore
const handleDelete = (id: string) => {
setTemplates(current => current.filter(t => t.id !== id));
};
+ // Update to navigate to the template details screen
const handleTemplatePress = (template: Template) => {
- setSelectedTemplate(toWorkoutTemplate(template));
+ router.push(`/template/${template.id}`);
};
- const handleStartWorkout = (template: Template) => {
- // TODO: Navigate to workout screen with template
- console.log('Starting workout with template:', template);
+ const handleStartWorkout = async (template: Template) => {
+ try {
+ // Use the workoutStore action to start a workout from template
+ await useWorkoutStore.getState().startWorkoutFromTemplate(template.id);
+
+ // Navigate to the active workout screen
+ router.push('/(workout)/create');
+ } catch (error) {
+ console.error("Error starting workout:", error);
+ }
};
- const handleFavorite = (template: Template) => {
- setTemplates(current =>
- current.map(t =>
- t.id === template.id
- ? { ...t, isFavorite: !t.isFavorite }
- : t
- )
- );
- };
+ const handleFavorite = async (template: Template) => {
+ try {
+ const workoutTemplate = toWorkoutTemplate(template);
+ const isFavorite = useWorkoutStore.getState().checkFavoriteStatus(template.id);
+
+ if (isFavorite) {
+ await useWorkoutStore.getState().removeFavorite(template.id);
+ } else {
+ await useWorkoutStore.getState().addFavorite(workoutTemplate);
+ }
+
+ // Update local state to reflect change
+ setTemplates(current =>
+ current.map(t =>
+ t.id === template.id
+ ? { ...t, isFavorite: !isFavorite }
+ : t
+ )
+ );
+ } catch (error) {
+ console.error('Error toggling favorite status:', error);
+ }
+ };
+
+ useFocusEffect(
+ React.useCallback(() => {
+ // Refresh template favorite status when tab gains focus
+ setTemplates(current => current.map(template => ({
+ ...template,
+ isFavorite: useWorkoutStore.getState().checkFavoriteStatus(template.id)
+ })));
+ return () => {};
+ }, [])
+ );
const handleAddTemplate = (template: Template) => {
setTemplates(prev => [...prev, template]);
@@ -120,7 +154,7 @@ export default function TemplatesScreen() {
- {/* Category filters */}
+{/* // Category filters
))}
-
+ */}
{/* Templates list */}
@@ -207,16 +241,7 @@ export default function TemplatesScreen() {
- {/* Template Details with tabs */}
- {selectedTemplate && (
- {
- if (!open) setSelectedTemplate(null);
- }}
- />
- )}
+ {/* Remove the TemplateDetails component since we're using router navigation now */}
-
+
+
-
-
-
- )
+ >
+
+
+
+
+ >
+ );
}
\ No newline at end of file
diff --git a/app/(workout)/add-exercises.tsx b/app/(workout)/add-exercises.tsx
index 31b3ba0..cf08535 100644
--- a/app/(workout)/add-exercises.tsx
+++ b/app/(workout)/add-exercises.tsx
@@ -10,8 +10,9 @@ import { useWorkoutStore } from '@/stores/workoutStore';
import { useSQLiteContext } from 'expo-sqlite';
import { LibraryService } from '@/lib/db/services/LibraryService';
import { TabScreen } from '@/components/layout/TabScreen';
-import { X } from 'lucide-react-native';
+import { ChevronLeft } from 'lucide-react-native';
import { BaseExercise } from '@/types/exercise';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function AddExercisesScreen() {
const db = useSQLiteContext();
@@ -19,6 +20,7 @@ export default function AddExercisesScreen() {
const [exercises, setExercises] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [search, setSearch] = useState('');
+ const insets = useSafeAreaInsets();
const { addExercises } = useWorkoutStore();
@@ -53,26 +55,26 @@ export default function AddExercisesScreen() {
const selectedExercises = exercises.filter(e => selectedIds.includes(e.id));
addExercises(selectedExercises);
- // Just go back - this will dismiss the modal and return to create screen
+ // Go back to create screen
router.back();
};
return (
-
- {/* Close button in the top right */}
-
+
+ {/* Standard header with back button */}
+
+ Add Exercises
- Add Exercises
;
+ const navigation = useNavigation();
+
// Check if we're coming from minimized state when component mounts
useEffect(() => {
if (isMinimized) {
maximizeWorkout();
}
+ }, [isMinimized, maximizeWorkout]);
+
+ // Handle back navigation
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('beforeRemove', (e) => {
+ // If we have an active workout, just minimize it before continuing
+ if (activeWorkout && !isMinimized) {
+ // Call minimizeWorkout to update the state
+ minimizeWorkout();
+
+ // Let the navigation continue naturally
+ // Don't call router.back() here to avoid recursion
+ }
+ });
- // No need to set up a timer here as it's now managed by the store
- }, [isMinimized]);
+ return unsubscribe;
+ }, [navigation, activeWorkout, isMinimized, minimizeWorkout]);
const [showCancelDialog, setShowCancelDialog] = useState(false);
const insets = useSafeAreaInsets();
@@ -104,12 +129,6 @@ export default function CreateWorkoutScreen() {
});
};
- // Handler for minimizing workout and going back
- const handleMinimize = () => {
- minimizeWorkout();
- router.back();
- };
-
// Show empty state when no workout is active
if (!activeWorkout) {
return (
@@ -172,76 +191,73 @@ export default function CreateWorkoutScreen() {
return (
- {/* Swipe indicator and back button */}
-
-
+ {/* Header with back button */}
+
+
+
+ Back
+
+
+
- {/* Header with Title and Finish Button */}
-
- {/* Top row with minimize and finish buttons */}
-
+ {/* Full-width workout title */}
+
+ updateWorkoutTitle(newTitle)}
+ placeholder="Workout Title"
+ textStyle={{
+ fontSize: 24,
+ fontWeight: '700',
+ }}
+ />
+
+
+ {/* Timer Display */}
+
+
+ {formatTime(elapsedTime)}
+
+
+ {status === 'active' ? (
-
+ ) : (
-
-
- {/* Full-width workout title */}
-
- updateWorkoutTitle(newTitle)}
- placeholder="Workout Title"
- textStyle={{
- fontSize: 24,
- fontWeight: '700',
- }}
- />
-
-
- {/* Timer Display */}
-
-
- {formatTime(elapsedTime)}
-
-
- {status === 'active' ? (
-
- ) : (
-
- )}
-
+ )}
{/* Content Area */}
@@ -317,43 +333,43 @@ export default function CreateWorkoutScreen() {
{previousSet ? `${previousSet.weight}×${previousSet.reps}` : '—'}
- {/* Weight Input */}
-
- {
- const weight = text === '' ? 0 : parseFloat(text);
- if (!isNaN(weight)) {
- updateSet(exerciseIndex, setIndex, { weight });
- }
- }}
- keyboardType="numeric"
- selectTextOnFocus
- />
-
+ {/* Weight Input */}
+
+ {
+ const weight = text === '' ? 0 : parseFloat(text);
+ if (!isNaN(weight)) {
+ updateSet(exerciseIndex, setIndex, { weight });
+ }
+ }}
+ keyboardType="numeric"
+ selectTextOnFocus
+ />
+
- {/* Reps Input */}
-
- {
- const reps = text === '' ? 0 : parseInt(text, 10);
- if (!isNaN(reps)) {
- updateSet(exerciseIndex, setIndex, { reps });
- }
- }}
- keyboardType="numeric"
- selectTextOnFocus
- />
-
+ {/* Reps Input */}
+
+ {
+ const reps = text === '' ? 0 : parseInt(text, 10);
+ if (!isNaN(reps)) {
+ updateSet(exerciseIndex, setIndex, { reps });
+ }
+ }}
+ keyboardType="numeric"
+ selectTextOnFocus
+ />
+
{/* Complete Button */}
)}
-
- {/* Add Exercise FAB - only shown when exercises exist */}
- {hasExercises && (
-
- router.push('/(workout)/add-exercises')}
- />
-
- )}
{/* Cancel Workout Dialog */}
@@ -464,10 +473,4 @@ export default function CreateWorkoutScreen() {
);
-}
-
-const styles = StyleSheet.create({
- timerText: {
- fontVariant: ['tabular-nums']
- }
-});
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/(workout)/template-select.tsx b/app/(workout)/template-select.tsx
index 2e5eec4..1f89eb3 100644
--- a/app/(workout)/template-select.tsx
+++ b/app/(workout)/template-select.tsx
@@ -2,12 +2,15 @@
import React from 'react';
import { View, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text';
-import { useRouter } from 'expo-router';
import { useWorkoutStore } from '@/stores/workoutStore';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
+import { TabScreen } from '@/components/layout/TabScreen';
+import { ChevronLeft } from 'lucide-react-native';
import { generateId } from '@/utils/ids';
-import type { TemplateType } from '@/types/templates';
+import type { TemplateType } from '@/types/templates';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useRouter } from 'expo-router';
// Temporary mock data - replace with actual template data
const MOCK_TEMPLATES = [
@@ -37,6 +40,7 @@ const MOCK_TEMPLATES = [
export default function TemplateSelectScreen() {
const router = useRouter();
+ const insets = useSafeAreaInsets();
const startWorkout = useWorkoutStore.use.startWorkout();
const handleSelectTemplate = (template: typeof MOCK_TEMPLATES[0]) => {
@@ -76,34 +80,54 @@ export default function TemplateSelectScreen() {
created_at: Date.now()
}))
});
- router.back();
+
+ // Navigate directly to the create screen instead of going back
+ router.push('/(workout)/create');
};
return (
-
- Recent Templates
-
- {MOCK_TEMPLATES.map(template => (
-
-
- {template.title}
- {template.category}
-
- {/* Exercise Preview */}
-
- {template.exercises.map((exercise, index) => (
-
- {exercise.title} - {exercise.sets}×{exercise.reps}
-
- ))}
-
+
+
+ {/* Standard header with back button */}
+
+ router.back()}
+ >
+
+
+ Select Template
+
+
+
+ Recent Templates
+
+
+ {MOCK_TEMPLATES.map(template => (
+
+
+ {template.title}
+ {template.category}
+
+ {/* Exercise Preview */}
+
+ {template.exercises.map((exercise, index) => (
+
+ {exercise.title} - {exercise.sets}×{exercise.reps}
+
+ ))}
+
- handleSelectTemplate(template)}>
- Start Workout
-
-
-
- ))}
-
+ handleSelectTemplate(template)}>
+ Start Workout
+
+
+
+ ))}
+
+
+
+
);
}
\ No newline at end of file
diff --git a/app/(workout)/template/[id]/_layout.tsx b/app/(workout)/template/[id]/_layout.tsx
new file mode 100644
index 0000000..a1a9dce
--- /dev/null
+++ b/app/(workout)/template/[id]/_layout.tsx
@@ -0,0 +1,283 @@
+// app/(workout)/template/[id]/_layout.tsx
+import React, { useState, useEffect } from 'react';
+import { View, ActivityIndicator } from 'react-native';
+import { useLocalSearchParams, router } from 'expo-router';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useTheme } from '@react-navigation/native';
+import { useSQLiteContext } from 'expo-sqlite';
+import { useWorkoutStore } from '@/stores/workoutStore';
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import {
+ ChevronLeft,
+ Play,
+ Heart
+} from 'lucide-react-native';
+
+// UI Components
+import { Text } from '@/components/ui/text';
+import { Button } from '@/components/ui/button';
+import { TabScreen } from '@/components/layout/TabScreen';
+
+// Import tab screens
+import OverviewTab from './index';
+import SocialTab from './social';
+import HistoryTab from './history';
+
+// Types
+import { WorkoutTemplate } from '@/types/templates';
+import type { CustomTheme } from '@/lib/theme';
+
+// Create the tab navigator
+const Tab = createMaterialTopTabNavigator();
+
+export default function TemplateDetailsLayout() {
+ const params = useLocalSearchParams();
+ const [isLoading, setIsLoading] = useState(true);
+ const [workoutTemplate, setWorkoutTemplate] = useState(null);
+ const [isFavorite, setIsFavorite] = useState(false);
+
+ const templateId = typeof params.id === 'string' ? params.id : '';
+ const db = useSQLiteContext();
+ const insets = useSafeAreaInsets();
+ const theme = useTheme() as CustomTheme;
+
+ // Use the workoutStore
+ const { startWorkoutFromTemplate, checkFavoriteStatus, addFavorite, removeFavorite } = useWorkoutStore();
+
+ useEffect(() => {
+ async function loadTemplate() {
+ try {
+ setIsLoading(true);
+
+ if (!templateId) {
+ router.replace('/');
+ return;
+ }
+
+ // Check if it's a favorite
+ const isFav = checkFavoriteStatus(templateId);
+ setIsFavorite(isFav);
+
+ // If it's a favorite, get it from favorites
+ if (isFav) {
+ const favorites = await useWorkoutStore.getState().getFavorites();
+ const favoriteTemplate = favorites.find(f => f.id === templateId);
+
+ if (favoriteTemplate) {
+ setWorkoutTemplate(favoriteTemplate.content);
+ setIsLoading(false);
+ return;
+ }
+ }
+
+ // If not found in favorites or not a favorite, fetch from database
+ // TODO: Implement fetching from database if needed
+
+ // For now, create a mock template if we couldn't find it
+ if (!workoutTemplate) {
+ // This is a fallback mock. In production, you'd show an error
+ const mockTemplate: WorkoutTemplate = {
+ id: templateId,
+ title: "Sample Workout",
+ type: "strength",
+ category: "Full Body",
+ exercises: [{
+ exercise: {
+ id: "ex1",
+ title: "Barbell Squat",
+ type: "strength",
+ category: "Legs",
+ tags: ["compound", "legs"],
+ availability: { source: ["local"] },
+ created_at: Date.now()
+ },
+ targetSets: 3,
+ targetReps: 8
+ }],
+ isPublic: false,
+ tags: ["strength", "beginner"],
+ version: 1,
+ created_at: Date.now(),
+ availability: { source: ["local"] }
+ };
+
+ setWorkoutTemplate(mockTemplate);
+ }
+
+ setIsLoading(false);
+ } catch (error) {
+ console.error("Error loading template:", error);
+ setIsLoading(false);
+ }
+ }
+
+ loadTemplate();
+ }, [templateId, db]);
+
+ const handleStartWorkout = async () => {
+ if (!workoutTemplate) return;
+
+ try {
+ // Use the workoutStore action to start a workout from template
+ await startWorkoutFromTemplate(workoutTemplate.id);
+
+ // Navigate to the active workout screen
+ router.push('/(workout)/create');
+ } catch (error) {
+ console.error("Error starting workout:", error);
+ }
+ };
+
+ const handleGoBack = () => {
+ router.back();
+ };
+
+ const handleToggleFavorite = async () => {
+ if (!workoutTemplate) return;
+
+ try {
+ if (isFavorite) {
+ await removeFavorite(workoutTemplate.id);
+ } else {
+ await addFavorite(workoutTemplate);
+ }
+
+ setIsFavorite(!isFavorite);
+ } catch (error) {
+ console.error("Error toggling favorite:", error);
+ }
+ };
+
+ if (isLoading || !workoutTemplate) {
+ return (
+
+
+
+
+
+
+
+ Template Details
+
+
+
+
+ Loading template...
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header with back button, title and Start button */}
+
+
+
+
+
+
+ {workoutTemplate.title}
+
+
+
+
+
+
+
+
+ {/* Updated to match Add Exercises button */}
+
+ Start Workout
+
+
+
+
+ {/* Context provider to pass template to the tabs */}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Create a context to share the template with the tab screens
+interface TemplateContextType {
+ template: WorkoutTemplate | null;
+}
+
+export const TemplateContext = React.createContext({
+ template: null
+});
+
+// Custom hook to access the template
+export function useTemplate() {
+ const context = React.useContext(TemplateContext);
+ if (!context.template) {
+ throw new Error('useTemplate must be used within a TemplateContext.Provider');
+ }
+ return context.template;
+}
\ No newline at end of file
diff --git a/app/(workout)/template/[id]/history.tsx b/app/(workout)/template/[id]/history.tsx
new file mode 100644
index 0000000..74ff1eb
--- /dev/null
+++ b/app/(workout)/template/[id]/history.tsx
@@ -0,0 +1,180 @@
+// app/(workout)/template/[id]/history.tsx
+import React, { useState, useEffect } from 'react';
+import { View, ScrollView, ActivityIndicator } from 'react-native';
+import { Text } from '@/components/ui/text';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent } from '@/components/ui/card';
+import { Calendar } from 'lucide-react-native';
+import { useTemplate } from './_layout';
+
+// Format date helper
+const formatDate = (date: Date) => {
+ return date.toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric'
+ });
+};
+
+// Mock workout history - this would come from your database in a real app
+const mockWorkoutHistory = [
+ {
+ id: 'hist1',
+ date: new Date(2024, 1, 25),
+ duration: 62, // minutes
+ completed: true,
+ notes: "Increased weight on squats by 10lbs",
+ exercises: [
+ { name: "Barbell Squat", sets: 3, reps: 8, weight: 215 },
+ { name: "Bench Press", sets: 3, reps: 8, weight: 175 },
+ { name: "Bent Over Row", sets: 3, reps: 8, weight: 155 }
+ ]
+ },
+ {
+ id: 'hist2',
+ date: new Date(2024, 1, 18),
+ duration: 58, // minutes
+ completed: true,
+ exercises: [
+ { name: "Barbell Squat", sets: 3, reps: 8, weight: 205 },
+ { name: "Bench Press", sets: 3, reps: 8, weight: 175 },
+ { name: "Bent Over Row", sets: 3, reps: 8, weight: 155 }
+ ]
+ },
+ {
+ id: 'hist3',
+ date: new Date(2024, 1, 11),
+ duration: 65, // minutes
+ completed: false,
+ notes: "Stopped early due to time constraints",
+ exercises: [
+ { name: "Barbell Squat", sets: 3, reps: 8, weight: 205 },
+ { name: "Bench Press", sets: 3, reps: 8, weight: 170 },
+ { name: "Bent Over Row", sets: 2, reps: 8, weight: 150 }
+ ]
+ }
+];
+
+export default function HistoryTab() {
+ const template = useTemplate();
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Simulate loading history data
+ useEffect(() => {
+ const loadHistory = async () => {
+ setIsLoading(true);
+ // Simulate loading delay
+ await new Promise(resolve => setTimeout(resolve, 500));
+ setIsLoading(false);
+ };
+
+ loadHistory();
+ }, [template.id]);
+
+ return (
+
+
+ {/* Performance Summary */}
+
+ Performance Summary
+
+
+
+
+ Total Workouts
+ {mockWorkoutHistory.length}
+
+
+
+ Avg Duration
+
+ {Math.round(mockWorkoutHistory.reduce((acc, w) => acc + w.duration, 0) / mockWorkoutHistory.length)} min
+
+
+
+
+ Completion
+
+ {Math.round(mockWorkoutHistory.filter(w => w.completed).length / mockWorkoutHistory.length * 100)}%
+
+
+
+
+
+
+
+ {/* History List */}
+
+ Workout History
+
+ {isLoading ? (
+
+
+ Loading history...
+
+ ) : mockWorkoutHistory.length > 0 ? (
+
+ {mockWorkoutHistory.map((workout) => (
+
+
+
+ {formatDate(workout.date)}
+
+ {workout.completed ? "Completed" : "Incomplete"}
+
+
+
+
+
+ Duration
+ {workout.duration} min
+
+
+
+ Sets
+
+ {workout.exercises.reduce((acc, ex) => acc + ex.sets, 0)}
+
+
+
+
+ Volume
+
+ {workout.exercises.reduce((acc, ex) => acc + (ex.sets * ex.reps * ex.weight), 0)} lbs
+
+
+
+
+ {workout.notes && (
+
+ {workout.notes}
+
+ )}
+
+
+ View Details
+
+
+
+ ))}
+
+ ) : (
+
+
+
+ No workout history available yet
+
+
+ Complete a workout using this template to see your history
+
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/(workout)/template/[id]/index.tsx b/app/(workout)/template/[id]/index.tsx
new file mode 100644
index 0000000..34d4253
--- /dev/null
+++ b/app/(workout)/template/[id]/index.tsx
@@ -0,0 +1,219 @@
+// app/(workout)/template/[id]/index.tsx
+import React from 'react';
+import { View, ScrollView } from 'react-native';
+import { Text } from '@/components/ui/text';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Separator } from '@/components/ui/separator';
+import { Card, CardContent } from '@/components/ui/card';
+import { useTemplate } from './_layout';
+import { formatTime } from '@/utils/formatTime';
+import {
+ Edit2,
+ Copy,
+ Share2,
+ Dumbbell,
+ Target,
+ Calendar,
+ Hash,
+ Clock,
+ Award
+} from 'lucide-react-native';
+
+export default function OverviewTab() {
+ const template = useTemplate();
+
+ const {
+ title,
+ type,
+ category,
+ description,
+ exercises = [],
+ tags = [],
+ metadata,
+ availability
+ } = template;
+
+ // Calculate source type from availability
+ const sourceType = availability.source.includes('nostr')
+ ? 'nostr'
+ : availability.source.includes('powr')
+ ? 'powr'
+ : 'local';
+
+ const isEditable = sourceType === 'local';
+
+ return (
+
+
+ {/* Basic Info Section */}
+
+
+ {sourceType === 'local' ? 'My Template' : sourceType === 'powr' ? 'POWR Template' : 'Nostr Template'}
+
+
+ {type}
+
+
+
+
+
+ {/* Category Section */}
+
+
+
+
+
+ Category
+ {category}
+
+
+
+ {/* Description Section */}
+ {description && (
+
+ Description
+ {description}
+
+ )}
+
+ {/* Exercises Section */}
+
+
+
+ Exercises
+
+
+ {exercises.map((exerciseConfig, index) => (
+
+
+ {exerciseConfig.exercise.title}
+
+
+ {exerciseConfig.targetSets} sets × {exerciseConfig.targetReps} reps
+
+ {exerciseConfig.notes && (
+
+ {exerciseConfig.notes}
+
+ )}
+
+ ))}
+
+
+
+ {/* Tags Section */}
+ {tags.length > 0 && (
+
+
+
+ Tags
+
+
+ {tags.map(tag => (
+
+ {tag}
+
+ ))}
+
+
+ )}
+
+ {/* Workout Parameters Section */}
+
+
+
+ Workout Parameters
+
+
+ {template.rounds && (
+
+ Rounds:
+ {template.rounds}
+
+ )}
+ {template.duration && (
+
+ Duration:
+ {formatTime(template.duration * 1000)}
+
+ )}
+ {template.interval && (
+
+ Interval:
+ {formatTime(template.interval * 1000)}
+
+ )}
+ {template.restBetweenRounds && (
+
+ Rest Between Rounds:
+
+ {formatTime(template.restBetweenRounds * 1000)}
+
+
+ )}
+ {metadata?.averageDuration && (
+
+ Average Completion Time:
+
+ {Math.round(metadata.averageDuration / 60)} minutes
+
+
+ )}
+
+
+
+ {/* Usage Stats Section */}
+ {metadata && (
+
+
+
+ Usage
+
+
+ {metadata.useCount && (
+
+ Used {metadata.useCount} times
+
+ )}
+ {metadata.lastUsed && (
+
+ Last used: {new Date(metadata.lastUsed).toLocaleDateString()}
+
+ )}
+
+
+ )}
+
+ {/* Action Buttons */}
+
+ {isEditable ? (
+ console.log('Edit template')}>
+
+ Edit Template
+
+ ) : (
+ console.log('Fork template')}>
+
+ Save as My Template
+
+ )}
+
+ console.log('Share template')}>
+
+ Share Template
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/(workout)/template/[id]/social.tsx b/app/(workout)/template/[id]/social.tsx
new file mode 100644
index 0000000..c777a09
--- /dev/null
+++ b/app/(workout)/template/[id]/social.tsx
@@ -0,0 +1,189 @@
+// app/(workout)/template/[id]/social.tsx
+import React, { useState } from 'react';
+import { View, ScrollView, ActivityIndicator } from 'react-native';
+import { Text } from '@/components/ui/text';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent } from '@/components/ui/card';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import {
+ MessageCircle,
+ ThumbsUp
+} from 'lucide-react-native';
+import { useTemplate } from './_layout';
+
+// Mock social feed data
+const mockSocialFeed = [
+ {
+ id: 'social1',
+ userName: 'FitnessFanatic',
+ userAvatar: 'https://randomuser.me/api/portraits/men/32.jpg',
+ pubkey: 'npub1q8s7vw...',
+ timestamp: new Date(Date.now() - 3600000 * 2), // 2 hours ago
+ content: 'Just crushed this Full Body workout! New PR on bench press 🎉',
+ metrics: {
+ duration: 58, // in minutes
+ volume: 4500, // total weight
+ exercises: 5
+ },
+ likes: 12,
+ comments: 3
+ },
+ {
+ id: 'social2',
+ userName: 'StrengthJourney',
+ userAvatar: 'https://randomuser.me/api/portraits/women/44.jpg',
+ pubkey: 'npub1z92r3...',
+ timestamp: new Date(Date.now() - 3600000 * 24), // 1 day ago
+ content: 'Modified this workout with extra leg exercises. Feeling the burn!',
+ metrics: {
+ duration: 65,
+ volume: 5200,
+ exercises: "5+2"
+ },
+ likes: 8,
+ comments: 1
+ },
+ {
+ id: 'social3',
+ userName: 'GymCoach',
+ userAvatar: 'https://randomuser.me/api/portraits/men/62.jpg',
+ pubkey: 'npub1xne8q...',
+ timestamp: new Date(Date.now() - 3600000 * 48), // 2 days ago
+ content: 'Great template for beginners! I recommend starting with lighter weights.',
+ metrics: {
+ duration: 45,
+ volume: 3600,
+ exercises: 5
+ },
+ likes: 24,
+ comments: 7
+ },
+ {
+ id: 'social4',
+ userName: 'WeightLifter',
+ userAvatar: 'https://randomuser.me/api/portraits/women/28.jpg',
+ pubkey: 'npub1r72df...',
+ timestamp: new Date(Date.now() - 3600000 * 72), // 3 days ago
+ content: 'Second time doing this workout. Improved my squat form significantly!',
+ metrics: {
+ duration: 52,
+ volume: 4100,
+ exercises: 5
+ },
+ likes: 15,
+ comments: 2
+ }
+];
+
+// Social Feed Item Component
+function SocialFeedItem({ item }: { item: typeof mockSocialFeed[0] }) {
+ return (
+
+
+ {/* User info and timestamp */}
+
+
+
+
+ {item.userName.substring(0, 2)}
+
+
+
+
+
+ {item.userName}
+
+ {item.timestamp.toLocaleDateString()}
+
+
+
+ {item.pubkey.substring(0, 10)}...
+
+
+
+
+ {/* Post content */}
+ {item.content}
+
+ {/* Workout metrics */}
+
+
+ Duration
+ {item.metrics.duration} min
+
+
+
+ Volume
+ {item.metrics.volume} lbs
+
+
+
+ Exercises
+ {item.metrics.exercises}
+
+
+
+ {/* Actions */}
+
+
+
+
+ {item.likes}
+
+
+
+ {item.comments}
+
+
+
+
+ View Workout
+
+
+
+
+ );
+}
+
+export default function SocialTab() {
+ const template = useTemplate();
+ const [isLoading, setIsLoading] = useState(false);
+
+ return (
+
+
+
+
+ Recent Activity
+
+
+ Nostr
+
+
+
+ {isLoading ? (
+
+
+ Loading activity...
+
+ ) : mockSocialFeed.length > 0 ? (
+ mockSocialFeed.map((item) => (
+
+ ))
+ ) : (
+
+
+ No social activity found
+
+ This workout hasn't been shared on Nostr yet
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/_layout.tsx b/app/_layout.tsx
index b9ff0d0..144ae55 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -15,6 +15,7 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
import { SettingsDrawerProvider } from '@/lib/contexts/SettingsDrawerContext';
import SettingsDrawer from '@/components/SettingsDrawer';
import { useNDKStore } from '@/lib/stores/ndk';
+import { useWorkoutStore } from '@/stores/workoutStore';
const LIGHT_THEME = {
...DefaultTheme,
@@ -42,6 +43,9 @@ export default function RootLayout() {
// Initialize NDK
await init();
+ // Load favorites from SQLite
+ await useWorkoutStore.getState().loadFavorites();
+
setIsInitialized(true);
} catch (error) {
console.error('Failed to initialize:', error);
diff --git a/components/templates/TemplateCard.tsx b/components/templates/TemplateCard.tsx
index c549f03..d3c0453 100644
--- a/components/templates/TemplateCard.tsx
+++ b/components/templates/TemplateCard.tsx
@@ -1,6 +1,7 @@
// components/templates/TemplateCard.tsx
import React from 'react';
import { View, TouchableOpacity, Platform } from 'react-native';
+import { router } from 'expo-router'; // Add this import
import { Text } from '@/components/ui/text';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -54,8 +55,13 @@ export function TemplateCard({
setShowDeleteAlert(false);
};
+ // Handle navigation to template details
+ const handleTemplatePress = () => {
+ router.push(`/template/${id}`);
+ };
+
return (
-
+
diff --git a/components/templates/TemplateDetails.tsx b/components/templates/TemplateDetails.tsx
deleted file mode 100644
index efe972b..0000000
--- a/components/templates/TemplateDetails.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-// components/templates/TemplateDetails.tsx
-import React from 'react';
-import { View, ScrollView } from 'react-native';
-import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
-import { Text } from '@/components/ui/text';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Separator } from '@/components/ui/separator';
-import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
-import {
- Edit2,
- Dumbbell,
- Target,
- Calendar,
- Hash,
- ClipboardList,
- Settings,
- LineChart
-} from 'lucide-react-native';
-import { WorkoutTemplate, getSourceDisplay } from '@/types/templates';
-import { useTheme } from '@react-navigation/native';
-import type { CustomTheme } from '@/lib/theme';
-
-const Tab = createMaterialTopTabNavigator();
-
-interface TemplateDetailsProps {
- template: WorkoutTemplate;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- onEdit?: () => void;
-}
-
-// Overview Tab Component
-function OverviewTab({ template, onEdit }: { template: WorkoutTemplate; onEdit?: () => void }) {
- const {
- title,
- type,
- category,
- description,
- exercises = [],
- tags = [],
- metadata,
- availability
- } = template;
-
- // Calculate source type from availability
- const sourceType = availability.source.includes('nostr')
- ? 'nostr'
- : availability.source.includes('powr')
- ? 'powr'
- : 'local';
-
- return (
-
-
- {/* Basic Info Section */}
-
-
- {getSourceDisplay(template)}
-
-
- {type}
-
-
-
-
-
- {/* Category Section */}
-
-
-
-
-
- Category
- {category}
-
-
-
- {/* Description Section */}
- {description && (
-
- Description
- {description}
-
- )}
-
- {/* Exercises Section */}
-
-
-
- Exercises
-
-
- {exercises.map((exerciseConfig, index) => (
-
-
- {exerciseConfig.exercise.title}
-
-
- {exerciseConfig.targetSets} sets × {exerciseConfig.targetReps} reps
-
- {exerciseConfig.notes && (
-
- {exerciseConfig.notes}
-
- )}
-
- ))}
-
-
-
- {/* Tags Section */}
- {tags.length > 0 && (
-
-
-
- Tags
-
-
- {tags.map(tag => (
-
- {tag}
-
- ))}
-
-
- )}
-
- {/* Usage Stats Section */}
- {metadata && (
-
-
-
- Usage
-
-
- {metadata.useCount && (
-
- Used {metadata.useCount} times
-
- )}
- {metadata.lastUsed && (
-
- Last used: {new Date(metadata.lastUsed).toLocaleDateString()}
-
- )}
- {metadata.averageDuration && (
-
- Average duration: {Math.round(metadata.averageDuration / 60)} minutes
-
- )}
-
-
- )}
-
- {/* Edit Button */}
- {onEdit && (
-
-
-
- Edit Template
-
-
- )}
-
-
- );
-}
-
-// History Tab Component
-function HistoryTab({ template }: { template: WorkoutTemplate }) {
- return (
-
-
- {/* Performance Stats */}
-
- Performance Summary
-
-
- Usage Stats
-
-
- Total Workouts
-
- {template.metadata?.useCount || 0}
-
-
-
- Avg. Duration
-
- {template.metadata?.averageDuration
- ? `${Math.round(template.metadata.averageDuration / 60)}m`
- : '--'}
-
-
-
- Completion Rate
- --
-
-
-
-
- {/* Progress Chart Placeholder */}
-
- Progress Over Time
-
-
-
- Progress tracking coming soon
-
-
-
-
-
-
- {/* History List */}
-
- Recent Workouts
-
- {/* Placeholder for when no history exists */}
-
-
-
- No workout history available yet
-
-
- Complete a workout using this template to see your history
-
-
-
-
-
-
- );
-}
-
-// Settings Tab Component
-function SettingsTab({ template }: { template: WorkoutTemplate }) {
- const {
- type,
- rounds,
- duration,
- interval,
- restBetweenRounds,
- } = template;
-
- // Helper function to format seconds into MM:SS
- const formatTime = (seconds: number) => {
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins}:${secs.toString().padStart(2, '0')}`;
- };
-
- return (
-
-
- {/* Workout Configuration */}
-
- Workout Settings
-
-
- Workout Type
- {type}
-
-
- {rounds && (
-
- Number of Rounds
- {rounds}
-
- )}
-
- {duration && (
-
- Total Duration
-
- {formatTime(duration)}
-
-
- )}
-
- {interval && (
-
- Interval Time
-
- {formatTime(interval)}
-
-
- )}
-
- {restBetweenRounds && (
-
- Rest Between Rounds
-
- {formatTime(restBetweenRounds)}
-
-
- )}
-
-
-
- {/* Sync Settings */}
-
- Sync Settings
-
- Template Source
-
- {getSourceDisplay(template)}
-
-
-
-
-
- );
-}
-
-export function TemplateDetails({
- template,
- open,
- onOpenChange,
- onEdit
-}: TemplateDetailsProps) {
- const theme = useTheme() as CustomTheme;
-
- return (
- onOpenChange(false)}>
-
-
- {template.title}
-
-
-
-
-
-
- {() => }
-
-
- {() => }
-
-
- {() => }
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/stores/workoutStore.ts b/stores/workoutStore.ts
index 13d7eaf..9e1f4c0 100644
--- a/stores/workoutStore.ts
+++ b/stores/workoutStore.ts
@@ -18,6 +18,7 @@ import type {
TemplateExerciseConfig
} from '@/types/templates';
import type { BaseExercise } from '@/types/exercise';
+import { openDatabaseSync } from 'expo-sqlite';
const AUTO_SAVE_INTERVAL = 30000; // 30 seconds
@@ -34,7 +35,8 @@ interface FavoriteItem {
interface ExtendedWorkoutState extends WorkoutState {
isActive: boolean;
isMinimized: boolean;
- favorites: FavoriteItem[];
+ favoriteIds: string[]; // Only store IDs in memory
+ favoritesLoaded: boolean;
}
interface WorkoutActions {
@@ -85,11 +87,12 @@ interface ExtendedWorkoutActions extends WorkoutActions {
// Timer Actions from original implementation
tick: (elapsed: number) => void;
- // New favorite management
+ // New favorite management with persistence
getFavorites: () => Promise;
addFavorite: (template: WorkoutTemplate) => Promise;
removeFavorite: (templateId: string) => Promise;
checkFavoriteStatus: (templateId: string) => boolean;
+ loadFavorites: () => Promise;
// New template management
startWorkoutFromTemplate: (templateId: string) => Promise;
@@ -121,7 +124,8 @@ const initialState: ExtendedWorkoutState = {
},
isActive: false,
isMinimized: false,
- favorites: []
+ favoriteIds: [],
+ favoritesLoaded: false
};
const useWorkoutStoreBase = create()((set, get) => ({
@@ -228,10 +232,13 @@ const useWorkoutStoreBase = create {
+ try {
+ const db = openDatabaseSync('powr.db');
+
+ // Ensure favorites table exists
+ await db.execAsync(`
+ CREATE TABLE IF NOT EXISTS favorites (
+ id TEXT PRIMARY KEY,
+ content_type TEXT NOT NULL,
+ content_id TEXT NOT NULL,
+ content TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ UNIQUE(content_type, content_id)
+ );
+ CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id);
+ `);
+
+ // Load just the IDs from SQLite
+ const result = await db.getAllAsync<{
+ content_id: string
+ }>(`SELECT content_id FROM favorites WHERE content_type = 'template'`);
+
+ const favoriteIds = result.map(item => item.content_id);
+
+ set({
+ favoriteIds,
+ favoritesLoaded: true
+ });
+
+ console.log(`Loaded ${favoriteIds.length} favorite IDs from database`);
+ // Don't return anything (void)
+ } catch (error) {
+ console.error('Error loading favorites:', error);
+ set({ favoritesLoaded: true }); // Mark as loaded even on error
+ }
+ },
+
getFavorites: async () => {
- const { favorites } = get();
- return favorites;
+ const { favoriteIds, favoritesLoaded } = get();
+
+ // If favorites haven't been loaded from database yet, load them
+ if (!favoritesLoaded) {
+ await get().loadFavorites();
+ }
+
+ // If no favorites, return empty array
+ if (get().favoriteIds.length === 0) {
+ return [];
+ }
+
+ // Query the full content from SQLite
+ try {
+ const db = openDatabaseSync('powr.db');
+
+ // Generate placeholders for the SQL query
+ const placeholders = get().favoriteIds.map(() => '?').join(',');
+
+ // Get full content for all favorited templates
+ const result = await db.getAllAsync<{
+ id: string,
+ content_type: string,
+ content_id: string,
+ content: string,
+ created_at: number
+ }>(`SELECT * FROM favorites WHERE content_type = 'template' AND content_id IN (${placeholders})`,
+ get().favoriteIds
+ );
+
+ return result.map(item => ({
+ id: item.content_id,
+ content: JSON.parse(item.content),
+ addedAt: item.created_at
+ }));
+ } catch (error) {
+ console.error('Error fetching favorites content:', error);
+ return [];
+ }
},
addFavorite: async (template: WorkoutTemplate) => {
- const favorites = [...get().favorites];
- favorites.push({
- id: template.id,
- content: template,
- addedAt: Date.now()
- });
- set({ favorites });
+ try {
+ const db = openDatabaseSync('powr.db');
+ const now = Date.now();
+
+ // Add to SQLite database
+ await db.runAsync(
+ `INSERT OR REPLACE INTO favorites (id, content_type, content_id, content, created_at)
+ VALUES (?, ?, ?, ?, ?)`,
+ [
+ generateId('local'), // Generate a unique ID for the favorite entry
+ 'template',
+ template.id,
+ JSON.stringify(template),
+ now
+ ]
+ );
+
+ // Update just the ID in memory state
+ set(state => {
+ // Only add if not already present
+ if (!state.favoriteIds.includes(template.id)) {
+ return { favoriteIds: [...state.favoriteIds, template.id] };
+ }
+ return state;
+ });
+
+ console.log(`Added template "${template.title}" to favorites`);
+ } catch (error) {
+ console.error('Error adding favorite:', error);
+ throw error;
+ }
},
removeFavorite: async (templateId: string) => {
- const favorites = get().favorites.filter(f => f.id !== templateId);
- set({ favorites });
+ try {
+ const db = openDatabaseSync('powr.db');
+
+ // Remove from SQLite database
+ await db.runAsync(
+ `DELETE FROM favorites WHERE content_type = 'template' AND content_id = ?`,
+ [templateId]
+ );
+
+ // Update IDs in memory state
+ set(state => ({
+ favoriteIds: state.favoriteIds.filter(id => id !== templateId)
+ }));
+
+ console.log(`Removed template with ID "${templateId}" from favorites`);
+ } catch (error) {
+ console.error('Error removing favorite:', error);
+ throw error;
+ }
},
checkFavoriteStatus: (templateId: string) => {
- return get().favorites.some(f => f.id === templateId);
+ return get().favoriteIds.includes(templateId);
},
endWorkout: async () => {
@@ -525,7 +647,16 @@ const useWorkoutStoreBase = create {
// TODO: Implement clearing autosave from storage
get().stopWorkoutTimer(); // Make sure to stop the timer
- set(initialState);
+
+ // Preserve favorites when resetting
+ const favoriteIds = get().favoriteIds;
+ const favoritesLoaded = get().favoritesLoaded;
+
+ set({
+ ...initialState,
+ favoriteIds,
+ favoritesLoaded
+ });
},
// New actions for minimized state
@@ -539,17 +670,39 @@ const useWorkoutStoreBase = create {
get().stopWorkoutTimer(); // Make sure to stop the timer
- set(initialState);
+
+ // Preserve favorites when resetting
+ const favoriteIds = get().favoriteIds;
+ const favoritesLoaded = get().favoritesLoaded;
+
+ set({
+ ...initialState,
+ favoriteIds,
+ favoritesLoaded
+ });
}
}));
// Helper functions
async function getTemplate(templateId: string): Promise {
- // This is a placeholder - you'll need to implement actual template fetching
- // from your database/storage service
try {
- // Example implementation:
- // return await db.getTemplate(templateId);
+ // Try to get it from favorites in the database
+ const db = openDatabaseSync('powr.db');
+
+ const result = await db.getFirstAsync<{
+ content: string
+ }>(
+ `SELECT content FROM favorites WHERE content_type = 'template' AND content_id = ?`,
+ [templateId]
+ );
+
+ if (result && result.content) {
+ return JSON.parse(result.content);
+ }
+
+ // If not found in favorites, could implement fetching from template database
+ // Example: return await db.getTemplate(templateId);
+ console.log('Template not found in favorites:', templateId);
return null;
} catch (error) {
console.error('Error fetching template:', error);