update to add new exercise UI and database testing

This commit is contained in:
DocNR 2025-02-17 11:24:17 -05:00
parent 90ea708e9b
commit 76433b93e6
8 changed files with 283 additions and 106 deletions

View File

@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- Alphabetical quick scroll in exercise library
- Dynamic letter highlighting for available sections
- Smooth scrolling to selected sections
- Sticky section headers for better navigation
- Basic exercise template creation functionality - Basic exercise template creation functionality
- Input validation for required fields - Input validation for required fields
- Schema-compliant field constraints - Schema-compliant field constraints
@ -18,6 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added proper error types and propagation - Added proper error types and propagation
### Changed ### Changed
- Improved exercise library interface
- Removed "Recent Exercises" section for cleaner UI
- Added alphabetical section organization
- Enhanced keyboard handling for input fields
- Increased description text area size
- Updated NewExerciseScreen with constrained inputs - Updated NewExerciseScreen with constrained inputs
- Added dropdowns for equipment selection - Added dropdowns for equipment selection
- Added movement pattern selection - Added movement pattern selection
@ -28,6 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enhanced transaction rollback handling - Enhanced transaction rollback handling
- Added detailed debug logging - Added detailed debug logging
### Fixed
- Exercise deletion functionality
- Keyboard overlap issues in exercise creation form
- SQLite transaction handling for exercise operations
### Technical Details ### Technical Details
1. Database Schema Enforcement: 1. Database Schema Enforcement:
- Added CHECK constraints for equipment types - Added CHECK constraints for equipment types

View File

@ -1,6 +1,6 @@
// app/(tabs)/library/exercises.tsx // app/(tabs)/library/exercises.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { View, ScrollView, SectionList } from 'react-native'; import { View, SectionList, TouchableOpacity, SectionListData } from 'react-native';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { ExerciseCard } from '@/components/exercises/ExerciseCard'; import { ExerciseCard } from '@/components/exercises/ExerciseCard';
import { FloatingActionButton } from '@/components/shared/FloatingActionButton'; import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
@ -10,17 +10,46 @@ import { Exercise, BaseExercise } from '@/types/exercise';
import { useSQLiteContext } from 'expo-sqlite'; import { useSQLiteContext } from 'expo-sqlite';
import { ExerciseService } from '@/lib/db/services/ExerciseService'; import { ExerciseService } from '@/lib/db/services/ExerciseService';
interface ExerciseSection {
title: string;
data: Exercise[];
}
export default function ExercisesScreen() { export default function ExercisesScreen() {
const db = useSQLiteContext(); const db = useSQLiteContext();
const exerciseService = React.useMemo(() => new ExerciseService(db), [db]); const exerciseService = React.useMemo(() => new ExerciseService(db), [db]);
const sectionListRef = useRef<SectionList>(null);
const [exercises, setExercises] = useState<Exercise[]>([]); const [exercises, setExercises] = useState<Exercise[]>([]);
const [sections, setSections] = useState<ExerciseSection[]>([]);
const [showNewExercise, setShowNewExercise] = useState(false); const [showNewExercise, setShowNewExercise] = useState(false);
useEffect(() => { useEffect(() => {
loadExercises(); loadExercises();
}, []); }, []);
useEffect(() => {
// Organize exercises into sections when exercises array changes
const exercisesByLetter = exercises.reduce((acc, exercise) => {
const firstLetter = exercise.title[0].toUpperCase();
if (!acc[firstLetter]) {
acc[firstLetter] = [];
}
acc[firstLetter].push(exercise);
return acc;
}, {} as Record<string, Exercise[]>);
// Create sections array sorted alphabetically
const newSections = Object.entries(exercisesByLetter)
.map(([letter, exercises]) => ({
title: letter,
data: exercises.sort((a, b) => a.title.localeCompare(b.title))
}))
.sort((a, b) => a.title.localeCompare(b.title));
setSections(newSections);
}, [exercises]);
const loadExercises = async () => { const loadExercises = async () => {
try { try {
const loadedExercises = await exerciseService.getAllExercises(); const loadedExercises = await exerciseService.getAllExercises();
@ -57,40 +86,64 @@ export default function ExercisesScreen() {
console.log('Selected exercise:', exerciseId); console.log('Selected exercise:', exerciseId);
}; };
const scrollToSection = (letter: string) => {
const sectionIndex = sections.findIndex(section => section.title === letter);
if (sectionIndex !== -1) {
sectionListRef.current?.scrollToLocation({
sectionIndex,
itemIndex: 0,
animated: true,
viewOffset: 20
});
}
};
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const availableLetters = new Set(sections.map(section => section.title));
return ( return (
<View className="flex-1 bg-background"> <View className="flex-1 bg-background">
<View className="absolute right-0 top-0 bottom-0 w-6 z-10 justify-center bg-transparent"> <View className="absolute right-0 top-0 bottom-0 w-6 z-10 justify-center bg-transparent">
{alphabet.map((letter) => ( {alphabet.map((letter) => (
<Text <TouchableOpacity
key={letter} key={letter}
className="text-xs text-muted-foreground text-center" onPress={() => scrollToSection(letter)}
onPress={() => { className="py-0.5"
// TODO: Implement scroll to section
console.log('Scroll to:', letter);
}}
> >
{letter} <Text
</Text> className={`text-xs text-center ${
availableLetters.has(letter)
? 'text-primary font-medium'
: 'text-muted-foreground'
}`}
>
{letter}
</Text>
</TouchableOpacity>
))} ))}
</View> </View>
<ScrollView className="flex-1 mr-6"> <SectionList
<View className="py-4"> ref={sectionListRef}
<Text className="text-lg font-semibold mb-4 px-4">All Exercises</Text> sections={sections}
<View className="gap-3"> keyExtractor={(item) => item.id}
{exercises.map(exercise => ( renderSectionHeader={({ section }) => (
<ExerciseCard <View className="py-2 px-4 bg-background/80">
key={exercise.id} <Text className="text-lg font-semibold text-foreground">{section.title}</Text>
{...exercise}
onPress={() => handleExercisePress(exercise.id)}
onDelete={() => handleDelete(exercise.id)}
/>
))}
</View> </View>
</View> )}
</ScrollView> renderItem={({ item }) => (
<View className="px-4 py-1">
<ExerciseCard
{...item}
onPress={() => handleExercisePress(item.id)}
onDelete={() => handleDelete(item.id)}
/>
</View>
)}
stickySectionHeadersEnabled
className="flex-1"
/>
<FloatingActionButton <FloatingActionButton
icon={Dumbbell} icon={Dumbbell}

View File

@ -4,16 +4,22 @@ import { View, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2 } from 'lucide-react-native'; import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code } from 'lucide-react-native';
import { useSQLiteContext } from 'expo-sqlite'; import { useSQLiteContext } from 'expo-sqlite';
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise'; import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types'; import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
import { schema } from '@/lib/db/schema'; import { schema } from '@/lib/db/schema';
import { Platform } from 'react-native';
interface TableInfo { interface TableInfo {
name: string; name: string;
} }
interface TableSchema {
name: string;
sql: string;
}
interface SchemaVersion { interface SchemaVersion {
version: number; version: number;
} }
@ -41,7 +47,7 @@ export default function ProgramsScreen() {
initialized: false, initialized: false,
tables: [], tables: [],
}); });
const [schemas, setSchemas] = useState<TableSchema[]>([]);
const [testResults, setTestResults] = useState<{ const [testResults, setTestResults] = useState<{
success: boolean; success: boolean;
message: string; message: string;
@ -49,8 +55,20 @@ export default function ProgramsScreen() {
useEffect(() => { useEffect(() => {
checkDatabase(); checkDatabase();
inspectDatabase();
}, []); }, []);
const inspectDatabase = async () => {
try {
const result = await db.getAllAsync<TableSchema>(
"SELECT name, sql FROM sqlite_master WHERE type='table'"
);
setSchemas(result);
} catch (error) {
console.error('Error inspecting database:', error);
}
};
const checkDatabase = async () => { const checkDatabase = async () => {
try { try {
// Check schema_version table // Check schema_version table
@ -99,6 +117,7 @@ export default function ProgramsScreen() {
// Refresh database status // Refresh database status
checkDatabase(); checkDatabase();
inspectDatabase();
} catch (error) { } catch (error) {
console.error('Error resetting database:', error); console.error('Error resetting database:', error);
setTestResults({ setTestResults({
@ -186,6 +205,35 @@ export default function ProgramsScreen() {
<View className="py-4 space-y-4"> <View className="py-4 space-y-4">
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text> <Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
{/* Schema Inspector Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Code size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Schema ({Platform.OS})</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
{schemas.map((table) => (
<View key={table.name} className="space-y-2">
<Text className="font-semibold">{table.name}</Text>
<Text className="text-muted-foreground text-sm">
{table.sql}
</Text>
</View>
))}
</View>
<Button
className="mt-4"
onPress={inspectDatabase}
>
<Text className="text-primary-foreground">Refresh Schema</Text>
</Button>
</CardContent>
</Card>
{/* Status Card */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex-row items-center gap-2"> <CardTitle className="flex-row items-center gap-2">
@ -215,6 +263,7 @@ export default function ProgramsScreen() {
</CardContent> </CardContent>
</Card> </Card>
{/* Operations Card */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex-row items-center gap-2"> <CardTitle className="flex-row items-center gap-2">

View File

@ -150,7 +150,7 @@ export default function DatabaseDebug() {
return ( return (
<View className="p-4 mb-4"> <View className="p-4 mb-4">
<Card> <Card className="mt-4"> {/* Add mt-4 to create spacing */}
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
<Text className="text-xl font-semibold">Database Status</Text> <Text className="text-xl font-semibold">Database Status</Text>

View File

@ -1,13 +1,14 @@
// components/library/NewExerciseSheet.tsx // components/library/NewExerciseSheet.tsx
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text'; import { Text } from '@/components/ui/text';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { generateId } from '@/utils/ids';
import { BaseExercise, ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise'; import { BaseExercise, ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
import { StorageSource } from '@/types/shared'; import { StorageSource } from '@/types/shared';
import { Textarea } from '@/components/ui/textarea';
import { generateId } from '@/utils/ids';
interface NewExerciseSheetProps { interface NewExerciseSheetProps {
isOpen: boolean; isOpen: boolean;
@ -93,87 +94,93 @@ export function NewExerciseSheet({ isOpen, onClose, onSubmit }: NewExerciseSheet
<SheetTitle>New Exercise</SheetTitle> <SheetTitle>New Exercise</SheetTitle>
</SheetHeader> </SheetHeader>
<SheetContent> <SheetContent>
<View className="gap-4"> <KeyboardAvoidingView
<View> behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
<Text className="text-base font-medium mb-2">Exercise Name</Text> className="flex-1"
<Input >
value={formData.title} <ScrollView className="gap-4">
onChangeText={(text) => setFormData(prev => ({ ...prev, title: text }))} <View>
placeholder="e.g., Barbell Back Squat" <Text className="text-base font-medium mb-2">Exercise Name</Text>
/> <Input
</View> value={formData.title}
onChangeText={(text: string) => setFormData(prev => ({ ...prev, title: text }))}
<View> placeholder="e.g., Barbell Back Squat"
<Text className="text-base font-medium mb-2">Type</Text> />
<View className="flex-row flex-wrap gap-2">
{EXERCISE_TYPES.map((type) => (
<Button
key={type}
variant={formData.type === type ? 'purple' : 'outline'}
onPress={() => setFormData(prev => ({ ...prev, type }))}
>
<Text className={formData.type === type ? 'text-white' : ''}>
{type}
</Text>
</Button>
))}
</View> </View>
</View>
<View> <View>
<Text className="text-base font-medium mb-2">Category</Text> <Text className="text-base font-medium mb-2">Type</Text>
<View className="flex-row flex-wrap gap-2"> <View className="flex-row flex-wrap gap-2">
{CATEGORIES.map((category) => ( {EXERCISE_TYPES.map((type) => (
<Button <Button
key={category} key={type}
variant={formData.category === category ? 'purple' : 'outline'} variant={formData.type === type ? 'purple' : 'outline'}
onPress={() => setFormData(prev => ({ ...prev, category }))} onPress={() => setFormData(prev => ({ ...prev, type }))}
> >
<Text className={formData.category === category ? 'text-white' : ''}> <Text className={formData.type === type ? 'text-white' : ''}>
{category} {type}
</Text> </Text>
</Button> </Button>
))} ))}
</View>
</View> </View>
</View>
<View> <View>
<Text className="text-base font-medium mb-2">Equipment</Text> <Text className="text-base font-medium mb-2">Category</Text>
<View className="flex-row flex-wrap gap-2"> <View className="flex-row flex-wrap gap-2">
{EQUIPMENT_OPTIONS.map((eq) => ( {CATEGORIES.map((category) => (
<Button <Button
key={eq} key={category}
variant={formData.equipment === eq ? 'purple' : 'outline'} variant={formData.category === category ? 'purple' : 'outline'}
onPress={() => setFormData(prev => ({ ...prev, equipment: eq }))} onPress={() => setFormData(prev => ({ ...prev, category }))}
> >
<Text className={formData.equipment === eq ? 'text-white' : ''}> <Text className={formData.category === category ? 'text-white' : ''}>
{eq} {category}
</Text> </Text>
</Button> </Button>
))} ))}
</View>
</View> </View>
</View>
<View> <View>
<Text className="text-base font-medium mb-2">Description</Text> <Text className="text-base font-medium mb-2">Equipment</Text>
<Input <View className="flex-row flex-wrap gap-2">
value={formData.description} {EQUIPMENT_OPTIONS.map((eq) => (
onChangeText={(text) => setFormData(prev => ({ ...prev, description: text }))} <Button
placeholder="Exercise description..." key={eq}
multiline variant={formData.equipment === eq ? 'purple' : 'outline'}
numberOfLines={4} onPress={() => setFormData(prev => ({ ...prev, equipment: eq }))}
/> >
</View> <Text className={formData.equipment === eq ? 'text-white' : ''}>
{eq}
</Text>
</Button>
))}
</View>
</View>
<Button <View>
className="mt-4" <Text className="text-base font-medium mb-2">Description</Text>
variant='purple' <Textarea
onPress={handleSubmit} value={formData.description}
disabled={!formData.title || !formData.equipment} onChangeText={(text: string) => setFormData(prev => ({ ...prev, description: text }))}
> placeholder="Exercise description..."
<Text className="text-white font-semibold">Create Exercise</Text> numberOfLines={6}
</Button> className="min-h-[120px]"
</View> style={{ maxHeight: 200 }}
/>
</View>
<Button
className="mt-4"
variant='purple'
onPress={handleSubmit}
disabled={!formData.title || !formData.equipment}
>
<Text className="text-white font-semibold">Create Exercise</Text>
</Button>
</ScrollView>
</KeyboardAvoidingView>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
); );

View File

@ -1,22 +1,38 @@
// lib/db/schema.ts // lib/db/schema.ts
import { SQLiteDatabase } from 'expo-sqlite'; import { SQLiteDatabase } from 'expo-sqlite';
import { Platform } from 'react-native';
export const SCHEMA_VERSION = 2; export const SCHEMA_VERSION = 2;
class Schema { class Schema {
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> { private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
try { try {
// First check if the table exists
const tableExists = await db.getFirstAsync<{ count: number }>(
`SELECT count(*) as count FROM sqlite_master
WHERE type='table' AND name='schema_version'`
);
if (!tableExists || tableExists.count === 0) {
console.log('[Schema] No schema_version table found');
return 0;
}
const version = await db.getFirstAsync<{ version: number }>( const version = await db.getFirstAsync<{ version: number }>(
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1' 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
); );
console.log(`[Schema] Current version: ${version?.version ?? 0}`);
return version?.version ?? 0; return version?.version ?? 0;
} catch (error) { } catch (error) {
console.log('[Schema] Error getting version:', error);
return 0; // If table doesn't exist yet return 0; // If table doesn't exist yet
} }
} }
async createTables(db: SQLiteDatabase): Promise<void> { async createTables(db: SQLiteDatabase): Promise<void> {
try { try {
console.log(`[Schema] Initializing database on ${Platform.OS}`);
const currentVersion = await this.getCurrentVersion(db); const currentVersion = await this.getCurrentVersion(db);
// Schema version tracking // Schema version tracking
@ -28,6 +44,8 @@ class Schema {
`); `);
if (currentVersion === 0) { if (currentVersion === 0) {
console.log('[Schema] Performing fresh install');
// Drop existing tables if they exist // Drop existing tables if they exist
await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`); await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`);
await db.execAsync(`DROP TABLE IF EXISTS exercises`); await db.execAsync(`DROP TABLE IF EXISTS exercises`);
@ -66,10 +84,14 @@ class Schema {
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)', 'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[1, Date.now()] [1, Date.now()]
); );
console.log('[Schema] Base tables created successfully');
} }
// Update to version 2 if needed // Update to version 2 if needed
if (currentVersion < 2) { if (currentVersion < 2) {
console.log('[Schema] Upgrading to version 2');
await db.execAsync(` await db.execAsync(`
CREATE TABLE IF NOT EXISTS nostr_events ( CREATE TABLE IF NOT EXISTS nostr_events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@ -98,15 +120,22 @@ class Schema {
try { try {
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`); await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
} catch (e) { } catch (e) {
// Column might already exist console.log('[Schema] Note: nostr_event_id column may already exist');
} }
await db.runAsync( await db.runAsync(
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)', 'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
[2, Date.now()] [2, Date.now()]
); );
console.log('[Schema] Version 2 upgrade completed');
} }
// Verify final schema
const tables = await db.getAllAsync<{ name: string }>(
"SELECT name FROM sqlite_master WHERE type='table'"
);
console.log('[Schema] Final tables:', tables.map(t => t.name).join(', '));
console.log(`[Schema] Database initialized at version ${await this.getCurrentVersion(db)}`); console.log(`[Schema] Database initialized at version ${await this.getCurrentVersion(db)}`);
} catch (error) { } catch (error) {
console.error('[Schema] Error creating tables:', error); console.error('[Schema] Error creating tables:', error);

24
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@expo/cli": "^0.22.16", "@expo/cli": "^0.22.16",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
"@react-native-clipboard/clipboard": "^1.16.1",
"@react-navigation/material-top-tabs": "^7.1.0", "@react-navigation/material-top-tabs": "^7.1.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@rn-primitives/accordion": "^1.1.0", "@rn-primitives/accordion": "^1.1.0",
@ -4258,6 +4259,29 @@
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.1.tgz",
"integrity": "sha512-YdSwSS3P4IiJq5nW0iv3qpntDAzBf/xoew2zRPGJ6SJZr/Lhk4aWyR506EWl6BID+iQy7xQmzHXZYR5H4apM6g==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": ">= 16.9.0",
"react-native": ">= 0.61.5",
"react-native-macos": ">= 0.61.0",
"react-native-windows": ">= 0.61.0"
},
"peerDependenciesMeta": {
"react-native-macos": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.76.7", "version": "0.76.7",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz",

View File

@ -25,6 +25,7 @@
"dependencies": { "dependencies": {
"@expo/cli": "^0.22.16", "@expo/cli": "^0.22.16",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
"@react-native-clipboard/clipboard": "^1.16.1",
"@react-navigation/material-top-tabs": "^7.1.0", "@react-navigation/material-top-tabs": "^7.1.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@rn-primitives/accordion": "^1.1.0", "@rn-primitives/accordion": "^1.1.0",
@ -57,6 +58,7 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"expo": "^52.0.35", "expo": "^52.0.35",
"expo-file-system": "~18.0.10",
"expo-linking": "~7.0.4", "expo-linking": "~7.0.4",
"expo-navigation-bar": "~4.0.8", "expo-navigation-bar": "~4.0.8",
"expo-router": "~4.0.16", "expo-router": "~4.0.16",
@ -82,8 +84,7 @@
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss": "3.3.5", "tailwindcss": "3.3.5",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"zustand": "^4.4.7", "zustand": "^4.4.7"
"expo-file-system": "~18.0.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",