2025-02-18 12:15:19 -05:00
|
|
|
// lib/hooks/useExercises.ts
|
2025-03-17 23:37:08 -04:00
|
|
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
2025-02-18 12:15:19 -05:00
|
|
|
import { useSQLiteContext } from 'expo-sqlite';
|
2025-02-19 21:39:47 -05:00
|
|
|
import {
|
|
|
|
ExerciseDisplay,
|
|
|
|
ExerciseCategory,
|
|
|
|
Equipment,
|
|
|
|
ExerciseType,
|
|
|
|
BaseExercise,
|
|
|
|
toExerciseDisplay
|
|
|
|
} from '@/types/exercise';
|
2025-02-18 12:15:19 -05:00
|
|
|
import { LibraryService } from '../db/services/LibraryService';
|
2025-03-17 23:37:08 -04:00
|
|
|
import { useExerciseRefresh } from '@/lib/stores/libraryStore';
|
2025-02-18 12:15:19 -05:00
|
|
|
|
|
|
|
// Filtering types
|
|
|
|
export interface ExerciseFilters {
|
|
|
|
type?: ExerciseType[];
|
|
|
|
category?: ExerciseCategory[];
|
|
|
|
equipment?: Equipment[];
|
|
|
|
tags?: string[];
|
2025-02-19 21:39:47 -05:00
|
|
|
source?: ('local' | 'powr' | 'nostr')[];
|
2025-02-18 12:15:19 -05:00
|
|
|
searchQuery?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ExerciseStats {
|
|
|
|
totalCount: number;
|
2025-02-19 21:39:47 -05:00
|
|
|
byCategory: Partial<Record<ExerciseCategory, number>>;
|
|
|
|
byType: Partial<Record<ExerciseType, number>>;
|
|
|
|
byEquipment: Partial<Record<Equipment, number>>;
|
2025-02-18 12:15:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const initialStats: ExerciseStats = {
|
|
|
|
totalCount: 0,
|
2025-02-19 21:39:47 -05:00
|
|
|
byCategory: {},
|
|
|
|
byType: {},
|
|
|
|
byEquipment: {},
|
2025-02-18 12:15:19 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
export function useExercises() {
|
|
|
|
const db = useSQLiteContext();
|
|
|
|
const libraryService = React.useMemo(() => new LibraryService(db), [db]);
|
2025-03-17 23:37:08 -04:00
|
|
|
const { refreshCount, refreshExercises, isLoading, setLoading } = useExerciseRefresh();
|
2025-02-18 12:15:19 -05:00
|
|
|
|
2025-02-19 21:39:47 -05:00
|
|
|
const [exercises, setExercises] = useState<ExerciseDisplay[]>([]);
|
2025-02-18 12:15:19 -05:00
|
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
const [filters, setFilters] = useState<ExerciseFilters>({});
|
|
|
|
const [stats, setStats] = useState<ExerciseStats>(initialStats);
|
2025-03-17 23:37:08 -04:00
|
|
|
|
|
|
|
// Add a loaded flag to track if we've successfully loaded exercises at least once
|
|
|
|
const hasLoadedRef = useRef(false);
|
2025-02-18 12:15:19 -05:00
|
|
|
|
2025-03-17 23:37:08 -04:00
|
|
|
// Define loadExercises before using it in useEffect
|
|
|
|
const loadExercises = useCallback(async (showLoading: boolean = true) => {
|
2025-02-18 12:15:19 -05:00
|
|
|
try {
|
2025-03-17 23:37:08 -04:00
|
|
|
// Only show loading indicator if we haven't loaded before or if explicitly requested
|
|
|
|
if (showLoading && (!hasLoadedRef.current || exercises.length === 0)) {
|
|
|
|
setLoading(true);
|
|
|
|
}
|
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
const allExercises = await libraryService.getExercises();
|
|
|
|
setExercises(allExercises);
|
2025-03-17 23:37:08 -04:00
|
|
|
hasLoadedRef.current = true;
|
2025-02-18 12:15:19 -05:00
|
|
|
|
|
|
|
// Calculate stats
|
2025-02-19 21:39:47 -05:00
|
|
|
const newStats = allExercises.reduce((acc: ExerciseStats, exercise: ExerciseDisplay) => {
|
|
|
|
// Increment total count
|
2025-02-18 12:15:19 -05:00
|
|
|
acc.totalCount++;
|
2025-02-19 21:39:47 -05:00
|
|
|
|
|
|
|
// Update category stats with type checking
|
|
|
|
if (exercise.category) {
|
|
|
|
acc.byCategory[exercise.category] = (acc.byCategory[exercise.category] || 0) + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update type stats with type checking
|
|
|
|
if (exercise.type) {
|
|
|
|
acc.byType[exercise.type] = (acc.byType[exercise.type] || 0) + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update equipment stats with type checking
|
2025-02-18 12:15:19 -05:00
|
|
|
if (exercise.equipment) {
|
|
|
|
acc.byEquipment[exercise.equipment] = (acc.byEquipment[exercise.equipment] || 0) + 1;
|
|
|
|
}
|
2025-02-19 21:39:47 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
return acc;
|
|
|
|
}, {
|
|
|
|
totalCount: 0,
|
2025-02-19 21:39:47 -05:00
|
|
|
byCategory: {},
|
|
|
|
byType: {},
|
|
|
|
byEquipment: {},
|
2025-02-18 12:15:19 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
setStats(newStats);
|
|
|
|
setError(null);
|
|
|
|
} catch (err) {
|
|
|
|
setError(err instanceof Error ? err : new Error('Failed to load exercises'));
|
|
|
|
} finally {
|
|
|
|
setLoading(false);
|
|
|
|
}
|
2025-03-17 23:37:08 -04:00
|
|
|
}, [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]);
|
2025-02-18 12:15:19 -05:00
|
|
|
|
|
|
|
// Filter exercises based on current filters
|
|
|
|
const getFilteredExercises = useCallback(() => {
|
|
|
|
return exercises.filter(exercise => {
|
|
|
|
// Type filter
|
|
|
|
if (filters.type?.length && !filters.type.includes(exercise.type)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Category filter
|
|
|
|
if (filters.category?.length && !filters.category.includes(exercise.category)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Equipment filter
|
|
|
|
if (filters.equipment?.length && exercise.equipment && !filters.equipment.includes(exercise.equipment)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tags filter
|
2025-02-19 21:39:47 -05:00
|
|
|
if (filters.tags?.length && !exercise.tags.some((tag: string) => filters.tags?.includes(tag))) {
|
2025-02-18 12:15:19 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Source filter
|
|
|
|
if (filters.source?.length && !filters.source.includes(exercise.source)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Search query
|
|
|
|
if (filters.searchQuery) {
|
|
|
|
const query = filters.searchQuery.toLowerCase();
|
|
|
|
return (
|
|
|
|
exercise.title.toLowerCase().includes(query) ||
|
2025-02-19 21:39:47 -05:00
|
|
|
(exercise.description?.toLowerCase() || '').includes(query) ||
|
|
|
|
exercise.tags.some((tag: string) => tag.toLowerCase().includes(query))
|
2025-02-18 12:15:19 -05:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}, [exercises, filters]);
|
|
|
|
|
|
|
|
// Create a new exercise
|
2025-02-19 21:39:47 -05:00
|
|
|
const createExercise = useCallback(async (exercise: Omit<BaseExercise, 'id'>) => {
|
2025-02-18 12:15:19 -05:00
|
|
|
try {
|
2025-02-19 21:39:47 -05:00
|
|
|
// Create a display version of the exercise with source
|
|
|
|
const displayExercise: Omit<ExerciseDisplay, 'id'> = {
|
|
|
|
...exercise,
|
|
|
|
source: 'local', // Set default source for new exercises
|
|
|
|
isFavorite: false
|
|
|
|
};
|
|
|
|
|
|
|
|
const id = await libraryService.addExercise(displayExercise);
|
2025-03-17 23:37:08 -04:00
|
|
|
refreshExercises(); // Use the store's refresh function instead of loading directly
|
2025-02-18 12:15:19 -05:00
|
|
|
return id;
|
|
|
|
} catch (err) {
|
|
|
|
setError(err instanceof Error ? err : new Error('Failed to create exercise'));
|
|
|
|
throw err;
|
|
|
|
}
|
2025-03-17 23:37:08 -04:00
|
|
|
}, [libraryService, refreshExercises]);
|
2025-02-18 12:15:19 -05:00
|
|
|
|
|
|
|
// Delete an exercise
|
|
|
|
const deleteExercise = useCallback(async (id: string) => {
|
|
|
|
try {
|
|
|
|
await libraryService.deleteExercise(id);
|
2025-03-17 23:37:08 -04:00
|
|
|
refreshExercises(); // Use the store's refresh function
|
2025-02-18 12:15:19 -05:00
|
|
|
} catch (err) {
|
|
|
|
setError(err instanceof Error ? err : new Error('Failed to delete exercise'));
|
|
|
|
throw err;
|
|
|
|
}
|
2025-03-17 23:37:08 -04:00
|
|
|
}, [libraryService, refreshExercises]);
|
2025-02-18 12:15:19 -05:00
|
|
|
|
2025-03-06 09:19:16 -05:00
|
|
|
// Update an exercise
|
|
|
|
const updateExercise = useCallback(async (id: string, updateData: Partial<BaseExercise>) => {
|
|
|
|
try {
|
|
|
|
// Get the existing exercise first
|
|
|
|
const existingExercises = await libraryService.getExercises();
|
|
|
|
const existingExercise = existingExercises.find(ex => ex.id === id);
|
|
|
|
|
|
|
|
if (!existingExercise) {
|
|
|
|
throw new Error(`Exercise with ID ${id} not found`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete the old exercise
|
|
|
|
await libraryService.deleteExercise(id);
|
|
|
|
|
|
|
|
// Prepare the updated exercise data (without id since it's Omit<ExerciseDisplay, "id">)
|
|
|
|
const updatedExercise: Omit<ExerciseDisplay, 'id'> = {
|
|
|
|
...existingExercise,
|
|
|
|
...updateData,
|
|
|
|
source: existingExercise.source || 'local',
|
|
|
|
isFavorite: existingExercise.isFavorite || false
|
|
|
|
};
|
|
|
|
|
|
|
|
// Remove id property since it's not allowed in this type
|
|
|
|
const { id: _, ...exerciseWithoutId } = updatedExercise as any;
|
|
|
|
|
|
|
|
// Add the updated exercise with the same ID
|
|
|
|
await libraryService.addExercise(exerciseWithoutId);
|
|
|
|
|
2025-03-17 23:37:08 -04:00
|
|
|
// Refresh exercises to get the updated list
|
|
|
|
refreshExercises();
|
2025-03-06 09:19:16 -05:00
|
|
|
|
|
|
|
return id;
|
|
|
|
} catch (err) {
|
|
|
|
setError(err instanceof Error ? err : new Error('Failed to update exercise'));
|
|
|
|
throw err;
|
|
|
|
}
|
2025-03-17 23:37:08 -04:00
|
|
|
}, [libraryService, refreshExercises]);
|
2025-03-06 09:19:16 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
// Update filters
|
|
|
|
const updateFilters = useCallback((newFilters: Partial<ExerciseFilters>) => {
|
|
|
|
setFilters(current => ({
|
|
|
|
...current,
|
|
|
|
...newFilters
|
|
|
|
}));
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// Clear all filters
|
|
|
|
const clearFilters = useCallback(() => {
|
|
|
|
setFilters({});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return {
|
|
|
|
exercises: getFilteredExercises(),
|
2025-03-17 23:37:08 -04:00
|
|
|
loading: isLoading,
|
2025-02-18 12:15:19 -05:00
|
|
|
error,
|
|
|
|
stats,
|
|
|
|
filters,
|
|
|
|
updateFilters,
|
|
|
|
clearFilters,
|
|
|
|
createExercise,
|
|
|
|
deleteExercise,
|
2025-03-06 09:19:16 -05:00
|
|
|
updateExercise,
|
2025-03-17 23:37:08 -04:00
|
|
|
refreshExercises, // Return the refresh function from the store
|
|
|
|
silentRefresh // Add the silent refresh function
|
2025-02-18 12:15:19 -05:00
|
|
|
};
|
|
|
|
}
|