Skip to content

Commit

Permalink
feat: likes and dislikes
Browse files Browse the repository at this point in the history
  • Loading branch information
warmachine028 committed Oct 25, 2024
1 parent 5df7380 commit cccf9e0
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 15 deletions.
20 changes: 16 additions & 4 deletions client/src/api/posts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios'
import { PostsResponse, Post } from '@/types'
import { sleep } from '@/lib/utils'

const baseURL = import.meta.env.VITE_API_URL

Expand Down Expand Up @@ -27,6 +28,7 @@ export const getPosts = async (skip: number = 0, limit: number = 10): Promise<Po
// Other API functions remain similar but need to be updated with the new Post interface
export const createPost = async (post: Omit<Post, 'id' | 'imageUrl' | 'views'>): Promise<Post> => {
try {
await sleep(5000)
const { data } = await api.post(`/posts/add`, post)
return {
...data,
Expand Down Expand Up @@ -72,10 +74,7 @@ export const searchPosts = async (query: string): Promise<PostsResponse> => {
imageUrl: `https://picsum.photos/seed/${post.id}/800/600`
}))

return {
...data,
posts: postsWithImages
}
return { ...data, posts: postsWithImages }
} catch (error) {
throw handleApiError(error)
}
Expand All @@ -100,6 +99,19 @@ export const getPostsByUser = async (userId: number): Promise<PostsResponse> =>
}
}

export const updateReaction = async (postId: number, type: 'like' | 'dislike') => {
try {
await sleep(3000)
const { data } = await api.put(`/posts/${postId}`, {
body: JSON.stringify({ reactions: { [type]: +1 } })
})

return data
} catch (error) {
throw handleApiError(error)
}
}

// Error handling helper
const handleApiError = (error: unknown) => {
if (axios.isAxiosError(error)) {
Expand Down
8 changes: 8 additions & 0 deletions client/src/components/CreatePost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const CreatePost = () => {

if (name === 'image') {
setPost({ ...post, image: files?.[0] || null })
} else if (name === 'tags') {
setPost({
...post,
tags: value
.split(',')
.map((tag) => tag.trim())
.join(', ')
})
} else {
setPost({ ...post, [name]: value })
}
Expand Down
25 changes: 18 additions & 7 deletions client/src/components/Post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { useDeletePost, useUpdatePost } from '@/hooks'
import { useDeletePost, useUpdatePost, useUpdateReaction } from '@/hooks'
import { type Post as PostType } from '@/types'

const tagColors = [
Expand All @@ -32,10 +32,13 @@ const Post = ({ post }: { post: PostType }) => {

const updateMutation = useUpdatePost()
const deleteMutation = useDeletePost()
const handleUpdatePost = (post: PostType) => updateMutation.mutate({ ...post, tags: post.tags })
const reactionMutation = useUpdateReaction()

const handleUpdatePost = (post: PostType) => updateMutation.mutate({ ...post, tags: post.tags })
const handleDeletePost = (id: number) => deleteMutation.mutate(id)

const handleReaction = (type: 'like' | 'dislike') => reactionMutation.mutate({ postId: post.id, type })

const handleSave = () => {
handleUpdatePost(editedPost)
setIsEditing(false)
Expand Down Expand Up @@ -103,14 +106,22 @@ const Post = ({ post }: { post: PostType }) => {
</CardContent>
<CardFooter className="bg-muted flex justify-between items-center p-2 flex-shrink-0">
<div className="flex items-center space-x-1">
<Badge variant="secondary" className="flex items-center gap-1">
<Button
variant="secondary"
className="flex items-center gap-1"
onClick={() => handleReaction('like')}
>
<ThumbsUp className="h-4 w-4" />
<span className="hidden xl:block">{post.reactions.likes}</span>
</Badge>
<Badge variant="secondary" className="flex items-center gap-1">
</Button>
<Button
variant="secondary"
className="flex items-center gap-1"
onClick={() => handleReaction('dislike')}
>
<ThumbsDown className="h-4 w-4" />
<span className="hidden xl:block">{post.reactions.dislikes}</span>
</Badge>
</Button>
<Badge variant="secondary" className="flex items-center gap-1">
<Eye className="h-4 w-4" />
<span className="hidden xl:block">{post.views}</span>
Expand Down Expand Up @@ -139,7 +150,7 @@ const Post = ({ post }: { post: PostType }) => {
variant="ghost"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4" />
<Trash2 className="size-4" />
</Button>
</DialogTrigger>
<DialogContent>
Expand Down
59 changes: 55 additions & 4 deletions client/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Post, PostPage } from '@/types'
import { useContext, useEffect, useState } from 'react'
import { ThemeContext } from '@/contexts'
import { createPost, deletePost, getPosts, updatePost } from '@/api'
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { createPost, deletePost, getPosts, searchPosts, updatePost, updateReaction } from '@/api'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useStore } from '@/store'
import { useSearchParams } from 'react-router-dom'
import Fuse from 'fuse.js'
Expand Down Expand Up @@ -150,6 +150,13 @@ export const useDeletePost = () => {
})
}

export const useSearchPosts = () => {
return useQuery({
queryKey: ['searchPosts'],
queryFn: () => searchPosts
})
}

export const useRefresh = () => {
const queryClient = useQueryClient()
const { setOptimisticPages } = useStore()
Expand Down Expand Up @@ -177,7 +184,6 @@ export const useRefresh = () => {
}
}


export const useSearch = (posts: Post[]) => {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') || ''
Expand Down Expand Up @@ -205,4 +211,49 @@ export const useSearch = (posts: Post[]) => {
}

return { query, setQuery, results }
}
}

export const useUpdateReaction = () => {
const queryClient = useQueryClient()
const { optimisticPages, setOptimisticPages } = useStore()

return useMutation({
mutationFn: ({ postId, type }: { postId: number; type: 'like' | 'dislike' }) => updateReaction(postId, type),
onMutate: async ({ postId, type }) => {
await queryClient.cancelQueries({ queryKey: ['posts'] })
const previousData = queryClient.getQueryData(['posts'])

// Update both states with optimistic update
const newPages = optimisticPages.map((page) => ({
...page,
posts: page.posts.map((post) => {
if (post.id === postId) {
return {
...post,
reactions: {
...post.reactions,
likes: post.reactions.likes + (type === 'like' ? 1 : 0),
dislikes: post.reactions.dislikes + (type === 'dislike' ? 1 : 0)
}
}
}
return post
})
}))

queryClient.setQueryData<{ pages: PostPage[]; pageParams: number[] }>(['posts'], (old) => ({
...(old ?? { pageParams: [] }),
pages: newPages
}))
setOptimisticPages(newPages)

return { previousData }
},
onError: (_err, _variables, context) => {
const previousPages = (context?.previousData as { pages: PostPage[] })?.pages ?? []
queryClient.setQueryData(['posts'], context?.previousData)
setOptimisticPages(previousPages)
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] })
})
}

0 comments on commit cccf9e0

Please sign in to comment.