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:
parent
6ac0e80e45
commit
c6a9af080c
@ -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
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)
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user