aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShravya Ramesh <37447613+shravyaramesh@users.noreply.github.com>2020-11-17 18:06:14 -0800
committerGitHub <noreply@github.com>2020-11-17 21:06:14 -0500
commit713d169915a82edfcfe4b44622e3dce8c6adaf0c (patch)
tree3f0a0a9ef86e80442c4cc5b6b89be24cf1526268
parent9b4ba92df514ca8c5c92c4f9279144e2c9d49e36 (diff)
[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 <ivan@thetaggid.com> Co-authored-by: Ashm Walia <ashmwalia@outlook.com>
-rw-r--r--package.json1
-rw-r--r--src/assets/icons/more_horiz-24px.svg1
-rw-r--r--src/assets/ionicons/person-outline.svg1
-rw-r--r--src/components/common/BottomDrawer.tsx118
-rw-r--r--src/components/common/SocialLinkModal.tsx3
-rw-r--r--src/components/common/TaggDatePicker.tsx22
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/moments/Moment.tsx4
-rw-r--r--src/components/onboarding/BirthDatePicker.tsx9
-rw-r--r--src/components/onboarding/TaggDropDown.tsx7
-rw-r--r--src/components/profile/MoreInfoDrawer.tsx88
-rw-r--r--src/components/profile/ProfileBody.tsx4
-rw-r--r--src/components/profile/ProfileHeader.tsx39
-rw-r--r--src/components/profile/ToggleButton.tsx5
-rw-r--r--src/components/profile/index.ts5
-rw-r--r--src/components/search/RecentSearches.tsx3
-rw-r--r--src/components/taggs/TwitterTaggPost.tsx4
-rw-r--r--src/constants/constants.ts7
-rw-r--r--src/routes/authentication/AuthProvider.tsx13
-rw-r--r--src/routes/profile/Profile.tsx12
-rw-r--r--src/routes/profile/ProfileStack.tsx4
-rw-r--r--src/screens/onboarding/ProfileOnboarding.tsx150
-rw-r--r--src/screens/profile/EditProfile.tsx591
-rw-r--r--src/screens/profile/MomentCommentsScreen.tsx2
-rw-r--r--src/screens/profile/ProfileScreen.tsx9
-rw-r--r--src/screens/profile/index.ts1
-rw-r--r--src/screens/search/SearchScreen.tsx4
-rw-r--r--src/services/UserProfileService.ts7
-rw-r--r--src/types/types.ts2
29 files changed, 1002 insertions, 115 deletions
diff --git a/package.json b/package.json
index 84c8163a..91f30119 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"react-native-svg": "^12.1.0",
"react-native-vector-icons": "^7.0.0",
"react-promise-tracker": "^2.1.0",
+ "reanimated-bottom-sheet": "^1.0.0-alpha.22",
"rn-fetch-blob": "^0.12.0"
},
"devDependencies": {
diff --git a/src/assets/icons/more_horiz-24px.svg b/src/assets/icons/more_horiz-24px.svg
new file mode 100644
index 00000000..3d4fc0b5
--- /dev/null
+++ b/src/assets/icons/more_horiz-24px.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" fill="currentColor"/></svg> \ No newline at end of file
diff --git a/src/assets/ionicons/person-outline.svg b/src/assets/ionicons/person-outline.svg
new file mode 100644
index 00000000..fa39dc76
--- /dev/null
+++ b/src/assets/ionicons/person-outline.svg
@@ -0,0 +1 @@
+<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Person</title><path d='M344 144c-3.92 52.87-44 96-88 96s-84.15-43.12-88-96c-4-55 35-96 88-96s92 42 88 96z' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'/><path d='M256 304c-87 0-175.3 48-191.64 138.6C62.39 453.52 68.57 464 80 464h352c11.44 0 17.62-10.48 15.65-21.4C431.3 352 343 304 256 304z' fill='none' stroke='currentColor' stroke-miterlimit='10' stroke-width='32'/></svg> \ No newline at end of file
diff --git a/src/components/common/BottomDrawer.tsx b/src/components/common/BottomDrawer.tsx
new file mode 100644
index 00000000..bef9434a
--- /dev/null
+++ b/src/components/common/BottomDrawer.tsx
@@ -0,0 +1,118 @@
+import React, {Fragment, ReactText, useEffect, useRef, useState} from 'react';
+import {
+ Modal,
+ StyleSheet,
+ TouchableWithoutFeedback,
+ View,
+ ViewProps,
+} from 'react-native';
+import Animated from 'react-native-reanimated';
+import BottomSheet from 'reanimated-bottom-sheet';
+import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+
+interface BottomDrawerProps extends ViewProps {
+ initialSnapPosition?: ReactText;
+ isOpen: boolean;
+ setIsOpen: (open: boolean) => void;
+ showHeader: boolean;
+}
+
+// More examples here:
+// https://github.com/osdnk/react-native-reanimated-bottom-sheet/tree/master/Example
+const BottomDrawer: React.FC<BottomDrawerProps> = (props) => {
+ const {isOpen, setIsOpen, showHeader, initialSnapPosition} = props;
+ const drawerRef = useRef<BottomSheet>(null);
+ const [modalVisible, setModalVisible] = useState(isOpen);
+ const bgAlpha = new Animated.Value(isOpen ? 1 : 0);
+
+ useEffect(() => {
+ if (isOpen) {
+ setModalVisible(true);
+ } else {
+ bgAlpha.setValue(0);
+ drawerRef.current && drawerRef.current.snapTo(1);
+ }
+ }, [isOpen]);
+
+ const renderContent = () => {
+ return <View>{props.children}</View>;
+ };
+
+ const renderHeader = () => {
+ return showHeader ? (
+ <View style={styles.header}>
+ <View style={styles.panelHeader}>
+ <View style={styles.panelHandle} />
+ </View>
+ </View>
+ ) : (
+ <Fragment />
+ );
+ };
+
+ return (
+ <Modal
+ transparent
+ visible={modalVisible}
+ onShow={() => {
+ drawerRef.current && drawerRef.current.snapTo(0);
+ }}>
+ <BottomSheet
+ ref={drawerRef}
+ snapPoints={[initialSnapPosition ?? '30%', 0]}
+ initialSnap={1}
+ renderContent={renderContent}
+ renderHeader={renderHeader}
+ enabledContentGestureInteraction={false}
+ callbackNode={bgAlpha}
+ onCloseEnd={() => {
+ setModalVisible(false);
+ setIsOpen(false);
+ }}
+ />
+
+ <TouchableWithoutFeedback
+ onPress={() => {
+ setIsOpen(false);
+ }}>
+ <Animated.View
+ style={[
+ styles.backgroundView,
+ {
+ backgroundColor: Animated.interpolateColors(bgAlpha, {
+ inputRange: [0, 1],
+ outputColorRange: ['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)'],
+ }),
+ },
+ ]}
+ />
+ </TouchableWithoutFeedback>
+ </Modal>
+ );
+};
+
+const styles = StyleSheet.create({
+ header: {
+ backgroundColor: '#f7f5eee8',
+ shadowColor: '#000000',
+ paddingTop: 20,
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
+ },
+ panelHeader: {
+ alignItems: 'center',
+ },
+ panelHandle: {
+ width: 40,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: '#00000040',
+ marginBottom: 10,
+ },
+ backgroundView: {
+ height: SCREEN_HEIGHT,
+ width: SCREEN_WIDTH,
+ },
+});
+
+export default BottomDrawer;
diff --git a/src/components/common/SocialLinkModal.tsx b/src/components/common/SocialLinkModal.tsx
index 3cea2567..b307a62c 100644
--- a/src/components/common/SocialLinkModal.tsx
+++ b/src/components/common/SocialLinkModal.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {Modal, StyleSheet, Text, TouchableHighlight, View} from 'react-native';
import {TextInput} from 'react-native-gesture-handler';
+import { TAGG_TEXT_LIGHT_BLUE } from '../../constants';
import {SCREEN_WIDTH} from '../../utils';
interface SocialLinkModalProps {
@@ -104,7 +105,7 @@ const styles = StyleSheet.create({
fontSize: 14,
/* identical to box height */
textAlign: 'center',
- color: '#698DD3',
+ color: TAGG_TEXT_LIGHT_BLUE,
},
textInput: {
height: 20,
diff --git a/src/components/common/TaggDatePicker.tsx b/src/components/common/TaggDatePicker.tsx
index d8010251..059bf620 100644
--- a/src/components/common/TaggDatePicker.tsx
+++ b/src/components/common/TaggDatePicker.tsx
@@ -1,3 +1,4 @@
+import moment from 'moment';
import React, {useState} from 'react';
import DatePicker from 'react-native-date-picker';
@@ -5,23 +6,24 @@ interface TaggDatePickerProps {
handleDateUpdate: (_: Date) => void;
maxDate: Date;
textColor: string;
+ date: Date | undefined;
}
-const TaggDatePicker: React.FC<TaggDatePickerProps> = ({
- handleDateUpdate,
- maxDate,
- textColor,
-}) => {
- const [date, setDate] = useState(new Date());
+const TaggDatePicker: React.FC<TaggDatePickerProps> = (props) => {
+ const [date, setDate] = useState(
+ props.date
+ ? new Date(moment(props.date).add(1, 'day').format('YYYY-MM-DD'))
+ : undefined,
+ );
return (
<DatePicker
- date={date}
- textColor={textColor}
+ date={date ? date : props.maxDate}
+ textColor={props.textColor}
mode={'date'}
- maximumDate={maxDate}
+ maximumDate={props.maxDate}
onDateChange={(newDate) => {
setDate(newDate);
- handleDateUpdate(newDate);
+ props.handleDateUpdate(newDate);
}}
/>
);
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index c7ed13cd..883dae61 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -13,4 +13,5 @@ export {default as SocialLinkModal} from './SocialLinkModal';
export {default as ComingSoon} from './ComingSoon';
export {default as PostCarousel} from './PostCarousel';
export {default as TaggDatePicker} from './TaggDatePicker';
+export {default as BottomDrawer} from './BottomDrawer';
export * from './post';
diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx
index f905bfe3..9e138ef3 100644
--- a/src/components/moments/Moment.tsx
+++ b/src/components/moments/Moment.tsx
@@ -6,7 +6,7 @@ import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler';
import LinearGradient from 'react-native-linear-gradient';
import PlusIcon from '../../assets/icons/plus_icon-01.svg';
import BigPlusIcon from '../../assets/icons/plus_icon-02.svg';
-import {MOMENTS_TITLE_COLOR} from '../../constants';
+import {TAGG_TEXT_LIGHT_BLUE} from '../../constants';
import {SCREEN_WIDTH} from '../../utils';
import ImagePicker from 'react-native-image-crop-picker';
import MomentTile from './MomentTile';
@@ -104,7 +104,7 @@ const styles = StyleSheet.create({
titleText: {
fontSize: 16,
fontWeight: 'bold',
- color: MOMENTS_TITLE_COLOR,
+ color: TAGG_TEXT_LIGHT_BLUE,
},
scrollContainer: {
height: SCREEN_WIDTH / 2,
diff --git a/src/components/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx
index f97f1a72..0fc597c3 100644
--- a/src/components/onboarding/BirthDatePicker.tsx
+++ b/src/components/onboarding/BirthDatePicker.tsx
@@ -15,6 +15,8 @@ import {TaggDatePicker} from '../common';
interface BirthDatePickerProps extends TextInputProps {
handleBDUpdate: (_: Date) => void;
width?: number | string;
+ date: Date | undefined;
+ showPresetdate: boolean;
}
const BirthDatePicker = React.forwardRef(
@@ -23,7 +25,7 @@ const BirthDatePicker = React.forwardRef(
const maxDate = moment().subtract(13, 'y').subtract(1, 'd');
return maxDate.toDate();
};
- const [date, setDate] = useState(new Date(0));
+ const [date, setDate] = useState(props.date);
const [hidden, setHidden] = useState(true);
const [updated, setUpdated] = useState(false);
const textColor = updated ? 'white' : '#ddd';
@@ -42,7 +44,9 @@ const BirthDatePicker = React.forwardRef(
style={[styles.input, {width: props.width}, {color: textColor}]}
ref={ref}
{...props}>
- {updated ? moment(date).format('YYYY-MM-DD') : 'Date of Birth'}
+ {(updated || props.showPresetdate) && date
+ ? moment(date).format('YYYY-MM-DD')
+ : 'Date of Birth'}
</Text>
</TouchableOpacity>
<Modal visible={!hidden} transparent={true} animationType="fade">
@@ -67,6 +71,7 @@ const BirthDatePicker = React.forwardRef(
handleDateUpdate={updateDate}
maxDate={getMaxDate()}
textColor={'black'}
+ date={date}
/>
</View>
</TouchableWithoutFeedback>
diff --git a/src/components/onboarding/TaggDropDown.tsx b/src/components/onboarding/TaggDropDown.tsx
index a45426ca..db531cc4 100644
--- a/src/components/onboarding/TaggDropDown.tsx
+++ b/src/components/onboarding/TaggDropDown.tsx
@@ -1,8 +1,8 @@
import React from 'react';
-import RNSelectPicker from 'react-native-picker-select';
-import {View, StyleSheet, TextInputProps} from 'react-native';
+import {StyleSheet, View} from 'react-native';
+import RNSelectPicker, {PickerSelectProps} from 'react-native-picker-select';
-interface TaggDropDownProps extends TextInputProps {
+interface TaggDropDownProps extends PickerSelectProps {
width?: number | string;
}
@@ -19,7 +19,6 @@ const TaggDropDown = React.forwardRef((props: TaggDropDownProps, ref: any) => {
const styles = StyleSheet.create({
container: {
- width: '66.67%',
alignItems: 'center',
marginVertical: 11,
},
diff --git a/src/components/profile/MoreInfoDrawer.tsx b/src/components/profile/MoreInfoDrawer.tsx
new file mode 100644
index 00000000..719c1894
--- /dev/null
+++ b/src/components/profile/MoreInfoDrawer.tsx
@@ -0,0 +1,88 @@
+import {useNavigation} from '@react-navigation/native';
+import React, {useContext} from 'react';
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
+import {TAGG_TEXT_LIGHT_BLUE} from '../../constants';
+import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+import {AuthContext} from '../../routes';
+import {BottomDrawer} from '../common';
+import PersonOutline from '../../assets/ionicons/person-outline.svg';
+
+interface MoreInfoDrawerProps {
+ isOpen: boolean;
+ setIsOpen: (visible: boolean) => void;
+ isProfileView: boolean;
+}
+
+const MoreInfoDrawer: React.FC<MoreInfoDrawerProps> = (props) => {
+ const insets = useSafeAreaInsets();
+ const initialSnapPosition = 160 + insets.bottom;
+ const navigation = useNavigation();
+ const {
+ user: {userId, username},
+ } = useContext(AuthContext);
+
+ const goToEditProfile = () => {
+ navigation.push('EditProfile', {
+ userId: userId,
+ username: username,
+ });
+ props.setIsOpen(false);
+ };
+
+ return (
+ <BottomDrawer
+ {...props}
+ showHeader={false}
+ initialSnapPosition={initialSnapPosition}>
+ <View style={styles.panel}>
+ <TouchableOpacity style={styles.panelButton} onPress={goToEditProfile}>
+ <PersonOutline style={styles.icon} />
+ <Text style={styles.panelButtonTitle}>Edit Profile</Text>
+ </TouchableOpacity>
+ <View style={styles.divider} />
+ <TouchableOpacity
+ style={styles.panelButton}
+ onPress={() => props.setIsOpen(false)}>
+ {/* Just a placeholder "icon" for easier alignment */}
+ <View style={styles.icon} />
+ <Text style={styles.panelButtonTitleCancel}>Cancel</Text>
+ </TouchableOpacity>
+ </View>
+ </BottomDrawer>
+ );
+};
+
+const styles = StyleSheet.create({
+ panel: {
+ height: SCREEN_HEIGHT,
+ backgroundColor: 'white',
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
+ },
+ panelButton: {
+ height: 80,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ panelButtonTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: 'black',
+ },
+ icon: {
+ height: 25,
+ width: 25,
+ color: 'black',
+ marginLeft: SCREEN_WIDTH * 0.3,
+ marginRight: 25,
+ },
+ panelButtonTitleCancel: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: TAGG_TEXT_LIGHT_BLUE,
+ },
+ divider: {height: 1, borderWidth: 1, borderColor: '#e7e7e7'},
+});
+
+export default MoreInfoDrawer;
diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx
index db8c6e0b..c0253533 100644
--- a/src/components/profile/ProfileBody.tsx
+++ b/src/components/profile/ProfileBody.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {StyleSheet, View, Text, LayoutChangeEvent} from 'react-native';
-import {TOGGLE_BUTTON_TYPE} from '../../constants';
+import {TAGG_DARK_BLUE, TOGGLE_BUTTON_TYPE} from '../../constants';
import {AuthContext, ProfileContext} from '../../routes/';
import ToggleButton from './ToggleButton';
@@ -80,7 +80,7 @@ const styles = StyleSheet.create({
},
website: {
fontSize: 16,
- color: '#4E699C',
+ color: TAGG_DARK_BLUE,
marginBottom: 5,
},
});
diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx
index 6f11e806..62949746 100644
--- a/src/components/profile/ProfileHeader.tsx
+++ b/src/components/profile/ProfileHeader.tsx
@@ -1,10 +1,12 @@
-import React from 'react';
-
+import React, {useState} from 'react';
+import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
+import MoreIcon from '../../assets/icons/more_horiz-24px.svg';
+import {TAGG_DARK_BLUE} from '../../constants';
+import {AuthContext, ProfileContext} from '../../routes/';
+import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
import Avatar from './Avatar';
+import MoreInfoDrawer from './MoreInfoDrawer';
import FollowCount from './FollowCount';
-import {View, Text, StyleSheet} from 'react-native';
-import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
-import {AuthContext, ProfileContext} from '../../routes/';
type ProfileHeaderProps = {
isProfileView: boolean;
@@ -22,8 +24,26 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
} = isProfileView
? React.useContext(ProfileContext)
: React.useContext(AuthContext);
+ const [drawerVisible, setDrawerVisible] = useState(false);
+
return (
<View style={styles.container}>
+ {!isProfileView && (
+ <>
+ <TouchableOpacity
+ style={styles.more}
+ onPress={() => {
+ setDrawerVisible(true);
+ }}>
+ <MoreIcon height={30} width={30} color={TAGG_DARK_BLUE} />
+ </TouchableOpacity>
+ <MoreInfoDrawer
+ isOpen={drawerVisible}
+ setIsOpen={setDrawerVisible}
+ isProfileView={isProfileView}
+ />
+ </>
+ )}
<View style={styles.row}>
<Avatar style={styles.avatar} isProfileView={isProfileView} />
<View style={styles.header}>
@@ -51,8 +71,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({
const styles = StyleSheet.create({
container: {
top: SCREEN_HEIGHT / 2.4,
- paddingHorizontal: SCREEN_WIDTH / 20,
- marginBottom: SCREEN_HEIGHT / 10,
+ width: '100%',
position: 'absolute',
},
row: {
@@ -76,6 +95,12 @@ const styles = StyleSheet.create({
follows: {
marginHorizontal: SCREEN_HEIGHT / 50,
},
+ more: {
+ position: 'absolute',
+ right: '5%',
+ marginTop: '4%',
+ zIndex: 1,
+ },
});
export default ProfileHeader;
diff --git a/src/components/profile/ToggleButton.tsx b/src/components/profile/ToggleButton.tsx
index ff14cdde..4c8cb5b9 100644
--- a/src/components/profile/ToggleButton.tsx
+++ b/src/components/profile/ToggleButton.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import {StyleSheet, Text} from 'react-native';
import {TouchableOpacity} from 'react-native-gesture-handler';
+import { TAGG_TEXT_LIGHT_BLUE } from '../../constants';
import {getToggleButtonText, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
type ToggleButtonProps = {
@@ -31,14 +32,14 @@ const styles = StyleSheet.create({
height: SCREEN_WIDTH * 0.1,
borderRadius: 8,
marginTop: '3%',
- borderColor: '#698dd3',
+ borderColor: TAGG_TEXT_LIGHT_BLUE,
backgroundColor: 'white',
borderWidth: 3,
marginHorizontal: '1%',
},
text: {
fontWeight: 'bold',
- color: '#698dd3',
+ color: TAGG_TEXT_LIGHT_BLUE,
},
});
export default ToggleButton;
diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts
index 2e9c23ea..0f57347b 100644
--- a/src/components/profile/index.ts
+++ b/src/components/profile/index.ts
@@ -3,5 +3,6 @@ export {default as Content} from './Content';
export {default as ProfileCutout} from './ProfileCutout';
export {default as ProfileBody} from './ProfileBody';
export {default as ProfileHeader} from './ProfileHeader';
-export {default as ProfilePreview} from '../profile/ProfilePreview';
-export {default as Followers} from '../profile/Followers';
+export {default as ProfilePreview} from './ProfilePreview';
+export {default as Followers} from './Followers';
+export {default as MoreInfoDrawer} from './MoreInfoDrawer';
diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx
index a5c08984..f47f8879 100644
--- a/src/components/search/RecentSearches.tsx
+++ b/src/components/search/RecentSearches.tsx
@@ -7,6 +7,7 @@ import {
TouchableOpacityProps,
} from 'react-native';
import {ProfilePreviewType} from 'src/types';
+import { TAGG_TEXT_LIGHT_BLUE } from '../../constants';
import SearchResults from './SearchResults';
interface RecentSearchesProps extends TouchableOpacityProps {
@@ -45,7 +46,7 @@ const styles = StyleSheet.create({
clear: {
fontSize: 17,
fontWeight: 'bold',
- color: '#698DD3',
+ color: TAGG_TEXT_LIGHT_BLUE,
},
});
diff --git a/src/components/taggs/TwitterTaggPost.tsx b/src/components/taggs/TwitterTaggPost.tsx
index 158b5995..fb4cbd0f 100644
--- a/src/components/taggs/TwitterTaggPost.tsx
+++ b/src/components/taggs/TwitterTaggPost.tsx
@@ -3,7 +3,7 @@ import {Image, Linking, StyleSheet, View} from 'react-native';
import {Text} from 'react-native-animatable';
import Hyperlink from 'react-native-hyperlink';
import LinearGradient from 'react-native-linear-gradient';
-import {AVATAR_DIM, TAGGS_GRADIENT} from '../../constants';
+import {AVATAR_DIM, TAGGS_GRADIENT, TAGG_TEXT_LIGHT_BLUE} from '../../constants';
import {TwitterPostType} from '../../types';
import {SCREEN_WIDTH} from '../../utils';
import {DateLabel, PostCarousel} from '../common';
@@ -238,7 +238,7 @@ const styles = StyleSheet.create({
},
replyShowThisThread: {
fontSize: 15,
- color: '#698dd3',
+ color: TAGG_TEXT_LIGHT_BLUE,
},
});
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index dbd79b45..c2003fb4 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -1,5 +1,7 @@
import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils';
+export const CHIN_HEIGHT = 34;
+
export const PROFILE_CUTOUT_TOP_Y = SCREEN_HEIGHT / 2.3;
export const PROFILE_CUTOUT_BOTTOM_Y = SCREEN_HEIGHT / 1.8;
export const PROFILE_CUTOUT_CORNER_X = SCREEN_WIDTH / 2.9;
@@ -46,6 +48,9 @@ export const LINKEDIN_FONT_COLOR: string = '#78B5FD';
export const SNAPCHAT_FONT_COLOR: string = '#FFFC00';
export const YOUTUBE_FONT_COLOR: string = '#FCA4A4';
+export const TAGG_DARK_BLUE = '#4E699C';
+export const TAGG_TEXT_LIGHT_BLUE: string = '#698DD3';
+
export const TAGGS_GRADIENT = {
start: '#9F00FF',
end: '#27EAE9',
@@ -81,5 +86,3 @@ export const defaultMoments: Array<string> = [
'Creativity',
'Activity',
];
-
-export const MOMENTS_TITLE_COLOR: string = '#698DD3';
diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx
index 7da47b71..7046d04f 100644
--- a/src/routes/authentication/AuthProvider.tsx
+++ b/src/routes/authentication/AuthProvider.tsx
@@ -40,6 +40,8 @@ interface AuthContextProps {
blockedUsers: ProfilePreviewType[];
blockedUsersNeedUpdate: boolean;
updateBlockedUsers: (value: boolean) => void;
+ isEditedProfile: boolean;
+ updateIsEditedProfile: (value: boolean) => void;
}
const NO_USER: UserType = {
@@ -51,6 +53,8 @@ const NO_PROFILE: ProfileType = {
biography: '',
website: '',
name: '',
+ gender: '',
+ birthday: undefined,
};
const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = {
@@ -79,6 +83,8 @@ export const AuthContext = createContext<AuthContextProps>({
blockedUsers: [],
blockedUsersNeedUpdate: true,
updateBlockedUsers: () => {},
+ isEditedProfile: false,
+ updateIsEditedProfile: () => {},
});
/**
@@ -110,6 +116,7 @@ const AuthProvider: React.FC = ({children}) => {
const [blockedUsersNeedUpdate, setBlockedUsersNeedUpdate] = useState<boolean>(
true,
);
+ const [isEditedProfile, setIsEditedProfile] = useState<boolean>(false);
const {userId} = user;
useEffect(() => {
@@ -149,7 +156,7 @@ const AuthProvider: React.FC = ({children}) => {
}
};
loadData();
- }, [userId]);
+ }, [userId, isEditedProfile]);
useEffect(() => {
const loadNewMoments = async () => {
@@ -245,6 +252,7 @@ const AuthProvider: React.FC = ({children}) => {
followersNeedUpdate,
blockedUsers,
blockedUsersNeedUpdate,
+ isEditedProfile,
login: (id, username) => {
setUser({...user, userId: id, username});
},
@@ -274,6 +282,9 @@ const AuthProvider: React.FC = ({children}) => {
updateBlockedUsers: (value) => {
setBlockedUsersNeedUpdate(value);
},
+ updateIsEditedProfile: (value: boolean) => {
+ setIsEditedProfile(value);
+ },
}}>
{children}
</AuthContext.Provider>
diff --git a/src/routes/profile/Profile.tsx b/src/routes/profile/Profile.tsx
index bffa22ce..b6672c85 100644
--- a/src/routes/profile/Profile.tsx
+++ b/src/routes/profile/Profile.tsx
@@ -7,6 +7,7 @@ import {
ProfileScreen,
MomentCommentsScreen,
FollowersListScreen,
+ EditProfile,
} from '../../screens';
import {ProfileStack, ProfileStackParams} from './ProfileStack';
import {RouteProp} from '@react-navigation/native';
@@ -99,6 +100,17 @@ const Profile: React.FC<ProfileStackProps> = ({route}) => {
component={FollowersListScreen}
initialParams={{isProfileView: isProfileView}}
/>
+ <ProfileStack.Screen
+ name="EditProfile"
+ component={EditProfile}
+ options={{
+ headerShown: true,
+ headerTitle: 'Edit Profile',
+ headerTransparent: true,
+ headerBackTitleVisible: false,
+ headerTintColor: 'white',
+ }}
+ />
</ProfileStack.Navigator>
);
};
diff --git a/src/routes/profile/ProfileStack.tsx b/src/routes/profile/ProfileStack.tsx
index b1e86214..5590f78a 100644
--- a/src/routes/profile/ProfileStack.tsx
+++ b/src/routes/profile/ProfileStack.tsx
@@ -33,6 +33,10 @@ export type ProfileStackParams = {
isFollowers: boolean;
list: ProfilePreviewType[];
};
+ EditProfile: {
+ userId: boolean;
+ username: ProfilePreviewType[];
+ };
};
export const ProfileStack = createStackNavigator<ProfileStackParams>();
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<ProfileOnboardingProps> = ({
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<ProfileOnboardingProps> = ({
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<ProfileOnboardingProps> = ({
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<ProfileOnboardingProps> = ({
<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}
- />
- <BirthDatePicker
- ref={birthdateRef}
- handleBDUpdate={handleBirthdateUpdate}
- width={280}
- />
- <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 && (
+ <View style={styles.contentContainer}>
<TaggInput
- accessibilityHint="Custom"
- accessibilityLabel="Gender input field."
- placeholder="Enter your gender"
+ 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={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}
/>
- )}
- <TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}>
- <Text style={styles.submitBtnLabel}>Let's start!</Text>
- </TouchableOpacity>
+ <BirthDatePicker
+ ref={birthdateRef}
+ handleBDUpdate={handleBirthdateUpdate}
+ width={280}
+ date={form.birthdate}
+ showPresetdate={false}
+ />
+ <TaggDropDown
+ onValueChange={(value: string) => 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>
+ </View>
</Background>
);
};
@@ -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<ProfileOnboardingProps> = ({
+ route,
+ navigation,
+}) => {
+ const y: Animated.Value<number> = 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<boolean>(
+ 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 = () => (
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD LARGE PROFILE PIC HERE"
+ onPress={goToGalleryLargePic}
+ style={styles.largeProfileUploader}>
+ {form.largePic ? (
+ <Image source={{uri: form.largePic}} style={styles.largeProfilePic} />
+ ) : (
+ <Text style={styles.largeProfileText}>ADD LARGE PROFILE PIC HERE</Text>
+ )}
+ </TouchableOpacity>
+ );
+
+ /**
+ * Profile screen "Add Smaller Profile Pic Here" button
+ */
+ const SmallProfilePic = () => (
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD SMALLER PIC"
+ onPress={goToGallerySmallPic}
+ style={styles.smallProfileUploader}>
+ {form.smallPic ? (
+ <Image source={{uri: form.smallPic}} style={styles.smallProfilePic} />
+ ) : (
+ <Text style={styles.smallProfileText}>ADD SMALLER PIC</Text>
+ )}
+ </TouchableOpacity>
+ );
+
+ /*
+ * 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: () => (
+ <Button
+ title={'Save'}
+ buttonStyle={{backgroundColor: 'transparent'}}
+ titleStyle={{fontWeight: 'bold'}}
+ onPress={handleSubmit}
+ />
+ ),
+ });
+ }, [navigation, handleSubmit]);
+
+ return (
+ <Background centered>
+ <SafeAreaView>
+ <Animated.ScrollView
+ style={styles.container}
+ onScroll={(e) => y.setValue(e.nativeEvent.contentOffset.y)}
+ showsHorizontalScrollIndicator={false}
+ showsVerticalScrollIndicator={true}
+ scrollEventThrottle={1}
+ alwaysBounceVertical
+ contentContainerStyle={{paddingBottom: SCREEN_HEIGHT / 15}}>
+ <StatusBar barStyle="light-content" translucent={false} />
+ <View
+ style={{
+ position: 'relative',
+ alignSelf: 'center',
+ }}>
+ <View>
+ <View style={styles.profile}>
+ <LargeProfilePic />
+ <SmallProfilePic />
+ </View>
+ <View
+ style={{
+ position: 'relative',
+ width: 280,
+ alignSelf: 'center',
+ }}>
+ <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}
+ value={form.website}
+ />
+ <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}
+ value={form.bio}
+ />
+ <BirthDatePicker
+ ref={birthdateRef}
+ handleBDUpdate={handleBirthdateUpdate}
+ width={280}
+ date={form.birthdate}
+ showPresetdate={true}
+ />
+
+ <TaggDropDown
+ value={form.gender}
+ onValueChange={(value: string) => handleGenderUpdate(value)}
+ items={[
+ {label: 'Male', value: 'male'},
+ {label: 'Female', value: 'female'},
+ {label: 'Custom', value: 'custom'},
+ ]}
+ placeholder={{
+ label: 'Gender',
+ value: null,
+ color: '#fff',
+ }}
+ />
+ {isCustomGender && (
+ <TaggInput
+ style={styles.customGenderInput}
+ value={form.customGenderText}
+ 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'
+ }
+ />
+ )}
+ </View>
+ </View>
+ </View>
+ </Animated.ScrollView>
+ </SafeAreaView>
+ <TabsGradient />
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ marginTop: '10%',
+ flex: 1,
+ flexDirection: 'column',
+ width: '100%',
+ },
+ profile: {
+ flexDirection: 'row',
+ marginBottom: '5%',
+ justifyContent: 'flex-end',
+ },
+ largeProfileUploader: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 15,
+ backgroundColor: '#fff',
+ marginLeft: '13%',
+ marginTop: '5%',
+ height: 230,
+ width: 230,
+ borderRadius: 23,
+ },
+ largeProfileText: {
+ textAlign: 'center',
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: '#863FF9',
+ },
+ largeProfilePic: {
+ height: 230,
+ width: 230,
+ borderRadius: 23,
+ },
+ smallProfileUploader: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ backgroundColor: '#E1F0FF',
+ right: '18%',
+ marginTop: '38%',
+ height: 110,
+ width: 110,
+ borderRadius: 55,
+ },
+ smallProfileText: {
+ textAlign: 'center',
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: '#806DF4',
+ },
+ smallProfilePic: {
+ height: 110,
+ width: 110,
+ borderRadius: 55,
+ },
+ submitBtn: {
+ backgroundColor: '#8F01FF',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 150,
+ height: 40,
+ borderRadius: 5,
+ marginTop: '5%',
+ },
+ submitBtnLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#fff',
+ },
+ customGenderInput: {
+ width: '100%',
+ height: 40,
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#fff',
+ borderColor: '#fffdfd',
+ borderWidth: 2,
+ borderRadius: 20,
+ paddingLeft: 13,
+ },
+});
+
+export default ProfileOnboarding;
diff --git a/src/screens/profile/MomentCommentsScreen.tsx b/src/screens/profile/MomentCommentsScreen.tsx
index 0d685ad1..7a0bfa66 100644
--- a/src/screens/profile/MomentCommentsScreen.tsx
+++ b/src/screens/profile/MomentCommentsScreen.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import {RouteProp, useNavigation} from '@react-navigation/native';
import {ProfileStackParams} from '../../routes/profile';
-import {CenteredView, CommentTile, OverlayView} from '../../components';
+import {CenteredView, CommentTile} from '../../components';
import {CommentType} from '../../types';
import {ScrollView, StyleSheet, Text, View} from 'react-native';
import {SCREEN_WIDTH} from '../../utils/screenDimensions';
diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx
index 9579a696..d32eca98 100644
--- a/src/screens/profile/ProfileScreen.tsx
+++ b/src/screens/profile/ProfileScreen.tsx
@@ -1,9 +1,9 @@
-import React from 'react';
+import React, {useContext, useEffect} from 'react';
import {StatusBar} from 'react-native';
import Animated from 'react-native-reanimated';
import {Content, Cover, TabsGradient} from '../../components';
import {RouteProp} from '@react-navigation/native';
-import {ProfileStackParams, ProfileProvider} from '../../routes/';
+import {ProfileStackParams, ProfileProvider, AuthContext} from '../../routes/';
/**
* Profile Screen for a user's profile
@@ -19,6 +19,11 @@ interface ProfileOnboardingProps {
const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => {
const {isProfileView, username, userId} = route.params;
const y = Animated.useValue(0);
+ const {updateIsEditedProfile} = useContext(AuthContext);
+
+ useEffect(() => {
+ updateIsEditedProfile(false);
+ });
const profileView = () => {
return (
diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts
index c2bd4c4b..3bfe5d30 100644
--- a/src/screens/profile/index.ts
+++ b/src/screens/profile/index.ts
@@ -4,3 +4,4 @@ export {default as CaptionScreen} from './CaptionScreen';
export {default as IndividualMoment} from './IndividualMoment';
export {default as MomentCommentsScreen} from './MomentCommentsScreen';
export {default as FollowersListScreen} from './FollowersListScreen';
+export {default as EditProfile} from './EditProfile';
diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx
index f528358a..aef3d0a8 100644
--- a/src/screens/search/SearchScreen.tsx
+++ b/src/screens/search/SearchScreen.tsx
@@ -19,7 +19,7 @@ import {
SearchResultsBackground,
TabsGradient,
} from '../../components';
-import {SEARCH_ENDPOINT} from '../../constants';
+import {SEARCH_ENDPOINT, TAGG_TEXT_LIGHT_BLUE} from '../../constants';
import {AuthContext} from '../../routes/authentication';
import {ProfilePreviewType, UserType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
@@ -191,7 +191,7 @@ const styles = StyleSheet.create({
clear: {
fontSize: 17,
fontWeight: 'bold',
- color: '#698DD3',
+ color: TAGG_TEXT_LIGHT_BLUE,
},
image: {
width: SCREEN_WIDTH,
diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts
index 6e1d1ef5..38e04221 100644
--- a/src/services/UserProfileService.ts
+++ b/src/services/UserProfileService.ts
@@ -1,6 +1,7 @@
//Abstracted common profile api calls out here
import AsyncStorage from '@react-native-community/async-storage';
+import moment from 'moment';
import {Alert} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import {SocialAccountType, MomentType} from 'src/types';
@@ -29,8 +30,10 @@ export const loadProfileInfo = async (
const status = response.status;
if (status === 200) {
const info = await response.json();
- let {name, biography, website} = info;
- callback({name, biography, website});
+ let {name, biography, website, birthday, gender} = info;
+ // user should always have a birthday, but a safety check here
+ birthday = birthday && moment(birthday).format('YYYY-MM-DD');
+ callback({name, biography, website, birthday, gender});
}
} catch (error) {
Alert.alert(
diff --git a/src/types/types.ts b/src/types/types.ts
index 8b55e113..8336919e 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -17,6 +17,8 @@ export interface ProfileType {
name: string;
biography: string;
website: string;
+ gender: string;
+ birthday: Date | undefined;
}
export interface SocialAccountType {