mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
added library store and fixed refresh issues in exercise and template screens in user library
This commit is contained in:
parent
6ac0e80e45
commit
c6a9af080c
@ -14,6 +14,7 @@ import { Trash2, PackageOpen, Plus, X } from 'lucide-react-native';
|
||||
import { useIconColor } from '@/lib/theme/iconUtils';
|
||||
import { 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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
@ -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
127
lib/stores/libraryStore.ts
Normal file
@ -0,0 +1,127 @@
|
||||
// lib/stores/libraryStore.ts
|
||||
import { create } from 'zustand';
|
||||
import { createSelectors } from '@/utils/createSelectors';
|
||||
import { ExerciseDisplay } from '@/types/exercise';
|
||||
import { WorkoutTemplate } from '@/types/templates';
|
||||
import { POWRPackWithContent } from '@/types/powr-pack';
|
||||
|
||||
interface LibraryState {
|
||||
// Refresh counters
|
||||
exerciseRefreshCount: number;
|
||||
templateRefreshCount: number;
|
||||
packRefreshCount: number;
|
||||
|
||||
// Loading states
|
||||
exercisesLoading: boolean;
|
||||
templatesLoading: boolean;
|
||||
packsLoading: boolean;
|
||||
|
||||
// Optional cached data (for performance)
|
||||
cachedExercises: ExerciseDisplay[] | null;
|
||||
cachedTemplates: WorkoutTemplate[] | null;
|
||||
cachedPacks: POWRPackWithContent[] | null;
|
||||
}
|
||||
|
||||
interface LibraryActions {
|
||||
// Refresh triggers
|
||||
refreshExercises: () => void;
|
||||
refreshTemplates: () => void;
|
||||
refreshPacks: () => void;
|
||||
refreshAll: () => void;
|
||||
|
||||
// Loading state setters
|
||||
setExercisesLoading: (loading: boolean) => void;
|
||||
setTemplatesLoading: (loading: boolean) => void;
|
||||
setPacksLoading: (loading: boolean) => void;
|
||||
|
||||
// Cache management
|
||||
setCachedExercises: (exercises: ExerciseDisplay[] | null) => void;
|
||||
setCachedTemplates: (templates: WorkoutTemplate[] | null) => void;
|
||||
setCachedPacks: (packs: POWRPackWithContent[] | null) => void;
|
||||
clearCache: () => void;
|
||||
}
|
||||
|
||||
const initialState: LibraryState = {
|
||||
exerciseRefreshCount: 0,
|
||||
templateRefreshCount: 0,
|
||||
packRefreshCount: 0,
|
||||
exercisesLoading: false,
|
||||
templatesLoading: false,
|
||||
packsLoading: false,
|
||||
cachedExercises: null,
|
||||
cachedTemplates: null,
|
||||
cachedPacks: null
|
||||
};
|
||||
|
||||
const useLibraryStoreBase = create<LibraryState & LibraryActions>((set) => ({
|
||||
...initialState,
|
||||
|
||||
refreshExercises: () => set(state => ({
|
||||
exerciseRefreshCount: state.exerciseRefreshCount + 1
|
||||
})),
|
||||
|
||||
refreshTemplates: () => set(state => ({
|
||||
templateRefreshCount: state.templateRefreshCount + 1
|
||||
})),
|
||||
|
||||
refreshPacks: () => set(state => ({
|
||||
packRefreshCount: state.packRefreshCount + 1
|
||||
})),
|
||||
|
||||
refreshAll: () => set(state => ({
|
||||
exerciseRefreshCount: state.exerciseRefreshCount + 1,
|
||||
templateRefreshCount: state.templateRefreshCount + 1,
|
||||
packRefreshCount: state.packRefreshCount + 1
|
||||
})),
|
||||
|
||||
setExercisesLoading: (loading) => set({ exercisesLoading: loading }),
|
||||
setTemplatesLoading: (loading) => set({ templatesLoading: loading }),
|
||||
setPacksLoading: (loading) => set({ packsLoading: loading }),
|
||||
|
||||
setCachedExercises: (exercises) => set({ cachedExercises: exercises }),
|
||||
setCachedTemplates: (templates) => set({ cachedTemplates: templates }),
|
||||
setCachedPacks: (packs) => set({ cachedPacks: packs }),
|
||||
|
||||
clearCache: () => set({
|
||||
cachedExercises: null,
|
||||
cachedTemplates: null,
|
||||
cachedPacks: null
|
||||
})
|
||||
}));
|
||||
|
||||
// Create auto-generated selectors
|
||||
export const useLibraryStore = createSelectors(useLibraryStoreBase);
|
||||
|
||||
// Export some convenient hooks
|
||||
export function useExerciseRefresh() {
|
||||
return {
|
||||
refreshCount: useLibraryStore(state => state.exerciseRefreshCount),
|
||||
refreshExercises: useLibraryStore(state => state.refreshExercises),
|
||||
isLoading: useLibraryStore(state => state.exercisesLoading),
|
||||
setLoading: useLibraryStore(state => state.setExercisesLoading)
|
||||
};
|
||||
}
|
||||
|
||||
export function useTemplateRefresh() {
|
||||
return {
|
||||
refreshCount: useLibraryStore(state => state.templateRefreshCount),
|
||||
refreshTemplates: useLibraryStore(state => state.refreshTemplates),
|
||||
isLoading: useLibraryStore(state => state.templatesLoading),
|
||||
setLoading: useLibraryStore(state => state.setTemplatesLoading)
|
||||
};
|
||||
}
|
||||
|
||||
export function usePackRefresh() {
|
||||
return {
|
||||
refreshCount: useLibraryStore(state => state.packRefreshCount),
|
||||
refreshPacks: useLibraryStore(state => state.refreshPacks),
|
||||
isLoading: useLibraryStore(state => state.packsLoading),
|
||||
setLoading: useLibraryStore(state => state.setPacksLoading)
|
||||
};
|
||||
}
|
||||
|
||||
export function useLibraryRefresh() {
|
||||
return {
|
||||
refreshAll: useLibraryStore(state => state.refreshAll)
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user