// app/(tabs)/library/programs.tsx import React, { useState, useEffect } from 'react'; import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity, Modal } 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 { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, Code, Search, ListFilter, Wifi, Zap, FileJson, X, Info } 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'; // 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.EXERCISE); const [eventContent, setEventContent] = useState(''); const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); const [privateKey, setPrivateKey] = useState(''); const [error, setError] = useState(null); // Use the NDK hooks const { ndk, isLoading: ndkLoading } = useNDK(); const { currentUser, isAuthenticated } = useNDKCurrentUser(); const { login, logout, generateKeys } = useNDKAuth(); // Tab state const [activeTab, setActiveTab] = useState('database'); 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 { 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(); inspectDatabase(); } 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", 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); }; // Close login sheet const handleCloseLogin = () => { setIsLoginSheetOpen(false); }; // Handle key generation const handleGenerateKeys = async () => { try { const { nsec } = generateKeys(); setPrivateKey(nsec); setError(null); } catch (err) { setError('Failed to generate keys'); console.error('Key generation error:', err); } }; // Handle login const handleLogin = async () => { if (!privateKey.trim()) { setError('Please enter your private key or generate a new one'); return; } setError(null); try { const success = await login(privateKey); if (success) { setPrivateKey(''); handleCloseLogin(); } else { setError('Failed to login with the provided key'); } } catch (err) { console.error('Login error:', err); setError(err instanceof Error ? err.message : 'An unexpected error occurred'); } }; // 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) { // Your existing exercise event code const uniqueId = `exercise-${timestamp}`; tags.push( ['d', uniqueId], ['title', eventContent || 'Test Exercise'], // Rest of your tags... ); } 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 event = await useNDKStore.getState().publishEvent(eventKind, eventContent, 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 }; // Use the NDK store's fetchEventsByFilter function 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, uncomment them in stores/ndk.ts )} {/* Login Modal */} Login with Nostr Enter your Nostr private key (nsec) {error && ( {error} )} What is a Nostr Key? Nostr is a decentralized protocol where your private key (nsec) is your identity and password. Your private key is securely stored on your device and is never sent to any servers. {/* 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 New Keys" to create a new Nostr identity 3. Login with the generated keys 4. Select an event kind (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 for Nostr integration provides a more reliable experience than direct WebSocket connections. )} ); }