diff --git a/app/(packs)/manage.tsx b/app/(packs)/manage.tsx index 3eac3d4..0981f5d 100644 --- a/app/(packs)/manage.tsx +++ b/app/(packs)/manage.tsx @@ -14,6 +14,7 @@ import { Trash2, PackageOpen, Plus, X } from 'lucide-react-native'; import { useIconColor } from '@/lib/theme/iconUtils'; import { useColorScheme } from '@/lib/theme/useColorScheme'; import { COLORS } from '@/lib/theme/colors'; +import { FIXED_COLORS } from '@/lib/theme/colors'; export default function ManagePOWRPacksScreen() { const powrPackService = usePOWRPackService(); @@ -102,7 +103,6 @@ export default function ManagePOWRPacksScreen() { className="mb-4" style={{ backgroundColor: COLORS.purple.DEFAULT }} > - Import New Pack @@ -173,18 +173,28 @@ export default function ManagePOWRPacksScreen() { - Delete Pack + Delete Pack - This will remove the POWR Pack and all its associated templates and exercises from your library. + + This will remove the POWR Pack and all its associated templates and exercises from your library. + - - setShowDeleteDialog(false)}> - Cancel + + + - - Delete Pack + + diff --git a/app/(tabs)/library/exercises.tsx b/app/(tabs)/library/exercises.tsx index 27f13b1..cfadb4f 100644 --- a/app/(tabs)/library/exercises.tsx +++ b/app/(tabs)/library/exercises.tsx @@ -1,6 +1,6 @@ // app/(tabs)/library/exercises.tsx 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 { Input } from '@/components/ui/input'; import { Search, Dumbbell, ListFilter } from 'lucide-react-native'; @@ -15,6 +15,19 @@ import { useWorkoutStore } from '@/stores/workoutStore'; import { generateId } from '@/utils/ids'; import { useNDKStore } from '@/lib/stores/ndk'; 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 const availableFilters = { @@ -46,6 +59,10 @@ export default function ExercisesScreen() { // Exercise details state const [selectedExercise, setSelectedExercise] = useState(null); + // Delete alert state + const [exerciseToDelete, setExerciseToDelete] = useState(null); + const [showDeleteAlert, setShowDeleteAlert] = useState(false); + // Other hooks const { isActive, isMinimized } = useWorkoutStore(); const { currentUser } = useNDKStore(); @@ -59,9 +76,19 @@ export default function ExercisesScreen() { deleteExercise, updateExercise, refreshExercises, + silentRefresh, // Add this updateFilters, 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 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 ( - {/* Search bar with filter button */} - - - - - + {/* Search bar with filter button */} + + + + + + + + + - - + + + {/* Filter Sheet */} - {/* Exercises list */} - + {/* Loading indicator */} + {loading ? ( + + + Loading exercises... + + ) : ( + + )} {/* Exercise details sheet */} setSelectedExercise(null)} onEdit={handleEdit} @@ -272,6 +337,38 @@ export default function ExercisesScreen() { exerciseToEdit={exerciseToEdit} mode={editMode} /> + + {/* Delete Confirmation Dialog */} + + + + + Delete Exercise + + + + Are you sure you want to delete {exerciseToDelete?.title}? This action cannot be undone. + + + + + + + + + + + + + ); } \ No newline at end of file diff --git a/app/(tabs)/library/templates.tsx b/app/(tabs)/library/templates.tsx index 1d785c6..86c7115 100644 --- a/app/(tabs)/library/templates.tsx +++ b/app/(tabs)/library/templates.tsx @@ -1,6 +1,6 @@ // app/(tabs)/library/templates.tsx import React, { useState } from 'react'; -import { View, ScrollView } from 'react-native'; +import { View, ScrollView, ActivityIndicator } from 'react-native'; import { router } from 'expo-router'; import { Text } from '@/components/ui/text'; import { Input } from '@/components/ui/input'; @@ -18,7 +18,8 @@ import { toWorkoutTemplate } from '@/types/templates'; 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 const availableFilters = { @@ -34,27 +35,77 @@ const initialFilters: FilterOptions = { source: [] }; -// Initial templates - empty array -const initialTemplates: Template[] = []; - export default function TemplatesScreen() { - const templateService = useTemplateService(); // Get the template service + // State for UI elements const [showNewTemplate, setShowNewTemplate] = useState(false); - const [templates, setTemplates] = useState(initialTemplates); const [searchQuery, setSearchQuery] = useState(''); const [filterSheetOpen, setFilterSheetOpen] = useState(false); const [currentFilters, setCurrentFilters] = useState(initialFilters); const [activeFilters, setActiveFilters] = useState(0); - const { isActive, isMinimized } = useWorkoutStore(); - const shouldShowFAB = !isActive || !isMinimized; const [debugInfo, setDebugInfo] = useState(''); // State for the modal template details const [selectedTemplateId, setSelectedTemplateId] = useState(null); const [showTemplateModal, setShowTemplateModal] = useState(false); - const handleDelete = (id: string) => { - setTemplates(current => current.filter(t => t.id !== id)); + // Hooks + 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) => { @@ -65,9 +116,6 @@ export default function TemplatesScreen() { const handleStartWorkout = async (template: Template) => { try { - // Convert to WorkoutTemplate format - const workoutTemplate = toWorkoutTemplate(template); - // Start the workout - use the template ID await useWorkoutStore.getState().startWorkoutFromTemplate(template.id); @@ -88,15 +136,6 @@ export default function TemplatesScreen() { } else { await useWorkoutStore.getState().addFavorite(workoutTemplate); } - - // Update local state to reflect change - setTemplates(current => - current.map(t => - t.id === template.id - ? { ...t, isFavorite: !isFavorite } - : t - ) - ); } catch (error) { console.error('Error toggling favorite status:', error); } @@ -118,97 +157,22 @@ export default function TemplatesScreen() { // Handle favorite change from modal const handleModalFavoriteChange = (templateId: string, isFavorite: boolean) => { - // Update local state to reflect change - setTemplates(current => - current.map(t => - t.id === templateId - ? { ...t, isFavorite } - : t - ) - ); + // The templates will be refreshed automatically through the store }; - 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) => { - setTemplates(prev => [...prev, template]); + // Convert UI Template to WorkoutTemplate + const workoutTemplate = toWorkoutTemplate(template); + + // Create the template + createTemplate(workoutTemplate); + + // Close the sheet setShowNewTemplate(false); }; // Filter templates based on search and applied filters - const filteredTemplates = templates.filter(template => { + const filteredTemplates = formattedTemplates.filter(template => { // Filter by search query const matchesSearch = !searchQuery || template.title.toLowerCase().includes(searchQuery.toLowerCase()); @@ -278,79 +242,68 @@ export default function TemplatesScreen() { availableFilters={availableFilters} /> - {/* Debug button */} - - - - - {/* Debug info display */} - {debugInfo ? ( - - - {debugInfo} - + {/* Templates list with loading state */} + {loading ? ( + + + Loading templates... - ) : null} - - {/* Templates list */} - - {/* Favorites Section */} - {favoriteTemplates.length > 0 && ( - - - Favorites - - - {favoriteTemplates.map(template => ( - handleTemplatePress(template)} - onDelete={handleDelete} - onFavorite={() => handleFavorite(template)} - onStartWorkout={() => handleStartWorkout(template)} - /> - ))} - - - )} - - {/* All Templates Section */} - - - All Templates - - {regularTemplates.length > 0 ? ( - - {regularTemplates.map(template => ( - handleTemplatePress(template)} - onDelete={handleDelete} - onFavorite={() => handleFavorite(template)} - onStartWorkout={() => handleStartWorkout(template)} - /> - ))} - - ) : ( - - - No templates found. {templates.length > 0 ? 'Try changing your filters.' : 'Create a new workout template by clicking the + button.'} + ) : ( + + {/* Favorites Section */} + {favoriteTemplates.length > 0 && ( + + + Favorites + + {favoriteTemplates.map(template => ( + handleTemplatePress(template)} + onDelete={() => handleDelete(template.id)} + onFavorite={() => handleFavorite(template)} + onStartWorkout={() => handleStartWorkout(template)} + /> + ))} + )} - - {/* Add some bottom padding for FAB */} - - + {/* All Templates Section */} + + + All Templates + + {regularTemplates.length > 0 ? ( + + {regularTemplates.map(template => ( + handleTemplatePress(template)} + onDelete={() => handleDelete(template.id)} + onFavorite={() => handleFavorite(template)} + onStartWorkout={() => handleStartWorkout(template)} + /> + ))} + + ) : ( + + + {formattedTemplates.length > 0 + ? 'No templates match your current filters.' + : 'No templates found. Create a new workout template by clicking the + button.'} + + + )} + + + {/* Add some bottom padding for FAB */} + + + )} {shouldShowFAB && ( { + 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(() => { async function initDatabase() { try { diff --git a/components/exercises/SimplifiedExerciseList.tsx b/components/exercises/SimplifiedExerciseList.tsx index e312e0e..c2b4441 100644 --- a/components/exercises/SimplifiedExerciseList.tsx +++ b/components/exercises/SimplifiedExerciseList.tsx @@ -4,6 +4,17 @@ import { View, SectionList, TouchableOpacity, ViewToken } from 'react-native'; import { Text } from '@/components/ui/text'; import { Badge } from '@/components/ui/badge'; 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 interface DisplayWorkoutExercise extends ExerciseDisplay, WorkoutExercise {} @@ -11,14 +22,23 @@ interface DisplayWorkoutExercise extends ExerciseDisplay, WorkoutExercise {} interface SimplifiedExerciseListProps { exercises: ExerciseDisplay[]; onExercisePress: (exercise: ExerciseDisplay) => void; + onDeletePress?: (exercise: ExerciseDisplay) => void; // Add this } export const SimplifiedExerciseList = ({ exercises, - onExercisePress + onExercisePress, + onDeletePress }: SimplifiedExerciseListProps) => { const sectionListRef = useRef(null); const [currentSection, setCurrentSection] = useState(''); + const [exerciseToDelete, setExerciseToDelete] = useState(null); + const [showDeleteAlert, setShowDeleteAlert] = useState(false); + + const handleDeletePress = (exercise: ExerciseDisplay) => { + setExerciseToDelete(exercise); + setShowDeleteAlert(true); + }; // Organize exercises into sections 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) => ({ length: 85, // Approximate height of each item offset: 85 * index, @@ -78,6 +86,7 @@ export const SimplifiedExerciseList = ({ const renderExerciseItem = ({ item }: { item: ExerciseDisplay }) => { const firstLetter = item.title.charAt(0).toUpperCase(); + const canDelete = item.source === 'local'; return ( + {item.sets?.[0]?.weight && `${item.sets[0].weight} lb`} {item.sets?.[0]?.weight && item.sets?.[0]?.reps && ' '} @@ -145,6 +154,20 @@ export const SimplifiedExerciseList = ({ )} + + {/* Delete button (only for local exercises) */} + {canDelete && onDeletePress && ( + { + e.stopPropagation(); // Prevent triggering the parent TouchableOpacity + onDeletePress(item); + }} + className="p-2" + hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }} + > + + + )} ); }; diff --git a/components/social/NostrLoginPrompt.tsx b/components/social/NostrLoginPrompt.tsx index 30d95c4..1aef032 100644 --- a/components/social/NostrLoginPrompt.tsx +++ b/components/social/NostrLoginPrompt.tsx @@ -25,7 +25,6 @@ export default function NostrLoginPrompt({ message }: NostrLoginPromptProps) { onPress={() => setIsLoginSheetOpen(true)} className="px-6" > - Login with Nostr diff --git a/components/templates/TemplateCard.tsx b/components/templates/TemplateCard.tsx index 5db59e4..0c59e0a 100644 --- a/components/templates/TemplateCard.tsx +++ b/components/templates/TemplateCard.tsx @@ -19,6 +19,7 @@ import { } from '@/components/ui/alert-dialog'; import { Template, TemplateExerciseDisplay } from '@/types/templates'; import { useIconColor } from '@/lib/theme/iconUtils'; +import { FIXED_COLORS } from '@/lib/theme/colors'; interface TemplateCardProps { template: Template; @@ -199,8 +200,12 @@ export function TemplateCard({ - diff --git a/lib/db/services/ExerciseService.ts b/lib/db/services/ExerciseService.ts index 80903b8..a3c7788 100644 --- a/lib/db/services/ExerciseService.ts +++ b/lib/db/services/ExerciseService.ts @@ -366,6 +366,37 @@ export class ExerciseService { } } + // Add this to lib/db/services/ExerciseService.ts + async deleteExercise(id: string): Promise { + 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): Promise { try { // Check if we already have this exercise diff --git a/lib/db/services/POWRPackService.ts b/lib/db/services/POWRPackService.ts index 0cc5135..d7a69ef 100644 --- a/lib/db/services/POWRPackService.ts +++ b/lib/db/services/POWRPackService.ts @@ -16,6 +16,7 @@ import { } from '@/types/templates'; import '@/types/ndk-extensions'; import { safeAddRelay, safeRemoveRelay } from '@/types/ndk-common'; +import { useLibraryStore } from '@/lib/stores/libraryStore'; /** * Service for managing POWR Packs (importable collections of templates and exercises) @@ -592,6 +593,11 @@ export default class POWRPackService { // Finally, save the pack itself await this.savePack(packImport.packEvent, selection); + + // Trigger refresh of templates and exercises + useLibraryStore.getState().refreshTemplates(); + useLibraryStore.getState().refreshExercises(); + useLibraryStore.getState().refreshPacks(); // Get total counts const totalNostrTemplates = await this.db.getFirstAsync<{ count: number }>( @@ -949,6 +955,13 @@ export default class POWRPackService { [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`); }); } catch (error) { diff --git a/lib/db/services/TemplateService.ts b/lib/db/services/TemplateService.ts index d44c7e3..e84e45b 100644 --- a/lib/db/services/TemplateService.ts +++ b/lib/db/services/TemplateService.ts @@ -452,6 +452,22 @@ export class TemplateService { throw error; } } + + /** + * Check if a template exists in the database + */ + async templateExists(id: string): Promise { + 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 diff --git a/lib/hooks/useExercises.ts b/lib/hooks/useExercises.ts index b106ae2..d2f0eb8 100644 --- a/lib/hooks/useExercises.ts +++ b/lib/hooks/useExercises.ts @@ -1,5 +1,5 @@ // 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 { ExerciseDisplay, @@ -10,6 +10,7 @@ import { toExerciseDisplay } from '@/types/exercise'; import { LibraryService } from '../db/services/LibraryService'; +import { useExerciseRefresh } from '@/lib/stores/libraryStore'; // Filtering types export interface ExerciseFilters { @@ -38,19 +39,27 @@ const initialStats: ExerciseStats = { export function useExercises() { const db = useSQLiteContext(); const libraryService = React.useMemo(() => new LibraryService(db), [db]); + const { refreshCount, refreshExercises, isLoading, setLoading } = useExerciseRefresh(); const [exercises, setExercises] = useState([]); - const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filters, setFilters] = useState({}); const [stats, setStats] = useState(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 - const loadExercises = useCallback(async () => { + // Define loadExercises before using it in useEffect + const loadExercises = useCallback(async (showLoading: boolean = true) => { 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(); setExercises(allExercises); + hasLoadedRef.current = true; // Calculate stats const newStats = allExercises.reduce((acc: ExerciseStats, exercise: ExerciseDisplay) => { @@ -87,7 +96,17 @@ export function useExercises() { } finally { 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 const getFilteredExercises = useCallback(() => { @@ -142,24 +161,24 @@ export function useExercises() { }; 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; } catch (err) { setError(err instanceof Error ? err : new Error('Failed to create exercise')); throw err; } - }, [libraryService, loadExercises]); + }, [libraryService, refreshExercises]); // Delete an exercise const deleteExercise = useCallback(async (id: string) => { try { await libraryService.deleteExercise(id); - await loadExercises(); // Reload to update stats + refreshExercises(); // Use the store's refresh function } catch (err) { setError(err instanceof Error ? err : new Error('Failed to delete exercise')); throw err; } - }, [libraryService, loadExercises]); + }, [libraryService, refreshExercises]); // Update an exercise const updateExercise = useCallback(async (id: string, updateData: Partial) => { @@ -189,15 +208,15 @@ export function useExercises() { // Add the updated exercise with the same ID await libraryService.addExercise(exerciseWithoutId); - // Reload exercises to get the updated list - await loadExercises(); + // Refresh exercises to get the updated list + refreshExercises(); return id; } catch (err) { setError(err instanceof Error ? err : new Error('Failed to update exercise')); throw err; } - }, [libraryService, loadExercises]); + }, [libraryService, refreshExercises]); // Update filters const updateFilters = useCallback((newFilters: Partial) => { @@ -212,14 +231,9 @@ export function useExercises() { setFilters({}); }, []); - // Initial load - useEffect(() => { - loadExercises(); - }, [loadExercises]); - return { exercises: getFilteredExercises(), - loading, + loading: isLoading, error, stats, filters, @@ -228,6 +242,7 @@ export function useExercises() { createExercise, deleteExercise, updateExercise, - refreshExercises: loadExercises + refreshExercises, // Return the refresh function from the store + silentRefresh // Add the silent refresh function }; } \ No newline at end of file diff --git a/lib/hooks/usePOWRpacks.ts b/lib/hooks/usePOWRpacks.ts index ce630ff..d7217ab 100644 --- a/lib/hooks/usePOWRpacks.ts +++ b/lib/hooks/usePOWRpacks.ts @@ -1,20 +1,21 @@ -// lib/hooks/usePOWRPacks.ts +// lib/hooks/usePOWRpacks.ts import { useState, useEffect, useCallback } from 'react'; import { Alert } from 'react-native'; import { usePOWRPackService } from '@/components/DatabaseProvider'; import { useNDK } from '@/lib/hooks/useNDK'; import { POWRPackWithContent, POWRPackImport, POWRPackSelection } from '@/types/powr-pack'; -import { router } from 'expo-router'; +import { usePackRefresh } from '@/lib/stores/libraryStore'; export function usePOWRPacks() { const powrPackService = usePOWRPackService(); const { ndk } = useNDK(); + const { refreshCount, refreshPacks, isLoading, setLoading } = usePackRefresh(); + const [packs, setPacks] = useState([]); - const [isLoading, setIsLoading] = useState(false); // Load all packs const loadPacks = useCallback(async () => { - setIsLoading(true); + setLoading(true); try { const importedPacks = await powrPackService.getImportedPacks(); setPacks(importedPacks); @@ -23,14 +24,14 @@ export function usePOWRPacks() { console.error('Error loading POWR packs:', error); return []; } finally { - setIsLoading(false); + setLoading(false); } - }, [powrPackService]); + }, [powrPackService, setLoading]); - // Load packs on mount + // Load packs when refreshCount changes useEffect(() => { loadPacks(); - }, [loadPacks]); + }, [refreshCount, loadPacks]); // Fetch a pack from an naddr const fetchPack = useCallback(async (naddr: string): Promise => { @@ -52,28 +53,28 @@ export function usePOWRPacks() { const importPack = useCallback(async (packImport: POWRPackImport, selection: POWRPackSelection) => { try { 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; } catch (error) { console.error('Error importing pack:', error); Alert.alert('Error', error instanceof Error ? error.message : 'Failed to import pack'); return false; } - }, [powrPackService, loadPacks]); + }, [powrPackService]); // Delete a pack const deletePack = useCallback(async (packId: string) => { try { // Always delete everything 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; } catch (error) { console.error('Error deleting pack:', error); Alert.alert('Error', error instanceof Error ? error.message : 'Failed to delete pack'); return false; } - }, [powrPackService, loadPacks]); + }, [powrPackService]); // Helper to copy pack address to clipboard (for future implementation) const copyPackAddress = useCallback((naddr: string) => { @@ -90,6 +91,7 @@ export function usePOWRPacks() { fetchPack, importPack, deletePack, - copyPackAddress + copyPackAddress, + refreshPacks // Return the refresh function from the store }; } \ No newline at end of file diff --git a/lib/hooks/useTemplates.ts b/lib/hooks/useTemplates.ts index b56ee68..64f3132 100644 --- a/lib/hooks/useTemplates.ts +++ b/lib/hooks/useTemplates.ts @@ -1,20 +1,30 @@ // lib/hooks/useTemplates.ts -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { WorkoutTemplate } from '@/types/templates'; import { useTemplateService } from '@/components/DatabaseProvider'; +import { useTemplateRefresh } from '@/lib/stores/libraryStore'; export function useTemplates() { const templateService = useTemplateService(); + const { refreshCount, refreshTemplates, isLoading, setLoading } = useTemplateRefresh(); + const [templates, setTemplates] = useState([]); - const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [archivedTemplates, setArchivedTemplates] = useState([]); + + // 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 { - 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); setTemplates(data); + hasLoadedRef.current = true; setError(null); } catch (err) { console.error('Error loading templates:', err); @@ -24,8 +34,14 @@ export function useTemplates() { } finally { 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) => { try { return await templateService.getTemplate(id); @@ -41,86 +57,81 @@ export function useTemplates() { const templateWithDefaults = { ...template, 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); - await loadTemplates(); // Refresh the list + refreshTemplates(); // Use the store's refresh function return id; } catch (err) { console.error('Error creating template:', err); throw err; } - }, [templateService, loadTemplates]); + }, [templateService, refreshTemplates]); const updateTemplate = useCallback(async (id: string, updates: Partial) => { try { await templateService.updateTemplate(id, updates); - await loadTemplates(); // Refresh the list + refreshTemplates(); // Use the store's refresh function } catch (err) { console.error('Error updating template:', err); throw err; } - }, [templateService, loadTemplates]); + }, [templateService, refreshTemplates]); const deleteTemplate = useCallback(async (id: string) => { try { await templateService.deleteTemplate(id); setTemplates(current => current.filter(t => t.id !== id)); + refreshTemplates(); // Also trigger a refresh to ensure consistency } catch (err) { console.error('Error deleting template:', err); throw err; } - }, [templateService]); + }, [templateService, refreshTemplates]); - // Add new archive/unarchive method const archiveTemplate = useCallback(async (id: string, archive: boolean = true) => { try { await templateService.archiveTemplate(id, archive); - await loadTemplates(); // Refresh the list + refreshTemplates(); // Use the store's refresh function } catch (err) { console.error(`Error ${archive ? 'archiving' : 'unarchiving'} template:`, err); throw err; } - }, [templateService, loadTemplates]); + }, [templateService, refreshTemplates]); - // Add support for loading archived templates const loadArchivedTemplates = useCallback(async (limit: number = 50, offset: number = 0) => { try { setLoading(true); 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); + setError(null); } catch (err) { console.error('Error loading archived templates:', err); setError(err instanceof Error ? err : new Error('Failed to load archived templates')); - setTemplates([]); + setArchivedTemplates([]); } finally { setLoading(false); } - }, [templateService]); + }, [templateService, setLoading]); - // Initial load - useEffect(() => { - loadTemplates(); + // Add a silentRefresh method that doesn't show loading indicators + const silentRefresh = useCallback(() => { + loadTemplates(50, 0, false); }, [loadTemplates]); return { templates, archivedTemplates, - loading, + loading: isLoading, error, loadTemplates, + silentRefresh, loadArchivedTemplates, getTemplate, createTemplate, updateTemplate, deleteTemplate, archiveTemplate, - refreshTemplates: loadTemplates + refreshTemplates }; } \ No newline at end of file diff --git a/lib/stores/libraryStore.ts b/lib/stores/libraryStore.ts new file mode 100644 index 0000000..0573d1e --- /dev/null +++ b/lib/stores/libraryStore.ts @@ -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((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) + }; +} \ No newline at end of file