2025-03-06 09:19:16 -05:00
|
|
|
// components/library/ExerciseSheet.tsx
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
import { View, ScrollView, KeyboardAvoidingView, Platform, TouchableWithoutFeedback,
|
|
|
|
Keyboard, Modal, TouchableOpacity } from 'react-native';
|
|
|
|
import { Text } from '@/components/ui/text';
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
import { generateId } from '@/utils/ids';
|
|
|
|
import { X } from 'lucide-react-native';
|
2025-03-12 19:23:28 -04:00
|
|
|
import { useColorScheme } from '@/lib/theme/useColorScheme';
|
2025-03-06 09:19:16 -05:00
|
|
|
import {
|
|
|
|
BaseExercise,
|
|
|
|
ExerciseType,
|
|
|
|
ExerciseCategory,
|
|
|
|
Equipment,
|
|
|
|
ExerciseFormat,
|
|
|
|
ExerciseFormatUnits,
|
|
|
|
ExerciseDisplay
|
|
|
|
} from '@/types/exercise';
|
|
|
|
import { StorageSource } from '@/types/shared';
|
2025-03-06 16:34:50 -05:00
|
|
|
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
2025-03-06 09:19:16 -05:00
|
|
|
import { useNDKStore } from '@/lib/stores/ndk';
|
2025-03-06 16:34:50 -05:00
|
|
|
import { useExerciseService, usePublicationQueue } from '@/components/DatabaseProvider';
|
2025-03-06 09:19:16 -05:00
|
|
|
|
|
|
|
interface ExerciseSheetProps {
|
|
|
|
isOpen: boolean;
|
|
|
|
onClose: () => void;
|
|
|
|
onSubmit: (exercise: BaseExercise) => void;
|
|
|
|
exerciseToEdit?: ExerciseDisplay; // Optional - if provided, we're in edit mode
|
|
|
|
mode?: 'create' | 'edit' | 'fork'; // Optional - defaults to 'create' or 'edit' based on exerciseToEdit
|
|
|
|
}
|
|
|
|
|
|
|
|
const EXERCISE_TYPES: ExerciseType[] = ['strength', 'cardio', 'bodyweight'];
|
|
|
|
const CATEGORIES: ExerciseCategory[] = ['Push', 'Pull', 'Legs', 'Core'];
|
|
|
|
const EQUIPMENT_OPTIONS: Equipment[] = [
|
|
|
|
'bodyweight',
|
|
|
|
'barbell',
|
|
|
|
'dumbbell',
|
|
|
|
'kettlebell',
|
|
|
|
'machine',
|
|
|
|
'cable',
|
|
|
|
'other'
|
|
|
|
];
|
|
|
|
|
|
|
|
// Default empty form data
|
|
|
|
const DEFAULT_FORM_DATA = {
|
|
|
|
title: '',
|
|
|
|
type: 'strength' as ExerciseType,
|
|
|
|
category: 'Push' as ExerciseCategory,
|
|
|
|
equipment: undefined as Equipment | undefined,
|
|
|
|
description: '',
|
|
|
|
tags: [] as string[],
|
|
|
|
format: {
|
|
|
|
weight: true,
|
|
|
|
reps: true,
|
|
|
|
rpe: true,
|
|
|
|
set_type: true
|
|
|
|
} as ExerciseFormat,
|
|
|
|
format_units: {
|
|
|
|
weight: 'kg',
|
|
|
|
reps: 'count',
|
|
|
|
rpe: '0-10',
|
|
|
|
set_type: 'warmup|normal|drop|failure'
|
|
|
|
} as ExerciseFormatUnits
|
|
|
|
};
|
|
|
|
|
|
|
|
export function ExerciseSheet({ isOpen, onClose, onSubmit, exerciseToEdit, mode: explicitMode }: ExerciseSheetProps) {
|
|
|
|
const { isDarkColorScheme } = useColorScheme();
|
|
|
|
const [formData, setFormData] = useState(DEFAULT_FORM_DATA);
|
|
|
|
const ndkStore = useNDKStore();
|
2025-03-06 16:34:50 -05:00
|
|
|
const publicationQueue = usePublicationQueue();
|
2025-03-06 09:19:16 -05:00
|
|
|
|
|
|
|
// Determine if we're in edit, create, or fork mode
|
|
|
|
const hasExercise = !!exerciseToEdit;
|
|
|
|
const isNostrExercise = exerciseToEdit?.source === 'nostr';
|
|
|
|
const isCurrentUserAuthor = isNostrExercise &&
|
|
|
|
exerciseToEdit?.availability?.lastSynced?.nostr?.metadata?.pubkey === ndkStore.currentUser?.pubkey;
|
|
|
|
|
|
|
|
// Use explicit mode if provided, otherwise determine based on context
|
|
|
|
const mode = explicitMode || (hasExercise ? (isNostrExercise && !isCurrentUserAuthor ? 'fork' : 'edit') : 'create');
|
|
|
|
|
|
|
|
const isEditMode = mode === 'edit';
|
|
|
|
const isForkMode = mode === 'fork';
|
|
|
|
|
|
|
|
// Load data from exerciseToEdit when in edit mode
|
|
|
|
useEffect(() => {
|
|
|
|
if (isOpen && exerciseToEdit) {
|
|
|
|
setFormData({
|
|
|
|
title: exerciseToEdit.title,
|
|
|
|
type: exerciseToEdit.type,
|
|
|
|
category: exerciseToEdit.category,
|
|
|
|
equipment: exerciseToEdit.equipment,
|
|
|
|
description: exerciseToEdit.description || '',
|
|
|
|
tags: exerciseToEdit.tags || [],
|
|
|
|
format: exerciseToEdit.format || DEFAULT_FORM_DATA.format,
|
|
|
|
format_units: exerciseToEdit.format_units || DEFAULT_FORM_DATA.format_units
|
|
|
|
});
|
|
|
|
} else if (isOpen && !exerciseToEdit) {
|
|
|
|
// Reset form when opening in create mode
|
|
|
|
setFormData(DEFAULT_FORM_DATA);
|
|
|
|
}
|
|
|
|
}, [isOpen, exerciseToEdit]);
|
|
|
|
|
|
|
|
// Reset form data when modal closes
|
|
|
|
useEffect(() => {
|
|
|
|
if (!isOpen) {
|
|
|
|
// Add a delay to ensure the closing animation completes first
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
setFormData(DEFAULT_FORM_DATA);
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
}
|
|
|
|
}, [isOpen]);
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
if (!formData.title || !formData.equipment) return;
|
|
|
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
const isNostrExercise = exerciseToEdit?.source === 'nostr';
|
|
|
|
const canEditNostr = isNostrExercise && isCurrentUserAuthor;
|
|
|
|
|
|
|
|
// Create BaseExercise
|
|
|
|
const exercise: BaseExercise = {
|
|
|
|
// Generate new ID when forking, otherwise use existing or generate new
|
|
|
|
id: isForkMode ? generateId() : (exerciseToEdit?.id || generateId()),
|
|
|
|
title: formData.title,
|
|
|
|
type: formData.type,
|
|
|
|
category: formData.category,
|
|
|
|
equipment: formData.equipment,
|
|
|
|
description: formData.description,
|
|
|
|
tags: formData.tags.length ? formData.tags : [formData.category.toLowerCase()],
|
|
|
|
format: formData.format,
|
|
|
|
format_units: formData.format_units,
|
|
|
|
// Use current timestamp for fork, otherwise preserve original or use current
|
|
|
|
created_at: isForkMode ? timestamp : (exerciseToEdit?.created_at || timestamp),
|
|
|
|
// For forked exercises, create new local availability
|
|
|
|
availability: isForkMode ? {
|
|
|
|
source: ['local' as StorageSource],
|
|
|
|
lastSynced: undefined
|
|
|
|
} : (exerciseToEdit?.availability || {
|
|
|
|
source: ['local' as StorageSource],
|
|
|
|
lastSynced: undefined
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
|
|
|
// If this is a Nostr exercise we can edit OR a new exercise while authenticated,
|
|
|
|
// we should create and possibly publish the Nostr event
|
|
|
|
if ((canEditNostr || (!exerciseToEdit && ndkStore.isAuthenticated)) && !isForkMode) {
|
|
|
|
try {
|
|
|
|
// Create tags for the exercise
|
|
|
|
const nostrTags = [
|
|
|
|
['d', exercise.id], // Use the same 'd' tag to make it replaceable
|
|
|
|
['title', exercise.title],
|
|
|
|
['type', exercise.type],
|
|
|
|
['category', exercise.category],
|
|
|
|
['equipment', exercise.equipment || ''],
|
|
|
|
...(exercise.tags.map(tag => ['t', tag])),
|
|
|
|
// Format tags - handle possible undefined with null coalescing operator
|
|
|
|
['format', ...Object.keys(exercise.format || {}).filter(k =>
|
|
|
|
exercise.format && exercise.format[k as keyof ExerciseFormat]
|
|
|
|
)]
|
|
|
|
];
|
|
|
|
|
|
|
|
// Add format units if they exist
|
|
|
|
if (exercise.format_units) {
|
|
|
|
const unitEntries = Object.entries(exercise.format_units);
|
|
|
|
if (unitEntries.length > 0) {
|
|
|
|
nostrTags.push(['format_units', ...unitEntries.flat()]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create and attempt to publish the event
|
2025-03-06 16:34:50 -05:00
|
|
|
const event = new NDKEvent(ndkStore.ndk || undefined);
|
|
|
|
event.kind = 33401; // Or whatever kind you need
|
|
|
|
event.content = exercise.description || '';
|
|
|
|
event.tags = nostrTags;
|
|
|
|
await event.sign();
|
2025-03-06 09:19:16 -05:00
|
|
|
|
|
|
|
if (event) {
|
|
|
|
// Queue for publication (this will publish immediately if online)
|
2025-03-06 16:34:50 -05:00
|
|
|
await publicationQueue.queueEvent(event);
|
2025-03-06 09:19:16 -05:00
|
|
|
|
|
|
|
// If this is a new exercise, add nostr to sources
|
|
|
|
if (!exerciseToEdit) {
|
|
|
|
exercise.availability.source.push('nostr');
|
|
|
|
|
|
|
|
// Add nostr metadata
|
|
|
|
exercise.availability.lastSynced = {
|
|
|
|
...exercise.availability.lastSynced,
|
|
|
|
nostr: {
|
|
|
|
timestamp: Date.now(),
|
|
|
|
metadata: {
|
|
|
|
id: event.id || exercise.id,
|
|
|
|
pubkey: ndkStore.currentUser?.pubkey || '',
|
|
|
|
relayUrl: 'wss://relay.damus.io', // Default relay
|
|
|
|
created_at: event.created_at || Math.floor(Date.now() / 1000)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(isEditMode ? 'Exercise updated on Nostr' : 'Exercise published to Nostr');
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error with Nostr event:', error);
|
|
|
|
// Continue with local update even if Nostr fails
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close first, then submit with a small delay
|
|
|
|
onClose();
|
|
|
|
setTimeout(() => {
|
|
|
|
onSubmit(exercise);
|
|
|
|
}, 50);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Purple color used throughout the app
|
|
|
|
const purpleColor = 'hsl(261, 90%, 66%)';
|
|
|
|
|
|
|
|
// Get title and button text based on mode
|
|
|
|
const getTitle = () => {
|
|
|
|
if (isEditMode) return "Edit Exercise";
|
|
|
|
if (isForkMode) return "Fork Exercise";
|
|
|
|
return "Create New Exercise";
|
|
|
|
};
|
|
|
|
|
|
|
|
const getButtonText = () => {
|
|
|
|
if (isEditMode) return "Update Exercise";
|
|
|
|
if (isForkMode) return "Save as My Exercise";
|
|
|
|
return "Create Exercise";
|
|
|
|
};
|
|
|
|
|
|
|
|
// Return null if not open
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Modal
|
|
|
|
visible={isOpen}
|
|
|
|
transparent={true}
|
|
|
|
animationType="slide"
|
|
|
|
onRequestClose={onClose}
|
|
|
|
>
|
|
|
|
<View className="flex-1 justify-center items-center bg-black/70">
|
|
|
|
<View
|
|
|
|
className={`bg-background ${isDarkColorScheme ? 'bg-card border border-border' : ''} rounded-lg w-[95%] h-[85%] max-w-xl shadow-xl overflow-hidden`}
|
|
|
|
style={{ maxHeight: 700 }}
|
|
|
|
>
|
|
|
|
{/* Header */}
|
|
|
|
<View className="flex-row justify-between items-center p-4 border-b border-border">
|
|
|
|
<Text className="text-xl font-bold text-foreground">{getTitle()}</Text>
|
|
|
|
<TouchableOpacity onPress={onClose} className="p-1">
|
|
|
|
<X size={24} />
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
<KeyboardAvoidingView
|
|
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
>
|
|
|
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
|
|
<View style={{ flex: 1 }}>
|
|
|
|
<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
|
|
|
|
<View className="gap-5 py-5 px-4">
|
|
|
|
{/* Source badge for edit/fork mode */}
|
|
|
|
{(isEditMode || isForkMode) && (
|
|
|
|
<View className="flex-row mb-2 items-center gap-2">
|
|
|
|
<View className={`px-2 py-1 rounded-md ${exerciseToEdit?.source === 'nostr' ? 'bg-purple-100 dark:bg-purple-900' : 'bg-blue-100 dark:bg-blue-900'}`}>
|
|
|
|
<Text className={`text-xs ${exerciseToEdit?.source === 'nostr' ? 'text-purple-800 dark:text-purple-200' : 'text-blue-800 dark:text-blue-200'}`}>
|
|
|
|
{exerciseToEdit?.source === 'nostr' ? 'Nostr' : exerciseToEdit?.source}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* Show forked badge when in fork mode */}
|
|
|
|
{isForkMode && (
|
|
|
|
<View className="px-2 py-1 rounded-md bg-amber-100 dark:bg-amber-900">
|
|
|
|
<Text className="text-xs text-amber-800 dark:text-amber-200">
|
|
|
|
Creating Local Copy
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Exercise Name</Text>
|
|
|
|
<Input
|
|
|
|
value={formData.title}
|
|
|
|
onChangeText={(text) => setFormData(prev => ({ ...prev, title: text }))}
|
|
|
|
placeholder="e.g., Barbell Back Squat"
|
|
|
|
className="text-foreground"
|
|
|
|
/>
|
|
|
|
{!formData.title && (
|
|
|
|
<Text className="text-xs text-muted-foreground mt-1 ml-1">
|
|
|
|
* Required field
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Type</Text>
|
|
|
|
<View className="flex-row flex-wrap gap-2">
|
|
|
|
{EXERCISE_TYPES.map((type) => (
|
|
|
|
<Button
|
|
|
|
key={type}
|
|
|
|
variant={formData.type === type ? 'default' : 'outline'}
|
|
|
|
onPress={() => setFormData(prev => ({ ...prev, type }))}
|
|
|
|
style={formData.type === type ? { backgroundColor: purpleColor } : {}}
|
|
|
|
>
|
|
|
|
<Text className={formData.type === type ? 'text-white' : ''}>
|
|
|
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
|
|
</Text>
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Category</Text>
|
|
|
|
<View className="flex-row flex-wrap gap-2">
|
|
|
|
{CATEGORIES.map((category) => (
|
|
|
|
<Button
|
|
|
|
key={category}
|
|
|
|
variant={formData.category === category ? 'default' : 'outline'}
|
|
|
|
onPress={() => setFormData(prev => ({ ...prev, category }))}
|
|
|
|
style={formData.category === category ? { backgroundColor: purpleColor } : {}}
|
|
|
|
>
|
|
|
|
<Text className={formData.category === category ? 'text-white' : ''}>
|
|
|
|
{category}
|
|
|
|
</Text>
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Equipment</Text>
|
|
|
|
<View className="flex-row flex-wrap gap-2">
|
|
|
|
{EQUIPMENT_OPTIONS.map((eq) => (
|
|
|
|
<Button
|
|
|
|
key={eq}
|
|
|
|
variant={formData.equipment === eq ? 'default' : 'outline'}
|
|
|
|
onPress={() => setFormData(prev => ({ ...prev, equipment: eq }))}
|
|
|
|
style={formData.equipment === eq ? { backgroundColor: purpleColor } : {}}
|
|
|
|
>
|
|
|
|
<Text className={formData.equipment === eq ? 'text-white' : ''}>
|
|
|
|
{eq.charAt(0).toUpperCase() + eq.slice(1)}
|
|
|
|
</Text>
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</View>
|
|
|
|
{!formData.equipment && (
|
|
|
|
<Text className="text-xs text-muted-foreground mt-1 ml-1">
|
|
|
|
* Required field
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Description</Text>
|
|
|
|
<Input
|
|
|
|
value={formData.description}
|
|
|
|
onChangeText={(text) => setFormData(prev => ({ ...prev, description: text }))}
|
|
|
|
placeholder="Exercise description..."
|
|
|
|
multiline
|
|
|
|
numberOfLines={4}
|
|
|
|
textAlignVertical="top"
|
|
|
|
className="min-h-24 py-2"
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View>
|
|
|
|
<Text className="text-base font-medium mb-2">Tags</Text>
|
|
|
|
<Input
|
|
|
|
value={formData.tags.join(', ')}
|
|
|
|
onChangeText={(text) => {
|
|
|
|
const tags = text.split(',')
|
|
|
|
.map(tag => tag.trim())
|
|
|
|
.filter(tag => tag.length > 0);
|
|
|
|
setFormData(prev => ({ ...prev, tags }));
|
|
|
|
}}
|
|
|
|
placeholder="strength, compound, legs..."
|
|
|
|
className="text-foreground"
|
|
|
|
/>
|
|
|
|
<Text className="text-xs text-muted-foreground mt-1 ml-1">
|
|
|
|
Separate tags with commas
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* Additional Nostr information */}
|
|
|
|
{exerciseToEdit?.source === 'nostr' && exerciseToEdit?.availability?.lastSynced?.nostr && (
|
|
|
|
<View className="mt-2 p-3 bg-muted rounded-md">
|
|
|
|
<Text className="text-sm text-muted-foreground">
|
|
|
|
Last synced with Nostr: {new Date(exerciseToEdit.availability.lastSynced.nostr.timestamp).toLocaleString()}
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
{isEditMode && !isCurrentUserAuthor && (
|
|
|
|
<Text className="text-xs text-amber-500 mt-1">
|
|
|
|
You're not the original author. Use the "Fork" option to create your own copy.
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{isEditMode && isCurrentUserAuthor && !ndkStore.isAuthenticated && (
|
|
|
|
<Text className="text-xs">
|
|
|
|
Changes will be saved locally and synced to Nostr when you're online and logged in.
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{isForkMode && (
|
|
|
|
<Text className="text-xs text-green-500 mt-1">
|
|
|
|
Creating a local copy of this exercise that you can customize
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{isNostrExercise && exerciseToEdit.availability.lastSynced.nostr.metadata.pubkey && (
|
|
|
|
<Text className="text-xs text-muted-foreground mt-1">
|
|
|
|
Author: {exerciseToEdit.availability.lastSynced.nostr.metadata.pubkey.substring(0, 8)}...
|
|
|
|
</Text>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
</ScrollView>
|
|
|
|
|
|
|
|
{/* Create/Update button at bottom */}
|
|
|
|
<View className="p-4 border-t border-border">
|
|
|
|
{/* Show fork button when editing Nostr content we don't own */}
|
|
|
|
{isEditMode && isNostrExercise && !isCurrentUserAuthor ? (
|
|
|
|
<View className="flex-row gap-2">
|
|
|
|
<Button
|
|
|
|
className="flex-1 py-5"
|
|
|
|
variant='outline'
|
|
|
|
onPress={onClose}
|
|
|
|
>
|
|
|
|
<Text>Cancel</Text>
|
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
className="flex-1 py-5"
|
|
|
|
variant='default'
|
|
|
|
onPress={() => {
|
|
|
|
// Close this modal and reopen in fork mode
|
|
|
|
onClose();
|
|
|
|
// This would be implemented in the parent component
|
|
|
|
// by reopening the modal with mode="fork"
|
|
|
|
}}
|
|
|
|
style={{ backgroundColor: purpleColor }}
|
|
|
|
>
|
|
|
|
<Text className="text-white font-semibold">
|
|
|
|
Fork Exercise
|
|
|
|
</Text>
|
|
|
|
</Button>
|
|
|
|
</View>
|
|
|
|
) : (
|
|
|
|
// Regular submit button for create/edit/fork
|
|
|
|
<Button
|
|
|
|
className="w-full py-5"
|
|
|
|
variant='default'
|
|
|
|
onPress={handleSubmit}
|
|
|
|
disabled={!formData.title || !formData.equipment}
|
|
|
|
style={(!formData.title || !formData.equipment)
|
|
|
|
? {}
|
|
|
|
: { backgroundColor: purpleColor }}
|
|
|
|
>
|
|
|
|
<Text className={(!formData.title || !formData.equipment)
|
|
|
|
? 'text-white opacity-50'
|
|
|
|
: 'text-white font-semibold'}>
|
|
|
|
{getButtonText()}
|
|
|
|
</Text>
|
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
</TouchableWithoutFeedback>
|
|
|
|
</KeyboardAvoidingView>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
}
|