From 713d169915a82edfcfe4b44622e3dce8c6adaf0c Mon Sep 17 00:00:00 2001 From: Shravya Ramesh <37447613+shravyaramesh@users.noreply.github.com> Date: Tue, 17 Nov 2020 18:06:14 -0800 Subject: [TMA-382] Edit profile screen (#121) * added more icon * a less fat icon * and the actual icon asset * bottom drawer skeleton done * removed warning, better code * a more completed skeleton done * bottom drawer done! * Added content container, sent birthday picker props, minor styling * differenciating defined and undefined birthdate in birthdate, datepicker * removed restricting width for TaggDropDown * Added edit profile screen to navigator stack * Add EditProfile view, refresh profile view on save * Removes unnecessary import * Stores gender and birthdate as part of ProfileType * Added gender, birthdate, isEditProfile to AuthProv * Conditional view applied for edit profile button * Includes discarded changes in previous merge- BD * removed unused icon * resolved scary warnings * added icon to drawer * Small fix * minor code improvement * sc * fixed birthday bug * custom gender updation fixed * small change to birthday default value * missed something * cleaned up types! Warnings gone! * fixed another gender picker bug * fixed gender bug and cleaned up logic * removed warning, MUCH better code now Co-authored-by: Ivan Chen Co-authored-by: Ashm Walia --- src/screens/onboarding/ProfileOnboarding.tsx | 150 +++---- src/screens/profile/EditProfile.tsx | 591 +++++++++++++++++++++++++++ src/screens/profile/MomentCommentsScreen.tsx | 2 +- src/screens/profile/ProfileScreen.tsx | 9 +- src/screens/profile/index.ts | 1 + src/screens/search/SearchScreen.tsx | 4 +- 6 files changed, 682 insertions(+), 75 deletions(-) create mode 100644 src/screens/profile/EditProfile.tsx (limited to 'src/screens') diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 3979de38..f21f3864 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -15,7 +15,6 @@ import { Background, TaggBigInput, TaggInput, - TaggDatePicker, TaggDropDown, BirthDatePicker, } from '../../components'; @@ -52,12 +51,13 @@ const ProfileOnboarding: React.FC = ({ navigation, }) => { const {userId, username} = route.params; + let emptyDate: string | undefined; const [form, setForm] = React.useState({ largePic: '', smallPic: '', website: '', bio: '', - birthdate: '', + birthdate: emptyDate, gender: '', isValidWebsite: true, isValidBio: true, @@ -65,7 +65,7 @@ const ProfileOnboarding: React.FC = ({ attemptedSubmit: false, token: '', }); - const [customGender, setCustomGender] = React.useState(); + const [customGender, setCustomGender] = React.useState(Boolean); // refs for changing focus const bioRef = React.useRef(); @@ -232,7 +232,7 @@ const ProfileOnboarding: React.FC = ({ const handleBirthdateUpdate = (birthdate: Date) => { setForm({ ...form, - birthdate: moment(birthdate).format('YYYY-MM-DD'), + birthdate: birthdate && moment(birthdate).format('YYYY-MM-DD'), }); }; @@ -357,81 +357,85 @@ const ProfileOnboarding: React.FC = ({ - handleFocusChange('bio')} - blurOnSubmit={false} - valid={form.isValidWebsite} - attemptedSubmit={form.attemptedSubmit} - invalidWarning={'Website must be a valid link to your website'} - width={280} - /> - 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} - /> - - handleGenderUpdate(value)} - items={[ - {label: 'Male', value: 'male'}, - {label: 'Female', value: 'female'}, - {label: 'Custom', value: 'custom'}, - ]} - placeholder={{ - label: 'Gender', - value: null, - color: '#ddd', - }} - /> - {customGender && ( + handleFocusChange('bio')} + blurOnSubmit={false} + valid={form.isValidWebsite} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={'Website must be a valid link to your website'} + width={280} + /> + handleFocusChange('bio')} blurOnSubmit={false} - ref={customGenderRef} - onChangeText={handleCustomGenderUpdate} - onSubmitEditing={() => handleSubmit()} - valid={form.isValidGender} + ref={bioRef} + valid={form.isValidBio} attemptedSubmit={form.attemptedSubmit} - invalidWarning={'Custom field can only contain letters and hyphens'} + invalidWarning={ + 'Bio must be less than 150 characters and must contain valid characters' + } width={280} /> - )} - - Let's start! - + + handleGenderUpdate(value)} + items={[ + {label: 'Male', value: 'male'}, + {label: 'Female', value: 'female'}, + {label: 'Custom', value: 'custom'}, + ]} + placeholder={{ + label: 'Gender', + value: null, + color: '#ddd', + }} + /> + {customGender && ( + handleSubmit()} + valid={form.isValidGender} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={'Custom field can only contain letters and hyphens'} + width={280} + /> + )} + + Let's start! + + ); }; @@ -441,6 +445,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', marginBottom: '5%', }, + contentContainer: { + position: 'relative', + width: 280, + alignSelf: 'center', + }, largeProfileUploader: { justifyContent: 'center', alignItems: 'center', @@ -493,6 +502,7 @@ const styles = StyleSheet.create({ height: 40, borderRadius: 5, marginTop: '5%', + alignSelf: 'center', }, submitBtnLabel: { fontSize: 16, diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx new file mode 100644 index 00000000..01b67155 --- /dev/null +++ b/src/screens/profile/EditProfile.tsx @@ -0,0 +1,591 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import {RouteProp} from '@react-navigation/native'; +import moment from 'moment'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + Text, + StatusBar, + StyleSheet, + Image, + TouchableOpacity, + Alert, + View, + SafeAreaView, +} from 'react-native'; +import {Button} from 'react-native-elements'; +import { + Background, + TaggBigInput, + TaggInput, + TaggDropDown, + BirthDatePicker, + TabsGradient, +} from '../../components'; +import {OnboardingStackParams} from '../../routes/onboarding'; +import ImagePicker from 'react-native-image-crop-picker'; +import { + EDIT_PROFILE_ENDPOINT, + websiteRegex, + bioRegex, + genderRegex, +} from '../../constants'; +import AsyncStorage from '@react-native-community/async-storage'; +import {AuthContext} from '../../routes'; +import Animated from 'react-native-reanimated'; +import {SCREEN_HEIGHT} from '../../utils'; + +type ProfileOnboardingScreenRouteProp = RouteProp< + OnboardingStackParams, + 'ProfileOnboarding' +>; +type ProfileOnboardingScreenNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'ProfileOnboarding' +>; +interface ProfileOnboardingProps { + route: ProfileOnboardingScreenRouteProp; + navigation: ProfileOnboardingScreenNavigationProp; +} + +/** + * Create profile screen for onboarding. + * @param navigation react-navigation navigation object + */ + +const ProfileOnboarding: React.FC = ({ + route, + navigation, +}) => { + const y: Animated.Value = Animated.useValue(0); + const {userId, username} = route.params; + const { + profile: {website, biography, birthday, gender}, + avatar, + cover, + updateIsEditedProfile, + } = React.useContext(AuthContext); + const [needsUpdate, setNeedsUpdate] = useState(false); + + useEffect(() => { + updateIsEditedProfile(needsUpdate); + }, [needsUpdate, updateIsEditedProfile]); + + const [isCustomGender, setIsCustomGender] = React.useState( + gender !== '' && gender !== 'female' && gender !== 'male', + ); + + const [form, setForm] = React.useState({ + largePic: cover ? cover : '', + smallPic: avatar ? avatar : '', + website: website ? website : '', + bio: biography ? biography : '', + birthdate: birthday && moment(birthday).format('YYYY-MM-DD'), + gender: isCustomGender ? 'custom' : gender, + customGenderText: isCustomGender ? gender : '', + isValidWebsite: true, + isValidBio: true, + isValidGender: true, + attemptedSubmit: false, + }); + // 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; + } + }; + + /** + * Profile screen "Add Large Profile Pic Here" button + */ + const LargeProfilePic = () => ( + + {form.largePic ? ( + + ) : ( + ADD LARGE PROFILE PIC HERE + )} + + ); + + /** + * Profile screen "Add Smaller Profile Pic Here" button + */ + const SmallProfilePic = () => ( + + {form.smallPic ? ( + + ) : ( + ADD SMALLER PIC + )} + + ); + + /* + * Handles tap on add profile picture buttons by navigating to camera access + * and selecting a picture from gallery for large profile picture + */ + const goToGalleryLargePic = () => { + ImagePicker.openPicker({ + width: 580, + height: 580, + cropping: true, + cropperToolbarTitle: 'Large profile picture', + mediaType: 'photo', + }) + .then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + largePic: picture.path, + }); + } + }) + .catch(() => {}); + }; + + /* + * Handles tap on add profile picture buttons by navigating to camera access + * and selecting a picture from gallery for small profile picture + */ + const goToGallerySmallPic = () => { + ImagePicker.openPicker({ + width: 580, + height: 580, + cropping: true, + cropperToolbarTitle: 'Small profile picture', + mediaType: 'photo', + cropperCircleOverlay: true, + }) + .then((picture) => { + if ('path' in picture) { + 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) => { + website = website.trim(); + 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') { + setForm({...form, gender}); + setIsCustomGender(true); + } else if (gender === null) { + // not doing anything will make the picker "bounce back" + } else { + setIsCustomGender(false); + let isValidGender: boolean = true; + setForm({ + ...form, + gender, + isValidGender, + }); + } + }; + + const handleCustomGenderUpdate = (customGenderText: string) => { + let isValidGender: boolean = genderRegex.test(customGenderText); + customGenderText = customGenderText.replace(' ', '-'); + setForm({ + ...form, + customGenderText, + isValidGender, + }); + }; + + const handleBirthdateUpdate = (birthdate: Date) => { + setForm({ + ...form, + birthdate: birthdate && moment(birthdate).format('YYYY-MM-DD'), + }); + }; + + const handleSubmit = useCallback(async () => { + if (!form.largePic) { + Alert.alert('Please upload a large profile picture!'); + return; + } + if (!form.smallPic) { + Alert.alert('Please upload a small profile picture!'); + return; + } + 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 (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 (isCustomGender) { + if (form.isValidGender) { + request.append('gender', form.customGenderText); + } 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 = EDIT_PROFILE_ENDPOINT + `${userId}/`; + try { + const token = await AsyncStorage.getItem('token'); + let response = await fetch(endpoint, { + method: 'PATCH', + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: 'Token ' + token, + }, + body: request, + }); + let statusCode = response.status; + let data = await response.json(); + if (statusCode === 200) { + setNeedsUpdate(true); + navigation.pop(); + } else if (statusCode === 400) { + Alert.alert( + 'Profile update failed. 😔', + data.error || 'Something went wrong! 😭', + ); + } else { + Alert.alert( + 'Something went wrong! 😭', + "Would you believe me if I told you that I don't know what happened?", + ); + } + } catch (error) { + Alert.alert( + 'Profile creation failed 😓', + 'Please double-check your network connection and retry.', + ); + return { + name: 'Profile creation error', + description: error, + }; + } + }, [isCustomGender, form, navigation, userId]); + + React.useLayoutEffect(() => { + navigation.setOptions({ + headerRight: () => ( +