[PART 23] Create a Twitter clone with GraphQL, Typescript, and React ( followers suggestions)
Hi everyone ;).
As a reminder, I'm doing this Tweeter challenge
Github repository ( Frontend )
Today, we're going to retrieve a list of users to follow that we'll propose to the user.
Here is the final result:
Backend
Not much to do on this side. We'll just add a Query to our FollowerResolver.
src/resolvers/FollowerResolver.ts
@Query(() => [User])
@Authorized()
async followersSuggestions(@Ctx() ctx: MyContext) {
const { db, userId } = ctx
const followersIds = await db('followers')
.where('follower_id', userId)
.pluck('following_id')
const followersSuggestions = await db('users')
.select(
db.raw(
`(SELECT count(id) from followers f WHERE f.following_id = users.id ) as "followersCount"`
),
'users.*'
)
.whereNotIn('id', [...followersIds, userId])
.orderBy('followersCount', 'desc')
// .orderByRaw('random()') // Look for TABLESAMPLE for better performance
.limit(2)
return followersSuggestions
}
I get here the two users who have the most followers and that I am not following already (also excluding the logged-in user).
I also added an optional property "followersCount" to the User entity.
Frontend
I create a new component for the sidebar.
src/components/sidebar/followers/UsersToFollow.tsx
import { useQuery } from '@apollo/client'
import { USERS_TO_FOLLOW } from '../../../graphql/followers/queries'
import { UserType } from '../../../types/types'
import BasicLoader from '../../loaders/BasicLoader'
import { SingleUser } from './SingleUser'
const UsersToFollow = () => {
const { data, loading, error } = useQuery(USERS_TO_FOLLOW)
if (loading) return <BasicLoader />
if (error) return <div>An error occured</div>
return (
<div className="rounded-lg shadow bg-white p-4 mt-4">
<h3 className="mb-1 font-semibold text-gray5">Who to follow</h3>
<hr />
{data?.followersSuggestions.length && (
<ul>
{data?.followersSuggestions.map((user: UserType) => {
return <SingleUser key={user.id} user={user} />
})}
</ul>
)}
</div>
)
}
export default UsersToFollow
And for the GraphQL Query and mutations:
src/graphql/followers/queries
import { gql } from '@apollo/client'
export const USERS_TO_FOLLOW = gql`
query {
followersSuggestions {
id
username
display_name
bio
avatar
banner
followersCount
}
}
`
src/graphql/followers/mutations
import { gql } from '@apollo/client'
export const TOGGLE_FOLLOW = gql`
mutation($following_id: Float!) {
toggleFollow(following_id: $following_id)
}
`
The logic for adding/removing followers had already been set up.
As for the rendering of users to be followed:
src/components/followers/SingleUser.tsx
import { useMutation } from '@apollo/client'
import { useState } from 'react'
import { MdCheck, MdPersonAdd } from 'react-icons/md'
import { TOGGLE_FOLLOW } from '../../../graphql/followers/mutations'
import { UserType } from '../../../types/types'
import { pluralize } from '../../../utils/utils'
import Avatar from '../../Avatar'
import Button from '../../Button'
import MyImage from '../../MyImage'
type SingleUserProps = {
user: UserType
}
export const SingleUser = ({ user }: SingleUserProps) => {
const [followUser] = useMutation(TOGGLE_FOLLOW)
const [following, setFollowing] = useState(false)
const onClick = async () => {
if (following) return false
try {
setFollowing(true)
await followUser({
variables: {
following_id: user.id,
},
})
} catch (e) {
console.log('e', e)
setFollowing(false)
}
}
return (
<div className="my-6 border-b last:border-b-0 pb-6 last:pb-0">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex">
<Avatar className="mr-2" user={user} />
<div>
<p className="">{user.display_name}</p>
<p className="text-xs text-gray7">
{pluralize(user?.followersCount!, 'Follower')}
</p>
</div>
</div>
<Button
onClick={onClick}
text="Follow"
variant={following ? 'success' : 'primary'}
disabled={following}
icon={
following ? (
<MdCheck className="text-white" />
) : (
<MdPersonAdd className="text-white" />
)
}
/>
</div>
{/* Bio */}
{user.bio && <p className="text-gray7">{user.bio}</p>}
{/* Banner */}
{user.banner && (
<MyImage style={{ height: '100px' }} src={user?.banner!} alt="banner" />
)}
</div>
)
}
Don't forget to add the "block" to my "Home" page.
src/pages/Home.tsx
<div className="hidden md:block w-sidebarWidth flex-none">
{/* Hashtags */}
<Hashtags />
{/* Followers Suggestions */}
<UsersToFollow />
</div>
I'm not sure about the behavior when the user follows another user. For the moment I'm not doing anything, but maybe I should re-fetch the feed. I'll see that later ;)
That's all for today :D.
Bye and take care! ;)
You learned 2-3 things and want to buy me a coffee ;)? buymeacoffee.com/ipscoding