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.
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:
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;
}
}
`
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)
}
}
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>
)
}
Create the ProfileBio Component
This component holds the user's information and the follow button:
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;
}
}
}
`
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
}
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>
)
}
Create the TabList Component
The TabList component shows the "Tweets", "Tweets & Replies", "Media" and "Likes" tabs:
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>
)
}
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>
)
}
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;
}
`
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 />
}
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>
)
}
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>
)
}
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'
And the route:
<Route element={<Profile />} path="/:user_id" />
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)
}
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()
}, [])
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)
}
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 }
}
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'
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>
)
}
Now, you have the follow functionality. You can test it by going to a different user's profile and clicking the follow button:
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:
- Go to the start page (/), and select user getstream_io
- Create two tweets
- Go back to the start page and select user iamdillion
- Go to user getstream_io's profile, and follow the user
- Go to the homepage, and you should see getstream_io's tweets
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.
Top comments (0)