From 1279249ee9355f88913578f51e3b0bf7d99672f6 Mon Sep 17 00:00:00 2001 From: Leon Jiang <35908040+leonyjiang@users.noreply.github.com> Date: Wed, 5 Aug 2020 14:15:06 -0700 Subject: [TMA-122] User Profile Screen UI (#27) * Fix yarn lint issues * Add react-native-svg to project * Create UserType & PostType * Create temporary Post component * Fix import cycle warning, update AuthContext * Update onboarding screen imports * Update config files * Add rn-fetch-blob package * Update types * Add profile fetching to AuthContext * Update post component * Import placeholder images from designs * Add profile UI components * Create screen offset constants * Add new api endpoints * Create screen layout utils * Create Profile screen UI * Remove some unused styling * Restructure ProfileScreen and fix animations * Add gradient back to screen * Update Moment circle styling --- src/components/profile/Avatar.tsx | 31 +++++++++++++ src/components/profile/Content.tsx | 58 ++++++++++++++++++++++++ src/components/profile/Cover.tsx | 41 +++++++++++++++++ src/components/profile/Feed.tsx | 25 +++++++++++ src/components/profile/FollowCount.tsx | 42 ++++++++++++++++++ src/components/profile/Moment.tsx | 35 +++++++++++++++ src/components/profile/MomentsBar.tsx | 75 ++++++++++++++++++++++++++++++++ src/components/profile/ProfileBody.tsx | 46 ++++++++++++++++++++ src/components/profile/ProfileCutout.tsx | 23 ++++++++++ src/components/profile/ProfileHeader.tsx | 62 ++++++++++++++++++++++++++ src/components/profile/index.ts | 7 +++ 11 files changed, 445 insertions(+) create mode 100644 src/components/profile/Avatar.tsx create mode 100644 src/components/profile/Content.tsx create mode 100644 src/components/profile/Cover.tsx create mode 100644 src/components/profile/Feed.tsx create mode 100644 src/components/profile/FollowCount.tsx create mode 100644 src/components/profile/Moment.tsx create mode 100644 src/components/profile/MomentsBar.tsx create mode 100644 src/components/profile/ProfileBody.tsx create mode 100644 src/components/profile/ProfileCutout.tsx create mode 100644 src/components/profile/ProfileHeader.tsx create mode 100644 src/components/profile/index.ts (limited to 'src/components/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 = ({style}) => { + const {avatar} = React.useContext(AuthContext); + return ( + + ); +}; + +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; + user: UserType; +} +const Content: React.FC = ({y, user}) => { + const [profileBodyHeight, setProfileBodyHeight] = useState(0); + const onLayout = (e: LayoutChangeEvent) => { + const {height} = e.nativeEvent.layout; + setProfileBodyHeight(height); + }; + return ( + y.setValue(e.nativeEvent.contentOffset.y)} + showsVerticalScrollIndicator={false} + scrollEventThrottle={1} + stickyHeaderIndices={[2, 4]}> + + + + + + + + + ); +}; + +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; +} +const Cover: React.FC = ({y}) => { + const {cover} = React.useContext(AuthContext); + const scale: Animated.Node = interpolate(y, { + inputRange: [-COVER_HEIGHT, 0], + outputRange: [1.5, 1.25], + extrapolateRight: Extrapolate.CLAMP, + }); + return ( + + + + ); +}; + +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 = ({user}) => { + const posts: Array = []; + const dummyPost: PostType = { + owner: user, + }; + for (let i = 0; i < 20; i++) { + posts.push(dummyPost); + } + return ( + <> + {posts.map((post, index) => ( + + ))} + + ); +}; + +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 = ({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 ( + + {displayed} + + {mode === 'followers' ? 'Followers' : 'Following'} + + + ); +}; + +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 = ({style}) => { + return ( + + + + ); +}; + +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; + profileBodyHeight: number; +} +const MomentsBar: React.FC = ({y, profileBodyHeight}) => { + const moments: Array = []; + for (let i = 0; i < 10; i++) { + moments.push(); + } + const shadowOpacity: Animated.Node = 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 = 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 = interpolate(y, { + inputRange: [ + 0, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight - 30, + PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight, + ], + outputRange: [30, 30, 15], + extrapolate: Extrapolate.CLAMP, + }); + return ( + + + {moments} + + + ); +}; + +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 = ({onLayout}) => { + const { + profile, + user: {username}, + } = React.useContext(AuthContext); + const {biography, website} = profile; + return ( + + {`@${username}`} + {`${biography}`} + {`${website}`} + + ); +}; + +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 ( + + + {children} + + ); +}; + +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 ( + + + + + {name} + + + + + + + + ); +}; + +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'; -- cgit v1.2.3-70-g09d2