mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-05 08:42:05 +00:00
sqlite db wip - add exercise template now works
This commit is contained in:
parent
855c034a35
commit
18d5886e40
4
app.json
4
app.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "powr-rebuild",
|
||||
"slug": "powr-rebuild",
|
||||
"name": "POWR",
|
||||
"slug": "POWR",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
|
@ -187,6 +187,12 @@ export default function LibraryScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteContent = useCallback((deletedContent: LibraryContent) => {
|
||||
setContent(prevContent =>
|
||||
prevContent.filter(item => item.id !== deletedContent.id)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleTabPress = useCallback((index: number) => {
|
||||
pagerRef.current?.setPage(index);
|
||||
setActiveSection(index);
|
||||
@ -235,13 +241,14 @@ export default function LibraryScreen() {
|
||||
onPageSelected={handlePageSelected}
|
||||
>
|
||||
<View key="my-library" style={styles.pageContainer}>
|
||||
<MyLibrary
|
||||
savedContent={filteredContent}
|
||||
onContentPress={handleContentPress}
|
||||
onFavoritePress={handleFavoritePress}
|
||||
isLoading={isLoading}
|
||||
isVisible
|
||||
/>
|
||||
<MyLibrary
|
||||
savedContent={filteredContent}
|
||||
onContentPress={handleContentPress}
|
||||
onFavoritePress={handleFavoritePress}
|
||||
onDeleteContent={handleDeleteContent}
|
||||
isLoading={isLoading}
|
||||
isVisible={activeSection === 0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View key="programs" style={styles.pageContainer}>
|
||||
|
@ -1,58 +1,70 @@
|
||||
// app/(workout)/new-exercise.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { Input } from '@/components/form/Input';
|
||||
import { Select } from '@/components/form/Select';
|
||||
import { Button } from '@/components/form/Button';
|
||||
import { LibraryService } from '@/services/LibraryService';
|
||||
import { libraryService } from '@/services/LibraryService';
|
||||
import { spacing } from '@/styles/sharedStyles';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
|
||||
// Types based on NIP-XX spec
|
||||
const EQUIPMENT_OPTIONS = [
|
||||
{ label: 'Barbell', value: 'barbell' },
|
||||
{ label: 'Dumbbell', value: 'dumbbell' },
|
||||
{ label: 'Bodyweight', value: 'bodyweight' },
|
||||
{ label: 'Machine', value: 'machine' },
|
||||
{ label: 'Cardio', value: 'cardio' }
|
||||
// Define valid options based on schema and NIP-XX constraints
|
||||
const EQUIPMENT_OPTIONS: Equipment[] = [
|
||||
'bodyweight',
|
||||
'barbell',
|
||||
'dumbbell',
|
||||
'kettlebell',
|
||||
'machine',
|
||||
'cable',
|
||||
'other'
|
||||
];
|
||||
|
||||
const EXERCISE_TYPES: ExerciseType[] = [
|
||||
'strength',
|
||||
'cardio',
|
||||
'bodyweight'
|
||||
];
|
||||
|
||||
const CATEGORIES: ExerciseCategory[] = [
|
||||
'Push',
|
||||
'Pull',
|
||||
'Legs',
|
||||
'Core'
|
||||
];
|
||||
|
||||
const DIFFICULTY_OPTIONS = [
|
||||
{ label: 'Beginner', value: 'beginner' },
|
||||
{ label: 'Intermediate', value: 'intermediate' },
|
||||
{ label: 'Advanced', value: 'advanced' }
|
||||
];
|
||||
'beginner',
|
||||
'intermediate',
|
||||
'advanced'
|
||||
] as const;
|
||||
|
||||
const MUSCLE_GROUP_OPTIONS = [
|
||||
{ label: 'Chest', value: 'chest' },
|
||||
{ label: 'Back', value: 'back' },
|
||||
{ label: 'Legs', value: 'legs' },
|
||||
{ label: 'Shoulders', value: 'shoulders' },
|
||||
{ label: 'Arms', value: 'arms' },
|
||||
{ label: 'Core', value: 'core' }
|
||||
];
|
||||
type Difficulty = typeof DIFFICULTY_OPTIONS[number];
|
||||
|
||||
const MOVEMENT_TYPE_OPTIONS = [
|
||||
{ label: 'Push', value: 'push' },
|
||||
{ label: 'Pull', value: 'pull' },
|
||||
{ label: 'Squat', value: 'squat' },
|
||||
{ label: 'Hinge', value: 'hinge' },
|
||||
{ label: 'Carry', value: 'carry' }
|
||||
];
|
||||
const MOVEMENT_PATTERNS = [
|
||||
'push',
|
||||
'pull',
|
||||
'squat',
|
||||
'hinge',
|
||||
'carry',
|
||||
'rotation'
|
||||
] as const;
|
||||
|
||||
type MovementPattern = typeof MOVEMENT_PATTERNS[number];
|
||||
|
||||
export default function NewExerciseScreen() {
|
||||
const router = useRouter();
|
||||
const { colors } = useColorScheme();
|
||||
|
||||
// Form state matching Nostr spec
|
||||
// Required fields based on NIP-XX spec
|
||||
const [title, setTitle] = useState('');
|
||||
const [equipment, setEquipment] = useState('');
|
||||
const [difficulty, setDifficulty] = useState('');
|
||||
const [muscleGroups, setMuscleGroups] = useState<string[]>([]);
|
||||
const [movementTypes, setMovementTypes] = useState<string[]>([]);
|
||||
const [exerciseType, setExerciseType] = useState<ExerciseType>(EXERCISE_TYPES[0]);
|
||||
const [category, setCategory] = useState<ExerciseCategory>(CATEGORIES[0]);
|
||||
const [equipment, setEquipment] = useState<Equipment>(EQUIPMENT_OPTIONS[0]);
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(DIFFICULTY_OPTIONS[0]);
|
||||
const [movementPattern, setMovementPattern] = useState<MovementPattern>(MOVEMENT_PATTERNS[0]);
|
||||
const [instructions, setInstructions] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@ -67,39 +79,37 @@ export default function NewExerciseScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!equipment) {
|
||||
setError('Equipment type is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create exercise template following NIP-XX spec
|
||||
const exerciseTemplate = {
|
||||
id: generateId(), // UUID for template identification
|
||||
title: title.trim(),
|
||||
type: 'exercise',
|
||||
format: ['weight', 'reps', 'rpe', 'set_type'], // Required format params
|
||||
format_units: ['kg', 'count', '0-10', 'warmup|normal|drop|failure'], // Required unit definitions
|
||||
type: exerciseType,
|
||||
category,
|
||||
equipment,
|
||||
difficulty,
|
||||
content: instructions.trim(), // Form instructions in content field
|
||||
description: instructions.trim(),
|
||||
tags: [
|
||||
['d', generateId()], // Required UUID tag
|
||||
['title', title.trim()],
|
||||
['equipment', equipment],
|
||||
...muscleGroups.map(group => ['t', group]),
|
||||
...movementTypes.map(type => ['t', type]),
|
||||
['format', 'weight', 'reps', 'rpe', 'set_type'],
|
||||
['format_units', 'kg', 'count', '0-10', 'warmup|normal|drop|failure'],
|
||||
difficulty ? ['difficulty', difficulty] : [],
|
||||
].filter(tag => tag.length > 0), // Remove empty tags
|
||||
source: 'local',
|
||||
created_at: Date.now(),
|
||||
availability: {
|
||||
source: ['local']
|
||||
difficulty,
|
||||
movementPattern,
|
||||
category.toLowerCase()
|
||||
],
|
||||
format: {
|
||||
weight: true,
|
||||
reps: true,
|
||||
rpe: true,
|
||||
set_type: true
|
||||
},
|
||||
format_units: {
|
||||
weight: 'kg' as const,
|
||||
reps: 'count' as const,
|
||||
rpe: '0-10' as const,
|
||||
set_type: 'warmup|normal|drop|failure' as const
|
||||
}
|
||||
};
|
||||
|
||||
await LibraryService.addExercise(exerciseTemplate);
|
||||
if (__DEV__) {
|
||||
console.log('Creating exercise:', exerciseTemplate);
|
||||
}
|
||||
|
||||
await libraryService.addExercise(exerciseTemplate);
|
||||
router.back();
|
||||
|
||||
} catch (err) {
|
||||
@ -116,10 +126,12 @@ export default function NewExerciseScreen() {
|
||||
contentContainerStyle={styles.content}
|
||||
>
|
||||
<View style={styles.form}>
|
||||
{/* Basic Information */}
|
||||
<View style={styles.section}>
|
||||
<ThemedText type="subtitle" style={styles.sectionTitle}>
|
||||
Basic Information
|
||||
</ThemedText>
|
||||
|
||||
<Input
|
||||
label="Exercise Name"
|
||||
value={title}
|
||||
@ -127,49 +139,102 @@ export default function NewExerciseScreen() {
|
||||
placeholder="e.g., Barbell Back Squat"
|
||||
required
|
||||
/>
|
||||
<Select
|
||||
label="Equipment"
|
||||
value={equipment}
|
||||
onValueChange={setEquipment}
|
||||
items={EQUIPMENT_OPTIONS}
|
||||
placeholder="Select equipment type"
|
||||
required
|
||||
/>
|
||||
<Select
|
||||
label="Difficulty"
|
||||
value={difficulty}
|
||||
onValueChange={setDifficulty}
|
||||
items={DIFFICULTY_OPTIONS}
|
||||
placeholder="Select difficulty level"
|
||||
/>
|
||||
|
||||
<View style={styles.pickerContainer}>
|
||||
<ThemedText style={styles.label}>Exercise Type</ThemedText>
|
||||
<Picker<ExerciseType>
|
||||
selectedValue={exerciseType}
|
||||
onValueChange={(value: ExerciseType) => setExerciseType(value)}
|
||||
style={[styles.picker, { backgroundColor: colors.cardBg }]}
|
||||
>
|
||||
{EXERCISE_TYPES.map((option) => (
|
||||
<Picker.Item
|
||||
key={option}
|
||||
label={option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View style={styles.pickerContainer}>
|
||||
<ThemedText style={styles.label}>Category</ThemedText>
|
||||
<Picker<ExerciseCategory>
|
||||
selectedValue={category}
|
||||
onValueChange={(value: ExerciseCategory) => setCategory(value)}
|
||||
style={[styles.picker, { backgroundColor: colors.cardBg }]}
|
||||
>
|
||||
{CATEGORIES.map((option) => (
|
||||
<Picker.Item key={option} label={option} value={option} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View style={styles.pickerContainer}>
|
||||
<ThemedText style={styles.label}>Equipment</ThemedText>
|
||||
<Picker<Equipment>
|
||||
selectedValue={equipment}
|
||||
onValueChange={(value: Equipment) => setEquipment(value)}
|
||||
style={[styles.picker, { backgroundColor: colors.cardBg }]}
|
||||
>
|
||||
{EQUIPMENT_OPTIONS.map((option) => (
|
||||
<Picker.Item
|
||||
key={option}
|
||||
label={option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Categorization */}
|
||||
<View style={styles.section}>
|
||||
<ThemedText type="subtitle" style={styles.sectionTitle}>
|
||||
Categorization
|
||||
</ThemedText>
|
||||
<Select
|
||||
label="Muscle Groups"
|
||||
value={muscleGroups}
|
||||
onValueChange={setMuscleGroups}
|
||||
items={MUSCLE_GROUP_OPTIONS}
|
||||
placeholder="Select muscle groups"
|
||||
multiple
|
||||
/>
|
||||
<Select
|
||||
label="Movement Types"
|
||||
value={movementTypes}
|
||||
onValueChange={setMovementTypes}
|
||||
items={MOVEMENT_TYPE_OPTIONS}
|
||||
placeholder="Select movement types"
|
||||
multiple
|
||||
/>
|
||||
|
||||
<View style={styles.pickerContainer}>
|
||||
<ThemedText style={styles.label}>Movement Pattern</ThemedText>
|
||||
<Picker<MovementPattern>
|
||||
selectedValue={movementPattern}
|
||||
onValueChange={(value: MovementPattern) => setMovementPattern(value)}
|
||||
style={[styles.picker, { backgroundColor: colors.cardBg }]}
|
||||
>
|
||||
{MOVEMENT_PATTERNS.map((option) => (
|
||||
<Picker.Item
|
||||
key={option}
|
||||
label={option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View style={styles.pickerContainer}>
|
||||
<ThemedText style={styles.label}>Difficulty</ThemedText>
|
||||
<Picker<Difficulty>
|
||||
selectedValue={difficulty}
|
||||
onValueChange={(value: Difficulty) => setDifficulty(value)}
|
||||
style={[styles.picker, { backgroundColor: colors.cardBg }]}
|
||||
>
|
||||
{DIFFICULTY_OPTIONS.map((option) => (
|
||||
<Picker.Item
|
||||
key={option}
|
||||
label={option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Instructions */}
|
||||
<View style={styles.section}>
|
||||
<ThemedText type="subtitle" style={styles.sectionTitle}>
|
||||
Instructions
|
||||
</ThemedText>
|
||||
|
||||
<Input
|
||||
label="Form Instructions"
|
||||
value={instructions}
|
||||
@ -205,6 +270,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
content: {
|
||||
padding: spacing.medium,
|
||||
paddingBottom: Platform.OS === 'ios' ? 100 : 80,
|
||||
},
|
||||
form: {
|
||||
gap: spacing.large,
|
||||
@ -213,14 +279,29 @@ const styles = StyleSheet.create({
|
||||
gap: spacing.medium,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.small,
|
||||
},
|
||||
error: {
|
||||
textAlign: 'center',
|
||||
marginTop: spacing.small,
|
||||
fontSize: 14,
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: spacing.large,
|
||||
paddingBottom: Platform.OS === 'ios' ? spacing.xl : spacing.large,
|
||||
},
|
||||
pickerContainer: {
|
||||
marginBottom: spacing.medium,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: spacing.small,
|
||||
},
|
||||
picker: {
|
||||
borderRadius: 8,
|
||||
padding: spacing.small,
|
||||
},
|
||||
});
|
@ -10,6 +10,8 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import * as ExpoSplashScreen from 'expo-splash-screen';
|
||||
import SplashScreen from '@/components/SplashScreen';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
|
||||
// Prevent auto-hide of splash screen
|
||||
ExpoSplashScreen.preventAutoHideAsync();
|
||||
@ -114,14 +116,16 @@ function RootLayoutNav() {
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer>
|
||||
<AppearanceProvider>
|
||||
<WorkoutProvider>
|
||||
<RootLayoutNav />
|
||||
</WorkoutProvider>
|
||||
</AppearanceProvider>
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer>
|
||||
<AppearanceProvider>
|
||||
<WorkoutProvider>
|
||||
<RootLayoutNav />
|
||||
</WorkoutProvider>
|
||||
</AppearanceProvider>
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
// components/library/LibraryContentCard.tsx
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, Alert } from 'react-native';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LibraryContent } from '@/types/exercise';
|
||||
import { spacing } from '@/styles/sharedStyles';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import Swipeable from 'react-native-gesture-handler/Swipeable';
|
||||
import { RectButton } from 'react-native-gesture-handler';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
export interface LibraryContentCardProps {
|
||||
content: LibraryContent;
|
||||
onPress: () => void;
|
||||
onFavoritePress: () => void;
|
||||
onDelete?: () => Promise<void>;
|
||||
isVerified?: boolean;
|
||||
}
|
||||
|
||||
@ -18,67 +22,132 @@ export default function LibraryContentCard({
|
||||
content,
|
||||
onPress,
|
||||
onFavoritePress,
|
||||
onDelete,
|
||||
isVerified
|
||||
}: LibraryContentCardProps) {
|
||||
const { colors } = useColorScheme();
|
||||
const swipeableRef = React.useRef<Swipeable>(null);
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
// Play haptic feedback
|
||||
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
|
||||
// Show confirmation alert
|
||||
Alert.alert(
|
||||
'Delete Exercise',
|
||||
'Are you sure you want to delete this exercise?',
|
||||
[
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
swipeableRef.current?.close();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
if (onDelete) {
|
||||
await onDelete();
|
||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting exercise:', error);
|
||||
Alert.alert('Error', 'Failed to delete exercise');
|
||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error handling delete:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRightActions = () => {
|
||||
if (!onDelete) return null;
|
||||
|
||||
return (
|
||||
<RectButton
|
||||
style={[styles.deleteAction, { backgroundColor: colors.error }]}
|
||||
onPress={handleDelete}
|
||||
>
|
||||
<Feather name="trash-2" size={24} color="white" />
|
||||
<ThemedText style={[styles.deleteText, { color: 'white' }]}>
|
||||
Delete
|
||||
</ThemedText>
|
||||
</RectButton>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, { backgroundColor: colors.cardBg }]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
friction={2}
|
||||
enableTrackpadTwoFingerGesture
|
||||
rightThreshold={40}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<ThemedText type="subtitle">
|
||||
{content.title}
|
||||
<RectButton
|
||||
style={[styles.container, { backgroundColor: colors.cardBg }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<ThemedText type="subtitle">
|
||||
{content.title}
|
||||
</ThemedText>
|
||||
{isVerified && (
|
||||
<View style={styles.verifiedBadge}>
|
||||
<Feather name="check-circle" size={16} color={colors.primary} />
|
||||
<ThemedText style={[styles.verifiedText, { color: colors.primary }]}>
|
||||
POW Verified
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<RectButton
|
||||
onPress={onFavoritePress}
|
||||
style={styles.favoriteButton}
|
||||
>
|
||||
<Feather
|
||||
name="star"
|
||||
size={24}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</RectButton>
|
||||
</View>
|
||||
|
||||
{content.description && (
|
||||
<ThemedText
|
||||
style={styles.description}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{content.description}
|
||||
</ThemedText>
|
||||
{isVerified && (
|
||||
<View style={styles.verifiedBadge}>
|
||||
<Feather name="check-circle" size={16} color={colors.primary} />
|
||||
<ThemedText style={[styles.verifiedText, { color: colors.primary }]}>
|
||||
POW Verified
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={onFavoritePress}
|
||||
style={styles.favoriteButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Feather
|
||||
name="star"
|
||||
size={24}
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{content.description && (
|
||||
<ThemedText
|
||||
style={styles.description}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{content.description}
|
||||
</ThemedText>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.tags}>
|
||||
{content.tags.map(tag => (
|
||||
<View
|
||||
key={tag}
|
||||
style={[styles.tag, { backgroundColor: colors.primary + '20' }]}
|
||||
>
|
||||
<ThemedText style={[styles.tagText, { color: colors.primary }]}>
|
||||
{tag}
|
||||
</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.tags}>
|
||||
{content.tags.map(tag => (
|
||||
<View
|
||||
key={tag}
|
||||
style={[styles.tag, { backgroundColor: colors.primary + '20' }]}
|
||||
>
|
||||
<ThemedText style={[styles.tagText, { color: colors.primary }]}>
|
||||
{tag}
|
||||
</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</RectButton>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
@ -136,4 +205,16 @@ const styles = StyleSheet.create({
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
deleteAction: {
|
||||
flex: 1,
|
||||
backgroundColor: 'red',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: 80,
|
||||
},
|
||||
deleteText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
@ -4,6 +4,7 @@ import { View, FlatList, StyleSheet, Platform } from 'react-native';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { LibraryContent } from '@/types/exercise';
|
||||
import { libraryService } from '@/services/LibraryService';
|
||||
import LibraryContentCard from '@/components/library/LibraryContentCard';
|
||||
import { spacing } from '@/styles/sharedStyles';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
@ -12,15 +13,16 @@ interface MyLibraryProps {
|
||||
savedContent: LibraryContent[];
|
||||
onContentPress: (content: LibraryContent) => void;
|
||||
onFavoritePress: (content: LibraryContent) => Promise<void>;
|
||||
onDeleteContent?: (content: LibraryContent) => void;
|
||||
isLoading?: boolean;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
// components/library/MyLibrary.tsx
|
||||
export default function MyLibrary({
|
||||
savedContent,
|
||||
onContentPress,
|
||||
onFavoritePress,
|
||||
onDeleteContent,
|
||||
isVisible = true
|
||||
}: MyLibraryProps) {
|
||||
const { colors } = useColorScheme();
|
||||
@ -30,6 +32,18 @@ export default function MyLibrary({
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDelete = async (content: LibraryContent) => {
|
||||
try {
|
||||
if (content.type === 'exercise') {
|
||||
await libraryService.deleteExercise(content.id);
|
||||
onDeleteContent?.(content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting content:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Separate exercises and workouts
|
||||
const exercises = savedContent.filter(content => content.type === 'exercise');
|
||||
const workouts = savedContent.filter(content => content.type === 'workout');
|
||||
@ -49,6 +63,7 @@ export default function MyLibrary({
|
||||
content={item}
|
||||
onPress={() => onContentPress(item)}
|
||||
onFavoritePress={() => onFavoritePress(item)}
|
||||
onDelete={item.type === 'exercise' ? () => handleDelete(item) : undefined}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={item => item.id}
|
||||
@ -114,7 +129,4 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%',
|
||||
},
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
18
package-lock.json
generated
18
package-lock.json
generated
@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "powr-rebuild",
|
||||
"name": "powr",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "powr-rebuild",
|
||||
"name": "powr",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.4",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"@react-navigation/native-stack": "^7.2.0",
|
||||
@ -3559,6 +3560,19 @@
|
||||
"react-native": "^0.0.0-0 || >=0.60 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz",
|
||||
"integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.76.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "powr-rebuild",
|
||||
"name": "powr",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.4",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-picker/picker": "^2.11.0",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"@react-navigation/native-stack": "^7.2.0",
|
||||
|
@ -8,9 +8,12 @@ import {
|
||||
LibraryContent
|
||||
} from '@/types/exercise';
|
||||
import { WorkoutTemplate } from '@/types/workout';
|
||||
import { StorageSource } from '@/types/shared';
|
||||
import { SQLiteError } from '@/types/sqlite';
|
||||
|
||||
class LibraryService {
|
||||
private db: DbService;
|
||||
private readonly DEBUG = __DEV__;
|
||||
|
||||
constructor() {
|
||||
this.db = new DbService('powr.db');
|
||||
@ -20,13 +23,22 @@ class LibraryService {
|
||||
const id = generateId();
|
||||
const timestamp = Date.now();
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Creating exercise with payload:', {
|
||||
id,
|
||||
timestamp,
|
||||
exercise,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.withTransaction(async () => {
|
||||
// Insert main exercise data
|
||||
await this.db.executeWrite(
|
||||
// 1. First insert main exercise data
|
||||
const mainResult = await this.db.executeWrite(
|
||||
`INSERT INTO exercises (
|
||||
id, title, type, category, equipment, description, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, title, type, category, equipment, description,
|
||||
created_at, updated_at, source
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
exercise.title,
|
||||
@ -35,35 +47,22 @@ class LibraryService {
|
||||
exercise.equipment || null,
|
||||
exercise.description || null,
|
||||
timestamp,
|
||||
timestamp
|
||||
timestamp,
|
||||
'local'
|
||||
]
|
||||
);
|
||||
|
||||
// Insert instructions if provided
|
||||
if (exercise.instructions?.length) {
|
||||
for (const [index, instruction] of exercise.instructions.entries()) {
|
||||
await this.db.executeWrite(
|
||||
`INSERT INTO exercise_instructions (
|
||||
exercise_id, instruction, display_order
|
||||
) VALUES (?, ?, ?)`,
|
||||
[id, instruction, index]
|
||||
);
|
||||
}
|
||||
if (this.DEBUG) {
|
||||
console.log('Main exercise insert result:', mainResult);
|
||||
}
|
||||
|
||||
// Insert tags if provided
|
||||
if (exercise.tags?.length) {
|
||||
for (const tag of exercise.tags) {
|
||||
await this.db.executeWrite(
|
||||
`INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)`,
|
||||
[id, tag]
|
||||
);
|
||||
}
|
||||
if (!mainResult.rowsAffected) {
|
||||
throw new Error('Main exercise insert failed');
|
||||
}
|
||||
|
||||
// Insert format settings if provided
|
||||
// 2. Insert format settings if provided
|
||||
if (exercise.format) {
|
||||
await this.db.executeWrite(
|
||||
const formatResult = await this.db.executeWrite(
|
||||
`INSERT INTO exercise_format (
|
||||
exercise_id, format_json, units_json
|
||||
) VALUES (?, ?, ?)`,
|
||||
@ -73,12 +72,86 @@ class LibraryService {
|
||||
JSON.stringify(exercise.format_units || {})
|
||||
]
|
||||
);
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Format insert result:', formatResult);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Insert tags if provided
|
||||
if (exercise.tags?.length) {
|
||||
for (const tag of exercise.tags) {
|
||||
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()) {
|
||||
const instructionResult = await this.db.executeWrite(
|
||||
`INSERT INTO exercise_instructions (
|
||||
exercise_id, instruction, display_order
|
||||
) VALUES (?, ?, ?)`,
|
||||
[id, instruction, index]
|
||||
);
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Instruction insert result:', instructionResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.DEBUG) {
|
||||
console.log('Exercise successfully created with ID:', id);
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (err) {
|
||||
if (this.DEBUG) {
|
||||
// Type check the error
|
||||
if (err instanceof Error) {
|
||||
const sqlError = err as SQLiteError;
|
||||
console.error('Detailed error in addExercise:', {
|
||||
message: err.message,
|
||||
sql: 'sql' in sqlError ? sqlError.sql : undefined,
|
||||
params: 'params' in sqlError ? sqlError.params : undefined,
|
||||
code: 'code' in sqlError ? sqlError.code : undefined
|
||||
});
|
||||
} else {
|
||||
console.error('Unknown error in addExercise:', err);
|
||||
}
|
||||
}
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to insert exercise');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExercise(id: string): Promise<void> {
|
||||
try {
|
||||
if (this.DEBUG) {
|
||||
console.log('Deleting exercise:', id);
|
||||
}
|
||||
|
||||
await this.db.withTransaction(async () => {
|
||||
// Delete from main exercise table
|
||||
const result = await this.db.executeWrite(
|
||||
'DELETE FROM exercises WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!result.rowsAffected) {
|
||||
throw new Error('Exercise not found');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error adding exercise:', error);
|
||||
console.error('Error deleting exercise:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -118,7 +191,7 @@ class LibraryService {
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
availability: {
|
||||
source: ['local']
|
||||
source: ['local' as StorageSource]
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@ -163,7 +236,7 @@ class LibraryService {
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
availability: {
|
||||
source: ['local']
|
||||
source: ['local' as StorageSource]
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -177,7 +250,7 @@ class LibraryService {
|
||||
try {
|
||||
// First get exercises
|
||||
const exercises = await this.getExercises();
|
||||
const exerciseContent = exercises.map(exercise => ({
|
||||
const exerciseContent: LibraryContent[] = exercises.map(exercise => ({
|
||||
id: exercise.id,
|
||||
title: exercise.title,
|
||||
type: 'exercise' as const,
|
||||
@ -188,7 +261,7 @@ class LibraryService {
|
||||
tags: exercise.tags,
|
||||
created_at: exercise.created_at,
|
||||
availability: {
|
||||
source: ['local']
|
||||
source: ['local' as StorageSource]
|
||||
}
|
||||
}));
|
||||
|
||||
@ -251,7 +324,8 @@ class LibraryService {
|
||||
title: templateRow.title,
|
||||
type: templateRow.type,
|
||||
category: templateRow.category,
|
||||
description: templateRow.description,
|
||||
description: templateRow.description || '',
|
||||
notes: templateRow.notes || '', // Added missing notes field
|
||||
author: templateRow.author_name ? {
|
||||
name: templateRow.author_name,
|
||||
pubkey: templateRow.author_pubkey
|
||||
@ -265,7 +339,9 @@ class LibraryService {
|
||||
isPublic: Boolean(templateRow.is_public),
|
||||
created_at: templateRow.created_at,
|
||||
metadata: templateRow.metadata_json ? JSON.parse(templateRow.metadata_json) : undefined,
|
||||
availability: JSON.parse(templateRow.availability_json)
|
||||
availability: {
|
||||
source: ['local' as StorageSource]
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting template:', error);
|
||||
@ -275,8 +351,8 @@ class LibraryService {
|
||||
|
||||
private async getTemplateExercises(templateId: string): Promise<Array<{
|
||||
exercise: BaseExercise;
|
||||
targetSets?: number;
|
||||
targetReps?: number;
|
||||
targetSets: number;
|
||||
targetReps: number;
|
||||
notes?: string;
|
||||
}>> {
|
||||
try {
|
||||
@ -295,6 +371,11 @@ class LibraryService {
|
||||
const exercise = await this.getExercise(row.exercise_id);
|
||||
if (!exercise) throw new Error(`Exercise ${row.exercise_id} not found`);
|
||||
|
||||
// Ensure required fields are present
|
||||
if (typeof row.target_sets !== 'number' || typeof row.target_reps !== 'number') {
|
||||
throw new Error(`Missing required target sets/reps for exercise ${row.exercise_id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
exercise,
|
||||
targetSets: row.target_sets,
|
||||
|
@ -3,7 +3,6 @@
|
||||
export interface SQLiteRow {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SQLiteResult<T = SQLiteRow> {
|
||||
rows: {
|
||||
_array: T[];
|
||||
@ -16,6 +15,7 @@ export interface SQLiteResult<T = SQLiteRow> {
|
||||
|
||||
export interface SQLiteError extends Error {
|
||||
code?: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SQLiteStatement {
|
||||
|
@ -1,28 +1,42 @@
|
||||
// utils/db/db-service.ts
|
||||
import {
|
||||
openDatabaseSync,
|
||||
SQLiteDatabase
|
||||
} from 'expo-sqlite';
|
||||
import {
|
||||
SQLiteResult,
|
||||
SQLiteError,
|
||||
SQLiteRow
|
||||
} from '@/types/sqlite';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { SQLiteResult, SQLiteRow, SQLiteError } from '@/types/sqlite';
|
||||
|
||||
export class DbService {
|
||||
private db: SQLiteDatabase | null = null;
|
||||
private readonly DEBUG = __DEV__;
|
||||
|
||||
constructor(dbName: string) {
|
||||
try {
|
||||
this.db = openDatabaseSync(dbName);
|
||||
console.log('Database opened:', this.db);
|
||||
this.db = SQLite.openDatabaseSync(dbName);
|
||||
if (this.DEBUG) {
|
||||
console.log('Database opened:', dbName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async executeSql<T extends SQLiteRow = any>(
|
||||
async withTransaction<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
try {
|
||||
if (this.DEBUG) console.log('Starting transaction');
|
||||
await this.db.runAsync('BEGIN TRANSACTION');
|
||||
const result = await operation();
|
||||
await this.db.runAsync('COMMIT');
|
||||
if (this.DEBUG) console.log('Transaction committed');
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.DEBUG) console.log('Rolling back transaction due to:', error);
|
||||
await this.db.runAsync('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async executeSql<T extends SQLiteRow>(
|
||||
sql: string,
|
||||
params: (string | number | null)[] = []
|
||||
): Promise<SQLiteResult<T>> {
|
||||
@ -31,64 +45,112 @@ export class DbService {
|
||||
}
|
||||
|
||||
try {
|
||||
const statement = this.db.prepareSync(sql);
|
||||
const result = statement.executeSync<T>(params);
|
||||
statement.finalizeSync();
|
||||
if (this.DEBUG) {
|
||||
console.log('Executing SQL:', sql);
|
||||
console.log('Parameters:', params);
|
||||
}
|
||||
|
||||
// Use the appropriate method based on the SQL operation type
|
||||
const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
|
||||
|
||||
if (isSelect) {
|
||||
const results = await this.db.getAllAsync<T>(sql, params);
|
||||
return {
|
||||
rows: {
|
||||
_array: results,
|
||||
length: results.length,
|
||||
item: (idx: number) => {
|
||||
// For existing interface compatibility, return first item of array
|
||||
// when index is out of bounds instead of undefined
|
||||
return results[idx >= 0 && idx < results.length ? idx : 0];
|
||||
}
|
||||
},
|
||||
rowsAffected: 0 // SELECT doesn't modify rows
|
||||
};
|
||||
} else {
|
||||
const result = await this.db.runAsync(sql, params);
|
||||
return {
|
||||
rows: {
|
||||
_array: [],
|
||||
length: 0,
|
||||
item: (_: number) => ({} as T) // Return empty object for non-SELECT operations
|
||||
},
|
||||
rowsAffected: result.changes,
|
||||
insertId: result.lastInsertRowId
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: {
|
||||
_array: Array.isArray(result) ? result : [],
|
||||
length: Array.isArray(result) ? result.length : 0,
|
||||
item: (idx: number) => (Array.isArray(result) ? result[idx] : null) as T
|
||||
},
|
||||
rowsAffected: Array.isArray(result) ? result.length : 0,
|
||||
insertId: undefined // SQLite doesn't provide this directly
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('SQL Error:', error, sql, params);
|
||||
// Create proper SQLiteError with all required Error properties
|
||||
const sqlError: SQLiteError = Object.assign(new Error(), {
|
||||
name: 'SQLiteError',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
code: error instanceof Error ? (error as any).code : undefined
|
||||
});
|
||||
|
||||
console.error('SQL Error:', {
|
||||
message: sqlError.message,
|
||||
code: sqlError.code,
|
||||
sql,
|
||||
params
|
||||
});
|
||||
|
||||
throw sqlError;
|
||||
}
|
||||
}
|
||||
|
||||
async executeWrite<T extends SQLiteRow>(
|
||||
sql: string,
|
||||
params: (string | number | null)[] = []
|
||||
): Promise<SQLiteResult<T>> {
|
||||
try {
|
||||
const result = await this.executeSql<T>(sql, params);
|
||||
|
||||
// For INSERT/UPDATE/DELETE operations, verify the operation had an effect
|
||||
const isWriteOperation = /^(INSERT|UPDATE|DELETE)\b/i.test(sql.trim());
|
||||
|
||||
if (isWriteOperation && !result.rowsAffected) {
|
||||
const error = `Write operation failed: ${sql.split(' ')[0]}`;
|
||||
if (this.DEBUG) {
|
||||
console.warn(error, { sql, params });
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.DEBUG) {
|
||||
console.error('Write operation failed:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async executeWrite<T extends SQLiteRow = any>(
|
||||
sql: string,
|
||||
params: (string | number | null)[] = []
|
||||
): Promise<SQLiteResult<T>> {
|
||||
return this.executeSql<T>(sql, params);
|
||||
}
|
||||
|
||||
async executeWriteMany<T extends SQLiteRow = any>(
|
||||
async executeWriteMany<T extends SQLiteRow>(
|
||||
queries: Array<{
|
||||
sql: string;
|
||||
args?: (string | number | null)[]
|
||||
}>
|
||||
): Promise<SQLiteResult<T>[]> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
const results: SQLiteResult<T>[] = [];
|
||||
|
||||
for (const query of queries) {
|
||||
try {
|
||||
return this.withTransaction(async () => {
|
||||
const results: SQLiteResult<T>[] = [];
|
||||
|
||||
for (const query of queries) {
|
||||
const result = await this.executeSql<T>(query.sql, query.args || []);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
console.error('Error executing query:', query, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
async tableExists(tableName: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.executeSql<{ name: string }>(
|
||||
const result = await this.db?.getFirstAsync<{ name: string }>(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
||||
[tableName]
|
||||
);
|
||||
return result.rows._array.length > 0;
|
||||
return !!result;
|
||||
} catch (error) {
|
||||
console.error('Error checking table existence:', error);
|
||||
return false;
|
||||
@ -97,10 +159,25 @@ export class DbService {
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.executeSql('PRAGMA foreign_keys = ON;');
|
||||
await this.db?.execAsync(`
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA journal_mode = WAL;
|
||||
`);
|
||||
} catch (error) {
|
||||
console.error('Error initializing database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
if (this.db) {
|
||||
await this.db.closeAsync();
|
||||
this.db = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error closing database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
@ -69,6 +69,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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user