Hi everyone ;).
As a reminder, I'm doing this Tweeter challenge
Github repository ( Frontend )
Bookmarks
For the favorites, I'll let you take a look at the Github Repository since it's exactly the same logic as for the "likes".
Comments
For the comments, there's not going to be much to do either. We already have the logic on the backend side. All that remains is to modify a little bit our TweetForm.
type TweetFormProps = {
tweet_id?: number
type?: TweetTypeEnum
onSuccess?: Function
}
export enum TweetTypeEnum {
TWEET = 'tweet',
COMMENT = 'comment',
}
const TweetForm = ({ tweet_id, type, onSuccess }: TweetFormProps) => {
// Global state
const user = useRecoilValue(userState)
const setTweets = useSetRecoilState(tweetsState)
// Local state
const [body, setBody] = useState('')
const [addTweetMutation, { data }] = useMutation(ADD_TWEET)
// I create a local state for loading instead of using the apollo loading
// It's because of the urlShortener function.
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<ValidationError | null>(null)
const [serverErrors, setServerErrors] = useState<any[]>([])
const addTweet = async () => {
setErrors(null)
setServerErrors([])
setLoading(true)
// extract info from the tweet body ( urls, hashtags for now)
const { hashtags, urls } = await extractMetadata(body)
// Shorten the urls
let shortenedURLS: any
let newBody = body.slice() /* make a copy of the body */
if (urls && urls.length > 0) {
// Shorten the url via tinyURL
// Not ideal but ok for now as I didn't create my own service to shorten the url
// and I don't think I will create one ;)
shortenedURLS = await shortenURLS(urls)
shortenedURLS.forEach((el: any) => {
// Need to escape characters for the regex to work
const pattern = el.original.replace(/[^a-zA-Z0-9]/g, '\\$&')
newBody = newBody.replace(new RegExp(pattern), el.shorten)
})
}
try {
// Honestly, I should not validate against hashtags and shortenedURLS as
// it's an "intern" thing. I let it for now mostly for development purposes.
await addTweetSchema.validate({
body,
hashtags,
shortenedURLS,
})
const payload: any = {
body: newBody ?? body,
hashtags,
url: shortenedURLS ? shortenedURLS[0].shorten : null,
}
if (type) {
payload.type = type
}
if (tweet_id) {
payload.parent_id = tweet_id
}
await addTweetMutation({
variables: {
payload,
},
})
if (onSuccess) {
onSuccess()
}
} catch (e) {
if (e instanceof ValidationError) {
setErrors(e)
} else if (e instanceof ApolloError) {
setServerErrors(handleErrors(e))
}
console.log('e', e)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (data) {
setTweets((old) => {
return [data.addTweet].concat(old)
})
setBody('')
}
}, [data])
const commentHeader = () => {
return (
<>
<span>In response to </span>
<Link to="/" className="text-primary hover:text-primary_hover">
@{user!.username}
</Link>
</>
)
}
return (
<div
className={`mb-4 p-4 w-full rounded-lg shadow bg-white ${
type === TweetTypeEnum.COMMENT ? 'mt-4 border border-primary' : ''
}`}
>
{serverErrors.length > 0 && (
<div className="mb-4">
{serverErrors.map((e: any, index: number) => {
return (
<Alert
key={index}
variant="danger"
message={Array.isArray(e) ? e[0].message : e.message}
/>
)
})}
</div>
)}
<h3 className={type === TweetTypeEnum.COMMENT ? 'text-sm' : ''}>
{type === TweetTypeEnum.COMMENT ? commentHeader() : 'Tweet something'}
</h3>
<hr className="my-2" />
<div className="flex w-full">
<Avatar className="mr-2" display_name={user!.display_name} />
<div className="w-full">
<div className="w-full mb-2">
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
className="w-full placeholder-gray4 p-2 "
placeholder="What's happening"
></textarea>
{errors && errors.path === 'body' && (
<span className="text-red-500 text-sm">{errors.message}</span>
)}
</div>
{/* Actions */}
<div className="flex justify-between">
<div className="flex items-center">
<MdImage className="text-primary mr-2" />
<div className="text-primary inline-flex items-center">
<MdPublic className="mr-1" />
<span className="text-xs">Everyone can reply</span>
</div>
</div>
<Button
text={type === TweetTypeEnum.COMMENT ? 'Comment' : 'Tweet'}
variant="primary"
onClick={addTweet}
disabled={loading}
loading={loading}
/>
</div>
</div>
</div>
</div>
)
}
export default TweetForm
To add a comment, I need the id of the parent tweet, the type and I also pass a function that will let me know when the addition is finished. I could, with this function, hide the form for example.
It is in my Tweet component that I will show/hide the TweetForm in the case of a comment.
src/components/tweets/Tweet.tsx
const [showCommentForm, setShowCommentForm] = useState(false)
I create a local state and also a function to toggle the status of the form:
const toggleCommentForm = (e: any) => {
setShowCommentForm((old) => (old = !old))
}
I just have to use this function on my comment button:
<Button
text="Comment"
variant="default"
className="text-lg md:text-sm"
icon={<MdModeComment />}
alignment="left"
hideTextOnMobile={true}
onClick={toggleCommentForm}
/>
And just below, I display the TweetForm.
{showCommentForm && (
<TweetForm
type={TweetTypeEnum.COMMENT}
tweet_id={tweet.id}
onSuccess={() => setShowCommentForm(false)}
/>
)}
That's what it looks like:
Trending Hashtags [Backend]
I start with the Hashtag entity
src/entities/Hashtag.ts
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
class Hashtag {
@Field()
id: number
@Field()
hashtag: string
@Field({ nullable: true })
tweetsCount?: number
}
export default Hashtag
And then I create the resolver to fetch the hashtags
src/resolvers/HashtagResolver.ts
import { Ctx, Query, Resolver } from 'type-graphql'
import Hashtag from '../entities/Hashtag'
import { MyContext } from '../types/types'
@Resolver()
class HashtagResolver {
@Query(() => [Hashtag])
async trendingHashtags(@Ctx() ctx: MyContext) {
const { db } = ctx
const hashtags = await db({ h: 'hashtags' })
.distinct('h.hashtag', 'h.id')
.select(
db.raw(
'(SELECT count(hashtags_tweets.hashtag_id) from hashtags_tweets WHERE hashtags_tweets.hashtag_id = h.id) as "tweetsCount"'
)
)
.innerJoin('hashtags_tweets as ht', 'h.id', '=', 'ht.hashtag_id')
.whereRaw(`ht.created_at > NOW() - interval '7 days'`)
.groupBy('h.id', 'ht.created_at')
.orderBy('tweetsCount', 'desc')
.limit(10)
return hashtags
}
}
export default HashtagResolver
I retrieve the most popular hashtags over the last 7 days.
I don't forget to add the resolver to the server.
src/server.ts
export const schema = async () => {
return await buildSchema({
resolvers: [
AuthResolver,
TweetResolver,
LikeResolver,
FollowerResolver,
RetweetResolver,
BookmarkResolver,
HashtagResolver,
],
authChecker: authChecker,
})
}
And this is what I get when I launch my request:
I now have everything I need to make the sidebar on the front end.
Trending Hashtags [Frontend]
I start by creating the component Hashtags.tsx in a sub-directory sidebars.
src/components/sidebars/Hashtags.tsx
import { useQuery } from '@apollo/client'
import { Link } from 'react-router-dom'
import { HASHTAGS } from '../../graphql/hashtags/queries'
import { HashtagType } from '../../types/types'
import { pluralize } from '../../utils/utils'
import BasicLoader from '../loaders/BasicLoader'
const Hashtags = () => {
const { data, loading, error } = useQuery(HASHTAGS)
if (loading) return <BasicLoader />
if (error) return <div>Error loading the hashtags</div>
return (
<div className="rounded-lg shadow bg-white p-4">
<h3 className="mb-1 font-semibold text-gray5">Trends</h3>
<hr />
{data && data.trendingHashtags ? (
<ul className="mt-4">
{data.trendingHashtags.map((h: HashtagType) => {
return (
<li className="mb-4 text-noto">
<Link
to={`/hashtags/${h.hashtag.replace('#', '')}`}
className="font-semibold text-gray8 mb-3 hover:text-gray-500 transition-colors duration-300"
>
{h.hashtag}
</Link>
<p className="text-gray7 text-xs">
{pluralize(h.tweetsCount!, 'Tweet')}
</p>
</li>
)
})}
</ul>
) : null}
</div>
)
}
export default Hashtags
Nothing special here. I do my graphql query and once I have the data, I make a loop and display the hashtags.
src/graphql/hashtags/queries.ts
import { gql } from '@apollo/client'
export const HASHTAGS = gql`
query {
trendingHashtags {
id
hashtag
tweetsCount
}
}
`
And in my Home page I replace the placeholder:
src/pages/Home.tsx
{/* Hashtags */}
<div className="hidden md:block w-sidebarWidth flex-none">
<Hashtags />
</div>
That's all for today ;).
Bye and take care ;)
Top comments (0)