DEV Community

Cover image for [PART 23] Creating a Twitter clone with GraphQL, Typescript, and React ( followers suggestions)
ips-coding-challenge
ips-coding-challenge

Posted on • Updated on

[PART 23] Creating a Twitter clone with GraphQL, Typescript, and React ( followers suggestions)

Hi everyone ;).

As a reminder, I'm doing this Tweeter challenge

Github repository ( Backend )

Github repository ( Frontend )

Db diagram

Today, we're going to retrieve a list of users to follow that we'll propose to the user.

Here is the final result:

Followers suggestions bloc

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
  }
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`

Enter fullscreen mode Exit fullscreen mode

src/graphql/followers/mutations

import { gql } from '@apollo/client'

export const TOGGLE_FOLLOW = gql`
  mutation($following_id: Float!) {
    toggleFollow(following_id: $following_id)
  }
`

Enter fullscreen mode Exit fullscreen mode

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

I 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>
Enter fullscreen mode Exit fullscreen mode

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 refetch 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 ;)?
https://www.buymeacoffee.com/ipscoding

Top comments (2)

Collapse
 
atulcodex profile image
๐Ÿšฉ Atul Prajapati ๐Ÿ‡ฎ๐Ÿ‡ณ

You are doing great bro :)

Collapse
 
ipscodingchallenge profile image
ips-coding-challenge

Thank you! ;)