Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 64 additions & 73 deletions src/components/post-vote/PostVoteClient.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,110 @@
'use client'
"use client";

import { useCustomToasts } from '@/hooks/use-custom-toasts'
import { PostVoteRequest } from '@/lib/validators/vote'
import { usePrevious } from '@mantine/hooks'
import { VoteType } from '@prisma/client'
import { useMutation } from '@tanstack/react-query'
import axios, { AxiosError } from 'axios'
import { useEffect, useState } from 'react'
import { toast } from '../../hooks/use-toast'
import { Button } from '../ui/Button'
import { ArrowBigDown, ArrowBigUp } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useCustomToasts } from "@/hooks/use-custom-toasts";
import { usePrevious } from "@mantine/hooks";
import { VoteType } from "@prisma/client";
import { useEffect, useState } from "react";
import { Button } from "../ui/Button";
import { cn } from "@/lib/utils";
import { ArrowBigUp, ArrowBigDown } from "lucide-react";
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
initialVotesAmt: number
initialVote?: VoteType | null
}
type Props = {
postId: string;
initialVotesAmt: number;
initialVote?: VoteType | null;
};

const PostVoteClient = ({
postId,
initialVotesAmt,
initialVote,
}: PostVoteClientProps) => {
const { loginToast } = useCustomToasts()
const [votesAmt, setVotesAmt] = useState<number>(initialVotesAmt)
const [currentVote, setCurrentVote] = useState(initialVote)
const prevVote = usePrevious(currentVote)
function PostVoteClient({ initialVotesAmt, postId, initialVote }: Props) {
const { loginToast } = useCustomToasts();
const [votesAmt, setVotesAmt] = useState(initialVotesAmt);
const [currentVote, setCurrentVote] = useState(initialVote);
const prevVote = usePrevious(currentVote);
const prevVotesAmt = usePrevious(votesAmt);

// ensure sync with server
useEffect(() => {
setCurrentVote(initialVote)
}, [initialVote])
setCurrentVote(initialVote);
}, [initialVote]);

const { mutate: vote } = useMutation({
mutationFn: async (type: VoteType) => {
mutationFn: async (voteType: VoteType) => {
const payload: PostVoteRequest = {
voteType: type,
postId: postId,
}
postId,
voteType,
};

await axios.patch('/api/subreddit/post/vote', payload)
await axios.patch("/api/subreddit/post/vote", payload);
},
onError: (err, voteType) => {
if (voteType === 'UP') setVotesAmt((prev) => prev - 1)
else setVotesAmt((prev) => prev + 1)

// reset current vote
setCurrentVote(prevVote)
onError: (err, type) => {
setVotesAmt(prevVotesAmt!);
setCurrentVote(prevVote);

if (err instanceof AxiosError) {
if (err.response?.status === 401) {
return loginToast()
return loginToast();
}
}

return toast({
title: 'Something went wrong.',
description: 'Your vote was not registered. Please try again.',
variant: 'destructive',
})
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') setVotesAmt((prev) => prev - 1)
else if (type === 'DOWN') setVotesAmt((prev) => prev + 1)
setCurrentVote(undefined);
if (type === "UP") setVotesAmt((prev) => prev - 1);
else if (type === "DOWN") setVotesAmt((prev) => prev + 1);
} else {
// User is voting in the opposite direction, so subtract 2
setCurrentVote(type)
if (type === 'UP') setVotesAmt((prev) => prev + (currentVote ? 2 : 1))
else if (type === 'DOWN')
setVotesAmt((prev) => prev - (currentVote ? 2 : 1))
setCurrentVote(type);
if (type === "UP") setVotesAmt((prev) => prev + (currentVote ? 2 : 1));
else setVotesAmt((prev) => prev - (currentVote ? 2 : 1));
}
},
})
});

return (
<div className='flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0'>
<div className="flex flex-col gap-4 sm:gap-0 pr-6 sm:w-20 pb-4 sm:pb-0">
{/* upvote */}
<Button
onClick={() => vote('UP')}
size='sm'
variant='ghost'
aria-label='upvote'>
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',
className={cn("h-5 w-5 text-zinc-700", {
"text-emerald-500 fill-emerald-500": currentVote === "UP",
})}
/>
</Button>

{/* score */}
<p className='text-center py-2 font-medium text-sm text-zinc-900'>
<p className="text-center py-2 font-medium text-sm text-zinc-900">
{votesAmt}
</p>

{/* downvote */}
<Button
onClick={() => vote('DOWN')}
size='sm'
onClick={() => vote("DOWN")}
size="sm"
className={cn({
'text-emerald-500': currentVote === 'DOWN',
"text-emerald-500": currentVote === "DOWN",
})}
variant='ghost'
aria-label='downvote'>
variant="ghost"
aria-label="downvote"
>
<ArrowBigDown
className={cn('h-5 w-5 text-zinc-700', {
'text-red-500 fill-red-500': currentVote === 'DOWN',
className={cn("h-5 w-5 text-zinc-700", {
"text-red-500 fill-red-500": currentVote === "DOWN",
})}
/>
</Button>
</div>
)
);
}

export default PostVoteClient
export default PostVoteClient;