diff options
author | Ivan Chen <ivan@thetaggid.com> | 2021-01-12 12:38:46 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-12 12:38:46 -0500 |
commit | 6892c63b899b46fedc9d99b8274a17e9043fe361 (patch) | |
tree | 454d836c5848b4d9b2e082ae19e4e64679ccd49d | |
parent | d955c6bc31be3b2e3e289a8dec8b5970251d4090 (diff) |
[TMA-527/506/523] Custom Moment Categories (#174)
* changed logic to allow ≥ 1 categories
* now using array of strings for moment categories
* updated error strings
* formatting and check for picker cancellation
* initial UI done
* cleaned up logic, added custom icon
* renamed onboarding stack to match main stack
* removed unused import
* deterministic color picker
* custom category defaults to selected instead of added
* removed function in route
24 files changed, 451 insertions, 269 deletions
diff --git a/src/assets/icons/plus_icon-01.svg b/src/assets/icons/plus_icon-01.svg index 32632897..7a3b21d2 100644 --- a/src/assets/icons/plus_icon-01.svg +++ b/src/assets/icons/plus_icon-01.svg @@ -1 +1 @@ -<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke:#718dc3;stroke-miterlimit:10;stroke-width:11px;}.cls-2{fill:#718dc3;}</style></defs><circle class="cls-1" cx="108" cy="108" r="84.9"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)"/></svg>
\ No newline at end of file +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke-miterlimit:10;stroke-width:11px;}</style></defs><circle stroke="currentColor" class="cls-1" cx="108" cy="108" r="84.9" /><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" fill="currentColor"/><rect class="cls-2" x="101.05" y="59.11" width="13.91" height="97.78" rx="6.34" transform="translate(0 216) rotate(-90)" fill="currentColor" /></svg>
\ No newline at end of file diff --git a/src/assets/moment-categories/custom-icon.png b/src/assets/moment-categories/custom-icon.png Binary files differnew file mode 100644 index 00000000..f81546d6 --- /dev/null +++ b/src/assets/moment-categories/custom-icon.png diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 0d2c2b62..be6f78a8 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -11,14 +11,14 @@ import {TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import {SCREEN_WIDTH} from '../../utils'; import ImagePicker from 'react-native-image-crop-picker'; import MomentTile from './MomentTile'; -import {MomentCategoryType, MomentType, ScreenType} from 'src/types'; +import {MomentType, ScreenType} from 'src/types'; interface MomentProps { - title: MomentCategoryType; + title: string; images: MomentType[] | undefined; userXId: string | undefined; screenType: ScreenType; - handleMomentCategoryDelete: (_: MomentCategoryType) => void; + handleMomentCategoryDelete: (_: string) => void; shouldAllowDeletion: boolean; } @@ -57,7 +57,9 @@ const Moment: React.FC<MomentProps> = ({ } }) .catch((err) => { - Alert.alert('Unable to upload moment!'); + if (err.code && err.code !== 'E_PICKER_CANCELLED') { + Alert.alert('Unable to upload moment!'); + } }); }; return ( @@ -70,6 +72,7 @@ const Moment: React.FC<MomentProps> = ({ width={21} height={21} onPress={() => navigateToImagePicker()} + color={TAGG_TEXT_LIGHT_BLUE} style={{marginRight: 10}} /> {shouldAllowDeletion && ( diff --git a/src/components/onboarding/MomentCategory.tsx b/src/components/onboarding/MomentCategory.tsx index 827ab207..97099b9e 100644 --- a/src/components/onboarding/MomentCategory.tsx +++ b/src/components/onboarding/MomentCategory.tsx @@ -1,19 +1,17 @@ -import * as React from 'react'; +import React from 'react'; import {StyleSheet} from 'react-native'; import {Image, Text} from 'react-native-animatable'; import {TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; -import {BACKGROUND_GRADIENT_MAP} from '../../constants'; -import {MomentCategoryType} from '../../types'; +import { + BACKGROUND_GRADIENT_MAP, + MOMENT_CATEGORY_BG_COLORS, +} from '../../constants'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; type MomentCategoryProps = { - categoryType: MomentCategoryType; - onSelect: ( - category: MomentCategoryType, - isSelected: boolean, - isAdded: boolean, - ) => void; + categoryType: string; + onSelect: (category: string, isSelected: boolean, isAdded: boolean) => void; isSelected: boolean; isAdded: boolean; }; @@ -32,63 +30,75 @@ const MomentCategory: React.FC<MomentCategoryProps> = ({ switch (categoryType) { case 'Friends': icon = require('../../assets/moment-categories/friends-icon.png'); - bgColor = '#5E4AE4'; + bgColor = MOMENT_CATEGORY_BG_COLORS[0]; break; case 'Adventure': icon = require('../../assets/moment-categories/adventure-icon.png'); - bgColor = '#5044A6'; + bgColor = MOMENT_CATEGORY_BG_COLORS[1]; break; case 'Photo Dump': icon = require('../../assets/moment-categories/photo-dump-icon.png'); - bgColor = '#4755A1'; + bgColor = MOMENT_CATEGORY_BG_COLORS[2]; break; case 'Food': icon = require('../../assets/moment-categories/food-icon.png'); - bgColor = '#444BA8'; + bgColor = MOMENT_CATEGORY_BG_COLORS[3]; break; case 'Music': icon = require('../../assets/moment-categories/music-icon.png'); - bgColor = '#374898'; + bgColor = MOMENT_CATEGORY_BG_COLORS[4]; break; case 'Art': icon = require('../../assets/moment-categories/art-icon.png'); - bgColor = '#3F5C97'; + bgColor = MOMENT_CATEGORY_BG_COLORS[5]; break; case 'Sports': icon = require('../../assets/moment-categories/sports-icon.png'); - bgColor = '#3A649F'; + bgColor = MOMENT_CATEGORY_BG_COLORS[6]; break; case 'Fashion': icon = require('../../assets/moment-categories/fashion-icon.png'); - bgColor = '#386A95'; + bgColor = MOMENT_CATEGORY_BG_COLORS[7]; break; case 'Travel': icon = require('../../assets/moment-categories/travel-icon.png'); - bgColor = '#366D84'; + bgColor = MOMENT_CATEGORY_BG_COLORS[8]; break; case 'Pets': icon = require('../../assets/moment-categories/pets-icon.png'); - bgColor = '#335E76'; + bgColor = MOMENT_CATEGORY_BG_COLORS[9]; break; case 'Fitness': icon = require('../../assets/moment-categories/fitness-icon.png'); - bgColor = '#2E5471'; + bgColor = MOMENT_CATEGORY_BG_COLORS[10]; break; case 'DIY': icon = require('../../assets/moment-categories/diy-icon.png'); - bgColor = '#274765'; + bgColor = MOMENT_CATEGORY_BG_COLORS[11]; break; case 'Nature': icon = require('../../assets/moment-categories/nature-icon.png'); - bgColor = '#225363'; + bgColor = MOMENT_CATEGORY_BG_COLORS[12]; break; case 'Early Life': icon = require('../../assets/moment-categories/early-life-icon.png'); - bgColor = '#365F6A'; + bgColor = MOMENT_CATEGORY_BG_COLORS[13]; break; case 'Beauty': icon = require('../../assets/moment-categories/beauty-icon.png'); - bgColor = '#4E7175'; + bgColor = MOMENT_CATEGORY_BG_COLORS[14]; + break; + default: + // All custom categories + icon = require('../../assets/moment-categories/custom-icon.png'); + // A quick deterministic "random" color picker by summing up ascii char codees + const charCodeSum = categoryType + .split('') + .reduce((acc: number, x: string) => acc + x.charCodeAt(0), 0); + bgColor = + MOMENT_CATEGORY_BG_COLORS[ + charCodeSum % MOMENT_CATEGORY_BG_COLORS.length + ]; break; } diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index 3a304938..5fa05588 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -12,18 +12,13 @@ import { import Animated from 'react-native-reanimated'; import { CategorySelectionScreenType, - MomentCategoryType, MomentType, ProfilePreviewType, ProfileType, ScreenType, UserType, } from '../../types'; -import { - COVER_HEIGHT, - MOMENT_CATEGORIES, - TAGG_TEXT_LIGHT_BLUE, -} from '../../constants'; +import {COVER_HEIGHT, TAGG_TEXT_LIGHT_BLUE} from '../../constants'; import {fetchUserX, SCREEN_HEIGHT, userLogin} from '../../utils'; import TaggsBar from '../taggs/TaggsBar'; import {Moment} from '../moments'; @@ -45,7 +40,6 @@ import { NO_PROFILE, EMPTY_PROFILE_PREVIEW_LIST, EMPTY_MOMENTS_LIST, - MOMENT_CATEGORIES_MAP, } from '../../store/initialStates'; import {Cover} from '.'; import {TouchableOpacity} from 'react-native-gesture-handler'; @@ -77,7 +71,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { ? useSelector((state: RootState) => state.userX[screenType][userXId]) : useSelector((state: RootState) => state.moments); - const {momentCategories = MOMENT_CATEGORIES_MAP} = userXId + const {momentCategories = []} = userXId ? useSelector((state: RootState) => state.userX[screenType][userXId]) : useSelector((state: RootState) => state.momentCategories); @@ -103,13 +97,6 @@ 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) { @@ -226,7 +213,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { * Confirm with user before deleting the category * @param category category to be deleted */ - const handleCategoryDeletion = (category: MomentCategoryType) => { + const handleCategoryDeletion = (category: string) => { Alert.alert( 'Category Deletion', `Are you sure that you want to delete the category ${category} ?`, @@ -239,7 +226,12 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { text: 'Yes', onPress: () => { dispatch( - updateMomentCategories([category], false, loggedInUser.userId), + updateMomentCategories( + momentCategories.filter( + (mc) => mc !== category, + loggedInUser.userId, + ), + ), ); dispatch(deleteUserMomentsForCategory(category)); }, @@ -304,7 +296,7 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { } has not posted any moments yet`}</Text> </View> )} - {userMomentCategories.map( + {momentCategories.map( (title, index) => (!userXId || imagesMap.get(title)) && ( <Moment @@ -314,15 +306,14 @@ const Content: React.FC<ContentProps> = ({y, userXId, screenType}) => { userXId={userXId} screenType={screenType} handleMomentCategoryDelete={handleCategoryDeletion} - shouldAllowDeletion={userMomentCategories.length > 2} + shouldAllowDeletion={momentCategories.length > 1} /> ), )} - {!userXId && userMomentCategories.length < 6 && ( + {!userXId && ( <TouchableOpacity onPress={() => navigation.push('CategorySelection', { - categories: momentCategories, screenType: CategorySelectionScreenType.Profile, user: loggedInUser, }) diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 531420e6..8d8b7dfe 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,5 +1,5 @@ import {ReactText} from 'react'; -import {BackgroundGradientType, MomentCategoryType} from './../types/'; +import {BackgroundGradientType} from './../types/'; import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils'; export const CHIN_HEIGHT = 34; @@ -103,7 +103,7 @@ export const BROWSABLE_SOCIAL_URLS: Record<string, string> = { Twitter: 'https://twitter.com/', }; -export const MOMENT_CATEGORIES: Array<MomentCategoryType> = [ +export const MOMENT_CATEGORIES: string[] = [ 'Friends', 'Adventure', 'Photo Dump', @@ -140,3 +140,21 @@ export const CLASS_YEAR_LIST: Array<string> = [ '2025', '2026', ]; + +export const MOMENT_CATEGORY_BG_COLORS: string[] = [ + '#5E4AE4', + '#5044A6', + '#4755A1', + '#444BA8', + '#374898', + '#3F5C97', + '#3A649F', + '#386A95', + '#366D84', + '#335E76', + '#2E5471', + '#274765', + '#225363', + '#365F6A', + '#4E7175', +]; diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index 4614168b..950f3ffc 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.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 {CategorySelectionScreenType, MomentType, ScreenType} from '../../types'; +import {MomentType, ScreenType} from '../../types'; export type MainStackParams = { Search: { @@ -40,10 +40,8 @@ export type MainStackParams = { userId: string; username: string; }; - CategorySelection: { - categories: Array<string>; - screenType: CategorySelectionScreenType; - }; + CategorySelection: {}; + CreateCustomCategory: {}; Notifications: { screenType: ScreenType; }; diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index bf643fd8..4ad5bf40 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -10,6 +10,7 @@ import { CategorySelection, FriendsListScreen, NotificationsScreen, + CreateCustomCategory, } from '../../screens'; import {MainStack, MainStackParams} from './MainStackNavigator'; import {RouteProp} from '@react-navigation/native'; @@ -141,6 +142,17 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { }} /> <MainStack.Screen + name="CreateCustomCategory" + component={CreateCustomCategory} + options={{ + headerShown: true, + headerTransparent: true, + headerBackTitleVisible: false, + headerTintColor: 'white', + headerTitle: '', + }} + /> + <MainStack.Screen name="IndividualMoment" component={IndividualMoment} options={{ diff --git a/src/routes/onboarding/OnboardingStack.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx index 7ff00271..ea7ce8e8 100644 --- a/src/routes/onboarding/OnboardingStack.tsx +++ b/src/routes/onboarding/OnboardingStackNavigator.tsx @@ -1,7 +1,6 @@ import {createStackNavigator} from '@react-navigation/stack'; import { CategorySelectionScreenType, - MomentCategoryType, TaggPopupType, UserType, VerificationScreenType, @@ -28,9 +27,14 @@ export type OnboardingStackParams = { ProfileOnboarding: {username: string; userId: string}; SocialMedia: {username: string; userId: string}; CategorySelection: { - categories: Record<MomentCategoryType, boolean>; screenType: CategorySelectionScreenType; user: UserType; + newCustomCategory: string | undefined; + }; + CreateCustomCategory: { + screenType: CategorySelectionScreenType; + user: UserType; + existingCategories: string[]; }; TaggPopup: { popupProps: TaggPopupType; diff --git a/src/routes/onboarding/Onboarding.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx index a3d281f5..54614b32 100644 --- a/src/routes/onboarding/Onboarding.tsx +++ b/src/routes/onboarding/OnboardingStackScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {OnboardingStack} from './OnboardingStack'; +import {OnboardingStack} from './OnboardingStackNavigator'; import { Login, InvitationCodeVerification, diff --git a/src/routes/onboarding/index.ts b/src/routes/onboarding/index.ts index 66b0f3f4..ce9ac046 100644 --- a/src/routes/onboarding/index.ts +++ b/src/routes/onboarding/index.ts @@ -1,2 +1,2 @@ -export * from './OnboardingStack'; -export {default} from './Onboarding'; +export * from './OnboardingStackNavigator'; +export {default} from './OnboardingStackScreen'; diff --git a/src/screens/onboarding/CategorySelection.tsx b/src/screens/onboarding/CategorySelection.tsx index b9677ed4..540b106f 100644 --- a/src/screens/onboarding/CategorySelection.tsx +++ b/src/screens/onboarding/CategorySelection.tsx @@ -1,8 +1,8 @@ import {RouteProp} from '@react-navigation/native'; -import React, {useCallback, useEffect, useState} from 'react'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useEffect, useState} from 'react'; import { Alert, - KeyboardAvoidingView, Platform, StatusBar, StyleSheet, @@ -10,20 +10,17 @@ import { TouchableOpacity, View, } from 'react-native'; -import {useDispatch} from 'react-redux'; -import { - BackgroundGradientType, - CategorySelectionScreenType, - MomentCategoryType, -} from '../../types'; +import {ScrollView} from 'react-native-gesture-handler'; +import {useDispatch, useSelector} from 'react-redux'; +import PlusIcon from '../../assets/icons/plus_icon-01.svg'; import {Background, MomentCategory} from '../../components'; import {MOMENT_CATEGORIES} from '../../constants'; import {OnboardingStackParams} from '../../routes'; -import {StackNavigationProp} from '@react-navigation/stack'; -import {getTokenOrLogout, userLogin} from '../../utils'; import {fcmService, postMomentCategories} from '../../services'; import {updateMomentCategories} from '../../store/actions/momentCategories'; -import {ScrollView} from 'react-native-gesture-handler'; +import {RootState} from '../../store/rootReducer'; +import {BackgroundGradientType, CategorySelectionScreenType} from '../../types'; +import {getTokenOrLogout, SCREEN_WIDTH, userLogin} from '../../utils'; type CategorySelectionRouteProps = RouteProp< OnboardingStackParams, @@ -47,17 +44,47 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ /** * Same component to be used for category selection while onboarding and while on profile */ - const {categories, screenType, user} = route.params; + const {screenType, user} = route.params; const isOnBoarding: boolean = screenType === CategorySelectionScreenType.Onboarding; const {userId, username} = user; - const [selectedCategories, setSelectedCategories] = useState< - Array<MomentCategoryType> + // During onboarding this will fail and default to [] + const {momentCategories = []} = useSelector( + (state: RootState) => state.momentCategories, + ); + + // Stores all the categories that will be saved to the store + const [selectedCategories, setSelectedCategories] = useState<string[]>([]); + + /** + * Stores all the custom categories for the UI, allow easier logic for + * unchecking a custom category. + * + * Each uncommited custom category should also have a copy in selectedCategories + * since that's the final value that will be stored in the store. + */ + const [uncommitedCustomCategories, setUncommitedCustomCategories] = useState< + string[] >([]); + const customCategories = momentCategories.filter( + (mc) => !MOMENT_CATEGORIES.includes(mc), + ); + const dispatch = useDispatch(); + useEffect(() => { + const newCustomCategory = route.params.newCustomCategory; + if (newCustomCategory) { + setUncommitedCustomCategories([ + ...uncommitedCustomCategories, + newCustomCategory, + ]); + selectedCategories.push(newCustomCategory); + } + }, [route.params?.newCustomCategory]); + /** * Show the tutorial if a new user is OnBoarding */ @@ -71,7 +98,7 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ next: { messageHeader: 'Select Categories', messageBody: - 'Select between 2 - 6 categories to begin creating moments!', + 'Select at least a category to begin creating moments!', next: undefined, }, }, @@ -89,11 +116,13 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ * Remove from the selected categories */ const onSelect = ( - category: MomentCategoryType, + category: string, isSelected: boolean, isAdded: boolean, ) => { - if (isAdded) return; + if (isAdded) { + return; + } if (isSelected) { setSelectedCategories((prev) => [...prev, category]); } else { @@ -104,30 +133,35 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ }; /** - * if onboarding - * Count of already added categories will always be 0 - * else - * Calculate number of selected categories by iterating through the user's pre-selected categories + * Handle deselection of custom category. + * + * Custom categories is "added" and "selected" by CreateCustomCategory screen. + * User can only "deselect" an uncommited custom category. + * + * case isAdded || isSelected: + * Return without doing anything + * default: + * Remove from selected categories AND uncommitedCustomCategories */ - const addedLength = !isOnBoarding - ? Object.keys(categories).filter((key) => { - return categories[key as MomentCategoryType] === true; - }).length - : 0; + const onDeselectCustomCategory = ( + category: string, + isSelected: boolean, + isAdded: boolean, + ) => { + if (isAdded || isSelected) { + return; + } + setSelectedCategories( + selectedCategories.filter((item) => item !== category), + ); + setUncommitedCustomCategories( + uncommitedCustomCategories.filter((item) => item !== category), + ); + }; const handleButtonPress = async () => { - /** - * Check for lower and upper bound before creating new categories - */ - const totalCategories = addedLength + selectedCategories.length; - if (totalCategories < 2) { - Alert.alert('Please select atleast 2 categories'); - return; - } else if (totalCategories > 6) { - Alert.alert('You may not add more than 6 categories'); - return; - } else if (selectedCategories.length === 0) { - Alert.alert('Please select some categories!'); + if (momentCategories.length + selectedCategories.length === 0) { + Alert.alert('Please select at least 1 category'); return; } try { @@ -137,7 +171,9 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ userLogin(dispatch, {userId: userId, username: username}); fcmService.sendFcmTokenToServer(); } else { - dispatch(updateMomentCategories(selectedCategories, true, userId)); + dispatch( + updateMomentCategories(momentCategories.concat(selectedCategories)), + ); navigation.goBack(); } } catch (error) { @@ -155,15 +191,55 @@ const CategorySelection: React.FC<CategorySelectionProps> = ({ style={styles.container} gradientType={BackgroundGradientType.Dark}> <StatusBar barStyle="light-content" /> - <Text style={styles.subtext}>Create new categories</Text> + <Text style={styles.subtext}>Create Categories</Text> <View style={styles.container}> + {!isOnBoarding && ( + <TouchableOpacity + style={styles.createCategory} + onPress={() => { + navigation.push('CreateCustomCategory', { + screenType, + user, + existingCategories: momentCategories.concat( + selectedCategories, + ), + }); + }}> + <PlusIcon width={30} height={30} color="white" /> + <Text style={styles.createCategoryLabel}> + Create your own category + </Text> + </TouchableOpacity> + )} <View style={styles.linkerContainer}> + {/* commited custom categories */} + {customCategories.map((category, index) => ( + <MomentCategory + key={index} + categoryType={category} + isSelected={false} + isAdded={true} + onSelect={onDeselectCustomCategory} + /> + ))} + {/* uncommited custom categroies */} + {uncommitedCustomCategories.map((category, index) => ( + <MomentCategory + key={index} + categoryType={category} + isSelected={selectedCategories.includes(category)} + isAdded={false} + onSelect={onDeselectCustomCategory} + /> + ))} + {customCategories.length + uncommitedCustomCategories.length !== + 0 && <View style={styles.divider} />} {MOMENT_CATEGORIES.map((category, index) => ( <MomentCategory key={index} categoryType={category} isSelected={selectedCategories.includes(category)} - isAdded={categories[category]} + isAdded={momentCategories.includes(category)} onSelect={onSelect} /> ))} @@ -215,11 +291,12 @@ const styles = StyleSheet.create({ }, subtext: { color: '#fff', - fontSize: 16, + fontSize: 20, fontWeight: '600', textAlign: 'center', marginVertical: '8%', marginHorizontal: '10%', + marginTop: '15%', }, finalAction: { backgroundColor: 'white', @@ -237,6 +314,31 @@ const styles = StyleSheet.create({ fontWeight: '500', color: 'black', }, + createCategory: { + backgroundColor: '#53329B', + width: SCREEN_WIDTH * 0.9, + height: 70, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 10, + flexDirection: 'row', + marginBottom: '5%', + }, + createCategoryLabel: { + color: 'white', + marginLeft: '3%', + fontSize: 18, + fontWeight: '500', + }, + plusIcon: { + color: 'white', + }, + divider: { + borderColor: 'white', + borderBottomWidth: 1, + width: SCREEN_WIDTH * 0.9, + marginVertical: '2%', + }, }); export default CategorySelection; diff --git a/src/screens/onboarding/CreateCustomCategory.tsx b/src/screens/onboarding/CreateCustomCategory.tsx new file mode 100644 index 00000000..eab72c7d --- /dev/null +++ b/src/screens/onboarding/CreateCustomCategory.tsx @@ -0,0 +1,123 @@ +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, +} from 'react-native'; +import {Background} from '../../components'; +import {OnboardingStackParams} from '../../routes'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +type CreateCustomCategoryRouteProps = RouteProp< + OnboardingStackParams, + 'CreateCustomCategory' +>; + +type CreateCustomCategoryNavigationProps = StackNavigationProp< + OnboardingStackParams, + 'CreateCustomCategory' +>; + +interface CreateCustomCategoryProps { + route: CreateCustomCategoryRouteProps; + navigation: CreateCustomCategoryNavigationProps; +} + +const CreateCustomCategory: React.FC<CreateCustomCategoryProps> = ({ + route, + navigation, +}) => { + /** + * Same component to be used for category selection while onboarding and while on profile + */ + const {existingCategories} = route.params; + const [newCategory, setNewCategory] = useState(''); + + const handleButtonPress = () => { + if (existingCategories.includes(newCategory)) { + Alert.alert('Looks like you already have that one created!'); + } else { + navigation.navigate('CategorySelection', { + screenType: route.params.screenType, + user: route.params.user, + newCustomCategory: newCategory, + }); + } + }; + + return ( + <> + <StatusBar barStyle="light-content" /> + <Background + style={styles.container} + gradientType={BackgroundGradientType.Dark}> + <KeyboardAvoidingView + style={styles.innerContainer} + behavior={'padding'}> + <Text style={styles.title}>Give your category a name</Text> + <TextInput + style={styles.input} + selectionColor={'white'} + onChangeText={setNewCategory} + autoFocus={true} + /> + <TouchableOpacity + onPress={handleButtonPress} + style={styles.finalAction}> + <Text style={styles.finalActionLabel}>{'Create'}</Text> + </TouchableOpacity> + </KeyboardAvoidingView> + </Background> + </> + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + minHeight: SCREEN_HEIGHT, + }, + innerContainer: { + height: '40%', + top: '20%', + justifyContent: 'space-around', + alignItems: 'center', + }, + title: { + color: 'white', + fontSize: 20, + fontWeight: '600', + }, + input: { + width: SCREEN_WIDTH * 0.75, + fontSize: 30, + color: 'white', + textAlign: 'center', + borderBottomWidth: 1, + borderBottomColor: 'white', + }, + finalAction: { + backgroundColor: 'white', + justifyContent: 'center', + alignItems: 'center', + width: 150, + height: 40, + borderRadius: 5, + borderWidth: 1, + borderColor: '#8F01FF', + }, + finalActionLabel: { + fontSize: 16, + fontWeight: '500', + color: 'black', + }, +}); + +export default CreateCustomCategory; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 3e59b00e..006b38db 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -17,15 +17,10 @@ import {OnboardingStackParams} from '../../routes/onboarding'; import {Background, TaggInput, SubmitButton} from '../../components'; import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; -import { - BackgroundGradientType, - CategorySelectionScreenType, - UserType, -} from '../../types'; +import {BackgroundGradientType, UserType} from '../../types'; import {useDispatch} from 'react-redux'; import {userLogin} from '../../utils'; import SplashScreen from 'react-native-splash-screen'; -import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 70550f36..1f8e58da 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -147,43 +147,51 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({ const goToGalleryLargePic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Header', mediaType: 'photo', - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - largePic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + largePic: picture.path, + }); + } + }); }; const goToGallerySmallPic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Profile Picture', mediaType: 'photo', cropperCircleOverlay: true, - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - smallPic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + smallPic: picture.path, + }); + } + }); }; /* diff --git a/src/screens/onboarding/SocialMedia.tsx b/src/screens/onboarding/SocialMedia.tsx index d2a43e7a..32beb4bc 100644 --- a/src/screens/onboarding/SocialMedia.tsx +++ b/src/screens/onboarding/SocialMedia.tsx @@ -2,7 +2,6 @@ import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import React from 'react'; import { - Alert, KeyboardAvoidingView, Platform, StatusBar, @@ -22,9 +21,8 @@ import { LinkSocialMedia, RegistrationWizard, } from '../../components'; -import {SOCIAL_LIST} from '../../constants/'; +import {SOCIAL_LIST, MOMENT_CATEGORIES} from '../../constants/'; import {OnboardingStackParams} from '../../routes'; -import {MOMENT_CATEGORIES_MAP} from '../../store/initialStates'; /** * Social Media Screen for displaying social media linkers @@ -55,8 +53,6 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { linkers.push(linker); } - const dispatch = useDispatch(); - /** * Just commenting this out, in case we need it in the future */ @@ -69,7 +65,6 @@ const SocialMedia: React.FC<SocialMediaProps> = ({route, navigation}) => { const handleNext = () => { navigation.navigate('CategorySelection', { - categories: MOMENT_CATEGORIES_MAP, screenType: CategorySelectionScreenType.Onboarding, user: {userId: userId, username: username}, }); diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index ec833929..20a8020d 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -11,3 +11,4 @@ export {default as PasswordResetRequest} from './PasswordResetRequest'; export {default as PasswordReset} from './PasswordReset'; export {default as WelcomeScreen} from './WelcomeScreen'; export {default as CategorySelection} from './CategorySelection'; +export {default as CreateCustomCategory} from './CreateCustomCategory'; diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 55e19a51..d86ae7cb 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -131,43 +131,51 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { const goToGalleryLargePic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Header', mediaType: 'photo', - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - largePic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + largePic: picture.path, + }); + } + }); }; const goToGallerySmallPic = () => { ImagePicker.openPicker({ - smartAlbums: ['Favorites', 'RecentlyAdded', 'SelfPortraits', 'Screenshots', 'UserLibrary'], + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], width: 580, height: 580, cropping: true, cropperToolbarTitle: 'Select Profile Picture', mediaType: 'photo', cropperCircleOverlay: true, - }) - .then((picture) => { - if ('path' in picture) { - setForm({ - ...form, - smallPic: picture.path, - }); - } - }) - .catch(() => {}); + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + smallPic: picture.path, + }); + } + }); }; /* diff --git a/src/services/MomentCategoryService.ts b/src/services/MomentCategoryService.ts index 8bdb70d2..32c721ae 100644 --- a/src/services/MomentCategoryService.ts +++ b/src/services/MomentCategoryService.ts @@ -1,12 +1,11 @@ import {Alert} from 'react-native'; -import {MomentCategoryType} from './../types/types'; import {MOMENT_CATEGORY_ENDPOINT} from '../constants'; export const loadMomentCategories: ( userId: string, token: string, -) => Promise<MomentCategoryType[]> = async (userId, token) => { - let categories: MomentCategoryType[] = []; +) => Promise<string[]> = async (userId, token) => { + let categories: string[] = []; try { const response = await fetch(MOMENT_CATEGORY_ENDPOINT + `${userId}/`, { method: 'GET', @@ -17,7 +16,7 @@ export const loadMomentCategories: ( const status = response.status; if (status === 200) { const data = await response.json(); - categories = data['categories']; + categories = data.categories; } else { console.log('Could not load categories!'); return []; @@ -30,7 +29,7 @@ export const loadMomentCategories: ( }; export const postMomentCategories: ( - categories: Array<MomentCategoryType>, + categories: string[], token: string, ) => Promise<boolean> = async (categories, token) => { let success = false; @@ -47,38 +46,8 @@ export const postMomentCategories: ( 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!'); + Alert.alert('There was a problem updating categories!'); + console.log('Unable to update categories'); } } catch (err) { console.log(err); diff --git a/src/store/actions/momentCategories.tsx b/src/store/actions/momentCategories.tsx index a522c3e0..987fc9e5 100644 --- a/src/store/actions/momentCategories.tsx +++ b/src/store/actions/momentCategories.tsx @@ -1,13 +1,8 @@ import {RootState} from '../rootReducer'; -import { - deleteMomentCategories, - loadMomentCategories, - postMomentCategories, -} from '../../services'; +import {loadMomentCategories, postMomentCategories} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; import {momentCategoriesFetched} from '../reducers'; import {getTokenOrLogout} from '../../utils'; -import {MomentCategoryType} from '../../types'; /** * Load all categories for user @@ -23,7 +18,7 @@ export const loadUserMomentCategories = ( const categories = await loadMomentCategories(userId, token); dispatch({ type: momentCategoriesFetched.type, - payload: {categories, add: true}, + payload: {categories}, }); } catch (error) { console.log(error); @@ -33,28 +28,20 @@ export const loadUserMomentCategories = ( /** * Handle addition / deletion of categories for a user * @param categories List of categories - * @param add boolean, if true, we add new categories, else we delete * @param userId id of the user for whom categories should be updated */ export const updateMomentCategories = ( - categories: Array<MomentCategoryType>, - add: boolean, - userId: string, + categories: 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); - } + const success = await postMomentCategories(categories, token); if (success) { dispatch({ type: momentCategoriesFetched.type, - payload: {categories, add}, + payload: {categories}, }); } } catch (error) { diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index da3ef3b0..09607758 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,5 +1,4 @@ import { - MomentCategoryType, MomentType, NotificationType, ProfilePreviewType, @@ -70,23 +69,7 @@ export const NO_BLOCKED_USERS = { blockedUsers: EMPTY_PROFILE_PREVIEW_LIST, }; -export const MOMENT_CATEGORIES_MAP: Record<MomentCategoryType, boolean> = { - Friends: false, - Adventure: false, - 'Photo Dump': false, - Food: false, - Music: false, - Art: false, - Sports: false, - Fashion: false, - Travel: false, - Pets: false, - Fitness: false, - DIY: false, - Nature: false, - 'Early Life': false, - Beauty: false, -}; +export const EMPTY_MOMENT_CATEGORIES: string[] = []; /** * The dummy userId and username serve the purpose of preventing app crash @@ -99,7 +82,7 @@ export const DUMMY_USERNAME = 'tagg_userX'; export const EMPTY_USER_X = <UserXType>{ friends: EMPTY_PROFILE_PREVIEW_LIST, moments: EMPTY_MOMENTS_LIST, - momentCategories: MOMENT_CATEGORIES_MAP, + momentCategories: EMPTY_MOMENT_CATEGORIES, socialAccounts: NO_SOCIAL_ACCOUNTS, user: NO_USER, profile: NO_PROFILE, @@ -124,5 +107,5 @@ export const EMPTY_SCREEN_TO_USERS_LIST: Record< }; export const INITIAL_CATEGORIES_STATE = { - momentCategories: MOMENT_CATEGORIES_MAP, + momentCategories: EMPTY_MOMENT_CATEGORIES, }; diff --git a/src/store/reducers/momentCategoryReducer.tsx b/src/store/reducers/momentCategoryReducer.tsx index d1f448f9..b6909b87 100644 --- a/src/store/reducers/momentCategoryReducer.tsx +++ b/src/store/reducers/momentCategoryReducer.tsx @@ -1,19 +1,16 @@ import {createSlice} from '@reduxjs/toolkit'; import {INITIAL_CATEGORIES_STATE} from '../initialStates'; -import {MomentCategoryType} from '../../types'; const momentCategoriesSlice = createSlice({ name: 'momentCategories', initialState: INITIAL_CATEGORIES_STATE, reducers: { /** - * One stop to add / delete / update categories for a user + * Replace a new copy of moment categories for a user */ momentCategoriesFetched: (state, action) => { - const categories: Array<MomentCategoryType> = action.payload.categories; - for (let category of categories) { - state.momentCategories[category] = action.payload.add; - } + const categories: string[] = action.payload.categories; + state.momentCategories = categories; }, }, }); diff --git a/src/store/reducers/userXReducer.ts b/src/store/reducers/userXReducer.ts index fa1598b2..3b00cf88 100644 --- a/src/store/reducers/userXReducer.ts +++ b/src/store/reducers/userXReducer.ts @@ -1,4 +1,4 @@ -import {MomentCategoryType, ScreenType} from '../../types/types'; +import {ScreenType} from '../../types/types'; import {EMPTY_SCREEN_TO_USERS_LIST, EMPTY_USER_X} from '../initialStates'; import {createSlice} from '@reduxjs/toolkit'; @@ -24,12 +24,10 @@ const userXSlice = createSlice({ }, userXMomentCategoriesFetched: (state, action) => { - const categories: Array<MomentCategoryType> = action.payload.data; - for (let category of categories) { - state[<ScreenType>action.payload.screenType][ - action.payload.userId - ].momentCategories[category] = true; - } + const categories: string[] = action.payload.data; + state[<ScreenType>action.payload.screenType][ + action.payload.userId + ].momentCategories = categories; }, userXMomentsFetched: (state, action) => { diff --git a/src/types/types.ts b/src/types/types.ts index b3148bc4..ee5103a2 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -113,7 +113,7 @@ export interface UserXType { friends: ProfilePreviewType[]; moments: MomentType[]; socialAccounts: Record<string, SocialAccountType>; - momentCategories: Record<MomentCategoryType, boolean>; + momentCategories: string[]; user: UserType; profile: ProfileType; avatar: string; @@ -129,26 +129,6 @@ export enum VerificationScreenType { } /** - * Default moment categories - */ -export type MomentCategoryType = - | 'Friends' - | 'Adventure' - | 'Photo Dump' - | 'Food' - | 'Music' - | 'Art' - | 'Sports' - | 'Fashion' - | 'Travel' - | 'Pets' - | 'Fitness' - | 'DIY' - | 'Nature' - | 'Early Life' - | 'Beauty'; - -/** * Two types for category selection screen */ export enum CategorySelectionScreenType { |