[PART 25] Creating a Twitter clone with GraphQL, Typescript, and React ( user's tweets page )

ipscodingchallenge

ips-coding-challenge

Posted on February 15, 2021

[PART 25] Creating a Twitter clone with GraphQL, Typescript, and React ( user's tweets page )

Hi everyone ;).

As a reminder, I'm doing this Tweeter challenge

Github repository ( Backend )

Github repository ( Frontend )

Db diagram

Profile page

Backend

Having had much less time to work on this challenge, I won't detail everything I did ;). I'll let you go to the Github repository if you need more details. Otherwise, don't hesitate to ask me questions ;).

For tweets retrieval, I created another "endpoint" where I will be able to filter among the user's tweets. I will need to retrieve tweets + retweets, tweets + retweets + comments, user's tweets that contain media, and finally tweets that the user liked.

I created a TweetRepository to separate the code a bit. I should have done that from the beginning but it wasn't really the goal of this challenge (I just wanted to learn and practice graphQL). However, I went for the simplest way. I just added the repository to the context to be able to reuse it in the resolvers. No dependency injection system or anything ;).

src/repositories/TweetRepository

// get the tweets from a particular user
  async tweets(
    userId: number,
    limit: number = 20,
    offset: number = 0,
    filter?: Filters
  ) {
    const qb = this.db('tweets')
    let select = ['tweets.*', ...selectCountsForTweet(this.db)]

    if (
      filter === Filters.TWEETS_RETWEETS ||
      filter === Filters.WITH_COMMENTS
    ) {
      select = [
        ...select,
        this.db.raw(
          'greatest(tweets.created_at, retweets.created_at) as greatest_created_at'
        ),
        this.db.raw(
          '(select rt.tweet_id from retweets rt where rt.tweet_id = tweets.id and rt.user_id = ?) as original_tweet_id',
          [userId]
        ),
      ]
      qb.fullOuterJoin('retweets', 'retweets.tweet_id', '=', 'tweets.id')
      qb.orderBy('greatest_created_at', 'desc')
      qb.orWhere('retweets.user_id', userId)
      qb.orWhere('tweets.user_id', userId)

      if (filter === Filters.TWEETS_RETWEETS) {
        qb.andWhere('type', 'tweet')
      }
    }

    if (filter === Filters.ONLY_MEDIA) {
      qb.innerJoin('medias', 'medias.tweet_id', 'tweets.id')
      qb.where('medias.user_id', userId)
      qb.orderBy('created_at', 'desc')
    }

    if (filter === Filters.ONLY_LIKES) {
      select = [
        ...select,
        this.db.raw(
          'greatest(tweets.created_at, likes.created_at) as greatest_created_at'
        ),
        this.db.raw(
          '(select l.tweet_id from likes l where l.tweet_id = tweets.id and l.user_id = ?) as original_tweet_id',
          [userId]
        ),
      ]
      qb.innerJoin('likes', 'likes.tweet_id', 'tweets.id')
      qb.where('likes.user_id', userId)
      qb.orderBy('greatest_created_at', 'desc')
    }

    return await qb.select(select).limit(limit).offset(offset)
  }
Enter fullscreen mode Exit fullscreen mode

I just create a query builder that I modify according to passed filters to be able to modify the SQL query. It's far from perfect but it does the job ;).

src/resolvers/TweetResolver.ts

@Query(() => [Tweet])
  @Authorized()
  async tweets(
    @Args() { limit, offset, filter }: ArgsFilters,
    @Arg('user_id') user_id: number,
    @Ctx() ctx: MyContext
  ) {
    const {
      repositories: { tweetRepository },
    } = ctx

    const tweets = await tweetRepository.tweets(user_id, limit, offset, filter)

    return tweets
  }
Enter fullscreen mode Exit fullscreen mode

The resolver is therefore quite simple. As for the @Args() property, here it is :

@ArgsType()
class ArgsFilters {
  @Field(() => Int, { nullable: true })
  limit?: number = 20

  @Field(() => Int, { nullable: true })
  offset?: number = 0

  @Field(() => Filters, { nullable: true })
  filter?: Filters = Filters.TWEETS_RETWEETS
}
Enter fullscreen mode Exit fullscreen mode

This is the first time I use the @ArgsType() annotation. Since I haven't handled pagination yet, I would use this class to pass the necessary properties.

Frontend

src/pages/Profile.tsx

import { useLazyQuery, useQuery } from '@apollo/client'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useRecoilState } from 'recoil'
import Layout from '../components/Layout'
import BasicLoader from '../components/loaders/BasicLoader'
import Banner from '../components/profile/Banner'
import UserInfos from '../components/profile/UserInfos'
import Comments from '../components/tweets/Comments'
import Tweet from '../components/tweets/Tweet'
import { TWEETS } from '../graphql/tweets/queries'
import { USER } from '../graphql/users/queries'
import { tweetsState } from '../state/tweetsState'
import { TweetType, UserType } from '../types/types'

const Profile = () => {
  const [tweets, setTweets] = useRecoilState(tweetsState)

  const [user, setUser] = useState<UserType | null>(null)
  const [filter, setFilter] = useState('TWEETS_RETWEETS')

  const params: any = useParams()
  const { data, loading, error } = useQuery(USER, {
    variables: {
      username: params.username,
    },
  })

  const [
    fetchTweets,
    { data: tweetsData, loading: tweetsLoading, error: tweetsError },
  ] = useLazyQuery(TWEETS)

  useEffect(() => {
    if (data) {
      setUser(data.user)
      fetchTweets({
        variables: {
          user_id: data.user.id,
        },
      })
    }
  }, [data])

  useEffect(() => {
    if (tweetsData) {
      setTweets(() => tweetsData.tweets)
    }
  }, [tweetsData])

  useEffect(() => {
    console.log('filter changed')
    if (data && filter) {
      fetchTweets({
        variables: {
          user_id: data.user.id,
          filter,
        },
      })
    }
  }, [filter, data])

  return (
    <Layout>
      {loading && <BasicLoader />}
      {data ? (
        <div>
          {/* Header */}
          {user && (
            <>
              <div className="3xl:max-w-container-lg mx-auto">
                {user.banner ? (
                  <Banner src={user?.banner} alt="Banner" />
                ) : (
                  <div className="h-tweetImage bg-gray-700 w-full"></div>
                )}
              </div>
              <div className="max-w-container-lg px-4 mx-auto">
                <UserInfos user={user!} />
              </div>
            </>
          )}

          {/* Tweets */}
          {tweetsLoading ? (
            <BasicLoader />
          ) : (
            <div className="w-full md:p-4 flex flex-col justify-center items-center overflow-y-auto md:overflow-y-visible">
              {/* Tweet Column */}
              <div className="container max-w-container flex flex-col md:flex-row mx-auto gap-6 p-4 md:p-0 overflow-y-auto">
                {/* Sidebar */}
                <div className="w-full md:w-sidebarWidth">
                  <ul className="bg-white rounded-lg shadow py-4">
                    <li
                      className={`profile_link ${
                        filter === 'TWEETS_RETWEETS' ? 'active' : ''
                      }`}
                      onClick={() => setFilter('TWEETS_RETWEETS')}
                    >
                      Tweets
                    </li>
                    <li
                      className={`profile_link ${
                        filter === 'WITH_COMMENTS' ? 'active' : ''
                      }`}
                      onClick={() => setFilter('WITH_COMMENTS')}
                    >
                      Tweets & Answers
                    </li>
                    <li
                      className={`profile_link ${
                        filter === 'ONLY_MEDIA' ? 'active' : ''
                      }`}
                      onClick={() => setFilter('ONLY_MEDIA')}
                    >
                      Medias
                    </li>
                    <li
                      className={`profile_link ${
                        filter === 'ONLY_LIKES' ? 'active' : ''
                      }`}
                      onClick={() => setFilter('ONLY_LIKES')}
                    >
                      Likes
                    </li>
                  </ul>
                </div>

                <div className="w-full">
                  {/* Tweet Feed */}
                  {tweets && tweets.length === 0 && (
                    <h5 className="text-gray7 text-2xl text-center mt-2">
                      No tweets found ;)
                    </h5>
                  )}
                  {tweets && tweets.length > 0 && (
                    <ul>
                      {tweets.map((t: TweetType, index: number) => {
                        const key = `${t.id}_${index}`
                        if (t.parent !== null) {
                          return <Comments tweet={t} key={key} />
                        } else {
                          return <Tweet key={key} tweet={t} />
                        }
                      })}
                    </ul>
                  )}
                </div>
              </div>
            </div>
          )}
        </div>
      ) : null}
    </Layout>
  )
}

export default Profile

Enter fullscreen mode Exit fullscreen mode

Here I use several useEffect that will react according to the data I receive. First of all, I start by retrieving the user according to the username passed in the URL. Then, I will retrieve the tweets from this user. I also have a useEffect that will listen to the filter change. And I pass the filter as a variable of my GraphQL query.

I'll let you go to Github to get a better overview of the whole (if you're interested). On my side, I started this project to learn GraphQL. I've already learned a lot and started to see the pros and cons of **graphQL* compared to a Rest API. I will try to move forward on my side because I would like to finish this project and writing at the same time takes me a lot more time. I will try to write an article every time I implement a new feature.

Bye and take care! ;)

💖 💪 🙅 🚩
ipscodingchallenge
ips-coding-challenge

Posted on February 15, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related