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.
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Delete
+
+
+
+
+
);
}
\ 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 Templates
-
-
-
- {/* 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({
-
- Delete
+
+ Delete
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