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.

format_list_bulleted
help_outline