diff options
Diffstat (limited to 'src')
25 files changed, 335 insertions, 189 deletions
diff --git a/src/assets/icons/plus-icon-thin.svg b/src/assets/icons/plus-icon-thin.svg new file mode 100644 index 00000000..1a582474 --- /dev/null +++ b/src/assets/icons/plus-icon-thin.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 216"><defs><style>.cls-1{fill:none;stroke-miterlimit:10;stroke-width:7.33px;}}</style></defs><circle class="cls-1" cx="108.05" cy="108.21" r="103.73" stroke="currentColor"/><rect class="cls-2" x="99.56" y="48.48" width="16.99" height="119.46" rx="7.75" fill="currentColor"/><rect class="cls-2" x="99.56" y="48.48" width="16.99" height="119.46" rx="7.75" transform="translate(-0.16 216.26) rotate(-90)" fill="currentColor"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/plus_icon-02.svg b/src/assets/icons/plus-icon-white.svg index 25527911..25527911 100644 --- a/src/assets/icons/plus_icon-02.svg +++ b/src/assets/icons/plus-icon-white.svg diff --git a/src/assets/icons/plus_icon-01.svg b/src/assets/icons/plus-icon.svg index 7a3b21d2..7a3b21d2 100644 --- a/src/assets/icons/plus_icon-01.svg +++ b/src/assets/icons/plus-icon.svg diff --git a/src/components/common/BadgeDetailView.tsx b/src/components/common/BadgeDetailView.tsx index 6504300c..19f1e74d 100644 --- a/src/components/common/BadgeDetailView.tsx +++ b/src/components/common/BadgeDetailView.tsx @@ -5,11 +5,15 @@ import {TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import {useDispatch, useSelector} from 'react-redux'; import CloseIcon from '../../assets/ionicons/close-outline.svg'; -import {BADGE_GRADIENT_FIRST} from '../../constants'; -import {BADGE_DATA} from '../../constants/badges'; +import {BADGE_GRADIENT_FIRST, BADGE_LIMIT} from '../../constants'; +import {removeUserBadge} from '../../store/actions'; import {RootState} from '../../store/rootreducer'; -import {ScreenType} from '../../types'; -import {getUniversityBadge, normalize, removeUserBadge} from '../../utils'; +import {ScreenType, UniversityBadgeDisplayType} from '../../types'; +import { + badgesToDisplayBadges, + getUniversityBadge, + normalize, +} from '../../utils'; interface BadgeDetailModalProps { userXId: string | undefined; @@ -34,35 +38,24 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({ userXId ? state.userX[screenType][userXId] : state.user, ); const navigation = useNavigation(); - const [selectedBadgesWithImage, setSelectedBadgesWithImage] = useState<any[]>( - [], - ); + const [displayBadges, setDisplayBadges] = useState< + UniversityBadgeDisplayType[] + >([]); + const atLimit = badges.length >= BADGE_LIMIT; useEffect(() => { - let badgesWithImage = []; - badges.forEach((e) => { - const uniData = BADGE_DATA[e.university]; - const categoryData = uniData.filter((u) => { - return u.title === e.category; - }); - const badgeData = categoryData[0].data.filter((c) => { - return c.badgeName === e.name; - }); - badgeData.forEach((c) => { - const obj = {...e, badgeImage: c.badgeImage}; - badgesWithImage.push(obj); - }); - }); - setTimeout(() => { - setSelectedBadgesWithImage(badgesWithImage); - }, 250); + setDisplayBadges(badgesToDisplayBadges(badges, university)); }, [badges]); const removeBadgeCell = async (badgeName: string) => { - await removeUserBadge(badges, badgeName, user.userId, dispatch); + dispatch(removeUserBadge(badgeName, user.userId)); }; - const badgeEditCell = ({item: {id, name, badgeImage}}) => { + const badgeEditCell = ({ + item: {id, name, img}, + }: { + item: UniversityBadgeDisplayType; + }) => { return ( <TouchableOpacity style={styles.badgeCellContainerStyles} @@ -71,7 +64,7 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({ navigation.navigate('MutualBadgeHolders', { badge_id: id, badge_title: name, - badge_img: badgeImage, + badge_img: img, }); }}> <View @@ -88,7 +81,7 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({ <Image resizeMode="cover" style={styles.badgeImageStyles} - source={badgeImage} + source={img} /> </LinearGradient> {isEditable && ( @@ -121,10 +114,20 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({ }; const modalHeader = () => { - const heading = isEditable ? 'Edit your badges!' : userFullName; - const subheading = isEditable - ? 'Add or delete your badges' - : 'View badges to discover groups!'; + let heading = ''; + let subheading = ''; + if (isEditable) { + if (atLimit) { + heading = 'You have reached your badge limit'; + subheading = 'Remove a badge if you wish to add more'; + } else { + heading = 'Edit your badges!'; + subheading = 'Add or delete your badges'; + } + } else { + heading = userFullName!; + subheading = 'View badges to discover groups!'; + } return ( <View> <Text style={styles.modalHeadingStyles}>{heading}</Text> @@ -155,7 +158,7 @@ const BadgeDetailView: React.FC<BadgeDetailModalProps> = ({ <FlatList contentContainerStyle={styles.modalListStyles} scrollEnabled={false} - data={selectedBadgesWithImage} + data={displayBadges} numColumns={3} renderItem={badgeEditCell} keyExtractor={(item) => item.id.toString()} @@ -228,6 +231,8 @@ const styles = StyleSheet.create({ fontSize: normalize(17), lineHeight: normalize(20.29), textAlign: 'center', + marginVertical: normalize(10), + marginTop: normalize(20), }, modalSubheadingStyles: { fontWeight: '600', diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 0ceb8542..cde5b2e0 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -7,8 +7,8 @@ import ImagePicker from 'react-native-image-crop-picker'; import LinearGradient from 'react-native-linear-gradient'; import DeleteIcon from '../../assets/icons/delete-logo.svg'; import DownIcon from '../../assets/icons/down_icon.svg'; -import PlusIcon from '../../assets/icons/plus_icon-01.svg'; -import BigPlusIcon from '../../assets/icons/plus_icon-02.svg'; +import PlusIcon from '../../assets/icons/plus-icon.svg'; +import BigPlusIcon from '../../assets/icons/plus-icon-white.svg'; import UpIcon from '../../assets/icons/up_icon.svg'; import {TAGG_LIGHT_BLUE} from '../../constants'; import {ERROR_UPLOAD} from '../../constants/strings'; diff --git a/src/components/profile/ProfileBadges.tsx b/src/components/profile/ProfileBadges.tsx new file mode 100644 index 00000000..8e68dc46 --- /dev/null +++ b/src/components/profile/ProfileBadges.tsx @@ -0,0 +1,148 @@ +import {useNavigation} from '@react-navigation/core'; +import React, {FC, useEffect, useState} from 'react'; +import {StyleSheet, Text, View} from 'react-native'; +import {ScrollView, TouchableOpacity} from 'react-native-gesture-handler'; +import {useSelector} from 'react-redux'; +import {BadgeIcon} from '..'; +import PlusIconImage from '../../assets/icons/plus-icon-thin.svg'; +import {BADGE_LIMIT} from '../../constants'; +import {RootState} from '../../store/rootReducer'; +import {ScreenType, UniversityBadgeDisplayType} from '../../types'; +import {badgesToDisplayBadges, normalize} from '../../utils'; +import BadgeDetailView from '../common/BadgeDetailView'; + +interface ProfileBadgesProps { + userXId: string | undefined; + screenType: ScreenType; +} + +const ProfileBadges: React.FC<ProfileBadgesProps> = ({userXId, screenType}) => { + const navigation = useNavigation(); + const {badges, name, university} = useSelector((state: RootState) => + userXId ? state.userX[screenType][userXId].profile : state.user.profile, + ); + const [displayBadges, setDisplayBadges] = useState< + UniversityBadgeDisplayType[] + >([]); + const [isEditBadgeModalVisible, setIsEditBadgeModalVisible] = useState(false); + const isOwnProfile = userXId === undefined; + + useEffect(() => { + setDisplayBadges(badgesToDisplayBadges(badges, university)); + }, [badges]); + + const PlusIcon: FC = () => ( + <TouchableOpacity + onPress={() => navigation.navigate('BadgeSelection', {editing: true})}> + <PlusIconImage style={styles.plus} /> + </TouchableOpacity> + ); + + const CloseIcon: FC = () => ( + <TouchableOpacity onPress={() => setIsEditBadgeModalVisible(true)}> + <PlusIconImage style={styles.close} /> + </TouchableOpacity> + ); + + return ( + <> + {/* Tutorial text */} + {displayBadges.length === 0 && isOwnProfile && ( + <> + <Text style={styles.title}>Badges</Text> + <Text style={styles.body}> + Proudly represent your team, club, or organization! + </Text> + </> + )} + {displayBadges.length === 0 && isOwnProfile && ( + // Grey circle placeholders + <ScrollView + contentContainerStyle={styles.badgeContainer} + scrollEnabled={false} + horizontal> + <PlusIcon /> + {Array(BADGE_LIMIT) + .fill(0) + .map(() => ( + <View style={[styles.grey, styles.circle]} /> + ))} + </ScrollView> + )} + {displayBadges.length !== 0 && ( + // Populating actual badges + <ScrollView + contentContainerStyle={styles.badgeContainer} + scrollEnabled={false} + horizontal> + {/* Actual badges */} + {displayBadges.map((displayBadge) => ( + <BadgeIcon key={displayBadge.id} badge={displayBadge} /> + ))} + {/* Plus icon */} + {displayBadges.length < BADGE_LIMIT && isOwnProfile && <PlusIcon />} + {/* Empty placeholders for space-between styling */} + {Array(BADGE_LIMIT + 1) + .fill(0) + .splice(displayBadges.length + 1, BADGE_LIMIT) + .map(() => ( + <View style={styles.circle} /> + ))} + {/* X button */} + {displayBadges.length === BADGE_LIMIT && isOwnProfile && ( + <CloseIcon /> + )} + </ScrollView> + )} + {isEditBadgeModalVisible && ( + <BadgeDetailView + userXId={userXId} + screenType={screenType} + isEditable={isOwnProfile} + userFullName={name} + setBadgeViewVisible={setIsEditBadgeModalVisible} + /> + )} + </> + ); +}; + +const styles = StyleSheet.create({ + title: { + fontWeight: '600', + fontSize: normalize(13.5), + lineHeight: normalize(18), + }, + body: { + fontSize: normalize(13.5), + lineHeight: normalize(17), + marginBottom: 10, + }, + badgeContainer: { + width: '100%', + justifyContent: 'space-between', + marginTop: 10, + marginBottom: 15, + }, + circle: { + width: normalize(31), + height: normalize(31), + borderRadius: normalize(31) / 2, + }, + grey: { + backgroundColor: '#c4c4c4', + }, + plus: { + width: normalize(31), + height: normalize(31), + color: 'black', + }, + close: { + width: normalize(31), + height: normalize(31), + color: 'grey', + transform: [{rotate: '45deg'}], + }, +}); + +export default ProfileBadges; diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx index 8743acfb..c0ee508a 100644 --- a/src/components/profile/ProfileBody.tsx +++ b/src/components/profile/ProfileBody.tsx @@ -20,6 +20,7 @@ import { import {canViewProfile} from '../../utils/users'; import {FriendsButton} from '../common'; import {MessageButton} from '../messages'; +import ProfileBadges from './ProfileBadges'; import ToggleButton from './ToggleButton'; interface ProfileBodyProps { @@ -65,6 +66,7 @@ const ProfileBody: React.FC<ProfileBodyProps> = ({ return ( <View onLayout={onLayout} style={styles.container}> + <ProfileBadges {...{userXId, screenType}} /> <Text style={styles.username}>{`@${username}`}</Text> {biography.length > 0 && ( <Text style={styles.biography}>{`${biography}`}</Text> @@ -137,7 +139,6 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', }, container: { - paddingVertical: '1%', paddingHorizontal: 18, backgroundColor: 'white', }, diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index c544c3f2..faf273d9 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -9,3 +9,4 @@ export {default as ProfileMoreInfoDrawer} from './ProfileMoreInfoDrawer'; export {default as MomentMoreInfoDrawer} from './MomentMoreInfoDrawer'; export {default as UniversityIcon} from './UniversityIcon'; export {default as TaggAvatar} from './TaggAvatar'; +export {default as ProfileBadges} from './ProfileBadges'; diff --git a/src/components/suggestedPeople/BadgeIcon.tsx b/src/components/suggestedPeople/BadgeIcon.tsx index 8f576a43..616bac93 100644 --- a/src/components/suggestedPeople/BadgeIcon.tsx +++ b/src/components/suggestedPeople/BadgeIcon.tsx @@ -1,24 +1,17 @@ import {useNavigation} from '@react-navigation/core'; import React from 'react'; -import { - Image, - ImageSourcePropType, - StyleProp, - StyleSheet, - ViewStyle, -} from 'react-native'; +import {Image, StyleProp, StyleSheet, ViewStyle} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; -import {UniversityBadge} from '../../types'; +import {UniversityBadgeDisplayType} from '../../types'; import {normalize} from '../../utils'; interface BadgeIconProps { - badge: UniversityBadge; - img: ImageSourcePropType; + badge: UniversityBadgeDisplayType; style?: StyleProp<ViewStyle>; } -const BadgeIcon: React.FC<BadgeIconProps> = ({badge, img, style}) => { +const BadgeIcon: React.FC<BadgeIconProps> = ({badge, style}) => { const navigation = useNavigation(); return ( <TouchableOpacity @@ -27,7 +20,7 @@ const BadgeIcon: React.FC<BadgeIconProps> = ({badge, img, style}) => { navigation.navigate('MutualBadgeHolders', { badge_id: badge.id, badge_title: badge.name, - badge_img: img, + badge_img: badge.img, }); }}> <LinearGradient @@ -36,7 +29,7 @@ const BadgeIcon: React.FC<BadgeIconProps> = ({badge, img, style}) => { angle={154.72} angleCenter={{x: 0.5, y: 0.5}} style={styles.badgeBackground}> - <Image source={img} style={styles.icon} /> + <Image source={badge.img} style={styles.icon} /> </LinearGradient> </TouchableOpacity> ); diff --git a/src/components/suggestedPeople/index.ts b/src/components/suggestedPeople/index.ts index 34bb96d4..ecdf4f35 100644 --- a/src/components/suggestedPeople/index.ts +++ b/src/components/suggestedPeople/index.ts @@ -1,3 +1,2 @@ export {default as MutualFriends} from './MutualFriends'; -export {default as SPTaggsBar} from './SPTaggsBar'; export {default as BadgeIcon} from './BadgeIcon'; diff --git a/src/components/suggestedPeople/legacy/BadgesDropdown.tsx b/src/components/suggestedPeople/legacy/BadgesDropdown.tsx index 267355f3..2c177e69 100644 --- a/src/components/suggestedPeople/legacy/BadgesDropdown.tsx +++ b/src/components/suggestedPeople/legacy/BadgesDropdown.tsx @@ -1,18 +1,15 @@ import React, {useEffect, useState} from 'react'; -import {ImageSourcePropType, StyleSheet} from 'react-native'; +import {StyleSheet} from 'react-native'; import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated, {Easing} from 'react-native-reanimated'; import {BadgeIcon, UniversityIcon} from '../..'; -import {UniversityBadge, UniversityType} from '../../../types'; +import {UniversityBadgeDisplayType, UniversityType} from '../../../types'; import {normalize} from '../../../utils'; import UniversityIconClicked from '../UniversityIconClicked'; interface BadgesDropdownProps { university: UniversityType; - localBadges: { - badge: UniversityBadge; - img: ImageSourcePropType; - }[]; + localBadges: UniversityBadgeDisplayType[]; } const BadgesDropdown: React.FC<BadgesDropdownProps> = ({ @@ -92,7 +89,7 @@ const BadgesDropdown: React.FC<BadgesDropdownProps> = ({ )} </TouchableOpacity> {localBadges && - localBadges.map(({badge, img}, index) => ( + localBadges.map((badge, index) => ( <Animated.View key={badge.id} style={[ @@ -102,7 +99,7 @@ const BadgesDropdown: React.FC<BadgesDropdownProps> = ({ zIndex: -1 * badge.id, }, ]}> - <BadgeIcon badge={badge} img={img} /> + <BadgeIcon badge={badge} /> </Animated.View> ))} </Animated.View> diff --git a/src/components/suggestedPeople/SPTaggsBar.tsx b/src/components/suggestedPeople/legacy/SPTaggsBar.tsx index 3ab33da1..3273f88d 100644 --- a/src/components/suggestedPeople/SPTaggsBar.tsx +++ b/src/components/suggestedPeople/legacy/SPTaggsBar.tsx @@ -2,12 +2,12 @@ import React, {useEffect, useState} from 'react'; import {StyleSheet} from 'react-native'; import Animated from 'react-native-reanimated'; import {useDispatch, useSelector} from 'react-redux'; -import {INTEGRATED_SOCIAL_LIST, SOCIAL_LIST} from '../../constants'; -import {getLinkedSocials} from '../../services'; -import {loadIndividualSocial, updateSocial} from '../../store/actions'; -import {RootState} from '../../store/rootReducer'; -import {ScreenType} from '../../types'; -import Tagg from '../taggs/Tagg'; +import {INTEGRATED_SOCIAL_LIST, SOCIAL_LIST} from '../../../constants'; +import {getLinkedSocials} from '../../../services'; +import {loadIndividualSocial, updateSocial} from '../../../store/actions'; +import {RootState} from '../../../store/rootReducer'; +import {ScreenType} from '../../../types'; +import Tagg from '../../taggs/Tagg'; const {View, ScrollView} = Animated; interface TaggsBarProps { @@ -66,7 +66,6 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} - whiteRing={true} screenType={screenType} />, ); @@ -84,7 +83,6 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ setSocialDataNeedUpdate={handleSocialUpdate} userXId={userXId} user={user} - whiteRing={true} screenType={screenType} />, ); diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 4a58bacb..0e0311c5 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -5,13 +5,8 @@ import {useSelector} from 'react-redux'; import PurpleRingPlus from '../../assets/icons/purple_ring+.svg'; import PurpleRing from '../../assets/icons/purple_ring.svg'; import RingPlus from '../../assets/icons/ring+.svg'; -import WhiteRing from '../../assets/icons/ring-white.svg'; import Ring from '../../assets/icons/ring.svg'; -import { - INTEGRATED_SOCIAL_LIST, - SOCIAL_ICON_SIZE_ADJUSTMENT, - TAGG_RING_DIM, -} from '../../constants'; +import {INTEGRATED_SOCIAL_LIST, TAGG_RING_DIM} from '../../constants'; import { ERROR_LINK, ERROR_UNABLE_TO_FIND_PROFILE, @@ -24,8 +19,8 @@ import { } from '../../services'; import {RootState} from '../../store/rootReducer'; import {ScreenType, UserType} from '../../types'; -import {canViewProfile, normalize} from '../../utils'; -import {SmallSocialIcon, SocialIcon, SocialLinkModal} from '../common'; +import {canViewProfile} from '../../utils'; +import {SocialIcon, SocialLinkModal} from '../common'; interface TaggProps { social: string; @@ -35,7 +30,6 @@ interface TaggProps { setSocialDataNeedUpdate: (social: string, username: string) => void; userXId: string | undefined; user: UserType; - whiteRing: boolean | undefined; screenType: ScreenType; } @@ -48,7 +42,6 @@ const Tagg: React.FC<TaggProps> = ({ userXId, screenType, user, - whiteRing, }) => { const navigation = useNavigation(); const state = useSelector((s: RootState) => s); @@ -107,9 +100,6 @@ const Tagg: React.FC<TaggProps> = ({ const pickTheRightRingHere = () => { if (youMayPass) { - if (whiteRing) { - return <WhiteRing width={TAGG_RING_DIM} height={TAGG_RING_DIM} />; - } if (social === 'Tagg') { return <Ring width={TAGG_RING_DIM} height={TAGG_RING_DIM} />; } else { @@ -139,7 +129,7 @@ const Tagg: React.FC<TaggProps> = ({ return ( <> - {(userXId && !isLinked) || (whiteRing && !userXId) ? ( + {userXId && !isLinked ? ( <Fragment /> ) : ( <> @@ -149,27 +139,13 @@ const Tagg: React.FC<TaggProps> = ({ setModalVisible={setModalVisible} completionCallback={linkNonIntegratedSocial} /> - <View style={whiteRing ? styles.spcontainer : styles.container}> + <View style={styles.container}> <TouchableOpacity style={styles.iconTap} onPress={modalOrAuthBrowserOrPass}> <SocialIcon style={styles.icon} social={social} whiteRing /> {pickTheRightRingHere()} </TouchableOpacity> - {!whiteRing && ( - <View style={styles.smallIconContainer}> - <SmallSocialIcon - style={[ - styles.smallIcon, - { - height: SOCIAL_ICON_SIZE_ADJUSTMENT[social], - width: SOCIAL_ICON_SIZE_ADJUSTMENT[social], - }, - ]} - social={social} - /> - </View> - )} </View> </> )} @@ -178,19 +154,10 @@ const Tagg: React.FC<TaggProps> = ({ }; const styles = StyleSheet.create({ - spcontainer: { - justifyContent: 'space-between', - alignItems: 'center', - marginRight: 15, - marginLeft: 19, - height: normalize(60), - }, container: { justifyContent: 'space-between', alignItems: 'center', - marginRight: 15, - marginLeft: 15, - height: normalize(90), + marginHorizontal: 15, }, iconTap: { justifyContent: 'center', @@ -202,17 +169,6 @@ const styles = StyleSheet.create({ borderRadius: 30, position: 'absolute', }, - smallIconContainer: { - height: 20, - width: 20, - position: 'absolute', - justifyContent: 'center', - alignItems: 'center', - bottom: 0, - }, - smallIcon: { - borderRadius: 1000, - }, }); export default Tagg; diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index a7e8fc7a..7076ffaf 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -82,7 +82,6 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1} setTaggsNeedUpdate={setTaggsNeedUpdate} setSocialDataNeedUpdate={handleSocialUpdate} - whiteRing={false} />, ); i++; @@ -100,7 +99,6 @@ const TaggsBar: React.FC<TaggsBarProps> = ({ userXId={userXId} screenType={screenType} user={user} - whiteRing={false} />, ); i++; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index e6c23554..f4ffd750 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -23,6 +23,7 @@ export const TAGG_RING_DIM = normalize(60); // default height of the navigation bar, from react native library, unless on ipad export const NAV_BAR_HEIGHT = 49; +export const BADGE_LIMIT = 5; export const INTEGRATED_SOCIAL_LIST: string[] = [ 'Instagram', diff --git a/src/constants/regex.ts b/src/constants/regex.ts index 61523203..f934185d 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -36,7 +36,7 @@ export const nameRegex: RegExp = /^[A-Za-z'\-,. ]{2,20}$/; * - match alphanumerics, and special characters used in URLs */ export const websiteRegex: RegExp = - /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]{0,35})$/; + /^$|^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,50}\.[a-zA-Z0-9()]{2,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]{0,35})$/; /** * The website regex has the following constraints diff --git a/src/screens/profile/CategorySelection.tsx b/src/screens/profile/CategorySelection.tsx index c02eef0d..ea443fce 100644 --- a/src/screens/profile/CategorySelection.tsx +++ b/src/screens/profile/CategorySelection.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import {useDispatch, useSelector} from 'react-redux'; -import PlusIcon from '../../assets/icons/plus_icon-01.svg'; +import PlusIcon from '../../assets/icons/plus-icon.svg'; import {Background, MomentCategory} from '../../components'; import {MOMENT_CATEGORIES, TAGG_LIGHT_BLUE_2} from '../../constants'; import {ERROR_SOMETHING_WENT_WRONG} from '../../constants/strings'; diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index 26802e45..765bbf01 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -305,14 +305,13 @@ const EditProfile: React.FC<EditProfileProps> = ({route, navigation}) => { type: 'image/jpg', }); } - if (form.website) { - if (form.isValidWebsite) { - request.append('website', form.website); - } else { - setForm({...form, attemptedSubmit: false}); - setTimeout(() => setForm({...form, attemptedSubmit: true})); - invalidFields = true; - } + + if (form.isValidWebsite) { + request.append('website', form.website); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; } if (form.bio) { diff --git a/src/screens/suggestedPeople/SPBody.tsx b/src/screens/suggestedPeople/SPBody.tsx index eb80da49..fea67950 100644 --- a/src/screens/suggestedPeople/SPBody.tsx +++ b/src/screens/suggestedPeople/SPBody.tsx @@ -1,19 +1,24 @@ import {useNavigation} from '@react-navigation/native'; import React, {Fragment, useEffect, useMemo, useState} from 'react'; -import {ImageSourcePropType, StyleSheet, Text, View} from 'react-native'; +import {StyleSheet, Text, View} from 'react-native'; import {Image} from 'react-native-animatable'; import {TouchableOpacity} from 'react-native-gesture-handler'; import RequestedButton from '../../assets/ionicons/requested-button.svg'; import {UniversityIcon} from '../../components'; import {BadgeIcon, MutualFriends} from '../../components/suggestedPeople'; -import {BADGE_DATA} from '../../constants/badges'; import { ProfilePreviewType, ScreenType, SuggestedPeopleDataType, - UniversityBadge, + UniversityBadgeDisplayType, } from '../../types'; -import {isIPhoneX, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import { + badgesToDisplayBadges, + isIPhoneX, + normalize, + SCREEN_HEIGHT, + SCREEN_WIDTH, +} from '../../utils'; interface SPBodyProps { item: SuggestedPeopleDataType; @@ -39,30 +44,12 @@ const SPBody: React.FC<SPBodyProps> = ({ }) => { const firstItem = itemIndex === 0; const screenType = ScreenType.SuggestedPeople; - const [localBadges, setLocalBadges] = useState< - { - badge: UniversityBadge; - img: ImageSourcePropType; - }[] + const [displayBadges, setDisplayBadges] = useState< + UniversityBadgeDisplayType[] >([]); const navigation = useNavigation(); useEffect(() => { - const newBadges: {badge: UniversityBadge; img: any}[] = []; - const findBadgeIcons = (badge: UniversityBadge) => { - BADGE_DATA[university]?.forEach((item) => { - if (item.title === badge.category) { - item.data.forEach((object) => { - if (object.badgeName === badge.name) { - newBadges.push({badge, img: object.badgeImage}); - } - }); - } - }); - setLocalBadges(newBadges); - }; - badges - ? badges.forEach((badge) => findBadgeIcons(badge)) - : console.log('NO BADGES FOUND'); + setDisplayBadges(badgesToDisplayBadges(badges, university)); }, []); const FriendButton = () => { @@ -131,12 +118,15 @@ const SPBody: React.FC<SPBodyProps> = ({ const Badges = () => ( // Badges aligned left and spaced as if there are 5 items <View style={styles.badgeContainer}> - {localBadges.map(({badge, img}, index) => ( - <BadgeIcon key={index} badge={badge} img={img} style={styles.badge} /> - ))} - {[0, 0, 0, 0, 0].splice(localBadges.length, 5).map((_, index) => ( - <View key={index} style={styles.badge} /> + {displayBadges.map((displayBadge, index) => ( + <BadgeIcon key={index} badge={displayBadge} style={styles.badge} /> ))} + {Array(5) + .fill(0) + .splice(displayBadges.length, 5) + .map((_, index) => ( + <View key={index} style={styles.badge} /> + ))} </View> ); @@ -159,7 +149,7 @@ const SPBody: React.FC<SPBodyProps> = ({ {user.id !== loggedInUserId && <FriendButton />} </View> </View> - {localBadges.length !== 0 && <Badges />} + {displayBadges.length !== 0 && <Badges />} <View style={styles.marginManager}> <MutualFriends user={user} friends={mutual_friends} /> </View> diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 941101df..b1cb8719 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -4,12 +4,14 @@ import {Action, ThunkAction} from '@reduxjs/toolkit'; import { getProfilePic, loadProfileInfo, + removeBadgesService, sendSuggestedPeopleLinked, } from '../../services'; import {UniversityBadge, UserType} from '../../types/types'; import {getTokenOrLogout} from '../../utils'; import { clearHeaderAndProfileImages, + profileBadgeRemoved, profileBadgesUpdated, profileCompletionStageUpdated, setIsOnboardedUser, @@ -107,6 +109,27 @@ export const updateUserBadges = } }; +/** + * Removes a single badge from logged-in user by badge name. + * @param badgeName name of badge to be removed + * @param loggedInUserId userId of loggedInUser + */ +export const removeUserBadge = + ( + badgeName: string, + loggedInUserId: string, + ): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => + async (dispatch) => { + try { + const success = await removeBadgesService([badgeName], loggedInUserId); + if (success) { + dispatch({type: profileBadgeRemoved.type, payload: {badge: badgeName}}); + } + } catch (error) { + console.log(error); + } + }; + export const updateProfileCompletionStage = ( stage: number, diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index e0f9d776..e2902a2d 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -1,4 +1,4 @@ -import {CommentThreadType} from './../types/types'; +import {CommentThreadType, UniversityType} from './../types/types'; import { MomentType, NotificationType, @@ -17,7 +17,7 @@ export const NO_PROFILE: ProfileInfoType = { gender: '', birthday: undefined, university_class: 2021, - university: undefined, + university: UniversityType.Empty, badges: [], //Default to an invalid value and ignore it gracefully while showing tutorials / popups. profile_completion_stage: -1, diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index 97bf845c..4692c5d3 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -46,6 +46,12 @@ const userDataSlice = createSlice({ state.profile.badges = action.payload.badges; }, + profileBadgeRemoved: (state, action) => { + state.profile.badges = state.profile.badges.filter( + (badge) => badge.name !== action.payload.badge, + ); + }, + profileCompletionStageUpdated: (state, action) => { state.profile.profile_completion_stage = action.payload.stage; }, @@ -95,6 +101,6 @@ export const { setSuggestedPeopleImage, clearHeaderAndProfileImages, profileBadgesUpdated, - // setChatClientReady, + profileBadgeRemoved, } = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/types/types.ts b/src/types/types.ts index b294e3f1..e54c2201 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,4 @@ +import {ImageSourcePropType} from 'react-native'; import Animated from 'react-native-reanimated'; import {Channel as ChannelType, StreamChat} from 'stream-chat'; @@ -262,6 +263,10 @@ export type UniversityBadge = { category: string; }; +export interface UniversityBadgeDisplayType extends UniversityBadge { + img: ImageSourcePropType; +} + export type SuggestedPeopleDataType = { user: ProfilePreviewType; university: UniversityType; @@ -291,7 +296,16 @@ export type ContactType = { }; export type UniversityBadgeType = 'Search' | 'Crest'; -export type BadgeDataType = Record<UniversityType, any[]>; +export type BadgeDataType = Record< + UniversityType, + { + title: string; + data: { + badgeName: string; + badgeImage: ImageSourcePropType; + }[]; + }[] +>; // Stream Chat Types export type LocalAttachmentType = Record<string, unknown>; diff --git a/src/utils/common.ts b/src/utils/common.ts index cb0bd62d..645f229a 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,11 +1,17 @@ import AsyncStorage from '@react-native-community/async-storage'; import moment from 'moment'; -import {Linking} from 'react-native'; +import {ImageSourcePropType, Linking} from 'react-native'; import {getAll} from 'react-native-contacts'; -import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants'; +import { + BADGE_DATA, + BROWSABLE_SOCIAL_URLS, + TOGGLE_BUTTON_TYPE, +} from '../constants'; import { ContactType, NotificationType, + UniversityBadge, + UniversityBadgeDisplayType, UniversityBadgeType, UniversityType, } from './../types/types'; @@ -198,7 +204,37 @@ export const validateImageLink = async (url: string | undefined) => { }); }; +/** + * Turns a list of badges into display badges (just a badge with img) by + * looking up the img source from our badge asset lookup constant. + * + * WARNING: Assumes a small list of badges, complexity goes up exponentially. + * + * @param badges list of university badges + * @param university university of which all the badges belong + * @returns list of display badges + */ +export const badgesToDisplayBadges = ( + badges: UniversityBadge[], + university: UniversityType, +) => { + const badgeSet: Set<string> = new Set(badges.map((b) => b.category + b.name)); + const badgeToImgMap: Record<string, ImageSourcePropType> = {}; + BADGE_DATA[university].forEach((category) => { + category.data.forEach((badgeInfo) => { + const key = category.title + badgeInfo.badgeName; + if (badgeSet.has(key)) { + badgeToImgMap[key] = badgeInfo.badgeImage; + } + }); + }); + return <UniversityBadgeDisplayType[]>badges.map((b) => ({ + ...b, + img: badgeToImgMap[b.category + b.name], + })); +}; + // Documentation: https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript export const numberWithCommas = (digits: number) => { return digits.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -}; +
\ No newline at end of file diff --git a/src/utils/users.ts b/src/utils/users.ts index 8505cde2..64ad10e9 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -1,7 +1,8 @@ -import {Alert} from 'react-native'; import AsyncStorage from '@react-native-community/async-storage'; +import {Alert} from 'react-native'; +import ImagePicker from 'react-native-image-crop-picker'; import {INTEGRATED_SOCIAL_LIST} from '../constants'; -import {isUserBlocked, loadSocialPosts, removeBadgesService} from '../services'; +import {isUserBlocked, loadSocialPosts, patchEditProfile} from '../services'; import { loadAllSocials, loadBlockedList, @@ -11,7 +12,6 @@ import { loadUserMoments, loadUserNotifications, logout, - updateUserBadges, } from '../store/actions'; import {NO_SOCIAL_ACCOUNTS} from '../store/initialStates'; import {loadUserMomentCategories} from './../store/actions/momentCategories'; @@ -23,10 +23,7 @@ import { ProfilePreviewType, ScreenType, UserType, - UniversityBadge, } from './../types/types'; -import ImagePicker from 'react-native-image-crop-picker'; -import {patchEditProfile} from '../services'; const loadData = async (dispatch: AppDispatch, user: UserType) => { await Promise.all([ @@ -205,23 +202,6 @@ export const canViewProfile = ( return false; }; -/* Function to call remove badge service, - * remove selected badge from list passed in and - * dispatch thunk action to update store - */ -export const removeUserBadge = async ( - badges: UniversityBadge[], - badgeName: string, - userId: string, - dispatch: AppDispatch, -) => { - const success = await removeBadgesService([badgeName], userId); - if (success === true) { - badges = badges.filter((badge) => badge.name !== badgeName); - dispatch(updateUserBadges(badges)); - } -}; - export const navigateToProfile = async ( state: RootState, dispatch: any, |