mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-03 15:52:06 +00:00
add new exercises to sqlite db
This commit is contained in:
parent
df91cbd9bb
commit
90ea708e9b
@ -1,90 +1,82 @@
|
||||
// app/(tabs)/library/exercises.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { View, ScrollView } from 'react-native';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, ScrollView, SectionList } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { ExerciseCard } from '@/components/exercises/ExerciseCard';
|
||||
import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
|
||||
import { NewExerciseSheet } from '@/components/library/NewExerciseSheet';
|
||||
import { Dumbbell } from 'lucide-react-native';
|
||||
import { Exercise } from '@/types/library';
|
||||
import { generateId } from '@/utils/ids';
|
||||
import DatabaseDebug from '@/components/DatabaseDebug'; // Add this import
|
||||
|
||||
const initialExercises: Exercise[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Barbell Back Squat',
|
||||
category: 'Legs',
|
||||
equipment: 'barbell',
|
||||
tags: ['compound', 'strength'],
|
||||
source: 'local',
|
||||
description: 'A compound exercise that primarily targets the quadriceps, hamstrings, and glutes.',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Pull-ups',
|
||||
category: 'Pull',
|
||||
equipment: 'bodyweight',
|
||||
tags: ['upper-body', 'compound'],
|
||||
source: 'local',
|
||||
description: 'An upper body pulling exercise that targets the latissimus dorsi and biceps.',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Bench Press',
|
||||
category: 'Push',
|
||||
equipment: 'barbell',
|
||||
tags: ['push', 'strength'],
|
||||
source: 'nostr',
|
||||
description: 'A compound pushing exercise that targets the chest, shoulders, and triceps.',
|
||||
},
|
||||
];
|
||||
import { Exercise, BaseExercise } from '@/types/exercise';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { ExerciseService } from '@/lib/db/services/ExerciseService';
|
||||
|
||||
export default function ExercisesScreen() {
|
||||
const [exercises, setExercises] = useState<Exercise[]>(initialExercises);
|
||||
const db = useSQLiteContext();
|
||||
const exerciseService = React.useMemo(() => new ExerciseService(db), [db]);
|
||||
|
||||
const [exercises, setExercises] = useState<Exercise[]>([]);
|
||||
const [showNewExercise, setShowNewExercise] = useState(false);
|
||||
|
||||
const handleAddExercise = (exerciseData: Omit<Exercise, 'id' | 'source'>) => {
|
||||
const newExercise: Exercise = {
|
||||
...exerciseData,
|
||||
id: generateId(),
|
||||
source: 'local',
|
||||
};
|
||||
setExercises(prev => [...prev, newExercise]);
|
||||
setShowNewExercise(false);
|
||||
useEffect(() => {
|
||||
loadExercises();
|
||||
}, []);
|
||||
|
||||
const loadExercises = async () => {
|
||||
try {
|
||||
const loadedExercises = await exerciseService.getAllExercises();
|
||||
setExercises(loadedExercises);
|
||||
} catch (error) {
|
||||
console.error('Error loading exercises:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get recent exercises
|
||||
const recentExercises = exercises.slice(0, 2);
|
||||
const handleAddExercise = async (exerciseData: BaseExercise) => {
|
||||
try {
|
||||
await exerciseService.createExercise({
|
||||
...exerciseData,
|
||||
created_at: Date.now(),
|
||||
source: 'local'
|
||||
});
|
||||
await loadExercises();
|
||||
setShowNewExercise(false);
|
||||
} catch (error) {
|
||||
console.error('Error adding exercise:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setExercises(current => current.filter(ex => ex.id !== id));
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await exerciseService.deleteExercise(id);
|
||||
await loadExercises();
|
||||
} catch (error) {
|
||||
console.error('Error deleting exercise:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExercisePress = (exerciseId: string) => {
|
||||
console.log('Selected exercise:', exerciseId);
|
||||
};
|
||||
|
||||
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-background">
|
||||
{__DEV__ && <DatabaseDebug />} {/* Only show in development */}
|
||||
<ScrollView className="flex-1">
|
||||
{/* Recent Exercises Section */}
|
||||
<View className="py-4">
|
||||
<Text className="text-lg font-semibold mb-4 px-4">Recent Exercises</Text>
|
||||
<View className="gap-3">
|
||||
{recentExercises.map(exercise => (
|
||||
<ExerciseCard
|
||||
key={exercise.id}
|
||||
{...exercise}
|
||||
onPress={() => handleExercisePress(exercise.id)}
|
||||
onDelete={() => handleDelete(exercise.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View className="absolute right-0 top-0 bottom-0 w-6 z-10 justify-center bg-transparent">
|
||||
{alphabet.map((letter) => (
|
||||
<Text
|
||||
key={letter}
|
||||
className="text-xs text-muted-foreground text-center"
|
||||
onPress={() => {
|
||||
// TODO: Implement scroll to section
|
||||
console.log('Scroll to:', letter);
|
||||
}}
|
||||
>
|
||||
{letter}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* All Exercises Section */}
|
||||
<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">
|
||||
|
@ -1,11 +1,282 @@
|
||||
// app/(tabs)/library/(tabs)/programs.tsx
|
||||
import { View } from 'react-native';
|
||||
// app/(tabs)/library/programs.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { 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';
|
||||
|
||||
interface TableInfo {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SchemaVersion {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface ExerciseRow {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
category: string;
|
||||
equipment: string | null;
|
||||
description: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
format_json: string;
|
||||
format_units_json: string;
|
||||
}
|
||||
|
||||
export default function ProgramsScreen() {
|
||||
const db = useSQLiteContext();
|
||||
const [dbStatus, setDbStatus] = useState<{
|
||||
initialized: boolean;
|
||||
tables: string[];
|
||||
error?: string;
|
||||
}>({
|
||||
initialized: false,
|
||||
tables: [],
|
||||
});
|
||||
|
||||
const [testResults, setTestResults] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkDatabase();
|
||||
}, []);
|
||||
|
||||
const checkDatabase = async () => {
|
||||
try {
|
||||
// Check schema_version table
|
||||
const version = await db.getFirstAsync<SchemaVersion>(
|
||||
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
|
||||
// Get all tables
|
||||
const tables = await db.getAllAsync<TableInfo>(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
);
|
||||
|
||||
setDbStatus({
|
||||
initialized: !!version,
|
||||
tables: tables.map(t => t.name),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking database:', error);
|
||||
setDbStatus(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const resetDatabase = async () => {
|
||||
try {
|
||||
await db.withTransactionAsync(async () => {
|
||||
// Drop all tables
|
||||
const tables = await db.getAllAsync<{ name: string }>(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
||||
);
|
||||
|
||||
for (const { name } of tables) {
|
||||
await db.execAsync(`DROP TABLE IF EXISTS ${name}`);
|
||||
}
|
||||
|
||||
// Recreate schema
|
||||
await schema.createTables(db);
|
||||
});
|
||||
|
||||
setTestResults({
|
||||
success: true,
|
||||
message: 'Database reset successfully'
|
||||
});
|
||||
|
||||
// Refresh database status
|
||||
checkDatabase();
|
||||
} catch (error) {
|
||||
console.error('Error resetting database:', error);
|
||||
setTestResults({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error during reset'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const runTestInsert = async () => {
|
||||
try {
|
||||
// Test exercise
|
||||
const testExercise = {
|
||||
title: "Test Squat",
|
||||
type: "strength" as ExerciseType,
|
||||
category: "Legs" as ExerciseCategory,
|
||||
equipment: "barbell" as Equipment,
|
||||
description: "Test exercise",
|
||||
tags: ["test", "legs"],
|
||||
format: {
|
||||
weight: true,
|
||||
reps: true
|
||||
},
|
||||
format_units: {
|
||||
weight: "kg" as const,
|
||||
reps: "count" as const
|
||||
}
|
||||
};
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Insert exercise using withTransactionAsync
|
||||
await db.withTransactionAsync(async () => {
|
||||
// Insert exercise
|
||||
await db.runAsync(
|
||||
`INSERT INTO exercises (
|
||||
id, title, type, category, equipment, description,
|
||||
format_json, format_units_json, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
'test-1',
|
||||
testExercise.title,
|
||||
testExercise.type,
|
||||
testExercise.category,
|
||||
testExercise.equipment || null,
|
||||
testExercise.description || null,
|
||||
JSON.stringify(testExercise.format),
|
||||
JSON.stringify(testExercise.format_units),
|
||||
timestamp,
|
||||
timestamp
|
||||
]
|
||||
);
|
||||
|
||||
// Insert tags
|
||||
for (const tag of testExercise.tags) {
|
||||
await db.runAsync(
|
||||
"INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)",
|
||||
['test-1', tag]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify insert
|
||||
const result = await db.getFirstAsync<ExerciseRow>(
|
||||
"SELECT * FROM exercises WHERE id = ?",
|
||||
['test-1']
|
||||
);
|
||||
|
||||
setTestResults({
|
||||
success: true,
|
||||
message: `Successfully inserted and verified test exercise: ${JSON.stringify(result, null, 2)}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test insert error:', error);
|
||||
setTestResults({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Text>Programs (Coming Soon)</Text>
|
||||
</View>
|
||||
<ScrollView className="flex-1 bg-background p-4">
|
||||
<View className="py-4 space-y-4">
|
||||
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
<Database size={20} className="text-foreground" />
|
||||
<Text className="text-lg font-semibold">Database Status</Text>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<View className="space-y-2">
|
||||
<Text>Initialized: {dbStatus.initialized ? '✅' : '❌'}</Text>
|
||||
<Text>Tables Found: {dbStatus.tables.length}</Text>
|
||||
<View className="pl-4">
|
||||
{dbStatus.tables.map(table => (
|
||||
<Text key={table} className="text-muted-foreground">• {table}</Text>
|
||||
))}
|
||||
</View>
|
||||
{dbStatus.error && (
|
||||
<View className="mt-4 p-4 bg-destructive/10 rounded-lg border border-destructive">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<AlertCircle className="text-destructive" size={20} />
|
||||
<Text className="font-semibold text-destructive">Error</Text>
|
||||
</View>
|
||||
<Text className="mt-2 text-destructive">{dbStatus.error}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex-row items-center gap-2">
|
||||
<RefreshCcw size={20} className="text-foreground" />
|
||||
<Text className="text-lg font-semibold">Database Operations</Text>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<View className="space-y-4">
|
||||
<Button
|
||||
onPress={runTestInsert}
|
||||
className="w-full"
|
||||
>
|
||||
<Text className="text-primary-foreground">Run Test Insert</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={resetDatabase}
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
>
|
||||
<Trash2 size={18} className="mr-2" />
|
||||
<Text className="text-destructive-foreground">Reset Database</Text>
|
||||
</Button>
|
||||
|
||||
{testResults && (
|
||||
<View className={`mt-4 p-4 rounded-lg border ${
|
||||
testResults.success
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'bg-destructive/10 border-destructive'
|
||||
}`}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
{testResults.success ? (
|
||||
<CheckCircle2
|
||||
className="text-primary"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<AlertCircle
|
||||
className="text-destructive"
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
<Text className={`font-semibold ${
|
||||
testResults.success ? 'text-primary' : 'text-destructive'
|
||||
}`}>
|
||||
{testResults.success ? "Success" : "Error"}
|
||||
</Text>
|
||||
</View>
|
||||
<ScrollView className="mt-2">
|
||||
<Text className={`${
|
||||
testResults.success ? 'text-foreground' : 'text-destructive'
|
||||
}`}>
|
||||
{testResults.message}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { AlertCircle, CheckCircle2 } from 'lucide-react-native';
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types';
|
||||
|
||||
interface TableInfo {
|
||||
name: string;
|
||||
@ -15,6 +16,20 @@ interface SchemaVersion {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface ExerciseRow {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
category: string;
|
||||
equipment: string | null;
|
||||
description: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
source: string;
|
||||
format_json: string;
|
||||
format_units_json: string;
|
||||
}
|
||||
|
||||
export default function DatabaseDebug() {
|
||||
const db = useSQLiteContext();
|
||||
const [dbStatus, setDbStatus] = useState<{
|
||||
@ -52,6 +67,7 @@ export default function DatabaseDebug() {
|
||||
tables: tables.map(t => t.name),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking database:', error);
|
||||
setDbStatus(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
@ -78,17 +94,17 @@ export default function DatabaseDebug() {
|
||||
reps: "count" as const
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Insert exercise using withTransactionAsync
|
||||
await db.withTransactionAsync(async () => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Insert exercise
|
||||
await db.runAsync(
|
||||
`INSERT INTO exercises (
|
||||
id, title, type, category, equipment, description,
|
||||
created_at, updated_at, source,
|
||||
format_json, format_units_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
format_json, format_units_json, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
'test-1',
|
||||
testExercise.title,
|
||||
@ -96,14 +112,13 @@ export default function DatabaseDebug() {
|
||||
testExercise.category,
|
||||
testExercise.equipment || null,
|
||||
testExercise.description || null,
|
||||
timestamp,
|
||||
timestamp,
|
||||
'local',
|
||||
JSON.stringify(testExercise.format),
|
||||
JSON.stringify(testExercise.format_units)
|
||||
JSON.stringify(testExercise.format_units),
|
||||
timestamp,
|
||||
timestamp
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
// Insert tags
|
||||
for (const tag of testExercise.tags) {
|
||||
await db.runAsync(
|
||||
@ -112,18 +127,20 @@ export default function DatabaseDebug() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Verify insert
|
||||
const result = await db.getFirstAsync(
|
||||
"SELECT * FROM exercises WHERE id = ?",
|
||||
const result = await db.getFirstAsync<ExerciseRow>(
|
||||
"SELECT * FROM exercises WHERE id = ?",
|
||||
['test-1']
|
||||
);
|
||||
|
||||
|
||||
setTestResults({
|
||||
success: true,
|
||||
message: `Successfully inserted and verified test exercise: ${JSON.stringify(result, null, 2)}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Test insert error:', error);
|
||||
setTestResults({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Exercise } from '@/types/library';
|
||||
import { Exercise } from '@/types/exercise';
|
||||
|
||||
interface ExerciseCardProps extends Exercise {
|
||||
onPress: () => void;
|
||||
@ -48,23 +48,17 @@ export function ExerciseCard({
|
||||
const [showSheet, setShowSheet] = React.useState(false);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = React.useState(false);
|
||||
|
||||
const handleDeletePress = () => {
|
||||
setShowDeleteAlert(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
onDelete(id);
|
||||
setShowDeleteAlert(false);
|
||||
};
|
||||
|
||||
const handleCardPress = () => {
|
||||
setShowSheet(true);
|
||||
onPress();
|
||||
if (showSheet) {
|
||||
setShowSheet(false); // Close detail sheet if open
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity onPress={handleCardPress} activeOpacity={0.7}>
|
||||
<TouchableOpacity onPress={() => setShowSheet(true)} activeOpacity={0.7}>
|
||||
<Card className="mx-4">
|
||||
<CardContent className="p-4">
|
||||
<View className="flex-row justify-between items-start">
|
||||
@ -143,8 +137,8 @@ export function ExerciseCard({
|
||||
<Trash2
|
||||
size={20}
|
||||
color={Platform.select({
|
||||
ios: undefined, // Let className handle it
|
||||
android: '#8B5CF6' // Explicit color for Android
|
||||
ios: undefined,
|
||||
android: '#dc2626'
|
||||
})}
|
||||
className="text-destructive"
|
||||
/>
|
||||
@ -175,7 +169,6 @@ export function ExerciseCard({
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Bottom sheet section */}
|
||||
<Sheet isOpen={showSheet} onClose={() => setShowSheet(false)}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
|
@ -52,18 +52,8 @@ export function NewExerciseSheet({ isOpen, onClose, onSubmit }: NewExerciseSheet
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title || !formData.equipment) return;
|
||||
|
||||
// Cast to any as a temporary workaround for the TypeScript error
|
||||
const exercise = {
|
||||
// BaseExercise properties
|
||||
title: formData.title,
|
||||
type: formData.type,
|
||||
category: formData.category,
|
||||
equipment: formData.equipment,
|
||||
description: formData.description,
|
||||
tags: formData.tags,
|
||||
format: formData.format,
|
||||
format_units: formData.format_units,
|
||||
// SyncableContent properties
|
||||
...formData,
|
||||
id: generateId('local'),
|
||||
created_at: Date.now(),
|
||||
availability: {
|
||||
|
124
lib/db/schema.ts
124
lib/db/schema.ts
@ -1,11 +1,25 @@
|
||||
// lib/db/schema.ts
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
|
||||
export const SCHEMA_VERSION = 1;
|
||||
export const SCHEMA_VERSION = 2;
|
||||
|
||||
class Schema {
|
||||
private async getCurrentVersion(db: SQLiteDatabase): Promise<number> {
|
||||
try {
|
||||
const version = await db.getFirstAsync<{ version: number }>(
|
||||
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
return version?.version ?? 0;
|
||||
} catch (error) {
|
||||
return 0; // If table doesn't exist yet
|
||||
}
|
||||
}
|
||||
|
||||
async createTables(db: SQLiteDatabase): Promise<void> {
|
||||
try {
|
||||
// Version tracking
|
||||
const currentVersion = await this.getCurrentVersion(db);
|
||||
|
||||
// Schema version tracking
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
@ -13,43 +27,87 @@ class Schema {
|
||||
);
|
||||
`);
|
||||
|
||||
// Exercise Definitions
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS exercises (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
equipment TEXT,
|
||||
description TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
if (currentVersion === 0) {
|
||||
// Drop existing tables if they exist
|
||||
await db.execAsync(`DROP TABLE IF EXISTS exercise_tags`);
|
||||
await db.execAsync(`DROP TABLE IF EXISTS exercises`);
|
||||
await db.execAsync(`DROP TABLE IF EXISTS event_tags`);
|
||||
await db.execAsync(`DROP TABLE IF EXISTS nostr_events`);
|
||||
|
||||
// Create base tables
|
||||
await db.execAsync(`
|
||||
CREATE TABLE exercises (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('strength', 'cardio', 'bodyweight')),
|
||||
category TEXT NOT NULL,
|
||||
equipment TEXT,
|
||||
description TEXT,
|
||||
format_json TEXT,
|
||||
format_units_json TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'local'
|
||||
);
|
||||
`);
|
||||
|
||||
// Exercise Tags
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS exercise_tags (
|
||||
exercise_id TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
UNIQUE(exercise_id, tag)
|
||||
);
|
||||
await db.execAsync(`
|
||||
CREATE TABLE exercise_tags (
|
||||
exercise_id TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
UNIQUE(exercise_id, tag)
|
||||
);
|
||||
CREATE INDEX idx_exercise_tags ON exercise_tags(tag);
|
||||
`);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_tags ON exercise_tags(tag);
|
||||
`);
|
||||
|
||||
// Set initial schema version if not exists
|
||||
const version = await db.getFirstAsync<{ version: number }>(
|
||||
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
|
||||
if (!version) {
|
||||
// Set initial version
|
||||
await db.runAsync(
|
||||
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||
[SCHEMA_VERSION, Date.now()]
|
||||
[1, Date.now()]
|
||||
);
|
||||
}
|
||||
|
||||
// Update to version 2 if needed
|
||||
if (currentVersion < 2) {
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS nostr_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
pubkey TEXT NOT NULL,
|
||||
kind INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sig TEXT,
|
||||
raw_event TEXT NOT NULL,
|
||||
received_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS event_tags (
|
||||
event_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
index_num INTEGER NOT NULL,
|
||||
FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_tags ON event_tags(name, value);
|
||||
`);
|
||||
|
||||
// Add Nostr reference to exercises if not exists
|
||||
try {
|
||||
await db.execAsync(`ALTER TABLE exercises ADD COLUMN nostr_event_id TEXT REFERENCES nostr_events(id)`);
|
||||
} catch (e) {
|
||||
// Column might already exist
|
||||
}
|
||||
|
||||
await db.runAsync(
|
||||
'INSERT INTO schema_version (version, updated_at) VALUES (?, ?)',
|
||||
[2, Date.now()]
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[Schema] Database initialized at version ${await this.getCurrentVersion(db)}`);
|
||||
} catch (error) {
|
||||
console.error('[Schema] Error creating tables:', error);
|
||||
throw error;
|
||||
|
444
lib/db/services/ExerciseService.ts
Normal file
444
lib/db/services/ExerciseService.ts
Normal file
@ -0,0 +1,444 @@
|
||||
// lib/db/services/ExerciseService.ts
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { Exercise, ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise';
|
||||
import { generateId } from '@/utils/ids';
|
||||
|
||||
export class ExerciseService {
|
||||
constructor(private db: SQLiteDatabase) {}
|
||||
|
||||
// Add this new method
|
||||
async getAllExercises(): Promise<Exercise[]> {
|
||||
try {
|
||||
const exercises = await this.db.getAllAsync<any>(`
|
||||
SELECT * FROM exercises ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
const exerciseIds = exercises.map(e => e.id);
|
||||
if (exerciseIds.length === 0) return [];
|
||||
|
||||
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
|
||||
`SELECT exercise_id, tag
|
||||
FROM exercise_tags
|
||||
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
|
||||
exerciseIds
|
||||
);
|
||||
|
||||
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
|
||||
acc[exercise_id] = acc[exercise_id] || [];
|
||||
acc[exercise_id].push(tag);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
return exercises.map(exercise => ({
|
||||
...exercise,
|
||||
tags: tagsByExercise[exercise.id] || [],
|
||||
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
|
||||
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
|
||||
availability: { source: [exercise.source] }
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error getting all exercises:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update createExercise to handle all required fields
|
||||
async createExercise(exercise: Omit<Exercise, 'id' | 'availability'>): Promise<string> {
|
||||
const id = generateId();
|
||||
const timestamp = Date.now();
|
||||
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO exercises (
|
||||
id, title, type, category, equipment, description,
|
||||
format_json, format_units_json, created_at, updated_at, source
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
exercise.title,
|
||||
exercise.type,
|
||||
exercise.category,
|
||||
exercise.equipment || null,
|
||||
exercise.description || null,
|
||||
exercise.format ? JSON.stringify(exercise.format) : null,
|
||||
exercise.format_units ? JSON.stringify(exercise.format_units) : null,
|
||||
timestamp,
|
||||
timestamp,
|
||||
exercise.source || 'local'
|
||||
]
|
||||
);
|
||||
|
||||
if (exercise.tags?.length) {
|
||||
for (const tag of exercise.tags) {
|
||||
await this.db.runAsync(
|
||||
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
|
||||
[id, tag]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error('Error creating exercise:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getExercise(id: string): Promise<Exercise | null> {
|
||||
try {
|
||||
// Get exercise data
|
||||
const exercise = await this.db.getFirstAsync<Exercise>(
|
||||
`SELECT * FROM exercises WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!exercise) return null;
|
||||
|
||||
// Get tags
|
||||
const tags = await this.db.getAllAsync<{ tag: string }>(
|
||||
'SELECT tag FROM exercise_tags WHERE exercise_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
return {
|
||||
...exercise,
|
||||
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
|
||||
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
|
||||
tags: tags.map(t => t.tag)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting exercise:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateExercise(id: string, exercise: Partial<Exercise>): Promise<void> {
|
||||
const timestamp = Date.now();
|
||||
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
// Build update query dynamically based on provided fields
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (exercise.title !== undefined) {
|
||||
updates.push('title = ?');
|
||||
values.push(exercise.title);
|
||||
}
|
||||
|
||||
if (exercise.type !== undefined) {
|
||||
updates.push('type = ?');
|
||||
values.push(exercise.type);
|
||||
}
|
||||
|
||||
if (exercise.category !== undefined) {
|
||||
updates.push('category = ?');
|
||||
values.push(exercise.category);
|
||||
}
|
||||
|
||||
if (exercise.equipment !== undefined) {
|
||||
updates.push('equipment = ?');
|
||||
values.push(exercise.equipment);
|
||||
}
|
||||
|
||||
if (exercise.description !== undefined) {
|
||||
updates.push('description = ?');
|
||||
values.push(exercise.description);
|
||||
}
|
||||
|
||||
if (exercise.format !== undefined) {
|
||||
updates.push('format_json = ?');
|
||||
values.push(JSON.stringify(exercise.format));
|
||||
}
|
||||
|
||||
if (exercise.format_units !== undefined) {
|
||||
updates.push('format_units_json = ?');
|
||||
values.push(JSON.stringify(exercise.format_units));
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
updates.push('updated_at = ?');
|
||||
values.push(timestamp);
|
||||
|
||||
// Add id to values array
|
||||
values.push(id);
|
||||
|
||||
await this.db.runAsync(
|
||||
`UPDATE exercises SET ${updates.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
}
|
||||
|
||||
// Update tags if provided
|
||||
if (exercise.tags !== undefined) {
|
||||
// Delete existing tags
|
||||
await this.db.runAsync(
|
||||
'DELETE FROM exercise_tags WHERE exercise_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
// Insert new tags
|
||||
for (const tag of exercise.tags) {
|
||||
await this.db.runAsync(
|
||||
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
|
||||
[id, tag]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating exercise:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExercise(id: string): Promise<void> {
|
||||
try {
|
||||
console.log('Deleting exercise:', id);
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
// Due to ON DELETE CASCADE, we only need to delete from exercises
|
||||
const result = await this.db.runAsync('DELETE FROM exercises WHERE id = ?', [id]);
|
||||
console.log('Delete result:', result);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting exercise:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async searchExercises(query?: string, filters?: {
|
||||
types?: ExerciseType[];
|
||||
categories?: ExerciseCategory[];
|
||||
equipment?: Equipment[];
|
||||
tags?: string[];
|
||||
source?: 'local' | 'powr' | 'nostr';
|
||||
}): Promise<Exercise[]> {
|
||||
try {
|
||||
let sql = `
|
||||
SELECT DISTINCT e.*
|
||||
FROM exercises e
|
||||
LEFT JOIN exercise_tags et ON e.id = et.exercise_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
// Add search condition
|
||||
if (query) {
|
||||
sql += ' AND (e.title LIKE ? OR e.description LIKE ?)';
|
||||
params.push(`%${query}%`, `%${query}%`);
|
||||
}
|
||||
|
||||
// Add filter conditions
|
||||
if (filters?.types?.length) {
|
||||
sql += ` AND e.type IN (${filters.types.map(() => '?').join(',')})`;
|
||||
params.push(...filters.types);
|
||||
}
|
||||
|
||||
if (filters?.categories?.length) {
|
||||
sql += ` AND e.category IN (${filters.categories.map(() => '?').join(',')})`;
|
||||
params.push(...filters.categories);
|
||||
}
|
||||
|
||||
if (filters?.equipment?.length) {
|
||||
sql += ` AND e.equipment IN (${filters.equipment.map(() => '?').join(',')})`;
|
||||
params.push(...filters.equipment);
|
||||
}
|
||||
|
||||
if (filters?.source) {
|
||||
sql += ' AND e.source = ?';
|
||||
params.push(filters.source);
|
||||
}
|
||||
|
||||
// Handle tag filtering
|
||||
if (filters?.tags?.length) {
|
||||
sql += `
|
||||
AND e.id IN (
|
||||
SELECT exercise_id
|
||||
FROM exercise_tags
|
||||
WHERE tag IN (${filters.tags.map(() => '?').join(',')})
|
||||
GROUP BY exercise_id
|
||||
HAVING COUNT(DISTINCT tag) = ?
|
||||
)
|
||||
`;
|
||||
params.push(...filters.tags, filters.tags.length);
|
||||
}
|
||||
|
||||
// Add ordering
|
||||
sql += ' ORDER BY e.title ASC';
|
||||
|
||||
// Get exercises
|
||||
const exercises = await this.db.getAllAsync<Exercise>(sql, params);
|
||||
|
||||
// Get tags for all exercises
|
||||
const exerciseIds = exercises.map(e => e.id);
|
||||
if (exerciseIds.length) {
|
||||
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
|
||||
`SELECT exercise_id, tag
|
||||
FROM exercise_tags
|
||||
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
|
||||
exerciseIds
|
||||
);
|
||||
|
||||
// Group tags by exercise
|
||||
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
|
||||
acc[exercise_id] = acc[exercise_id] || [];
|
||||
acc[exercise_id].push(tag);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
// Add tags to exercises
|
||||
return exercises.map(exercise => ({
|
||||
...exercise,
|
||||
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
|
||||
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
|
||||
tags: tagsByExercise[exercise.id] || []
|
||||
}));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
} catch (error) {
|
||||
console.error('Error searching exercises:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentExercises(limit: number = 10): Promise<Exercise[]> {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT e.*
|
||||
FROM exercises e
|
||||
ORDER BY e.updated_at DESC
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
const exercises = await this.db.getAllAsync<Exercise>(sql, [limit]);
|
||||
|
||||
// Get tags for these exercises
|
||||
const exerciseIds = exercises.map(e => e.id);
|
||||
if (exerciseIds.length) {
|
||||
const tags = await this.db.getAllAsync<{ exercise_id: string; tag: string }>(
|
||||
`SELECT exercise_id, tag
|
||||
FROM exercise_tags
|
||||
WHERE exercise_id IN (${exerciseIds.map(() => '?').join(',')})`,
|
||||
exerciseIds
|
||||
);
|
||||
|
||||
// Group tags by exercise
|
||||
const tagsByExercise = tags.reduce((acc, { exercise_id, tag }) => {
|
||||
acc[exercise_id] = acc[exercise_id] || [];
|
||||
acc[exercise_id].push(tag);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
return exercises.map(exercise => ({
|
||||
...exercise,
|
||||
format: exercise.format_json ? JSON.parse(exercise.format_json) : undefined,
|
||||
format_units: exercise.format_units_json ? JSON.parse(exercise.format_units_json) : undefined,
|
||||
tags: tagsByExercise[exercise.id] || []
|
||||
}));
|
||||
}
|
||||
|
||||
return exercises;
|
||||
} catch (error) {
|
||||
console.error('Error getting recent exercises:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getExerciseTags(): Promise<{ tag: string; count: number }[]> {
|
||||
try {
|
||||
return await this.db.getAllAsync<{ tag: string; count: number }>(
|
||||
`SELECT tag, COUNT(*) as count
|
||||
FROM exercise_tags
|
||||
GROUP BY tag
|
||||
ORDER BY count DESC, tag ASC`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error getting exercise tags:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async bulkImport(exercises: Omit<Exercise, 'id'>[]): Promise<string[]> {
|
||||
const ids: string[] = [];
|
||||
|
||||
try {
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
for (const exercise of exercises) {
|
||||
const id = await this.createExercise(exercise);
|
||||
ids.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return ids;
|
||||
} catch (error) {
|
||||
console.error('Error bulk importing exercises:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to sync with Nostr events
|
||||
async syncWithNostrEvent(eventId: string, exercise: Omit<Exercise, 'id'>): Promise<string> {
|
||||
try {
|
||||
// Check if we already have this exercise
|
||||
const existing = await this.db.getFirstAsync<{ id: string }>(
|
||||
'SELECT id FROM exercises WHERE nostr_event_id = ?',
|
||||
[eventId]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// Update existing exercise
|
||||
await this.updateExercise(existing.id, exercise);
|
||||
return existing.id;
|
||||
} else {
|
||||
// Create new exercise with Nostr reference
|
||||
const id = generateId();
|
||||
const timestamp = Date.now();
|
||||
|
||||
await this.db.withTransactionAsync(async () => {
|
||||
await this.db.runAsync(
|
||||
`INSERT INTO exercises (
|
||||
id, nostr_event_id, title, type, category, equipment, description,
|
||||
format_json, format_units_json, created_at, updated_at, source
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
id,
|
||||
eventId,
|
||||
exercise.title,
|
||||
exercise.type,
|
||||
exercise.category,
|
||||
exercise.equipment || null,
|
||||
exercise.description || null,
|
||||
exercise.format ? JSON.stringify(exercise.format) : null,
|
||||
exercise.format_units ? JSON.stringify(exercise.format_units) : null,
|
||||
timestamp,
|
||||
timestamp,
|
||||
'nostr'
|
||||
]
|
||||
);
|
||||
|
||||
if (exercise.tags?.length) {
|
||||
for (const tag of exercise.tags) {
|
||||
await this.db.runAsync(
|
||||
'INSERT INTO exercise_tags (exercise_id, tag) VALUES (?, ?)',
|
||||
[id, tag]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error syncing exercise with Nostr event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Helper function to create an instance
|
||||
export const createExerciseService = (db: SQLiteDatabase) => new ExerciseService(db);
|
||||
|
||||
// Also export a type for the service if needed
|
||||
export type ExerciseServiceType = ExerciseService;
|
36
lib/db/types.ts
Normal file
36
lib/db/types.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
|
||||
export interface SQLTransaction {
|
||||
executeSql: (
|
||||
sqlStatement: string,
|
||||
args?: any[],
|
||||
callback?: (transaction: SQLTransaction, resultSet: SQLResultSet) => void,
|
||||
errorCallback?: (transaction: SQLTransaction, error: SQLError) => boolean
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface SQLResultSet {
|
||||
insertId?: number;
|
||||
rowsAffected: number;
|
||||
rows: {
|
||||
length: number;
|
||||
item: (index: number) => any;
|
||||
_array: any[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SQLError {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Extend the SQLiteDatabase type to include transaction methods
|
||||
declare module 'expo-sqlite' {
|
||||
interface SQLiteDatabase {
|
||||
transaction(
|
||||
callback: (transaction: SQLTransaction) => void,
|
||||
errorCallback?: (error: SQLError) => void,
|
||||
successCallback?: () => void
|
||||
): void;
|
||||
}
|
||||
}
|
@ -15,6 +15,15 @@ export type Equipment =
|
||||
| 'cable'
|
||||
| 'other';
|
||||
|
||||
export interface Exercise extends BaseExercise {
|
||||
source: 'local' | 'powr' | 'nostr';
|
||||
usageCount?: number;
|
||||
lastUsed?: Date;
|
||||
format_json?: string; // For database storage
|
||||
format_units_json?: string; // For database storage
|
||||
nostr_event_id?: string; // For Nostr integration
|
||||
}
|
||||
|
||||
// Base library content interface
|
||||
export interface LibraryContent extends SyncableContent {
|
||||
title: string;
|
||||
@ -26,7 +35,7 @@ export interface LibraryContent extends SyncableContent {
|
||||
};
|
||||
category?: ExerciseCategory;
|
||||
equipment?: Equipment;
|
||||
source: 'local' | 'pow' | 'nostr';
|
||||
source: 'local' | 'powr' | 'nostr';
|
||||
tags: string[];
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Available storage sources for content
|
||||
*/
|
||||
export type StorageSource = 'local' | 'backup' | 'nostr';
|
||||
export type StorageSource = 'local' | 'powr' | 'nostr';
|
||||
|
||||
/**
|
||||
* Nostr sync metadata
|
||||
@ -51,4 +51,4 @@ export interface ContentMetadata {
|
||||
export interface SyncableContent extends ContentMetadata {
|
||||
id: string;
|
||||
availability: ContentAvailability;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user