1
0
mirror of https://github.com/DocNR/POWR.git synced 2025-05-17 03:35:51 +00:00

added library store and fixed refresh issues in exercise and template screens in user library

This commit is contained in:
DocNR 2025-03-17 23:37:08 -04:00
parent 6ac0e80e45
commit c6a9af080c
14 changed files with 608 additions and 295 deletions

@ -14,6 +14,7 @@ import { Trash2, PackageOpen, Plus, X } from 'lucide-react-native';
import { useIconColor } from '@/lib/theme/iconUtils'; import { useIconColor } from '@/lib/theme/iconUtils';
import { useColorScheme } from '@/lib/theme/useColorScheme'; import { useColorScheme } from '@/lib/theme/useColorScheme';
import { COLORS } from '@/lib/theme/colors'; import { COLORS } from '@/lib/theme/colors';
import { FIXED_COLORS } from '@/lib/theme/colors';
export default function ManagePOWRPacksScreen() { export default function ManagePOWRPacksScreen() {
const powrPackService = usePOWRPackService(); const powrPackService = usePOWRPackService();
@ -102,7 +103,6 @@ export default function ManagePOWRPacksScreen() {
className="mb-4" className="mb-4"
style={{ backgroundColor: COLORS.purple.DEFAULT }} style={{ backgroundColor: COLORS.purple.DEFAULT }}
> >
<Plus size={18} color="#fff" style={{ marginRight: 8 }} />
<Text style={{ color: '#fff', fontWeight: '500' }}>Import New Pack</Text> <Text style={{ color: '#fff', fontWeight: '500' }}>Import New Pack</Text>
</Button> </Button>
@ -173,18 +173,28 @@ export default function ManagePOWRPacksScreen() {
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
<Text>Delete Pack</Text> <Text className="text-xl font-semibold text-foreground">Delete Pack</Text>
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<Text>This will remove the POWR Pack and all its associated templates and exercises from your library.</Text> <Text className="text-muted-foreground">
This will remove the POWR Pack and all its associated templates and exercises from your library.
</Text>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<View className="flex-row justify-center gap-3 px-4 mt-2"> <View className="flex-row justify-end gap-3">
<AlertDialogCancel onPress={() => setShowDeleteDialog(false)}> <AlertDialogCancel asChild>
<Text>Cancel</Text> <Button variant="outline" className="mr-2">
<Text>Cancel</Text>
</Button>
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction onPress={handleDeleteConfirm} className='bg-destructive'> <AlertDialogAction asChild>
<Text className='text-destructive-foreground'>Delete Pack</Text> <Button
variant="destructive"
onPress={handleDeleteConfirm}
style={{ backgroundColor: FIXED_COLORS.destructive }}
>
<Text style={{ color: '#FFFFFF' }}>Delete Pack</Text>
</Button>
</AlertDialogAction> </AlertDialogAction>
</View> </View>
</AlertDialogContent> </AlertDialogContent>

@ -1,6 +1,6 @@
// app/(tabs)/library/exercises.tsx // app/(tabs)/library/exercises.tsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, ActivityIndicator, ScrollView } from 'react-native'; import { View, ActivityIndicator, ScrollView, Alert } from 'react-native';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Search, Dumbbell, ListFilter } from 'lucide-react-native'; import { Search, Dumbbell, ListFilter } from 'lucide-react-native';
@ -15,6 +15,19 @@ import { useWorkoutStore } from '@/stores/workoutStore';
import { generateId } from '@/utils/ids'; import { generateId } from '@/utils/ids';
import { useNDKStore } from '@/lib/stores/ndk'; import { useNDKStore } from '@/lib/stores/ndk';
import { useIconColor } from '@/lib/theme/iconUtils'; import { useIconColor } from '@/lib/theme/iconUtils';
import { useFocusEffect } from '@react-navigation/native';
import { FIXED_COLORS } from '@/lib/theme/colors';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Text } from '@/components/ui/text';
// Default available filters // Default available filters
const availableFilters = { const availableFilters = {
@ -46,6 +59,10 @@ export default function ExercisesScreen() {
// Exercise details state // Exercise details state
const [selectedExercise, setSelectedExercise] = useState<ExerciseDisplay | null>(null); const [selectedExercise, setSelectedExercise] = useState<ExerciseDisplay | null>(null);
// Delete alert state
const [exerciseToDelete, setExerciseToDelete] = useState<ExerciseDisplay | null>(null);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
// Other hooks // Other hooks
const { isActive, isMinimized } = useWorkoutStore(); const { isActive, isMinimized } = useWorkoutStore();
const { currentUser } = useNDKStore(); const { currentUser } = useNDKStore();
@ -59,9 +76,19 @@ export default function ExercisesScreen() {
deleteExercise, deleteExercise,
updateExercise, updateExercise,
refreshExercises, refreshExercises,
silentRefresh, // Add this
updateFilters, updateFilters,
clearFilters clearFilters
} = useExercises(); } = useExercises();
// Add this to refresh on screen focus
useFocusEffect(
React.useCallback(() => {
// This will refresh without showing loading indicators if possible
silentRefresh();
return () => {};
}, [silentRefresh])
);
// Filter exercises based on search query // Filter exercises based on search query
React.useEffect(() => { React.useEffect(() => {
@ -200,38 +227,68 @@ export default function ExercisesScreen() {
} }
}; };
const handleDeleteExercise = (exercise: ExerciseDisplay) => {
setExerciseToDelete(exercise);
setShowDeleteAlert(true);
};
const handleConfirmDelete = async () => {
if (!exerciseToDelete) return;
try {
await deleteExercise(exerciseToDelete.id);
// If we were showing details for this exercise, close it
if (selectedExercise?.id === exerciseToDelete.id) {
setSelectedExercise(null);
}
// Refresh the list
refreshExercises();
} catch (error) {
Alert.alert(
"Cannot Delete Exercise",
error instanceof Error ? error.message :
"This exercise cannot be deleted. It may be part of a POWR Pack."
);
} finally {
setShowDeleteAlert(false);
setExerciseToDelete(null);
}
};
return ( return (
<View className="flex-1 bg-background"> <View className="flex-1 bg-background">
{/* Search bar with filter button */} {/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border"> <View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center"> <View className="flex-row items-center">
<View className="relative flex-1"> <View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center"> <View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} {...getIconProps('primary')} /> <Search size={18} {...getIconProps('primary')} />
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search exercises"
className="pl-9 pr-10 border-0 bg-background"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter size={20} {...getIconProps('primary')} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View> </View>
<Input </Button>
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search exercises"
className="pl-9 pr-10 border-0 bg-background"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter size={20} {...getIconProps('primary')} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View>
</Button>
</View>
</View>
</View> </View>
</View> </View>
</View>
</View>
{/* Filter Sheet */} {/* Filter Sheet */}
<FilterSheet <FilterSheet
@ -242,15 +299,23 @@ export default function ExercisesScreen() {
availableFilters={availableFilters} availableFilters={availableFilters}
/> />
{/* Exercises list */} {/* Loading indicator */}
<SimplifiedExerciseList {loading ? (
exercises={exercises} <View className="flex-1 items-center justify-center">
onExercisePress={handleExercisePress} <ActivityIndicator size="small" className="mb-2" />
/> <Text className="text-muted-foreground">Loading exercises...</Text>
</View>
) : (
<SimplifiedExerciseList
exercises={exercises}
onExercisePress={handleExercisePress}
onDeletePress={handleDeleteExercise}
/>
)}
{/* Exercise details sheet */} {/* Exercise details sheet */}
<ModalExerciseDetails <ModalExerciseDetails
exercise={selectedExercise} // This can now be null exercise={selectedExercise}
open={!!selectedExercise} open={!!selectedExercise}
onClose={() => setSelectedExercise(null)} onClose={() => setSelectedExercise(null)}
onEdit={handleEdit} onEdit={handleEdit}
@ -272,6 +337,38 @@ export default function ExercisesScreen() {
exerciseToEdit={exerciseToEdit} exerciseToEdit={exerciseToEdit}
mode={editMode} mode={editMode}
/> />
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteAlert} onOpenChange={setShowDeleteAlert}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Text className="text-xl font-semibold text-foreground">Delete Exercise</Text>
</AlertDialogTitle>
<AlertDialogDescription>
<Text className="text-muted-foreground">
Are you sure you want to delete {exerciseToDelete?.title}? This action cannot be undone.
</Text>
</AlertDialogDescription>
</AlertDialogHeader>
<View className="flex-row justify-end gap-3">
<AlertDialogCancel asChild>
<Button variant="outline" className="mr-2">
<Text>Cancel</Text>
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="destructive"
onPress={handleConfirmDelete}
style={{ backgroundColor: FIXED_COLORS.destructive }}
>
<Text style={{ color: '#FFFFFF' }}>Delete</Text>
</Button>
</AlertDialogAction>
</View>
</AlertDialogContent>
</AlertDialog>
</View> </View>
); );
} }

@ -1,6 +1,6 @@
// app/(tabs)/library/templates.tsx // app/(tabs)/library/templates.tsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, ScrollView } from 'react-native'; import { View, ScrollView, ActivityIndicator } from 'react-native';
import { router } from 'expo-router'; import { router } from 'expo-router';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -18,7 +18,8 @@ import {
toWorkoutTemplate toWorkoutTemplate
} from '@/types/templates'; } from '@/types/templates';
import { useWorkoutStore } from '@/stores/workoutStore'; import { useWorkoutStore } from '@/stores/workoutStore';
import { useTemplateService } from '@/components/DatabaseProvider'; import { useTemplates } from '@/lib/hooks/useTemplates';
import { useIconColor } from '@/lib/theme/iconUtils';
// Default available filters // Default available filters
const availableFilters = { const availableFilters = {
@ -34,27 +35,77 @@ const initialFilters: FilterOptions = {
source: [] source: []
}; };
// Initial templates - empty array
const initialTemplates: Template[] = [];
export default function TemplatesScreen() { export default function TemplatesScreen() {
const templateService = useTemplateService(); // Get the template service // State for UI elements
const [showNewTemplate, setShowNewTemplate] = useState(false); const [showNewTemplate, setShowNewTemplate] = useState(false);
const [templates, setTemplates] = useState(initialTemplates);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [filterSheetOpen, setFilterSheetOpen] = useState(false); const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters); const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const [activeFilters, setActiveFilters] = useState(0); const [activeFilters, setActiveFilters] = useState(0);
const { isActive, isMinimized } = useWorkoutStore();
const shouldShowFAB = !isActive || !isMinimized;
const [debugInfo, setDebugInfo] = useState(''); const [debugInfo, setDebugInfo] = useState('');
// State for the modal template details // State for the modal template details
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null); const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
const [showTemplateModal, setShowTemplateModal] = useState(false); const [showTemplateModal, setShowTemplateModal] = useState(false);
const handleDelete = (id: string) => { // Hooks
setTemplates(current => current.filter(t => t.id !== id)); const { getIconProps } = useIconColor();
const { isActive, isMinimized } = useWorkoutStore();
const {
templates,
loading,
error,
silentRefresh,
refreshTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
archiveTemplate
} = useTemplates();
// Check if floating action button should be shown
const shouldShowFAB = !isActive || !isMinimized;
// Convert WorkoutTemplates to Template format for the UI
const formattedTemplates = React.useMemo(() => {
return templates.map(template => {
// Get favorite status
const isFavorite = useWorkoutStore.getState().checkFavoriteStatus(template.id);
// Convert to Template format for the UI
return {
id: template.id,
title: template.title,
type: template.type,
category: template.category,
exercises: template.exercises.map(ex => ({
title: ex.exercise.title,
targetSets: ex.targetSets || 0,
targetReps: ex.targetReps || 0,
equipment: ex.exercise.equipment
})),
tags: template.tags || [],
source: template.availability?.source[0] || 'local',
isFavorite
};
});
}, [templates]);
// Refresh templates when the screen is focused
useFocusEffect(
React.useCallback(() => {
// This will refresh without showing loading indicators if possible
silentRefresh();
return () => {};
}, [silentRefresh])
);
const handleDelete = async (id: string) => {
try {
await deleteTemplate(id);
} catch (error) {
console.error('Error deleting template:', error);
}
}; };
const handleTemplatePress = (template: Template) => { const handleTemplatePress = (template: Template) => {
@ -65,9 +116,6 @@ export default function TemplatesScreen() {
const handleStartWorkout = async (template: Template) => { const handleStartWorkout = async (template: Template) => {
try { try {
// Convert to WorkoutTemplate format
const workoutTemplate = toWorkoutTemplate(template);
// Start the workout - use the template ID // Start the workout - use the template ID
await useWorkoutStore.getState().startWorkoutFromTemplate(template.id); await useWorkoutStore.getState().startWorkoutFromTemplate(template.id);
@ -88,15 +136,6 @@ export default function TemplatesScreen() {
} else { } else {
await useWorkoutStore.getState().addFavorite(workoutTemplate); 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) { } catch (error) {
console.error('Error toggling favorite status:', error); console.error('Error toggling favorite status:', error);
} }
@ -118,97 +157,22 @@ export default function TemplatesScreen() {
// Handle favorite change from modal // Handle favorite change from modal
const handleModalFavoriteChange = (templateId: string, isFavorite: boolean) => { const handleModalFavoriteChange = (templateId: string, isFavorite: boolean) => {
// Update local state to reflect change // The templates will be refreshed automatically through the store
setTemplates(current =>
current.map(t =>
t.id === templateId
? { ...t, isFavorite }
: t
)
);
}; };
const handleDebugDB = async () => {
try {
// Get all templates directly
const allTemplates = await templateService.getAllTemplates(100);
let info = "Database Stats:\n";
info += "--------------\n";
info += `Total templates: ${allTemplates.length}\n\n`;
// Template list
info += "Templates:\n";
allTemplates.forEach(template => {
info += `- ${template.title} (${template.id}) [${template.availability?.source[0] || 'unknown'}]\n`;
info += ` Exercises: ${template.exercises.length}\n`;
});
setDebugInfo(info);
console.log(info);
} catch (error) {
console.error('Debug error:', error);
setDebugInfo(`Error: ${String(error)}`);
}
};
// Load templates when the screen is focused
useFocusEffect(
React.useCallback(() => {
async function loadTemplates() {
try {
console.log('[TemplateScreen] Loading templates...');
// Load templates from the database
const data = await templateService.getAllTemplates(100);
console.log(`[TemplateScreen] Loaded ${data.length} templates from database`);
// Convert to Template[] format that the screen expects
const formattedTemplates: Template[] = [];
for (const template of data) {
// Get favorite status
const isFavorite = useWorkoutStore.getState().checkFavoriteStatus(template.id);
// Convert to Template format
formattedTemplates.push({
id: template.id,
title: template.title,
type: template.type,
category: template.category,
exercises: template.exercises.map(ex => ({
title: ex.exercise.title,
targetSets: ex.targetSets || 0,
targetReps: ex.targetReps || 0,
equipment: ex.exercise.equipment
})),
tags: template.tags || [],
source: template.availability?.source[0] || 'local',
isFavorite
});
}
// Update the templates state
setTemplates(formattedTemplates);
} catch (error) {
console.error('[TemplateScreen] Error loading templates:', error);
}
}
loadTemplates();
return () => {};
}, [])
);
const handleAddTemplate = (template: Template) => { const handleAddTemplate = (template: Template) => {
setTemplates(prev => [...prev, template]); // Convert UI Template to WorkoutTemplate
const workoutTemplate = toWorkoutTemplate(template);
// Create the template
createTemplate(workoutTemplate);
// Close the sheet
setShowNewTemplate(false); setShowNewTemplate(false);
}; };
// Filter templates based on search and applied filters // Filter templates based on search and applied filters
const filteredTemplates = templates.filter(template => { const filteredTemplates = formattedTemplates.filter(template => {
// Filter by search query // Filter by search query
const matchesSearch = !searchQuery || const matchesSearch = !searchQuery ||
template.title.toLowerCase().includes(searchQuery.toLowerCase()); template.title.toLowerCase().includes(searchQuery.toLowerCase());
@ -278,79 +242,68 @@ export default function TemplatesScreen() {
availableFilters={availableFilters} availableFilters={availableFilters}
/> />
{/* Debug button */} {/* Templates list with loading state */}
<View className="p-2"> {loading ? (
<Button <View className="flex-1 items-center justify-center">
variant="outline" <ActivityIndicator size="small" className="mb-2" />
size="sm" <Text className="text-muted-foreground">Loading templates...</Text>
onPress={handleDebugDB}
>
<Text className="text-xs">Debug Templates</Text>
</Button>
</View>
{/* Debug info display */}
{debugInfo ? (
<View className="px-4 py-2 mx-4 mb-2 bg-primary/10 rounded-md">
<ScrollView style={{ maxHeight: 150 }}>
<Text className="font-mono text-xs">{debugInfo}</Text>
</ScrollView>
</View> </View>
) : null} ) : (
<ScrollView className="flex-1">
{/* Templates list */} {/* Favorites Section */}
<ScrollView className="flex-1"> {favoriteTemplates.length > 0 && (
{/* Favorites Section */} <View className="py-4">
{favoriteTemplates.length > 0 && ( <Text className="text-lg font-semibold mb-4 px-4">
<View className="py-4"> Favorites
<Text className="text-lg font-semibold mb-4 px-4">
Favorites
</Text>
<View className="gap-3">
{favoriteTemplates.map(template => (
<TemplateCard
key={template.id}
template={template}
onPress={() => handleTemplatePress(template)}
onDelete={handleDelete}
onFavorite={() => handleFavorite(template)}
onStartWorkout={() => handleStartWorkout(template)}
/>
))}
</View>
</View>
)}
{/* All Templates Section */}
<View className="py-4">
<Text className="text-lg font-semibold mb-4 px-4">
All Templates
</Text>
{regularTemplates.length > 0 ? (
<View className="gap-3">
{regularTemplates.map(template => (
<TemplateCard
key={template.id}
template={template}
onPress={() => handleTemplatePress(template)}
onDelete={handleDelete}
onFavorite={() => handleFavorite(template)}
onStartWorkout={() => handleStartWorkout(template)}
/>
))}
</View>
) : (
<View className="px-4">
<Text className="text-muted-foreground">
No templates found. {templates.length > 0 ? 'Try changing your filters.' : 'Create a new workout template by clicking the + button.'}
</Text> </Text>
<View className="gap-3">
{favoriteTemplates.map(template => (
<TemplateCard
key={template.id}
template={template}
onPress={() => handleTemplatePress(template)}
onDelete={() => handleDelete(template.id)}
onFavorite={() => handleFavorite(template)}
onStartWorkout={() => handleStartWorkout(template)}
/>
))}
</View>
</View> </View>
)} )}
</View>
{/* Add some bottom padding for FAB */} {/* All Templates Section */}
<View className="h-20" /> <View className="py-4">
</ScrollView> <Text className="text-lg font-semibold mb-4 px-4">
All Templates
</Text>
{regularTemplates.length > 0 ? (
<View className="gap-3">
{regularTemplates.map(template => (
<TemplateCard
key={template.id}
template={template}
onPress={() => handleTemplatePress(template)}
onDelete={() => handleDelete(template.id)}
onFavorite={() => handleFavorite(template)}
onStartWorkout={() => handleStartWorkout(template)}
/>
))}
</View>
) : (
<View className="px-4">
<Text className="text-muted-foreground">
{formattedTemplates.length > 0
? 'No templates match your current filters.'
: 'No templates found. Create a new workout template by clicking the + button.'}
</Text>
</View>
)}
</View>
{/* Add some bottom padding for FAB */}
<View className="h-20" />
</ScrollView>
)}
{shouldShowFAB && ( {shouldShowFAB && (
<FloatingActionButton <FloatingActionButton

@ -1,3 +1,4 @@
// components/DatabaseProvider.tsx
import React from 'react'; import React from 'react';
import { View, ActivityIndicator, ScrollView, Text } from 'react-native'; import { View, ActivityIndicator, ScrollView, Text } from 'react-native';
import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite'; import { SQLiteProvider, openDatabaseSync, SQLiteDatabase } from 'expo-sqlite';
@ -10,6 +11,7 @@ import { TemplateService } from '@/lib/db/services/TemplateService';
import POWRPackService from '@/lib/db/services/POWRPackService'; import POWRPackService from '@/lib/db/services/POWRPackService';
import { logDatabaseInfo } from '@/lib/db/debug'; import { logDatabaseInfo } from '@/lib/db/debug';
import { useNDKStore } from '@/lib/stores/ndk'; import { useNDKStore } from '@/lib/stores/ndk';
import { useLibraryStore } from '@/lib/stores/libraryStore';
// Create context for services // Create context for services
interface DatabaseServicesContextValue { interface DatabaseServicesContextValue {
@ -85,6 +87,15 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
} }
}, [ndk, services]); }, [ndk, services]);
// Effect to trigger initial data refresh when database is ready
React.useEffect(() => {
if (isReady && services.db) {
console.log('[DB] Database ready - triggering initial library refresh');
// Refresh all library data
useLibraryStore.getState().refreshAll();
}
}, [isReady, services.db]);
React.useEffect(() => { React.useEffect(() => {
async function initDatabase() { async function initDatabase() {
try { try {

@ -4,6 +4,17 @@ import { View, SectionList, TouchableOpacity, ViewToken } from 'react-native';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ExerciseDisplay, WorkoutExercise } from '@/types/exercise'; import { ExerciseDisplay, WorkoutExercise } from '@/types/exercise';
import { Trash2 } from 'lucide-react-native';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
// Create a combined interface for exercises that could have workout data // Create a combined interface for exercises that could have workout data
interface DisplayWorkoutExercise extends ExerciseDisplay, WorkoutExercise {} interface DisplayWorkoutExercise extends ExerciseDisplay, WorkoutExercise {}
@ -11,14 +22,23 @@ interface DisplayWorkoutExercise extends ExerciseDisplay, WorkoutExercise {}
interface SimplifiedExerciseListProps { interface SimplifiedExerciseListProps {
exercises: ExerciseDisplay[]; exercises: ExerciseDisplay[];
onExercisePress: (exercise: ExerciseDisplay) => void; onExercisePress: (exercise: ExerciseDisplay) => void;
onDeletePress?: (exercise: ExerciseDisplay) => void; // Add this
} }
export const SimplifiedExerciseList = ({ export const SimplifiedExerciseList = ({
exercises, exercises,
onExercisePress onExercisePress,
onDeletePress
}: SimplifiedExerciseListProps) => { }: SimplifiedExerciseListProps) => {
const sectionListRef = useRef<SectionList>(null); const sectionListRef = useRef<SectionList>(null);
const [currentSection, setCurrentSection] = useState<string>(''); const [currentSection, setCurrentSection] = useState<string>('');
const [exerciseToDelete, setExerciseToDelete] = useState<ExerciseDisplay | null>(null);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const handleDeletePress = (exercise: ExerciseDisplay) => {
setExerciseToDelete(exercise);
setShowDeleteAlert(true);
};
// Organize exercises into sections // Organize exercises into sections
const sections = React.useMemo(() => { const sections = React.useMemo(() => {
@ -50,18 +70,6 @@ export const SimplifiedExerciseList = ({
} }
}, []); }, []);
const scrollToSection = useCallback((letter: string) => {
const sectionIndex = sections.findIndex(section => section.title === letter);
if (sectionIndex !== -1 && sectionListRef.current) {
sectionListRef.current.scrollToLocation({
animated: true,
sectionIndex,
itemIndex: 0,
viewPosition: 0,
});
}
}, [sections]);
const getItemLayout = useCallback((data: any, index: number) => ({ const getItemLayout = useCallback((data: any, index: number) => ({
length: 85, // Approximate height of each item length: 85, // Approximate height of each item
offset: 85 * index, offset: 85 * index,
@ -78,6 +86,7 @@ export const SimplifiedExerciseList = ({
const renderExerciseItem = ({ item }: { item: ExerciseDisplay }) => { const renderExerciseItem = ({ item }: { item: ExerciseDisplay }) => {
const firstLetter = item.title.charAt(0).toUpperCase(); const firstLetter = item.title.charAt(0).toUpperCase();
const canDelete = item.source === 'local';
return ( return (
<TouchableOpacity <TouchableOpacity
@ -137,7 +146,7 @@ export const SimplifiedExerciseList = ({
{/* Weight/Rep information if it was a WorkoutExercise */} {/* Weight/Rep information if it was a WorkoutExercise */}
{isWorkoutExercise(item) && ( {isWorkoutExercise(item) && (
<View className="items-end"> <View className="items-end mr-2">
<Text className="text-muted-foreground text-sm"> <Text className="text-muted-foreground text-sm">
{item.sets?.[0]?.weight && `${item.sets[0].weight} lb`} {item.sets?.[0]?.weight && `${item.sets[0].weight} lb`}
{item.sets?.[0]?.weight && item.sets?.[0]?.reps && ' '} {item.sets?.[0]?.weight && item.sets?.[0]?.reps && ' '}
@ -145,6 +154,20 @@ export const SimplifiedExerciseList = ({
</Text> </Text>
</View> </View>
)} )}
{/* Delete button (only for local exercises) */}
{canDelete && onDeletePress && (
<TouchableOpacity
onPress={(e) => {
e.stopPropagation(); // Prevent triggering the parent TouchableOpacity
onDeletePress(item);
}}
className="p-2"
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
>
<Trash2 size={18} color="#ef4444" />
</TouchableOpacity>
)}
</TouchableOpacity> </TouchableOpacity>
); );
}; };

@ -25,7 +25,6 @@ export default function NostrLoginPrompt({ message }: NostrLoginPromptProps) {
onPress={() => setIsLoginSheetOpen(true)} onPress={() => setIsLoginSheetOpen(true)}
className="px-6" className="px-6"
> >
<Key size={18} className="mr-2" />
<Text>Login with Nostr</Text> <Text>Login with Nostr</Text>
</Button> </Button>
</View> </View>

@ -19,6 +19,7 @@ import {
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Template, TemplateExerciseDisplay } from '@/types/templates'; import { Template, TemplateExerciseDisplay } from '@/types/templates';
import { useIconColor } from '@/lib/theme/iconUtils'; import { useIconColor } from '@/lib/theme/iconUtils';
import { FIXED_COLORS } from '@/lib/theme/colors';
interface TemplateCardProps { interface TemplateCardProps {
template: Template; template: Template;
@ -199,8 +200,12 @@ export function TemplateCard({
</Button> </Button>
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction asChild> <AlertDialogAction asChild>
<Button variant="destructive" onPress={handleConfirmDelete}> <Button
<Text className="text-destructive-foreground">Delete</Text> variant="destructive"
onPress={handleConfirmDelete}
style={{ backgroundColor: FIXED_COLORS.destructive }}
>
<Text style={{ color: '#FFFFFF' }}>Delete</Text>
</Button> </Button>
</AlertDialogAction> </AlertDialogAction>
</View> </View>

@ -366,6 +366,37 @@ export class ExerciseService {
} }
} }
// Add this to lib/db/services/ExerciseService.ts
async deleteExercise(id: string): Promise<boolean> {
try {
// First check if the exercise is from a POWR Pack
const exercise = await this.db.getFirstAsync<{ source: string }>(
'SELECT source FROM exercises WHERE id = ?',
[id]
);
if (!exercise) {
throw new Error(`Exercise with ID ${id} not found`);
}
if (exercise.source === 'nostr' || exercise.source === 'powr') {
// This is a POWR Pack exercise - don't allow direct deletion
throw new Error('This exercise is part of a POWR Pack and cannot be deleted individually. You can remove the entire POWR Pack from the settings menu.');
}
// For local exercises, proceed with deletion
await this.db.runAsync('DELETE FROM exercises WHERE id = ?', [id]);
// Also delete any references in template_exercises
await this.db.runAsync('DELETE FROM template_exercises WHERE exercise_id = ?', [id]);
return true;
} catch (error) {
console.error('Error deleting exercise:', error);
throw error;
}
}
async syncWithNostrEvent(eventId: string, exercise: Omit<BaseExercise, 'id'>): Promise<string> { async syncWithNostrEvent(eventId: string, exercise: Omit<BaseExercise, 'id'>): Promise<string> {
try { try {
// Check if we already have this exercise // Check if we already have this exercise

@ -16,6 +16,7 @@ import {
} from '@/types/templates'; } from '@/types/templates';
import '@/types/ndk-extensions'; import '@/types/ndk-extensions';
import { safeAddRelay, safeRemoveRelay } from '@/types/ndk-common'; import { safeAddRelay, safeRemoveRelay } from '@/types/ndk-common';
import { useLibraryStore } from '@/lib/stores/libraryStore';
/** /**
* Service for managing POWR Packs (importable collections of templates and exercises) * Service for managing POWR Packs (importable collections of templates and exercises)
@ -592,6 +593,11 @@ export default class POWRPackService {
// Finally, save the pack itself // Finally, save the pack itself
await this.savePack(packImport.packEvent, selection); await this.savePack(packImport.packEvent, selection);
// Trigger refresh of templates and exercises
useLibraryStore.getState().refreshTemplates();
useLibraryStore.getState().refreshExercises();
useLibraryStore.getState().refreshPacks();
// Get total counts // Get total counts
const totalNostrTemplates = await this.db.getFirstAsync<{ count: number }>( const totalNostrTemplates = await this.db.getFirstAsync<{ count: number }>(
@ -949,6 +955,13 @@ export default class POWRPackService {
[packId] [packId]
); );
// Trigger refresh of templates and exercises
useLibraryStore.getState().refreshTemplates();
useLibraryStore.getState().refreshExercises();
useLibraryStore.getState().refreshPacks();
// Clear any cached data
useLibraryStore.getState().clearCache();
console.log(`[POWRPackService] Successfully deleted pack ${packId} with ${templates.length} templates and ${exerciseIdsToDelete.size} exercises`); console.log(`[POWRPackService] Successfully deleted pack ${packId} with ${templates.length} templates and ${exerciseIdsToDelete.size} exercises`);
}); });
} catch (error) { } catch (error) {

@ -452,6 +452,22 @@ export class TemplateService {
throw error; throw error;
} }
} }
/**
* Check if a template exists in the database
*/
async templateExists(id: string): Promise<boolean> {
try {
const result = await this.db.getFirstAsync<{ count: number }>(
'SELECT COUNT(*) as count FROM templates WHERE id = ?',
[id]
);
return result ? result.count > 0 : false;
} catch (error) {
console.error('Error checking if template exists:', error);
return false;
}
}
/** /**
* Delete template from Nostr * Delete template from Nostr

@ -1,5 +1,5 @@
// lib/hooks/useExercises.ts // lib/hooks/useExercises.ts
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useSQLiteContext } from 'expo-sqlite'; import { useSQLiteContext } from 'expo-sqlite';
import { import {
ExerciseDisplay, ExerciseDisplay,
@ -10,6 +10,7 @@ import {
toExerciseDisplay toExerciseDisplay
} from '@/types/exercise'; } from '@/types/exercise';
import { LibraryService } from '../db/services/LibraryService'; import { LibraryService } from '../db/services/LibraryService';
import { useExerciseRefresh } from '@/lib/stores/libraryStore';
// Filtering types // Filtering types
export interface ExerciseFilters { export interface ExerciseFilters {
@ -38,19 +39,27 @@ const initialStats: ExerciseStats = {
export function useExercises() { export function useExercises() {
const db = useSQLiteContext(); const db = useSQLiteContext();
const libraryService = React.useMemo(() => new LibraryService(db), [db]); const libraryService = React.useMemo(() => new LibraryService(db), [db]);
const { refreshCount, refreshExercises, isLoading, setLoading } = useExerciseRefresh();
const [exercises, setExercises] = useState<ExerciseDisplay[]>([]); const [exercises, setExercises] = useState<ExerciseDisplay[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const [filters, setFilters] = useState<ExerciseFilters>({}); const [filters, setFilters] = useState<ExerciseFilters>({});
const [stats, setStats] = useState<ExerciseStats>(initialStats); const [stats, setStats] = useState<ExerciseStats>(initialStats);
// Add a loaded flag to track if we've successfully loaded exercises at least once
const hasLoadedRef = useRef(false);
// Load all exercises from the database // Define loadExercises before using it in useEffect
const loadExercises = useCallback(async () => { const loadExercises = useCallback(async (showLoading: boolean = true) => {
try { try {
setLoading(true); // Only show loading indicator if we haven't loaded before or if explicitly requested
if (showLoading && (!hasLoadedRef.current || exercises.length === 0)) {
setLoading(true);
}
const allExercises = await libraryService.getExercises(); const allExercises = await libraryService.getExercises();
setExercises(allExercises); setExercises(allExercises);
hasLoadedRef.current = true;
// Calculate stats // Calculate stats
const newStats = allExercises.reduce((acc: ExerciseStats, exercise: ExerciseDisplay) => { const newStats = allExercises.reduce((acc: ExerciseStats, exercise: ExerciseDisplay) => {
@ -87,7 +96,17 @@ export function useExercises() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [libraryService]); }, [libraryService, setLoading, exercises.length]);
// Add a silentRefresh method that doesn't show loading indicators
const silentRefresh = useCallback(() => {
loadExercises(false);
}, [loadExercises]);
// Load exercises when refreshCount changes
useEffect(() => {
loadExercises();
}, [refreshCount, loadExercises]);
// Filter exercises based on current filters // Filter exercises based on current filters
const getFilteredExercises = useCallback(() => { const getFilteredExercises = useCallback(() => {
@ -142,24 +161,24 @@ export function useExercises() {
}; };
const id = await libraryService.addExercise(displayExercise); const id = await libraryService.addExercise(displayExercise);
await loadExercises(); // Reload all exercises to update stats refreshExercises(); // Use the store's refresh function instead of loading directly
return id; return id;
} catch (err) { } catch (err) {
setError(err instanceof Error ? err : new Error('Failed to create exercise')); setError(err instanceof Error ? err : new Error('Failed to create exercise'));
throw err; throw err;
} }
}, [libraryService, loadExercises]); }, [libraryService, refreshExercises]);
// Delete an exercise // Delete an exercise
const deleteExercise = useCallback(async (id: string) => { const deleteExercise = useCallback(async (id: string) => {
try { try {
await libraryService.deleteExercise(id); await libraryService.deleteExercise(id);
await loadExercises(); // Reload to update stats refreshExercises(); // Use the store's refresh function
} catch (err) { } catch (err) {
setError(err instanceof Error ? err : new Error('Failed to delete exercise')); setError(err instanceof Error ? err : new Error('Failed to delete exercise'));
throw err; throw err;
} }
}, [libraryService, loadExercises]); }, [libraryService, refreshExercises]);
// Update an exercise // Update an exercise
const updateExercise = useCallback(async (id: string, updateData: Partial<BaseExercise>) => { const updateExercise = useCallback(async (id: string, updateData: Partial<BaseExercise>) => {
@ -189,15 +208,15 @@ export function useExercises() {
// Add the updated exercise with the same ID // Add the updated exercise with the same ID
await libraryService.addExercise(exerciseWithoutId); await libraryService.addExercise(exerciseWithoutId);
// Reload exercises to get the updated list // Refresh exercises to get the updated list
await loadExercises(); refreshExercises();
return id; return id;
} catch (err) { } catch (err) {
setError(err instanceof Error ? err : new Error('Failed to update exercise')); setError(err instanceof Error ? err : new Error('Failed to update exercise'));
throw err; throw err;
} }
}, [libraryService, loadExercises]); }, [libraryService, refreshExercises]);
// Update filters // Update filters
const updateFilters = useCallback((newFilters: Partial<ExerciseFilters>) => { const updateFilters = useCallback((newFilters: Partial<ExerciseFilters>) => {
@ -212,14 +231,9 @@ export function useExercises() {
setFilters({}); setFilters({});
}, []); }, []);
// Initial load
useEffect(() => {
loadExercises();
}, [loadExercises]);
return { return {
exercises: getFilteredExercises(), exercises: getFilteredExercises(),
loading, loading: isLoading,
error, error,
stats, stats,
filters, filters,
@ -228,6 +242,7 @@ export function useExercises() {
createExercise, createExercise,
deleteExercise, deleteExercise,
updateExercise, updateExercise,
refreshExercises: loadExercises refreshExercises, // Return the refresh function from the store
silentRefresh // Add the silent refresh function
}; };
} }

@ -1,20 +1,21 @@
// lib/hooks/usePOWRPacks.ts // lib/hooks/usePOWRpacks.ts
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { usePOWRPackService } from '@/components/DatabaseProvider'; import { usePOWRPackService } from '@/components/DatabaseProvider';
import { useNDK } from '@/lib/hooks/useNDK'; import { useNDK } from '@/lib/hooks/useNDK';
import { POWRPackWithContent, POWRPackImport, POWRPackSelection } from '@/types/powr-pack'; import { POWRPackWithContent, POWRPackImport, POWRPackSelection } from '@/types/powr-pack';
import { router } from 'expo-router'; import { usePackRefresh } from '@/lib/stores/libraryStore';
export function usePOWRPacks() { export function usePOWRPacks() {
const powrPackService = usePOWRPackService(); const powrPackService = usePOWRPackService();
const { ndk } = useNDK(); const { ndk } = useNDK();
const { refreshCount, refreshPacks, isLoading, setLoading } = usePackRefresh();
const [packs, setPacks] = useState<POWRPackWithContent[]>([]); const [packs, setPacks] = useState<POWRPackWithContent[]>([]);
const [isLoading, setIsLoading] = useState(false);
// Load all packs // Load all packs
const loadPacks = useCallback(async () => { const loadPacks = useCallback(async () => {
setIsLoading(true); setLoading(true);
try { try {
const importedPacks = await powrPackService.getImportedPacks(); const importedPacks = await powrPackService.getImportedPacks();
setPacks(importedPacks); setPacks(importedPacks);
@ -23,14 +24,14 @@ export function usePOWRPacks() {
console.error('Error loading POWR packs:', error); console.error('Error loading POWR packs:', error);
return []; return [];
} finally { } finally {
setIsLoading(false); setLoading(false);
} }
}, [powrPackService]); }, [powrPackService, setLoading]);
// Load packs on mount // Load packs when refreshCount changes
useEffect(() => { useEffect(() => {
loadPacks(); loadPacks();
}, [loadPacks]); }, [refreshCount, loadPacks]);
// Fetch a pack from an naddr // Fetch a pack from an naddr
const fetchPack = useCallback(async (naddr: string): Promise<POWRPackImport | null> => { const fetchPack = useCallback(async (naddr: string): Promise<POWRPackImport | null> => {
@ -52,28 +53,28 @@ export function usePOWRPacks() {
const importPack = useCallback(async (packImport: POWRPackImport, selection: POWRPackSelection) => { const importPack = useCallback(async (packImport: POWRPackImport, selection: POWRPackSelection) => {
try { try {
await powrPackService.importPack(packImport, selection); await powrPackService.importPack(packImport, selection);
await loadPacks(); // Refresh the list // No need to call loadPacks here as the store refresh will trigger it
return true; return true;
} catch (error) { } catch (error) {
console.error('Error importing pack:', error); console.error('Error importing pack:', error);
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to import pack'); Alert.alert('Error', error instanceof Error ? error.message : 'Failed to import pack');
return false; return false;
} }
}, [powrPackService, loadPacks]); }, [powrPackService]);
// Delete a pack // Delete a pack
const deletePack = useCallback(async (packId: string) => { const deletePack = useCallback(async (packId: string) => {
try { try {
// Always delete everything // Always delete everything
await powrPackService.deletePack(packId, false); await powrPackService.deletePack(packId, false);
await loadPacks(); // Refresh the list // No need to call loadPacks here as the store refresh will trigger it
return true; return true;
} catch (error) { } catch (error) {
console.error('Error deleting pack:', error); console.error('Error deleting pack:', error);
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to delete pack'); Alert.alert('Error', error instanceof Error ? error.message : 'Failed to delete pack');
return false; return false;
} }
}, [powrPackService, loadPacks]); }, [powrPackService]);
// Helper to copy pack address to clipboard (for future implementation) // Helper to copy pack address to clipboard (for future implementation)
const copyPackAddress = useCallback((naddr: string) => { const copyPackAddress = useCallback((naddr: string) => {
@ -90,6 +91,7 @@ export function usePOWRPacks() {
fetchPack, fetchPack,
importPack, importPack,
deletePack, deletePack,
copyPackAddress copyPackAddress,
refreshPacks // Return the refresh function from the store
}; };
} }

@ -1,20 +1,30 @@
// lib/hooks/useTemplates.ts // lib/hooks/useTemplates.ts
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { WorkoutTemplate } from '@/types/templates'; import { WorkoutTemplate } from '@/types/templates';
import { useTemplateService } from '@/components/DatabaseProvider'; import { useTemplateService } from '@/components/DatabaseProvider';
import { useTemplateRefresh } from '@/lib/stores/libraryStore';
export function useTemplates() { export function useTemplates() {
const templateService = useTemplateService(); const templateService = useTemplateService();
const { refreshCount, refreshTemplates, isLoading, setLoading } = useTemplateRefresh();
const [templates, setTemplates] = useState<WorkoutTemplate[]>([]); const [templates, setTemplates] = useState<WorkoutTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const [archivedTemplates, setArchivedTemplates] = useState<WorkoutTemplate[]>([]); const [archivedTemplates, setArchivedTemplates] = useState<WorkoutTemplate[]>([]);
// Add a loaded flag to track if we've successfully loaded templates at least once
const hasLoadedRef = useRef(false);
const loadTemplates = useCallback(async (limit: number = 50, offset: number = 0) => { const loadTemplates = useCallback(async (limit: number = 50, offset: number = 0, showLoading: boolean = true) => {
try { try {
setLoading(true); // Only show loading indicator if we haven't loaded before or if explicitly requested
if (showLoading && (!hasLoadedRef.current || templates.length === 0)) {
setLoading(true);
}
const data = await templateService.getAllTemplates(limit, offset); const data = await templateService.getAllTemplates(limit, offset);
setTemplates(data); setTemplates(data);
hasLoadedRef.current = true;
setError(null); setError(null);
} catch (err) { } catch (err) {
console.error('Error loading templates:', err); console.error('Error loading templates:', err);
@ -24,8 +34,14 @@ export function useTemplates() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [templateService]); }, [templateService, setLoading, templates.length]);
// Load templates when refreshCount changes
useEffect(() => {
loadTemplates();
}, [refreshCount, loadTemplates]);
// The rest of your methods remain the same
const getTemplate = useCallback(async (id: string) => { const getTemplate = useCallback(async (id: string) => {
try { try {
return await templateService.getTemplate(id); return await templateService.getTemplate(id);
@ -41,86 +57,81 @@ export function useTemplates() {
const templateWithDefaults = { const templateWithDefaults = {
...template, ...template,
isArchived: template.isArchived !== undefined ? template.isArchived : false, isArchived: template.isArchived !== undefined ? template.isArchived : false,
// Only set authorPubkey if not provided and we have an authenticated user
// (you would need to import useNDKCurrentUser from your NDK hooks)
}; };
const id = await templateService.createTemplate(templateWithDefaults); const id = await templateService.createTemplate(templateWithDefaults);
await loadTemplates(); // Refresh the list refreshTemplates(); // Use the store's refresh function
return id; return id;
} catch (err) { } catch (err) {
console.error('Error creating template:', err); console.error('Error creating template:', err);
throw err; throw err;
} }
}, [templateService, loadTemplates]); }, [templateService, refreshTemplates]);
const updateTemplate = useCallback(async (id: string, updates: Partial<WorkoutTemplate>) => { const updateTemplate = useCallback(async (id: string, updates: Partial<WorkoutTemplate>) => {
try { try {
await templateService.updateTemplate(id, updates); await templateService.updateTemplate(id, updates);
await loadTemplates(); // Refresh the list refreshTemplates(); // Use the store's refresh function
} catch (err) { } catch (err) {
console.error('Error updating template:', err); console.error('Error updating template:', err);
throw err; throw err;
} }
}, [templateService, loadTemplates]); }, [templateService, refreshTemplates]);
const deleteTemplate = useCallback(async (id: string) => { const deleteTemplate = useCallback(async (id: string) => {
try { try {
await templateService.deleteTemplate(id); await templateService.deleteTemplate(id);
setTemplates(current => current.filter(t => t.id !== id)); setTemplates(current => current.filter(t => t.id !== id));
refreshTemplates(); // Also trigger a refresh to ensure consistency
} catch (err) { } catch (err) {
console.error('Error deleting template:', err); console.error('Error deleting template:', err);
throw err; throw err;
} }
}, [templateService]); }, [templateService, refreshTemplates]);
// Add new archive/unarchive method
const archiveTemplate = useCallback(async (id: string, archive: boolean = true) => { const archiveTemplate = useCallback(async (id: string, archive: boolean = true) => {
try { try {
await templateService.archiveTemplate(id, archive); await templateService.archiveTemplate(id, archive);
await loadTemplates(); // Refresh the list refreshTemplates(); // Use the store's refresh function
} catch (err) { } catch (err) {
console.error(`Error ${archive ? 'archiving' : 'unarchiving'} template:`, err); console.error(`Error ${archive ? 'archiving' : 'unarchiving'} template:`, err);
throw err; throw err;
} }
}, [templateService, loadTemplates]); }, [templateService, refreshTemplates]);
// Add support for loading archived templates
const loadArchivedTemplates = useCallback(async (limit: number = 50, offset: number = 0) => { const loadArchivedTemplates = useCallback(async (limit: number = 50, offset: number = 0) => {
try { try {
setLoading(true); setLoading(true);
const data = await templateService.getArchivedTemplates(limit, offset); const data = await templateService.getArchivedTemplates(limit, offset);
// You might want to store archived templates in a separate state variable
// For now, I'll assume you want to replace the main templates list
setTemplates(data);
setError(null);
setArchivedTemplates(data); setArchivedTemplates(data);
setError(null);
} catch (err) { } catch (err) {
console.error('Error loading archived templates:', err); console.error('Error loading archived templates:', err);
setError(err instanceof Error ? err : new Error('Failed to load archived templates')); setError(err instanceof Error ? err : new Error('Failed to load archived templates'));
setTemplates([]); setArchivedTemplates([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [templateService]); }, [templateService, setLoading]);
// Initial load // Add a silentRefresh method that doesn't show loading indicators
useEffect(() => { const silentRefresh = useCallback(() => {
loadTemplates(); loadTemplates(50, 0, false);
}, [loadTemplates]); }, [loadTemplates]);
return { return {
templates, templates,
archivedTemplates, archivedTemplates,
loading, loading: isLoading,
error, error,
loadTemplates, loadTemplates,
silentRefresh,
loadArchivedTemplates, loadArchivedTemplates,
getTemplate, getTemplate,
createTemplate, createTemplate,
updateTemplate, updateTemplate,
deleteTemplate, deleteTemplate,
archiveTemplate, archiveTemplate,
refreshTemplates: loadTemplates refreshTemplates
}; };
} }

127
lib/stores/libraryStore.ts Normal file

@ -0,0 +1,127 @@
// lib/stores/libraryStore.ts
import { create } from 'zustand';
import { createSelectors } from '@/utils/createSelectors';
import { ExerciseDisplay } from '@/types/exercise';
import { WorkoutTemplate } from '@/types/templates';
import { POWRPackWithContent } from '@/types/powr-pack';
interface LibraryState {
// Refresh counters
exerciseRefreshCount: number;
templateRefreshCount: number;
packRefreshCount: number;
// Loading states
exercisesLoading: boolean;
templatesLoading: boolean;
packsLoading: boolean;
// Optional cached data (for performance)
cachedExercises: ExerciseDisplay[] | null;
cachedTemplates: WorkoutTemplate[] | null;
cachedPacks: POWRPackWithContent[] | null;
}
interface LibraryActions {
// Refresh triggers
refreshExercises: () => void;
refreshTemplates: () => void;
refreshPacks: () => void;
refreshAll: () => void;
// Loading state setters
setExercisesLoading: (loading: boolean) => void;
setTemplatesLoading: (loading: boolean) => void;
setPacksLoading: (loading: boolean) => void;
// Cache management
setCachedExercises: (exercises: ExerciseDisplay[] | null) => void;
setCachedTemplates: (templates: WorkoutTemplate[] | null) => void;
setCachedPacks: (packs: POWRPackWithContent[] | null) => void;
clearCache: () => void;
}
const initialState: LibraryState = {
exerciseRefreshCount: 0,
templateRefreshCount: 0,
packRefreshCount: 0,
exercisesLoading: false,
templatesLoading: false,
packsLoading: false,
cachedExercises: null,
cachedTemplates: null,
cachedPacks: null
};
const useLibraryStoreBase = create<LibraryState & LibraryActions>((set) => ({
...initialState,
refreshExercises: () => set(state => ({
exerciseRefreshCount: state.exerciseRefreshCount + 1
})),
refreshTemplates: () => set(state => ({
templateRefreshCount: state.templateRefreshCount + 1
})),
refreshPacks: () => set(state => ({
packRefreshCount: state.packRefreshCount + 1
})),
refreshAll: () => set(state => ({
exerciseRefreshCount: state.exerciseRefreshCount + 1,
templateRefreshCount: state.templateRefreshCount + 1,
packRefreshCount: state.packRefreshCount + 1
})),
setExercisesLoading: (loading) => set({ exercisesLoading: loading }),
setTemplatesLoading: (loading) => set({ templatesLoading: loading }),
setPacksLoading: (loading) => set({ packsLoading: loading }),
setCachedExercises: (exercises) => set({ cachedExercises: exercises }),
setCachedTemplates: (templates) => set({ cachedTemplates: templates }),
setCachedPacks: (packs) => set({ cachedPacks: packs }),
clearCache: () => set({
cachedExercises: null,
cachedTemplates: null,
cachedPacks: null
})
}));
// Create auto-generated selectors
export const useLibraryStore = createSelectors(useLibraryStoreBase);
// Export some convenient hooks
export function useExerciseRefresh() {
return {
refreshCount: useLibraryStore(state => state.exerciseRefreshCount),
refreshExercises: useLibraryStore(state => state.refreshExercises),
isLoading: useLibraryStore(state => state.exercisesLoading),
setLoading: useLibraryStore(state => state.setExercisesLoading)
};
}
export function useTemplateRefresh() {
return {
refreshCount: useLibraryStore(state => state.templateRefreshCount),
refreshTemplates: useLibraryStore(state => state.refreshTemplates),
isLoading: useLibraryStore(state => state.templatesLoading),
setLoading: useLibraryStore(state => state.setTemplatesLoading)
};
}
export function usePackRefresh() {
return {
refreshCount: useLibraryStore(state => state.packRefreshCount),
refreshPacks: useLibraryStore(state => state.refreshPacks),
isLoading: useLibraryStore(state => state.packsLoading),
setLoading: useLibraryStore(state => state.setPacksLoading)
};
}
export function useLibraryRefresh() {
return {
refreshAll: useLibraryStore(state => state.refreshAll)
};
}