[PART 25] Create 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 ( Frontend )
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 tweet 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, users' 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 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)
}
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
}
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
}
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
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! ;)