Chapter 13: Profile Posts
Full Stack React Native
link Profile Posts
The Story So Far
So far, we set up a React Native application with authentication and associative logic. The app is designed to allow users to sign up, login and create votes. In this lesson, we will include a new tab for profile posts.
App Setup
git clone https://github.com/Maelstroms38/movies-starter.git
cd movies-starter
yarn
yarn start
Once you have confirmed the project is running, we will add new tabs in the profile section to display votes and posts.
yarn add @react-navigation/material-top-tabs react-native-tab-view
link Profile Component
Let's start by tweaking ./components/Profile.js
to support votes and posts.
export default function Profile(props) {
- const { currentUser } = props;
- const { username, email, votes } = currentUser;
+ const { currentUser, isVotes } = props;
+ const { username, email, votes, posts } = currentUser;
return (
<View style={styles.container}>
<View style={styles.row}>
{/* ... */}
</Text>
<View style={styles.right}>
<Text style={styles.text} numberOfLines={1}>
- {votes.length} Vote(s)
+ {isVotes ? `${votes.length} Vote(s)` : `${posts.length} Post(s)`}
</Text>
</View>
</View>
{/* ... */}
const styles = StyleSheet.create({
container: {
- padding: 10,
+ paddingHorizontal: 10,
height: 60
},
row: {
link Profile Screen
Next we will modify the ./screens/ProfileScreen.js
to include the updated Profile
component's props:
export default function ProfileScreen(props) {
const { username, email, votes } = currentUser;
return (
<View style={styles.container}>
- <Profile currentUser={currentUser} />
+ <Profile currentUser={currentUser} isVotes />
{votes && votes.length ? (
<FlatList
data={votes}
{/* ... */}
link ProfileLinks
In the profile section, we would like to display both votes and posts. Here we can add a new screen called ./screens/ProfileLinks.js
and insert the following:
import React, { useEffect } from 'react';
import {
View,
Text,
FlatList,
StyleSheet,
AsyncStorage,
ActivityIndicator
} from 'react-native';
import Post from '../components/Post';
import { useQuery } from '@apollo/react-hooks';
import Profile from '../components/Profile';
import gql from 'graphql-tag';
const PROFILE_QUERY = gql`
query {
currentUser {
id
username
email
posts {
id
title
link
imageUrl
author {
id
username
}
}
}
}
`;
export default function ProfileLinks({ navigation }) {
const { data, loading, error } = useQuery(PROFILE_QUERY, {
fetchPolicy: 'network-only'
});
if (!data || !data.currentUser) {
return (
<ActivityIndicator
color="#161616"
style={{ ...StyleSheet.absoluteFillObject }}
/>
);
}
const { currentUser } = data;
const { username, email, posts } = currentUser;
return (
<View style={styles.container}>
<Profile currentUser={currentUser} />
{posts && posts.length ? (
<FlatList
data={posts}
keyExtractor={(item, index) => {
return `${index}`;
}}
decelerationRate="fast"
renderItem={({ item, index }) => {
return (
<Post
post={item}
onPress={() => navigation.navigate('LinkDetail', { post: item })}
/>
);
}}
/>
) : null}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 15,
backgroundColor: '#fff'
},
});
link Post Component
Let's reuse the code from our previous section and create a new file called ./components/Post.js
, with the following contents:
import React, { useState } from 'react';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Image,
Dimensions
} from 'react-native';
const { width } = Dimensions.get('window');
export default function Post(props) {
const { post, navigation, onPress } = props;
const { title, link, imageUrl } = post;
return (
<TouchableOpacity
style={styles.container}
onPress={() => {
onPress && onPress();
}}
>
{!!imageUrl && (
<View style={{ flex: 2 }}>
<Image source={{ uri: imageUrl }} style={styles.image} />
</View>
)}
<View style={styles.footer}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
<Text style={styles.text} numberOfLines={1}>
{link}
</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
width,
borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: '#dddddd'
},
image: {
flex: 2,
width,
height: width / 2,
resizeMode: 'cover'
},
active: {
backgroundColor: 'rgba(255,255,255,0.05)'
},
footer: {
flex: 0.5,
alignItems: 'flex-start',
justifyContent: 'center',
paddingLeft: 10,
paddingVertical: 10
},
title: {
color: '#161616',
fontSize: 20
},
text: {
color: '#161616',
fontSize: 16
}
});
link TopTabNavigator
Up next, we will create a new tab navigator for the profile screen. Create a new file called ./navigation/TopTapNavigator.js
and insert the following:
import React from 'react';
import { StyleSheet, Text, View, Dimensions, Platform } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
const Tab = createMaterialTopTabNavigator();
const Stack = createStackNavigator();
// Components
import ProfileScreen from '../screens/ProfileScreen';
import ProfileLinks from '../screens/ProfileLinks';
// get screen dimensions
const { width, height } = Dimensions.get('window');
export default function TopTabNavigator() {
return (
<Tab.Navigator tabBarOptions={{
scrollEnabled: true,
tabStyle: {
width: width/2,
},
style: {
fontSize: 24,
fontWeight: '700',
backgroundColor: '#fff',
},
labelStyle: {
color: '#161616',
},
indicatorStyle: {
backgroundColor: '#161616',
}
}}>
<Tab.Screen name="Profile" component={ProfileScreen} />
<Tab.Screen name="Posts" component={ProfileLinks} />
</Tab.Navigator>
);
}
With the new TopTabNavigator
, we will be able to configure ./navigation/MainTabNavigator
to display two separate tabs called "Votes" and "Posts":
import React from 'react';
-import { Platform } from 'react-native';
+import { Platform, AsyncStorage } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Ionicons } from '@expo/vector-icons';
import TabBarIcon from '../components/TabBarIcon';
import HomeScreen from '../screens/HomeScreen';
import AuthLoading from '../screens/AuthLoading';
import LinkDetail from '../screens/LinkDetail';
import LinkForm from '../screens/LinkForm';
+import ProfileTab from './TopTabNavigator';
After modifying your MainTabNavigator
top level imports, replace the current ProfileStack
with the following snippet:
const Stack = createStackNavigator();
function ProfileStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Auth" component={AuthLoading} />
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen
name="Profile"
component={ProfileTab}
options={({ navigation, route }) => ({
headerRight: props => (
<Ionicons
onPress={async () => {
await AsyncStorage.removeItem('token');
navigation.replace('Login')
}
}
name="md-exit"
size={25}
color={"#161616"}
style={{
position: 'relative',
left: 20,
zIndex: 8
}} />
),
})} />
</Stack.Navigator>
)
};
At this point you should see your updated Profile screen with two top tabs, "Votes" and "Posts". It is expected that the "Posts" tab is empty, we will include queries to populate it in the following chapters.
Wrapping Up
Today we made a React Native app with a top tab navigator. The next chapter ties in the previous "Post" model to our current Movies API.