Skip to content

Commit

Permalink
voting complete
Browse files Browse the repository at this point in the history
  • Loading branch information
imonaar committed Jan 17, 2024
1 parent 387be0f commit 73bb506
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 8 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.17.12",
"@uploadthing/react": "^4.1.3",
"@upstash/redis": "^1.28.1",
"axios": "^1.6.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
Expand Down
153 changes: 153 additions & 0 deletions src/app/api/subreddit/post/vote/route.ts
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 }
)
}
}

6 changes: 5 additions & 1 deletion src/components/post-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"
import axios from 'axios'

import { ExtendedPost } from "@/types/db"
import { useIntersection } from "@/hooks/use-intersection"
import { useIntersection } from "@/hooks/@mantine-hooks/use-intersection"
import { INFINITE_SCROLLING_PAGINATION_RESULTS } from "../../config"
import Post from "./post"

Expand Down Expand Up @@ -73,6 +73,8 @@ export function PostFeed({ initialPosts, subredditName, userId }: PostFeedProps)
subredditName={post.subreddit.name}
post={post}
commentAmt={post.comments.length}
votesAmt={totalVotes}
currentVote={currentVote}
/>
</li>
)
Expand All @@ -83,6 +85,8 @@ export function PostFeed({ initialPosts, subredditName, userId }: PostFeedProps)
subredditName={post.subreddit.name}
post={post}
commentAmt={post.comments.length}
votesAmt={totalVotes}
currentVote={currentVote}
/>
</li>
)
Expand Down
95 changes: 95 additions & 0 deletions src/components/post-vote/post-vote-client.tsx
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>
)
}
24 changes: 17 additions & 7 deletions src/components/post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,32 @@ import { ExtendedPost } from "@/types/db";
import Link from "next/link";
import { MessageSquare } from "lucide-react";
import { EditorOutput } from "./editor-output";
import { PostVoteClient } from "./post-vote/post-vote-client"
import { Vote } from "@prisma/client";

type PartialVote = Pick<Vote, 'type'>

interface PostProps {
subredditName: string,
post: ExtendedPost,
commentAmt: number
subredditName: string;
post: ExtendedPost;
commentAmt: number;
votesAmt: number;
currentVote?: PartialVote
}

const PostPage = ({ subredditName, post, commentAmt }: PostProps) => {
const PostPage = ({ subredditName, post, commentAmt, votesAmt, currentVote }: PostProps) => {
const pRef = useRef<HTMLDivElement>(null)

return (
<div className="rounded-md bg-white shadow">
<div className="px-6 py-4 justify-between">
{/* TODO: post votes */}
<div className="px-6 py-4 flex flex-col sm:flex-row justify-between">
<PostVoteClient
postId={post.id}
initialVotesAmount={votesAmt}
initialVote={currentVote?.type}
/>

<div className="flex-1">
<div className="flex-1 order-first sm:order-last">
<div className="max-h-40 mt-1 text-xs text-gray-500">
{
subredditName ? (
Expand Down
File renamed without changes.
11 changes: 11 additions & 0 deletions src/hooks/@mantine-hooks/use-previous.ts
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;
}
9 changes: 9 additions & 0 deletions src/lib/redis.ts
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 }
Loading

0 comments on commit 73bb506

Please sign in to comment.