From a954d6b6b88485dddc0ccfda634ffd102cb34ccd Mon Sep 17 00:00:00 2001 From: Ashm Walia <40498934+ashmgarv@users.noreply.github.com> Date: Tue, 22 Dec 2020 08:50:27 -0800 Subject: [TMA 446] Create category (#144) * Added welcome page * Working code * Small fix * Some more cleanup * Fixes * Cleanup * Fix again * Use gradient for white bg as well * Fixed type --- src/assets/icons/added-border.svg | 1 + src/assets/icons/delete-logo.png | Bin 0 -> 122662 bytes src/assets/icons/delete-logo.svg | 1 + src/assets/icons/plus-logo.png | Bin 0 -> 19640 bytes src/assets/images/welcome.png | Bin 0 -> 304406 bytes src/assets/moment-categories/adventure-icon.png | Bin 0 -> 2833 bytes src/assets/moment-categories/art-icon.png | Bin 0 -> 2929 bytes src/assets/moment-categories/beauty-icon.png | Bin 0 -> 3906 bytes src/assets/moment-categories/diy-icon.png | Bin 0 -> 3113 bytes src/assets/moment-categories/early-life-icon.png | Bin 0 -> 1995 bytes src/assets/moment-categories/fashion-icon.png | Bin 0 -> 2132 bytes src/assets/moment-categories/food-icon.png | Bin 0 -> 3465 bytes src/assets/moment-categories/friends-icon.png | Bin 0 -> 3412 bytes src/assets/moment-categories/music-icon.png | Bin 0 -> 2363 bytes src/assets/moment-categories/nature-icon.png | Bin 0 -> 3016 bytes src/assets/moment-categories/nightlife-icon.png | Bin 0 -> 4288 bytes src/assets/moment-categories/pets-icon.png | Bin 0 -> 3307 bytes src/assets/moment-categories/photo-dump-icon.png | Bin 0 -> 3935 bytes src/assets/moment-categories/sports-icon.png | Bin 0 -> 4040 bytes src/assets/moment-categories/travel-icon.png | Bin 0 -> 2662 bytes src/components/common/ComingSoon.tsx | 5 +- src/components/common/TaggPopup.tsx | 133 ++++++++++++ src/components/common/index.ts | 1 + src/components/moments/Moment.tsx | 32 ++- src/components/onboarding/Background.tsx | 12 +- src/components/onboarding/MomentCategory.tsx | 175 +++++++++++++++ src/components/onboarding/index.ts | 1 + src/components/profile/Content.tsx | 96 +++++++- src/constants/api.ts | 1 + src/constants/constants.ts | 28 +++ src/routes/onboarding/Onboarding.tsx | 44 ++++ src/routes/onboarding/OnboardingStack.tsx | 17 +- src/routes/profile/Profile.tsx | 12 + src/routes/profile/ProfileStack.tsx | 6 +- src/screens/onboarding/CategorySelection.tsx | 241 +++++++++++++++++++++ src/screens/onboarding/Checkpoint.tsx | 5 +- .../onboarding/InvitationCodeVerification.tsx | 10 +- src/screens/onboarding/Login.tsx | 18 +- src/screens/onboarding/PasswordReset.tsx | 5 +- src/screens/onboarding/PasswordResetRequest.tsx | 6 +- src/screens/onboarding/ProfileOnboarding.tsx | 3 +- src/screens/onboarding/RegistrationOne.tsx | 6 +- src/screens/onboarding/RegistrationThree.tsx | 5 +- src/screens/onboarding/RegistrationTwo.tsx | 5 +- src/screens/onboarding/SocialMedia.tsx | 42 ++-- src/screens/onboarding/Verification.tsx | 7 +- src/screens/onboarding/WelcomeScreen.tsx | 94 ++++++++ src/screens/onboarding/index.ts | 2 + src/screens/profile/EditProfile.tsx | 4 +- src/services/MomentCategoryService.ts | 88 ++++++++ src/services/index.ts | 1 + src/store/actions/index.ts | 1 + src/store/actions/momentCategories.tsx | 63 ++++++ src/store/actions/userX.ts | 10 +- src/store/initialStates.ts | 25 ++- src/store/reducers/index.ts | 1 + src/store/reducers/momentCategoryReducer.tsx | 22 ++ src/store/reducers/userXReducer.ts | 17 +- src/store/rootReducer.ts | 2 + src/types/types.ts | 46 ++++ src/utils/users.ts | 2 + 61 files changed, 1235 insertions(+), 61 deletions(-) create mode 100644 src/assets/icons/added-border.svg create mode 100644 src/assets/icons/delete-logo.png create mode 100644 src/assets/icons/delete-logo.svg create mode 100644 src/assets/icons/plus-logo.png create mode 100644 src/assets/images/welcome.png create mode 100644 src/assets/moment-categories/adventure-icon.png create mode 100644 src/assets/moment-categories/art-icon.png create mode 100644 src/assets/moment-categories/beauty-icon.png create mode 100644 src/assets/moment-categories/diy-icon.png create mode 100644 src/assets/moment-categories/early-life-icon.png create mode 100644 src/assets/moment-categories/fashion-icon.png create mode 100644 src/assets/moment-categories/food-icon.png create mode 100644 src/assets/moment-categories/friends-icon.png create mode 100644 src/assets/moment-categories/music-icon.png create mode 100644 src/assets/moment-categories/nature-icon.png create mode 100644 src/assets/moment-categories/nightlife-icon.png create mode 100644 src/assets/moment-categories/pets-icon.png create mode 100644 src/assets/moment-categories/photo-dump-icon.png create mode 100644 src/assets/moment-categories/sports-icon.png create mode 100644 src/assets/moment-categories/travel-icon.png create mode 100644 src/components/common/TaggPopup.tsx create mode 100644 src/components/onboarding/MomentCategory.tsx create mode 100644 src/screens/onboarding/CategorySelection.tsx create mode 100644 src/screens/onboarding/WelcomeScreen.tsx create mode 100644 src/services/MomentCategoryService.ts create mode 100644 src/store/actions/momentCategories.tsx create mode 100644 src/store/reducers/momentCategoryReducer.tsx (limited to 'src') diff --git a/src/assets/icons/added-border.svg b/src/assets/icons/added-border.svg new file mode 100644 index 00000000..ee6a9da3 --- /dev/null +++ b/src/assets/icons/added-border.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/delete-logo.png b/src/assets/icons/delete-logo.png new file mode 100644 index 00000000..54a7228b Binary files /dev/null and b/src/assets/icons/delete-logo.png differ diff --git a/src/assets/icons/delete-logo.svg b/src/assets/icons/delete-logo.svg new file mode 100644 index 00000000..7e8e445e --- /dev/null +++ b/src/assets/icons/delete-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/plus-logo.png b/src/assets/icons/plus-logo.png new file mode 100644 index 00000000..195f28fc Binary files /dev/null and b/src/assets/icons/plus-logo.png differ diff --git a/src/assets/images/welcome.png b/src/assets/images/welcome.png new file mode 100644 index 00000000..46ab4f9f Binary files /dev/null and b/src/assets/images/welcome.png differ diff --git a/src/assets/moment-categories/adventure-icon.png b/src/assets/moment-categories/adventure-icon.png new file mode 100644 index 00000000..33f821ec Binary files /dev/null and b/src/assets/moment-categories/adventure-icon.png differ diff --git a/src/assets/moment-categories/art-icon.png b/src/assets/moment-categories/art-icon.png new file mode 100644 index 00000000..c43d941b Binary files /dev/null and b/src/assets/moment-categories/art-icon.png differ diff --git a/src/assets/moment-categories/beauty-icon.png b/src/assets/moment-categories/beauty-icon.png new file mode 100644 index 00000000..1df48648 Binary files /dev/null and b/src/assets/moment-categories/beauty-icon.png differ diff --git a/src/assets/moment-categories/diy-icon.png b/src/assets/moment-categories/diy-icon.png new file mode 100644 index 00000000..2c339489 Binary files /dev/null and b/src/assets/moment-categories/diy-icon.png differ diff --git a/src/assets/moment-categories/early-life-icon.png b/src/assets/moment-categories/early-life-icon.png new file mode 100644 index 00000000..91aca375 Binary files /dev/null and b/src/assets/moment-categories/early-life-icon.png differ diff --git a/src/assets/moment-categories/fashion-icon.png b/src/assets/moment-categories/fashion-icon.png new file mode 100644 index 00000000..106b81ec Binary files /dev/null and b/src/assets/moment-categories/fashion-icon.png differ diff --git a/src/assets/moment-categories/food-icon.png b/src/assets/moment-categories/food-icon.png new file mode 100644 index 00000000..476521a8 Binary files /dev/null and b/src/assets/moment-categories/food-icon.png differ diff --git a/src/assets/moment-categories/friends-icon.png b/src/assets/moment-categories/friends-icon.png new file mode 100644 index 00000000..6b595dd1 Binary files /dev/null and b/src/assets/moment-categories/friends-icon.png differ diff --git a/src/assets/moment-categories/music-icon.png b/src/assets/moment-categories/music-icon.png new file mode 100644 index 00000000..8e5d82f9 Binary files /dev/null and b/src/assets/moment-categories/music-icon.png differ diff --git a/src/assets/moment-categories/nature-icon.png b/src/assets/moment-categories/nature-icon.png new file mode 100644 index 00000000..2870694c Binary files /dev/null and b/src/assets/moment-categories/nature-icon.png differ diff --git a/src/assets/moment-categories/nightlife-icon.png b/src/assets/moment-categories/nightlife-icon.png new file mode 100644 index 00000000..1e473b6c Binary files /dev/null and b/src/assets/moment-categories/nightlife-icon.png differ diff --git a/src/assets/moment-categories/pets-icon.png b/src/assets/moment-categories/pets-icon.png new file mode 100644 index 00000000..91f65f3c Binary files /dev/null and b/src/assets/moment-categories/pets-icon.png differ diff --git a/src/assets/moment-categories/photo-dump-icon.png b/src/assets/moment-categories/photo-dump-icon.png new file mode 100644 index 00000000..ee5585c3 Binary files /dev/null and b/src/assets/moment-categories/photo-dump-icon.png differ diff --git a/src/assets/moment-categories/sports-icon.png b/src/assets/moment-categories/sports-icon.png new file mode 100644 index 00000000..9edd76b8 Binary files /dev/null and b/src/assets/moment-categories/sports-icon.png differ diff --git a/src/assets/moment-categories/travel-icon.png b/src/assets/moment-categories/travel-icon.png new file mode 100644 index 00000000..5a913ac7 Binary files /dev/null and b/src/assets/moment-categories/travel-icon.png differ diff --git a/src/components/common/ComingSoon.tsx b/src/components/common/ComingSoon.tsx index 16b65b58..d7654a20 100644 --- a/src/components/common/ComingSoon.tsx +++ b/src/components/common/ComingSoon.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; import {StyleSheet, View, Text, Image} from 'react-native'; +import {BackgroundGradientType} from '../../types'; import {SCREEN_WIDTH} from '../../utils'; import {Background} from '../onboarding'; const ComingSoon: React.FC = () => { return ( - + Coming Soon diff --git a/src/components/common/TaggPopup.tsx b/src/components/common/TaggPopup.tsx new file mode 100644 index 00000000..db24adb8 --- /dev/null +++ b/src/components/common/TaggPopup.tsx @@ -0,0 +1,133 @@ +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import * as React from 'react'; +import {Platform, Text, StyleSheet, TouchableOpacity} from 'react-native'; +import {Image, View} from 'react-native-animatable'; +import {ArrowButton} from '..'; +import {OnboardingStackParams} from '../../routes'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import CloseIcon from '../../assets/ionicons/close-outline.svg'; + +type TaggPopupRouteProps = RouteProp; +type TaggPopupNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'TaggPopup' +>; + +interface TaggPopupProps { + route: TaggPopupRouteProps; + navigation: TaggPopupNavigationProps; +} + +const TaggPopup: React.FC = ({route, navigation}) => { + /** + * Custom popup / Tutorial screen for Tagg + * Just like a Singly Linked List, we have a next node + * if (next !== undefined) + * Display the next button and navigate to next popup node on click + * else + * Display close button, navigate back on close + */ + const {messageHeader, messageBody, next} = route.params.popupProps; + + return ( + + + + + {messageHeader} + {messageBody} + + {!next && ( + { + navigation.goBack(); + }}> + + + )} + + {next && ( + + { + navigation.navigate('TaggPopup', {popupProps: next}); + }} + /> + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + whiteColor: { + color: 'white', + }, + closeButton: { + position: 'relative', + height: '50%', + aspectRatio: 1, + left: '20%', + }, + textContainer: { + flex: 1, + flexDirection: 'column', + }, + icon: { + width: 40, + height: 40, + marginVertical: '1%', + }, + header: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + textAlign: 'justify', + marginBottom: '2%', + marginHorizontal: '2%', + }, + subtext: { + color: '#fff', + fontSize: 12, + fontWeight: '600', + textAlign: 'justify', + marginBottom: '15%', + marginHorizontal: '2%', + }, + popup: { + width: SCREEN_WIDTH * 0.8, + height: SCREEN_WIDTH * 0.2, + backgroundColor: 'black', + borderRadius: 8, + flexDirection: 'row', + alignSelf: 'auto', + flexWrap: 'wrap', + position: 'absolute', + bottom: SCREEN_HEIGHT * 0.7, + }, + footer: { + marginLeft: '50%', + flexDirection: 'column-reverse', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); +export default TaggPopup; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 661d2f52..d5d36297 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -17,3 +17,4 @@ export {default as TaggDatePicker} from './TaggDatePicker'; export {default as BottomDrawer} from './BottomDrawer'; export {default as TaggLoadingTndicator} from './TaggLoadingIndicator'; export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer'; +export {default as TaggPopUp} from './TaggPopup'; diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 940b519c..fb6186c8 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -5,18 +5,21 @@ 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 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 {MomentType, ScreenType} from 'src/types'; +import {MomentCategoryType, MomentType, ScreenType} from 'src/types'; interface MomentProps { - title: string; + title: MomentCategoryType; images: MomentType[] | undefined; userXId: string | undefined; screenType: ScreenType; + handleMomentCategoryDelete: (_: MomentCategoryType) => void; + shouldAllowDeletion: boolean; } const Moment: React.FC = ({ @@ -24,6 +27,8 @@ const Moment: React.FC = ({ images, userXId, screenType, + handleMomentCategoryDelete, + shouldAllowDeletion, }) => { const navigation = useNavigation(); @@ -53,11 +58,21 @@ const Moment: React.FC = ({ {title} {!userXId ? ( - navigateToImagePicker()} - /> + <> + navigateToImagePicker()} + style={{marginRight: 10}} + /> + {shouldAllowDeletion && ( + handleMomentCategoryDelete(title)} + width={19} + height={19} + /> + )} + ) : ( )} @@ -113,6 +128,9 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: 'bold', color: TAGG_TEXT_LIGHT_BLUE, + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', }, scrollContainer: { height: SCREEN_WIDTH / 3.25, diff --git a/src/components/onboarding/Background.tsx b/src/components/onboarding/Background.tsx index 054eeff6..fb08e945 100644 --- a/src/components/onboarding/Background.tsx +++ b/src/components/onboarding/Background.tsx @@ -8,23 +8,27 @@ import { SafeAreaView, } from 'react-native'; import {CenteredView} from '../common'; +import {BackgroundGradientType} from '../../types'; +import {BACKGROUND_GRADIENT_MAP} from '../../constants'; interface BackgroundProps extends ViewProps { centered?: boolean; + gradientType: BackgroundGradientType; } const Background: React.FC = (props) => { + const {centered, gradientType, children} = props; return ( - {props.centered ? ( - {props.children} + {centered ? ( + {children} ) : ( - {props.children} + {children} )} diff --git a/src/components/onboarding/MomentCategory.tsx b/src/components/onboarding/MomentCategory.tsx new file mode 100644 index 00000000..25e8995a --- /dev/null +++ b/src/components/onboarding/MomentCategory.tsx @@ -0,0 +1,175 @@ +import * as 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 {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +type MomentCategoryProps = { + categoryType: MomentCategoryType; + onSelect: ( + category: MomentCategoryType, + isSelected: boolean, + isAdded: boolean, + ) => void; + isSelected: boolean; + isAdded: boolean; +}; + +const MomentCategory: React.FC = ({ + categoryType, + isSelected, + isAdded, + onSelect, +}) => { + var icon, bgColor; + + /** + * Choose icon and color based on category type + */ + switch (categoryType) { + case 'Friends': + icon = require('../../assets/moment-categories/friends-icon.png'); + bgColor = '#5E4AE4'; + break; + case 'Adventure': + icon = require('../../assets/moment-categories/adventure-icon.png'); + bgColor = '#5044A6'; + break; + case 'Photo Dump': + icon = require('../../assets/moment-categories/photo-dump-icon.png'); + bgColor = '#4755A1'; + break; + case 'Food': + icon = require('../../assets/moment-categories/food-icon.png'); + bgColor = '#444BA8'; + break; + case 'Music': + icon = require('../../assets/moment-categories/music-icon.png'); + bgColor = '#374898'; + break; + case 'Art': + icon = require('../../assets/moment-categories/art-icon.png'); + bgColor = '#3F5C97'; + break; + case 'Sports': + icon = require('../../assets/moment-categories/sports-icon.png'); + bgColor = '#3A649F'; + break; + case 'Fashion': + icon = require('../../assets/moment-categories/fashion-icon.png'); + bgColor = '#386A95'; + break; + case 'Travel': + icon = require('../../assets/moment-categories/travel-icon.png'); + bgColor = '#366D84'; + break; + case 'Pets': + icon = require('../../assets/moment-categories/pets-icon.png'); + bgColor = '#335E76'; + break; + case 'Nightlife': + icon = require('../../assets/moment-categories/nightlife-icon.png'); + bgColor = '#2E5471'; + break; + case 'DIY': + icon = require('../../assets/moment-categories/diy-icon.png'); + bgColor = '#274765'; + break; + case 'Nature': + icon = require('../../assets/moment-categories/nature-icon.png'); + bgColor = '#225363'; + break; + case 'Early Life': + icon = require('../../assets/moment-categories/early-life-icon.png'); + bgColor = '#365F6A'; + break; + case 'Beauty': + icon = require('../../assets/moment-categories/beauty-icon.png'); + bgColor = '#4E7175'; + break; + } + + /** + * The Linear Gradient helps us add a gradient border when the category is already added /selected by user + * if(isAdded) + * gradient background + * if(isSelected) + * white background + * else + * transparent background + */ + return ( + + onSelect(categoryType, !isSelected, isAdded)} + style={[ + styles.container, + styles.touchable, + {backgroundColor: bgColor}, + ]}> + + {categoryType} + {isAdded && ( + + )} + + + ); +}; + +const styles = StyleSheet.create({ + gradient: { + width: SCREEN_WIDTH / 3.7, + height: SCREEN_HEIGHT / 5.8, + marginHorizontal: '2%', + marginVertical: '2%', + }, + touchable: { + width: SCREEN_WIDTH / 4, + height: SCREEN_HEIGHT / 6.2, + marginHorizontal: '2%', + marginVertical: '4%', + }, + container: { + borderRadius: 8, + shadowRadius: 2, + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.4, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + icon: { + width: 40, + height: 40, + marginVertical: '8%', + }, + label: { + fontWeight: '500', + color: 'white', + }, + tick: { + marginTop: '3%', + width: 15, + height: 15, + }, +}); + +export default MomentCategory; diff --git a/src/components/onboarding/index.ts b/src/components/onboarding/index.ts index fde4e0af..b790933f 100644 --- a/src/components/onboarding/index.ts +++ b/src/components/onboarding/index.ts @@ -9,3 +9,4 @@ export {default as BirthDatePicker} from './BirthDatePicker'; export {default as TaggDropDown} from './TaggDropDown'; export {default as SocialMediaLinker} from './SocialMediaLinker'; export {default as LinkSocialMedia} from './LinkSocialMedia'; +export {default as MomentCategory} from './MomentCategory'; diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index f2e0db0a..7064f775 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -1,21 +1,29 @@ import React, {useCallback, useEffect, useState} from 'react'; import { + Alert, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, RefreshControl, StyleSheet, + Text, View, } from 'react-native'; import Animated from 'react-native-reanimated'; import { + CategorySelectionScreenType, + MomentCategoryType, MomentType, ProfilePreviewType, ProfileType, ScreenType, UserType, } from '../../types'; -import {COVER_HEIGHT, defaultMoments} from '../../constants'; +import { + COVER_HEIGHT, + MOMENT_CATEGORIES, + TAGG_TEXT_LIGHT_BLUE, +} from '../../constants'; import {fetchUserX, SCREEN_HEIGHT, userLogin} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; import {Moment} from '../moments'; @@ -29,15 +37,19 @@ import { blockUnblockUser, loadFollowData, updateUserXFollowersAndFollowing, + updateMomentCategories, } from '../../store/actions'; import { NO_USER, NO_PROFILE, EMPTY_PROFILE_PREVIEW_LIST, EMPTY_MOMENTS_LIST, + MOMENT_CATEGORIES_MAP, } from '../../store/initialStates'; import {Cover} from '.'; -import {Background} from '../onboarding'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useNavigation} from '@react-navigation/native'; +import {deleteMomentCategories} from '../../services'; interface ContentProps { y: Animated.Value; @@ -60,6 +72,10 @@ const Content: React.FC = ({y, userXId, screenType}) => { ? useSelector((state: RootState) => state.userX[screenType][userXId]) : useSelector((state: RootState) => state.moments); + const {momentCategories = MOMENT_CATEGORIES_MAP} = userXId + ? useSelector((state: RootState) => state.userX[screenType][userXId]) + : useSelector((state: RootState) => state.momentCategories); + const {blockedUsers = EMPTY_PROFILE_PREVIEW_LIST} = useSelector( (state: RootState) => state.blocked, ); @@ -68,6 +84,8 @@ const Content: React.FC = ({y, userXId, screenType}) => { ); const state = useStore().getState(); + const navigation = useNavigation(); + /** * States */ @@ -80,6 +98,13 @@ const Content: React.FC = ({y, userXId, screenType}) => { const [shouldBounce, setShouldBounce] = useState(true); const [refreshing, setRefreshing] = useState(false); + /** + * Filter list of categories already selected by user + */ + const userMomentCategories = MOMENT_CATEGORIES.filter( + (category) => momentCategories[category] === true, + ); + const onRefresh = useCallback(() => { const refrestState = async () => { if (!userXId) { @@ -194,6 +219,33 @@ const Content: React.FC = ({y, userXId, screenType}) => { await dispatch(updateUserXFollowersAndFollowing(user.userId, state)); }; + /** + * Handle deletion of a category + * Confirm with user before deleting the category + * @param category category to be deleted + */ + const handleCategoryDeletion = (category: MomentCategoryType) => { + Alert.alert( + 'Category Deletion', + `Are you sure that you want to delete the category ${category} ?`, + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Yes', + onPress: () => { + dispatch( + updateMomentCategories([category], false, loggedInUser.userId), + ); + }, + }, + ], + {cancelable: true}, + ); + }; + const handleScroll = (e: NativeSyntheticEvent) => { /** * Set the new y position @@ -239,32 +291,60 @@ const Content: React.FC = ({y, userXId, screenType}) => { /> - {defaultMoments.map((title, index) => ( + {userMomentCategories.map((title, index) => ( 2} /> ))} + {!userXId && userMomentCategories.length < 6 && ( + + navigation.push('CategorySelection', { + categories: momentCategories, + screenType: CategorySelectionScreenType.Profile, + user: loggedInUser, + }) + } + style={styles.createCategoryButton}> + + Create a new category + + + )} ); }; const styles = StyleSheet.create({ - refreshControlContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, container: { flex: 1, }, momentsContainer: { backgroundColor: '#f2f2f2', paddingBottom: SCREEN_HEIGHT / 10, + flex: 1, + flexDirection: 'column', + }, + createCategoryButton: { + backgroundColor: TAGG_TEXT_LIGHT_BLUE, + justifyContent: 'center', + alignItems: 'center', + width: '70%', + height: 30, + marginTop: '15%', + alignSelf: 'center', + }, + createCategoryButtonLabel: { + fontSize: 16, + fontWeight: '500', + color: 'white', }, }); diff --git a/src/constants/api.ts b/src/constants/api.ts index f9ac3d7c..890ef102 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -26,6 +26,7 @@ export const ALL_USERS_ENDPOINT: string = API_URL + 'users/'; export const REPORT_ISSUE_ENDPOINT: string = API_URL + 'report/'; 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/'; // Register Social Link (Non-integrated) export const LINK_SNAPCHAT_ENDPOINT: string = API_URL + 'link-sc/'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 3fed8fe6..52a52de6 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,3 +1,5 @@ +import {ReactText} from 'react'; +import {BackgroundGradientType, MomentCategoryType} from './../types/'; import {SCREEN_WIDTH, SCREEN_HEIGHT, isIPhoneX} from '../utils'; export const CHIN_HEIGHT = 34; @@ -102,3 +104,29 @@ export const BROWSABLE_SOCIAL_URLS: Record = { Instagram: 'https://instagram.com/', Twitter: 'https://twitter.com/', }; + +export const MOMENT_CATEGORIES: Array = [ + 'Friends', + 'Adventure', + 'Photo Dump', + 'Food', + 'Music', + 'Art', + 'Sports', + 'Fashion', + 'Travel', + 'Pets', + 'Nightlife', + 'DIY', + 'Nature', + 'Early Life', + 'Beauty', +]; + +export const BACKGROUND_GRADIENT_MAP: Record< + BackgroundGradientType, + Array +> = { + [BackgroundGradientType.Light]: ['#9F00FF', '#27EAE9'], + [BackgroundGradientType.Dark]: ['#421566', '#385D5E'], +}; diff --git a/src/routes/onboarding/Onboarding.tsx b/src/routes/onboarding/Onboarding.tsx index 63a75934..a3d281f5 100644 --- a/src/routes/onboarding/Onboarding.tsx +++ b/src/routes/onboarding/Onboarding.tsx @@ -12,8 +12,11 @@ import { SocialMedia, PasswordResetRequest, PasswordReset, + WelcomeScreen, + CategorySelection, } from '../../screens'; import {StackCardInterpolationProps} from '@react-navigation/stack'; +import TaggPopup from '../../components/common/TaggPopup'; const forFade = ({current}: StackCardInterpolationProps) => ({ cardStyle: { @@ -41,6 +44,47 @@ const Onboarding: React.FC = () => { gestureEnabled: false, }} /> + + + ({ + cardStyle: { + opacity: progress.interpolate({ + inputRange: [0, 0.5, 0.9, 1], + outputRange: [0, 0.25, 0.7, 1], + }), + }, + overlayStyle: { + backgroundColor: '#505050', + opacity: progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.9], + extrapolate: 'clamp', + }), + }, + }), + }} + /> ; + screenType: CategorySelectionScreenType; + user: UserType; + }; + TaggPopup: { + popupProps: TaggPopupType; + }; }; export const OnboardingStack = createStackNavigator(); diff --git a/src/routes/profile/Profile.tsx b/src/routes/profile/Profile.tsx index 3cb928e5..4c93b1ee 100644 --- a/src/routes/profile/Profile.tsx +++ b/src/routes/profile/Profile.tsx @@ -8,6 +8,7 @@ import { MomentCommentsScreen, FollowersListScreen, EditProfile, + CategorySelection, } from '../../screens'; import {ProfileStack, ProfileStackParams} from './ProfileStack'; import {RouteProp} from '@react-navigation/native'; @@ -90,6 +91,17 @@ const Profile: React.FC = ({route}) => { }} initialParams={{screenType}} /> + {isProfileStack ? ( ) : ( diff --git a/src/routes/profile/ProfileStack.tsx b/src/routes/profile/ProfileStack.tsx index e7db9f37..bc0a9560 100644 --- a/src/routes/profile/ProfileStack.tsx +++ b/src/routes/profile/ProfileStack.tsx @@ -2,7 +2,7 @@ * Note the name userXId here, it refers to the id of the user being visited */ import {createStackNavigator} from '@react-navigation/stack'; -import {MomentType, ScreenType} from '../../types'; +import {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; export type ProfileStackParams = { Search: { @@ -41,6 +41,10 @@ export type ProfileStackParams = { userId: string; username: string; }; + CategorySelection: { + categories: Array; + screenType: CategorySelectionScreenType; + }; }; export const ProfileStack = createStackNavigator(); diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx new file mode 100644 index 00000000..f92b7e39 --- /dev/null +++ b/src/screens/onboarding/CategorySelection.tsx @@ -0,0 +1,241 @@ +import {RouteProp} from '@react-navigation/native'; +import React, {useCallback, useEffect, useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import {useDispatch} from 'react-redux'; +import { + BackgroundGradientType, + CategorySelectionScreenType, + MomentCategoryType, +} from '../../types'; +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 {postMomentCategories} from '../../services'; +import {updateMomentCategories} from '../../store/actions/momentCategories'; +import {ScrollView} from 'react-native-gesture-handler'; + +type CategorySelectionRouteProps = RouteProp< + OnboardingStackParams, + 'CategorySelection' +>; + +type CategorySelectionNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'CategorySelection' +>; + +interface CategorySelectionProps { + route: CategorySelectionRouteProps; + navigation: CategorySelectionNavigationProps; +} + +const CategorySelection: React.FC = ({ + route, + navigation, +}) => { + /** + * Same component to be used for category selection while onboarding and while on profile + */ + const {categories, screenType, user} = route.params; + const isOnBoarding: boolean = + screenType === CategorySelectionScreenType.Onboarding; + const {userId, username} = user; + + const [selectedCategories, setSelectedCategories] = useState< + Array + >([]); + + const dispatch = useDispatch(); + + /** + * Show the tutorial if a new user is OnBoarding + */ + useEffect(() => { + if (isOnBoarding) { + navigation.navigate('TaggPopup', { + 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, + }, + }, + }); + } + }, [isOnBoarding]); + + /** + * Handle selection of a new category + * case isAdded: + * Return without doing anything + * case isSelected: + * Add to the selected categories + * case not isSelected: + * Remove from the selected categories + */ + const onSelect = ( + category: MomentCategoryType, + isSelected: boolean, + isAdded: boolean, + ) => { + if (isAdded) return; + if (isSelected) { + setSelectedCategories((prev) => [...prev, category]); + } else { + setSelectedCategories( + selectedCategories.filter((item) => item !== category), + ); + } + }; + + /** + * 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 + */ + const addedLength = !isOnBoarding + ? Object.keys(categories).filter((key) => { + return categories[key as MomentCategoryType] === true; + }).length + : 0; + + 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!'); + return; + } + try { + if (isOnBoarding) { + const token = await getTokenOrLogout(dispatch); + await postMomentCategories(selectedCategories, token); + userLogin(dispatch, {userId: userId, username: username}); + } else { + dispatch(updateMomentCategories(selectedCategories, true, userId)); + navigation.goBack(); + } + } catch (error) { + console.log(error); + Alert.alert('There was a problem'); + } + }; + + /** + * Using a scroll view to accomodate dynamic category creation later on + */ + return ( + + + + Create new categories + + + {MOMENT_CATEGORIES.map((category, index) => ( + + ))} + + + + {isOnBoarding ? 'Login' : 'Create'} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'space-around', + marginBottom: '10%', + }, + wizard: { + ...Platform.select({ + ios: { + top: 50, + }, + android: { + bottom: 40, + }, + }), + }, + linkerContainer: { + flex: 1, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + alignContent: 'center', + marginBottom: '10%', + }, + header: { + color: '#fff', + fontSize: 22, + fontWeight: '600', + textAlign: 'center', + marginBottom: '4%', + }, + subtext: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + marginVertical: '8%', + marginHorizontal: '10%', + }, + finalAction: { + backgroundColor: 'white', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#8F01FF', + marginBottom: '25%', + }, + finalActionLabel: { + fontSize: 16, + fontWeight: '500', + color: 'black', + }, +}); + +export default CategorySelection; diff --git a/src/screens/onboarding/Checkpoint.tsx b/src/screens/onboarding/Checkpoint.tsx index 83a8a2bc..b0b42203 100644 --- a/src/screens/onboarding/Checkpoint.tsx +++ b/src/screens/onboarding/Checkpoint.tsx @@ -12,6 +12,7 @@ import { import {OnboardingStackParams} from '../../routes'; import {RegistrationWizard, Background} from '../../components'; +import {BackgroundGradientType} from '../../types'; type CheckpointRouteProp = RouteProp; type CheckpointNavigationProp = StackNavigationProp< @@ -44,7 +45,9 @@ const Checkpoint: React.FC = ({route, navigation}) => { }; return ( - + diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx index 3f0ea124..afdf6d3f 100644 --- a/src/screens/onboarding/InvitationCodeVerification.tsx +++ b/src/screens/onboarding/InvitationCodeVerification.tsx @@ -26,7 +26,8 @@ import { Alert, Platform, } from 'react-native'; -import {trackPromise} from 'react-promise-tracker'; + +import {BackgroundGradientType} from '../../types'; type InvitationCodeVerificationScreenNavigationProp = StackNavigationProp< OnboardingStackParams, @@ -86,13 +87,16 @@ const InvitationCodeVerification: React.FC = ({ navigation.navigate('Login')} + onPress={() => navigation.navigate('WelcomeScreen')} /> ); return ( - + Enter the code diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index cb550ef8..1315fdf5 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -17,10 +17,15 @@ 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 {UserType} from '../../types'; +import { + BackgroundGradientType, + CategorySelectionScreenType, + 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; type VerificationScreenNavigationProp = StackNavigationProp< @@ -194,8 +199,8 @@ const Login: React.FC = ({navigation}: LoginProps) => { /* * Handles tap on "Get Started" text by resetting fields & navigating to the registration page. */ - const goToRegistration = () => { - navigation.navigate('InvitationCodeVerification'); + const startRegistrationProcess = () => { + navigation.navigate('WelcomeScreen'); setForm({...form, attemptedSubmit: false}); }; @@ -244,7 +249,7 @@ const Login: React.FC = ({navigation}: LoginProps) => { accessible={true} accessibilityLabel="Get started" style={styles.getStarted} - onPress={goToRegistration}> + onPress={startRegistrationProcess}> Get started! @@ -252,7 +257,10 @@ const Login: React.FC = ({navigation}: LoginProps) => { ); return ( - + = ({ ); return ( - + = ({ ); return ( - + = ({ }; return ( - + diff --git a/src/screens/onboarding/RegistrationOne.tsx b/src/screens/onboarding/RegistrationOne.tsx index 3373b903..54c4e210 100644 --- a/src/screens/onboarding/RegistrationOne.tsx +++ b/src/screens/onboarding/RegistrationOne.tsx @@ -27,7 +27,7 @@ import {trackPromise} from 'react-promise-tracker'; import {SEND_OTP_ENDPOINT} from '../../constants'; import {phoneRegex} from '../../constants'; -import {VerificationScreenType} from '../../types'; +import {BackgroundGradientType, VerificationScreenType} from '../../types'; type RegistrationScreenOneRouteProp = RouteProp< OnboardingStackParams, @@ -138,7 +138,9 @@ const RegistrationOne: React.FC = ({navigation}) => { ); return ( - + = ({ ); return ( - + = ({ ); return ( - + ; +type SocialMediaNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'SocialMedia' +>; + interface SocialMediaProps { route: SocialMediaRouteProps; + navigation: SocialMediaNavigationProps; } -const SocialMedia: React.FC = ({route}) => { +const SocialMedia: React.FC = ({route, navigation}) => { const {userId, username} = route.params; const linkers: Array = []; @@ -56,17 +67,18 @@ const SocialMedia: React.FC = ({route}) => { // }); // }; - const handleLogin = () => { - try { - userLogin(dispatch, {userId: userId, username: username}); - } catch (error) { - console.log(error); - Alert.alert('There was a problem logging you in'); - } + const handleNext = () => { + navigation.navigate('CategorySelection', { + categories: MOMENT_CATEGORIES_MAP, + screenType: CategorySelectionScreenType.Onboarding, + user: {userId: userId, username: username}, + }); }; return ( - + = ({route}) => { ))} - - Login + + Next ); @@ -133,7 +145,7 @@ const styles = StyleSheet.create({ marginBottom: '35%', marginHorizontal: '10%', }, - loginButton: { + nextButton: { backgroundColor: '#8F01FF', justifyContent: 'center', alignItems: 'center', @@ -144,7 +156,7 @@ const styles = StyleSheet.create({ borderColor: '#8F01FF', marginBottom: '15%', }, - loginButtonLabel: { + nextButtonLabel: { fontSize: 16, fontWeight: '500', color: '#ddd', diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx index 9fa1c12f..c808f30b 100644 --- a/src/screens/onboarding/Verification.tsx +++ b/src/screens/onboarding/Verification.tsx @@ -27,7 +27,7 @@ import { Platform, } from 'react-native'; import {trackPromise} from 'react-promise-tracker'; -import {VerificationScreenType} from '../../types'; +import {BackgroundGradientType, VerificationScreenType} from '../../types'; import { handlePasswordCodeVerification, sendOtp, @@ -137,7 +137,10 @@ const Verification: React.FC = ({route, navigation}) => { ); return ( - + {isPhoneVerification ? ( ) : ( diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx new file mode 100644 index 00000000..fcdd9bc5 --- /dev/null +++ b/src/screens/onboarding/WelcomeScreen.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import {StyleSheet, View, Text, Image, TouchableOpacity} from 'react-native'; +import {SCREEN_WIDTH} from '../../utils'; +import {Background} from '../../components'; +import {OnboardingStackParams} from '../../routes'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {BackgroundGradientType} from '../../types'; + +type WelcomeScreenNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'WelcomeScreen' +>; + +interface WelcomeScreenProps { + navigation: WelcomeScreenNavigationProps; +} + +const WelcomeScreen: React.FC = ({navigation}) => { + const handleNext = () => { + navigation.navigate('InvitationCodeVerification'); + }; + return ( + + + + + Welcome to TAGG! + + This is the new social networking platform for you! It will help you + create your own personalized digital space where you can express who + you are, along with all the moments that comprehensively define you! + + + + Next + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + /** + * Set primary axis to column + * Align items to centre along that primary axis and the secondary axis as well + */ + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + image: { + width: SCREEN_WIDTH, + height: SCREEN_WIDTH, + }, + header: { + color: '#fff', + fontSize: 32, + fontWeight: '600', + textAlign: 'center', + marginBottom: '4%', + marginHorizontal: '10%', + }, + subtext: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + marginBottom: '15%', + marginHorizontal: '10%', + }, + nextButton: { + backgroundColor: '#8F01FF', + justifyContent: 'center', + alignItems: 'center', + width: '70%', + height: '10%', + borderRadius: 5, + borderWidth: 1, + borderColor: '#8F01FF', + marginBottom: '15%', + }, + nextButtonLabel: { + fontSize: 30, + fontWeight: '500', + color: '#ddd', + }, +}); +export default WelcomeScreen; diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 2411a7e7..ec833929 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -9,3 +9,5 @@ export {default as InvitationCodeVerification} from './InvitationCodeVerificatio export {default as SocialMedia} from './SocialMedia'; export {default as PasswordResetRequest} from './PasswordResetRequest'; export {default as PasswordReset} from './PasswordReset'; +export {default as WelcomeScreen} from './WelcomeScreen'; +export {default as CategorySelection} from './CategorySelection'; diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 50e1c006..316ad5d4 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -39,6 +39,7 @@ import {HeaderHeight, SCREEN_HEIGHT} from '../../utils'; import {RootState} from '../../store/rootReducer'; import {useDispatch, useSelector} from 'react-redux'; import {loadUserData} from '../../store/actions'; +import {BackgroundGradientType} from '../../types'; type EditProfileNavigationProp = StackNavigationProp< ProfileStackParams, @@ -219,7 +220,6 @@ const EditProfile: React.FC = ({route, navigation}) => { }); }; - const handleSnapchatUpdate = (newUsername: string) => { // Allow any username, empty means to "un-link" it // TODO: refresh taggs bar after @@ -373,7 +373,7 @@ const EditProfile: React.FC = ({route, navigation}) => { }, [navigation, handleSubmit]); return ( - + diff --git a/src/services/MomentCategoryService.ts b/src/services/MomentCategoryService.ts new file mode 100644 index 00000000..8bdb70d2 --- /dev/null +++ b/src/services/MomentCategoryService.ts @@ -0,0 +1,88 @@ +import {Alert} from 'react-native'; +import {MomentCategoryType} from './../types/types'; +import {MOMENT_CATEGORY_ENDPOINT} from '../constants'; + +export const loadMomentCategories: ( + userId: string, + token: string, +) => Promise = async (userId, token) => { + let categories: MomentCategoryType[] = []; + try { + const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const status = response.status; + if (status === 200) { + const data = await response.json(); + categories = data['categories']; + } else { + console.log('Could not load categories!'); + return []; + } + } catch (err) { + console.log(err); + return []; + } + return categories; +}; + +export const postMomentCategories: ( + categories: Array, + token: string, +) => Promise = async (categories, token) => { + let success = false; + try { + const response = await fetch(MOMENT_CATEGORY_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Token ' + token, + }, + body: JSON.stringify({categories}), + }); + const status = response.status; + if (status === 200) { + success = true; + } else { + Alert.alert('There was a problem creating categories!'); + console.log('Could not post categories!'); + } + } catch (err) { + console.log(err); + return success; + } + return success; +}; + +export const deleteMomentCategories: ( + categories: Array, + userId: string, + token: string, +) => Promise = 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; +}; diff --git a/src/services/index.ts b/src/services/index.ts index bce3a75a..d98996ba 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -5,3 +5,4 @@ export * from './ExploreServices'; export * from './UserFollowServices'; export * from './ReportingService'; export * from './BlockUserService'; +export * from './MomentCategoryService'; diff --git a/src/store/actions/index.ts b/src/store/actions/index.ts index 04fa9767..f9fd5e9c 100644 --- a/src/store/actions/index.ts +++ b/src/store/actions/index.ts @@ -1,6 +1,7 @@ export * from './user'; export * from './userFollow'; export * from './userMoments'; +export * from './momentCategories'; export * from './socials'; export * from './taggUsers'; export * from './userBlock'; diff --git a/src/store/actions/momentCategories.tsx b/src/store/actions/momentCategories.tsx new file mode 100644 index 00000000..a522c3e0 --- /dev/null +++ b/src/store/actions/momentCategories.tsx @@ -0,0 +1,63 @@ +import {RootState} from '../rootReducer'; +import { + deleteMomentCategories, + loadMomentCategories, + postMomentCategories, +} from '../../services'; +import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {momentCategoriesFetched} from '../reducers'; +import {getTokenOrLogout} from '../../utils'; +import {MomentCategoryType} from '../../types'; + +/** + * Load all categories for user + * @param userId id of the user for whom categories should be loaded + */ +export const loadUserMomentCategories = ( + userId: string, +): ThunkAction, RootState, unknown, Action> => async ( + dispatch, +) => { + try { + const token = await getTokenOrLogout(dispatch); + const categories = await loadMomentCategories(userId, token); + dispatch({ + type: momentCategoriesFetched.type, + payload: {categories, add: true}, + }); + } catch (error) { + console.log(error); + } +}; + +/** + * 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 + */ +export const updateMomentCategories = ( + categories: Array, + add: boolean, + userId: string, +): ThunkAction, RootState, unknown, Action> => 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); + } + if (success) { + dispatch({ + type: momentCategoriesFetched.type, + payload: {categories, add}, + }); + } + } catch (error) { + console.log(error); + } +}; diff --git a/src/store/actions/userX.ts b/src/store/actions/userX.ts index 5468f762..87162eb1 100644 --- a/src/store/actions/userX.ts +++ b/src/store/actions/userX.ts @@ -1,6 +1,7 @@ +import {loadMomentCategories} from './../../services/MomentCategoryService'; import {userXInStore} from './../../utils/'; import {getTokenOrLogout, loadAllSocialsForUser} from './../../utils'; -import {UserType, ScreenType, ProfilePreviewType} from '../../types/types'; +import {UserType, ScreenType} from '../../types/types'; import {RootState} from '../rootReducer'; import {Action, ThunkAction} from '@reduxjs/toolkit'; import { @@ -13,6 +14,7 @@ import { userXProfileFetched, userXSocialsFetched, userXUserFetched, + userXMomentCategoriesFetched, resetScreen, } from '../reducers'; import { @@ -80,6 +82,12 @@ export const loadUserX = ( payload: {screenType, userId, data}, }), ); + loadMomentCategories(userId, token).then((data) => { + dispatch({ + type: userXMomentCategoriesFetched.type, + payload: {screenType, userId, data}, + }); + }); } catch (error) { console.log(error); } diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 817af86b..8f4a2e84 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,4 +1,4 @@ -import {MomentType} from 'src/types'; +import {MomentCategoryType, MomentType} from '../types'; import { ProfileType, SocialAccountType, @@ -62,6 +62,24 @@ export const NO_BLOCKED_USERS = { blockedUsers: EMPTY_PROFILE_PREVIEW_LIST, }; +export const MOMENT_CATEGORIES_MAP: Record = { + Friends: false, + Adventure: false, + 'Photo Dump': false, + Food: false, + Music: false, + Art: false, + Sports: false, + Fashion: false, + Travel: false, + Pets: false, + Nightlife: false, + DIY: false, + Nature: false, + 'Early Life': false, + Beauty: false, +}; + /** * The dummy userId and username serve the purpose of preventing app crash * For instance, if it may happen that data in our store is not loaded yet for the userXId being visited. @@ -74,6 +92,7 @@ export const EMPTY_USER_X = { followers: EMPTY_PROFILE_PREVIEW_LIST, following: EMPTY_PROFILE_PREVIEW_LIST, moments: EMPTY_MOMENTS_LIST, + momentCategories: MOMENT_CATEGORIES_MAP, socialAccounts: NO_SOCIAL_ACCOUNTS, user: NO_USER, profile: NO_PROFILE, @@ -95,3 +114,7 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record< [ScreenType.Profile]: EMPTY_USERX_LIST, [ScreenType.Search]: EMPTY_USERX_LIST, }; + +export const INITIAL_CATEGORIES_STATE = { + momentCategories: MOMENT_CATEGORIES_MAP, +}; diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index 0e378bc5..e09b41ee 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -5,3 +5,4 @@ export * from './userSocialsReducer'; export * from './taggUsersReducer'; export * from './userBlockReducer'; export * from './userXReducer'; +export * from './momentCategoryReducer'; diff --git a/src/store/reducers/momentCategoryReducer.tsx b/src/store/reducers/momentCategoryReducer.tsx new file mode 100644 index 00000000..d1f448f9 --- /dev/null +++ b/src/store/reducers/momentCategoryReducer.tsx @@ -0,0 +1,22 @@ +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 + */ + momentCategoriesFetched: (state, action) => { + const categories: Array = action.payload.categories; + for (let category of categories) { + state.momentCategories[category] = action.payload.add; + } + }, + }, +}); + +export const {momentCategoriesFetched} = momentCategoriesSlice.actions; +export const momentCategoriesReducer = momentCategoriesSlice.reducer; diff --git a/src/store/reducers/userXReducer.ts b/src/store/reducers/userXReducer.ts index 154dd7dc..bb142864 100644 --- a/src/store/reducers/userXReducer.ts +++ b/src/store/reducers/userXReducer.ts @@ -1,4 +1,4 @@ -import {ScreenType} from '../../types/types'; +import {MomentCategoryType, ScreenType} from '../../types/types'; import {EMPTY_SCREEN_TO_USERS_LIST, EMPTY_USER_X} from '../initialStates'; import {createSlice} from '@reduxjs/toolkit'; @@ -23,31 +23,45 @@ const userXSlice = createSlice({ action.payload.user; }, + userXMomentCategoriesFetched: (state, action) => { + const categories: Array = action.payload.data; + for (let category of categories) { + state[action.payload.screenType][ + action.payload.userId + ].momentCategories[category] = true; + } + }, + userXMomentsFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId ].moments = action.payload.data; }, + userXFollowersFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId ].followers = action.payload.data; }, + userXFollowingFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId ].following = action.payload.data; }, + userXAvatarFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId ].avatar = action.payload.data; }, + userXCoverFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId ].cover = action.payload.data; }, + userXSocialsFetched: (state, action) => { state[action.payload.screenType][ action.payload.userId @@ -72,6 +86,7 @@ export const { userXMomentsFetched, userXProfileFetched, userXSocialsFetched, + userXMomentCategoriesFetched, resetScreen, } = userXSlice.actions; export const userXReducer = userXSlice.reducer; diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 695ed8c7..8f002de0 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -7,6 +7,7 @@ import { taggUsersReducer, userBlockReducer, userXReducer, + momentCategoriesReducer, } from './reducers'; /** @@ -20,6 +21,7 @@ const rootReducer = combineReducers({ socialAccounts: userSocialsReducer, taggUsers: taggUsersReducer, blocked: userBlockReducer, + momentCategories: momentCategoriesReducer, userX: userXReducer, }); diff --git a/src/types/types.ts b/src/types/types.ts index e25d1ca7..25160d34 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -112,6 +112,7 @@ export interface UserXType { following: ProfilePreviewType[]; moments: MomentType[]; socialAccounts: Record; + momentCategories: Record; user: UserType; profile: ProfileType; avatar: string; @@ -125,3 +126,48 @@ export enum VerificationScreenType { Phone, Password, } + +/** + * Default moment categories + */ +export type MomentCategoryType = + | 'Friends' + | 'Adventure' + | 'Photo Dump' + | 'Food' + | 'Music' + | 'Art' + | 'Sports' + | 'Fashion' + | 'Travel' + | 'Pets' + | 'Nightlife' + | 'DIY' + | 'Nature' + | 'Early Life' + | 'Beauty'; + +/** + * Two types for category selection screen + */ +export enum CategorySelectionScreenType { + Onboarding, + Profile, +} + +/** + * Gradient type to accomodate new g background gradients for Tagg + */ +export enum BackgroundGradientType { + Light, + Dark, +} + +/** + * Linked List style type to accomodate for reusable TaggPopup for displaying popups or running a tutorial + */ +export type TaggPopupType = { + messageHeader: string; + messageBody: string; + next?: TaggPopupType; +}; diff --git a/src/utils/users.ts b/src/utils/users.ts index 0ed490c7..4f93347d 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,3 +1,4 @@ +import {loadUserMomentCategories} from './../store/actions/momentCategories'; import {loadUserX} from './../store/actions/userX'; import {RootState} from './../store/rootReducer'; import AsyncStorage from '@react-native-community/async-storage'; @@ -20,6 +21,7 @@ const loadData = async (dispatch: AppDispatch, user: UserType) => { await Promise.all([ dispatch(loadUserData(user)), dispatch(loadFollowData(user.userId)), + dispatch(loadUserMomentCategories(user.userId)), dispatch(loadUserMoments(user.userId)), dispatch(loadAllSocials(user.userId)), dispatch(loadBlockedList(user.userId)), -- cgit v1.2.3-70-g09d2