DEV Community

Dillion Megida
Dillion Megida

Posted on • Originally published at getstream.io

Twitter Clone Part 2: Creating a Profile Page and Following Users

In this article, the second part of the Build a Twitter Clone series, you will create a Profile Page for users and add the follow-users feature.

Part 1 focuses on creating the Twitter layout, authenticating users with Stream, adding the create tweet feature, and displaying the home page activity feeds. That is a required step before you can follow the tutorial in this article, so kindly check that first before continuing with this.

Create a Profile Page for Users

The Profile Page shows a user's information such as their cover photo, profile image, tweet count, name, username, bio, date of joining, number of followers, and followings. This page also shows the follow button, which allows other users to follow and unfollow a user. And lastly, the page shows a feed that contains the tweets made by this user.

Profile Page

We will break this page into different components. Let's start from the header.

Create a ProfileHeader Component

This component holds the user's cover photo, the number of tweets created, and the user's name:

Profile Header

Create a new file src/components/Profile/ProfileHeader.js. Start with the imports and styles:

import { useContext, useEffect, useState } from 'react'
import { useStreamContext } from 'react-activity-feed'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'

import ArrowLeft from '../Icons/ArrowLeft'
import { ProfileContext } from './ProfileContent'

const Header = styled.header`
  .top {
    display: flex;
    align-items: center;
    padding: 15px;
    color: white;
    width: 100%;
    backdrop-filter: blur(2px);
    background-color: rgba(0, 0, 0, 0.5);

    .info {
      margin-left: 30px;

      h1 {
        font-size: 20px;
      }

      &__tweets-count {
        font-size: 14px;
        margin-top: 2px;
        color: #888;
      }
    }
  }

  .cover {
    width: 100%;
    background-color: #555;
    height: 200px;
    overflow: hidden;

    img {
      width: 100%;
      object-fit: cover;
      object-position: center;
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

And next, the component:

export default function ProfileHeader() {
  const navigate = useNavigate()
  const { user } = useContext(ProfileContext)
  const { client } = useStreamContext()

  const [activitiesCount, setActivitiesCount] = useState(0)

  useEffect(() => {
    const feed = client.feed('user', user.id)

    async function getActivitiesCount() {
      const activities = await feed.get()

      setActivitiesCount(activities.results.length)
    }

    getActivitiesCount()
  }, [])

  const navigateBack = () => {
    navigate(-1)
  }
}
Enter fullscreen mode Exit fullscreen mode

When the component mounts, you get all the activities and update the activities count state.

Now, for the UI:

export default function ProfileHeader() {
  // ...

  return (
    <Header>
      <div className="top">
        <button onClick={navigateBack}>
          <ArrowLeft size={20} color="white" />
        </button>
        <div className="info">
          <h1>{user.data.name}</h1>
          <span className="info__tweets-count">{activitiesCount} Tweets</span>
        </div>
      </div>
      <div className="cover">
        <img src="https://picsum.photos/500/300" />
      </div>
    </Header>
  )
}
Enter fullscreen mode Exit fullscreen mode

Create the ProfileBio Component

This component holds the user's information and the follow button:

Profile Bio

Create a new file src/components/Profile/ProfileBio.js. Import the required utilities and components, and add the styles:

import { useContext } from 'react'
import styled from 'styled-components'
import { format } from 'date-fns'
import { useStreamContext } from 'react-activity-feed'

import More from '../Icons/More'
import Mail from '../Icons/Mail'
import Calendar from '../Icons/Calendar'
import { formatStringWithLink } from '../../utils/string'
import { ProfileContext } from './ProfileContent'
import FollowBtn from '../FollowBtn'

const Container = styled.div`
  padding: 20px;
  position: relative;

  .top {
    display: flex;
    justify-content: space-between;
    margin-top: calc(var(--profile-image-size) / -2);

    .image {
      width: var(--profile-image-size);
      height: var(--profile-image-size);
      border-radius: 50%;
      overflow: hidden;
      border: 4px solid black;
      background-color: #444;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }

    .actions {
      position: relative;
      top: 55px;
      display: flex;

      .action-btn {
        border: 1px solid #777;
        margin-right: 10px;
        width: 30px;
        height: 30px;
        border-radius: 50%;
        display: flex;
        justify-content: center;
        align-items: center;
      }
    }
  }

  .details {
    color: #888;
    margin-top: 20px;

    .user {
      &__name {
        color: white;
        font-weight: bold;
      }

      &__id {
        margin-top: 2px;
        font-size: 15px;
      }

      &__bio {
        color: white;
        margin-top: 10px;
        a {
          color: var(--theme-color);
          text-decoration: none;
        }
      }

      &__joined {
        display: flex;
        align-items: center;
        margin-top: 15px;
        font-size: 15px;

        &--text {
          margin-left: 5px;
        }
      }

      &__follows {
        font-size: 15px;
        display: flex;
        margin-top: 15px;

        b {
          color: white;
        }

        &__followers {
          margin-left: 20px;
        }
      }

      &__followed-by {
        font-size: 13px;
        margin-top: 15px;
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

This component imports the FollowBtn component for the follow functionality.

ProfileContext comes from ProfileContent, which you will create soon. From that context, this component can get the user's information of the active profile.

And for the component:

const actions = [
  {
    Icon: More,
    id: 'more',
  },
  {
    Icon: Mail,
    id: 'message',
  },
]

export default function ProfileBio() {
  const { user } = useContext(ProfileContext)

  const joinedDate = format(new Date(user.created_at), 'MMMM RRRR')

  const bio = formatStringWithLink(user.data.bio)

  const isLoggedInUserProfile = user.id === client.userId
}
Enter fullscreen mode Exit fullscreen mode

The isLoogedInUserProfile is required so that you can conditionally render the follow button; that is, if the profile page is not for the logged-in user.

And the UI:

export default function ProfileBio() {
  // ...

  return (
    <Container>
      <div className="top">
        <div className="image">
          {' '}
          <img src={user.data.image} alt="" />
        </div>
        {!isLoggedInUserProfile && (
          <div className="actions">
            {actions.map((action) => (
              <button className="action-btn" key={action.id}>
                <action.Icon color="white" size={21} />
              </button>
            ))}
            <FollowBtn userId={user.id} />
          </div>
        )}
      </div>
      <div className="details">
        <span className="user__name">{user.data.name}</span>
        <span className="user__id">@{user.id}</span>
        <span className="user__bio" dangerouslySetInnerHTML={{ __html: bio }} />
        <div className="user__joined">
          <Calendar color="#777" size={20} />
          <span className="user__joined--text">Joined {joinedDate}</span>
        </div>
        <div className="user__follows">
          <span className="user__follows__following">
            <b>{user.following_count || 0}</b> Following
          </span>
          <span className="user__follows__followers">
            <b>{user.followers_count || 0}</b> Followers
          </span>
        </div>
        <div className="user__followed-by">
          Not followed by anyone you are following
        </div>
      </div>
    </Container>
  )
}
Enter fullscreen mode Exit fullscreen mode

Create the TabList Component

The TabList component shows the "Tweets", "Tweets & Replies", "Media" and "Likes" tabs:

TabList

Although the only functioning tab will be "Tweets", as that is the scope of this tutorial, it's nice also to have this on the UI.

Create a new file called src/components/Profile/TabList.js and paste the following:

import classNames from 'classnames'
import { useState } from 'react'
import styled from 'styled-components'

const Container = styled.div`
  display: grid;
  grid-template-columns: 1fr 2fr 1fr 1fr;
  border-bottom: 1px solid #555;
  width: 100%;

  .tab {
    color: #777;
    padding: 0 35px;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: bold;
    font-size: 15px;

    &:hover {
      background-color: #111;
    }

    &__label {
      position: relative;
      width: 100%;
      padding: 20px 7px;

      &.active {
        color: white;

        &::after {
          content: '';
          height: 3px;
          width: 100%;
          background-color: var(--theme-color);
          border-radius: 40px;
          position: absolute;
          bottom: 0;
          left: 0;
        }
      }
    }
  }
`

const tabs = [
  {
    id: 'tweets',
    label: 'Tweets',
  },
  {
    id: 'tweet-replies',
    label: 'Tweets & replies',
  },
  {
    id: 'media',
    label: 'Media',
  },
  {
    id: 'likes',
    label: 'Likes',
  },
]

export default function TabList() {
  const [activeTab, setActiveTab] = useState(tabs[0].id)

  return (
    <Container>
      {tabs.map((tab) => (
        <button
          onClick={() => setActiveTab(tab.id)}
          className="tab"
          key={tab.id}
        >
          <span
            className={classNames(
              'tab__label',
              activeTab === tab.id && 'active'
            )}
          >
            {tab.label}
          </span>
        </button>
      ))}
    </Container>
  )
}
Enter fullscreen mode Exit fullscreen mode

This component also sets the active tab on clicking each tab.

Create a ProfileTweets Component

This component shows a feed of tweet activities for the user on the active profile. Create a new file src/components/Profile/ProfileTweets.js with the following code:

import { useContext } from 'react'
import { FlatFeed } from 'react-activity-feed'

import TweetBlock from '../Tweet/TweetBlock'
import { ProfileContext } from './ProfileContent'

export default function MyTweets() {
  const { user } = useContext(ProfileContext)

  return (
    <div>
      <FlatFeed
        Activity={TweetBlock}
        userId={user.id}
        feedGroup="user"
        notify
      />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

From the ProfileContext (which you will create soon), you get the profile user. Using the FlatFeed component from react-activity-feed and the custom TweetBlock created in part one, you can display the activities made by this user.

Create a ProfileContent Component

With the profile page components created, you can compose the ProfileContent component.

Create a new file src/components/Profile/ProfileContent.js. Add the imports and styles:

import styled from 'styled-components'
import { createContext, useEffect, useState } from 'react'
import { useStreamContext } from 'react-activity-feed'
import { useParams } from 'react-router-dom'

import ProfileHeader from './ProfileHeader'
import LoadingIndicator from '../LoadingIndicator'
import ProfileBio from './ProfileBio'
import TabList from './TabList'
import ProfileTweets from './ProfileTweets'

const Container = styled.div`
  --profile-image-size: 120px;

  .tab-list {
    margin-top: 30px;
  }
`
Enter fullscreen mode Exit fullscreen mode

And next, the context and component:

export const ProfileContext = createContext()

export default function ProfileContent() {
  const { client } = useStreamContext()

  const [user, setUser] = useState(null)
  const { user_id } = useParams()

  useEffect(() => {
    const getUser = async () => {
      const user = await client.user(user_id).get({ with_follow_counts: true })

      setUser(user.full)
    }

    getUser()
  }, [user_id])

  if (!client || !user) return <LoadingIndicator />
}
Enter fullscreen mode Exit fullscreen mode

In the useEffect hook, you get the user's details and update the user state with the full details. As for the UI:

export default function ProfileContent() {
  // ...

  return (
    <ProfileContext.Provider value={{ user }}>
      <Container>
        <ProfileHeader />
        <main>
          <ProfileBio />
          <div className="tab-list">
            <TabList />
          </div>
          <ProfileTweets />
        </main>
      </Container>
    </ProfileContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Profile.Context provides the user object to the children components, as you have seen when creating the profile components.

Finally, the last component – the page component.

Create a Profile Page Component

Create a new file: src/pages/Profile.js with the following code:

import Layout from '../components/Layout'
import ProfileContent from '../components/Profile/ProfileContent'

export default function Profile() {
  return (
    <Layout>
      <ProfileContent />
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

The next step is to add a route for this page in App.js. Import the ProfileContent component first:

// other imports
import Profile from './pages/Profile'
Enter fullscreen mode Exit fullscreen mode

And the route:

<Route element={<Profile />} path="/:user_id" />
Enter fullscreen mode Exit fullscreen mode

With your development server on, when you click on the profile link in the left section or go to a user, for example, localhost:3000/getstream_io, you will see the profile page of this user with their tweets.

Add a Follow Feature

When a user, say userA, follows another user, say userB, userA subscribes to userB's feed. They can then see the activities made by the user they followed. Bringing this idea to tweets, when userA follows userB, userA can see the tweets made by userB on userA's timeline (the homepage).

Let us implement the follow feature.

Build a Custom useFollow Hook

Although this implementation will only be used in the FollowBtn component, it will be helpful to have this as a custom hook to avoid making the component file ambiguous.

Create a new file src/hooks/useFollow.js. I will walk you through building this hook gradually. Add the imports and initialize the state:

import { useEffect, useState } from 'react'
import { useStreamContext } from 'react-activity-feed'

export default function useFollow({ userId }) {
  const { client } = useStreamContext()

  const [isFollowing, setIsFollowing] = useState(false)
}
Enter fullscreen mode Exit fullscreen mode

The component receives the userId prop. This prop is the id of the user that is to be followed or unfollowed. The client object from useStreamContext provides the id of the logged-in user. Going forward, I will refer to the logged-in user as userA and the user to be followed as userB.

The next step is to check if userA is already following userB. You can do this when the component mounts with useEffect:

useEffect(() => {
  async function init() {
    const response = await client
      .feed('timeline', client.userId)
      .following({ filter: [`user:${userId}`] })

    setIsFollowing(!!response.results.length)
  }

  init()
}, [])
Enter fullscreen mode Exit fullscreen mode

In the useEffect hook, you have an init function which, when called, gets userA's timeline feed and filters the results based on following to include userB. If the final results array is not empty, it means userA already follows userB's timeline feed; else, A does not follow B.

Using that result, you can update the following state.

Next, create a toggleFollow function:

const toggleFollow = async () => {
  const action = isFollowing ? 'unfollow' : 'follow'

  const timelineFeed = client.feed('timeline', client.userId)
  await timelineFeed[action]('user', userId)

  setIsFollowing((isFollowing) => !isFollowing)
}
Enter fullscreen mode Exit fullscreen mode

In this function, you get the timeLineFeed on the logged-in user, and on that feed, you can either call the follow() or unfollow() method on userB's feed. Both methods accept the "user" feed type and the userId.

At the end of this hook, you will return the isFollowing state and the toggleFollow method. The hook file should include this code:

import { useEffect, useState } from 'react'
import { useStreamContext } from 'react-activity-feed'

export default function useFollow({ userId }) {
  const { client } = useStreamContext()

  const [isFollowing, setIsFollowing] = useState(false)

  useEffect(() => {
    async function init() {
      const response = await client
        .feed('timeline', client.userId)
        .following({ filter: [`user:${userId}`] })

      setIsFollowing(!!response.results.length)
    }

    init()
  }, [])

  const toggleFollow = async () => {
    const action = isFollowing ? 'unfollow' : 'follow'

    const timelineFeed = client.feed('timeline', client.userId)
    await timelineFeed[action]('user', userId)

    setIsFollowing((isFollowing) => !isFollowing)
  }

  return { isFollowing, toggleFollow }
}
Enter fullscreen mode Exit fullscreen mode

Add Follow Functionality to the FollowBtn Component

Now, you can add this hook to FollowBtn. Go to src/components/FollowBtn.js, remove the useState import and import the follow hook:

// other imports
import useFollow from '../hooks/useFollow'
Enter fullscreen mode Exit fullscreen mode

Then, replace the useState declaration in the component with the hook and also update the component UI with the values from the hook:

export default function FollowBtn({ userId }) {
  const { isFollowing, toggleFollow } = useFollow({ userId })

  return (
    <Container>
      <button
        className={classNames(isFollowing ? 'following' : 'not-following')}
        onClick={toggleFollow}
      >
        {isFollowing ? (
          <div className="follow-text">
            <span className="follow-text__following">Following</span>
            <span className="follow-text__unfollow">Unfollow</span>
          </div>
        ) : (
          'Follow'
        )}
      </button>
    </Container>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now, you have the follow functionality. You can test it by going to a different user's profile and clicking the follow button:

Twitter Follow Feature

Show Tweets of a User You Follow

When userA follows userB, A should see the tweets of B on A's homepage. Currently, the homepage shows A's tweets (as we concluded in Part 1), so let us fix that.

Go to src/components/Home/Timeline.js. In this component, you will see the Feed component with a feedGroup prop of "user". Change the prop value to "timeline" to show the timeline feed on the homepage. The timeline feed shows activities from different user feeds that the timeline feed follows.

Now, when you go to the homepage of a logged-in user, you should see the tweets made by the users they follow.

To ensure you have the following, I'll use user getstream_io and user iamdillion to show you what to do:

  1. Go to the start page (/), and select user getstream_io
  2. Create two tweets
  3. Go back to the start page and select user iamdillion
  4. Go to user getstream_io's profile, and follow the user
  5. Go to the homepage, and you should see getstream_io's tweets

Show Timeline from homepage

Conclusion

In this tutorial, you have successfully created a profile page, added the follow functionality, and populated the homepage with the tweets of users that the logged-in user follows. What Streamer lacks now is reactions (likes and comments), tweet threads (which show the list of comments made to a tweet), and notifications.

Stay tuned for part three (coming soon) where you learn how to add reactions, threads and a notifications page.

Discussion (0)