Finalized all of the passport application flow feature except for step 6

This commit is contained in:
Mochammad Adhi Buchori
2025-04-25 17:27:12 +07:00
parent 2fa2b05918
commit ad67df1461
20 changed files with 1418 additions and 115 deletions

View File

@ -0,0 +1,321 @@
import React, {useState} from 'react';
import {
StyleSheet,
Text,
View,
Modal,
Pressable,
ScrollView,
} from 'react-native';
import {Portal, Button} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import FontFamily from '../../../assets/styles/FontFamily';
import TextInputComponent from '../TextInput';
import genderData from '../../data/DropdownData/GenderData';
type SheetEditDataProps = {
visible: boolean;
onClose: () => void;
showCivilStatusDocumentsInfoDialog: () => void;
selectedPassportOption: string;
};
interface DocumentUploadSectionProps {
title: string;
isRequired?: boolean;
isIcon?: boolean;
showDialogCivilStatusDocumentsInfo?: () => void;
}
const DocumentUploadSection = (props: DocumentUploadSectionProps) => {
const {title, isRequired, isIcon, showDialogCivilStatusDocumentsInfo} = props;
const [uploadedFileName, setUploadedFileName] = useState<string | null>(null);
const handleUpload = () => {
let fileName = `${title.toLowerCase().replace(/ /g, '')}.jpg`;
if (
title === 'Akta kelahiran/ijazah/akta perkawinan/buku nikah/surat baptis'
) {
fileName = 'ijazah.jpg';
}
setUploadedFileName(fileName);
};
const handleDelete = () => setUploadedFileName(null);
return (
<View>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>
{title} {isRequired && <Text style={styles.required}>*</Text>}
</Text>
{isIcon && (
<Pressable
style={({pressed}) => ({
transform: [{scale: pressed ? 0.925 : 1}],
})}
onPress={showDialogCivilStatusDocumentsInfo}>
<Icon name="information" size={24} color={Colors.primary30.color} />
</Pressable>
)}
</View>
{!uploadedFileName ? (
<View style={styles.uploadButtonGroup}>
<Button
icon="camera-outline"
mode="contained"
style={styles.uploadButton}
textColor={Colors.neutral100.color}
labelStyle={{fontSize: 12}}
onPress={handleUpload}>
Foto Dokumen
</Button>
<Button
icon="tray-arrow-up"
mode="contained"
style={styles.uploadButton}
textColor={Colors.neutral100.color}
labelStyle={{fontSize: 12}}
onPress={handleUpload}>
Unggah Dokumen
</Button>
</View>
) : (
<View style={styles.uploadedContainer}>
<View style={styles.uploadedTextWrapper}>
<Text style={styles.uploadedTitle}>Berhasil dipilih</Text>
<Text style={styles.uploadedFilename}>{uploadedFileName}</Text>
</View>
<Icon
name="trash-can-outline"
size={24}
color={Colors.indicatorRed.color}
onPress={handleDelete}
/>
</View>
)}
</View>
);
};
const SheetEditData = (props: SheetEditDataProps) => {
const {
visible,
onClose,
showCivilStatusDocumentsInfoDialog,
selectedPassportOption,
} = props;
return (
<Portal>
<Modal visible={visible} transparent animationType="slide">
<Pressable style={styles.backdrop} onPress={onClose} />
<View style={styles.modalContainer}>
<ScrollView
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
contentContainerStyle={{paddingBottom: 24}}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Ubah Data Pemohon</Text>
<Pressable
style={({pressed}) => ({
transform: [{scale: pressed ? 0.925 : 1}],
})}
onPress={onClose}>
<Icon name="close" size={24} color={Colors.primary30.color} />
</Pressable>
</View>
<View style={styles.infoList}>
<View style={styles.infoItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.infoText}>
Unggah dokumen hanya bisa berbentuk JPG.
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.bullet}></Text>
<Text style={styles.infoText}>
Data yang bertanda <Text style={styles.required}>*</Text>{' '}
wajib diisi.
</Text>
</View>
</View>
<View style={{gap: 16}}>
<TextInputComponent
title="Nama Pemohon"
placeholder="Salwa Aisyah Adhani"
isRequired
isDisabled
/>
<View style={styles.row}>
<View style={styles.flex}>
<TextInputComponent
title="Tanggal Lahir"
placeholder="22/02/2002"
isRequired
isDate
isDisabled
/>
</View>
<View style={styles.flex}>
<TextInputComponent
title="Jenis Kelamin"
placeholder="Wanita"
isRequired
isDropdown
isDisabled
dropdownItemData={genderData}
/>
</View>
</View>
<TextInputComponent
title="NIK"
placeholder="3271234560009123456"
isRequired
isDisabled
/>
<DocumentUploadSection title="e-KTP" isRequired />
<DocumentUploadSection title="Kartu Keluarga" />
<DocumentUploadSection
title="Akta kelahiran/ijazah/akta perkawinan/buku nikah/surat baptis"
isIcon
showDialogCivilStatusDocumentsInfo={
showCivilStatusDocumentsInfoDialog
}
/>
{selectedPassportOption !== 'already' && (
<DocumentUploadSection title="Paspor Lama" isRequired />
)}
</View>
<View style={styles.saveButtonWrapper}>
<Button
style={styles.saveButton}
mode="contained"
textColor={Colors.neutral100.color}
onPress={onClose}>
Simpan
</Button>
</View>
</ScrollView>
</View>
</Modal>
</Portal>
);
};
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
},
modalContainer: {
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 24,
paddingHorizontal: 16,
position: 'absolute',
bottom: 0,
width: '100%',
maxHeight: '90%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
modalTitle: {
fontSize: 12,
color: Colors.primary30.color,
...FontFamily.notoSansBold,
},
infoList: {
marginVertical: 16,
},
infoItem: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: 6,
},
bullet: {
fontSize: 10,
...FontFamily.notoSansBold,
color: Colors.primary10.color,
lineHeight: 20,
},
infoText: {
fontSize: 10,
...FontFamily.notoSansBold,
color: Colors.primary10.color,
lineHeight: 20,
flex: 1,
textAlign: 'justify',
},
required: {
color: Colors.indicatorRed.color,
},
row: {
flexDirection: 'row',
gap: 12,
},
flex: {
flex: 1,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
},
sectionTitle: {
marginBottom: 8,
fontSize: 12,
color: Colors.primary30.color,
...FontFamily.notoSansBold,
flex: 1,
},
uploadButtonGroup: {
flexDirection: 'row',
gap: 16,
},
uploadButton: {
flex: 1,
backgroundColor: Colors.secondary30.color,
},
uploadedContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderWidth: 1,
borderColor: Colors.secondary50.color,
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 8,
},
uploadedTextWrapper: {
gap: 4,
},
uploadedTitle: {
color: Colors.primary30.color,
fontSize: 12,
...FontFamily.notoSansMedium,
},
uploadedFilename: {
color: Colors.primary40.color,
fontSize: 10,
...FontFamily.notoSansRegular,
},
saveButtonWrapper: {
marginTop: 24,
},
saveButton: {
backgroundColor: Colors.primary30.color,
},
});
export default SheetEditData;

View File

@ -0,0 +1,241 @@
import {
FlatList,
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import {Button, Portal, TextInput} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import FontFamily from '../../../assets/styles/FontFamily';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import immigrationOfficesData from '../../data/Steps/ImmigrationOfficesData';
import {useState} from 'react';
type SheetSearchLocationProps = {
visible: boolean;
onClose: () => void;
};
const ItemSeparator = () => <View style={styles.flatllistGap} />;
const renderSearchInput = () => {
return (
<View>
<TextInput
mode="outlined"
style={styles.searchInput}
theme={{roundness: 12}}
placeholderTextColor={Colors.primary60.color}
activeOutlineColor={Colors.primary10.color}
placeholder="Cari lokasi"
textColor="#48454E"
contentStyle={{marginLeft: 48}}
left={
<TextInput.Icon
icon="magnify"
color="#48454E"
style={{marginLeft: 8}}
/>
}
/>
</View>
);
};
const renderCurrentLocation = (onPress: () => void) => {
return (
<Pressable
onPress={onPress}
style={({pressed}) => [
styles.currentLocationContainer,
{
transform: [{scale: pressed ? 0.985 : 1}],
},
]}>
<Icon name="crosshairs-gps" size={24} color={Colors.primary30.color} />
<View style={styles.currentLocationTextContainer}>
<Text style={styles.currentLocationTitle}>
Pakai lokasi saya saat ini
</Text>
<Text style={styles.currentLocationSubtitle}>
Jl. Raya Muchtar No. 8, Depok, 16511
</Text>
</View>
</Pressable>
);
};
const renderNearestLocationItem = ({
item,
onPress,
}: {
item: any;
onPress: () => void;
}) => {
return (
<Pressable
onPress={onPress}
style={({pressed}) => [
styles.nearestLocationContainer,
{
transform: [{scale: pressed ? 0.985 : 1}],
},
]}>
<View style={styles.nearestLocationInfoWrapper}>
<View style={styles.nearestLocationHeader}>
<Text style={styles.locationName}>{item.name}</Text>
<Text style={styles.locationDistance}>{item.distance}</Text>
</View>
<Text style={styles.locationAddress}>{item.address}</Text>
</View>
<View style={styles.buttonWrapper}>
<Button
mode="contained"
textColor={Colors.neutral100.color}
style={styles.selectButton}>
Pilih Kantor
</Button>
<Button
mode="outlined"
textColor={Colors.primary30.color}
style={styles.viewButton}>
Lihat Lokasi
</Button>
</View>
</Pressable>
);
};
const SheetSearchLocation = (props: SheetSearchLocationProps) => {
const {visible, onClose} = props;
const [showNearestLocations, setShowNearestLocations] = useState(false);
return (
<Portal>
<Modal visible={visible} transparent animationType="slide">
<Pressable style={styles.backdrop} onPress={onClose} />
<View style={styles.modalContainer}>
{renderSearchInput()}
{!showNearestLocations &&
renderCurrentLocation(() => setShowNearestLocations(true))}
{showNearestLocations && (
<FlatList
data={immigrationOfficesData}
keyExtractor={item => item.id}
renderItem={({item}) =>
renderNearestLocationItem({item, onPress: onClose})
}
contentContainerStyle={{marginVertical: 16, paddingBottom: 32}}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={ItemSeparator}
/>
)}
</View>
</Modal>
</Portal>
);
};
const styles = StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.3)',
},
modalContainer: {
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 24,
paddingHorizontal: 16,
position: 'absolute',
bottom: 0,
height: '90%',
width: '100%',
maxHeight: '90%',
},
searchInput: {
backgroundColor: Colors.neutral100.color,
fontSize: 13,
...FontFamily.notoSansRegular,
includeFontPadding: false,
},
currentLocationContainer: {
marginVertical: 16,
padding: 16,
flexDirection: 'row',
gap: 12,
alignItems: 'center',
borderWidth: 1,
borderRadius: 8,
borderColor: Colors.primary30.color,
},
currentLocationTextContainer: {
gap: 6,
},
currentLocationTitle: {
includeFontPadding: false,
fontSize: 14,
...FontFamily.notoSansMedium,
color: Colors.primary30.color,
},
currentLocationSubtitle: {
includeFontPadding: false,
fontSize: 12,
...FontFamily.notoSansRegular,
color: Colors.primary40.color,
},
nearestLocationContainer: {
padding: 16,
borderRadius: 8,
gap: 16,
borderWidth: 1,
borderColor: Colors.secondary30.color,
},
nearestLocationInfoWrapper: {
gap: 8,
},
nearestLocationHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
},
locationName: {
color: Colors.secondary30.color,
includeFontPadding: false,
...FontFamily.notoSansBold,
fontSize: 14,
flex: 1,
},
locationDistance: {
color: Colors.primary60.color,
includeFontPadding: false,
fontSize: 12,
...FontFamily.notoSansMedium,
},
locationAddress: {
color: Colors.primary40.color,
includeFontPadding: false,
fontSize: 12,
...FontFamily.notoSansRegular,
},
buttonWrapper: {
flexDirection: 'row',
gap: 12,
},
selectButton: {
backgroundColor: Colors.primary30.color,
flex: 1,
},
viewButton: {
borderColor: Colors.primary30.color,
flex: 1,
},
flatllistGap: {
height: 16,
},
});
export default SheetSearchLocation;

View File

@ -0,0 +1,96 @@
import {StyleSheet, Text, View} from 'react-native';
import {Dialog, Portal, Button} from 'react-native-paper';
import Colors from '../../../assets/styles/Colors';
import FontFamily from '../../../assets/styles/FontFamily';
type Props = {
visible: boolean;
onClose: () => void;
};
const SheetSelectDate = (props: Props) => {
const {visible, onClose} = props;
return (
<Portal>
<Dialog visible={visible} style={styles.dialogContainer}>
<Dialog.Title style={styles.dialogTitle}>
Jenis dan Manfaat Paspor
</Dialog.Title>
<View style={styles.dialogContentContainer}>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
1. Paspor Biasa
</Text>
<Text style={styles.dialogDesc}>
Biaya pemrosesan lebih murah, tersedia di seluruh lokasi Kanim.
{'\n'}Biaya pembuatan: Rp350.000
</Text>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
2. Paspor Elektronik
</Text>
<Text style={styles.dialogDesc}>
Terdapat chip pada cover paspor yang menyimpan data pemegang paspor
sehingga meningkatkan fitur keamanan, tersedia di 35 Kanim.{'\n'}
Biaya pembuatan: Rp650.000
</Text>
<Text style={[styles.dialogDesc, {...FontFamily.notoSansBold}]}>
3. Paspor Elektronik Polikarbonat
</Text>
<Text style={styles.dialogDesc}>
Berbahan polikarbonat (PC) pada halaman data pemegang paspor
(halaman 2) yang memiliki fitur keamanan tinggi dan lebih kuat,
tersedia di Kanim Jakarta Selatan, Kanim Jakarta Barat, dan Kanim
Soekarno Hatta.{'\n'}Biaya pembuatan: Rp650.000
</Text>
<View>
<Button
style={styles.buttonContinue}
mode="contained"
textColor={Colors.neutral100.color}
onPress={() => {
onClose();
}}>
Lanjut
</Button>
</View>
</View>
</Dialog>
</Portal>
);
};
const styles = StyleSheet.create({
dialogContainer: {
backgroundColor: 'white',
elevation: 0,
shadowColor: 'transparent',
borderRadius: 20,
},
dialogContentContainer: {
marginHorizontal: 24,
marginBottom: 24,
gap: 16,
},
dialogTitle: {
fontSize: 22,
color: Colors.secondary30.color,
},
dialogDesc: {
fontSize: 14,
...FontFamily.notoSansRegular,
includeFontPadding: false,
color: Colors.primary30.color,
lineHeight: 22,
},
dialogDescRed: {
...FontFamily.notoSansBold,
color: Colors.indicatorRed.color,
includeFontPadding: false,
},
buttonContinue: {
backgroundColor: Colors.primary30.color,
marginTop: 12,
},
});
export default SheetSelectDate;