mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
413 lines
14 KiB
TypeScript
413 lines
14 KiB
TypeScript
// app/(packs)/import.tsx
|
|
import React, { useState, useEffect } from 'react';
|
|
import { View, ScrollView, StyleSheet, ActivityIndicator, Platform } from 'react-native';
|
|
import { router, Stack } from 'expo-router';
|
|
import { useNDK } from '@/lib/hooks/useNDK';
|
|
import { Text } from '@/components/ui/text';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { nip19 } from 'nostr-tools'; // Fix import from nostr-tools
|
|
import { findTagValue } from '@/utils/nostr-utils';
|
|
import POWRPackService from '@/lib/db/services/POWRPackService';
|
|
import { usePOWRPackService } from '@/components/DatabaseProvider'; // Use the proper hook
|
|
import { POWRPackImport, POWRPackSelection } from '@/types/powr-pack';
|
|
import { InfoIcon } from 'lucide-react-native';
|
|
|
|
export default function ImportPOWRPackScreen() {
|
|
const { ndk } = useNDK();
|
|
const powrPackService = usePOWRPackService();
|
|
const [naddrInput, setNaddrInput] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [packData, setPackData] = useState<POWRPackImport | null>(null);
|
|
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([]);
|
|
const [selectedExercises, setSelectedExercises] = useState<string[]>([]);
|
|
const [dependencies, setDependencies] = useState<Record<string, string[]>>({});
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
const [importSuccess, setImportSuccess] = useState(false);
|
|
|
|
// Handle fetch button click
|
|
const handleFetchPack = async () => {
|
|
if (!naddrInput.trim()) {
|
|
setError('Please enter a valid naddr');
|
|
return;
|
|
}
|
|
|
|
if (!ndk) {
|
|
setError('NDK is not initialized');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setPackData(null);
|
|
setSelectedTemplates([]);
|
|
setSelectedExercises([]);
|
|
setDependencies({});
|
|
|
|
try {
|
|
// Validate naddr format
|
|
const isValid = naddrInput.startsWith('naddr1');
|
|
if (!isValid) {
|
|
throw new Error('Invalid naddr format. Should start with "naddr1"');
|
|
}
|
|
|
|
// Fetch pack data
|
|
const packImport = await powrPackService.fetchPackFromNaddr(naddrInput, ndk);
|
|
|
|
// Debug logging
|
|
console.log("Fetched pack event:", packImport.packEvent.id);
|
|
console.log("Templates count:", packImport.templates.length);
|
|
console.log("Exercises count:", packImport.exercises.length);
|
|
|
|
setPackData(packImport);
|
|
|
|
// Analyze dependencies
|
|
const deps = powrPackService.analyzeDependencies(packImport.templates, packImport.exercises);
|
|
setDependencies(deps);
|
|
|
|
// Pre-select all items
|
|
setSelectedTemplates(packImport.templates.map(t => t.id));
|
|
setSelectedExercises(packImport.exercises.map(e => e.id));
|
|
} catch (err) {
|
|
console.error('Error fetching pack:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to fetch pack');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// Handle template selection change
|
|
const handleTemplateChange = (templateId: string, isSelected: boolean) => {
|
|
setSelectedTemplates(prev => {
|
|
const updated = isSelected
|
|
? [...prev, templateId]
|
|
: prev.filter(id => id !== templateId);
|
|
|
|
// Update required exercises
|
|
updateRequiredExercises(updated);
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
// Handle exercise selection change
|
|
const handleExerciseChange = (exerciseId: string, isSelected: boolean) => {
|
|
// Don't allow deselecting if it's required by a selected template
|
|
if (!isSelected && isRequiredByTemplate(exerciseId)) {
|
|
return;
|
|
}
|
|
|
|
setSelectedExercises(prev =>
|
|
isSelected
|
|
? [...prev, exerciseId]
|
|
: prev.filter(id => id !== exerciseId)
|
|
);
|
|
};
|
|
|
|
// Check if an exercise is required by any selected template
|
|
const isRequiredByTemplate = (exerciseId: string): boolean => {
|
|
return selectedTemplates.some(templateId =>
|
|
dependencies[templateId]?.includes(exerciseId)
|
|
);
|
|
};
|
|
|
|
// Update exercise selection based on template dependencies
|
|
const updateRequiredExercises = (selectedTemplateIds: string[]) => {
|
|
// Start with currently manually selected exercises
|
|
const manuallySelected = selectedExercises.filter(id =>
|
|
!Object.values(dependencies).flat().includes(id)
|
|
);
|
|
|
|
// Add all exercises required by selected templates
|
|
const requiredExercises = selectedTemplateIds.flatMap(templateId =>
|
|
dependencies[templateId] || []
|
|
);
|
|
|
|
// Combine manual selections with required ones, removing duplicates
|
|
const allExercises = [...new Set([...manuallySelected, ...requiredExercises])];
|
|
setSelectedExercises(allExercises);
|
|
};
|
|
|
|
// Handle import button click
|
|
const handleImport = async () => {
|
|
if (!packData) return;
|
|
|
|
setIsImporting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const packId = generatePackId();
|
|
const selection: POWRPackSelection = {
|
|
packId,
|
|
selectedTemplates,
|
|
selectedExercises,
|
|
templateDependencies: dependencies
|
|
};
|
|
|
|
await powrPackService.importPack(packData, selection);
|
|
setImportSuccess(true);
|
|
|
|
// Navigate back after a short delay
|
|
setTimeout(() => {
|
|
router.back();
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Error importing pack:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to import pack');
|
|
} finally {
|
|
setIsImporting(false);
|
|
}
|
|
};
|
|
|
|
// Generate a unique pack ID
|
|
const generatePackId = (): string => {
|
|
return 'pack_' + Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
};
|
|
|
|
// Get pack title from event
|
|
const getPackTitle = (): string => {
|
|
if (!packData?.packEvent) return 'Unknown Pack';
|
|
return findTagValue(packData.packEvent.tags, 'title') || 'Unnamed Pack';
|
|
};
|
|
|
|
// Get pack description from event
|
|
const getPackDescription = (): string => {
|
|
if (!packData?.packEvent) return '';
|
|
return findTagValue(packData.packEvent.tags, 'description') || packData.packEvent.content || '';
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Stack.Screen
|
|
options={{
|
|
title: 'Import POWR Pack',
|
|
headerShown: true,
|
|
}}
|
|
/>
|
|
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
{/* Input section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
<Text className="text-xl font-semibold">Enter POWR Pack Address</Text>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
<Text className="text-muted-foreground">Paste a POWR Pack naddr to import</Text>
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{/* Helper text explaining naddr format */}
|
|
<View className="mb-4">
|
|
<Text className="text-sm text-muted-foreground">
|
|
Paste a POWR Pack address (naddr1...) to import templates and exercises shared by the community.
|
|
</Text>
|
|
</View>
|
|
<Input
|
|
placeholder="naddr1..."
|
|
value={naddrInput}
|
|
onChangeText={setNaddrInput}
|
|
style={styles.input}
|
|
/>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button
|
|
onPress={handleFetchPack}
|
|
disabled={isLoading || !naddrInput.trim()}
|
|
className="w-full"
|
|
>
|
|
{isLoading ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Text className="text-primary-foreground">Fetch Pack</Text>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
{/* Error message */}
|
|
{error && (
|
|
<View className="mb-4 mt-4 p-4 bg-destructive/10 border border-destructive rounded-md flex-row items-center">
|
|
<Text className="text-destructive ml-2">{error}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Success message */}
|
|
{importSuccess && (
|
|
<View className="mb-4 mt-4 p-4 bg-green-50 border border-green-200 rounded-md flex-row items-center">
|
|
<Text className="ml-2 text-green-800">Pack successfully imported!</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Pack content */}
|
|
{packData && (
|
|
<View style={styles.packContent}>
|
|
<Card className="mb-4">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
<Text className="text-xl font-semibold">{getPackTitle()}</Text>
|
|
</CardTitle>
|
|
{getPackDescription() ? (
|
|
<CardDescription>
|
|
<Text className="text-muted-foreground">{getPackDescription()}</Text>
|
|
</CardDescription>
|
|
) : null}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Text className="mb-2">Select items to import:</Text>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Templates section */}
|
|
{packData.templates && packData.templates.length > 0 ? (
|
|
<Card className="mb-4">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
<Text className="text-lg font-semibold">Workout Templates</Text>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
<Text className="text-muted-foreground">{packData.templates.length} templates available</Text>
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{packData.templates.map(template => {
|
|
const title = findTagValue(template.tags, 'title') || 'Unnamed Template';
|
|
return (
|
|
<View key={template.id} style={styles.itemRow}>
|
|
<Checkbox
|
|
checked={selectedTemplates.includes(template.id)}
|
|
onCheckedChange={(checked) =>
|
|
handleTemplateChange(template.id, checked === true)
|
|
}
|
|
id={`template-${template.id}`}
|
|
/>
|
|
<Text className="ml-2 flex-1" onPress={() =>
|
|
handleTemplateChange(template.id, !selectedTemplates.includes(template.id))
|
|
}>
|
|
{title}
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="mb-4">
|
|
<CardContent>
|
|
<Text className="text-center text-muted-foreground py-4">No templates available in this pack</Text>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Exercises section */}
|
|
{packData.exercises && packData.exercises.length > 0 ? (
|
|
<Card className="mb-4">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
<Text className="text-lg font-semibold">Exercises</Text>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
<Text className="text-muted-foreground">{packData.exercises.length} exercises available</Text>
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{packData.exercises.map(exercise => {
|
|
const title = findTagValue(exercise.tags, 'title') || 'Unnamed Exercise';
|
|
const isRequired = isRequiredByTemplate(exercise.id);
|
|
|
|
return (
|
|
<View key={exercise.id} style={styles.itemRow}>
|
|
<Checkbox
|
|
checked={selectedExercises.includes(exercise.id)}
|
|
onCheckedChange={(checked) =>
|
|
handleExerciseChange(exercise.id, checked === true)
|
|
}
|
|
disabled={isRequired}
|
|
id={`exercise-${exercise.id}`}
|
|
/>
|
|
<Text
|
|
className={`ml-2 flex-1 ${isRequired ? 'font-medium' : ''}`}
|
|
onPress={() => {
|
|
if (!isRequired) {
|
|
handleExerciseChange(exercise.id, !selectedExercises.includes(exercise.id))
|
|
}
|
|
}}
|
|
>
|
|
{title}
|
|
</Text>
|
|
{isRequired && (
|
|
<View style={styles.requiredBadge}>
|
|
<InfoIcon size={14} color="#6b7280" />
|
|
<Text className="text-xs text-gray-500 ml-1">Required</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card className="mb-4">
|
|
<CardContent>
|
|
<Text className="text-center text-muted-foreground py-4">No exercises available in this pack</Text>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Import button */}
|
|
<Button
|
|
onPress={handleImport}
|
|
disabled={isImporting || (selectedTemplates.length === 0 && selectedExercises.length === 0)}
|
|
className="w-full"
|
|
>
|
|
{isImporting ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Text className="text-primary-foreground">
|
|
Import {selectedTemplates.length + selectedExercises.length} Items
|
|
</Text>
|
|
)}
|
|
</Button>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 16,
|
|
},
|
|
scrollContent: {
|
|
paddingBottom: 80,
|
|
},
|
|
input: {
|
|
marginBottom: 16,
|
|
},
|
|
packContent: {
|
|
marginTop: 16,
|
|
},
|
|
itemRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 8,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#f0f0f0',
|
|
},
|
|
requiredBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: '#f9fafb',
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 2,
|
|
borderRadius: 12,
|
|
marginLeft: 8,
|
|
}
|
|
}); |