// app/(tabs)/library/programs.tsx import React, { useState, useEffect } from 'react'; import { View, ScrollView, TextInput, ActivityIndicator, Platform, Alert, TouchableOpacity } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import NostrLoginSheet from '@/components/sheets/NostrLoginSheet'; import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code, Search, ListFilter, Wifi, Zap, FileJson } from 'lucide-react-native'; import { useSQLiteContext } from 'expo-sqlite'; import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK'; import { schema } from '@/lib/db/schema'; import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet'; import { Separator } from '@/components/ui/separator'; import { NostrEventKind } from '@/types/nostr'; import { useNDKStore } from '@/lib/stores/ndk'; import * as SecureStore from 'expo-secure-store'; // Define relay status enum RelayStatus { DISCONNECTED = 'disconnected', CONNECTING = 'connecting', CONNECTED = 'connected', ERROR = 'error' } // Interface for event display interface DisplayEvent { id: string; pubkey: string; kind: number; created_at: number; content: string; tags: string[][]; sig?: string; } // Default available filters for programs const availableFilters = { equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'], tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'], source: ['local', 'powr', 'nostr'] as SourceType[] }; // Initial filter state const initialFilters: FilterOptions = { equipment: [], tags: [], source: [] }; export default function ProgramsScreen() { const db = useSQLiteContext(); // Database state const [dbStatus, setDbStatus] = useState<{ initialized: boolean; tables: string[]; error?: string; }>({ initialized: false, tables: [], }); const [schemas, setSchemas] = useState<{name: string, sql: string}[]>([]); const [testResults, setTestResults] = useState<{ success: boolean; message: string; } | null>(null); const [searchQuery, setSearchQuery] = useState(''); const [filterSheetOpen, setFilterSheetOpen] = useState(false); const [currentFilters, setCurrentFilters] = useState(initialFilters); const [activeFilters, setActiveFilters] = useState(0); // Nostr state const [relayStatus, setRelayStatus] = useState(RelayStatus.DISCONNECTED); const [statusMessage, setStatusMessage] = useState(''); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [eventKind, setEventKind] = useState(NostrEventKind.TEXT); const [eventContent, setEventContent] = useState(''); const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); // Use the NDK hooks const { ndk, isLoading: ndkLoading } = useNDK(); const { currentUser, isAuthenticated } = useNDKCurrentUser(); const { login, logout, generateKeys } = useNDKAuth(); // Tab state const [activeTab, setActiveTab] = useState('nostr'); // Default to nostr tab for testing useEffect(() => { // Check database status checkDatabase(); inspectDatabase(); // Update relay status when NDK changes if (ndk) { setRelayStatus(RelayStatus.CONNECTED); setStatusMessage(isAuthenticated ? `Connected as ${currentUser?.npub?.slice(0, 8)}...` : 'Connected to relays via NDK'); } else if (ndkLoading) { setRelayStatus(RelayStatus.CONNECTING); setStatusMessage('Connecting to relays...'); } else { setRelayStatus(RelayStatus.DISCONNECTED); setStatusMessage('Not connected'); } }, [ndk, ndkLoading, isAuthenticated, currentUser]); // DATABASE FUNCTIONS const inspectDatabase = async () => { try { const result = await db.getAllAsync<{name: string, sql: string}>( "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 const version = await db.getFirstAsync<{version: number}>( 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1' ); // Get all tables const tables = await db.getAllAsync<{name: string}>( "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 { setTestResults(null); // Clear stored keys first try { await SecureStore.deleteItemAsync('nostr_privkey'); console.log('[Database Reset] Cleared stored Nostr keys'); } catch (keyError) { console.warn('[Database Reset] Error clearing keys:', keyError); } // Use the new complete reset method await schema.resetDatabaseCompletely(db); // Show success message setTestResults({ success: true, message: 'Database completely reset. The app will need to be restarted to see the changes.' }); // Recommend a restart Alert.alert( 'Database Reset Complete', 'The database has been completely reset. Please restart the app for the changes to take effect fully.', [{ text: 'OK', style: 'default' }] ); } catch (error) { console.error('[Database Reset] Error resetting database:', error); setTestResults({ success: false, message: error instanceof Error ? error.message : 'Unknown error during database reset' }); } }; const runTestInsert = async () => { try { // Test exercise const testExercise = { title: "Test Squat", type: "strength", category: "Legs", equipment: "barbell", description: "Test exercise", tags: ["test", "legs"], format: { weight: true, reps: true }, format_units: { weight: "kg", reps: "count" } }; 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( "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' }); } }; const handleApplyFilters = (filters: FilterOptions) => { setCurrentFilters(filters); const totalFilters = Object.values(filters).reduce( (acc, curr) => acc + curr.length, 0 ); setActiveFilters(totalFilters); // Implement filtering logic for programs when available }; // NOSTR FUNCTIONS // Handle login dialog const handleShowLogin = () => { setIsLoginSheetOpen(true); }; const handleCloseLogin = () => { setIsLoginSheetOpen(false); }; // Handle logout const handleLogout = async () => { try { setLoading(true); await logout(); setStatusMessage('Logged out'); setEvents([]); } catch (error) { console.error('Logout error:', error); setStatusMessage(`Logout error: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; // Publish an event const handlePublishEvent = async () => { if (!isAuthenticated || !ndk || !currentUser) { setStatusMessage('You must login first'); return; } setLoading(true); try { console.log('Creating event...'); // Prepare tags based on event kind const tags: string[][] = []; const timestamp = Math.floor(Date.now() / 1000); // Add appropriate tags based on event kind if (eventKind === NostrEventKind.TEXT) { // For regular text notes, we can add some simple tags tags.push( ['t', 'powr'], // Adding a hashtag ['t', 'test'], // Another hashtag ['client', 'POWR App'] // Client info ); // If no content was provided, use a default message if (!eventContent || eventContent.trim() === '') { setEventContent('Hello from POWR App - Test Note'); } } else if (eventKind === NostrEventKind.EXERCISE) { const uniqueId = `exercise-${timestamp}`; tags.push( ['d', uniqueId], ['title', eventContent || 'Test Exercise'], ['type', 'strength'], ['category', 'Legs'], ['format', 'weight', 'reps'], ['format_units', 'kg', 'count'], ['equipment', 'barbell'], ['t', 'test'], ['t', 'powr'] ); } else if (eventKind === NostrEventKind.TEMPLATE) { const uniqueId = `template-${timestamp}`; tags.push( ['d', uniqueId], ['title', eventContent || 'Test Workout Template'], ['type', 'strength'], ['t', 'strength'], ['t', 'legs'], ['t', 'powr'], // Add exercise references - these would normally reference real exercise events ['exercise', `33401:exercise-${timestamp-1}`, '3', '10', 'normal'] ); } else if (eventKind === NostrEventKind.WORKOUT) { const uniqueId = `workout-${timestamp}`; const startTime = timestamp - 3600; // 1 hour ago tags.push( ['d', uniqueId], ['title', eventContent || 'Test Workout Record'], ['start', `${startTime}`], ['end', `${timestamp}`], ['completed', 'true'], ['t', 'powr'], // Add exercise data - these would normally reference real exercise events ['exercise', `33401:exercise-${timestamp-1}`, '100', '10', '8', 'normal'] ); } // Use the NDK store's publishEvent function const content = eventContent || `Test ${eventKind === NostrEventKind.TEXT ? 'note' : 'event'} from POWR App`; const event = await useNDKStore.getState().publishEvent(eventKind, content, tags); if (event) { // Add the published event to our display list const displayEvent: DisplayEvent = { id: event.id || '', pubkey: event.pubkey, kind: event.kind || eventKind, // Add fallback to eventKind if kind is undefined created_at: event.created_at || Math.floor(Date.now() / 1000), // Add fallback timestamp content: event.content, tags: event.tags.map(tag => tag.map(item => String(item))), sig: event.sig }; setEvents(prev => [displayEvent, ...prev]); // Clear content field setEventContent(''); setStatusMessage('Event published successfully!'); } else { setStatusMessage('Failed to publish event'); } } catch (error) { console.error('Error publishing event:', error); setStatusMessage(`Error publishing: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; // Query events from NDK const queryEvents = async () => { if (!ndk) { setStatusMessage('NDK not initialized'); return; } setLoading(true); setEvents([]); try { // Create a filter for the specific kind const filter = { kinds: [eventKind as number], limit: 20 }; // Get events using NDK const fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter); const displayEvents: DisplayEvent[] = []; fetchedEvents.forEach(event => { // Ensure we handle potentially undefined values displayEvents.push({ id: event.id || '', pubkey: event.pubkey, kind: event.kind || eventKind, // Use eventKind as fallback created_at: event.created_at || Math.floor(Date.now() / 1000), // Use current time as fallback content: event.content, // Convert tags to string[][] tags: event.tags.map(tag => tag.map(item => String(item))), sig: event.sig }); }); setEvents(displayEvents); setStatusMessage(`Fetched ${displayEvents.length} events`); } catch (error) { console.error('Error querying events:', error); setStatusMessage(`Error querying: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setLoading(false); } }; return ( {/* Search bar with filter button */} {/* Filter Sheet */} setFilterSheetOpen(false)} options={currentFilters} onApplyFilters={handleApplyFilters} availableFilters={availableFilters} /> {/* Custom Tab Bar */} setActiveTab('database')} > Database setActiveTab('nostr')} > Nostr {/* Tab Content */} {activeTab === 'database' && ( Programs Coming Soon Training programs will allow you to organize your workouts into structured training plans. Database Debug Panel {/* Schema Inspector Card */} Database Schema ({Platform.OS}) {schemas.map((table) => ( {table.name} {table.sql} ))} {/* Status Card */} Database Status Initialized: {dbStatus.initialized ? '✅' : '❌'} Tables Found: {dbStatus.tables.length} {dbStatus.tables.map(table => ( • {table} ))} {dbStatus.error && ( Error {dbStatus.error} )} {/* Operations Card */} Database Operations {testResults && ( {testResults.success ? ( ) : ( )} {testResults.success ? "Success" : "Error"} {testResults.message} )} )} {activeTab === 'nostr' && ( Nostr Integration Test {/* Connection status and controls */} Nostr Connection Status: {relayStatus} {statusMessage ? ( {statusMessage} ) : null} {isAuthenticated && currentUser && ( Logged in as: {currentUser.npub} {currentUser.profile?.displayName && ( {currentUser.profile.displayName} )} {/* Display active relay */} Active Relay: wss://powr.duckdns.org Note: To publish to additional relays, update them in stores/ndk.ts )} {/* NostrLoginSheet component */} {/* Create Event */} Create Event Event Kind: Content: {/* Event List */} Recent Events {loading && events.length === 0 ? ( Loading events... ) : events.length === 0 ? ( No events yet ) : ( {events.map((event, index) => ( Kind: {event.kind} | Created: {new Date(event.created_at * 1000).toLocaleString()} ID: {event.id} Pubkey: {event.pubkey.slice(0, 8)}... {/* Display tags */} {event.tags && event.tags.length > 0 && ( Tags: {event.tags.map((tag, tagIndex) => ( {tag.join(', ')} ))} )} Content: {event.content} {/* Display signature */} {event.sig && ( Signature: {event.sig.slice(0, 16)}... )} {index < events.length - 1 && } ))} )} {/* Event JSON Viewer */} Event Details {events.length > 0 ? ( Selected Event (Latest): {JSON.stringify(events[0], null, 2)} ) : ( No events to display )} {/* Testing Guide */} Testing Guide How to test Nostr integration: 1. Click "Login with Nostr" to authenticate 2. On the login sheet, click "Generate Key" to create a new Nostr identity 3. Login with the generated keys 4. Select an event kind (Text Note, Exercise, Template, or Workout) 5. Enter optional content and click "Publish" 6. Use "Query Events" to fetch existing events of the selected kind Using NDK Mobile for Nostr integration provides a more reliable experience with proper cryptographic operations. )} ); }