From d9d8f238b131577cdd0b44f5fccc176e18fdab20 Mon Sep 17 00:00:00 2001 From: DocNR Date: Wed, 5 Feb 2025 21:59:03 -0500 Subject: [PATCH] updated new exercise template WIP --- CHANGELOG.md | 52 ++++++++++ app/(tabs)/_layout.tsx | 28 +++--- app/(tabs)/library.tsx | 103 +++++++++++++++---- app/(workout)/new-exercise.tsx | 17 ++-- components/library/MyLibrary.tsx | 32 +++++- services/LibraryService.ts | 81 +++++++++++++-- utils/db/schema.ts | 167 ++++++++++++++++++------------- 7 files changed, 363 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 714d82b..38f6270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,58 @@ All notable changes to the POWR project will be documented in this file. ## [Unreleased] +# Changelog + +All notable changes to the POWR project will be documented in this file. + +## [Unreleased] + +### 2024-02-05 +#### Added +- Basic exercise template creation functionality + - Added input validation for required fields + - Implemented schema-compliant field constraints + - Added native picker components for standardized inputs +- Enhanced error handling in database operations + - Added detailed SQLite error logging + - Improved transaction management + - Added proper error types and propagation + +#### Changed +- Updated NewExerciseScreen with constrained inputs + - Added dropdowns for equipment selection + - Added movement pattern selection + - Added difficulty selection + - Added exercise type selection +- Improved DbService with better error handling + - Added proper SQLite error types + - Enhanced transaction rollback handling + - Added detailed debug logging + +#### Technical Details +1. Database Schema Enforcement: + - Added CHECK constraints for equipment types + - Added CHECK constraints for exercise types + - Added CHECK constraints for categories + - Proper handling of foreign key constraints + +2. Input Validation: + - Equipment options: bodyweight, barbell, dumbbell, kettlebell, machine, cable, other + - Exercise types: strength, cardio, bodyweight + - Categories: Push, Pull, Legs, Core + - Difficulty levels: beginner, intermediate, advanced + - Movement patterns: push, pull, squat, hinge, carry, rotation + +3. Error Handling: + - Added SQLite error type definitions + - Improved error propagation in LibraryService + - Added transaction rollback on constraint violations + +#### Migration Notes +- Exercise creation now enforces schema constraints +- Input validation prevents invalid data entry +- Enhanced error messages provide better debugging information + ### 2024-02-04 #### Added diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index e87b254..e026dc1 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -27,12 +27,12 @@ export default function TabLayout() { marginBottom: Platform.OS === 'ios' ? 0 : 4, }, }}> - ( - + ), }} /> @@ -45,6 +45,15 @@ export default function TabLayout() { ), }} /> + ( + + ), + }} + /> - ( - - ), - }} - /> - ); +); } \ No newline at end of file diff --git a/app/(tabs)/library.tsx b/app/(tabs)/library.tsx index 14926a9..6a55cf4 100644 --- a/app/(tabs)/library.tsx +++ b/app/(tabs)/library.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, TouchableOpacity, StyleSheet, Platform } from 'react-native'; import { useColorScheme } from '@/hooks/useColorScheme'; import { router, useLocalSearchParams } from 'expo-router'; +import { useFocusEffect } from '@react-navigation/native'; import { Plus } from 'lucide-react-native'; import { ThemedText } from '@/components/ThemedText'; import TabLayout from '@/components/TabLayout'; @@ -71,16 +72,20 @@ export default function LibraryScreen() { const defaultSection = TABS.find(tab => tab.key === currentSection) ?? TABS[0]; const [activeSection, setActiveSection] = useState(defaultSection.index); - // Load library content + // load library content const loadContent = useCallback(async () => { if (mounted) { + console.log('Starting content load'); setIsLoading(true); try { const [exercises, templates] = await Promise.all([ libraryService.getExercises(), libraryService.getTemplates() ]); - + + console.log('Loaded exercises:', exercises.length); + console.log('Loaded templates:', templates.length); + const exerciseContent: LibraryContent[] = exercises.map(exercise => ({ id: exercise.id, title: exercise.title, @@ -95,8 +100,15 @@ export default function LibraryScreen() { source: ['local'] } })); - - setContent([...exerciseContent, ...templates]); + + const newContent = [...exerciseContent, ...templates]; + console.log('Setting new content:', newContent.length); + setContent(newContent); + + // Force a re-filter of content + setFilteredContent(newContent.filter(item => { + // ... your existing filter logic ... + })); } catch (error) { console.error('Error loading library content:', error); } finally { @@ -105,11 +117,28 @@ export default function LibraryScreen() { } }, [mounted]); - useEffect(() => { - loadContent(); - }, [loadContent]); + useFocusEffect( + useCallback(() => { + console.log('Library screen focused, checking mount state:', mounted); + if (mounted) { + console.log('Loading content due to screen focus'); + loadContent(); + } + return () => { + console.log('Library screen unfocused'); + }; + }, [mounted, loadContent]) + ); useEffect(() => { + console.log('Initial content load'); + if (mounted) { + loadContent(); + } + }, [mounted]); + + useEffect(() => { + console.log('Filtering content:', content.length); const filtered = content.filter(item => { if (searchQuery) { const searchLower = searchQuery.toLowerCase(); @@ -140,6 +169,7 @@ export default function LibraryScreen() { return true; }); + console.log('Filtered content length:', filtered.length); setFilteredContent(filtered); }, [content, searchQuery, filterOptions]); @@ -176,7 +206,7 @@ export default function LibraryScreen() { } }; - const handleAddContent = (type: 'exercise' | 'template') => { + const handleAddContent = useCallback((type: 'exercise' | 'template') => { setShowAddContent(false); if (type === 'exercise') { router.push('/(workout)/new-exercise' as const); @@ -185,7 +215,27 @@ export default function LibraryScreen() { pathname: '/(workout)/create-template' as const, }); } - }; + }, []); + + // Then enhance the useFocusEffect to be more robust + useFocusEffect( + useCallback(() => { + console.log('Library screen focused, checking mount state:', mounted); + const loadIfMounted = async () => { + if (mounted) { + console.log('Loading content due to screen focus'); + await loadContent(); + console.log('Content load complete'); + } + }; + + loadIfMounted(); + + return () => { + console.log('Library screen unfocused'); + }; + }, [mounted, loadContent]) + ); const handleDeleteContent = useCallback((deletedContent: LibraryContent) => { setContent(prevContent => @@ -232,6 +282,19 @@ export default function LibraryScreen() { onChangeText={setSearchQuery} onFilterPress={() => setShowFilters(true)} /> + { + try { + await libraryService.clearDatabase(); + await loadContent(); + } catch (error) { + console.error('Error clearing database:', error); + } + }} + > + Clear Database + - + @@ -354,4 +417,10 @@ const styles = StyleSheet.create({ fontWeight: '500', paddingHorizontal: spacing.small, }, + clearButton: { + marginTop: spacing.small, + padding: spacing.small, + borderRadius: 8, + alignItems: 'center', + }, }); \ No newline at end of file diff --git a/app/(workout)/new-exercise.tsx b/app/(workout)/new-exercise.tsx index 50eecf1..96ca118 100644 --- a/app/(workout)/new-exercise.tsx +++ b/app/(workout)/new-exercise.tsx @@ -73,12 +73,19 @@ export default function NewExerciseScreen() { try { setError(null); setIsSubmitting(true); - + if (!title.trim()) { setError('Exercise name is required'); return; } - + + // Create unique tags array + const tags = Array.from(new Set([ + difficulty, + movementPattern, + category.toLowerCase() + ])); + const exerciseTemplate = { title: title.trim(), type: exerciseType, @@ -86,11 +93,7 @@ export default function NewExerciseScreen() { equipment, difficulty, description: instructions.trim(), - tags: [ - difficulty, - movementPattern, - category.toLowerCase() - ], + tags, // Now using deduplicated tags format: { weight: true, reps: true, diff --git a/components/library/MyLibrary.tsx b/components/library/MyLibrary.tsx index 802ea01..d3af885 100644 --- a/components/library/MyLibrary.tsx +++ b/components/library/MyLibrary.tsx @@ -1,6 +1,6 @@ // components/library/MyLibrary.tsx import React from 'react'; -import { View, FlatList, StyleSheet, Platform } from 'react-native'; +import { View, FlatList, StyleSheet, Platform, ActivityIndicator } from 'react-native'; import { Feather } from '@expo/vector-icons'; import { useColorScheme } from '@/hooks/useColorScheme'; import { LibraryContent } from '@/types/exercise'; @@ -23,15 +23,32 @@ export default function MyLibrary({ onContentPress, onFavoritePress, onDeleteContent, + isLoading = false, isVisible = true }: MyLibraryProps) { const { colors } = useColorScheme(); + console.log('MyLibrary render:', { + contentLength: savedContent.length, + isVisible, + isLoading + }); + // Don't render anything if not visible if (!isVisible) { + console.log('MyLibrary not visible, returning null'); return null; } + // Show loading state + if (isLoading) { + return ( + + + + ); + } + const handleDelete = async (content: LibraryContent) => { try { if (content.type === 'exercise') { @@ -48,6 +65,12 @@ export default function MyLibrary({ const exercises = savedContent.filter(content => content.type === 'exercise'); const workouts = savedContent.filter(content => content.type === 'workout'); + console.log('Content breakdown:', { + total: savedContent.length, + exercises: exercises.length, + workouts: workouts.length + }); + const renderSection = (title: string, items: LibraryContent[]) => { if (items.length === 0) return null; @@ -66,7 +89,7 @@ export default function MyLibrary({ onDelete={item.type === 'exercise' ? () => handleDelete(item) : undefined} /> )} - keyExtractor={item => item.id} + keyExtractor={(item, index) => `${title}-${item.id}-${index}`} scrollEnabled={false} /> @@ -129,4 +152,9 @@ const styles = StyleSheet.create({ textAlign: 'center', maxWidth: '80%', }, + loadingContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, }); \ No newline at end of file diff --git a/services/LibraryService.ts b/services/LibraryService.ts index 408d463..4fb1418 100644 --- a/services/LibraryService.ts +++ b/services/LibraryService.ts @@ -10,6 +10,7 @@ import { import { WorkoutTemplate } from '@/types/workout'; import { StorageSource } from '@/types/shared'; import { SQLiteError } from '@/types/sqlite'; +import { schema } from '@/utils/db/schema'; class LibraryService { private db: DbService; @@ -22,15 +23,18 @@ class LibraryService { async addExercise(exercise: Omit): Promise { const id = generateId(); const timestamp = Date.now(); - + + // Deduplicate tags + const uniqueTags = Array.from(new Set(exercise.tags)); + if (this.DEBUG) { console.log('Creating exercise with payload:', { id, timestamp, - exercise, + exercise: { ...exercise, tags: uniqueTags }, }); } - + try { await this.db.withTransaction(async () => { // 1. First insert main exercise data @@ -79,19 +83,19 @@ class LibraryService { } // 3. Insert tags if provided - if (exercise.tags?.length) { - for (const tag of exercise.tags) { + if (uniqueTags?.length) { + for (const tag of uniqueTags) { const tagResult = await this.db.executeWrite( `INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`, [id, tag] ); - + if (this.DEBUG) { console.log('Tag insert result:', tagResult); } } } - + // 4. Insert instructions if provided if (exercise.instructions?.length) { for (const [index, instruction] of exercise.instructions.entries()) { @@ -217,8 +221,16 @@ class LibraryService { GROUP BY e.id ORDER BY e.title `; - + const result = await this.db.executeSql(query, category ? [category] : []); + + // Add this logging + console.log(`Found ${result.rows.length} exercises in database:`, + Array.from({ length: result.rows.length }, (_, i) => ({ + id: result.rows.item(i).id, + title: result.rows.item(i).title + })) + ); return Array.from({ length: result.rows.length }, (_, i) => { const row = result.rows.item(i); @@ -295,7 +307,15 @@ class LibraryService { }; }); - return [...exerciseContent, ...templateContent]; + const allContent = [...exerciseContent, ...templateContent]; + + // Ensure no duplicates by ID + const uniqueContent = Array.from( + new Map(allContent.map(item => [item.id, item])).values() + ); + + return uniqueContent; + } catch (error) { console.error('Error getting templates:', error); throw error; @@ -349,6 +369,49 @@ class LibraryService { } } + async clearDatabase(): Promise { + if (this.DEBUG) { + console.log('Clearing database...'); + } + + try { + // Disable foreign key constraints + await this.db.executeWrite('PRAGMA foreign_keys = OFF;'); + + // Drop all tables + const dropQueries = [ + 'DROP TABLE IF EXISTS template_tags', + 'DROP TABLE IF EXISTS template_exercises', + 'DROP TABLE IF EXISTS exercise_tags', + 'DROP TABLE IF EXISTS exercise_instructions', + 'DROP TABLE IF EXISTS exercise_format', + 'DROP TABLE IF EXISTS templates', + 'DROP TABLE IF EXISTS exercises', + 'DROP TABLE IF EXISTS schema_version' + ]; + + for (const query of dropQueries) { + if (this.DEBUG) { + console.log('Executing:', query); + } + await this.db.executeWrite(query); + } + + // Re-enable foreign key constraints + await this.db.executeWrite('PRAGMA foreign_keys = ON;'); + + // Recreate schema using the schema utility + await schema.migrate(); + + if (this.DEBUG) { + console.log('Database cleared and reinitialized successfully'); + } + } catch (error) { + console.error('Error clearing database:', error); + throw error; + } + } + private async getTemplateExercises(templateId: string): Promise { - const currentVersion = await this.getCurrentVersion(); - - if (currentVersion < SCHEMA_VERSION) { - if (currentVersion < 1) { - await this.createTables(); - await this.setVersion(1); - } + try { + const currentVersion = await this.getCurrentVersion(); - // Migration to version 2 - Add format table - if (currentVersion < 2) { - await this.db.executeWrite(` - CREATE TABLE IF NOT EXISTS exercise_format ( - exercise_id TEXT PRIMARY KEY, - format_json TEXT NOT NULL, - units_json TEXT, - FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE - ); - `); - await this.setVersion(2); - } - - // Migration to version 3 - Add template tables - if (currentVersion < 3) { - await this.db.executeWriteMany([ - { - sql: `CREATE TABLE IF NOT EXISTS templates ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL, - type TEXT NOT NULL CHECK(type IN ('strength', 'circuit', 'emom', 'amrap')), - category TEXT NOT NULL CHECK(category IN ('Full Body', 'Custom', 'Push/Pull/Legs', 'Upper/Lower', 'Cardio', 'CrossFit', 'Strength')), - description TEXT, - author_name TEXT, - author_pubkey TEXT, - rounds INTEGER, - duration INTEGER, - interval_time INTEGER, - rest_between_rounds INTEGER, - is_public BOOLEAN NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - metadata_json TEXT, - availability_json TEXT NOT NULL, - source TEXT NOT NULL DEFAULT 'local' - );` - }, - { - sql: `CREATE TABLE IF NOT EXISTS template_exercises ( - template_id TEXT NOT NULL, - exercise_id TEXT NOT NULL, - target_sets INTEGER, - target_reps INTEGER, - target_weight REAL, - target_rpe INTEGER CHECK(target_rpe BETWEEN 0 AND 10), - notes TEXT, - display_order INTEGER NOT NULL, - FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE, - FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE, - PRIMARY KEY(template_id, exercise_id, display_order) - );` - }, - { - sql: `CREATE TABLE IF NOT EXISTS template_tags ( - template_id TEXT NOT NULL, - tag TEXT NOT NULL, - FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE, - UNIQUE(template_id, tag) - );` + console.log('Current database version:', currentVersion); + console.log('Target database version:', SCHEMA_VERSION); + + if (currentVersion < SCHEMA_VERSION) { + console.log('Starting database migration...'); + + // Initial migration + if (currentVersion < 1) { + console.log('Running migration to version 1...'); + await this.createTables(); + await this.setVersion(1); + } + + // Migration to version 2 - Add format table + if (currentVersion < 2) { + console.log('Running migration to version 2...'); + try { + await this.db.executeWrite(` + CREATE TABLE IF NOT EXISTS exercise_format ( + exercise_id TEXT PRIMARY KEY, + format_json TEXT NOT NULL, + units_json TEXT, + FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE + ); + `); + await this.setVersion(2); + console.log('Migration to version 2 completed'); + } catch (error) { + console.error('Error in version 2 migration:', error); + throw error; } - ]); - await this.setVersion(3); + } + + // Migration to version 3 - Add template tables + if (currentVersion < 3) { + console.log('Running migration to version 3...'); + try { + await this.db.executeWriteMany([ + { + sql: `CREATE TABLE IF NOT EXISTS templates ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + type TEXT NOT NULL CHECK(type IN ('strength', 'circuit', 'emom', 'amrap')), + category TEXT NOT NULL CHECK(category IN ('Full Body', 'Custom', 'Push/Pull/Legs', 'Upper/Lower', 'Cardio', 'CrossFit', 'Strength')), + description TEXT, + notes TEXT, + author_name TEXT, + author_pubkey TEXT, + rounds INTEGER, + duration INTEGER, + interval_time INTEGER, + rest_between_rounds INTEGER, + is_public BOOLEAN NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + metadata_json TEXT, + availability_json TEXT NOT NULL, + source TEXT NOT NULL DEFAULT 'local' + )` + }, + { + sql: `CREATE TABLE IF NOT EXISTS template_exercises ( + template_id TEXT NOT NULL, + exercise_id TEXT NOT NULL, + target_sets INTEGER, + target_reps INTEGER, + target_weight REAL, + target_rpe INTEGER CHECK(target_rpe BETWEEN 0 AND 10), + notes TEXT, + display_order INTEGER NOT NULL, + FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE, + FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE, + PRIMARY KEY(template_id, exercise_id, display_order) + )` + }, + { + sql: `CREATE TABLE IF NOT EXISTS template_tags ( + template_id TEXT NOT NULL, + tag TEXT NOT NULL, + FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE, + UNIQUE(template_id, tag) + )` + } + ]); + await this.setVersion(3); + console.log('Migration to version 3 completed'); + } catch (error) { + console.error('Error in version 3 migration:', error); + throw error; + } + } + + console.log('All migrations completed successfully'); + } else { + console.log('Database is up to date'); } + } catch (error) { + console.error('Migration failed:', error); + throw error; } }