-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
331 additions
and
8 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { NextResponse } from "next/server"; | ||
import * as z from 'zod'; | ||
|
||
import { getAuthSession } from "@/lib/auth"; | ||
import { db } from "@/lib/db"; | ||
import { redis } from "@/lib/redis"; | ||
import { PostVoteValidator } from "@/lib/validators/vote"; | ||
import { CachedPost } from "@/types/redis"; | ||
|
||
const CACHE_AFTER_UPVOTES = 1 | ||
|
||
export async function PATCH(req: Request) { | ||
try { | ||
const body = await req.json() | ||
const { postId, voteType } = PostVoteValidator.parse(body) | ||
const session = await getAuthSession() | ||
|
||
if (!session?.user) { | ||
return new NextResponse('Unauthorized', { status: 401 }) | ||
} | ||
|
||
// check if user has already voted on this post | ||
const existingVote = await db.vote.findFirst({ | ||
where: { | ||
userId: session.user.id, | ||
postId, | ||
}, | ||
}) | ||
|
||
const post = await db.post.findUnique({ | ||
where: { | ||
id: postId, | ||
}, | ||
include: { | ||
author: true, | ||
votes: true, | ||
}, | ||
}) | ||
|
||
if (!post) { | ||
return new NextResponse('Post not found', { status: 404 }) | ||
} | ||
|
||
if (existingVote) { | ||
// if vote type is the same as existing vote, delete the vote | ||
if (existingVote.type === voteType) { | ||
|
||
await db.vote.delete({ | ||
where: { | ||
userId_postId: { | ||
postId, | ||
userId: session.user.id, | ||
}, | ||
}, | ||
}) | ||
|
||
//recount the votes | ||
const votesAmt = post.votes.reduce((acc, vote) => { | ||
if (vote.type === 'UP') return acc + 1 | ||
if (vote.type === 'DOWN') return acc - 1 | ||
return acc | ||
}, 0) | ||
|
||
if (votesAmt > CACHE_AFTER_UPVOTES) { | ||
const cachePayload: CachedPost = { | ||
id: post.id, | ||
authorUsername: post.author.username ?? "", | ||
content: JSON.stringify(post.content), | ||
title: post.title, | ||
currentVote: voteType, | ||
createdAt: post.createdAt | ||
} | ||
await redis.hset(`post:${postId}`, cachePayload) | ||
} | ||
return new NextResponse('OK') | ||
} | ||
|
||
// if vote type is different, update the vote | ||
await db.vote.update({ | ||
where: { | ||
userId_postId: { | ||
postId, | ||
userId: session.user.id, | ||
}, | ||
}, | ||
data: { | ||
type: voteType, | ||
}, | ||
}) | ||
|
||
//recount the votes | ||
const votesAmt = post.votes.reduce((acc, vote) => { | ||
if (vote.type === 'UP') return acc + 1 | ||
if (vote.type === 'DOWN') return acc - 1 | ||
return acc | ||
}, 0) | ||
|
||
if (votesAmt > CACHE_AFTER_UPVOTES) { | ||
const cachePayload: CachedPost = { | ||
id: post.id, | ||
authorUsername: post.author.username ?? "", | ||
content: JSON.stringify(post.content), | ||
title: post.title, | ||
currentVote: voteType, | ||
createdAt: post.createdAt | ||
} | ||
await redis.hset(`post:${postId}`, cachePayload) | ||
} | ||
return new NextResponse('OK') | ||
} | ||
|
||
//if not existing, create a new vote type | ||
await db.vote.create({ | ||
data: { | ||
type: voteType, | ||
userId: session.user.id, | ||
postId, | ||
}, | ||
}) | ||
|
||
// Recount the votes | ||
const votesAmt = post.votes.reduce((acc, vote) => { | ||
if (vote.type === 'UP') return acc + 1 | ||
if (vote.type === 'DOWN') return acc - 1 | ||
return acc | ||
}, 0) | ||
|
||
if (votesAmt >= CACHE_AFTER_UPVOTES) { | ||
const cachePayload: CachedPost = { | ||
authorUsername: post.author.username ?? '', | ||
content: JSON.stringify(post.content), | ||
id: post.id, | ||
title: post.title, | ||
currentVote: voteType, | ||
createdAt: post.createdAt, | ||
} | ||
|
||
await redis.hset(`post:${postId}`, cachePayload) // Store the post data as a hash | ||
} | ||
return new NextResponse('OK') | ||
} catch (error) { | ||
(error) | ||
if (error instanceof z.ZodError) { | ||
return new NextResponse(error.message, { status: 400 }) | ||
} | ||
|
||
return new NextResponse( | ||
'Could not vote at this time. Please try later', | ||
{ status: 500 } | ||
) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
"use client" | ||
|
||
import { usePrevious } from "@/hooks/@mantine-hooks/use-previous"; | ||
import { useCustomToast } from "@/hooks/use-custom-tost"; | ||
import { VoteType } from "@prisma/client"; | ||
import { useEffect, useState } from "react"; | ||
import { Button } from "../ui/Button"; | ||
import { ArrowBigDown, ArrowBigUp } from "lucide-react"; | ||
import { cn } from "@/lib/utils"; | ||
import { useMutation } from "@tanstack/react-query"; | ||
import { PostVoteRequest } from "@/lib/validators/vote"; | ||
import axios, { AxiosError } from "axios"; | ||
import { toast } from "@/hooks/use-toast"; | ||
|
||
interface PostVoteClientProps { | ||
postId: string; | ||
initialVotesAmount: number; | ||
initialVote?: VoteType | null | ||
} | ||
|
||
export function PostVoteClient({ postId, initialVotesAmount, initialVote }: PostVoteClientProps) { | ||
const { logInToast } = useCustomToast() | ||
const [votesAmount, setVotesAmount] = useState(initialVotesAmount) | ||
const [currentVote, setCurrentVote] = useState(initialVote) | ||
const prevVote = usePrevious(currentVote) | ||
|
||
useEffect(() => { | ||
setCurrentVote(initialVote) | ||
}, [initialVote]) | ||
|
||
const { mutate: vote } = useMutation({ | ||
mutationFn: async (voteType: VoteType) => { | ||
const payload: PostVoteRequest = { | ||
postId, | ||
voteType | ||
} | ||
await axios.patch('/api/subreddit/post/vote', payload) | ||
}, | ||
|
||
onError: (err, voteType) => { | ||
if (voteType === 'UP') setVotesAmount((prev) => prev - 1) | ||
else setVotesAmount((prev) => prev + 1) | ||
|
||
// reset current vote | ||
setCurrentVote(prevVote) | ||
|
||
if (err instanceof AxiosError) { | ||
if (err.response?.status === 401) { | ||
return logInToast() | ||
} | ||
} | ||
|
||
return toast({ | ||
title: 'Something went wrong.', | ||
description: 'Your vote was not registered. Please try again.', | ||
variant: 'destructive', | ||
}) | ||
}, | ||
|
||
onMutate: (type: VoteType) => { | ||
if (currentVote === type) { | ||
// User is voting the same way again, so remove their vote | ||
setCurrentVote(undefined) | ||
if (type === 'UP') setVotesAmount((prev) => prev - 1) | ||
else if (type === 'DOWN') setVotesAmount((prev) => prev + 1) | ||
} else { | ||
// User is voting in the opposite direction, so subtract 2 | ||
setCurrentVote(type) | ||
if (type === 'UP') setVotesAmount((prev) => prev + (currentVote ? 2 : 1)) | ||
else if (type === 'DOWN') | ||
setVotesAmount((prev) => prev - (currentVote ? 2 : 1)) | ||
} | ||
}, | ||
}) | ||
|
||
return ( | ||
<div className="flex sm:flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0"> | ||
<Button | ||
onClick={() => vote('UP')} | ||
size={"sm"} variant="ghost" aria-label="upvote"> | ||
<ArrowBigUp className={cn('h-5 w-5 text-zinc-700', { | ||
'text-emerald-500 fill-emerald-500': currentVote == 'UP' | ||
})} /> | ||
</Button> | ||
<p className="text-center py-2 font-medium text-sm text-zinc-900"> {votesAmount} </p> | ||
<Button | ||
onClick={() => vote('DOWN')} | ||
size={"sm"} variant="ghost" aria-label="dowmvote"> | ||
<ArrowBigDown className={cn('h-5 w-5 text-zinc-700', { | ||
'text-red-500 fill-red-500': currentVote == 'DOWN' | ||
})} /> | ||
</Button> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { useEffect, useRef } from 'react'; | ||
|
||
export function usePrevious<T>(value: T): T | undefined { | ||
const ref = useRef<T>(); | ||
|
||
useEffect(() => { | ||
ref.current = value; | ||
}, [value]); | ||
|
||
return ref.current; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Redis } from '@upstash/redis' | ||
|
||
|
||
const redis = new Redis({ | ||
url: process.env.REDIS_URL!, | ||
token: process.env.REDIS_SECRET! | ||
}) | ||
|
||
export { redis } |
Oops, something went wrong.