aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/assets/gifs/dotted-arrow-white.gifbin0 -> 11793 bytes
-rw-r--r--src/assets/icons/celebration-logo.svg1
-rw-r--r--src/assets/icons/down_icon.svg1
-rw-r--r--src/assets/icons/plus_icon-01.svg2
-rw-r--r--src/assets/icons/up_icon.svg1
-rw-r--r--src/assets/moment-categories/custom-icon.pngbin0 -> 4257 bytes
-rw-r--r--src/components/comments/CommentsCount.tsx2
-rw-r--r--src/components/common/TaggPopup.tsx19
-rw-r--r--src/components/common/TaggPrompt.tsx79
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/moments/Moment.tsx86
-rw-r--r--src/components/notifications/Notification.tsx42
-rw-r--r--src/components/onboarding/MomentCategory.tsx58
-rw-r--r--src/components/onboarding/TaggBigInput.tsx3
-rw-r--r--src/components/onboarding/TaggInput.tsx3
-rw-r--r--src/components/onboarding/TermsAndConditionsText.tsx13
-rw-r--r--src/components/profile/Content.tsx128
-rw-r--r--src/components/profile/ProfilePreview.tsx19
-rw-r--r--src/components/search/Explore.tsx35
-rw-r--r--src/components/search/ExploreSection.tsx26
-rw-r--r--src/components/search/ExploreSectionUser.tsx81
-rw-r--r--src/constants/api.ts2
-rw-r--r--src/constants/constants.ts33
-rw-r--r--src/routes/main/MainStackNavigator.tsx18
-rw-r--r--src/routes/main/MainStackScreen.tsx44
-rw-r--r--src/routes/onboarding/OnboardingStackNavigator.tsx (renamed from src/routes/onboarding/OnboardingStack.tsx)10
-rw-r--r--src/routes/onboarding/OnboardingStackScreen.tsx (renamed from src/routes/onboarding/Onboarding.tsx)12
-rw-r--r--src/routes/onboarding/index.ts4
-rw-r--r--src/routes/tabs/NavigationBar.tsx6
-rw-r--r--src/screens/main/NotificationsScreen.tsx9
-rw-r--r--src/screens/onboarding/AddWaitlistUserScreen.tsx238
-rw-r--r--src/screens/onboarding/CategorySelection.tsx202
-rw-r--r--src/screens/onboarding/CreateCustomCategory.tsx123
-rw-r--r--src/screens/onboarding/InvitationCodeVerification.tsx29
-rw-r--r--src/screens/onboarding/Login.tsx15
-rw-r--r--src/screens/onboarding/ProfileOnboarding.tsx52
-rw-r--r--src/screens/onboarding/SocialMedia.tsx8
-rw-r--r--src/screens/onboarding/WaitlistSuccessScreen.tsx156
-rw-r--r--src/screens/onboarding/index.ts3
-rw-r--r--src/screens/profile/CaptionScreen.tsx64
-rw-r--r--src/screens/profile/EditProfile.tsx52
-rw-r--r--src/screens/profile/MomentUploadPromptScreen.tsx114
-rw-r--r--src/screens/profile/ProfileScreen.tsx4
-rw-r--r--src/screens/profile/index.ts1
-rw-r--r--src/screens/search/SearchScreen.tsx51
-rw-r--r--src/services/ExploreServices.ts34
-rw-r--r--src/services/MomentCategoryService.ts53
-rw-r--r--src/services/MomentServices.ts52
-rw-r--r--src/services/UserProfileService.ts2
-rw-r--r--src/services/WaitlistUserService.tsx45
-rw-r--r--src/services/index.ts1
-rw-r--r--src/store/actions/momentCategories.tsx37
-rw-r--r--src/store/actions/taggUsers.ts10
-rw-r--r--src/store/actions/user.ts39
-rw-r--r--src/store/initialStates.ts40
-rw-r--r--src/store/reducers/momentCategoryReducer.tsx9
-rw-r--r--src/store/reducers/taggUsersReducer.ts4
-rw-r--r--src/store/reducers/userReducer.ts10
-rw-r--r--src/store/reducers/userXReducer.ts12
-rw-r--r--src/types/types.ts31
-rw-r--r--src/utils/common.ts25
-rw-r--r--src/utils/users.ts5
-rw-r--r--tsconfig.json2
63 files changed, 1808 insertions, 453 deletions
diff --git a/src/assets/gifs/dotted-arrow-white.gif b/src/assets/gifs/dotted-arrow-white.gif
new file mode 100644
index 00000000..a3f0a153
--- /dev/null
+++ b/src/assets/gifs/dotted-arrow-white.gif
Binary files differ
diff --git a/src/assets/icons/celebration-logo.svg b/src/assets/icons/celebration-logo.svg
new file mode 100644
index 00000000..5e4e89a0
--- /dev/null
+++ b/src/assets/icons/celebration-logo.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-miterlimit:10;stroke-width:16px;}.cls-1{stroke-linecap:round;}</style></defs><path class="cls-1" d="M204.9,167.55,28.36,646.4S1.75,738.3,98.49,728.63l474-241.84s12.09-65.3-166.87-208c0,0-128.76-110.7-167.1-104.59s-.72,56.67-.72,56.67"/><path class="cls-1" d="M229.34,306.39S331,523.27,447.9,550.36"/><path class="cls-1" d="M253.14,60.8S384.68,150,315.57,321.65"/><line class="cls-1" x1="360.41" y1="346.18" x2="427.04" y2="228.01"/><line class="cls-1" x1="427.04" y1="339.49" x2="624.4" y2="216.5"/><path class="cls-1" d="M460.48,377.39s165-42.71,251.94,19.89"/><path class="cls-1" d="M509.83,448.45s181.48,37.33,143.76,85.09-68.67-73.61-23.17-88S809.22,484.4,768,513.07s-61.64-91.86-25.16-106.5"/><path class="cls-1" d="M102.2,292.31s61.68-84.89,0-93.63S2.53,214.54,42.88,161.89s57-50.28,11.87-55-9.49-64.09-9.49-64.09"/><rect class="cls-2" x="332.02" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(204.55 -229.35) rotate(49.23)"/><rect class="cls-2" x="540.13" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(276.77 -386.97) rotate(49.23)"/><rect class="cls-2" x="439.94" y="157.42" width="41.09" height="41.09" rx="7.51" transform="translate(518.03 -312.35) rotate(75.56)"/><rect class="cls-2" x="170.83" y="88.01" width="41.09" height="41.09" rx="7.51" transform="translate(148.62 -107.27) rotate(49.23)"/><rect class="cls-2" x="594.88" y="556.9" width="41.09" height="41.09" rx="7.51" transform="translate(250.85 -183.66) rotate(21.23)"/><rect class="cls-2" x="464.79" y="561.95" width="41.09" height="41.09" rx="7.51" transform="translate(686.08 -148.64) rotate(55.15)"/><rect class="cls-2" x="57.36" y="386.02" width="41.09" height="41.09" rx="7.51" transform="translate(275.76 42.94) rotate(39.4)"/><path class="cls-1" d="M128.59,421.33s31.27,145.46,170.25,203"/><path class="cls-1" d="M77.91,544.6s9.62,149.81,65.73,159.48"/></svg> \ No newline at end of file
diff --git a/src/assets/icons/down_icon.svg b/src/assets/icons/down_icon.svg
new file mode 100644
index 00000000..1c3ed71a
--- /dev/null
+++ b/src/assets/icons/down_icon.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1{fill:#6c8dc8;}.cls-2{fill:none;stroke:#6c8dc8;stroke-miterlimit:10;stroke-width:47.9px;}</style></defs><path class="cls-1" d="M522.4,416.32l-18.18-16.86a13.07,13.07,0,0,0-18.47.68l-62.59,67.45V248.64a22,22,0,0,0-22-22h-8.21a22,22,0,0,0-22,22V470.19l-65-70.05a13.11,13.11,0,0,0-18.5-.68l-18.18,16.86a13.1,13.1,0,0,0-.68,18.51l96.51,104,20.39,22,1.52,1.4a14,14,0,0,0,2.52,1.84,14.37,14.37,0,0,0,2.85,1.17,2.18,2.18,0,0,0,.88.2,6.27,6.27,0,0,0,1.2.2,11.3,11.3,0,0,0,1.16.08H396a11.3,11.3,0,0,0,1.16-.08,6.68,6.68,0,0,0,1.29-.2,4.48,4.48,0,0,0,.92-.24c.48-.12.92-.32,1.4-.48l1-.49a13.55,13.55,0,0,0,2.84-2l1.53-1.4,20.42-22,96.48-104A13.11,13.11,0,0,0,522.4,416.32Z"/><circle class="cls-2" cx="395.82" cy="396.18" r="369.73"/></svg> \ No newline at end of file
diff --git a/src/assets/icons/plus_icon-01.svg b/src/assets/icons/plus_icon-01.svg
index 32632897..7a3b21d2 100644
--- a/src/assets/icons/plus_icon-01.svg
+++ b/src/assets/icons/plus_icon-01.svg
@@ -1 +1 @@
-<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke:#718dc3;stroke-miterlimit:10;stroke-width:11px;}.cls-2{fill:#718dc3;}</style></defs><circle class="cls-1" cx="108" cy="108" r="84.9"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)"/></svg> \ No newline at end of file
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke-miterlimit:10;stroke-width:11px;}</style></defs><circle stroke="currentColor" class="cls-1" cx="108" cy="108" r="84.9" /><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" fill="currentColor"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)" fill="currentColor" /></svg> \ No newline at end of file
diff --git a/src/assets/icons/up_icon.svg b/src/assets/icons/up_icon.svg
new file mode 100644
index 00000000..20773a8a
--- /dev/null
+++ b/src/assets/icons/up_icon.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 792 792"><defs><style>.cls-1{fill:#6c8dc8;}.cls-2{fill:none;stroke:#718cc2;stroke-miterlimit:10;stroke-width:47.9px;}</style></defs><path class="cls-1" d="M522.58,375.86l-18.19,16.86a13.06,13.06,0,0,1-18.46-.68l-62.6-67.45v219a22,22,0,0,1-22,22H393.1a22,22,0,0,1-22-22V322l-65,70.05a13.11,13.11,0,0,1-18.5.68l-18.19-16.86a13.12,13.12,0,0,1-.68-18.51l96.52-104,20.39-22,1.52-1.4a13.6,13.6,0,0,1,2.52-1.85,14.44,14.44,0,0,1,2.85-1.16,2.18,2.18,0,0,1,.88-.2,6.27,6.27,0,0,1,1.2-.2,11.3,11.3,0,0,1,1.16-.08h.44a11.3,11.3,0,0,1,1.16.08,6.58,6.58,0,0,1,1.28.2,4.48,4.48,0,0,1,.92.24c.48.12.93.32,1.41.48l1,.48a13.24,13.24,0,0,1,2.84,2l1.52,1.4,20.43,22,96.48,104A13.12,13.12,0,0,1,522.58,375.86Z"/><circle class="cls-2" cx="396" cy="396" r="369.73"/></svg> \ No newline at end of file
diff --git a/src/assets/moment-categories/custom-icon.png b/src/assets/moment-categories/custom-icon.png
new file mode 100644
index 00000000..f81546d6
--- /dev/null
+++ b/src/assets/moment-categories/custom-icon.png
Binary files differ
diff --git a/src/components/comments/CommentsCount.tsx b/src/components/comments/CommentsCount.tsx
index 325e2788..f4f8197d 100644
--- a/src/components/comments/CommentsCount.tsx
+++ b/src/components/comments/CommentsCount.tsx
@@ -30,7 +30,7 @@ const CommentsCount: React.FC<CommentsCountProps> = ({
};
return (
<>
- <TouchableOpacity onPress={() => navigateToCommentsScreen()}>
+ <TouchableOpacity onPress={navigateToCommentsScreen}>
<CommentIcon style={styles.image} />
<Text style={styles.count}>
{commentsCount !== '0' ? commentsCount : ''}
diff --git a/src/components/common/TaggPopup.tsx b/src/components/common/TaggPopup.tsx
index db24adb8..86a472b1 100644
--- a/src/components/common/TaggPopup.tsx
+++ b/src/components/common/TaggPopup.tsx
@@ -31,7 +31,11 @@ const TaggPopup: React.FC<TaggPopupProps> = ({route, navigation}) => {
const {messageHeader, messageBody, next} = route.params.popupProps;
return (
- <View style={styles.container}>
+ <TouchableOpacity
+ style={styles.container}
+ onPressOut={() => {
+ navigation.goBack();
+ }}>
<View style={styles.popup}>
<Image
style={styles.icon}
@@ -61,7 +65,7 @@ const TaggPopup: React.FC<TaggPopupProps> = ({route, navigation}) => {
/>
</View>
)}
- </View>
+ </TouchableOpacity>
);
};
@@ -92,23 +96,23 @@ const styles = StyleSheet.create({
},
header: {
color: '#fff',
- fontSize: 16,
+ fontSize: SCREEN_WIDTH / 25,
fontWeight: '600',
textAlign: 'justify',
marginBottom: '2%',
- marginHorizontal: '2%',
+ marginLeft: '4%',
},
subtext: {
color: '#fff',
- fontSize: 12,
+ fontSize: SCREEN_WIDTH / 30,
fontWeight: '600',
textAlign: 'justify',
marginBottom: '15%',
- marginHorizontal: '2%',
+ marginLeft: '3%',
},
popup: {
width: SCREEN_WIDTH * 0.8,
- height: SCREEN_WIDTH * 0.2,
+ height: SCREEN_WIDTH * 0.24,
backgroundColor: 'black',
borderRadius: 8,
flexDirection: 'row',
@@ -116,6 +120,7 @@ const styles = StyleSheet.create({
flexWrap: 'wrap',
position: 'absolute',
bottom: SCREEN_HEIGHT * 0.7,
+ padding: SCREEN_WIDTH / 40,
},
footer: {
marginLeft: '50%',
diff --git a/src/components/common/TaggPrompt.tsx b/src/components/common/TaggPrompt.tsx
new file mode 100644
index 00000000..5cd3ac3f
--- /dev/null
+++ b/src/components/common/TaggPrompt.tsx
@@ -0,0 +1,79 @@
+import * as React from 'react';
+import {Platform, Text, StyleSheet, TouchableOpacity} from 'react-native';
+import {Image, View} from 'react-native-animatable';
+import {SCREEN_HEIGHT} from '../../utils';
+import CloseIcon from '../../assets/ionicons/close-outline.svg';
+
+type TaggPromptProps = {
+ messageHeader: string;
+ messageBody: string;
+ logoType: string;
+ onClose: () => void;
+};
+
+const TaggPrompt: React.FC<TaggPromptProps> = ({
+ messageHeader,
+ messageBody,
+ logoType,
+ onClose,
+}) => {
+ /**
+ * Generic prompt for Tagg
+ */
+
+ return (
+ <View style={styles.container}>
+ <Image
+ style={styles.icon}
+ source={require('../../assets/icons/plus-logo.png')}
+ />
+ <Text style={styles.header}>{messageHeader}</Text>
+ <Text style={styles.subtext}>{messageBody}</Text>
+ <TouchableOpacity
+ style={styles.closeButton}
+ onPress={() => {
+ onClose();
+ }}>
+ <CloseIcon height={'50%'} width={'50%'} color="gray" />
+ </TouchableOpacity>
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'white',
+ height: SCREEN_HEIGHT / 4.5,
+ paddingTop: SCREEN_HEIGHT / 10,
+ paddingBottom: SCREEN_HEIGHT / 50,
+ },
+ closeButton: {
+ position: 'relative',
+ height: '40%',
+ bottom: SCREEN_HEIGHT / 6,
+ aspectRatio: 1,
+ alignSelf: 'flex-end',
+ },
+ icon: {
+ width: 40,
+ height: 40,
+ },
+ header: {
+ color: 'black',
+ fontSize: 16,
+ fontWeight: '600',
+ textAlign: 'center',
+ marginTop: '2%',
+ },
+ subtext: {
+ color: 'gray',
+ fontSize: 12,
+ fontWeight: '500',
+ textAlign: 'center',
+ marginTop: '2%',
+ },
+});
+export default TaggPrompt;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index d5d36297..9162ec70 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -18,3 +18,4 @@ export {default as BottomDrawer} from './BottomDrawer';
export {default as TaggLoadingTndicator} from './TaggLoadingIndicator';
export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer';
export {default as TaggPopUp} from './TaggPopup';
+export {default as TaggPrompt} from './TaggPrompt';
diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx
index 0d2c2b62..6dbcd170 100644
--- a/src/components/moments/Moment.tsx
+++ b/src/components/moments/Moment.tsx
@@ -1,25 +1,39 @@
import {useNavigation} from '@react-navigation/native';
-import React from 'react';
-import {Alert, StyleSheet, View} from 'react-native';
+import React, {Fragment} from 'react';
+import {
+ Alert,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewProps,
+ ViewStyle,
+} from 'react-native';
import {Text} from 'react-native-animatable';
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 UpIcon from '../../assets/icons/up_icon.svg';
+import DownIcon from '../../assets/icons/down_icon.svg';
import DeleteIcon from '../../assets/icons/delete-logo.svg';
import BigPlusIcon from '../../assets/icons/plus_icon-02.svg';
import {TAGG_TEXT_LIGHT_BLUE} from '../../constants';
import {SCREEN_WIDTH} from '../../utils';
import ImagePicker from 'react-native-image-crop-picker';
import MomentTile from './MomentTile';
-import {MomentCategoryType, MomentType, ScreenType} from 'src/types';
+import {MomentType, ScreenType} from 'src/types';
+import {useDispatch} from 'react-redux';
interface MomentProps {
- title: MomentCategoryType;
+ title: string;
images: MomentType[] | undefined;
userXId: string | undefined;
screenType: ScreenType;
- handleMomentCategoryDelete: (_: MomentCategoryType) => void;
+ handleMomentCategoryDelete: (_: string) => void;
shouldAllowDeletion: boolean;
+ showUpButton: boolean;
+ showDownButton: boolean;
+ move?: (direction: 'up' | 'down', title: string) => void;
+ externalStyles?: Record<string, StyleProp<ViewStyle>>;
}
const Moment: React.FC<MomentProps> = ({
@@ -29,8 +43,13 @@ const Moment: React.FC<MomentProps> = ({
screenType,
handleMomentCategoryDelete,
shouldAllowDeletion,
+ showUpButton,
+ showDownButton,
+ move,
+ externalStyles,
}) => {
const navigation = useNavigation();
+ const dispatch = useDispatch();
const navigateToImagePicker = () => {
ImagePicker.openPicker({
@@ -57,19 +76,50 @@ const Moment: React.FC<MomentProps> = ({
}
})
.catch((err) => {
- Alert.alert('Unable to upload moment!');
+ if (err.code && err.code !== 'E_PICKER_CANCELLED') {
+ Alert.alert('Unable to upload moment!');
+ }
});
};
+
return (
- <View style={styles.container}>
- <View style={styles.header}>
- <Text style={styles.titleText}>{title}</Text>
+ <View style={[styles.container, externalStyles?.container]}>
+ <View style={[styles.header, externalStyles?.header]}>
+ <Text style={[styles.titleText, externalStyles?.titleText]}>
+ {title}
+ </Text>
+ {!userXId ? (
+ <>
+ {showUpButton && move && (
+ <UpIcon
+ width={19}
+ height={19}
+ onPress={() => move('up', title)}
+ color={TAGG_TEXT_LIGHT_BLUE}
+ style={{marginLeft: 5}}
+ />
+ )}
+ {showDownButton && move && (
+ <DownIcon
+ width={19}
+ height={19}
+ onPress={() => move('down', title)}
+ color={TAGG_TEXT_LIGHT_BLUE}
+ style={{marginLeft: 5}}
+ />
+ )}
+ </>
+ ) : (
+ <Fragment />
+ )}
+ <View style={styles.flexer} />
{!userXId ? (
<>
<PlusIcon
width={21}
height={21}
onPress={() => navigateToImagePicker()}
+ color={TAGG_TEXT_LIGHT_BLUE}
style={{marginRight: 10}}
/>
{shouldAllowDeletion && (
@@ -87,7 +137,7 @@ const Moment: React.FC<MomentProps> = ({
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
- style={styles.scrollContainer}>
+ style={[styles.scrollContainer, externalStyles?.scrollContainer]}>
{images &&
images.map((imageObj: MomentType) => (
<MomentTile
@@ -104,7 +154,7 @@ const Moment: React.FC<MomentProps> = ({
<View style={styles.defaultImage}>
<BigPlusIcon width={24} height={24} />
<Text style={styles.defaultImageText}>
- Add a moment of your {title.toLowerCase()}!
+ Add a moment of your {title?.toLowerCase()}!
</Text>
</View>
</LinearGradient>
@@ -123,21 +173,23 @@ const styles = StyleSheet.create({
},
header: {
flex: 1,
- paddingLeft: '3%',
- padding: 5,
- paddingTop: 20,
+ padding: '3%',
backgroundColor: 'white',
flexDirection: 'row',
- justifyContent: 'space-between',
alignItems: 'center',
},
titleText: {
fontSize: 16,
fontWeight: 'bold',
color: TAGG_TEXT_LIGHT_BLUE,
+ },
+ // titleContainer: {
+ // flex: 1,
+ // flexDirection: 'row',
+ // justifyContent: 'flex-end',
+ // },
+ flexer: {
flex: 1,
- flexDirection: 'row',
- justifyContent: 'flex-end',
},
scrollContainer: {
height: SCREEN_WIDTH / 3.25,
diff --git a/src/components/notifications/Notification.tsx b/src/components/notifications/Notification.tsx
index f6a04526..184e3f27 100644
--- a/src/components/notifications/Notification.tsx
+++ b/src/components/notifications/Notification.tsx
@@ -1,9 +1,22 @@
import {useNavigation} from '@react-navigation/native';
import React, {useEffect, useState} from 'react';
-import {Image, StyleSheet, Text, View} from 'react-native';
+import {
+ ActivityIndicatorBase,
+ Alert,
+ Image,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
-import {useDispatch, useStore} from 'react-redux';
+import {useDispatch, useSelector, useStore} from 'react-redux';
+import {MomentCommentsScreen} from '../../screens';
import {loadAvatar} from '../../services';
+import {
+ EMPTY_MOMENTS_LIST,
+ EMPTY_MOMENT_CATEGORIES,
+} from '../../store/initialStates';
+import {userSocialsReducer} from '../../store/reducers';
import {RootState} from '../../store/rootReducer';
import {NotificationType, ScreenType} from '../../types';
import {
@@ -15,6 +28,7 @@ import {
interface NotificationProps {
item: NotificationType;
+ userXId: string | undefined;
screenType: ScreenType;
}
@@ -27,11 +41,16 @@ const Notification: React.FC<NotificationProps> = (props) => {
notification_object,
unread,
},
+ userXId,
screenType,
} = props;
const navigation = useNavigation();
const state: RootState = useStore().getState();
const dispatch = useDispatch();
+ const {moments: loggedInUserMoments} =
+ notification_type === 'CMT'
+ ? useSelector((state: RootState) => state.moments)
+ : {moments: undefined};
const [avatarURI, setAvatarURI] = useState<string | undefined>(undefined);
const [momentURI, setMomentURI] = useState<string | undefined>(undefined);
@@ -81,6 +100,25 @@ const Notification: React.FC<NotificationProps> = (props) => {
screenType,
});
break;
+ case 'CMT':
+ // find the moment we need to display
+ const moment = loggedInUserMoments?.find(
+ (m) => m.moment_id === notification_object?.moment_id,
+ );
+ if (moment) {
+ navigation.push('IndividualMoment', {
+ moment,
+ userXId,
+ screenType,
+ });
+ setTimeout(() => {
+ navigation.push('MomentCommentsScreen', {
+ moment_id: moment.moment_id,
+ screenType,
+ });
+ }, 500);
+ }
+ break;
default:
break;
}
diff --git a/src/components/onboarding/MomentCategory.tsx b/src/components/onboarding/MomentCategory.tsx
index 827ab207..97099b9e 100644
--- a/src/components/onboarding/MomentCategory.tsx
+++ b/src/components/onboarding/MomentCategory.tsx
@@ -1,19 +1,17 @@
-import * as React from 'react';
+import React from 'react';
import {StyleSheet} from 'react-native';
import {Image, Text} from 'react-native-animatable';
import {TouchableOpacity} from 'react-native-gesture-handler';
import LinearGradient from 'react-native-linear-gradient';
-import {BACKGROUND_GRADIENT_MAP} from '../../constants';
-import {MomentCategoryType} from '../../types';
+import {
+ BACKGROUND_GRADIENT_MAP,
+ MOMENT_CATEGORY_BG_COLORS,
+} from '../../constants';
import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
type MomentCategoryProps = {
- categoryType: MomentCategoryType;
- onSelect: (
- category: MomentCategoryType,
- isSelected: boolean,
- isAdded: boolean,
- ) => void;
+ categoryType: string;
+ onSelect: (category: string, isSelected: boolean, isAdded: boolean) => void;
isSelected: boolean;
isAdded: boolean;
};
@@ -32,63 +30,75 @@ const MomentCategory: React.FC<MomentCategoryProps> = ({
switch (categoryType) {
case 'Friends':
icon = require('../../assets/moment-categories/friends-icon.png');
- bgColor = '#5E4AE4';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[0];
break;
case 'Adventure':
icon = require('../../assets/moment-categories/adventure-icon.png');
- bgColor = '#5044A6';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[1];
break;
case 'Photo Dump':
icon = require('../../assets/moment-categories/photo-dump-icon.png');
- bgColor = '#4755A1';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[2];
break;
case 'Food':
icon = require('../../assets/moment-categories/food-icon.png');
- bgColor = '#444BA8';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[3];
break;
case 'Music':
icon = require('../../assets/moment-categories/music-icon.png');
- bgColor = '#374898';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[4];
break;
case 'Art':
icon = require('../../assets/moment-categories/art-icon.png');
- bgColor = '#3F5C97';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[5];
break;
case 'Sports':
icon = require('../../assets/moment-categories/sports-icon.png');
- bgColor = '#3A649F';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[6];
break;
case 'Fashion':
icon = require('../../assets/moment-categories/fashion-icon.png');
- bgColor = '#386A95';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[7];
break;
case 'Travel':
icon = require('../../assets/moment-categories/travel-icon.png');
- bgColor = '#366D84';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[8];
break;
case 'Pets':
icon = require('../../assets/moment-categories/pets-icon.png');
- bgColor = '#335E76';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[9];
break;
case 'Fitness':
icon = require('../../assets/moment-categories/fitness-icon.png');
- bgColor = '#2E5471';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[10];
break;
case 'DIY':
icon = require('../../assets/moment-categories/diy-icon.png');
- bgColor = '#274765';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[11];
break;
case 'Nature':
icon = require('../../assets/moment-categories/nature-icon.png');
- bgColor = '#225363';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[12];
break;
case 'Early Life':
icon = require('../../assets/moment-categories/early-life-icon.png');
- bgColor = '#365F6A';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[13];
break;
case 'Beauty':
icon = require('../../assets/moment-categories/beauty-icon.png');
- bgColor = '#4E7175';
+ bgColor = MOMENT_CATEGORY_BG_COLORS[14];
+ break;
+ default:
+ // All custom categories
+ icon = require('../../assets/moment-categories/custom-icon.png');
+ // A quick deterministic "random" color picker by summing up ascii char codees
+ const charCodeSum = categoryType
+ .split('')
+ .reduce((acc: number, x: string) => acc + x.charCodeAt(0), 0);
+ bgColor =
+ MOMENT_CATEGORY_BG_COLORS[
+ charCodeSum % MOMENT_CATEGORY_BG_COLORS.length
+ ];
break;
}
diff --git a/src/components/onboarding/TaggBigInput.tsx b/src/components/onboarding/TaggBigInput.tsx
index ba965465..4e8e1ef7 100644
--- a/src/components/onboarding/TaggBigInput.tsx
+++ b/src/components/onboarding/TaggBigInput.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {View, TextInput, StyleSheet, TextInputProps} from 'react-native';
import * as Animatable from 'react-native-animatable';
+import {TAGG_LIGHT_PURPLE} from '../../constants';
interface TaggBigInputProps extends TextInputProps {
valid?: boolean;
@@ -55,7 +56,7 @@ const styles = StyleSheet.create({
warning: {
fontSize: 14,
marginTop: 5,
- color: '#f4ddff',
+ color: TAGG_LIGHT_PURPLE,
maxWidth: 350,
textAlign: 'center',
},
diff --git a/src/components/onboarding/TaggInput.tsx b/src/components/onboarding/TaggInput.tsx
index 12d99325..405564ab 100644
--- a/src/components/onboarding/TaggInput.tsx
+++ b/src/components/onboarding/TaggInput.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import {View, TextInput, StyleSheet, TextInputProps} from 'react-native';
import * as Animatable from 'react-native-animatable';
+import {TAGG_LIGHT_PURPLE} from '../../constants';
interface TaggInputProps extends TextInputProps {
valid?: boolean;
@@ -52,7 +53,7 @@ const styles = StyleSheet.create({
warning: {
fontSize: 14,
marginTop: 5,
- color: '#f4ddff',
+ color: TAGG_LIGHT_PURPLE,
maxWidth: 350,
textAlign: 'center',
},
diff --git a/src/components/onboarding/TermsAndConditionsText.tsx b/src/components/onboarding/TermsAndConditionsText.tsx
index 39450667..2102d613 100644
--- a/src/components/onboarding/TermsAndConditionsText.tsx
+++ b/src/components/onboarding/TermsAndConditionsText.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
-import {StyleSheet, Text} from 'react-native';
+import {Linking, StyleSheet, Text} from 'react-native';
+import {TAGG_WEBSITE} from '../../constants';
const TermsAndConditionsText: React.FC = () => {
const textWithBulletPoint = (data: string, style: object) => {
@@ -550,8 +551,14 @@ const TermsAndConditionsText: React.FC = () => {
By email: <Text style={styles.link}>support@tagg.id</Text>
</Text>
<Text style={styles.paraLeftAlign}>
- By visiting this page on our website:{' '}
- <Text style={styles.link}>https://www.tagg.id/</Text>
+ By visiting this page on our{' '}
+ <Text
+ style={styles.link}
+ onPress={() => {
+ Linking.openURL(TAGG_WEBSITE + 'terms-and-conditions/');
+ }}>
+ website
+ </Text>
</Text>
</React.Fragment>
);
diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx
index 3a304938..1d639a41 100644
--- a/src/components/profile/Content.tsx
+++ b/src/components/profile/Content.tsx
@@ -12,19 +12,14 @@ import {
import Animated from 'react-native-reanimated';
import {
CategorySelectionScreenType,
- MomentCategoryType,
MomentType,
ProfilePreviewType,
ProfileType,
ScreenType,
UserType,
} from '../../types';
-import {
- COVER_HEIGHT,
- MOMENT_CATEGORIES,
- TAGG_TEXT_LIGHT_BLUE,
-} from '../../constants';
-import {fetchUserX, SCREEN_HEIGHT, userLogin} from '../../utils';
+import {COVER_HEIGHT, TAGG_TEXT_LIGHT_BLUE} from '../../constants';
+import {fetchUserX, moveCategory, SCREEN_HEIGHT, userLogin} from '../../utils';
import TaggsBar from '../taggs/TaggsBar';
import {Moment} from '../moments';
import ProfileBody from './ProfileBody';
@@ -45,12 +40,12 @@ import {
NO_PROFILE,
EMPTY_PROFILE_PREVIEW_LIST,
EMPTY_MOMENTS_LIST,
- MOMENT_CATEGORIES_MAP,
} from '../../store/initialStates';
import {Cover} from '.';
import {TouchableOpacity} from 'react-native-gesture-handler';
-import {useNavigation} from '@react-navigation/native';
+import {useFocusEffect, useNavigation} from '@react-navigation/native';
import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg';
+import {TaggPrompt} from '../common';
interface ContentProps {
y: Animated.Value<number>;
@@ -77,7 +72,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
? useSelector((state: RootState) => state.userX[screenType][userXId])
: useSelector((state: RootState) => state.moments);
- const {momentCategories = MOMENT_CATEGORIES_MAP} = userXId
+ const {momentCategories = []} = userXId
? useSelector((state: RootState) => state.userX[screenType][userXId])
: useSelector((state: RootState) => state.momentCategories);
@@ -103,12 +98,15 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
const [shouldBounce, setShouldBounce] = useState<boolean>(true);
const [refreshing, setRefreshing] = useState<boolean>(false);
- /**
- * Filter list of categories already selected by user
- */
- const userMomentCategories = MOMENT_CATEGORIES.filter(
- (category) => momentCategories[category] === true,
+ const [isStageTwoPromptClosed, setIsStageTwoPromptClosed] = useState<boolean>(
+ false,
+ );
+ const [isStageOnePromptClosed, setIsStageOnePromptClosed] = useState<boolean>(
+ false,
);
+ const [isStageThreePromptClosed, setIsStageThreePromptClosed] = useState<
+ boolean
+ >(false);
const onRefresh = useCallback(() => {
const refrestState = async () => {
@@ -146,6 +144,61 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
createImagesMap();
}, [createImagesMap]);
+ const move = (direction: 'up' | 'down', title: string) => {
+ let categories = [...momentCategories];
+ categories = moveCategory(categories, title, direction === 'up');
+ dispatch(updateMomentCategories(categories, false));
+ };
+
+ /**
+ * Prompt user to perform an activity based on their profile completion stage
+ * To fire 2 seconds after the screen comes in focus
+ * 1 means STAGE_1:
+ * The user must upload a moment, so take them to a screen guiding them to post a moment
+ * 2 means STAGE_2:
+ * The user must create another category so show a prompt on top of the screen
+ * 3 means STAGE_3:
+ * The user must upload a moment to the second category, so show a prompt on top of the screen
+ * Else, profile is complete and no prompt needs to be shown
+ */
+ useFocusEffect(
+ useCallback(() => {
+ const navigateToMomentUploadPrompt = () => {
+ switch (profile.profile_completion_stage) {
+ case 1:
+ if (
+ momentCategories &&
+ momentCategories[0] &&
+ !isStageOnePromptClosed
+ ) {
+ navigation.navigate('MomentUploadPrompt', {
+ screenType,
+ momentCategory: momentCategories[0],
+ });
+ setIsStageOnePromptClosed(true);
+ }
+ break;
+ case 2:
+ setIsStageTwoPromptClosed(false);
+ break;
+ case 3:
+ setIsStageThreePromptClosed(false);
+ break;
+ default:
+ break;
+ }
+ };
+ if (!userXId) {
+ setTimeout(navigateToMomentUploadPrompt, 2000);
+ }
+ }, [
+ profile.profile_completion_stage,
+ momentCategories,
+ userXId,
+ isStageOnePromptClosed,
+ ]),
+ );
+
/**
* This hook is called on load of profile and when you update the friends list.
*/
@@ -226,7 +279,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
* Confirm with user before deleting the category
* @param category category to be deleted
*/
- const handleCategoryDeletion = (category: MomentCategoryType) => {
+ const handleCategoryDeletion = (category: string) => {
Alert.alert(
'Category Deletion',
`Are you sure that you want to delete the category ${category} ?`,
@@ -239,7 +292,10 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
text: 'Yes',
onPress: () => {
dispatch(
- updateMomentCategories([category], false, loggedInUser.userId),
+ updateMomentCategories(
+ momentCategories.filter((mc) => mc !== category),
+ false,
+ ),
);
dispatch(deleteUserMomentsForCategory(category));
},
@@ -304,7 +360,35 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
} has not posted any moments yet`}</Text>
</View>
)}
- {userMomentCategories.map(
+ {!userXId &&
+ profile.profile_completion_stage === 2 &&
+ !isStageTwoPromptClosed && (
+ <TaggPrompt
+ messageHeader="Create a new category"
+ messageBody={
+ 'Post your first moment to continue building your digital identity!'
+ }
+ logoType=""
+ onClose={() => {
+ setIsStageTwoPromptClosed(true);
+ }}
+ />
+ )}
+ {!userXId &&
+ profile.profile_completion_stage === 3 &&
+ !isStageThreePromptClosed && (
+ <TaggPrompt
+ messageHeader="Continue to build your profile"
+ messageBody={
+ 'Continue to personalize your own digital space in\nthis community by filling your profile with\ncategories and moments!'
+ }
+ logoType=""
+ onClose={() => {
+ setIsStageThreePromptClosed(true);
+ }}
+ />
+ )}
+ {momentCategories.map(
(title, index) =>
(!userXId || imagesMap.get(title)) && (
<Moment
@@ -314,15 +398,17 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => {
userXId={userXId}
screenType={screenType}
handleMomentCategoryDelete={handleCategoryDeletion}
- shouldAllowDeletion={userMomentCategories.length > 2}
+ shouldAllowDeletion={momentCategories.length > 1}
+ showUpButton={index !== 0}
+ showDownButton={index !== momentCategories.length - 1}
+ move={move}
/>
),
)}
- {!userXId && userMomentCategories.length < 6 && (
+ {!userXId && (
<TouchableOpacity
onPress={() =>
navigation.push('CategorySelection', {
- categories: momentCategories,
screenType: CategorySelectionScreenType.Profile,
user: loggedInUser,
})
diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx
index bd015811..134e94cd 100644
--- a/src/components/profile/ProfilePreview.tsx
+++ b/src/components/profile/ProfilePreview.tsx
@@ -134,20 +134,23 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
}
}
+ const userXId =
+ loggedInUser.username === user.username ? undefined : user.id;
+
/**
- * Dispatch an event to Fetch the user details
- * If the user is already present in store, do not fetch again
- * Finally, Navigate to profile of the user selected
+ * Dispatch an event to Fetch the user details only if we're navigating to
+ * a userX's profile.
+ * If the user is already present in store, do not fetch again.
+ * Finally, Navigate to profile of the user selected.
*/
-
- if (!userXInStore(state, screenType, user.id)) {
+ if (userXId && !userXInStore(state, screenType, user.id)) {
await fetchUserX(
dispatch,
{userId: user.id, username: user.username},
screenType,
);
}
- const userXId = loggedInUser.username === user.username ? undefined : user.id;
+
navigation.push('Profile', {
userXId,
screenType,
@@ -205,7 +208,6 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({
usernameStyle = styles.searchResultUsername;
nameStyle = styles.searchResultName;
}
-
return (
<TouchableOpacity
onPress={addToRecentlyStoredAndNavigateToProfile}
@@ -257,9 +259,9 @@ const styles = StyleSheet.create({
discoverUsersContainer: {
alignItems: 'center',
textAlign: 'center',
- margin: '0.5%',
width: '32%',
marginVertical: 10,
+ borderWidth: 1,
},
searchResultAvatar: {
height: 60,
@@ -290,6 +292,7 @@ const styles = StyleSheet.create({
discoverUsersNameContainer: {
justifyContent: 'space-evenly',
alignSelf: 'stretch',
+ marginTop: 5,
},
searchResultUsername: {
fontSize: 18,
diff --git a/src/components/search/Explore.tsx b/src/components/search/Explore.tsx
index a02205a4..c07c66b8 100644
--- a/src/components/search/Explore.tsx
+++ b/src/components/search/Explore.tsx
@@ -1,27 +1,18 @@
import React from 'react';
-import {View, StyleSheet} from 'react-native';
+import {StyleSheet, Text, View} from 'react-native';
+import {useSelector} from 'react-redux';
+import {EXPLORE_SECTION_TITLES} from '../../constants';
+import {RootState} from '../../store/rootReducer';
+import {ExploreSectionType} from '../../types';
import ExploreSection from './ExploreSection';
const Explore: React.FC = () => {
- const sections: Array<string> = [
- 'People you follow',
- 'People you may know',
- 'Trending in sports',
- 'Trending on Tagg',
- 'Trending in music',
- ];
- const users: Array<string> = [
- 'Sam Davis',
- 'Becca Smith',
- 'Ann Taylor',
- 'Clara Johnson',
- 'Sarah Jung',
- 'Lila Hernandez',
- ];
+ const {explores} = useSelector((state: RootState) => state.taggUsers);
return (
<View style={styles.container}>
- {sections.map((title) => (
- <ExploreSection key={title} title={title} users={users} />
+ <Text style={styles.header}>Search Profiles</Text>
+ {EXPLORE_SECTION_TITLES.map((title: ExploreSectionType) => (
+ <ExploreSection key={title} title={title} users={explores[title]} />
))}
</View>
);
@@ -30,6 +21,14 @@ const Explore: React.FC = () => {
const styles = StyleSheet.create({
container: {
zIndex: 0,
+ // margin: '5%',
+ },
+ header: {
+ fontWeight: '700',
+ fontSize: 22,
+ color: '#fff',
+ marginBottom: '2%',
+ margin: '5%',
},
});
export default Explore;
diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx
index 8e826bd9..8e8b4988 100644
--- a/src/components/search/ExploreSection.tsx
+++ b/src/components/search/ExploreSection.tsx
@@ -1,5 +1,6 @@
-import React from 'react';
-import {View, Text, ScrollView, StyleSheet} from 'react-native';
+import React, {Fragment} from 'react';
+import {ScrollView, StyleSheet, Text, View} from 'react-native';
+import {ProfilePreviewType} from '../../types';
import ExploreSectionUser from './ExploreSectionUser';
/**
@@ -9,33 +10,40 @@ import ExploreSectionUser from './ExploreSectionUser';
interface ExploreSectionProps {
title: string;
- users: Array<string>;
+ users: ProfilePreviewType[];
}
const ExploreSection: React.FC<ExploreSectionProps> = ({title, users}) => {
- return (
+ return users.length !== 0 ? (
<View style={styles.container}>
<Text style={styles.header}>{title}</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
- {users.map((name, key) => (
- <ExploreSectionUser {...{name, key}} style={styles.user} />
+ <View style={styles.padding} />
+ {users.map((user) => (
+ <ExploreSectionUser key={user.id} user={user} style={styles.user} />
))}
</ScrollView>
</View>
+ ) : (
+ <Fragment />
);
};
const styles = StyleSheet.create({
container: {
- marginBottom: 30,
+ marginVertical: '5%',
},
header: {
fontWeight: '600',
fontSize: 20,
color: '#fff',
- marginBottom: 20,
+ marginLeft: '5%',
+ marginBottom: '5%',
},
user: {
- marginHorizontal: 15,
+ marginHorizontal: 5,
+ },
+ padding: {
+ width: 10,
},
});
diff --git a/src/components/search/ExploreSectionUser.tsx b/src/components/search/ExploreSectionUser.tsx
index a9fce063..0bf68a20 100644
--- a/src/components/search/ExploreSectionUser.tsx
+++ b/src/components/search/ExploreSectionUser.tsx
@@ -1,12 +1,18 @@
-import React from 'react';
+import {useNavigation} from '@react-navigation/native';
+import React, {useEffect, useState} from 'react';
import {
+ Image,
StyleSheet,
Text,
- ViewProps,
- Image,
TouchableOpacity,
+ ViewProps,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
+import {useDispatch, useSelector, useStore} from 'react-redux';
+import {loadAvatar} from '../../services';
+import {RootState} from '../../store/rootReducer';
+import {ProfilePreviewType, ScreenType} from '../../types';
+import {fetchUserX, userXInStore} from '../../utils';
/**
* Search Screen for user recommendations and a search
@@ -14,14 +20,52 @@ import LinearGradient from 'react-native-linear-gradient';
*/
interface ExploreSectionUserProps extends ViewProps {
- name: string;
+ user: ProfilePreviewType;
}
const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
- name,
+ user,
style,
}) => {
+ const {id, username, first_name, last_name} = user;
+ const [avatar, setAvatar] = useState<string | null>(null);
+ const navigation = useNavigation();
+ const {user: loggedInUser} = useSelector((state: RootState) => state.user);
+ const state: RootState = useStore().getState();
+ const screenType = ScreenType.Search;
+
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ let mounted = true;
+ const loadAvatarImage = async () => {
+ const response = await loadAvatar(id, true);
+ if (mounted) {
+ setAvatar(response);
+ }
+ };
+ loadAvatarImage();
+ return () => {
+ mounted = false;
+ };
+ }, [user]);
+
+ const handlePress = async () => {
+ if (!userXInStore(state, screenType, user.id)) {
+ await fetchUserX(
+ dispatch,
+ {userId: user.id, username: user.username},
+ screenType,
+ );
+ }
+ const userXId = loggedInUser.username === user.username ? undefined : id;
+ navigation.push('Profile', {
+ userXId,
+ screenType,
+ });
+ };
+
return (
- <TouchableOpacity style={[styles.container, style]}>
+ <TouchableOpacity style={[styles.container, style]} onPress={handlePress}>
<LinearGradient
colors={['#9F00FF', '#27EAE9']}
useAngle
@@ -29,12 +73,18 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
angleCenter={{x: 0.5, y: 0.5}}
style={styles.gradient}>
<Image
- source={require('../../assets/images/avatar-placeholder.png')}
+ source={
+ avatar
+ ? {uri: avatar}
+ : require('../../assets/images/avatar-placeholder.png')
+ }
style={styles.profile}
/>
</LinearGradient>
- <Text style={styles.name}>{name}</Text>
- <Text style={styles.username}>{`@${name.split(' ').join('')}`}</Text>
+ <Text style={styles.name} numberOfLines={2}>
+ {first_name} {last_name}
+ </Text>
+ <Text style={styles.username} numberOfLines={1}>{`@${username}`}</Text>
</TouchableOpacity>
);
};
@@ -42,27 +92,30 @@ const ExploreSectionUser: React.FC<ExploreSectionUserProps> = ({
const styles = StyleSheet.create({
container: {
alignItems: 'center',
+ width: 100,
},
gradient: {
- height: 80,
- width: 80,
+ height: 60,
+ aspectRatio: 1,
borderRadius: 40,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10,
},
profile: {
- height: 76,
- width: 76,
+ height: 55,
+ aspectRatio: 1,
borderRadius: 38,
},
name: {
fontWeight: '600',
+ flexWrap: 'wrap',
fontSize: 16,
color: '#fff',
+ textAlign: 'center',
},
username: {
- fontWeight: '600',
+ fontWeight: '400',
fontSize: 14,
color: '#fff',
},
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 3b2289fd..de43b94d 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -27,6 +27,8 @@ export const BLOCK_USER_ENDPOINT: string = API_URL + 'block/';
export const PASSWORD_RESET_ENDPOINT: string = API_URL + 'password-reset/';
export const MOMENT_CATEGORY_ENDPOINT: string = API_URL + 'moment-category/';
export const NOTIFICATIONS_ENDPOINT: string = API_URL + 'notifications/';
+export const DISCOVER_ENDPOINT: string = API_URL + 'discover/';
+export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/';
// Register as FCM device
export const FCM_ENDPOINT: string = API_URL + 'fcm/';
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index 531420e6..b96d9438 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -1,5 +1,5 @@
import {ReactText} from 'react';
-import {BackgroundGradientType, MomentCategoryType} from './../types/';
+import {BackgroundGradientType, ExploreSectionType} from './../types/';
import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils';
export const CHIN_HEIGHT = 34;
@@ -60,6 +60,7 @@ export const YOUTUBE_FONT_COLOR: string = '#FCA4A4';
export const TAGG_DARK_BLUE = '#4E699C';
export const TAGG_TEXT_LIGHT_BLUE: string = '#698DD3';
+export const TAGG_LIGHT_PURPLE = '#F4DDFF';
export const TAGGS_GRADIENT = {
start: '#9F00FF',
@@ -103,7 +104,7 @@ export const BROWSABLE_SOCIAL_URLS: Record<string, string> = {
Twitter: 'https://twitter.com/',
};
-export const MOMENT_CATEGORIES: Array<MomentCategoryType> = [
+export const MOMENT_CATEGORIES: string[] = [
'Friends',
'Adventure',
'Photo Dump',
@@ -140,3 +141,31 @@ export const CLASS_YEAR_LIST: Array<string> = [
'2025',
'2026',
];
+
+export const TAGG_WEBSITE = 'https://www.tagg.id/';
+export const MOMENT_CATEGORY_BG_COLORS: string[] = [
+ '#5E4AE4',
+ '#5044A6',
+ '#4755A1',
+ '#444BA8',
+ '#374898',
+ '#3F5C97',
+ '#3A649F',
+ '#386A95',
+ '#366D84',
+ '#335E76',
+ '#2E5471',
+ '#274765',
+ '#225363',
+ '#365F6A',
+ '#4E7175',
+];
+
+export const EXPLORE_SECTION_TITLES: ExploreSectionType[] = [
+ 'New to Tagg',
+ 'People You May Know',
+ 'Trending on Tagg',
+ "Brown '21",
+ "Brown '22",
+ "Brown '23",
+];
diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx
index 4614168b..bd838ef2 100644
--- a/src/routes/main/MainStackNavigator.tsx
+++ b/src/routes/main/MainStackNavigator.tsx
@@ -2,7 +2,13 @@
* Note the name userXId here, it refers to the id of the user being visited
*/
import {createStackNavigator} from '@react-navigation/stack';
-import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types';
+import {Image} from 'react-native-image-crop-picker';
+import {
+ CategorySelectionScreenType,
+ MomentType,
+ ScreenType,
+ UserType,
+} from '../../types';
export type MainStackParams = {
Search: {
@@ -19,7 +25,7 @@ export type MainStackParams = {
};
CaptionScreen: {
title: string;
- image: object;
+ image: Image;
screenType: ScreenType;
};
IndividualMoment: {
@@ -41,12 +47,18 @@ export type MainStackParams = {
username: string;
};
CategorySelection: {
- categories: Array<string>;
screenType: CategorySelectionScreenType;
+ user: UserType;
+ newCustomCategory: string | undefined;
};
+ CreateCustomCategory: {};
Notifications: {
screenType: ScreenType;
};
+ MomentUploadPrompt: {
+ screenType: ScreenType;
+ momentCategory: string;
+ };
};
export const MainStack = createStackNavigator<MainStackParams>();
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index bf643fd8..b4eaa213 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -1,25 +1,25 @@
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationOptions} from '@react-navigation/stack';
import React from 'react';
import {
- IndividualMoment,
CaptionScreen,
- SocialMediaTaggs,
- SearchScreen,
- ProfileScreen,
- MomentCommentsScreen,
- EditProfile,
CategorySelection,
+ CreateCustomCategory,
+ EditProfile,
FriendsListScreen,
+ IndividualMoment,
+ MomentCommentsScreen,
+ MomentUploadPromptScreen,
NotificationsScreen,
+ ProfileScreen,
+ SearchScreen,
+ SocialMediaTaggs,
} from '../../screens';
-import {MainStack, MainStackParams} from './MainStackNavigator';
-import {RouteProp} from '@react-navigation/native';
import {ScreenType} from '../../types';
-import {AvatarHeaderHeight} from '../../utils';
-import {StackNavigationOptions} from '@react-navigation/stack';
-import {Screen} from 'react-native-screens';
+import {AvatarHeaderHeight, SCREEN_WIDTH} from '../../utils';
+import {MainStack, MainStackParams} from './MainStackNavigator';
/**
- * Trying to explain the purpose of each route on the stack (ACTUALLY A STACK)
* Profile : To display the logged in user's profile when the userXId passed in to it is (undefined | null | empty string) else displays profile of the user being visited.
* Search : To display the search screen. Search for a user on this screen, click on a result tile and navigate to the same.
* When you click on the search icon after looking at a user's profile, the stack gets reset and you come back to the top of the stack (First screen : Search in this case)
@@ -79,6 +79,7 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
<MainStack.Navigator
screenOptions={{
headerShown: false,
+ gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6},
}}
mode="card"
initialRouteName={initialRouteName}>
@@ -141,6 +142,17 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
}}
/>
<MainStack.Screen
+ name="CreateCustomCategory"
+ component={CreateCustomCategory}
+ options={{
+ headerShown: true,
+ headerTransparent: true,
+ headerBackTitleVisible: false,
+ headerTintColor: 'white',
+ headerTitle: '',
+ }}
+ />
+ <MainStack.Screen
name="IndividualMoment"
component={IndividualMoment}
options={{
@@ -157,6 +169,14 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
initialParams={{screenType}}
/>
<MainStack.Screen
+ name="MomentUploadPrompt"
+ component={MomentUploadPromptScreen}
+ options={{
+ ...modalStyle,
+ }}
+ initialParams={{screenType}}
+ />
+ <MainStack.Screen
name="FriendsListScreen"
component={FriendsListScreen}
options={{
diff --git a/src/routes/onboarding/OnboardingStack.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx
index 7ff00271..9f614f7c 100644
--- a/src/routes/onboarding/OnboardingStack.tsx
+++ b/src/routes/onboarding/OnboardingStackNavigator.tsx
@@ -1,7 +1,6 @@
import {createStackNavigator} from '@react-navigation/stack';
import {
CategorySelectionScreenType,
- MomentCategoryType,
TaggPopupType,
UserType,
VerificationScreenType,
@@ -28,13 +27,20 @@ export type OnboardingStackParams = {
ProfileOnboarding: {username: string; userId: string};
SocialMedia: {username: string; userId: string};
CategorySelection: {
- categories: Record<MomentCategoryType, boolean>;
screenType: CategorySelectionScreenType;
user: UserType;
+ newCustomCategory: string | undefined;
+ };
+ CreateCustomCategory: {
+ screenType: CategorySelectionScreenType;
+ user: UserType;
+ existingCategories: string[];
};
TaggPopup: {
popupProps: TaggPopupType;
};
+ AddWaitlistUser: undefined;
+ WaitlistSuccess: undefined;
};
export const OnboardingStack = createStackNavigator<OnboardingStackParams>();
diff --git a/src/routes/onboarding/Onboarding.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx
index a3d281f5..afc5be99 100644
--- a/src/routes/onboarding/Onboarding.tsx
+++ b/src/routes/onboarding/OnboardingStackScreen.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {OnboardingStack} from './OnboardingStack';
+import {OnboardingStack} from './OnboardingStackNavigator';
import {
Login,
InvitationCodeVerification,
@@ -14,6 +14,8 @@ import {
PasswordReset,
WelcomeScreen,
CategorySelection,
+ AddWaitlistUserScreen,
+ WaitlistSuccessScreen,
} from '../../screens';
import {StackCardInterpolationProps} from '@react-navigation/stack';
import TaggPopup from '../../components/common/TaggPopup';
@@ -97,6 +99,14 @@ const Onboarding: React.FC = () => {
component={InvitationCodeVerification}
/>
<OnboardingStack.Screen
+ name="AddWaitlistUser"
+ component={AddWaitlistUserScreen}
+ />
+ <OnboardingStack.Screen
+ name="WaitlistSuccess"
+ component={WaitlistSuccessScreen}
+ />
+ <OnboardingStack.Screen
name="RegistrationOne"
component={RegistrationOne}
/>
diff --git a/src/routes/onboarding/index.ts b/src/routes/onboarding/index.ts
index 66b0f3f4..ce9ac046 100644
--- a/src/routes/onboarding/index.ts
+++ b/src/routes/onboarding/index.ts
@@ -1,2 +1,2 @@
-export * from './OnboardingStack';
-export {default} from './Onboarding';
+export * from './OnboardingStackNavigator';
+export {default} from './OnboardingStackScreen';
diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx
index 9d7d4b12..3757c56b 100644
--- a/src/routes/tabs/NavigationBar.tsx
+++ b/src/routes/tabs/NavigationBar.tsx
@@ -1,12 +1,16 @@
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import React, {Fragment} from 'react';
+import {useSelector} from 'react-redux';
import {NavigationIcon} from '../../components';
+import {RootState} from '../../store/rootReducer';
import {ScreenType} from '../../types';
import MainStackScreen from '../main/MainStackScreen';
const Tabs = createBottomTabNavigator();
const NavigationBar: React.FC = () => {
+ const {isOnboardedUser} = useSelector((state: RootState) => state.user);
+
return (
<Tabs.Navigator
screenOptions={({route}) => ({
@@ -27,7 +31,7 @@ const NavigationBar: React.FC = () => {
}
},
})}
- initialRouteName="Search"
+ initialRouteName={isOnboardedUser ? 'Profile' : 'Search'}
tabBarOptions={{
showLabel: false,
style: {
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index d6d47b02..8aa47299 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -17,6 +17,7 @@ import {NotificationType, ScreenType} from '../../types';
import {getDateAge, SCREEN_HEIGHT} from '../../utils';
const NotificationsScreen: React.FC = () => {
+ const {user: loggedInUser} = useSelector((state: RootState) => state.user);
const [refreshing, setRefreshing] = useState(false);
// used for figuring out which ones are unread
const [lastViewed, setLastViewed] = useState<moment.Moment | undefined>(
@@ -95,7 +96,13 @@ const NotificationsScreen: React.FC = () => {
}, [lastViewed, notifications]);
const renderNotification = ({item}: {item: NotificationType}) => (
- <Notification item={item} screenType={ScreenType.Notifications} />
+ <Notification
+ item={item}
+ userXId={
+ item.actor.id === loggedInUser.userId ? undefined : item.actor.id
+ }
+ screenType={ScreenType.Notifications}
+ />
);
const renderSectionHeader = ({section: {title, data}}) =>
diff --git a/src/screens/onboarding/AddWaitlistUserScreen.tsx b/src/screens/onboarding/AddWaitlistUserScreen.tsx
new file mode 100644
index 00000000..1c13ffb5
--- /dev/null
+++ b/src/screens/onboarding/AddWaitlistUserScreen.tsx
@@ -0,0 +1,238 @@
+import {StackNavigationProp} from '@react-navigation/stack';
+import * as React from 'react';
+import {
+ KeyboardAvoidingView,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {
+ ArrowButton,
+ Background,
+ LoadingIndicator,
+ SubmitButton,
+ TaggInput,
+} from '../../components';
+import {nameRegex, phoneRegex} from '../../constants';
+import {OnboardingStackParams} from '../../routes';
+import {adduserToWaitlist} from '../../services';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_HEIGHT} from '../../utils';
+
+type AddWaitlistUserScreenProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'AddWaitlistUser'
+>;
+
+interface AddWaitlistUserScreenProps {
+ navigation: AddWaitlistUserScreenProp;
+}
+
+const AddWaitlistUserScreen: React.FC<AddWaitlistUserScreenProps> = ({
+ navigation,
+}) => {
+ const phoneRef = React.useRef();
+ const lnameRef = React.useRef();
+
+ const [form, setForm] = React.useState({
+ phone_number: {value: '', isValid: false},
+ first_name: {value: '', isValid: false},
+ last_name: {value: '', isValid: false},
+ attemptedSubmit: false,
+ });
+
+ //Handlers
+ const handleFocusChange = (field: string): void => {
+ switch (field) {
+ case 'last_name':
+ const lnameField: any = lnameRef.current;
+ lnameField.focus();
+ break;
+ case 'phone_number':
+ const phoneField: any = phoneRef.current;
+ phoneField.focus();
+ break;
+ default:
+ return;
+ }
+ };
+
+ const validate = (value: string, type: string) => {
+ let isValid: boolean = false;
+ switch (type) {
+ case 'phone_number':
+ isValid = phoneRegex.test(value);
+ break;
+ default:
+ isValid = nameRegex.test(value);
+ break;
+ }
+ return isValid;
+ };
+
+ const handleUpdate = (value: string, type: string) => {
+ value = value.trim();
+ const isValid = validate(value, type);
+ setForm({
+ ...form,
+ [type]: {value, isValid},
+ });
+ };
+
+ const handleAddUser = async () => {
+ if (!form.attemptedSubmit) {
+ setForm({
+ ...form,
+ attemptedSubmit: true,
+ });
+ }
+ try {
+ const {phone_number, first_name, last_name} = form;
+ if (phone_number.isValid && first_name.isValid && last_name.isValid) {
+ const success = await adduserToWaitlist(
+ phone_number.value,
+ first_name.value,
+ last_name.value,
+ );
+ if (success) {
+ navigation.navigate('WaitlistSuccess');
+ }
+ } else {
+ setForm({...form, attemptedSubmit: false});
+ setTimeout(() => setForm({...form, attemptedSubmit: true}));
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ //Components
+ const Footer = () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() => navigation.navigate('InvitationCodeVerification')}
+ />
+ </View>
+ );
+
+ const {phone_number, first_name, last_name} = form;
+
+ return (
+ <Background
+ style={styles.container}
+ gradientType={BackgroundGradientType.Light}>
+ <StatusBar barStyle="light-content" />
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.container}>
+ <View>
+ <Text style={styles.formHeader}>JOIN WAITLIST</Text>
+ </View>
+ <TaggInput
+ accessibilityHint="Enter your first name."
+ accessibilityLabel="First name input field."
+ placeholder="First Name"
+ autoCompleteType="name"
+ textContentType="name"
+ returnKeyType="next"
+ onChangeText={(text) => handleUpdate(text, 'first_name')}
+ onSubmitEditing={() => handleFocusChange('first_name')}
+ blurOnSubmit={false}
+ valid={first_name.isValid}
+ invalidWarning="Please enter a valid first name."
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ accessibilityHint="Enter your last name."
+ accessibilityLabel="Last name input field."
+ placeholder="Last Name"
+ autoCompleteType="name"
+ textContentType="name"
+ returnKeyType="next"
+ onChangeText={(text) => handleUpdate(text, 'last_name')}
+ blurOnSubmit={false}
+ ref={lnameRef}
+ valid={last_name.isValid}
+ invalidWarning="Please enter a valid last name."
+ onSubmitEditing={() => handleFocusChange('phone_number')}
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ maxLength={12}
+ accessibilityHint="Enter your phone number."
+ accessibilityLabel="Phone number input field."
+ placeholder="Phone Number"
+ autoCompleteType="tel"
+ textContentType="telephoneNumber"
+ autoCapitalize="none"
+ returnKeyType="next"
+ keyboardType="phone-pad"
+ onChangeText={(text) => handleUpdate(text, 'phone_number')}
+ blurOnSubmit={false}
+ ref={phoneRef}
+ valid={phone_number.isValid}
+ invalidWarning="Please enter a valid 10 digit number."
+ onSubmitEditing={handleAddUser}
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TouchableOpacity onPress={handleAddUser} style={styles.finalAction}>
+ <Text style={styles.finalActionLabel}>Submit</Text>
+ </TouchableOpacity>
+ <LoadingIndicator />
+ </KeyboardAvoidingView>
+ <Footer />
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ finalAction: {
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 150,
+ height: 40,
+ borderRadius: 5,
+ borderWidth: 1,
+ borderColor: '#fff',
+ marginVertical: SCREEN_HEIGHT / 20,
+ },
+ finalActionLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: 'black',
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 30,
+ fontWeight: '600',
+ marginBottom: '16%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+});
+
+export default AddWaitlistUserScreen;
diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx
index b9677ed4..5589ea9e 100644
--- a/src/screens/onboarding/CategorySelection.tsx
+++ b/src/screens/onboarding/CategorySelection.tsx
@@ -1,8 +1,8 @@
import {RouteProp} from '@react-navigation/native';
-import React, {useCallback, useEffect, useState} from 'react';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useEffect, useState} from 'react';
import {
Alert,
- KeyboardAvoidingView,
Platform,
StatusBar,
StyleSheet,
@@ -10,20 +10,20 @@ import {
TouchableOpacity,
View,
} from 'react-native';
-import {useDispatch} from 'react-redux';
-import {
- BackgroundGradientType,
- CategorySelectionScreenType,
- MomentCategoryType,
-} from '../../types';
+import {ScrollView} from 'react-native-gesture-handler';
+import {useDispatch, useSelector} from 'react-redux';
+import PlusIcon from '../../assets/icons/plus_icon-01.svg';
import {Background, MomentCategory} from '../../components';
import {MOMENT_CATEGORIES} from '../../constants';
import {OnboardingStackParams} from '../../routes';
-import {StackNavigationProp} from '@react-navigation/stack';
-import {getTokenOrLogout, userLogin} from '../../utils';
import {fcmService, postMomentCategories} from '../../services';
-import {updateMomentCategories} from '../../store/actions/momentCategories';
-import {ScrollView} from 'react-native-gesture-handler';
+import {
+ updateMomentCategories,
+ updateIsOnboardedUser,
+} from '../../store/actions/';
+import {RootState} from '../../store/rootReducer';
+import {BackgroundGradientType, CategorySelectionScreenType} from '../../types';
+import {getTokenOrLogout, SCREEN_WIDTH, userLogin} from '../../utils';
type CategorySelectionRouteProps = RouteProp<
OnboardingStackParams,
@@ -47,17 +47,47 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
/**
* Same component to be used for category selection while onboarding and while on profile
*/
- const {categories, screenType, user} = route.params;
+ const {screenType, user} = route.params;
const isOnBoarding: boolean =
screenType === CategorySelectionScreenType.Onboarding;
const {userId, username} = user;
- const [selectedCategories, setSelectedCategories] = useState<
- Array<MomentCategoryType>
+ // During onboarding this will fail and default to []
+ const {momentCategories = []} = useSelector(
+ (state: RootState) => state.momentCategories,
+ );
+
+ // Stores all the categories that will be saved to the store
+ const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
+
+ /**
+ * Stores all the custom categories for the UI, allow easier logic for
+ * unchecking a custom category.
+ *
+ * Each uncommited custom category should also have a copy in selectedCategories
+ * since that's the final value that will be stored in the store.
+ */
+ const [uncommitedCustomCategories, setUncommitedCustomCategories] = useState<
+ string[]
>([]);
+ const customCategories = momentCategories.filter(
+ (mc) => !MOMENT_CATEGORIES.includes(mc),
+ );
+
const dispatch = useDispatch();
+ useEffect(() => {
+ const newCustomCategory = route.params.newCustomCategory;
+ if (newCustomCategory) {
+ setUncommitedCustomCategories([
+ ...uncommitedCustomCategories,
+ newCustomCategory,
+ ]);
+ selectedCategories.push(newCustomCategory);
+ }
+ }, [route.params?.newCustomCategory]);
+
/**
* Show the tutorial if a new user is OnBoarding
*/
@@ -67,13 +97,8 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
popupProps: {
messageHeader: 'Category And Moments',
messageBody:
- 'Use pictures and videos to share different aspects of you',
- next: {
- messageHeader: 'Select Categories',
- messageBody:
- 'Select between 2 - 6 categories to begin creating moments!',
- next: undefined,
- },
+ 'Use pictures and videos to share \ndifferent aspects of you',
+ next: undefined,
},
});
}
@@ -89,11 +114,13 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
* Remove from the selected categories
*/
const onSelect = (
- category: MomentCategoryType,
+ category: string,
isSelected: boolean,
isAdded: boolean,
) => {
- if (isAdded) return;
+ if (isAdded) {
+ return;
+ }
if (isSelected) {
setSelectedCategories((prev) => [...prev, category]);
} else {
@@ -104,40 +131,51 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
};
/**
- * if onboarding
- * Count of already added categories will always be 0
- * else
- * Calculate number of selected categories by iterating through the user's pre-selected categories
+ * Handle deselection of custom category.
+ *
+ * Custom categories is "added" and "selected" by CreateCustomCategory screen.
+ * User can only "deselect" an uncommited custom category.
+ *
+ * case isAdded || isSelected:
+ * Return without doing anything
+ * default:
+ * Remove from selected categories AND uncommitedCustomCategories
*/
- const addedLength = !isOnBoarding
- ? Object.keys(categories).filter((key) => {
- return categories[key as MomentCategoryType] === true;
- }).length
- : 0;
+ const onDeselectCustomCategory = (
+ category: string,
+ isSelected: boolean,
+ isAdded: boolean,
+ ) => {
+ if (isAdded || isSelected) {
+ return;
+ }
+ setSelectedCategories(
+ selectedCategories.filter((item) => item !== category),
+ );
+ setUncommitedCustomCategories(
+ uncommitedCustomCategories.filter((item) => item !== category),
+ );
+ };
const handleButtonPress = async () => {
- /**
- * Check for lower and upper bound before creating new categories
- */
- const totalCategories = addedLength + selectedCategories.length;
- if (totalCategories < 2) {
- Alert.alert('Please select atleast 2 categories');
- return;
- } else if (totalCategories > 6) {
- Alert.alert('You may not add more than 6 categories');
- return;
- } else if (selectedCategories.length === 0) {
- Alert.alert('Please select some categories!');
+ if (momentCategories.length + selectedCategories.length === 0) {
+ Alert.alert('Please select at least 1 category');
return;
}
try {
if (isOnBoarding) {
+ dispatch(updateIsOnboardedUser(true));
const token = await getTokenOrLogout(dispatch);
await postMomentCategories(selectedCategories, token);
userLogin(dispatch, {userId: userId, username: username});
fcmService.sendFcmTokenToServer();
} else {
- dispatch(updateMomentCategories(selectedCategories, true, userId));
+ dispatch(
+ updateMomentCategories(
+ momentCategories.concat(selectedCategories),
+ true,
+ ),
+ );
navigation.goBack();
}
} catch (error) {
@@ -155,15 +193,55 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({
style={styles.container}
gradientType={BackgroundGradientType.Dark}>
<StatusBar barStyle="light-content" />
- <Text style={styles.subtext}>Create new categories</Text>
+ <Text style={styles.subtext}>Create Categories</Text>
<View style={styles.container}>
+ {!isOnBoarding && (
+ <TouchableOpacity
+ style={styles.createCategory}
+ onPress={() => {
+ navigation.push('CreateCustomCategory', {
+ screenType,
+ user,
+ existingCategories: momentCategories.concat(
+ selectedCategories,
+ ),
+ });
+ }}>
+ <PlusIcon width={30} height={30} color="white" />
+ <Text style={styles.createCategoryLabel}>
+ Create your own category
+ </Text>
+ </TouchableOpacity>
+ )}
<View style={styles.linkerContainer}>
+ {/* commited custom categories */}
+ {customCategories.map((category, index) => (
+ <MomentCategory
+ key={index}
+ categoryType={category}
+ isSelected={false}
+ isAdded={true}
+ onSelect={onDeselectCustomCategory}
+ />
+ ))}
+ {/* uncommited custom categroies */}
+ {uncommitedCustomCategories.map((category, index) => (
+ <MomentCategory
+ key={index}
+ categoryType={category}
+ isSelected={selectedCategories.includes(category)}
+ isAdded={false}
+ onSelect={onDeselectCustomCategory}
+ />
+ ))}
+ {customCategories.length + uncommitedCustomCategories.length !==
+ 0 && <View style={styles.divider} />}
{MOMENT_CATEGORIES.map((category, index) => (
<MomentCategory
key={index}
categoryType={category}
isSelected={selectedCategories.includes(category)}
- isAdded={categories[category]}
+ isAdded={momentCategories.includes(category)}
onSelect={onSelect}
/>
))}
@@ -215,11 +293,12 @@ const styles = StyleSheet.create({
},
subtext: {
color: '#fff',
- fontSize: 16,
+ fontSize: 20,
fontWeight: '600',
textAlign: 'center',
marginVertical: '8%',
marginHorizontal: '10%',
+ marginTop: '15%',
},
finalAction: {
backgroundColor: 'white',
@@ -237,6 +316,31 @@ const styles = StyleSheet.create({
fontWeight: '500',
color: 'black',
},
+ createCategory: {
+ backgroundColor: '#53329B',
+ width: SCREEN_WIDTH * 0.9,
+ height: 70,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 10,
+ flexDirection: 'row',
+ marginBottom: '5%',
+ },
+ createCategoryLabel: {
+ color: 'white',
+ marginLeft: '3%',
+ fontSize: 18,
+ fontWeight: '500',
+ },
+ plusIcon: {
+ color: 'white',
+ },
+ divider: {
+ borderColor: 'white',
+ borderBottomWidth: 1,
+ width: SCREEN_WIDTH * 0.9,
+ marginVertical: '2%',
+ },
});
export default CategorySelection;
diff --git a/src/screens/onboarding/CreateCustomCategory.tsx b/src/screens/onboarding/CreateCustomCategory.tsx
new file mode 100644
index 00000000..eab72c7d
--- /dev/null
+++ b/src/screens/onboarding/CreateCustomCategory.tsx
@@ -0,0 +1,123 @@
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useState} from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TextInput,
+ TouchableOpacity,
+} from 'react-native';
+import {Background} from '../../components';
+import {OnboardingStackParams} from '../../routes';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+
+type CreateCustomCategoryRouteProps = RouteProp<
+ OnboardingStackParams,
+ 'CreateCustomCategory'
+>;
+
+type CreateCustomCategoryNavigationProps = StackNavigationProp<
+ OnboardingStackParams,
+ 'CreateCustomCategory'
+>;
+
+interface CreateCustomCategoryProps {
+ route: CreateCustomCategoryRouteProps;
+ navigation: CreateCustomCategoryNavigationProps;
+}
+
+const CreateCustomCategory: React.FC<CreateCustomCategoryProps> = ({
+ route,
+ navigation,
+}) => {
+ /**
+ * Same component to be used for category selection while onboarding and while on profile
+ */
+ const {existingCategories} = route.params;
+ const [newCategory, setNewCategory] = useState('');
+
+ const handleButtonPress = () => {
+ if (existingCategories.includes(newCategory)) {
+ Alert.alert('Looks like you already have that one created!');
+ } else {
+ navigation.navigate('CategorySelection', {
+ screenType: route.params.screenType,
+ user: route.params.user,
+ newCustomCategory: newCategory,
+ });
+ }
+ };
+
+ return (
+ <>
+ <StatusBar barStyle="light-content" />
+ <Background
+ style={styles.container}
+ gradientType={BackgroundGradientType.Dark}>
+ <KeyboardAvoidingView
+ style={styles.innerContainer}
+ behavior={'padding'}>
+ <Text style={styles.title}>Give your category a name</Text>
+ <TextInput
+ style={styles.input}
+ selectionColor={'white'}
+ onChangeText={setNewCategory}
+ autoFocus={true}
+ />
+ <TouchableOpacity
+ onPress={handleButtonPress}
+ style={styles.finalAction}>
+ <Text style={styles.finalActionLabel}>{'Create'}</Text>
+ </TouchableOpacity>
+ </KeyboardAvoidingView>
+ </Background>
+ </>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ minHeight: SCREEN_HEIGHT,
+ },
+ innerContainer: {
+ height: '40%',
+ top: '20%',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ },
+ title: {
+ color: 'white',
+ fontSize: 20,
+ fontWeight: '600',
+ },
+ input: {
+ width: SCREEN_WIDTH * 0.75,
+ fontSize: 30,
+ color: 'white',
+ textAlign: 'center',
+ borderBottomWidth: 1,
+ borderBottomColor: 'white',
+ },
+ finalAction: {
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 150,
+ height: 40,
+ borderRadius: 5,
+ borderWidth: 1,
+ borderColor: '#8F01FF',
+ },
+ finalActionLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: 'black',
+ },
+});
+
+export default CreateCustomCategory;
diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx
index a9d1c12e..cc7cd678 100644
--- a/src/screens/onboarding/InvitationCodeVerification.tsx
+++ b/src/screens/onboarding/InvitationCodeVerification.tsx
@@ -10,7 +10,10 @@ import {
LoadingIndicator,
} from '../../components';
-import {VERIFY_INVITATION_CODE_ENDPOUNT} from '../../constants';
+import {
+ TAGG_LIGHT_PURPLE,
+ VERIFY_INVITATION_CODE_ENDPOUNT,
+} from '../../constants';
import {Text} from 'react-native-animatable';
import {
@@ -83,6 +86,10 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
}
};
+ const navigateToAddWaitList = () => {
+ navigation.navigate('AddWaitlistUser');
+ };
+
const Footer = () => (
<View style={styles.footer}>
<ArrowButton
@@ -131,6 +138,13 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
accessibilityHint="Select this after entering your invitation code"
onPress={handleInvitationCodeVerification}
/>
+ <View style={styles.noInviteCode}>
+ <Text style={styles.inviteCodeText}>Don't have an invite? </Text>
+ <Text style={styles.inviteCodeLink} onPress={navigateToAddWaitList}>
+ {' '}
+ Join the Waitlist
+ </Text>
+ </View>
<LoadingIndicator />
</KeyboardAvoidingView>
<Footer />
@@ -206,6 +220,19 @@ const styles = StyleSheet.create({
},
}),
},
+ noInviteCode: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ inviteCodeText: {
+ color: TAGG_LIGHT_PURPLE,
+ fontSize: 18,
+ },
+ inviteCodeLink: {
+ color: 'white',
+ fontSize: 18,
+ textDecorationLine: 'underline',
+ },
});
export default InvitationCodeVerification;
diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx
index 3e59b00e..d1717fc1 100644
--- a/src/screens/onboarding/Login.tsx
+++ b/src/screens/onboarding/Login.tsx
@@ -15,17 +15,16 @@ import {
import {fcmService} from '../../services';
import {OnboardingStackParams} from '../../routes/onboarding';
import {Background, TaggInput, SubmitButton} from '../../components';
-import {usernameRegex, LOGIN_ENDPOINT} from '../../constants';
-import AsyncStorage from '@react-native-community/async-storage';
import {
- BackgroundGradientType,
- CategorySelectionScreenType,
- UserType,
-} from '../../types';
+ usernameRegex,
+ LOGIN_ENDPOINT,
+ TAGG_LIGHT_PURPLE,
+} from '../../constants';
+import AsyncStorage from '@react-native-community/async-storage';
+import {BackgroundGradientType, UserType} from '../../types';
import {useDispatch} from 'react-redux';
import {userLogin} from '../../utils';
import SplashScreen from 'react-native-splash-screen';
-import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates';
type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>;
type VerificationScreenNavigationProp = StackNavigationProp<
@@ -356,7 +355,7 @@ const styles = StyleSheet.create({
},
newUser: {
fontSize: 14,
- color: '#f4ddff',
+ color: TAGG_LIGHT_PURPLE,
},
getStarted: {
fontSize: 14,
diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx
index 70550f36..1f8e58da 100644
--- a/src/screens/onboarding/ProfileOnboarding.tsx
+++ b/src/screens/onboarding/ProfileOnboarding.tsx
@@ -147,43 +147,51 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({
const goToGalleryLargePic = () => {
ImagePicker.openPicker({
- smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'],
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
width: 580,
height: 580,
cropping: true,
cropperToolbarTitle: 'Select Header',
mediaType: 'photo',
- })
- .then((picture) => {
- if ('path' in picture) {
- setForm({
- ...form,
- largePic: picture.path,
- });
- }
- })
- .catch(() => {});
+ }).then((picture) => {
+ if ('path' in picture) {
+ setForm({
+ ...form,
+ largePic: picture.path,
+ });
+ }
+ });
};
const goToGallerySmallPic = () => {
ImagePicker.openPicker({
- smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'],
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
width: 580,
height: 580,
cropping: true,
cropperToolbarTitle: 'Select Profile Picture',
mediaType: 'photo',
cropperCircleOverlay: true,
- })
- .then((picture) => {
- if ('path' in picture) {
- setForm({
- ...form,
- smallPic: picture.path,
- });
- }
- })
- .catch(() => {});
+ }).then((picture) => {
+ if ('path' in picture) {
+ setForm({
+ ...form,
+ smallPic: picture.path,
+ });
+ }
+ });
};
/*
diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx
index d2a43e7a..2a978f94 100644
--- a/src/screens/onboarding/SocialMedia.tsx
+++ b/src/screens/onboarding/SocialMedia.tsx
@@ -2,7 +2,6 @@ import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
import React from 'react';
import {
- Alert,
KeyboardAvoidingView,
Platform,
StatusBar,
@@ -22,9 +21,8 @@ import {
LinkSocialMedia,
RegistrationWizard,
} from '../../components';
-import {SOCIAL_LIST} from '../../constants/';
+import {SOCIAL_LIST, MOMENT_CATEGORIES} from '../../constants/';
import {OnboardingStackParams} from '../../routes';
-import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates';
/**
* Social Media Screen for displaying social media linkers
@@ -55,8 +53,6 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => {
linkers.push(linker);
}
- const dispatch = useDispatch();
-
/**
* Just commenting this out, in case we need it in the future
*/
@@ -69,9 +65,9 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => {
const handleNext = () => {
navigation.navigate('CategorySelection', {
- categories: MOMENT_CATEGORIES_MAP,
screenType: CategorySelectionScreenType.Onboarding,
user: {userId: userId, username: username},
+ newCustomCategory: undefined,
});
};
diff --git a/src/screens/onboarding/WaitlistSuccessScreen.tsx b/src/screens/onboarding/WaitlistSuccessScreen.tsx
new file mode 100644
index 00000000..1f603e3a
--- /dev/null
+++ b/src/screens/onboarding/WaitlistSuccessScreen.tsx
@@ -0,0 +1,156 @@
+import {StackNavigationProp} from '@react-navigation/stack';
+import * as React from 'react';
+import {
+ KeyboardAvoidingView,
+ Linking,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {ArrowButton, Background, SubmitButton} from '../../components';
+import {OnboardingStackParams} from '../../routes';
+import {BackgroundGradientType} from '../../types';
+import CelebrationLogo from '../../assets/icons/celebration-logo.svg';
+import {SCREEN_HEIGHT} from '../../utils';
+import {TAGG_WEBSITE} from '../../constants';
+
+type WaitlistSuccessScreenProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'WaitlistSuccess'
+>;
+
+interface WaitlistSuccessScreenProps {
+ navigation: WaitlistSuccessScreenProp;
+}
+
+const WaitlistSuccessScreen: React.FC<WaitlistSuccessScreenProps> = ({
+ navigation,
+}) => {
+ const handleSignIn = () => {
+ navigation.navigate('InvitationCodeVerification');
+ };
+
+ const Footer = () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() => navigation.navigate('AddWaitlistUser')}
+ />
+ </View>
+ );
+ return (
+ <Background
+ style={styles.container}
+ gradientType={BackgroundGradientType.Light}>
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.container}>
+ <StatusBar barStyle="light-content" />
+ <CelebrationLogo width={100} height={100} />
+ <Text style={styles.heading}>
+ You've successfully joined{'\n'}
+ the waitlist, we'll let you know{'\n'}
+ as soon as your invite is{'\n'}ready!
+ </Text>
+ <Text style={[styles.subHeading, styles.subHeadOneMargin]}>
+ To learn more about Tagg you can visit our{'\n'}{' '}
+ <Text
+ style={styles.link}
+ onPress={() => {
+ Linking.openURL(TAGG_WEBSITE);
+ }}>
+ website
+ </Text>
+ . Thank you!
+ </Text>
+ <Text style={[styles.subHeading, styles.subHeadTwoMargin]}>
+ Got your invite text?
+ </Text>
+ <TouchableOpacity onPress={handleSignIn} style={styles.finalAction}>
+ <Text style={styles.finalActionLabel}>Sign In</Text>
+ </TouchableOpacity>
+ </KeyboardAvoidingView>
+ <Footer />
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ wizard: {
+ ...Platform.select({
+ ios: {
+ top: 50,
+ },
+ android: {
+ bottom: 40,
+ },
+ }),
+ },
+ link: {
+ textDecorationLine: 'underline',
+ },
+ finalAction: {
+ backgroundColor: 'white',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 150,
+ height: 40,
+ borderRadius: 5,
+ borderWidth: 1,
+ borderColor: '#fff',
+ marginBottom: SCREEN_HEIGHT / 20,
+ marginTop: SCREEN_HEIGHT / 45,
+ },
+ finalActionLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: 'black',
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 30,
+ fontWeight: '600',
+ marginBottom: '16%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+ heading: {
+ fontSize: 25,
+ fontWeight: 'bold',
+ color: 'white',
+ marginTop: SCREEN_HEIGHT / 25,
+ textAlign: 'center',
+ },
+ subHeading: {
+ color: 'white',
+ textAlign: 'center',
+ },
+ subHeadOneMargin: {
+ marginTop: SCREEN_HEIGHT / 30,
+ },
+ subHeadTwoMargin: {
+ marginTop: SCREEN_HEIGHT / 10,
+ },
+});
+
+export default WaitlistSuccessScreen;
diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts
index ec833929..14d0e405 100644
--- a/src/screens/onboarding/index.ts
+++ b/src/screens/onboarding/index.ts
@@ -11,3 +11,6 @@ export {default as PasswordResetRequest} from './PasswordResetRequest';
export {default as PasswordReset} from './PasswordReset';
export {default as WelcomeScreen} from './WelcomeScreen';
export {default as CategorySelection} from './CategorySelection';
+export {default as AddWaitlistUserScreen} from './AddWaitlistUserScreen';
+export {default as WaitlistSuccessScreen} from './WaitlistSuccessScreen';
+export {default as CreateCustomCategory} from './CreateCustomCategory';
diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx
index e9eed668..5537d6bf 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -14,20 +14,24 @@ import {SearchBackground, TaggBigInput} from '../../components';
import {SCREEN_WIDTH, StatusBarHeight} from '../../utils';
import AsyncStorage from '@react-native-community/async-storage';
import {RouteProp} from '@react-navigation/native';
-import {ProfileStackParams} from 'src/routes';
+import {MainStackParams} from 'src/routes';
import {StackNavigationProp} from '@react-navigation/stack';
import {CaptionScreenHeader} from '../../components/';
import {MOMENTS_ENDPOINT} from '../../constants';
import {useDispatch, useSelector} from 'react-redux';
-import {loadUserMoments} from '../../store/actions';
+import {
+ loadUserMoments,
+ updateProfileCompletionStage,
+} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
+import {postMoment} from '../../services';
/**
* Upload Screen to allow users to upload posts to Tagg
*/
-type CaptionScreenRouteProp = RouteProp<ProfileStackParams, 'CaptionScreen'>;
+type CaptionScreenRouteProp = RouteProp<MainStackParams, 'CaptionScreen'>;
type CaptionScreenNavigationProp = StackNavigationProp<
- ProfileStackParams,
+ MainStackParams,
'CaptionScreen'
>;
interface CaptionScreenProps {
@@ -47,15 +51,6 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
setCaption(caption);
};
- const checkImageUploadStatus = (statusMap: object) => {
- for (let [key, value] of Object.entries(statusMap)) {
- if (value != 'Success') {
- return false;
- }
- }
- return true;
- };
-
const navigateToProfile = () => {
//Since the logged In User is navigating to own profile, useXId is not required
navigation.navigate('Profile', {
@@ -66,43 +61,20 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => {
const handleShare = async () => {
try {
- const request = new FormData();
- const uri = image.path;
- var fileName = image.filename;
-
- //Manipulating filename to end with .jpg instead of .heic
- if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) {
- fileName = fileName.split('.')[0] + '.jpg';
- }
- request.append('image', {
- uri: uri,
- name: fileName,
- type: 'image/jpg',
- });
- request.append('moment', title);
- request.append('user_id', userId);
- request.append('captions', JSON.stringify({image: caption}));
-
- const token = await AsyncStorage.getItem('token');
- let response = await fetch(MOMENTS_ENDPOINT, {
- method: 'POST',
- headers: {
- 'Content-Type': 'multipart/form-data',
- Authorization: 'Token ' + token,
- },
- body: request,
- });
- let statusCode = response.status;
- let data = await response.json();
- if (statusCode === 200 && checkImageUploadStatus(data)) {
- Alert.alert('The picture was uploaded successfully!');
+ const data = await postMoment(
+ image.filename,
+ image.path,
+ caption,
+ title,
+ userId,
+ );
+ if (data) {
dispatch(loadUserMoments(userId));
+ dispatch(updateProfileCompletionStage(data));
navigateToProfile();
- } else {
- Alert.alert('An error occured while uploading. Please try again!');
}
} catch (err) {
- Alert.alert('An error occured during authenticaion. Please login again!');
+ console.log(err);
}
};
diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx
index ed012313..a6849c7a 100644
--- a/src/screens/profile/EditProfile.tsx
+++ b/src/screens/profile/EditProfile.tsx
@@ -131,43 +131,51 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => {
const goToGalleryLargePic = () => {
ImagePicker.openPicker({
- smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'],
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
width: 580,
height: 580,
cropping: true,
cropperToolbarTitle: 'Select Header',
mediaType: 'photo',
- })
- .then((picture) => {
- if ('path' in picture) {
- setForm({
- ...form,
- largePic: picture.path,
- });
- }
- })
- .catch(() => {});
+ }).then((picture) => {
+ if ('path' in picture) {
+ setForm({
+ ...form,
+ largePic: picture.path,
+ });
+ }
+ });
};
const goToGallerySmallPic = () => {
ImagePicker.openPicker({
- smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'],
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
width: 580,
height: 580,
cropping: true,
cropperToolbarTitle: 'Select Profile Picture',
mediaType: 'photo',
cropperCircleOverlay: true,
- })
- .then((picture) => {
- if ('path' in picture) {
- setForm({
- ...form,
- smallPic: picture.path,
- });
- }
- })
- .catch(() => {});
+ }).then((picture) => {
+ if ('path' in picture) {
+ setForm({
+ ...form,
+ smallPic: picture.path,
+ });
+ }
+ });
};
/*
diff --git a/src/screens/profile/MomentUploadPromptScreen.tsx b/src/screens/profile/MomentUploadPromptScreen.tsx
new file mode 100644
index 00000000..6111985d
--- /dev/null
+++ b/src/screens/profile/MomentUploadPromptScreen.tsx
@@ -0,0 +1,114 @@
+import * as React from 'react';
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import {MainStackParams} from '../../routes';
+import CloseIcon from '../../assets/ionicons/close-outline.svg';
+import {StyleSheet, Text, View} from 'react-native';
+import {Moment} from '../../components';
+import {Image} from 'react-native-animatable';
+
+type MomentUploadPromptScreenRouteProp = RouteProp<
+ MainStackParams,
+ 'MomentUploadPrompt'
+>;
+type MomentUploadPromptScreenNavigationProp = StackNavigationProp<
+ MainStackParams,
+ 'MomentUploadPrompt'
+>;
+
+interface MomentUploadPromptScreenProps {
+ route: MomentUploadPromptScreenRouteProp;
+ navigation: MomentUploadPromptScreenNavigationProp;
+}
+
+const MomentUploadPromptScreen: React.FC<MomentUploadPromptScreenProps> = ({
+ route,
+ navigation,
+}) => {
+ const {screenType, momentCategory} = route.params;
+ return (
+ <View style={styles.container}>
+ <CloseIcon
+ height={'10%'}
+ width={'10%'}
+ color={'white'}
+ style={styles.closeButton}
+ onPress={() => {
+ navigation.goBack();
+ }}
+ />
+
+ <Text style={styles.text}>
+ Post your first moment to {'\n'} continue building your digital {'\n'}{' '}
+ identity!
+ </Text>
+ <Image
+ source={require('../../assets/gifs/dotted-arrow-white.gif')}
+ style={styles.arrowGif}
+ />
+ <Moment
+ key={1}
+ title={momentCategory}
+ images={[]}
+ userXId={undefined}
+ screenType={screenType}
+ handleMomentCategoryDelete={() => {}}
+ shouldAllowDeletion={false}
+ externalStyles={{
+ container: styles.momentContainer,
+ titleText: styles.momentHeaderText,
+ header: styles.momentHeader,
+ scrollContainer: styles.momentScrollContainer,
+ }}
+ />
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'column',
+ justifyContent: 'center',
+ },
+ closeButton: {
+ position: 'relative',
+ height: '48%',
+ aspectRatio: 1,
+ top: 20,
+ },
+ text: {
+ justifyContent: 'center',
+ color: '#fff',
+ fontWeight: 'bold',
+ fontSize: 20,
+ textAlign: 'center',
+ position: 'relative',
+ top: '40%',
+ },
+ arrowGif: {
+ position: 'relative',
+ width: '25%',
+ height: '40%',
+ left: '40%',
+ aspectRatio: 1.2,
+ top: '50%',
+ transform: [{scaleX: -1}, {rotate: '15deg'}],
+ },
+
+ //Styles to adjust moment container
+ momentScrollContainer: {
+ backgroundColor: 'transparent',
+ },
+ momentContainer: {
+ top: '62%',
+ backgroundColor: 'transparent',
+ },
+ momentHeaderText: {
+ paddingBottom: '5%',
+ },
+ momentHeader: {
+ backgroundColor: 'transparent',
+ },
+});
+
+export default MomentUploadPromptScreen;
diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx
index 1b9a1049..0ea96cd2 100644
--- a/src/screens/profile/ProfileScreen.tsx
+++ b/src/screens/profile/ProfileScreen.tsx
@@ -3,7 +3,7 @@ import {StatusBar} from 'react-native';
import Animated from 'react-native-reanimated';
import {Content, Cover, TabsGradient} from '../../components';
import {RouteProp, useFocusEffect} from '@react-navigation/native';
-import {ProfileStackParams} from '../../routes/';
+import {MainStackParams} from '../../routes/';
import {resetScreenType} from '../../store/actions';
import {useDispatch, useStore} from 'react-redux';
import {DUMMY_USERID} from '../../store/initialStates';
@@ -13,7 +13,7 @@ import {DUMMY_USERID} from '../../store/initialStates';
* including posts, messaging, and settings
*/
-type ProfileScreenRouteProps = RouteProp<ProfileStackParams, 'Profile'>;
+type ProfileScreenRouteProps = RouteProp<MainStackParams, 'Profile'>;
interface ProfileOnboardingProps {
route: ProfileScreenRouteProps;
diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts
index b6a13144..9d651729 100644
--- a/src/screens/profile/index.ts
+++ b/src/screens/profile/index.ts
@@ -5,3 +5,4 @@ export {default as IndividualMoment} from './IndividualMoment';
export {default as MomentCommentsScreen} from './MomentCommentsScreen';
export {default as FriendsListScreen} from './FriendsListScreen';
export {default as EditProfile} from './EditProfile';
+export {default as MomentUploadPromptScreen} from './MomentUploadPromptScreen';
diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx
index 78c0c5cc..4505163c 100644
--- a/src/screens/search/SearchScreen.tsx
+++ b/src/screens/search/SearchScreen.tsx
@@ -1,9 +1,17 @@
import AsyncStorage from '@react-native-community/async-storage';
-import React, {useEffect, useState} from 'react';
-import {Keyboard, ScrollView, StatusBar, StyleSheet} from 'react-native';
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useCallback, useEffect, useState} from 'react';
+import {
+ Keyboard,
+ RefreshControl,
+ ScrollView,
+ StatusBar,
+ StyleSheet,
+} from 'react-native';
import Animated, {Easing, timing} from 'react-native-reanimated';
+import {useDispatch, useSelector} from 'react-redux';
import {
- DiscoverUsers,
+ Explore,
RecentSearches,
SearchBackground,
SearchBar,
@@ -13,6 +21,8 @@ import {
TabsGradient,
} from '../../components';
import {SEARCH_ENDPOINT, TAGG_TEXT_LIGHT_BLUE} from '../../constants';
+import {loadRecentlySearched, resetScreenType} from '../../store/actions';
+import {RootState} from '../../store/rootReducer';
import {ProfilePreviewType, ScreenType, UserType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
const NO_USER: UserType = {
@@ -20,18 +30,13 @@ const NO_USER: UserType = {
username: '',
};
-import {RootState} from '../../store/rootReducer';
-import {useSelector, useDispatch} from 'react-redux';
-import {resetScreenType} from '../../store/actions';
-import {useFocusEffect} from '@react-navigation/native';
-
/**
* Search Screen for user recommendations and a search
* tool to allow user to find other users
*/
const SearchScreen: React.FC = () => {
- const {recentSearches, taggUsers} = useSelector(
+ const {recentSearches, explores} = useSelector(
(state: RootState) => state.taggUsers,
);
const [query, setQuery] = useState<string>('');
@@ -42,6 +47,19 @@ const SearchScreen: React.FC = () => {
const [searching, setSearching] = useState(false);
const top = Animated.useValue(-SCREEN_HEIGHT);
const [user, setUser] = useState<UserType>(NO_USER);
+ const [refreshing, setRefreshing] = useState<boolean>(false);
+
+ const dispatch = useDispatch();
+
+ const onRefresh = useCallback(() => {
+ const refrestState = async () => {
+ dispatch(loadRecentlySearched());
+ };
+ setRefreshing(true);
+ refrestState().then(() => {
+ setRefreshing(false);
+ });
+ }, []);
useEffect(() => {
if (query.length < 3) {
@@ -76,8 +94,6 @@ const SearchScreen: React.FC = () => {
loadResults(query);
}, [query]);
- const dispatch = useDispatch();
-
/**
* Code under useFocusEffect gets executed every time the screen comes under focus / is being viewed by the user.
* This is done to reset the users stored in our store for the Search screen.
@@ -135,7 +151,10 @@ const SearchScreen: React.FC = () => {
keyboardShouldPersistTaps={'always'}
stickyHeaderIndices={[4]}
contentContainerStyle={styles.contentContainer}
- showsVerticalScrollIndicator={false}>
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
+ }>
<SearchHeader style={styles.header} {...{top}} />
<SearchBar
style={styles.searchBar}
@@ -146,13 +165,7 @@ const SearchScreen: React.FC = () => {
value={query}
{...{top, searching}}
/>
- {/* Removed for Alpha for now */}
- {/* <Explore /> */}
- <DiscoverUsers
- sectionTitle="Discover Users"
- users={taggUsers}
- screenType={ScreenType.Search}
- />
+ <Explore />
<SearchResultsBackground {...{top}}>
{results.length === 0 && recents.length !== 0 ? (
<RecentSearches
diff --git a/src/services/ExploreServices.ts b/src/services/ExploreServices.ts
index 2181ea7d..ca4f1b69 100644
--- a/src/services/ExploreServices.ts
+++ b/src/services/ExploreServices.ts
@@ -1,4 +1,8 @@
-import {ALL_USERS_ENDPOINT} from '../constants';
+import AsyncStorage from '@react-native-community/async-storage';
+import {getDeviceToken} from 'react-native-device-info';
+import {ALL_USERS_ENDPOINT, DISCOVER_ENDPOINT} from '../constants';
+import {EMPTY_EXPLORE_SECTIONS} from '../store/initialStates';
+import {ExploreSectionType, ProfilePreviewType} from '../types';
export const getAllTaggUsers = async (token: string) => {
try {
@@ -26,3 +30,31 @@ export const getAllTaggUsers = async (token: string) => {
);
}
};
+
+export const getAllExploreSections = async () => {
+ try {
+ const token = await AsyncStorage.getItem('token');
+ const response = await fetch(DISCOVER_ENDPOINT, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + token,
+ },
+ });
+ if (response.status !== 200) {
+ return EMPTY_EXPLORE_SECTIONS;
+ }
+ const data = await response.json();
+ const exploreSections: Record<ExploreSectionType, ProfilePreviewType[]> = {
+ 'New to Tagg': data.categories.new_to_tagg,
+ 'People You May Know': data.categories.people_you_may_know,
+ 'Trending on Tagg': data.categories.trending_on_tagg,
+ "Brown '21": data.categories.brown_21,
+ "Brown '22": data.categories.brown_22,
+ "Brown '23": data.categories.brown_23,
+ };
+
+ return exploreSections;
+ } catch (error) {
+ console.log('Unable to fetch explore data');
+ }
+};
diff --git a/src/services/MomentCategoryService.ts b/src/services/MomentCategoryService.ts
index 8bdb70d2..57e64830 100644
--- a/src/services/MomentCategoryService.ts
+++ b/src/services/MomentCategoryService.ts
@@ -1,12 +1,11 @@
import {Alert} from 'react-native';
-import {MomentCategoryType} from './../types/types';
import {MOMENT_CATEGORY_ENDPOINT} from '../constants';
export const loadMomentCategories: (
userId: string,
token: string,
-) => Promise<MomentCategoryType[]> = async (userId, token) => {
- let categories: MomentCategoryType[] = [];
+) => Promise<string[]> = async (userId, token) => {
+ let categories: string[] = [];
try {
const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, {
method: 'GET',
@@ -17,7 +16,7 @@ export const loadMomentCategories: (
const status = response.status;
if (status === 200) {
const data = await response.json();
- categories = data['categories'];
+ categories = data.categories;
} else {
console.log('Could not load categories!');
return [];
@@ -30,10 +29,9 @@ export const loadMomentCategories: (
};
export const postMomentCategories: (
- categories: Array<MomentCategoryType>,
+ categories: string[],
token: string,
-) => Promise<boolean> = async (categories, token) => {
- let success = false;
+) => Promise<number | undefined> = async (categories, token) => {
try {
const response = await fetch(MOMENT_CATEGORY_ENDPOINT, {
method: 'POST',
@@ -44,45 +42,16 @@ export const postMomentCategories: (
body: JSON.stringify({categories}),
});
const status = response.status;
+ const data = await response.json();
if (status === 200) {
- success = true;
+ return data['profile_completion_stage'];
} else {
- Alert.alert('There was a problem creating categories!');
- console.log('Could not post categories!');
+ Alert.alert('There was a problem updating categories!');
+ console.log('Unable to update categories');
}
} catch (err) {
console.log(err);
- return success;
+ return undefined;
}
- return success;
-};
-
-export const deleteMomentCategories: (
- categories: Array<MomentCategoryType>,
- userId: string,
- token: string,
-) => Promise<boolean> = async (categories, userId, token) => {
- let success = false;
- try {
- const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Token ' + token,
- },
- body: JSON.stringify({categories}),
- });
- const status = response.status;
- if (status === 200) {
- Alert.alert(`The category was successfully deleted!`);
- success = true;
- } else {
- Alert.alert('There was a problem while deleteing category!');
- console.log('Could not delete category!');
- }
- } catch (err) {
- console.log(err);
- return success;
- }
- return success;
+ return undefined;
};
diff --git a/src/services/MomentServices.ts b/src/services/MomentServices.ts
index 96643bc3..91ecf712 100644
--- a/src/services/MomentServices.ts
+++ b/src/services/MomentServices.ts
@@ -2,6 +2,7 @@ import AsyncStorage from '@react-native-community/async-storage';
import {Alert} from 'react-native';
import {COMMENTS_ENDPOINT, MOMENTS_ENDPOINT} from '../constants';
import {MomentType} from '../types';
+import {checkImageUploadStatus} from '../utils';
//Get all comments for a moment
export const getMomentComments = async (
@@ -97,6 +98,57 @@ export const getMomentCommentsCount = async (
}
};
+export const postMoment: (
+ fileName: string,
+ uri: string,
+ caption: string,
+ category: string,
+ userId: string,
+) => Promise<number | undefined> = async (
+ fileName,
+ uri,
+ caption,
+ category,
+ userId,
+) => {
+ try {
+ const request = new FormData();
+ //Manipulating filename to end with .jpg instead of .heic
+ if (fileName.endsWith('.heic') || fileName.endsWith('.HEIC')) {
+ fileName = fileName.split('.')[0] + '.jpg';
+ }
+ request.append('image', {
+ uri: uri,
+ name: fileName,
+ type: 'image/jpg',
+ });
+ request.append('moment', category);
+ request.append('user_id', userId);
+ request.append('captions', JSON.stringify({image: caption}));
+ const token = await AsyncStorage.getItem('token');
+ let response = await fetch(MOMENTS_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: 'Token ' + token,
+ },
+ body: request,
+ });
+ let statusCode = response.status;
+ let data = await response.json();
+ if (statusCode === 200 && checkImageUploadStatus(data['moments'])) {
+ Alert.alert('The picture was uploaded successfully!');
+ return data['profile_completion_stage'];
+ } else {
+ Alert.alert('An error occured while uploading. Please try again!');
+ }
+ } catch (err) {
+ console.log(err);
+ Alert.alert('An error occured during authenticaion. Please login again!');
+ }
+ return undefined;
+};
+
export const loadMoments: (
userId: string,
token: string,
diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts
index 75042830..793ee44d 100644
--- a/src/services/UserProfileService.ts
+++ b/src/services/UserProfileService.ts
@@ -38,6 +38,7 @@ export const loadProfileInfo = async (token: string, userId: string) => {
snapchat,
tiktok,
university_class,
+ profile_completion_stage,
} = info;
birthday = birthday && moment(birthday).format('YYYY-MM-DD');
return {
@@ -49,6 +50,7 @@ export const loadProfileInfo = async (token: string, userId: string) => {
snapchat,
tiktok,
university_class,
+ profile_completion_stage,
};
} else {
throw 'Unable to load profile data';
diff --git a/src/services/WaitlistUserService.tsx b/src/services/WaitlistUserService.tsx
new file mode 100644
index 00000000..516affe3
--- /dev/null
+++ b/src/services/WaitlistUserService.tsx
@@ -0,0 +1,45 @@
+import {Alert} from 'react-native';
+import {WAITLIST_USER_ENDPOINT} from '../constants';
+
+export const adduserToWaitlist: (
+ phone_number: string,
+ first_name: string,
+ last_name: string,
+) => Promise<boolean> = async (phone_number, first_name, last_name) => {
+ try {
+ console.log(phone_number, first_name, last_name);
+ const response = await fetch(WAITLIST_USER_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ phone_number,
+ first_name,
+ last_name,
+ }),
+ });
+ const status = response.status;
+ const message = await response.json();
+ if (status === 200) {
+ return true;
+ } else {
+ if (status === 409) {
+ Alert.alert('You are already on our waitlist / on our app');
+ } else if (status === 400) {
+ Alert.alert('Some information needed was missing / ill-formatted');
+ } else if (status === 500) {
+ Alert.alert(
+ 'Something went wrong. Sorry unable to add you to the waitlist 😔',
+ );
+ }
+ console.log(message);
+ }
+ } catch (err) {
+ Alert.alert(
+ 'Something went wrong. Sorry unable to add you to the waitlist 😔',
+ );
+ console.log(err);
+ }
+ return false;
+};
diff --git a/src/services/index.ts b/src/services/index.ts
index 7ea5bf5d..56cefddd 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -8,3 +8,4 @@ export * from './BlockUserService';
export * from './MomentCategoryService';
export * from './NotificationService';
export * from './FCMService';
+export * from './WaitlistUserService';
diff --git a/src/store/actions/momentCategories.tsx b/src/store/actions/momentCategories.tsx
index a522c3e0..c91e9ec8 100644
--- a/src/store/actions/momentCategories.tsx
+++ b/src/store/actions/momentCategories.tsx
@@ -1,13 +1,11 @@
import {RootState} from '../rootReducer';
-import {
- deleteMomentCategories,
- loadMomentCategories,
- postMomentCategories,
-} from '../../services';
+import {loadMomentCategories, postMomentCategories} from '../../services';
import {Action, ThunkAction} from '@reduxjs/toolkit';
-import {momentCategoriesFetched} from '../reducers';
+import {
+ momentCategoriesFetched,
+ profileCompletionStageUpdated,
+} from '../reducers';
import {getTokenOrLogout} from '../../utils';
-import {MomentCategoryType} from '../../types';
/**
* Load all categories for user
@@ -23,7 +21,7 @@ export const loadUserMomentCategories = (
const categories = await loadMomentCategories(userId, token);
dispatch({
type: momentCategoriesFetched.type,
- payload: {categories, add: true},
+ payload: {categories},
});
} catch (error) {
console.log(error);
@@ -33,29 +31,32 @@ export const loadUserMomentCategories = (
/**
* Handle addition / deletion of categories for a user
* @param categories List of categories
- * @param add boolean, if true, we add new categories, else we delete
- * @param userId id of the user for whom categories should be updated
+ * @param add true if the call to his function is to add categories
*/
export const updateMomentCategories = (
- categories: Array<MomentCategoryType>,
+ categories: string[],
add: boolean,
- userId: string,
): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async (
dispatch,
) => {
try {
const token = await getTokenOrLogout(dispatch);
let success = false;
- if (add) {
- success = await postMomentCategories(categories, token);
- } else {
- success = await deleteMomentCategories(categories, userId, token);
- }
+ let stage: number | undefined = 1;
+
+ stage = await postMomentCategories(categories, token);
+ success = stage ? true : false;
if (success) {
dispatch({
type: momentCategoriesFetched.type,
- payload: {categories, add},
+ payload: {categories},
});
+ if (add) {
+ dispatch({
+ type: profileCompletionStageUpdated.type,
+ payload: {stage},
+ });
+ }
}
} catch (error) {
console.log(error);
diff --git a/src/store/actions/taggUsers.ts b/src/store/actions/taggUsers.ts
index 7f841c51..7b6d4d5e 100644
--- a/src/store/actions/taggUsers.ts
+++ b/src/store/actions/taggUsers.ts
@@ -1,8 +1,7 @@
-import {RootState} from '../rootReducer';
-import {loadRecentlySearchedUsers, getAllTaggUsers} from '../../services';
import {Action, ThunkAction} from '@reduxjs/toolkit';
+import {getAllExploreSections, loadRecentlySearchedUsers} from '../../services';
import {taggUsersFetched} from '../reducers';
-import {getTokenOrLogout} from '../../utils';
+import {RootState} from '../rootReducer';
export const loadRecentlySearched = (): ThunkAction<
Promise<void>,
@@ -11,12 +10,11 @@ export const loadRecentlySearched = (): ThunkAction<
Action<string>
> => async (dispatch) => {
try {
- const token = await getTokenOrLogout(dispatch);
const recentSearches = await loadRecentlySearchedUsers();
- const taggUsers = await getAllTaggUsers(token);
+ const exploreSections = await getAllExploreSections();
dispatch({
type: taggUsersFetched.type,
- payload: {recentSearches, taggUsers},
+ payload: {recentSearches, explores: exploreSections},
});
} catch (error) {
console.log(error);
diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts
index eee5fcde..8550f3bd 100644
--- a/src/store/actions/user.ts
+++ b/src/store/actions/user.ts
@@ -2,7 +2,13 @@ import {RootState} from '../rootReducer';
import {UserType} from '../../types/types';
import {loadProfileInfo, loadAvatar, loadCover} from '../../services';
import {Action, ThunkAction} from '@reduxjs/toolkit';
-import {userLoggedIn, userDetailsFetched, socialEdited} from '../reducers';
+import {
+ userLoggedIn,
+ userDetailsFetched,
+ socialEdited,
+ profileCompletionStageUpdated,
+ setIsOnboardedUser,
+} from '../reducers';
import {getTokenOrLogout} from '../../utils';
/**
@@ -50,7 +56,6 @@ export const updateSocial = (
dispatch,
) => {
try {
- console.log(social);
dispatch({
type: socialEdited.type,
payload: {social, value},
@@ -60,6 +65,36 @@ export const updateSocial = (
}
};
+export const updateProfileCompletionStage = (
+ stage: number,
+): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async (
+ dispatch,
+) => {
+ try {
+ dispatch({
+ type: profileCompletionStageUpdated.type,
+ payload: {stage},
+ });
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+export const updateIsOnboardedUser = (
+ isOnboardedUser: boolean,
+): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async (
+ dispatch,
+) => {
+ try {
+ dispatch({
+ type: setIsOnboardedUser.type,
+ payload: {isOnboardedUser},
+ });
+ } catch (error) {
+ console.log(error);
+ }
+};
+
export const logout = (): ThunkAction<
Promise<void>,
RootState,
diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts
index da3ef3b0..87e1ce22 100644
--- a/src/store/initialStates.ts
+++ b/src/store/initialStates.ts
@@ -1,5 +1,5 @@
import {
- MomentCategoryType,
+ ExploreSectionType,
MomentType,
NotificationType,
ProfilePreviewType,
@@ -17,6 +17,7 @@ export const NO_PROFILE: ProfileType = {
gender: '',
birthday: undefined,
university_class: 2021,
+ profile_completion_stage: 1,
snapchat: '',
tiktok: '',
};
@@ -37,6 +38,7 @@ export const NO_USER_DATA = {
profile: <ProfileType>NO_PROFILE,
avatar: <string | null>'',
cover: <string | null>'',
+ isOnboardedUser: false,
};
export const NO_FRIENDS_DATA = {
@@ -57,9 +59,21 @@ export const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = {
Twitter: {posts: []},
};
+export const EMPTY_EXPLORE_SECTIONS: Record<
+ ExploreSectionType,
+ ProfilePreviewType[]
+> = {
+ 'People You May Know': EMPTY_PROFILE_PREVIEW_LIST,
+ 'New to Tagg': EMPTY_PROFILE_PREVIEW_LIST,
+ 'Trending on Tagg': EMPTY_PROFILE_PREVIEW_LIST,
+ "Brown '21": EMPTY_PROFILE_PREVIEW_LIST,
+ "Brown '22": EMPTY_PROFILE_PREVIEW_LIST,
+ "Brown '23": EMPTY_PROFILE_PREVIEW_LIST,
+};
+
export const NO_TAGG_USERS = {
recentSearches: EMPTY_PROFILE_PREVIEW_LIST,
- taggUsers: EMPTY_PROFILE_PREVIEW_LIST,
+ explores: EMPTY_EXPLORE_SECTIONS,
};
export const NO_SOCIALS = {
@@ -70,23 +84,7 @@ export const NO_BLOCKED_USERS = {
blockedUsers: EMPTY_PROFILE_PREVIEW_LIST,
};
-export const MOMENT_CATEGORIES_MAP: Record<MomentCategoryType, boolean> = {
- Friends: false,
- Adventure: false,
- 'Photo Dump': false,
- Food: false,
- Music: false,
- Art: false,
- Sports: false,
- Fashion: false,
- Travel: false,
- Pets: false,
- Fitness: false,
- DIY: false,
- Nature: false,
- 'Early Life': false,
- Beauty: false,
-};
+export const EMPTY_MOMENT_CATEGORIES: string[] = [];
/**
* The dummy userId and username serve the purpose of preventing app crash
@@ -99,7 +97,7 @@ export const DUMMY_USERNAME = 'tagg_userX';
export const EMPTY_USER_X = <UserXType>{
friends: EMPTY_PROFILE_PREVIEW_LIST,
moments: EMPTY_MOMENTS_LIST,
- momentCategories: MOMENT_CATEGORIES_MAP,
+ momentCategories: EMPTY_MOMENT_CATEGORIES,
socialAccounts: NO_SOCIAL_ACCOUNTS,
user: NO_USER,
profile: NO_PROFILE,
@@ -124,5 +122,5 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record<
};
export const INITIAL_CATEGORIES_STATE = {
- momentCategories: MOMENT_CATEGORIES_MAP,
+ momentCategories: EMPTY_MOMENT_CATEGORIES,
};
diff --git a/src/store/reducers/momentCategoryReducer.tsx b/src/store/reducers/momentCategoryReducer.tsx
index d1f448f9..b6909b87 100644
--- a/src/store/reducers/momentCategoryReducer.tsx
+++ b/src/store/reducers/momentCategoryReducer.tsx
@@ -1,19 +1,16 @@
import {createSlice} from '@reduxjs/toolkit';
import {INITIAL_CATEGORIES_STATE} from '../initialStates';
-import {MomentCategoryType} from '../../types';
const momentCategoriesSlice = createSlice({
name: 'momentCategories',
initialState: INITIAL_CATEGORIES_STATE,
reducers: {
/**
- * One stop to add / delete / update categories for a user
+ * Replace a new copy of moment categories for a user
*/
momentCategoriesFetched: (state, action) => {
- const categories: Array<MomentCategoryType> = action.payload.categories;
- for (let category of categories) {
- state.momentCategories[category] = action.payload.add;
- }
+ const categories: string[] = action.payload.categories;
+ state.momentCategories = categories;
},
},
});
diff --git a/src/store/reducers/taggUsersReducer.ts b/src/store/reducers/taggUsersReducer.ts
index ff30f7a0..33e2e18d 100644
--- a/src/store/reducers/taggUsersReducer.ts
+++ b/src/store/reducers/taggUsersReducer.ts
@@ -6,8 +6,8 @@ const taggUsersSlice = createSlice({
initialState: NO_TAGG_USERS,
reducers: {
taggUsersFetched: (state, action) => {
- state.recentSearches = action.payload.taggUsers;
- state.taggUsers = action.payload.taggUsers;
+ state.recentSearches = action.payload.recentSearches;
+ state.explores = action.payload.explores;
},
},
});
diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts
index 2fd5c462..2e71e38e 100644
--- a/src/store/reducers/userReducer.ts
+++ b/src/store/reducers/userReducer.ts
@@ -41,6 +41,14 @@ const userDataSlice = createSlice({
break;
}
},
+
+ profileCompletionStageUpdated: (state, action) => {
+ state.profile.profile_completion_stage = action.payload.stage;
+ },
+
+ setIsOnboardedUser: (state, action) => {
+ state.isOnboardedUser = action.payload.isOnboardedUser;
+ },
},
});
@@ -48,5 +56,7 @@ export const {
userLoggedIn,
userDetailsFetched,
socialEdited,
+ profileCompletionStageUpdated,
+ setIsOnboardedUser,
} = userDataSlice.actions;
export const userDataReducer = userDataSlice.reducer;
diff --git a/src/store/reducers/userXReducer.ts b/src/store/reducers/userXReducer.ts
index fa1598b2..3b00cf88 100644
--- a/src/store/reducers/userXReducer.ts
+++ b/src/store/reducers/userXReducer.ts
@@ -1,4 +1,4 @@
-import {MomentCategoryType, ScreenType} from '../../types/types';
+import {ScreenType} from '../../types/types';
import {EMPTY_SCREEN_TO_USERS_LIST, EMPTY_USER_X} from '../initialStates';
import {createSlice} from '@reduxjs/toolkit';
@@ -24,12 +24,10 @@ const userXSlice = createSlice({
},
userXMomentCategoriesFetched: (state, action) => {
- const categories: Array<MomentCategoryType> = action.payload.data;
- for (let category of categories) {
- state[<ScreenType>action.payload.screenType][
- action.payload.userId
- ].momentCategories[category] = true;
- }
+ const categories: string[] = action.payload.data;
+ state[<ScreenType>action.payload.screenType][
+ action.payload.userId
+ ].momentCategories = categories;
},
userXMomentsFetched: (state, action) => {
diff --git a/src/types/types.ts b/src/types/types.ts
index b3148bc4..093adbe4 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -19,6 +19,7 @@ export interface ProfileType {
website: string;
gender: string;
university_class: number;
+ profile_completion_stage: number;
birthday: Date | undefined;
snapchat: string;
tiktok: string;
@@ -102,6 +103,14 @@ export enum ScreenType {
Notifications,
}
+export type ExploreSectionType =
+ | 'People You May Know'
+ | 'New to Tagg'
+ | 'Trending on Tagg'
+ | "Brown '21"
+ | "Brown '22"
+ | "Brown '23";
+
/**
* Redux store to have a Record of ScreenType (Search, Profile, Home etc) mapped to
* A Record of userIXd mapped to UserXType
@@ -113,7 +122,7 @@ export interface UserXType {
friends: ProfilePreviewType[];
moments: MomentType[];
socialAccounts: Record<string, SocialAccountType>;
- momentCategories: Record<MomentCategoryType, boolean>;
+ momentCategories: string[];
user: UserType;
profile: ProfileType;
avatar: string;
@@ -129,26 +138,6 @@ export enum VerificationScreenType {
}
/**
- * Default moment categories
- */
-export type MomentCategoryType =
- | 'Friends'
- | 'Adventure'
- | 'Photo Dump'
- | 'Food'
- | 'Music'
- | 'Art'
- | 'Sports'
- | 'Fashion'
- | 'Travel'
- | 'Pets'
- | 'Fitness'
- | 'DIY'
- | 'Nature'
- | 'Early Life'
- | 'Beauty';
-
-/**
* Two types for category selection screen
*/
export enum CategorySelectionScreenType {
diff --git a/src/utils/common.ts b/src/utils/common.ts
index a2f88e8b..dbe8f270 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -47,3 +47,28 @@ export const getDateAge: (
return 'unknown';
}
};
+
+export const moveCategory: (
+ categories: string[],
+ category: string,
+ moveUp: boolean,
+) => string[] = (categories, category, moveUp) => {
+ const i = categories.indexOf(category);
+ const swapTarget = moveUp ? i - 1 : i + 1;
+ if ((moveUp && i === 0) || (!moveUp && i > categories.length)) {
+ return categories;
+ }
+ const tmp = categories[i];
+ categories[i] = categories[swapTarget];
+ categories[swapTarget] = tmp;
+ return categories;
+};
+
+export const checkImageUploadStatus = (statusMap: object) => {
+ for (let [key, value] of Object.entries(statusMap)) {
+ if (value != 'Success') {
+ return false;
+ }
+ }
+ return true;
+};
diff --git a/src/utils/users.ts b/src/utils/users.ts
index bcb43cbc..c54ea715 100644
--- a/src/utils/users.ts
+++ b/src/utils/users.ts
@@ -51,6 +51,11 @@ export const userLogin = async (dispatch: AppDispatch, user: UserType) => {
} else {
return;
}
+ } else {
+ await Promise.all([
+ AsyncStorage.setItem('userId', user.userId),
+ AsyncStorage.setItem('username', user.username),
+ ]);
}
await loadData(dispatch, localUser);
} catch (error) {
diff --git a/tsconfig.json b/tsconfig.json
index 2edf373a..cd6ac9a1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
/* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
- "lib": ["es6"], /* Specify library files to be included in the compilation. */
+ "lib": ["es6", "ES2016"], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */