updated new exercise template WIP

This commit is contained in:
DocNR 2025-02-05 21:59:03 -05:00
parent 18d5886e40
commit d9d8f238b1
7 changed files with 363 additions and 117 deletions

View File

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

View File

@ -28,11 +28,11 @@ export default function TabLayout() {
},
}}>
<Tabs.Screen
name="index"
name="profile"
options={{
title: 'Workout',
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Dumbbell size={size} color={color} />
<User size={size} color={color} />
),
}}
/>
@ -45,6 +45,15 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="index"
options={{
title: 'Workout',
tabBarIcon: ({ color, size }) => (
<Dumbbell size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="social"
options={{
@ -63,15 +72,6 @@ export default function TabLayout() {
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<User size={size} color={color} />
),
}}
/>
</Tabs>
);
);
}

View File

@ -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,9 +72,10 @@ export default function LibraryScreen() {
const defaultSection = TABS.find(tab => tab.key === currentSection) ?? TABS[0];
const [activeSection, setActiveSection] = useState<number>(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([
@ -81,6 +83,9 @@ export default function LibraryScreen() {
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,
@ -96,7 +101,14 @@ export default function LibraryScreen() {
}
}));
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(() => {
useFocusEffect(
useCallback(() => {
console.log('Library screen focused, checking mount state:', mounted);
if (mounted) {
console.log('Loading content due to screen focus');
loadContent();
}, [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,8 +215,28 @@ 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 =>
prevContent.filter(item => item.id !== deletedContent.id)
@ -232,6 +282,19 @@ export default function LibraryScreen() {
onChangeText={setSearchQuery}
onFilterPress={() => setShowFilters(true)}
/>
<TouchableOpacity
style={[styles.clearButton, { backgroundColor: colors.error }]}
onPress={async () => {
try {
await libraryService.clearDatabase();
await loadContent();
} catch (error) {
console.error('Error clearing database:', error);
}
}}
>
<ThemedText style={{ color: '#FFFFFF' }}>Clear Database</ThemedText>
</TouchableOpacity>
</View>
<Pager
@ -354,4 +417,10 @@ const styles = StyleSheet.create({
fontWeight: '500',
paddingHorizontal: spacing.small,
},
clearButton: {
marginTop: spacing.small,
padding: spacing.small,
borderRadius: 8,
alignItems: 'center',
},
});

View File

@ -79,6 +79,13 @@ export default function NewExerciseScreen() {
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,

View File

@ -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 (
<View style={[styles.loadingContainer, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
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}
/>
</View>
@ -129,4 +152,9 @@ const styles = StyleSheet.create({
textAlign: 'center',
maxWidth: '80%',
},
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@ -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;
@ -23,11 +24,14 @@ class LibraryService {
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 },
});
}
@ -79,8 +83,8 @@ 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]
@ -220,6 +224,14 @@ class LibraryService {
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);
return {
@ -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<void> {
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<Array<{
exercise: BaseExercise;
targetSets: number;

View File

@ -117,16 +117,26 @@ class Schema {
}
async migrate(): Promise<void> {
try {
const currentVersion = await this.getCurrentVersion();
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,
@ -136,10 +146,17 @@ class Schema {
);
`);
await this.setVersion(2);
console.log('Migration to version 2 completed');
} catch (error) {
console.error('Error in version 2 migration:', error);
throw error;
}
}
// 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 (
@ -148,6 +165,7 @@ class Schema {
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,
@ -160,7 +178,7 @@ class Schema {
metadata_json TEXT,
availability_json TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'local'
);`
)`
},
{
sql: `CREATE TABLE IF NOT EXISTS template_exercises (
@ -175,7 +193,7 @@ class Schema {
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 (
@ -183,12 +201,25 @@ class Schema {
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;
}
}
private async getCurrentVersion(): Promise<number> {