mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
update to add new exercise UI and database testing
This commit is contained in:
parent
90ea708e9b
commit
76433b93e6
14
CHANGELOG.md
14
CHANGELOG.md
@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
### 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
|
||||
- Input validation for required fields
|
||||
- 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
|
||||
|
||||
### 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
|
||||
- Added dropdowns for equipment 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
|
||||
- Added detailed debug logging
|
||||
|
||||
### Fixed
|
||||
- Exercise deletion functionality
|
||||
- Keyboard overlap issues in exercise creation form
|
||||
- SQLite transaction handling for exercise operations
|
||||
|
||||
### Technical Details
|
||||
1. Database Schema Enforcement:
|
||||
- Added CHECK constraints for equipment types
|
||||
|
@ -1,6 +1,6 @@
|
||||
// app/(tabs)/library/exercises.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, ScrollView, SectionList } from 'react-native';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { View, SectionList, TouchableOpacity, SectionListData } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { ExerciseCard } from '@/components/exercises/ExerciseCard';
|
||||
import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
|
||||
@ -10,17 +10,46 @@ import { Exercise, BaseExercise } from '@/types/exercise';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||
|
||||
interface ExerciseSection {
|
||||
title: string;
|
||||
data: Exercise[];
|
||||
}
|
||||
|
||||
export default function ExercisesScreen() {
|
||||
const db = useSQLiteContext();
|
||||
const exerciseService = React.useMemo(() => new ExerciseService(db), [db]);
|
||||
const sectionListRef = useRef<SectionList>(null);
|
||||
|
||||
const [exercises, setExercises] = useState<Exercise[]>([]);
|
||||
const [sections, setSections] = useState<ExerciseSection[]>([]);
|
||||
const [showNewExercise, setShowNewExercise] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
try {
|
||||
const loadedExercises = await exerciseService.getAllExercises();
|
||||
@ -57,40 +86,64 @@ export default function ExercisesScreen() {
|
||||
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 availableLetters = new Set(sections.map(section => section.title));
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-background">
|
||||
<View className="absolute right-0 top-0 bottom-0 w-6 z-10 justify-center bg-transparent">
|
||||
{alphabet.map((letter) => (
|
||||
<Text
|
||||
<TouchableOpacity
|
||||
key={letter}
|
||||
className="text-xs text-muted-foreground text-center"
|
||||
onPress={() => {
|
||||
// TODO: Implement scroll to section
|
||||
console.log('Scroll to:', letter);
|
||||
}}
|
||||
onPress={() => scrollToSection(letter)}
|
||||
className="py-0.5"
|
||||
>
|
||||
{letter}
|
||||
</Text>
|
||||
<Text
|
||||
className={`text-xs text-center ${
|
||||
availableLetters.has(letter)
|
||||
? 'text-primary font-medium'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{letter}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<ScrollView className="flex-1 mr-6">
|
||||
<View className="py-4">
|
||||
<Text className="text-lg font-semibold mb-4 px-4">All Exercises</Text>
|
||||
<View className="gap-3">
|
||||
{exercises.map(exercise => (
|
||||
<ExerciseCard
|
||||
key={exercise.id}
|
||||
{...exercise}
|
||||
onPress={() => handleExercisePress(exercise.id)}
|
||||
onDelete={() => handleDelete(exercise.id)}
|
||||
/>
|
||||
))}
|
||||
<SectionList
|
||||
ref={sectionListRef}
|
||||
sections={sections}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderSectionHeader={({ section }) => (
|
||||
<View className="py-2 px-4 bg-background/80">
|
||||
<Text className="text-lg font-semibold text-foreground">{section.title}</Text>
|
||||
</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
|
||||
icon={Dumbbell}
|
||||
|
@ -4,16 +4,22 @@ import { View, ScrollView } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
|
||||
import { schema } from '@/lib/db/schema';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
interface TableInfo {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface TableSchema {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
|
||||
interface SchemaVersion {
|
||||
version: number;
|
||||
}
|
||||
@ -41,7 +47,7 @@ export default function ProgramsScreen() {
|
||||
initialized: false,
|
||||
tables: [],
|
||||
});
|
||||
|
||||
const [schemas, setSchemas] = useState<TableSchema[]>([]);
|
||||
const [testResults, setTestResults] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
@ -49,8 +55,20 @@ export default function ProgramsScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
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 () => {
|
||||
try {
|
||||
// Check schema_version table
|
||||
@ -99,6 +117,7 @@ export default function ProgramsScreen() {
|
||||
|
||||
// Refresh database status
|
||||
checkDatabase();
|
||||
inspectDatabase();
|
||||
} catch (error) {
|
||||
console.error('Error resetting database:', error);
|
||||
setTestResults({
|
||||
@ -186,6 +205,35 @@ export default function ProgramsScreen() {
|
||||
<View className="py-4 space-y-4">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
@ -215,6 +263,7 @@ export default function ProgramsScreen() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Operations Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
|
@ -150,7 +150,7 @@ export default function DatabaseDebug() {
|
||||
|
||||
return (
|
||||
<View className="p-4 mb-4">
|
||||
<Card>
|
||||
<Card className="mt-4"> {/* Add mt-4 to create spacing */}
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Text className="text-xl font-semibold">Database Status</Text>
|
||||
|
@ -1,13 +1,14 @@
|
||||
// components/library/NewExerciseSheet.tsx
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import { BaseExercise, ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
import { StorageSource } from '@/types/shared';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { generateId } from '@/utils/ids';
|
||||
|
||||
interface NewExerciseSheetProps {
|
||||
isOpen: boolean;
|
||||
@ -93,87 +94,93 @@ export function NewExerciseSheet({ isOpen, onClose, onSubmit }: NewExerciseSheet
|
||||
<SheetTitle>New Exercise</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetContent>
|
||||
<View className="gap-4">
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Exercise Name</Text>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChangeText={(text) => setFormData(prev => ({ ...prev, title: text }))}
|
||||
placeholder="e.g., Barbell Back Squat"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<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>
|
||||
))}
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
<ScrollView className="gap-4">
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Exercise Name</Text>
|
||||
<Input
|
||||
value={formData.title}
|
||||
onChangeText={(text: string) => setFormData(prev => ({ ...prev, title: text }))}
|
||||
placeholder="e.g., Barbell Back Squat"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Category</Text>
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{CATEGORIES.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
variant={formData.category === category ? 'purple' : 'outline'}
|
||||
onPress={() => setFormData(prev => ({ ...prev, category }))}
|
||||
>
|
||||
<Text className={formData.category === category ? 'text-white' : ''}>
|
||||
{category}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
<View>
|
||||
<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>
|
||||
<Text className="text-base font-medium mb-2">Equipment</Text>
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{EQUIPMENT_OPTIONS.map((eq) => (
|
||||
<Button
|
||||
key={eq}
|
||||
variant={formData.equipment === eq ? 'purple' : 'outline'}
|
||||
onPress={() => setFormData(prev => ({ ...prev, equipment: eq }))}
|
||||
>
|
||||
<Text className={formData.equipment === eq ? 'text-white' : ''}>
|
||||
{eq}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Category</Text>
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{CATEGORIES.map((category) => (
|
||||
<Button
|
||||
key={category}
|
||||
variant={formData.category === category ? 'purple' : 'outline'}
|
||||
onPress={() => setFormData(prev => ({ ...prev, category }))}
|
||||
>
|
||||
<Text className={formData.category === category ? 'text-white' : ''}>
|
||||
{category}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Description</Text>
|
||||
<Input
|
||||
value={formData.description}
|
||||
onChangeText={(text) => setFormData(prev => ({ ...prev, description: text }))}
|
||||
placeholder="Exercise description..."
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Equipment</Text>
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{EQUIPMENT_OPTIONS.map((eq) => (
|
||||
<Button
|
||||
key={eq}
|
||||
variant={formData.equipment === eq ? 'purple' : 'outline'}
|
||||
onPress={() => setFormData(prev => ({ ...prev, equipment: eq }))}
|
||||
>
|
||||
<Text className={formData.equipment === eq ? 'text-white' : ''}>
|
||||
{eq}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant='purple'
|
||||
onPress={handleSubmit}
|
||||
disabled={!formData.title || !formData.equipment}
|
||||
>
|
||||
<Text className="text-white font-semibold">Create Exercise</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-base font-medium mb-2">Description</Text>
|
||||
<Textarea
|
||||
value={formData.description}
|
||||
onChangeText={(text: string) => setFormData(prev => ({ ...prev, description: text }))}
|
||||
placeholder="Exercise description..."
|
||||
numberOfLines={6}
|
||||
className="min-h-[120px]"
|
||||
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>
|
||||
</Sheet>
|
||||
);
|
||||
|
@ -1,22 +1,38 @@
|
||||
// lib/db/schema.ts
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export const SCHEMA_VERSION = 2;
|
||||
|
||||
class Schema {
|
||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||
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 }>(
|
||||
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
|
||||
console.log(`[Schema] Current version: ${version?.version ?? 0}`);
|
||||
return version?.version ?? 0;
|
||||
} catch (error) {
|
||||
console.log('[Schema] Error getting version:', error);
|
||||
return 0; // If table doesn't exist yet
|
||||
}
|
||||
}
|
||||
|
||||
async createTables(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
console.log(`[Schema] Initializing database on ${Platform.OS}`);
|
||||
const currentVersion = await this.getCurrentVersion(db);
|
||||
|
||||
// Schema version tracking
|
||||
@ -28,6 +44,8 @@ class Schema {
|
||||
`);
|
||||
|
||||
if (currentVersion === 0) {
|
||||
console.log('[Schema] Performing fresh install');
|
||||
|
||||
// Drop existing tables if they exist
|
||||
await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`);
|
||||
await db.execAsync(`DROP TABLE IF EXISTS exercises`);
|
||||
@ -66,10 +84,14 @@ class Schema {
|
||||
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||
[1, Date.now()]
|
||||
);
|
||||
|
||||
console.log('[Schema] Base tables created successfully');
|
||||
}
|
||||
|
||||
// Update to version 2 if needed
|
||||
if (currentVersion < 2) {
|
||||
console.log('[Schema] Upgrading to version 2');
|
||||
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS nostr_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
@ -98,15 +120,22 @@ class Schema {
|
||||
try {
|
||||
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
|
||||
} catch (e) {
|
||||
// Column might already exist
|
||||
console.log('[Schema] Note: nostr_event_id column may already exist');
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||
[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)}`);
|
||||
} catch (error) {
|
||||
console.error('[Schema] Error creating tables:', error);
|
||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@expo/cli": "^0.22.16",
|
||||
"@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/native": "^7.0.0",
|
||||
"@rn-primitives/accordion": "^1.1.0",
|
||||
@ -4258,6 +4259,29 @@
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
|
||||
"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": {
|
||||
"version": "0.76.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz",
|
||||
|
@ -25,6 +25,7 @@
|
||||
"dependencies": {
|
||||
"@expo/cli": "^0.22.16",
|
||||
"@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/native": "^7.0.0",
|
||||
"@rn-primitives/accordion": "^1.1.0",
|
||||
@ -57,6 +58,7 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"expo": "^52.0.35",
|
||||
"expo-file-system": "~18.0.10",
|
||||
"expo-linking": "~7.0.4",
|
||||
"expo-navigation-bar": "~4.0.8",
|
||||
"expo-router": "~4.0.16",
|
||||
@ -82,8 +84,7 @@
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "3.3.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.4.7",
|
||||
"expo-file-system": "~18.0.10"
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user