feat: add expo mobile application source code
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native';
|
||||
import { Popup } from './Popup';
|
||||
import { Input } from './Input';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Button } from './Button';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { useAppTheme } from '../context/ThemeContext';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { useToast } from '../context/ToastContext';
|
||||
|
||||
interface DynamicFormPopupProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
formType: string | null;
|
||||
}
|
||||
|
||||
const LIME = '#C6F135';
|
||||
|
||||
export const DynamicFormPopup: React.FC<DynamicFormPopupProps> = ({ visible, onClose, formType }) => {
|
||||
const { colors, isDark } = useAppTheme();
|
||||
const { showToast } = useToast();
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [field1, setField1] = useState('');
|
||||
const [field2, setField2] = useState('');
|
||||
const [field3, setField3] = useState('');
|
||||
|
||||
const pickImage = async (useCamera: boolean) => {
|
||||
try {
|
||||
let result;
|
||||
if (useCamera) {
|
||||
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
showToast('Camera permission denied', 'error');
|
||||
return;
|
||||
}
|
||||
result = await ImagePicker.launchCameraAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
quality: 0.8,
|
||||
});
|
||||
} else {
|
||||
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permission.granted) {
|
||||
showToast('Gallery permission denied', 'error');
|
||||
return;
|
||||
}
|
||||
result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
quality: 0.8,
|
||||
});
|
||||
}
|
||||
|
||||
if (!result.canceled && result.assets && result.assets.length > 0) {
|
||||
setImages([...images, result.assets[0].uri]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Error selecting image', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
const newImages = [...images];
|
||||
newImages.splice(index, 1);
|
||||
setImages(newImages);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
showToast(`${formType} submitted successfully!`, 'success');
|
||||
setField1('');
|
||||
setField2('');
|
||||
setField3('');
|
||||
setImages([]);
|
||||
onClose();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const renderFields = () => {
|
||||
switch (formType) {
|
||||
case 'Feature A':
|
||||
return (
|
||||
<>
|
||||
<Input label="Reference ID" value={field1} onChangeText={setField1} required placeholder="Example: REF-001" />
|
||||
<Dropdown label="Category" value={field2} options={['Option 1', 'Option 2', 'Option 3']} onSelect={setField2} required />
|
||||
<Input label="Description" value={field3} onChangeText={setField3} multiline required infoText="Enter detailed information here." placeholder="Lorem ipsum dolor sit amet..." />
|
||||
</>
|
||||
);
|
||||
case 'Feature B':
|
||||
return (
|
||||
<>
|
||||
<Dropdown label="Type" value={field1} options={['Type A', 'Type B', 'Type C']} onSelect={setField1} required />
|
||||
<Input label="Location" value={field2} onChangeText={setField2} required placeholder="Example: Area 51" />
|
||||
<Input label="Notes" value={field3} onChangeText={setField3} multiline required infoText="Additional notes or comments." />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<Input label="Title" value={field1} onChangeText={setField1} required placeholder="Enter entry title" />
|
||||
<Input label="Additional Info" value={field2} onChangeText={setField2} multiline placeholder="Enter details..." />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!formType) return null;
|
||||
|
||||
const bg = isDark ? '#1A1A1A' : '#FFFFFF';
|
||||
const border = isDark ? '#2A2A2A' : '#EEEEEE';
|
||||
|
||||
return (
|
||||
<Popup visible={visible} onClose={onClose} title={`${formType}`} type="bottom">
|
||||
<View style={styles.container}>
|
||||
{renderFields()}
|
||||
|
||||
<Text style={[styles.label, { color: isDark ? '#6B6B6B' : '#9B9B9B' }]}>Attachments</Text>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.attachBtn, { backgroundColor: isDark ? '#222' : '#F5F5F5', borderColor: border }]}
|
||||
onPress={() => pickImage(true)}
|
||||
>
|
||||
<Feather name="camera" size={18} color={LIME} />
|
||||
<Text style={[styles.attachText, { color: colors.text }]}>Camera</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.attachBtn, { backgroundColor: isDark ? '#222' : '#F5F5F5', borderColor: border }]}
|
||||
onPress={() => pickImage(false)}
|
||||
>
|
||||
<Feather name="image" size={18} color={LIME} />
|
||||
<Text style={[styles.attachText, { color: colors.text }]}>Gallery</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{images.length > 0 && (
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.imageList}>
|
||||
{images.map((uri, idx) => (
|
||||
<View key={idx} style={styles.imageWrapper}>
|
||||
<Image source={{ uri }} style={[styles.imagePreview, { borderColor: border }]} />
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteBadge, { backgroundColor: '#EF4444' }]}
|
||||
onPress={() => removeImage(idx)}
|
||||
>
|
||||
<Feather name="x" size={10} color="#FFF" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title="Submit Entry"
|
||||
onPress={handleSubmit}
|
||||
loading={loading}
|
||||
style={{ marginTop: 24 }}
|
||||
/>
|
||||
</View>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingTop: 8,
|
||||
},
|
||||
label: {
|
||||
fontSize: 11,
|
||||
fontFamily: 'Outfit_700Bold',
|
||||
marginBottom: 10,
|
||||
marginTop: 16,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
attachBtn: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
gap: 10,
|
||||
},
|
||||
attachText: {
|
||||
fontSize: 14,
|
||||
fontFamily: 'Outfit_600SemiBold',
|
||||
},
|
||||
imageList: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 10,
|
||||
},
|
||||
imageWrapper: {
|
||||
marginRight: 14,
|
||||
position: 'relative',
|
||||
},
|
||||
imagePreview: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
},
|
||||
deleteBadge: {
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFF',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user