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

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

View File

@ -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 }}
>
<Plus size={18} color="#fff" style={{ marginRight: 8 }} />
<Text style={{ color: '#fff', fontWeight: '500' }}>Import New Pack</Text>
</Button>
@ -173,18 +173,28 @@ export default function ManagePOWRPacksScreen() {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Text>Delete Pack</Text>
<Text className="text-xl font-semibold text-foreground">Delete Pack</Text>
</AlertDialogTitle>
<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>
</AlertDialogHeader>
<View className="flex-row justify-center gap-3 px-4 mt-2">
<AlertDialogCancel onPress={() => setShowDeleteDialog(false)}>
<Text>Cancel</Text>
<View className="flex-row justify-end gap-3">
<AlertDialogCancel asChild>
<Button variant="outline" className="mr-2">
<Text>Cancel</Text>
</Button>
</AlertDialogCancel>
<AlertDialogAction onPress={handleDeleteConfirm} className='bg-destructive'>
<Text className='text-destructive-foreground'>Delete Pack</Text>
<AlertDialogAction asChild>
<Button
variant="destructive"
onPress={handleDeleteConfirm}
style={{ backgroundColor: FIXED_COLORS.destructive }}
>
<Text style={{ color: '#FFFFFF' }}>Delete Pack</Text>
</Button>
</AlertDialogAction>
</View>
</AlertDialogContent>

View File

@ -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<ExerciseDisplay | null>(null);
// Delete alert state
const [exerciseToDelete, setExerciseToDelete] = useState<ExerciseDisplay | null>(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 (
<View className="flex-1 bg-background">
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} {...getIconProps('primary')} />
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<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>
<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>
</Button>
</View>
</View>
</Button>
</View>
</View>
</View>
</View>
{/* Filter Sheet */}
<FilterSheet
@ -242,15 +299,23 @@ export default function ExercisesScreen() {
availableFilters={availableFilters}
/>
{/* Exercises list */}
<SimplifiedExerciseList
exercises={exercises}
onExercisePress={handleExercisePress}
/>
{/* Loading indicator */}
{loading ? (
<View className="flex-1 items-center justify-center">
<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 */}
<ModalExerciseDetails
exercise={selectedExercise} // This can now be null
exercise={selectedExercise}
open={!!selectedExercise}
onClose={() => setSelectedExercise(null)}
onEdit={handleEdit}
@ -272,6 +337,38 @@ export default function ExercisesScreen() {
exerciseToEdit={exerciseToEdit}
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 File

@ -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<FilterOptions>(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<string | null>(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 */}
<View className="p-2">
<Button
variant="outline"
size="sm"
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>
{/* Templates list with loading state */}
{loading ? (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="small" className="mb-2" />
<Text className="text-muted-foreground">Loading templates...</Text>
</View>
) : null}
{/* Templates list */}
<ScrollView className="flex-1">
{/* Favorites Section */}
{favoriteTemplates.length > 0 && (
<View className="py-4">
<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.'}
) : (
<ScrollView className="flex-1">
{/* Favorites Section */}
{favoriteTemplates.length > 0 && (
<View className="py-4">
<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(template.id)}
onFavorite={() => handleFavorite(template)}
onStartWorkout={() => handleStartWorkout(template)}
/>
))}
</View>
</View>
)}
</View>
{/* Add some bottom padding for FAB */}
<View className="h-20" />
</ScrollView>
{/* 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(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 && (
<FloatingActionButton

View File

@ -1,3 +1,4 @@
// components/DatabaseProvider.tsx
import React from 'react';
import { View, ActivityIndicator, ScrollView, Text } from 'react-native';
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 { logDatabaseInfo } from '@/lib/db/debug';
import { useNDKStore } from '@/lib/stores/ndk';
import { useLibraryStore } from '@/lib/stores/libraryStore';
// Create context for services
interface DatabaseServicesContextValue {
@ -85,6 +87,15 @@ export function DatabaseProvider({ children }: DatabaseProviderProps) {
}
}, [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(() => {
async function initDatabase() {
try {

View File

@ -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<SectionList>(null);
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
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 (
<TouchableOpacity
@ -137,7 +146,7 @@ export const SimplifiedExerciseList = ({
{/* Weight/Rep information if it was a WorkoutExercise */}
{isWorkoutExercise(item) && (
<View className="items-end">
<View className="items-end mr-2">
<Text className="text-muted-foreground text-sm">
{item.sets?.[0]?.weight && `${item.sets[0].weight} lb`}
{item.sets?.[0]?.weight && item.sets?.[0]?.reps && ' '}
@ -145,6 +154,20 @@ export const SimplifiedExerciseList = ({
</Text>
</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>
);
};

View File

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

View File

@ -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({
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button variant="destructive" onPress={handleConfirmDelete}>
<Text className="text-destructive-foreground">Delete</Text>
<Button
variant="destructive"
onPress={handleConfirmDelete}
style={{ backgroundColor: FIXED_COLORS.destructive }}
>
<Text style={{ color: '#FFFFFF' }}>Delete</Text>
</Button>
</AlertDialogAction>
</View>

View File

@ -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> {
try {
// Check if we already have this exercise

View File

@ -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) {

View File

@ -452,6 +452,22 @@ export class TemplateService {
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

View File

@ -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<ExerciseDisplay[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [filters, setFilters] = useState<ExerciseFilters>({});
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
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<BaseExercise>) => {
@ -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<ExerciseFilters>) => {
@ -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
};
}

View File

@ -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<POWRPackWithContent[]>([]);
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<POWRPackImport | null> => {
@ -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
};
}

View File

@ -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<WorkoutTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
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 {
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<WorkoutTemplate>) => {
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
};
}

127
lib/stores/libraryStore.ts Normal file
View 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)
};
}