aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/assets/icons/plus_icon-01.svg2
-rw-r--r--src/assets/moment-categories/custom-icon.pngbin0 -> 4257 bytes
-rw-r--r--src/components/moments/Moment.tsx11
-rw-r--r--src/components/onboarding/MomentCategory.tsx58
-rw-r--r--src/components/profile/Content.tsx33
-rw-r--r--src/constants/constants.ts22
-rw-r--r--src/routes/main/MainStackNavigator.tsx8
-rw-r--r--src/routes/main/MainStackScreen.tsx12
-rw-r--r--src/routes/onboarding/OnboardingStackNavigator.tsx (renamed from src/routes/onboarding/OnboardingStack.tsx)8
-rw-r--r--src/routes/onboarding/OnboardingStackScreen.tsx (renamed from src/routes/onboarding/Onboarding.tsx)2
-rw-r--r--src/routes/onboarding/index.ts4
-rw-r--r--src/screens/onboarding/CategorySelection.tsx186
-rw-r--r--src/screens/onboarding/CreateCustomCategory.tsx123
-rw-r--r--src/screens/onboarding/Login.tsx7
-rw-r--r--src/screens/onboarding/ProfileOnboarding.tsx52
-rw-r--r--src/screens/onboarding/SocialMedia.tsx7
-rw-r--r--src/screens/onboarding/index.ts1
-rw-r--r--src/screens/profile/EditProfile.tsx52
-rw-r--r--src/services/MomentCategoryService.ts43
-rw-r--r--src/store/actions/momentCategories.tsx23
-rw-r--r--src/store/initialStates.ts23
-rw-r--r--src/store/reducers/momentCategoryReducer.tsx9
-rw-r--r--src/store/reducers/userXReducer.ts12
-rw-r--r--src/types/types.ts22
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
new file mode 100644
index 00000000..f81546d6
--- /dev/null
+++ b/src/assets/moment-categories/custom-icon.png
Binary files differ
diff --git a/src/components/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 {