diff options
Diffstat (limited to 'src')
61 files changed, 1235 insertions, 61 deletions
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 @@ +<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 990.61 1129.48"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="499.51" y1="1150.26" x2="491.94" y2="95.78" gradientUnits="userSpaceOnUse"><stop offset="0.05" stop-color="#6ee7e7"/><stop offset="1" stop-color="#8f00ff"/></linearGradient></defs><path class="cls-1" d="M924.24,0H66.37A66.37,66.37,0,0,0,0,66.37v996.74a66.38,66.38,0,0,0,66.37,66.37H924.24a66.37,66.37,0,0,0,66.37-66.37V66.37A66.36,66.36,0,0,0,924.24,0Zm38.5,1054a46.55,46.55,0,0,1-46.55,46.56H74.43A46.56,46.56,0,0,1,27.87,1054V75.06A46.57,46.57,0,0,1,74.43,28.5H916.19a46.56,46.56,0,0,1,46.55,46.56Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/delete-logo.png b/src/assets/icons/delete-logo.png Binary files differnew file mode 100644 index 00000000..54a7228b --- /dev/null +++ b/src/assets/icons/delete-logo.png 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 @@ +<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:#ed2224;stroke-miterlimit:10;stroke-width:56.39px;}.cls-2{stroke-linecap:round;}</style></defs><ellipse class="cls-1" cx="395.85" cy="395.85" rx="365.48" ry="365.55"/><line class="cls-2" x1="198.4" y1="395.51" x2="593.29" y2="395.51"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/plus-logo.png b/src/assets/icons/plus-logo.png Binary files differnew file mode 100644 index 00000000..195f28fc --- /dev/null +++ b/src/assets/icons/plus-logo.png diff --git a/src/assets/images/welcome.png b/src/assets/images/welcome.png Binary files differnew file mode 100644 index 00000000..46ab4f9f --- /dev/null +++ b/src/assets/images/welcome.png diff --git a/src/assets/moment-categories/adventure-icon.png b/src/assets/moment-categories/adventure-icon.png Binary files differnew file mode 100644 index 00000000..33f821ec --- /dev/null +++ b/src/assets/moment-categories/adventure-icon.png diff --git a/src/assets/moment-categories/art-icon.png b/src/assets/moment-categories/art-icon.png Binary files differnew file mode 100644 index 00000000..c43d941b --- /dev/null +++ b/src/assets/moment-categories/art-icon.png diff --git a/src/assets/moment-categories/beauty-icon.png b/src/assets/moment-categories/beauty-icon.png Binary files differnew file mode 100644 index 00000000..1df48648 --- /dev/null +++ b/src/assets/moment-categories/beauty-icon.png diff --git a/src/assets/moment-categories/diy-icon.png b/src/assets/moment-categories/diy-icon.png Binary files differnew file mode 100644 index 00000000..2c339489 --- /dev/null +++ b/src/assets/moment-categories/diy-icon.png diff --git a/src/assets/moment-categories/early-life-icon.png b/src/assets/moment-categories/early-life-icon.png Binary files differnew file mode 100644 index 00000000..91aca375 --- /dev/null +++ b/src/assets/moment-categories/early-life-icon.png diff --git a/src/assets/moment-categories/fashion-icon.png b/src/assets/moment-categories/fashion-icon.png Binary files differnew file mode 100644 index 00000000..106b81ec --- /dev/null +++ b/src/assets/moment-categories/fashion-icon.png diff --git a/src/assets/moment-categories/food-icon.png b/src/assets/moment-categories/food-icon.png Binary files differnew file mode 100644 index 00000000..476521a8 --- /dev/null +++ b/src/assets/moment-categories/food-icon.png diff --git a/src/assets/moment-categories/friends-icon.png b/src/assets/moment-categories/friends-icon.png Binary files differnew file mode 100644 index 00000000..6b595dd1 --- /dev/null +++ b/src/assets/moment-categories/friends-icon.png diff --git a/src/assets/moment-categories/music-icon.png b/src/assets/moment-categories/music-icon.png Binary files differnew file mode 100644 index 00000000..8e5d82f9 --- /dev/null +++ b/src/assets/moment-categories/music-icon.png diff --git a/src/assets/moment-categories/nature-icon.png b/src/assets/moment-categories/nature-icon.png Binary files differnew file mode 100644 index 00000000..2870694c --- /dev/null +++ b/src/assets/moment-categories/nature-icon.png diff --git a/src/assets/moment-categories/nightlife-icon.png b/src/assets/moment-categories/nightlife-icon.png Binary files differnew file mode 100644 index 00000000..1e473b6c --- /dev/null +++ b/src/assets/moment-categories/nightlife-icon.png diff --git a/src/assets/moment-categories/pets-icon.png b/src/assets/moment-categories/pets-icon.png Binary files differnew file mode 100644 index 00000000..91f65f3c --- /dev/null +++ b/src/assets/moment-categories/pets-icon.png diff --git a/src/assets/moment-categories/photo-dump-icon.png b/src/assets/moment-categories/photo-dump-icon.png Binary files differnew file mode 100644 index 00000000..ee5585c3 --- /dev/null +++ b/src/assets/moment-categories/photo-dump-icon.png diff --git a/src/assets/moment-categories/sports-icon.png b/src/assets/moment-categories/sports-icon.png Binary files differnew file mode 100644 index 00000000..9edd76b8 --- /dev/null +++ b/src/assets/moment-categories/sports-icon.png diff --git a/src/assets/moment-categories/travel-icon.png b/src/assets/moment-categories/travel-icon.png Binary files differnew file mode 100644 index 00000000..5a913ac7 --- /dev/null +++ b/src/assets/moment-categories/travel-icon.png 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 ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <View style={styles.textContainer}> <Text style={styles.header}>Coming Soon</Text> <Text style={styles.subtext}> 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<OnboardingStackParams, 'TaggPopup'>; +type TaggPopupNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'TaggPopup' +>; + +interface TaggPopupProps { + route: TaggPopupRouteProps; + navigation: TaggPopupNavigationProps; +} + +const TaggPopup: React.FC<TaggPopupProps> = ({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 ( + <View style={styles.container}> + <View style={styles.popup}> + <Image + style={styles.icon} + source={require('../../assets/icons/plus-logo.png')} + /> + <View style={styles.textContainer}> + <Text style={styles.header}>{messageHeader}</Text> + <Text style={styles.subtext}>{messageBody}</Text> + </View> + {!next && ( + <TouchableOpacity + style={styles.closeButton} + onPress={() => { + navigation.goBack(); + }}> + <CloseIcon height={'50%'} width={'50%'} color={'white'} /> + </TouchableOpacity> + )} + </View> + {next && ( + <View style={styles.footer}> + <ArrowButton + direction="forward" + onPress={() => { + navigation.navigate('TaggPopup', {popupProps: next}); + }} + /> + </View> + )} + </View> + ); +}; + +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<MomentProps> = ({ @@ -24,6 +27,8 @@ const Moment: React.FC<MomentProps> = ({ images, userXId, screenType, + handleMomentCategoryDelete, + shouldAllowDeletion, }) => { const navigation = useNavigation(); @@ -53,11 +58,21 @@ const Moment: React.FC<MomentProps> = ({ <View style={styles.header}> <Text style={styles.titleText}>{title}</Text> {!userXId ? ( - <PlusIcon - width={21} - height={21} - onPress={() => navigateToImagePicker()} - /> + <> + <PlusIcon + width={21} + height={21} + onPress={() => navigateToImagePicker()} + style={{marginRight: 10}} + /> + {shouldAllowDeletion && ( + <DeleteIcon + onPress={() => handleMomentCategoryDelete(title)} + width={19} + height={19} + /> + )} + </> ) : ( <React.Fragment /> )} @@ -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<BackgroundProps> = (props) => { + const {centered, gradientType, children} = props; return ( <LinearGradient - colors={['#9F00FF', '#27EAE9']} + colors={BACKGROUND_GRADIENT_MAP[gradientType]} useAngle={true} angle={154.72} angleCenter={{x: 0.5, y: 0.5}} style={styles.container}> <TouchableWithoutFeedback accessible={false} onPress={Keyboard.dismiss}> - {props.centered ? ( - <CenteredView {...props}>{props.children}</CenteredView> + {centered ? ( + <CenteredView {...props}>{children}</CenteredView> ) : ( - <SafeAreaView {...props}>{props.children}</SafeAreaView> + <SafeAreaView {...props}>{children}</SafeAreaView> )} </TouchableWithoutFeedback> </LinearGradient> 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<MomentCategoryProps> = ({ + 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 ( + <LinearGradient + colors={ + isAdded + ? BACKGROUND_GRADIENT_MAP[0] + : isSelected + ? ['white', 'white'] + : ['transparent', 'transparent'] + } + start={{x: 0, y: 0}} + end={{x: 1, y: 0}} + style={[styles.container, styles.gradient]}> + <TouchableOpacity + activeOpacity={0.5} + onPress={() => onSelect(categoryType, !isSelected, isAdded)} + style={[ + styles.container, + styles.touchable, + {backgroundColor: bgColor}, + ]}> + <Image source={icon} style={styles.icon} /> + <Text style={styles.label}>{categoryType}</Text> + {isAdded && ( + <Image + source={require('../../assets/images/link-tick.png')} + style={styles.tick} + /> + )} + </TouchableOpacity> + </LinearGradient> + ); +}; + +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<number>; @@ -60,6 +72,10 @@ 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 + ? 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<ContentProps> = ({y, userXId, screenType}) => { ); const state = useStore().getState(); + const navigation = useNavigation(); + /** * States */ @@ -80,6 +98,13 @@ 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 onRefresh = useCallback(() => { const refrestState = async () => { if (!userXId) { @@ -194,6 +219,33 @@ const Content: React.FC<ContentProps> = ({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<NativeScrollEvent>) => { /** * Set the new y position @@ -239,32 +291,60 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { /> <TaggsBar {...{y, profileBodyHeight, userXId, screenType}} /> <View style={styles.momentsContainer}> - {defaultMoments.map((title, index) => ( + {userMomentCategories.map((title, index) => ( <Moment key={index} title={title} images={imagesMap.get(title)} userXId={userXId} screenType={screenType} + handleMomentCategoryDelete={handleCategoryDeletion} + shouldAllowDeletion={userMomentCategories.length > 2} /> ))} + {!userXId && userMomentCategories.length < 6 && ( + <TouchableOpacity + onPress={() => + navigation.push('CategorySelection', { + categories: momentCategories, + screenType: CategorySelectionScreenType.Profile, + user: loggedInUser, + }) + } + style={styles.createCategoryButton}> + <Text style={styles.createCategoryButtonLabel}> + Create a new category + </Text> + </TouchableOpacity> + )} </View> </Animated.ScrollView> ); }; 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<string, string> = { Instagram: 'https://instagram.com/', Twitter: 'https://twitter.com/', }; + +export const MOMENT_CATEGORIES: Array<MomentCategoryType> = [ + '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<ReactText> +> = { + [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: { @@ -42,6 +45,47 @@ const Onboarding: React.FC = () => { }} /> <OnboardingStack.Screen + name="WelcomeScreen" + component={WelcomeScreen} + options={{ + gestureEnabled: false, + }} + /> + <OnboardingStack.Screen + name="CategorySelection" + component={CategorySelection} + options={{ + gestureEnabled: false, + }} + /> + <OnboardingStack.Screen + name="TaggPopup" + component={TaggPopup} + options={{ + gestureEnabled: false, + cardStyle: { + backgroundColor: 'transparent', + }, + cardOverlayEnabled: true, + cardStyleInterpolator: ({current: {progress}}) => ({ + 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', + }), + }, + }), + }} + /> + <OnboardingStack.Screen name="PasswordReset" component={PasswordReset} options={{ diff --git a/src/routes/onboarding/OnboardingStack.tsx b/src/routes/onboarding/OnboardingStack.tsx index 33ff51ea..7ff00271 100644 --- a/src/routes/onboarding/OnboardingStack.tsx +++ b/src/routes/onboarding/OnboardingStack.tsx @@ -1,7 +1,14 @@ import {createStackNavigator} from '@react-navigation/stack'; -import {VerificationScreenType} from '../../types'; +import { + CategorySelectionScreenType, + MomentCategoryType, + TaggPopupType, + UserType, + VerificationScreenType, +} from '../../types'; export type OnboardingStackParams = { + WelcomeScreen: undefined; Login: undefined; PasswordResetRequest: undefined; PasswordReset: { @@ -20,6 +27,14 @@ export type OnboardingStackParams = { Verification: {id: string; screenType: VerificationScreenType}; ProfileOnboarding: {username: string; userId: string}; SocialMedia: {username: string; userId: string}; + CategorySelection: { + categories: Record<MomentCategoryType, boolean>; + screenType: CategorySelectionScreenType; + user: UserType; + }; + TaggPopup: { + popupProps: TaggPopupType; + }; }; export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); 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<ProfileStackProps> = ({route}) => { }} initialParams={{screenType}} /> + <ProfileStack.Screen + name="CategorySelection" + component={CategorySelection} + options={{ + headerShown: true, + headerTransparent: true, + headerBackTitleVisible: false, + headerTintColor: 'white', + headerTitle: '', + }} + /> {isProfileStack ? ( <ProfileStack.Screen name="CaptionScreen" component={CaptionScreen} /> ) : ( 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<string>; + screenType: CategorySelectionScreenType; + }; }; export const ProfileStack = createStackNavigator<ProfileStackParams>(); 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<CategorySelectionProps> = ({ + 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<MomentCategoryType> + >([]); + + 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 ( + <ScrollView bounces={false}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Dark}> + <StatusBar barStyle="light-content" /> + <Text style={styles.subtext}>Create new categories</Text> + <View style={styles.container}> + <View style={styles.linkerContainer}> + {MOMENT_CATEGORIES.map((category, index) => ( + <MomentCategory + key={index} + categoryType={category} + isSelected={selectedCategories.includes(category)} + isAdded={categories[category]} + onSelect={onSelect} + /> + ))} + </View> + <TouchableOpacity + onPress={handleButtonPress} + style={styles.finalAction}> + <Text style={styles.finalActionLabel}> + {isOnBoarding ? 'Login' : 'Create'} + </Text> + </TouchableOpacity> + </View> + </Background> + </ScrollView> + ); +}; + +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<OnboardingStackParams, 'Checkpoint'>; type CheckpointNavigationProp = StackNavigationProp< @@ -44,7 +45,9 @@ const Checkpoint: React.FC<CheckpointProps> = ({route, navigation}) => { }; return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <RegistrationWizard style={styles.wizard} step="six" /> <View style={styles.textContainer}> 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<InvitationCodeVerificationProps> = ({ <View style={styles.footer}> <ArrowButton direction="backward" - onPress={() => navigation.navigate('Login')} + onPress={() => navigation.navigate('WelcomeScreen')} /> </View> ); return ( - <Background centered style={styles.container}> + <Background + centered + style={styles.container} + gradientType={BackgroundGradientType.Light}> <RegistrationWizard style={styles.wizard} step="one" /> <KeyboardAvoidingView behavior="padding" style={styles.form}> <Text style={styles.formHeader}>Enter the code</Text> 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<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< @@ -194,8 +199,8 @@ const Login: React.FC<LoginProps> = ({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<LoginProps> = ({navigation}: LoginProps) => { accessible={true} accessibilityLabel="Get started" style={styles.getStarted} - onPress={goToRegistration}> + onPress={startRegistrationProcess}> Get started! </Text> </TouchableOpacity> @@ -252,7 +257,10 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { ); return ( - <Background centered style={styles.container}> + <Background + centered + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} diff --git a/src/screens/onboarding/PasswordReset.tsx b/src/screens/onboarding/PasswordReset.tsx index 25991d6e..11ca60d5 100644 --- a/src/screens/onboarding/PasswordReset.tsx +++ b/src/screens/onboarding/PasswordReset.tsx @@ -24,6 +24,7 @@ import { import {trackPromise} from 'react-promise-tracker'; import {passwordRegex} from '../../constants'; import {handlePasswordReset} from '../../services'; +import {BackgroundGradientType} from '../../types'; type PasswordResetRequestRouteProp = RouteProp< OnboardingStackParams, @@ -141,7 +142,9 @@ const PasswordResetRequest: React.FC<PasswordResetRequestProps> = ({ ); return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} diff --git a/src/screens/onboarding/PasswordResetRequest.tsx b/src/screens/onboarding/PasswordResetRequest.tsx index 5f67eb19..cf086f59 100644 --- a/src/screens/onboarding/PasswordResetRequest.tsx +++ b/src/screens/onboarding/PasswordResetRequest.tsx @@ -24,7 +24,7 @@ import { import {trackPromise} from 'react-promise-tracker'; import {emailRegex, usernameRegex} from '../../constants'; import {handlePasswordResetRequest} from '../../services'; -import {VerificationScreenType} from '../../types'; +import {BackgroundGradientType, VerificationScreenType} from '../../types'; type PasswordResetRequestRouteProp = RouteProp< OnboardingStackParams, @@ -115,7 +115,9 @@ const PasswordResetRequest: React.FC<PasswordResetRequestProps> = ({ ); return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 9d99f2b1..611f1598 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -27,6 +27,7 @@ import { genderRegex, } from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; +import {BackgroundGradientType} from '../../types'; type ProfileOnboardingScreenRouteProp = RouteProp< OnboardingStackParams, @@ -343,7 +344,7 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({ }; return ( - <Background centered> + <Background centered gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <View style={styles.profile}> <LargeProfilePic /> 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<RegistrationOneProps> = ({navigation}) => { ); return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <RegistrationWizard style={styles.wizard} step="two" /> <KeyboardAvoidingView diff --git a/src/screens/onboarding/RegistrationThree.tsx b/src/screens/onboarding/RegistrationThree.tsx index 614795ca..52a6de84 100644 --- a/src/screens/onboarding/RegistrationThree.tsx +++ b/src/screens/onboarding/RegistrationThree.tsx @@ -29,6 +29,7 @@ import { } from '../../components'; import {passwordRegex, usernameRegex, REGISTER_ENDPOINT} from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; +import {BackgroundGradientType} from '../../types'; type RegistrationScreenThreeRouteProp = RouteProp< OnboardingStackParams, @@ -241,7 +242,9 @@ const RegistrationThree: React.FC<RegistrationThreeProps> = ({ ); return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <RegistrationWizard style={styles.wizard} step="five" /> <KeyboardAvoidingView diff --git a/src/screens/onboarding/RegistrationTwo.tsx b/src/screens/onboarding/RegistrationTwo.tsx index edefebaf..2f67d8c8 100644 --- a/src/screens/onboarding/RegistrationTwo.tsx +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -22,6 +22,7 @@ import { } from '../../components'; import {nameRegex, emailRegex} from '../../constants'; +import {BackgroundGradientType} from '../../types'; type RegistrationScreenTwoRouteProp = RouteProp< OnboardingStackParams, @@ -170,7 +171,9 @@ const RegistrationTwo: React.FC<RegistrationTwoProps> = ({ ); return ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <RegistrationWizard style={styles.wizard} step="four" /> <KeyboardAvoidingView diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx index ee2bed10..d2a43e7a 100644 --- a/src/screens/onboarding/SocialMedia.tsx +++ b/src/screens/onboarding/SocialMedia.tsx @@ -1,4 +1,5 @@ import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; import { Alert, @@ -11,7 +12,11 @@ import { View, } from 'react-native'; import {useDispatch} from 'react-redux'; -import {LinkerType} from 'src/types'; +import { + BackgroundGradientType, + CategorySelectionScreenType, + LinkerType, +} from '../..//types'; import { Background, LinkSocialMedia, @@ -19,7 +24,7 @@ import { } from '../../components'; import {SOCIAL_LIST} from '../../constants/'; import {OnboardingStackParams} from '../../routes'; -import {userLogin} from '../../utils'; +import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates'; /** * Social Media Screen for displaying social media linkers @@ -27,11 +32,17 @@ import {userLogin} from '../../utils'; type SocialMediaRouteProps = RouteProp<OnboardingStackParams, 'SocialMedia'>; +type SocialMediaNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'SocialMedia' +>; + interface SocialMediaProps { route: SocialMediaRouteProps; + navigation: SocialMediaNavigationProps; } -const SocialMedia: React.FC<SocialMediaProps> = ({route}) => { +const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { const {userId, username} = route.params; const linkers: Array<LinkerType> = []; @@ -56,17 +67,18 @@ const SocialMedia: React.FC<SocialMediaProps> = ({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 ( - <Background style={styles.container}> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} @@ -84,8 +96,8 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route}) => { ))} </View> </KeyboardAvoidingView> - <TouchableOpacity onPress={handleLogin} style={styles.loginButton}> - <Text style={styles.loginButtonLabel}>Login</Text> + <TouchableOpacity onPress={handleNext} style={styles.nextButton}> + <Text style={styles.nextButtonLabel}>Next</Text> </TouchableOpacity> </Background> ); @@ -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<VerificationProps> = ({route, navigation}) => { ); return ( - <Background centered style={styles.container}> + <Background + centered + style={styles.container} + gradientType={BackgroundGradientType.Light}> {isPhoneVerification ? ( <RegistrationWizard style={styles.wizard} step="three" /> ) : ( 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<WelcomeScreenProps> = ({navigation}) => { + const handleNext = () => { + navigation.navigate('InvitationCodeVerification'); + }; + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <Image + source={require('../../assets/images/welcome.png')} + style={styles.image} + /> + + <View> + <Text style={styles.header}>Welcome to TAGG!</Text> + <Text style={styles.subtext}> + 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! + </Text> + </View> + <TouchableOpacity onPress={handleNext} style={styles.nextButton}> + <Text style={styles.nextButtonLabel}>Next</Text> + </TouchableOpacity> + </Background> + ); +}; + +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<EditProfileProps> = ({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<EditProfileProps> = ({route, navigation}) => { }, [navigation, handleSubmit]); return ( - <Background centered> + <Background centered gradientType={BackgroundGradientType.Light}> <SafeAreaView> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}> 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<MomentCategoryType[]> = 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<MomentCategoryType>, + token: string, +) => Promise<boolean> = 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<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; +}; 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<Promise<void>, RootState, unknown, Action<string>> => 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<MomentCategoryType>, + 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); + } + 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<MomentCategoryType, boolean> = { + 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 = <UserXType>{ 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<MomentCategoryType> = 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<MomentCategoryType> = action.payload.data; + for (let category of categories) { + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].momentCategories[category] = true; + } + }, + userXMomentsFetched: (state, action) => { state[<ScreenType>action.payload.screenType][ action.payload.userId ].moments = action.payload.data; }, + userXFollowersFetched: (state, action) => { state[<ScreenType>action.payload.screenType][ action.payload.userId ].followers = action.payload.data; }, + userXFollowingFetched: (state, action) => { state[<ScreenType>action.payload.screenType][ action.payload.userId ].following = action.payload.data; }, + userXAvatarFetched: (state, action) => { state[<ScreenType>action.payload.screenType][ action.payload.userId ].avatar = action.payload.data; }, + userXCoverFetched: (state, action) => { state[<ScreenType>action.payload.screenType][ action.payload.userId ].cover = action.payload.data; }, + userXSocialsFetched: (state, action) => { state[<ScreenType>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<string, SocialAccountType>; + momentCategories: Record<MomentCategoryType, boolean>; 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)), |