diff options
61 files changed, 1049 insertions, 249 deletions
diff --git a/.watchmanconfig b/.watchmanconfig deleted file mode 100644 index 9e26dfee..00000000 --- a/.watchmanconfig +++ /dev/null @@ -1 +0,0 @@ -{}
\ No newline at end of file diff --git a/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend-tvOS.xcscheme b/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend-tvOS.xcscheme index 3fe395bc..a11b4188 100644 --- a/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend-tvOS.xcscheme +++ b/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend-tvOS.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1130" + LastUpgradeVersion = "1160" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend.xcscheme b/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend.xcscheme index 5c33f141..c3b9ba97 100644 --- a/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend.xcscheme +++ b/ios/Frontend.xcodeproj/xcshareddata/xcschemes/Frontend.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1130" + LastUpgradeVersion = "1160" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/ios/Podfile b/ios/Podfile index 6f9beedf..23f0e008 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -101,4 +101,4 @@ target 'Frontend-tvOS' do inherit! :search_paths # Pods for testing end -end
\ No newline at end of file +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 061a9b9d..2e594aa9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -296,6 +296,8 @@ PODS: - React-cxxreact (= 0.62.2) - React-jsi (= 0.62.2) - ReactCommon/callinvoker (= 0.62.2) + - rn-fetch-blob (0.12.0): + - React-Core - RNCMaskedView (0.1.10): - React - RNGestureHandler (1.6.1): @@ -313,6 +315,8 @@ PODS: - React - RNScreens (2.9.0): - React + - RNSVG (12.1.0): + - React - TOCropViewController (2.5.3) - Yoga (1.14.0) - YogaKit (1.18.1): @@ -367,11 +371,13 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - rn-fetch-blob (from `../node_modules/rn-fetch-blob`) - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -443,6 +449,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/Vibration" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + rn-fetch-blob: + :path: "../node_modules/rn-fetch-blob" RNCMaskedView: :path: "../node_modules/@react-native-community/masked-view" RNGestureHandler: @@ -453,6 +461,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -494,15 +504,17 @@ SPEC CHECKSUMS: React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 + rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38 RNImageCropPicker: f0557a908758c4a3f83978894ec7227651529b45 RNReanimated: b5ccb50650ba06f6e749c7c329a1bc3ae0c88b43 RNScreens: c526239bbe0e957b988dacc8d75ac94ec9cb19da + RNSVG: ce9d996113475209013317e48b05c21ee988d42e TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6 Yoga: 3ebccbdd559724312790e7742142d062476b698e YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 7030158ba440c1ffa1ab55bb64e73d7d8c48256b +PODFILE CHECKSUM: f5b485075b23881307c1dba4f8874d96683d3678 COCOAPODS: 1.9.3 diff --git a/package.json b/package.json index d881c6f1..30456a10 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "react-native-reanimated": "^1.9.0", "react-native-safe-area-context": "^3.0.6", "react-native-screens": "^2.9.0", - "react-promise-tracker": "^2.1.0" + "react-native-svg": "^12.1.0", + "react-promise-tracker": "^2.1.0", + "rn-fetch-blob": "^0.12.0" }, "devDependencies": { "@babel/core": "^7.6.2", diff --git a/src/App.tsx b/src/App.tsx index 5abf1ff4..2e6865fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {NavigationContainer} from '@react-navigation/native'; -import {Routes, AuthContextProvider} from './routes'; +import Routes, {AuthProvider} from './routes'; const App = () => { return ( - <AuthContextProvider> + <AuthProvider> <NavigationContainer> <Routes /> </NavigationContainer> - </AuthContextProvider> + </AuthProvider> ); }; diff --git a/src/assets/images/avatar-placeholder.png b/src/assets/images/avatar-placeholder.png Binary files differnew file mode 100644 index 00000000..313f384e --- /dev/null +++ b/src/assets/images/avatar-placeholder.png diff --git a/src/assets/images/avatar-placeholder@2x.png b/src/assets/images/avatar-placeholder@2x.png Binary files differnew file mode 100644 index 00000000..d038441e --- /dev/null +++ b/src/assets/images/avatar-placeholder@2x.png diff --git a/src/assets/images/avatar-placeholder@3x.png b/src/assets/images/avatar-placeholder@3x.png Binary files differnew file mode 100644 index 00000000..814472ec --- /dev/null +++ b/src/assets/images/avatar-placeholder@3x.png diff --git a/src/assets/images/cover-placeholder.png b/src/assets/images/cover-placeholder.png Binary files differnew file mode 100644 index 00000000..e27eb9bd --- /dev/null +++ b/src/assets/images/cover-placeholder.png diff --git a/src/assets/images/cover-placeholder@2x.png b/src/assets/images/cover-placeholder@2x.png Binary files differnew file mode 100644 index 00000000..4aafe9d4 --- /dev/null +++ b/src/assets/images/cover-placeholder@2x.png diff --git a/src/assets/images/cover-placeholder@3x.png b/src/assets/images/cover-placeholder@3x.png Binary files differnew file mode 100644 index 00000000..991f5d63 --- /dev/null +++ b/src/assets/images/cover-placeholder@3x.png diff --git a/src/components/common/GradientBackground.tsx b/src/components/common/GradientBackground.tsx index f363bd61..c1247ca2 100644 --- a/src/components/common/GradientBackground.tsx +++ b/src/components/common/GradientBackground.tsx @@ -5,20 +5,19 @@ import { TouchableWithoutFeedback, Keyboard, ViewProps, - SafeAreaView, } from 'react-native'; interface GradientBackgroundProps extends ViewProps {} const GradientBackground: React.FC<GradientBackgroundProps> = (props) => { return ( - <LinearGradient - locations={[0.89, 1]} - colors={['transparent', 'rgba(0, 0, 0, 0.6)']} - style={styles.container}> - <TouchableWithoutFeedback accessible={false} onPress={Keyboard.dismiss}> - <SafeAreaView {...props}>{props.children}</SafeAreaView> - </TouchableWithoutFeedback> - </LinearGradient> + <TouchableWithoutFeedback accessible={false} onPress={Keyboard.dismiss}> + <LinearGradient + locations={[0.89, 1]} + colors={['transparent', 'rgba(0, 0, 0, 0.6)']} + style={styles.container}> + {props.children} + </LinearGradient> + </TouchableWithoutFeedback> ); }; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index a1bcc558..826675ff 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -4,3 +4,4 @@ export {default as RadioCheckbox} from './RadioCheckbox'; export {default as TaggInput} from './TaggInput'; export {default as NavigationIcon} from './NavigationIcon'; export {default as GradientBackground} from './GradientBackground'; +export {default as Post} from './post'; diff --git a/src/components/common/post/Post.tsx b/src/components/common/post/Post.tsx new file mode 100644 index 00000000..d6c5a7d6 --- /dev/null +++ b/src/components/common/post/Post.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {StyleSheet, View} from 'react-native'; +import {PostType} from '../../../types'; +import PostHeader from './PostHeader'; +import {SCREEN_WIDTH} from '../../../utils'; + +interface PostProps { + post: PostType; +} +const Post: React.FC<PostProps> = ({post: {owner}}) => { + return ( + <> + <PostHeader owner={owner} /> + <View style={styles.image} /> + </> + ); +}; + +const styles = StyleSheet.create({ + image: { + width: SCREEN_WIDTH, + height: SCREEN_WIDTH, + backgroundColor: '#eee', + }, +}); +export default Post; diff --git a/src/components/common/post/PostHeader.tsx b/src/components/common/post/PostHeader.tsx new file mode 100644 index 00000000..8558d21d --- /dev/null +++ b/src/components/common/post/PostHeader.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import {UserType} from '../../../types'; +import {View, StyleSheet, Image, Text} from 'react-native'; +import {AuthContext} from '../../../routes/authentication'; + +const AVATAR_DIM = 35; +interface PostHeaderProps { + owner: UserType; +} +const PostHeader: React.FC<PostHeaderProps> = ({owner: {username}}) => { + const {avatar} = React.useContext(AuthContext); + return ( + <View style={styles.container}> + <View style={styles.leftElem}> + <Image + style={styles.avatar} + source={ + avatar + ? {uri: avatar} + : require('../../../assets/images/avatar-placeholder.png') + } + /> + <Text style={styles.username}>{username}</Text> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + padding: 10, + backgroundColor: 'white', + }, + leftElem: { + flexDirection: 'row', + alignItems: 'center', + }, + avatar: { + width: AVATAR_DIM, + height: AVATAR_DIM, + borderRadius: AVATAR_DIM / 2, + marginRight: 10, + }, + username: { + fontSize: 18, + }, +}); + +export default PostHeader; diff --git a/src/components/common/post/index.ts b/src/components/common/post/index.ts new file mode 100644 index 00000000..033f8a8d --- /dev/null +++ b/src/components/common/post/index.ts @@ -0,0 +1 @@ +export {default} from './Post'; diff --git a/src/components/index.ts b/src/components/index.ts index 724b14ac..48b7df05 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,3 @@ export * from './common'; export * from './onboarding'; +export * from './profile'; diff --git a/src/components/profile/Avatar.tsx b/src/components/profile/Avatar.tsx new file mode 100644 index 00000000..a0f7596c --- /dev/null +++ b/src/components/profile/Avatar.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {Image, StyleSheet} from 'react-native'; +import {AuthContext} from '../../routes/authentication'; + +const PROFILE_DIM = 100; +interface AvatarProps { + style: object; +} +const Avatar: React.FC<AvatarProps> = ({style}) => { + const {avatar} = React.useContext(AuthContext); + return ( + <Image + style={[styles.image, style]} + source={ + avatar + ? {uri: avatar} + : require('../../assets/images/avatar-placeholder.png') + } + /> + ); +}; + +const styles = StyleSheet.create({ + image: { + height: PROFILE_DIM, + width: PROFILE_DIM, + borderRadius: PROFILE_DIM / 2, + }, +}); + +export default Avatar; diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx new file mode 100644 index 00000000..82b5fdc0 --- /dev/null +++ b/src/components/profile/Content.tsx @@ -0,0 +1,58 @@ +import React, {useState} from 'react'; +import {StyleSheet, LayoutChangeEvent} from 'react-native'; +import Animated from 'react-native-reanimated'; +const {ScrollView} = Animated; + +import {UserType} from '../../types'; +import ProfileCutout from './ProfileCutout'; +import ProfileHeader from './ProfileHeader'; +import ProfileBody from './ProfileBody'; +import MomentsBar from './MomentsBar'; +import Feed from './Feed'; +import LinearGradient from 'react-native-linear-gradient'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +interface ContentProps { + y: Animated.Value<number>; + user: UserType; +} +const Content: React.FC<ContentProps> = ({y, user}) => { + const [profileBodyHeight, setProfileBodyHeight] = useState(0); + const onLayout = (e: LayoutChangeEvent) => { + const {height} = e.nativeEvent.layout; + setProfileBodyHeight(height); + }; + return ( + <ScrollView + style={styles.container} + onScroll={(e) => y.setValue(e.nativeEvent.contentOffset.y)} + showsVerticalScrollIndicator={false} + scrollEventThrottle={1} + stickyHeaderIndices={[2, 4]}> + <ProfileCutout> + <ProfileHeader /> + </ProfileCutout> + <ProfileBody {...{onLayout}} /> + <MomentsBar {...{y, profileBodyHeight}} /> + <Feed {...{user}} /> + <LinearGradient + locations={[0.89, 1]} + colors={['transparent', 'rgba(0, 0, 0, 0.6)']} + style={styles.gradient} + /> + </ScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + gradient: { + height: SCREEN_HEIGHT, + width: SCREEN_WIDTH, + position: 'absolute', + }, +}); + +export default Content; diff --git a/src/components/profile/Cover.tsx b/src/components/profile/Cover.tsx new file mode 100644 index 00000000..01199f06 --- /dev/null +++ b/src/components/profile/Cover.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {Image, StyleSheet} from 'react-native'; +import Animated from 'react-native-reanimated'; +import {IMAGE_WIDTH, COVER_HEIGHT} from '../../constants'; +import {AuthContext} from '../../routes/authentication'; + +const {interpolate, Extrapolate} = Animated; +interface CoverProps { + y: Animated.Value<number>; +} +const Cover: React.FC<CoverProps> = ({y}) => { + const {cover} = React.useContext(AuthContext); + const scale: Animated.Node<number> = interpolate(y, { + inputRange: [-COVER_HEIGHT, 0], + outputRange: [1.5, 1.25], + extrapolateRight: Extrapolate.CLAMP, + }); + return ( + <Animated.View style={[styles.container, {transform: [{scale}]}]}> + <Image + style={styles.image} + source={ + cover + ? {uri: cover} + : require('../../assets/images/cover-placeholder.png') + } + /> + </Animated.View> + ); +}; + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + }, + image: { + width: IMAGE_WIDTH, + height: COVER_HEIGHT, + }, +}); +export default Cover; diff --git a/src/components/profile/Feed.tsx b/src/components/profile/Feed.tsx new file mode 100644 index 00000000..6780f8c5 --- /dev/null +++ b/src/components/profile/Feed.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {PostType, UserType} from '../../types'; +import {Post} from '../common'; + +interface FeedProps { + user: UserType; +} +const Feed: React.FC<FeedProps> = ({user}) => { + const posts: Array<PostType> = []; + const dummyPost: PostType = { + owner: user, + }; + for (let i = 0; i < 20; i++) { + posts.push(dummyPost); + } + return ( + <> + {posts.map((post, index) => ( + <Post key={index} post={post} /> + ))} + </> + ); +}; + +export default Feed; diff --git a/src/components/profile/FollowCount.tsx b/src/components/profile/FollowCount.tsx new file mode 100644 index 00000000..72817e7a --- /dev/null +++ b/src/components/profile/FollowCount.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import {View, Text, StyleSheet, ViewProps} from 'react-native'; + +interface FollowCountProps extends ViewProps { + mode: 'followers' | 'following'; + count: number; +} + +const FollowCount: React.FC<FollowCountProps> = ({style, mode, count}) => { + const displayed: string = + count < 5e3 + ? `${count}` + : count < 1e5 + ? `${(count / 1e3).toFixed(1)}k` + : count < 1e6 + ? `${(count / 1e3).toFixed(0)}k` + : `${count / 1e6}m`; + return ( + <View style={[styles.container, style]}> + <Text style={styles.count}>{displayed}</Text> + <Text style={styles.label}> + {mode === 'followers' ? 'Followers' : 'Following'} + </Text> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + }, + count: { + fontWeight: '700', + fontSize: 18, + }, + label: { + fontWeight: '400', + fontSize: 16, + }, +}); + +export default FollowCount; diff --git a/src/components/profile/Moment.tsx b/src/components/profile/Moment.tsx new file mode 100644 index 00000000..eaf43fea --- /dev/null +++ b/src/components/profile/Moment.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import {View, StyleSheet, ViewProps} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +interface MomentProps extends ViewProps {} +const Moment: React.FC<MomentProps> = ({style}) => { + return ( + <LinearGradient + colors={['#9F00FF', '#27EAE9']} + useAngle={true} + angle={154.72} + angleCenter={{x: 0.5, y: 0.5}} + style={[styles.gradient, style]}> + <View style={styles.image} /> + </LinearGradient> + ); +}; + +const styles = StyleSheet.create({ + gradient: { + width: 80, + height: 80, + borderRadius: 40, + justifyContent: 'center', + alignItems: 'center', + }, + image: { + width: 72, + height: 72, + borderRadius: 37.5, + backgroundColor: 'pink', + }, +}); + +export default Moment; diff --git a/src/components/profile/MomentsBar.tsx b/src/components/profile/MomentsBar.tsx new file mode 100644 index 00000000..dcc88d89 --- /dev/null +++ b/src/components/profile/MomentsBar.tsx @@ -0,0 +1,75 @@ +// @refresh react +import React from 'react'; +import {StyleSheet} from 'react-native'; +import Animated from 'react-native-reanimated'; +import Moment from './Moment'; +import {PROFILE_CUTOUT_BOTTOM_Y} from '../../constants'; +import {StatusBarHeight} from '../../utils'; + +const {View, ScrollView, interpolate, Extrapolate} = Animated; +interface MomentsBarProps { + y: Animated.Value<number>; + profileBodyHeight: number; +} +const MomentsBar: React.FC<MomentsBarProps> = ({y, profileBodyHeight}) => { + const moments: Array<JSX.Element> = []; + for (let i = 0; i < 10; i++) { + moments.push(<Moment key={i} style={styles.moment} />); + } + const shadowOpacity: Animated.Node<number> = interpolate(y, { + inputRange: [ + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight + 20, + ], + outputRange: [0, 0.2], + extrapolate: Extrapolate.CLAMP, + }); + const paddingTop: Animated.Node<number> = interpolate(y, { + inputRange: [ + 0, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight - 30, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, + ], + outputRange: [20, 20, StatusBarHeight], + extrapolate: Extrapolate.CLAMP, + }); + const paddingBottom: Animated.Node<number> = interpolate(y, { + inputRange: [ + 0, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight - 30, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, + ], + outputRange: [30, 30, 15], + extrapolate: Extrapolate.CLAMP, + }); + return ( + <View style={[styles.container, {shadowOpacity}]}> + <ScrollView + horizontal + showsHorizontalScrollIndicator={false} + style={{paddingTop, paddingBottom}} + contentContainerStyle={styles.contentContainer}> + {moments} + </ScrollView> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + shadowColor: '#000', + shadowRadius: 10, + shadowOffset: {width: 0, height: 2}, + zIndex: 1, + }, + contentContainer: { + alignItems: 'center', + paddingHorizontal: 15, + }, + moment: { + marginHorizontal: 14, + }, +}); + +export default MomentsBar; diff --git a/src/components/profile/ProfileBody.tsx b/src/components/profile/ProfileBody.tsx new file mode 100644 index 00000000..e8d8de62 --- /dev/null +++ b/src/components/profile/ProfileBody.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import {StyleSheet, View, Text, LayoutChangeEvent} from 'react-native'; +import {AuthContext} from '../../routes/authentication'; + +interface ProfileBodyProps { + onLayout: (event: LayoutChangeEvent) => void; +} +const ProfileBody: React.FC<ProfileBodyProps> = ({onLayout}) => { + const { + profile, + user: {username}, + } = React.useContext(AuthContext); + const {biography, website} = profile; + return ( + <View onLayout={onLayout} style={styles.container}> + <Text style={styles.username}>{`@${username}`}</Text> + <Text style={styles.biography}>{`${biography}`}</Text> + <Text style={styles.website}>{`${website}`}</Text> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + paddingVertical: 5, + paddingHorizontal: 20, + backgroundColor: 'white', + }, + username: { + fontWeight: '600', + fontSize: 16, + marginBottom: 5, + }, + biography: { + fontSize: 16, + lineHeight: 22, + marginBottom: 5, + }, + website: { + fontSize: 16, + color: '#4E699C', + marginBottom: 5, + }, +}); + +export default ProfileBody; diff --git a/src/components/profile/ProfileCutout.tsx b/src/components/profile/ProfileCutout.tsx new file mode 100644 index 00000000..c5deb06d --- /dev/null +++ b/src/components/profile/ProfileCutout.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Svg, {Polygon} from 'react-native-svg'; +import { + PROFILE_CUTOUT_CORNER_Y, + PROFILE_CUTOUT_CORNER_X, + PROFILE_CUTOUT_TOP_Y, + PROFILE_CUTOUT_BOTTOM_Y, +} from '../../constants'; +import {SCREEN_WIDTH} from '../../utils'; + +const ProfileCutout: React.FC = ({children}) => { + return ( + <Svg width={SCREEN_WIDTH} height={PROFILE_CUTOUT_BOTTOM_Y}> + <Polygon + points={`0,${PROFILE_CUTOUT_CORNER_Y} ${PROFILE_CUTOUT_CORNER_X},${PROFILE_CUTOUT_TOP_Y} ${SCREEN_WIDTH},${PROFILE_CUTOUT_TOP_Y} ${SCREEN_WIDTH},${PROFILE_CUTOUT_BOTTOM_Y}, 0,${PROFILE_CUTOUT_BOTTOM_Y}`} + fill={'white'} + /> + {children} + </Svg> + ); +}; + +export default ProfileCutout; diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx new file mode 100644 index 00000000..ec382357 --- /dev/null +++ b/src/components/profile/ProfileHeader.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import Avatar from './Avatar'; +import FollowCount from './FollowCount'; +import {View, Text, StyleSheet} from 'react-native'; +import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {AuthContext} from '../../routes/authentication'; + +const ProfileHeader: React.FC = () => { + const { + profile: {name}, + } = React.useContext(AuthContext); + return ( + <View style={styles.container}> + <View style={styles.row}> + <Avatar style={styles.avatar} /> + <View style={styles.header}> + <Text style={styles.name}>{name}</Text> + <View style={styles.row}> + <FollowCount + style={styles.follows} + mode="followers" + count={318412} + /> + <FollowCount style={styles.follows} mode="following" count={1036} /> + </View> + </View> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + top: SCREEN_HEIGHT / 2.4, + paddingHorizontal: SCREEN_WIDTH / 20, + marginBottom: SCREEN_HEIGHT / 10, + }, + row: { + flexDirection: 'row', + }, + header: { + justifyContent: 'center', + alignItems: 'center', + marginTop: SCREEN_HEIGHT / 40, + marginLeft: SCREEN_WIDTH / 10, + marginBottom: SCREEN_HEIGHT / 50, + }, + avatar: { + bottom: SCREEN_HEIGHT / 80, + }, + name: { + fontSize: 20, + fontWeight: '700', + marginBottom: SCREEN_HEIGHT / 80, + }, + follows: { + marginHorizontal: SCREEN_HEIGHT / 50, + }, +}); + +export default ProfileHeader; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts new file mode 100644 index 00000000..2052ee5b --- /dev/null +++ b/src/components/profile/index.ts @@ -0,0 +1,7 @@ +export {default as Cover} from './Cover'; +export {default as Content} from './Content'; +export {default as ProfileCutout} from './ProfileCutout'; +export {default as MomentsBar} from './MomentsBar'; +export {default as ProfileBody} from './ProfileBody'; +export {default as ProfileHeader} from './ProfileHeader'; +export {default as Feed} from './Feed'; diff --git a/src/constants/api.ts b/src/constants/api.ts index 657adf03..fd654c53 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1,7 +1,10 @@ -export const API_ENDPOINT: string = 'http://127.0.0.1:8000/api/'; -export const LOGIN_ENDPOINT: string = 'http://127.0.0.1:8000/api/login/'; -export const LOGOUT_ENDPOINT: string = 'http://127.0.0.1:8000/api/logout/'; -export const REGISTER_ENDPOINT: string = 'http://127.0.0.1:8000/api/register/'; -export const SEND_OTP_ENDPOINT: string = 'http://127.0.0.1:8000/api/send-otp/'; -export const VERIFY_OTP_ENDPOINT: string = - 'http://127.0.0.1:8000/api/verify-otp/'; +const BASE_URL: string = 'http://127.0.0.1:8000/'; +const API_URL: string = BASE_URL + 'api/'; +export const LOGIN_ENDPOINT: string = API_URL + 'login/'; +export const LOGOUT_ENDPOINT: string = API_URL + 'logout/'; +export const REGISTER_ENDPOINT: string = API_URL + 'register/'; +export const SEND_OTP_ENDPOINT: string = API_URL + 'send-otp/'; +export const VERIFY_OTP_ENDPOINT: string = API_URL + 'verify-otp/'; +export const PROFILE_INFO_ENDPOINT: string = API_URL + 'user-profile-info/'; +export const COVER_PHOTO_ENDPOINT: string = API_URL + 'large-profile-pic/'; +export const AVATAR_PHOTO_ENDPOINT: string = API_URL + 'small-profile-pic/'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts new file mode 100644 index 00000000..f79c2c5b --- /dev/null +++ b/src/constants/constants.ts @@ -0,0 +1,10 @@ +import {SCREEN_WIDTH, SCREEN_HEIGHT} from '../utils'; + +export const PROFILE_CUTOUT_TOP_Y = SCREEN_HEIGHT / 2.3; +export const PROFILE_CUTOUT_BOTTOM_Y = SCREEN_HEIGHT / 1.8; +export const PROFILE_CUTOUT_CORNER_X = SCREEN_WIDTH / 2.9; +export const PROFILE_CUTOUT_CORNER_Y = SCREEN_HEIGHT / 1.95; + +export const IMAGE_WIDTH = SCREEN_WIDTH; +export const IMAGE_HEIGHT = SCREEN_WIDTH; +export const COVER_HEIGHT = SCREEN_WIDTH * (7 / 5); diff --git a/src/constants/index.ts b/src/constants/index.ts index deb89e57..7fb47dc6 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,4 @@ export * from './api'; +export * from './constants'; export * from './regex'; export * from './termsConditions'; diff --git a/src/routes/NavigationBar.tsx b/src/routes/NavigationBar.tsx deleted file mode 100644 index 84c18e00..00000000 --- a/src/routes/NavigationBar.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import {ViewProps} from 'react-native'; -import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; -import {Fragment} from 'react'; -import {NavigationIcon} from '../components'; -import {Home, Notifications, Profile, Search, Upload} from '../screens/main'; - -interface NavigationBarProps extends ViewProps { - centered?: boolean; -} - -const Tab = createBottomTabNavigator(); - -const NavigationBar: React.FC<NavigationBarProps> = () => { - return ( - <Fragment> - <Tab.Navigator - screenOptions={({route}) => ({ - tabBarIcon: ({focused}) => { - if (route.name === 'Home') { - return focused ? ( - <NavigationIcon tab="Home" disabled={false} /> - ) : ( - <NavigationIcon tab="Home" disabled={true} /> - ); - } else if (route.name === 'Search') { - return focused ? ( - <NavigationIcon tab="Search" disabled={false} /> - ) : ( - <NavigationIcon tab="Search" disabled={true} /> - ); - } else if (route.name === 'Upload') { - return focused ? ( - <NavigationIcon tab="Upload" disabled={false} /> - ) : ( - <NavigationIcon tab="Upload" disabled={true} /> - ); - } else if (route.name === 'Notifications') { - return focused ? ( - <NavigationIcon tab="Notifications" disabled={false} /> - ) : ( - <NavigationIcon tab="Notifications" disabled={true} /> - ); - } else if (route.name === 'Profile') { - return focused ? ( - <NavigationIcon tab="Profile" disabled={false} /> - ) : ( - <NavigationIcon tab="Profile" disabled={true} /> - ); - } - }, - })} - initialRouteName="Home" - tabBarOptions={{ - showLabel: false, - style: { - backgroundColor: 'transparent', - position: 'absolute', - borderTopWidth: 0, - left: 0, - right: 0, - bottom: 0, - }, - }}> - <Tab.Screen name="Home" component={Home} /> - <Tab.Screen name="Search" component={Search} /> - <Tab.Screen name="Upload" component={Upload} /> - <Tab.Screen name="Notifications" component={Notifications} /> - <Tab.Screen name="Profile" component={Profile} /> - </Tab.Navigator> - </Fragment> - ); -}; - -export default NavigationBar; diff --git a/src/routes/OnboardingStack.tsx b/src/routes/OnboardingStack.tsx deleted file mode 100644 index 5e91fe9f..00000000 --- a/src/routes/OnboardingStack.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import {createStackNavigator} from '@react-navigation/stack'; -import { - Login, - RegistrationOne, - RegistrationTwo, - Verification, - ProfileOnboarding, -} from '../screens/onboarding'; - -export type RootStackParamList = { - Login: undefined; - RegistrationOne: undefined; - RegistrationTwo: - | {firstName: string; lastName: string; email: string} - | undefined; - Verification: {username: string; email: string; userId: string}; - ProfileOnboarding: {username: string; userId: string}; -}; - -const RootStack = createStackNavigator<RootStackParamList>(); - -interface OnboardingStackProps {} - -const OnboardingStack: React.FC<OnboardingStackProps> = ({}) => { - return ( - <RootStack.Navigator initialRouteName="Login"> - <RootStack.Screen - name="Login" - component={Login} - options={{headerShown: false}} - /> - <RootStack.Screen - name="RegistrationOne" - component={RegistrationOne} - options={{headerShown: false}} - /> - <RootStack.Screen - name="RegistrationTwo" - component={RegistrationTwo} - options={{headerShown: false}} - /> - <RootStack.Screen - name="Verification" - component={Verification} - options={{headerShown: false}} - /> - <RootStack.Screen - name="ProfileOnboarding" - component={ProfileOnboarding} - options={{headerShown: false}} - /> - </RootStack.Navigator> - ); -}; - -export default OnboardingStack; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 43a51b90..92cd3dd2 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -1,42 +1,14 @@ import React from 'react'; -import {OnboardingStack, NavigationBar} from './'; -interface RoutesProps {} -interface AuthProviderProps {} +import {AuthContext} from './authentication'; +import NavigationBar from './tabs'; +import Onboarding from './onboarding'; -export const AuthContext = React.createContext<{ - user: boolean; - login: () => void; - logout: () => void; -}>({ - user: false, - login: () => {}, - logout: () => {}, -}); - -export const AuthContextProvider: React.FC<AuthProviderProps> = ({ - children, -}) => { - const [loggedIn, setLoggedIn] = React.useState(false); // renders onboarding stack - return ( - <AuthContext.Provider - value={{ - user: loggedIn, - login: () => { - setLoggedIn(true); - }, - logout: () => { - setLoggedIn(false); - }, - }}> - {children} - </AuthContext.Provider> - ); -}; - -const Routes: React.FC<RoutesProps> = ({}) => { - const {user} = React.useContext(AuthContext); - return user ? <NavigationBar /> : <OnboardingStack />; +const Routes: React.FC = () => { + const { + user: {userId}, + } = React.useContext(AuthContext); + return userId ? <NavigationBar /> : <Onboarding />; }; export default Routes; diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx new file mode 100644 index 00000000..bd5706f3 --- /dev/null +++ b/src/routes/authentication/AuthProvider.tsx @@ -0,0 +1,122 @@ +import React, {useEffect} from 'react'; +import {createContext, useState} from 'react'; +import RNFetchBlob from 'rn-fetch-blob'; +import {UserType, ProfileType} from '../../types'; +import { + PROFILE_INFO_ENDPOINT, + AVATAR_PHOTO_ENDPOINT, + COVER_PHOTO_ENDPOINT, +} from '../../constants'; + +interface AuthContextProps { + user: UserType; + profile: ProfileType; + login: (userId: string, username: string) => void; + logout: () => void; + avatar: string | null; + cover: string | null; +} +const NO_USER: UserType = { + userId: '', + username: '', +}; +const NO_PROFILE: ProfileType = { + biography: '', + website: '', + name: '', +}; +export const AuthContext = createContext<AuthContextProps>({ + user: NO_USER, + profile: NO_PROFILE, + login: () => {}, + logout: () => {}, + avatar: null, + cover: null, +}); + +/** + * Authentication provider for the application. + */ +const AuthProvider: React.FC = ({children}) => { + const [user, setUser] = useState<UserType>(NO_USER); + const [profile, setProfile] = useState<ProfileType>(NO_PROFILE); + const [avatar, setAvatar] = useState<string | null>(null); + const [cover, setCover] = useState<string | null>(null); + + const {userId} = user; + useEffect(() => { + if (!userId) { + return; + } + const loadProfileInfo = async () => { + try { + const response = await fetch(PROFILE_INFO_ENDPOINT + `${userId}/`, { + method: 'GET', + }); + const status = response.status; + if (status === 200) { + const info = await response.json(); + let {name, biography, website} = info; + setProfile({name, biography, website}); + } + } catch (error) { + console.log(error); + } + }; + const loadAvatar = async () => { + try { + const response = await RNFetchBlob.config({ + fileCache: true, + appendExt: 'jpg', + }).fetch('GET', AVATAR_PHOTO_ENDPOINT + `${userId}/`); + const status = response.info().status; + if (status === 200) { + setAvatar(response.path()); + } else { + setAvatar(''); + } + } catch (error) { + console.log(error); + } + }; + const loadCover = async () => { + try { + let response = await RNFetchBlob.config({ + fileCache: true, + appendExt: 'jpg', + }).fetch('GET', COVER_PHOTO_ENDPOINT + `${userId}/`); + const status = response.info().status; + if (status === 200) { + setCover(response.path()); + } else { + setCover(''); + } + } catch (error) { + console.log(error); + } + }; + loadProfileInfo(); + loadAvatar(); + loadCover(); + }, [userId]); + + return ( + <AuthContext.Provider + value={{ + user, + profile, + avatar, + cover, + login: (id, username) => { + setUser({...user, userId: id, username}); + }, + logout: () => { + setUser(NO_USER); + }, + }}> + {children} + </AuthContext.Provider> + ); +}; + +export default AuthProvider; diff --git a/src/routes/authentication/index.ts b/src/routes/authentication/index.ts new file mode 100644 index 00000000..9968ae93 --- /dev/null +++ b/src/routes/authentication/index.ts @@ -0,0 +1,2 @@ +export * from './AuthProvider'; +export {default} from './AuthProvider'; diff --git a/src/routes/index.ts b/src/routes/index.ts index 054a25c3..69697b20 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,6 +1,4 @@ -export {default as OnboardingStack} from './OnboardingStack'; -export * from './OnboardingStack'; -export {default as NavigationBar} from './NavigationBar'; -export * from './NavigationBar'; -export {default as Routes} from './Routes'; -export * from './Routes'; +export {default as AuthProvider} from './authentication'; +export * from './authentication'; +export * from './onboarding'; +export {default} from './Routes'; diff --git a/src/routes/onboarding/Onboarding.tsx b/src/routes/onboarding/Onboarding.tsx new file mode 100644 index 00000000..d2bfbfd6 --- /dev/null +++ b/src/routes/onboarding/Onboarding.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import {OnboardingStack} from './OnboardingStack'; +import { + Login, + RegistrationOne, + RegistrationTwo, + Verification, + ProfileOnboarding, +} from '../../screens'; + +const Onboarding: React.FC = () => { + return ( + <OnboardingStack.Navigator initialRouteName="Login"> + <OnboardingStack.Screen + name="Login" + component={Login} + options={{headerShown: false}} + /> + <OnboardingStack.Screen + name="RegistrationOne" + component={RegistrationOne} + options={{headerShown: false}} + /> + <OnboardingStack.Screen + name="RegistrationTwo" + component={RegistrationTwo} + options={{headerShown: false}} + /> + <OnboardingStack.Screen + name="Verification" + component={Verification} + options={{headerShown: false}} + /> + <OnboardingStack.Screen + name="ProfileOnboarding" + component={ProfileOnboarding} + options={{headerShown: false}} + /> + </OnboardingStack.Navigator> + ); +}; + +export default Onboarding; diff --git a/src/routes/onboarding/OnboardingStack.tsx b/src/routes/onboarding/OnboardingStack.tsx new file mode 100644 index 00000000..f9722d46 --- /dev/null +++ b/src/routes/onboarding/OnboardingStack.tsx @@ -0,0 +1,13 @@ +import {createStackNavigator} from '@react-navigation/stack'; + +export type OnboardingStackParams = { + Login: undefined; + RegistrationOne: undefined; + RegistrationTwo: + | {firstName: string; lastName: string; email: string} + | undefined; + Verification: {username: string; email: string; userId: string}; + ProfileOnboarding: {username: string; userId: string}; +}; + +export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); diff --git a/src/routes/onboarding/index.ts b/src/routes/onboarding/index.ts new file mode 100644 index 00000000..66b0f3f4 --- /dev/null +++ b/src/routes/onboarding/index.ts @@ -0,0 +1,2 @@ +export * from './OnboardingStack'; +export {default} from './Onboarding'; diff --git a/src/routes/tabs/NavigationBar.tsx b/src/routes/tabs/NavigationBar.tsx new file mode 100644 index 00000000..aca968c2 --- /dev/null +++ b/src/routes/tabs/NavigationBar.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; +import {NavigationIcon} from '../../components'; +import { + ProfileScreen, + Home, + Notifications, + Search, + Upload, +} from '../../screens'; + +const Tabs = createBottomTabNavigator(); + +const NavigationBar: React.FC = () => { + return ( + <Tabs.Navigator + screenOptions={({route}) => ({ + tabBarIcon: ({focused}) => { + if (route.name === 'Home') { + return focused ? ( + <NavigationIcon tab="Home" disabled={false} /> + ) : ( + <NavigationIcon tab="Home" disabled={true} /> + ); + } else if (route.name === 'Search') { + return focused ? ( + <NavigationIcon tab="Search" disabled={false} /> + ) : ( + <NavigationIcon tab="Search" disabled={true} /> + ); + } else if (route.name === 'Upload') { + return focused ? ( + <NavigationIcon tab="Upload" disabled={false} /> + ) : ( + <NavigationIcon tab="Upload" disabled={true} /> + ); + } else if (route.name === 'Notifications') { + return focused ? ( + <NavigationIcon tab="Notifications" disabled={false} /> + ) : ( + <NavigationIcon tab="Notifications" disabled={true} /> + ); + } else if (route.name === 'Profile') { + return focused ? ( + <NavigationIcon tab="Profile" disabled={false} /> + ) : ( + <NavigationIcon tab="Profile" disabled={true} /> + ); + } + }, + })} + initialRouteName="Profile" + tabBarOptions={{ + showLabel: false, + style: { + backgroundColor: 'transparent', + position: 'absolute', + borderTopWidth: 0, + left: 0, + right: 0, + bottom: 0, + }, + }}> + <Tabs.Screen name="Home" component={Home} /> + <Tabs.Screen name="Search" component={Search} /> + <Tabs.Screen name="Upload" component={Upload} /> + <Tabs.Screen name="Notifications" component={Notifications} /> + <Tabs.Screen name="Profile" component={ProfileScreen} /> + </Tabs.Navigator> + ); +}; + +export default NavigationBar; diff --git a/src/routes/tabs/index.ts b/src/routes/tabs/index.ts new file mode 100644 index 00000000..8ea77e8f --- /dev/null +++ b/src/routes/tabs/index.ts @@ -0,0 +1 @@ +export {default} from './NavigationBar'; diff --git a/src/screens/index.ts b/src/screens/index.ts new file mode 100644 index 00000000..5dd3007a --- /dev/null +++ b/src/screens/index.ts @@ -0,0 +1,3 @@ +export * from './main'; +export * from './onboarding'; +export * from './profile'; diff --git a/src/screens/main/Profile.tsx b/src/screens/main/Profile.tsx deleted file mode 100644 index 3a6536e4..00000000 --- a/src/screens/main/Profile.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import {Text} from 'react-native-animatable'; -import {StyleSheet} from 'react-native'; -import {GradientBackground} from '../../components'; - -/** - * Profile Screen for a user's logged in profile - * including posts, messaging, and settings - */ - -const Profile: React.FC = () => { - return ( - <GradientBackground> - <Text style={styles.text}> Profile Screen 🤩 </Text> - </GradientBackground> - ); -}; -const styles = StyleSheet.create({ - text: { - justifyContent: 'center', - backgroundColor: 'transparent', - }, -}); -export default Profile; diff --git a/src/screens/main/index.ts b/src/screens/main/index.ts index 9bd00c57..fb1bf49b 100644 --- a/src/screens/main/index.ts +++ b/src/screens/main/index.ts @@ -1,5 +1,4 @@ export {default as Home} from './Home'; export {default as Notifications} from './Notifications'; -export {default as Profile} from './Profile'; export {default as Search} from './Search'; export {default as Upload} from './Upload'; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 7b76e97c..5c569ec3 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -13,13 +13,14 @@ import { Platform, } from 'react-native'; -import {RootStackParamList, AuthContext} from '../../routes'; +import {OnboardingStackParams} from '../../routes/onboarding'; +import {AuthContext} from '../../routes/authentication'; import {Background, TaggInput, SubmitButton} from '../../components'; import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; -type VerificationScreenRouteProp = RouteProp<RootStackParamList, 'Login'>; +type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< - RootStackParamList, + OnboardingStackParams, 'Login' >; interface LoginProps { @@ -98,9 +99,9 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { }; /** - * Handler for the Let's Start button or the Go button on the keyboard. - Makes a POST request to the Django login API and presents Alerts based on the status codes that the backend returns. - */ + * Handler for the Let's Start button or the Go button on the keyboard. + Makes a POST request to the Django login API and presents Alerts based on the status codes that the backend returns. + */ const handleLogin = async () => { if (!form.attemptedSubmit) { setForm({ @@ -110,17 +111,19 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { } try { if (form.isValidUser && form.isValidPassword) { + const {username, password} = form; let response = await fetch(LOGIN_ENDPOINT, { method: 'POST', body: JSON.stringify({ - username: form.username, - password: form.password, + username, + password, }), }); let statusCode = response.status; + let data = await response.json(); if (statusCode === 200) { - login(); + login(data.UserID, username); } else if (statusCode === 401) { Alert.alert( 'Login failed 😔', diff --git a/src/screens/onboarding/ProfileOnboarding.tsx b/src/screens/onboarding/ProfileOnboarding.tsx index 6ce1ff80..9405ca52 100644 --- a/src/screens/onboarding/ProfileOnboarding.tsx +++ b/src/screens/onboarding/ProfileOnboarding.tsx @@ -10,17 +10,18 @@ import { Alert, View, } from 'react-native'; -import {RootStackParamList, AuthContext} from '../../routes'; +import {OnboardingStackParams} from '../../routes/onboarding'; +import {AuthContext} from '../../routes/authentication'; import {Background} from '../../components'; import ImagePicker from 'react-native-image-crop-picker'; import {REGISTER_ENDPOINT} from '../../constants'; type ProfileOnboardingScreenRouteProp = RouteProp< - RootStackParamList, + OnboardingStackParams, 'ProfileOnboarding' >; type ProfileOnboardingScreenNavigationProp = StackNavigationProp< - RootStackParamList, + OnboardingStackParams, 'ProfileOnboarding' >; interface ProfileOnboardingProps { @@ -150,10 +151,10 @@ const ProfileOnboarding: React.FC<ProfileOnboardingProps> = ({route}) => { }, body: form, }); - let data = await response.json(); let statusCode = response.status; + let data = await response.json(); if (statusCode === 200) { - login(); + login(userId, username); } else if (statusCode === 400) { Alert.alert('Profile update failed. 😔', `${data}`); } else { diff --git a/src/screens/onboarding/RegistrationOne.tsx b/src/screens/onboarding/RegistrationOne.tsx index 3b9ddb3e..720fcaed 100644 --- a/src/screens/onboarding/RegistrationOne.tsx +++ b/src/screens/onboarding/RegistrationOne.tsx @@ -12,7 +12,7 @@ import { KeyboardAvoidingView, } from 'react-native'; -import {RootStackParamList} from '../../routes'; +import {OnboardingStackParams} from '../../routes'; import { ArrowButton, RegistrationWizard, @@ -22,11 +22,11 @@ import { import {nameRegex, emailRegex} from '../../constants'; type RegistrationScreenOneRouteProp = RouteProp< - RootStackParamList, + OnboardingStackParams, 'RegistrationOne' >; type RegistrationScreenOneNavigationProp = StackNavigationProp< - RootStackParamList, + OnboardingStackParams, 'RegistrationOne' >; interface RegistrationOneProps { diff --git a/src/screens/onboarding/RegistrationTwo.tsx b/src/screens/onboarding/RegistrationTwo.tsx index 09e217f6..b67c2403 100644 --- a/src/screens/onboarding/RegistrationTwo.tsx +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -14,7 +14,7 @@ import { } from 'react-native'; import {usePromiseTracker, trackPromise} from 'react-promise-tracker'; -import {RootStackParamList} from '../../routes'; +import {OnboardingStackParams} from '../../routes'; import { ArrowButton, RegistrationWizard, @@ -30,11 +30,11 @@ import { } from '../../constants'; type RegistrationScreenTwoRouteProp = RouteProp< - RootStackParamList, + OnboardingStackParams, 'RegistrationTwo' >; type RegistrationScreenTwoNavigationProp = StackNavigationProp< - RootStackParamList, + OnboardingStackParams, 'RegistrationTwo' >; interface RegistrationTwoProps { diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx index 197bc0ca..0676bb3a 100644 --- a/src/screens/onboarding/Verification.tsx +++ b/src/screens/onboarding/Verification.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {RootStackParamList} from '../../routes'; +import {OnboardingStackParams} from '../../routes'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import {Background, RegistrationWizard, SubmitButton} from '../../components'; @@ -23,11 +23,11 @@ import { import {usePromiseTracker, trackPromise} from 'react-promise-tracker'; type VerificationScreenRouteProp = RouteProp< - RootStackParamList, + OnboardingStackParams, 'Verification' >; type VerificationScreenNavigationProp = StackNavigationProp< - RootStackParamList, + OnboardingStackParams, 'Verification' >; interface VerificationProps { diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 9b2f4cb0..7a9816e7 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -1,5 +1,5 @@ export {default as Login} from './Login'; +export {default as ProfileOnboarding} from './ProfileOnboarding'; export {default as RegistrationOne} from './RegistrationOne'; export {default as RegistrationTwo} from './RegistrationTwo'; export {default as Verification} from './Verification'; -export {default as ProfileOnboarding} from './ProfileOnboarding'; diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx new file mode 100644 index 00000000..3d1ef2a8 --- /dev/null +++ b/src/screens/profile/ProfileScreen.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {Cover, Content} from '../../components'; +import Animated from 'react-native-reanimated'; +import {AuthContext} from '../../routes/authentication'; +import {StatusBar} from 'react-native'; + +// destructure Value object from Animated +const {Value} = Animated; + +/** + * Profile Screen for a user's logged in profile + * including posts, messaging, and settings + */ +const ProfileScreen: React.FC = () => { + const {user} = React.useContext(AuthContext); + const y = new Value(0); + return ( + <> + <StatusBar /> + <Cover {...{y, user}} /> + <Content {...{y, user}} /> + </> + ); +}; + +export default ProfileScreen; diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts new file mode 100644 index 00000000..0ade259d --- /dev/null +++ b/src/screens/profile/index.ts @@ -0,0 +1 @@ +export {default as ProfileScreen} from './ProfileScreen'; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..fcb073fe --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..1a023932 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,14 @@ +export interface UserType { + userId: string; + username: string; +} + +export interface ProfileType { + name: string; + biography: string; + website: string; +} + +export interface PostType { + owner: UserType; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..5bc168e3 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './screenDimensions'; +export * from './statusBarHeight'; diff --git a/src/utils/screenDimensions.ts b/src/utils/screenDimensions.ts new file mode 100644 index 00000000..56277ddc --- /dev/null +++ b/src/utils/screenDimensions.ts @@ -0,0 +1,6 @@ +import {Dimensions} from 'react-native'; + +const {width, height} = Dimensions.get('window'); + +export const SCREEN_WIDTH = width; +export const SCREEN_HEIGHT = height; diff --git a/src/utils/statusBarHeight.ts b/src/utils/statusBarHeight.ts new file mode 100644 index 00000000..4c68f9ee --- /dev/null +++ b/src/utils/statusBarHeight.ts @@ -0,0 +1,19 @@ +import {Platform, StatusBar} from 'react-native'; +import {SCREEN_WIDTH, SCREEN_HEIGHT} from './screenDimensions'; + +const X_WIDTH = 375; +const X_HEIGHT = 812; +const XSMAX_WIDTH = 414; +const XSMAX_HEIGHT = 896; + +const isIPhoneX = () => + Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS + ? (SCREEN_WIDTH === X_WIDTH && SCREEN_HEIGHT === X_HEIGHT) || + (SCREEN_WIDTH === XSMAX_WIDTH && SCREEN_HEIGHT === XSMAX_HEIGHT) + : false; + +export const StatusBarHeight = Platform.select({ + ios: isIPhoneX() ? 44 : 20, + android: StatusBar.currentHeight, + default: 0, +}); @@ -1621,6 +1621,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-64@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + base64-js@^1.1.2, base64-js@^1.2.3: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -1658,6 +1663,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + bplist-creator@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.0.8.tgz#56b2a6e79e9aec3fc33bf831d09347d73794e79c" @@ -2086,6 +2096,29 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +css-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-tree@^1.0.0-alpha.39: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" + integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== + dependencies: + mdn-data "2.0.6" + source-map "^0.6.1" + +css-what@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" + integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -2238,6 +2271,24 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -2245,6 +2296,14 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2287,6 +2346,11 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + envinfo@^7.1.0: version "7.5.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.5.1.tgz#93c26897225a00457c75e734d354ea9106a72236" @@ -2966,6 +3030,18 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob@7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" + integrity sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo= + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4168,6 +4244,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mdn-data@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" + integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== + merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" @@ -4464,7 +4545,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -4621,6 +4702,13 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + nullthrows@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" @@ -5154,6 +5242,14 @@ react-native-screens@^2.9.0: resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.9.0.tgz#ead2843107ba00fee259aa377582e457c74f1f3b" integrity sha512-5MaiUD6HA3nzY3JbVI8l3V7pKedtxQF3d8qktTVI0WmWXTI4QzqOU8r8fPVvfKo3MhOXwhWBjr+kQ7DZaIQQeg== +react-native-svg@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.1.0.tgz#acfe48c35cd5fca3d5fd767abae0560c36cfc03d" + integrity sha512-1g9qBRci7man8QsHoXn6tP3DhCDiypGgc6+AOWq+Sy+PmP6yiyf8VmvKuoqrPam/tf5x+ZaBT2KI0gl7bptZ7w== + dependencies: + css-select "^2.1.0" + css-tree "^1.0.0-alpha.39" + react-native@0.62.2: version "0.62.2" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.62.2.tgz#d831e11a3178705449142df19a70ac2ca16bad10" @@ -5473,6 +5569,14 @@ rimraf@~2.2.6: resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" integrity sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI= +rn-fetch-blob@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba" + integrity sha512-+QnR7AsJ14zqpVVUbzbtAjq0iI8c9tCg49tIoKO2ezjzRunN7YL6zFSFSWZm6d+mE/l9r+OeDM3jmb2tBb2WbA== + dependencies: + base-64 "0.1.0" + glob "7.0.6" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" |