diff options
Diffstat (limited to 'src/screens/onboarding')
| -rw-r--r-- | src/screens/onboarding/Checkpoint.tsx | 144 | ||||
| -rw-r--r-- | src/screens/onboarding/ProfileOnboarding.tsx | 310 | ||||
| -rw-r--r-- | src/screens/onboarding/Verification.tsx | 2 | ||||
| -rw-r--r-- | src/screens/onboarding/index.ts | 3 |
4 files changed, 419 insertions, 40 deletions
diff --git a/src/screens/onboarding/Checkpoint.tsx b/src/screens/onboarding/Checkpoint.tsx new file mode 100644 index 00000000..013a80d2 --- /dev/null +++ b/src/screens/onboarding/Checkpoint.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + View, + Text, + StyleSheet, + StatusBar, + Platform, + TouchableOpacity, +} from 'react-native'; + +import {RootStackParamList, AuthContext} from '../../routes'; +import {RegistrationWizard, Background} from '../../components'; + +type CheckpointRouteProp = RouteProp<RootStackParamList, 'Checkpoint'>; +type CheckpointNavigationProp = StackNavigationProp< + RootStackParamList, + 'Checkpoint' +>; +interface CheckpointProps { + route: CheckpointRouteProp; + navigation: CheckpointNavigationProp; +} +/** + * Registration screen 2 for email, username, password, and terms and conditions + * @param navigation react-navigation navigation object + */ +const Checkpoint: React.FC<CheckpointProps> = ({route, navigation}) => { + const {userId, username} = route.params; + + /** + * login: determines if user successully created an account to + * navigate to home and display main tab navigation bar + */ + const {login} = React.useContext(AuthContext); + + const handleSkip = () => { + login(userId, username); + }; + + const handleProceed = () => { + navigation.navigate('ProfileOnboarding', { + userId: userId, + username: username, + }); + }; + + return ( + <Background style={styles.container}> + <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="four" /> + + <View style={styles.textContainer}> + <Text style={styles.header}>Email verified!</Text> + <Text style={styles.subtext}> + We're almost there. Would you like to setup your profile now? + </Text> + <View style={styles.buttonContainer}> + <TouchableOpacity onPress={handleSkip} style={styles.skipButton}> + <Text style={styles.skipButtonLabel}>Do it later</Text> + </TouchableOpacity> + <TouchableOpacity + onPress={handleProceed} + style={styles.proceedButton}> + <Text style={styles.proceedButtonLabel}>Let's do it!</Text> + </TouchableOpacity> + </View> + </View> + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + }, + textContainer: { + marginTop: '65%', + }, + + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-evenly', + }, + wizard: { + ...Platform.select({ + ios: { + top: 50, + }, + android: { + bottom: 40, + }, + }), + }, + header: { + color: '#fff', + fontSize: 22, + fontWeight: '600', + textAlign: 'center', + marginBottom: '4%', + marginHorizontal: '10%', + }, + subtext: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + textAlign: 'center', + marginBottom: '16%', + marginHorizontal: '10%', + }, + proceedButton: { + backgroundColor: '#8F01FF', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + marginTop: '5%', + }, + proceedButtonLabel: { + fontSize: 16, + fontWeight: '500', + color: '#fff', + }, + skipButton: { + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#ddd', + marginTop: '5%', + }, + skipButtonLabel: { + fontSize: 16, + fontWeight: '500', + color: '#ddd', + }, +}); + +export default Checkpoint; diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 9405ca52..ea045434 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -10,11 +10,23 @@ import { Alert, View, } from 'react-native'; +import { + Background, + TaggBigInput, + TaggInput, + TaggDatePicker, + TaggDropDown, +} from '../../components'; import {OnboardingStackParams} from '../../routes/onboarding'; import {AuthContext} from '../../routes/authentication'; -import {Background} from '../../components'; import ImagePicker from 'react-native-image-crop-picker'; -import {REGISTER_ENDPOINT} from '../../constants'; +import { + REGISTER_ENDPOINT, + websiteRegex, + bioRegex, + genderRegex, +} from '../../constants'; +import moment from 'moment'; type ProfileOnboardingScreenRouteProp = RouteProp< OnboardingStackParams, @@ -36,8 +48,51 @@ interface ProfileOnboardingProps { const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { const {userId, username} = route.params; - const [largePic, setLargePic] = React.useState(''); - const [smallPic, setSmallPic] = React.useState(''); + const [form, setForm] = React.useState({ + largePic: '', + smallPic: '', + website: '', + bio: '', + birthdate: '', + gender: '', + isValidWebsite: true, + isValidBio: true, + isValidGender: true, + attemptedSubmit: false, + }); + const [customGender, setCustomGender] = React.useState(); + + // refs for changing focus + const bioRef = React.useRef(); + const birthdateRef = React.useRef(); + const genderRef = React.useRef(); + const customGenderRef = React.useRef(); + /** + * Handles focus change to the next input field. + * @param field key for field to move focus onto + */ + const handleFocusChange = (field: string): void => { + switch (field) { + case 'bio': + const bioField: any = bioRef.current; + bioField.focus(); + break; + case 'birthdate': + const birthdateField: any = birthdateRef.current; + birthdateField.focus(); + break; + case 'gender': + const genderField: any = genderRef.current; + genderField.focus(); + break; + case 'customGender': + const customGenderField: any = customGenderRef.current; + customGenderField.focus(); + break; + default: + return; + } + }; /** * login: determines if user successully created an account to @@ -54,9 +109,9 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { accessibilityLabel="ADD LARGE PROFILE PIC HERE" onPress={goToGalleryLargePic} style={styles.largeProfile}> - {largePic ? ( + {form.largePic ? ( <Image - source={{uri: largePic}} + source={{uri: form.largePic}} style={[styles.largeProfile, styles.profilePic]} /> ) : ( @@ -74,9 +129,9 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { accessibilityLabel="ADD SMALLER PIC" onPress={goToGallerySmallPic} style={styles.smallProfile}> - {smallPic ? ( + {form.smallPic ? ( <Image - source={{uri: smallPic}} + source={{uri: form.smallPic}} style={[styles.smallProfile, styles.profilePic]} /> ) : ( @@ -99,7 +154,10 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { }) .then((picture) => { if ('path' in picture) { - setLargePic(picture.path); + setForm({ + ...form, + largePic: picture.path, + }); } }) .catch(() => {}); @@ -120,28 +178,140 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { }) .then((picture) => { if ('path' in picture) { - setSmallPic(picture.path); + setForm({ + ...form, + smallPic: picture.path, + }); } }) .catch(() => {}); }; + /* + * Handles changes to the website field value and verifies the input by updating state and running a validation function. + */ + const handleWebsiteUpdate = (website: string) => { + let isValidWebsite: boolean = websiteRegex.test(website); + setForm({ + ...form, + website, + isValidWebsite, + }); + }; + + /* + * Handles changes to the bio field value and verifies the input by updating state and running a validation function. + */ + const handleBioUpdate = (bio: string) => { + let isValidBio: boolean = bioRegex.test(bio); + setForm({ + ...form, + bio, + isValidBio, + }); + }; + + const handleGenderUpdate = (gender: string) => { + if (gender === 'custom') { + setCustomGender(true); + } else { + setCustomGender(false); + let isValidGender: boolean = true; + setForm({ + ...form, + gender, + isValidGender, + }); + } + }; + + const handleCustomGenderUpdate = (gender: string) => { + let isValidGender: boolean = genderRegex.test(gender); + gender = gender.replace(' ', '-'); + setForm({ + ...form, + gender, + isValidGender, + }); + }; + + const handleBirthdateUpdate = (birthdate: string) => { + setForm({ + ...form, + birthdate, + }); + }; + + const getMaxDate = () => { + const maxDate = moment().subtract(13, 'y').subtract(1, 'd'); + return maxDate.format('YYYY-MM-DD'); + }; + const handleSubmit = async () => { - const form = new FormData(); - if (largePic) { - form.append('largeProfilePicture', { - uri: largePic, + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + let invalidFields: boolean = false; + const request = new FormData(); + if (form.largePic) { + request.append('largeProfilePicture', { + uri: form.largePic, name: 'large_profile_pic.jpg', type: 'image/jpg', }); } - if (smallPic) { - form.append('smallProfilePicture', { - uri: smallPic, + if (form.smallPic) { + request.append('smallProfilePicture', { + uri: form.smallPic, name: 'small_profile_pic.jpg', type: 'image/jpg', }); } + if (form.website) { + if (form.isValidWebsite) { + request.append('website', form.website); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + } + + if (form.bio) { + if (form.isValidBio) { + request.append('biography', form.bio); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + } + + if (form.birthdate) { + request.append('birthday', form.birthdate); + } + + if (customGender) { + if (form.isValidGender) { + request.append('gender', form.gender); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + } else { + if (form.isValidGender) { + request.append('gender', form.gender); + } + } + + if (invalidFields) { + return; + } + const endpoint = REGISTER_ENDPOINT + `${userId}/`; try { let response = await fetch(endpoint, { @@ -149,7 +319,7 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { headers: { 'Content-Type': 'multipart/form-data', }, - body: form, + body: request, }); let statusCode = response.status; let data = await response.json(); @@ -178,14 +348,82 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { return ( <Background centered> <StatusBar barStyle="light-content" /> - <LargeProfilePic /> - <SmallProfilePic /> - <View style={styles.dummyField}> - <Text>DUMMY WEBSITE</Text> - </View> - <View style={styles.dummyField}> - <Text>DUMMY BIO</Text> + <View style={styles.profile}> + <LargeProfilePic /> + <SmallProfilePic /> </View> + <TaggInput + accessibilityHint="Enter a website." + accessibilityLabel="Website input field." + placeholder="Website" + autoCompleteType="off" + textContentType="URL" + autoCapitalize="none" + returnKeyType="next" + onChangeText={handleWebsiteUpdate} + onSubmitEditing={() => handleFocusChange('bio')} + blurOnSubmit={false} + valid={form.isValidWebsite} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={'Website must be a valid link to your website'} + width={280} + /> + <TaggBigInput + accessibilityHint="Enter a bio." + accessibilityLabel="Bio input field." + placeholder="Bio" + autoCompleteType="off" + textContentType="none" + autoCapitalize="none" + returnKeyType="next" + onChangeText={handleBioUpdate} + onSubmitEditing={() => handleFocusChange('bio')} + blurOnSubmit={false} + ref={bioRef} + valid={form.isValidBio} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={ + 'Bio must be less than 150 characters and must contain valid characters' + } + width={280} + /> + <TaggDatePicker + date={form.birthdate} + maxDate={getMaxDate()} + onDateChange={(birthdate) => handleBirthdateUpdate(birthdate)} + /> + <TaggDropDown + onValueChange={(value) => handleGenderUpdate(value)} + items={[ + {label: 'Male', value: 'male'}, + {label: 'Female', value: 'female'}, + {label: 'Custom', value: 'custom'}, + ]} + placeholder={{ + label: 'Gender', + value: null, + color: '#ddd', + }} + /> + {customGender && ( + <TaggInput + accessibilityHint="Custom" + accessibilityLabel="Gender input field." + placeholder="Enter your gender" + autoCompleteType="off" + textContentType="none" + autoCapitalize="none" + returnKeyType="next" + blurOnSubmit={false} + ref={customGenderRef} + onChangeText={handleCustomGenderUpdate} + onSubmitEditing={() => handleSubmit()} + valid={form.isValidGender} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={'Custom field can only contain letters and hyphens'} + width={280} + /> + )} <TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}> <Text style={styles.submitBtnLabel}>Let's start!</Text> </TouchableOpacity> @@ -194,6 +432,10 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { }; const styles = StyleSheet.create({ + profile: { + flexDirection: 'row', + marginBottom: '5%', + }, largeProfile: { justifyContent: 'center', alignItems: 'center', @@ -202,7 +444,8 @@ const styles = StyleSheet.create({ width: 230, borderRadius: 23, backgroundColor: '#fff', - marginRight: '6%', + marginLeft: '13%', + marginTop: '5%', }, largeProfileText: { textAlign: 'center', @@ -218,8 +461,8 @@ const styles = StyleSheet.create({ width: 110, borderRadius: 55, backgroundColor: '#E1F0FF', - marginLeft: '45%', - bottom: '7%', + right: '18%', + marginTop: '38%', }, smallProfileText: { textAlign: 'center', @@ -232,16 +475,6 @@ const styles = StyleSheet.create({ marginLeft: 0, bottom: 0, }, - dummyField: { - height: '10%', - width: '80%', - justifyContent: 'center', - alignItems: 'center', - borderColor: '#fff', - borderWidth: 1, - borderRadius: 8, - marginBottom: '10%', - }, submitBtn: { backgroundColor: '#8F01FF', justifyContent: 'center', @@ -249,6 +482,7 @@ const styles = StyleSheet.create({ width: 150, height: 40, borderRadius: 5, + marginTop: '5%', }, submitBtnLabel: { fontSize: 16, diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx index 0676bb3a..7c74324a 100644 --- a/src/screens/onboarding/Verification.tsx +++ b/src/screens/onboarding/Verification.tsx @@ -59,7 +59,7 @@ const Verification: React.FC<VerificationProps> = ({route, navigation}) => { }); let statusCode = verifyOtpResponse.status; if (statusCode === 200) { - navigation.navigate('ProfileOnboarding', { + navigation.navigate('Checkpoint', { userId: userId, username: username, }); diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 7a9816e7..e6627ca7 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -1,5 +1,6 @@ export {default as Login} from './Login'; -export {default as ProfileOnboarding} from './ProfileOnboarding'; export {default as RegistrationOne} from './RegistrationOne'; export {default as RegistrationTwo} from './RegistrationTwo'; export {default as Verification} from './Verification'; +export {default as Checkpoint} from './Checkpoint'; +export {default as ProfileOnboarding} from './ProfileOnboarding'; |
