aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/common/GradientBackground.tsx17
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/common/post/Post.tsx26
-rw-r--r--src/components/common/post/PostHeader.tsx51
-rw-r--r--src/components/common/post/index.ts1
-rw-r--r--src/components/index.ts1
-rw-r--r--src/components/profile/Avatar.tsx31
-rw-r--r--src/components/profile/Content.tsx58
-rw-r--r--src/components/profile/Cover.tsx41
-rw-r--r--src/components/profile/Feed.tsx25
-rw-r--r--src/components/profile/FollowCount.tsx42
-rw-r--r--src/components/profile/Moment.tsx35
-rw-r--r--src/components/profile/MomentsBar.tsx75
-rw-r--r--src/components/profile/ProfileBody.tsx46
-rw-r--r--src/components/profile/ProfileCutout.tsx23
-rw-r--r--src/components/profile/ProfileHeader.tsx62
-rw-r--r--src/components/profile/index.ts7
17 files changed, 533 insertions, 9 deletions
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';