POWR/app/(tabs)/library/programs.tsx

995 lines
38 KiB
TypeScript
Raw Normal View History

2025-02-16 23:53:28 -05:00
// app/(tabs)/library/programs.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity, Modal } from 'react-native';
2025-02-09 20:38:38 -05:00
import { Text } from '@/components/ui/text';
2025-02-16 23:53:28 -05:00
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
2025-02-16 23:53:28 -05:00
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';
2025-02-16 23:53:28 -05:00
import { useSQLiteContext } from 'expo-sqlite';
import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK';
2025-02-16 23:53:28 -05:00
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;
}
2025-02-16 23:53:28 -05:00
// 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: []
};
2025-02-09 20:38:38 -05:00
export default function ProgramsScreen() {
2025-02-16 23:53:28 -05:00
const db = useSQLiteContext();
// Database state
2025-02-16 23:53:28 -05:00
const [dbStatus, setDbStatus] = useState<{
initialized: boolean;
tables: string[];
error?: string;
}>({
initialized: false,
tables: [],
});
const [schemas, setSchemas] = useState<{name: string, sql: string}[]>([]);
2025-02-16 23:53:28 -05:00
const [testResults, setTestResults] = useState<{
success: boolean;
message: string;
} | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
const [currentFilters, setCurrentFilters] = useState<FilterOptions>(initialFilters);
const [activeFilters, setActiveFilters] = useState(0);
// Nostr state
const [relayStatus, setRelayStatus] = useState<RelayStatus>(RelayStatus.DISCONNECTED);
const [statusMessage, setStatusMessage] = useState('');
const [events, setEvents] = useState<DisplayEvent[]>([]);
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<string | null>(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');
2025-02-16 23:53:28 -05:00
useEffect(() => {
// Check database status
2025-02-16 23:53:28 -05:00
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]);
2025-02-16 23:53:28 -05:00
// 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);
}
};
2025-02-16 23:53:28 -05:00
const checkDatabase = async () => {
try {
// Check schema_version table
const version = await db.getFirstAsync<{version: number}>(
2025-02-16 23:53:28 -05:00
'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1'
);
// Get all tables
const tables = await db.getAllAsync<{name: string}>(
2025-02-16 23:53:28 -05:00
"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',
}));
}
};
2025-02-16 23:53:28 -05:00
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();
2025-02-16 23:53:28 -05:00
} 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",
2025-02-16 23:53:28 -05:00
description: "Test exercise",
tags: ["test", "legs"],
format: {
weight: true,
reps: true
},
format_units: {
weight: "kg",
reps: "count"
2025-02-16 23:53:28 -05:00
}
};
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(
2025-02-16 23:53:28 -05:00
"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);
}
};
2025-02-09 20:38:38 -05:00
return (
<View className="flex-1 bg-background">
{/* Search bar with filter button */}
<View className="px-4 py-2 border-b border-border">
<View className="flex-row items-center">
<View className="relative flex-1">
<View className="absolute left-3 z-10 h-full justify-center">
<Search size={18} className="text-muted-foreground" />
</View>
<Input
value={searchQuery}
onChangeText={setSearchQuery}
placeholder="Search programs"
className="pl-9 pr-10 bg-muted/50 border-0"
/>
<View className="absolute right-2 z-10 h-full justify-center">
<Button
variant="ghost"
size="icon"
onPress={() => setFilterSheetOpen(true)}
>
<View className="relative">
<ListFilter className="text-muted-foreground" size={20} />
{activeFilters > 0 && (
<View className="absolute -top-1 -right-1 w-2.5 h-2.5 rounded-full" style={{ backgroundColor: '#f7931a' }} />
)}
</View>
</Button>
</View>
</View>
</View>
</View>
{/* Filter Sheet */}
<FilterSheet
isOpen={filterSheetOpen}
onClose={() => setFilterSheetOpen(false)}
options={currentFilters}
onApplyFilters={handleApplyFilters}
availableFilters={availableFilters}
/>
{/* Custom Tab Bar */}
<View className="flex-row mx-4 mt-4 bg-card rounded-lg overflow-hidden">
<TouchableOpacity
className={`flex-1 flex-row items-center justify-center py-3 ${activeTab === 'database' ? 'bg-primary' : ''}`}
onPress={() => setActiveTab('database')}
>
<Database size={18} className={`mr-2 ${activeTab === 'database' ? 'text-white' : 'text-foreground'}`} />
<Text className={activeTab === 'database' ? 'text-white' : 'text-foreground'}>Database</Text>
</TouchableOpacity>
<TouchableOpacity
className={`flex-1 flex-row items-center justify-center py-3 ${activeTab === 'nostr' ? 'bg-primary' : ''}`}
onPress={() => setActiveTab('nostr')}
>
<Zap size={18} className={`mr-2 ${activeTab === 'nostr' ? 'text-white' : 'text-foreground'}`} />
<Text className={activeTab === 'nostr' ? 'text-white' : 'text-foreground'}>Nostr</Text>
</TouchableOpacity>
</View>
{/* Tab Content */}
{activeTab === 'database' && (
<ScrollView className="flex-1 p-4">
<View className="py-4 space-y-4">
<Text className="text-lg font-semibold text-center mb-4">Programs Coming Soon</Text>
<Text className="text-center text-muted-foreground mb-6">
Training programs will allow you to organize your workouts into structured training plans.
</Text>
<Text className="text-lg font-semibold text-center mb-4">Database Debug Panel</Text>
{/* Schema Inspector Card */}
<Card>
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Code size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Database Schema ({Platform.OS})</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="space-y-4">
{schemas.map((table) => (
<View key={table.name} className="space-y-2">
<Text className="font-semibold">{table.name}</Text>
<Text className="text-muted-foreground text-sm">
{table.sql}
</Text>
</View>
))}
2025-02-16 23:53:28 -05:00
</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">
<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>
{/* Operations 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>
)}
{activeTab === 'nostr' && (
<ScrollView className="flex-1 p-4">
<View className="py-4 space-y-4">
<Text className="text-lg font-semibold text-center mb-4">Nostr Integration Test</Text>
{/* Connection status and controls */}
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Wifi size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Nostr Connection</Text>
</CardTitle>
</CardHeader>
<CardContent>
<View className="flex-row gap-4 mb-4">
<Button
className="flex-1"
onPress={handleShowLogin}
disabled={isAuthenticated || loading}
>
<Text className="text-primary-foreground">Login with Nostr</Text>
</Button>
<Button
variant="destructive"
className="flex-1"
onPress={handleLogout}
disabled={!isAuthenticated || loading}
>
<Text className="text-destructive-foreground">Logout</Text>
</Button>
</View>
<Text className={`mt-2 ${
relayStatus === RelayStatus.CONNECTED
? 'text-green-500'
: relayStatus === RelayStatus.ERROR
? 'text-red-500'
: 'text-yellow-500'
}`}>
Status: {relayStatus}
</Text>
{statusMessage ? (
<Text className="mt-2 text-muted-foreground">{statusMessage}</Text>
) : null}
{isAuthenticated && currentUser && (
<View className="mt-4 p-4 rounded-lg bg-muted">
<Text className="font-medium">Logged in as:</Text>
<Text className="text-sm text-muted-foreground mt-1" numberOfLines={1}>{currentUser.npub}</Text>
{currentUser.profile?.displayName && (
<Text className="text-sm mt-1">{currentUser.profile.displayName}</Text>
)}
{/* Display active relay */}
<Text className="font-medium mt-3">Active Relay:</Text>
<Text className="text-sm text-muted-foreground">wss://powr.duckdns.org</Text>
<Text className="text-xs text-muted-foreground mt-1">
Note: To publish to additional relays, uncomment them in stores/ndk.ts
</Text>
</View>
)}
</CardContent>
</Card>
{/* Login Modal */}
<Modal
visible={isLoginSheetOpen}
transparent={true}
animationType="slide"
onRequestClose={handleCloseLogin}
>
<View className="flex-1 justify-center items-center bg-black/50">
<View className="bg-background rounded-lg w-[90%] max-w-md p-4">
<View className="flex-row justify-between items-center mb-4">
<Text className="text-lg font-bold">Login with Nostr</Text>
<TouchableOpacity onPress={handleCloseLogin}>
<X size={24} />
</TouchableOpacity>
</View>
<View className="space-y-4">
<Text>Enter your Nostr private key (nsec)</Text>
<Input
placeholder="nsec1..."
value={privateKey}
onChangeText={setPrivateKey}
secureTextEntry
autoCapitalize="none"
/>
{error && (
<View className="p-3 bg-destructive/10 rounded-md border border-destructive">
<Text className="text-destructive">{error}</Text>
</View>
)}
<View className="flex-row space-x-2">
<Button
variant="outline"
onPress={handleGenerateKeys}
disabled={loading}
className="flex-1"
>
<Text>Generate New Keys</Text>
</Button>
<Button
onPress={handleLogin}
disabled={loading}
className="flex-1"
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text className="text-primary-foreground">Login</Text>
)}
</Button>
</View>
<View className="bg-secondary/30 p-3 rounded-md mt-4">
<View className="flex-row items-center mb-2">
<Info size={16} className="mr-2 text-muted-foreground" />
<Text className="font-semibold">What is a Nostr Key?</Text>
</View>
<Text className="text-sm text-muted-foreground">
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.
</Text>
</View>
</View>
</View>
</View>
</Modal>
{/* Create Event */}
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Zap size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Create Event</Text>
</CardTitle>
</CardHeader>
<CardContent>
<Text className="mb-1 font-medium">Event Kind:</Text>
<View className="flex-row gap-2 mb-4 flex-wrap">
<Button
variant={eventKind === NostrEventKind.TEXT ? "default" : "outline"}
onPress={() => setEventKind(NostrEventKind.TEXT)}
size="sm"
>
<Text className={eventKind === NostrEventKind.TEXT ? "text-white" : ""}>Text Note</Text>
</Button>
<Button
variant={eventKind === NostrEventKind.EXERCISE ? "default" : "outline"}
onPress={() => setEventKind(NostrEventKind.EXERCISE)}
size="sm"
>
<Text className={eventKind === NostrEventKind.EXERCISE ? "text-white" : ""}>Exercise</Text>
</Button>
<Button
variant={eventKind === NostrEventKind.TEMPLATE ? "default" : "outline"}
onPress={() => setEventKind(NostrEventKind.TEMPLATE)}
size="sm"
>
<Text className={eventKind === NostrEventKind.TEMPLATE ? "text-white" : ""}>Template</Text>
</Button>
<Button
variant={eventKind === NostrEventKind.WORKOUT ? "default" : "outline"}
onPress={() => setEventKind(NostrEventKind.WORKOUT)}
size="sm"
>
<Text className={eventKind === NostrEventKind.WORKOUT ? "text-white" : ""}>Workout</Text>
</Button>
</View>
<Text className="mb-1 font-medium">Content:</Text>
<TextInput
value={eventContent}
onChangeText={setEventContent}
placeholder="Event content"
multiline
numberOfLines={4}
className="border border-gray-300 dark:border-gray-700 rounded-md p-2 mb-4 min-h-24"
/>
<View className="flex-row gap-4">
<Button
onPress={handlePublishEvent}
disabled={!isAuthenticated || loading}
className="flex-1"
>
{loading ? (
<><ActivityIndicator size="small" color="#fff" /><Text className="text-white ml-2">Publishing...</Text></>
) : (
<Text className="text-white">Publish Event</Text>
)}
</Button>
<Button
onPress={queryEvents}
disabled={!isAuthenticated || loading}
variant="outline"
className="flex-1"
>
<Text>Query Events</Text>
</Button>
</View>
</CardContent>
</Card>
{/* Event List */}
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<Database size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Recent Events</Text>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{loading && events.length === 0 ? (
<View className="items-center justify-center p-4">
<ActivityIndicator size="large" />
<Text className="mt-2">Loading events...</Text>
</View>
) : events.length === 0 ? (
<View className="items-center justify-center p-4">
<Text className="text-muted-foreground">No events yet</Text>
</View>
) : (
<ScrollView className="p-4" style={{ maxHeight: 300 }}>
{events.map((event, index) => (
<View key={event.id || index} className="mb-4">
<Text className="font-bold">
Kind: {event.kind} | Created: {new Date(event.created_at * 1000).toLocaleString()}
</Text>
<Text className="mb-1">ID: {event.id}</Text>
<Text className="mb-1">Pubkey: {event.pubkey.slice(0, 8)}...</Text>
{/* Display tags */}
{event.tags && event.tags.length > 0 && (
<View className="mb-1">
<Text className="font-medium">Tags:</Text>
{event.tags.map((tag, tagIndex) => (
<Text key={tagIndex} className="ml-2">
{tag.join(', ')}
</Text>
))}
</View>
)}
<Text className="font-medium">Content:</Text>
<Text className="ml-2 mb-2">{event.content}</Text>
{/* Display signature */}
{event.sig && (
<Text className="text-xs text-muted-foreground">
Signature: {event.sig.slice(0, 16)}...
</Text>
)}
{index < events.length - 1 && <Separator className="my-2" />}
</View>
))}
</ScrollView>
)}
</CardContent>
</Card>
{/* Event JSON Viewer */}
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<FileJson size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Event Details</Text>
</CardTitle>
</CardHeader>
<CardContent>
{events.length > 0 ? (
<View>
<Text className="font-medium mb-2">Selected Event (Latest):</Text>
<ScrollView
className="border border-border p-2 rounded-md"
style={{ maxHeight: 200 }}
>
<Text className="font-mono">
{JSON.stringify(events[0], null, 2)}
</Text>
</ScrollView>
</View>
) : (
<Text className="text-muted-foreground">No events to display</Text>
)}
</CardContent>
</Card>
{/* Testing Guide */}
<Card className="mb-4">
<CardHeader>
<CardTitle className="flex-row items-center gap-2">
<AlertCircle size={20} className="text-foreground" />
<Text className="text-lg font-semibold">Testing Guide</Text>
</CardTitle>
</CardHeader>
<CardContent>
<Text className="font-medium mb-2">How to test Nostr integration:</Text>
<View className="space-y-2">
<Text>1. Click "Login with Nostr" to authenticate</Text>
<Text>2. On the login sheet, click "Generate New Keys" to create a new Nostr identity</Text>
<Text>3. Login with the generated keys</Text>
<Text>4. Select an event kind (Exercise, Template, or Workout)</Text>
<Text>5. Enter optional content and click "Publish"</Text>
<Text>6. Use "Query Events" to fetch existing events of the selected kind</Text>
<Text className="mt-2 text-muted-foreground">Using NDK for Nostr integration provides a more reliable experience than direct WebSocket connections.</Text>
</View>
</CardContent>
</Card>
</View>
</ScrollView>
)}
</View>
2025-02-09 20:38:38 -05:00
);
}