From 2900dbeb1ad217fb73471cb054db201f771b2dbd Mon Sep 17 00:00:00 2001 From: Terence Ho <> Date: Wed, 19 Oct 2022 12:09:41 +0800 Subject: [PATCH] [resumes][feat] Add upvote/downvote --- .../comments/ResumeCommentListItem.tsx | 82 +++++++++++++++++-- apps/portal/src/server/router/index.ts | 2 + .../router/resumes/resumes-comments-router.ts | 25 +++--- .../resumes-comments-upvotes-user-router.ts | 49 +++++++++++ apps/portal/src/types/resume-comments.d.ts | 4 +- 5 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 apps/portal/src/server/router/resumes/resumes-comments-upvotes-user-router.ts diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx index a313a228..1649c52f 100644 --- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx +++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import { useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -6,6 +7,7 @@ import { ArrowUpCircleIcon, } from '@heroicons/react/20/solid'; import { FaceSmileIcon } from '@heroicons/react/24/outline'; +import { Vote } from '@prisma/client'; import { Button, TextArea } from '@tih/ui'; import { trpc } from '~/utils/trpc'; @@ -52,6 +54,24 @@ export default function ResumeCommentListItem({ }, }, ); + const commentUpvoteUpsertMutation = trpc.useMutation( + 'resumes.comments.upvotes.user.upsert', + { + onSuccess: () => { + // Comment updated, invalidate query to trigger refetch + trpcContext.invalidateQueries(['resumes.comments.list']); + }, + }, + ); + const commentUpvoteDeleteMutation = trpc.useMutation( + 'resumes.comments.upvotes.user.delete', + { + onSuccess: () => { + // Comment updated, invalidate query to trigger refetch + trpcContext.invalidateQueries(['resumes.comments.list']); + }, + }, + ); const onCancel = () => { reset({ description: comment.description }); @@ -60,7 +80,7 @@ export default function ResumeCommentListItem({ const onSubmit: SubmitHandler = async (data) => { const { id } = comment; - return await commentUpdateMutation.mutate( + return commentUpdateMutation.mutate( { id, ...data, @@ -77,6 +97,19 @@ export default function ResumeCommentListItem({ setValue('description', value.trim(), { shouldDirty: true }); }; + const onVote = async (value: Vote) => { + if (comment.userVote?.value === value) { + return commentUpvoteDeleteMutation.mutate({ + commentId: comment.id, + }); + } + return commentUpvoteUpsertMutation.mutate({ + commentId: comment.id, + id: comment.userVote?.id, + value, + }); + }; + return (
@@ -154,18 +187,53 @@ export default function ResumeCommentListItem({ {/* Upvote and edit */}
- {/* TODO: Implement upvote */} - + +
{comment.numVotes}
- + + {isCommentOwner && !isEditingComment && ( - setIsEditingComment(true)}> Edit - + )}
diff --git a/apps/portal/src/server/router/index.ts b/apps/portal/src/server/router/index.ts index 8592986c..12d1bf6b 100644 --- a/apps/portal/src/server/router/index.ts +++ b/apps/portal/src/server/router/index.ts @@ -12,6 +12,7 @@ import { questionsAnswerRouter } from './questions-answer-router'; import { questionsQuestionCommentRouter } from './questions-question-comment-router'; import { questionsQuestionRouter } from './questions-question-router'; import { resumeCommentsRouter } from './resumes/resumes-comments-router'; +import { resumesCommentsUpvotesUserRouter } from './resumes/resumes-comments-upvotes-user-router'; import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router'; import { resumesRouter } from './resumes/resumes-resume-router'; import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router'; @@ -33,6 +34,7 @@ export const appRouter = createRouter() .merge('resumes.resume.', resumesStarUserRouter) .merge('resumes.comments.', resumeCommentsRouter) .merge('resumes.comments.user.', resumesCommentsUserRouter) + .merge('resumes.comments.upvotes.user.', resumesCommentsUpvotesUserRouter) .merge('questions.answers.comments.', questionsAnswerCommentRouter) .merge('questions.answers.', questionsAnswerRouter) .merge('questions.questions.comments.', questionsQuestionCommentRouter) diff --git a/apps/portal/src/server/router/resumes/resumes-comments-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-router.ts index fb9840ac..f663ad75 100644 --- a/apps/portal/src/server/router/resumes/resumes-comments-router.ts +++ b/apps/portal/src/server/router/resumes/resumes-comments-router.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import type { ResumesCommentVote } from '@prisma/client'; +import { Vote } from '@prisma/client'; import { createRouter } from '../context'; @@ -17,23 +19,13 @@ export const resumeCommentsRouter = createRouter().query('list', { // Number of votes, and whether the user (if-any) has voted const comments = await ctx.prisma.resumesComment.findMany({ include: { - _count: { - select: { - votes: true, - }, - }, user: { select: { image: true, name: true, }, }, - votes: { - take: 1, - where: { - userId, - }, - }, + votes: true, }, orderBy: { createdAt: 'desc', @@ -44,13 +36,17 @@ export const resumeCommentsRouter = createRouter().query('list', { }); return comments.map((data) => { - const hasVoted = data.votes.length > 0; - const numVotes = data._count.votes; + let userVote: ResumesCommentVote | undefined = undefined; + let numVotes = 0; + + data.votes.forEach((vote) => { + numVotes += vote.value === Vote.UPVOTE ? 1 : -1; + userVote = vote.userId === userId ? vote : undefined; + }); const comment: ResumeComment = { createdAt: data.createdAt, description: data.description, - hasVoted, id: data.id, numVotes, resumeId: data.resumeId, @@ -61,6 +57,7 @@ export const resumeCommentsRouter = createRouter().query('list', { name: data.user.name, userId: data.userId, }, + userVote, }; return comment; diff --git a/apps/portal/src/server/router/resumes/resumes-comments-upvotes-user-router.ts b/apps/portal/src/server/router/resumes/resumes-comments-upvotes-user-router.ts new file mode 100644 index 00000000..16167627 --- /dev/null +++ b/apps/portal/src/server/router/resumes/resumes-comments-upvotes-user-router.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; +import { Vote } from '@prisma/client'; + +import { createProtectedRouter } from '../context'; + +export const resumesCommentsUpvotesUserRouter = createProtectedRouter() + .mutation('upsert', { + input: z.object({ + commentId: z.string(), + id: z.string().optional(), + value: z.nativeEnum(Vote), + }), + async resolve({ ctx, input }) { + const userId = ctx.session.user.id; + const { id, commentId, value } = input; + + await ctx.prisma.resumesCommentVote.upsert({ + create: { + commentId, + userId, + value, + }, + update: { + commentId, + id, + userId, + value, + }, + where: { + userId_commentId: { commentId, userId }, + }, + }); + }, + }) + .mutation('delete', { + input: z.object({ + commentId: z.string(), + }), + async resolve({ ctx, input }) { + const userId = ctx.session.user.id; + const { commentId } = input; + + await ctx.prisma.resumesCommentVote.delete({ + where: { + userId_commentId: { commentId, userId }, + }, + }); + }, + }); diff --git a/apps/portal/src/types/resume-comments.d.ts b/apps/portal/src/types/resume-comments.d.ts index c0e181fb..d477fe2c 100644 --- a/apps/portal/src/types/resume-comments.d.ts +++ b/apps/portal/src/types/resume-comments.d.ts @@ -1,4 +1,4 @@ -import type { ResumesSection } from '@prisma/client'; +import type { ResumesCommentVote, ResumesSection } from '@prisma/client'; /** * Returned by `resumeCommentsRouter` (query for 'resumes.comments.list') and received as prop by `Comment` in `CommentsList` @@ -7,7 +7,6 @@ import type { ResumesSection } from '@prisma/client'; export type ResumeComment = Readonly<{ createdAt: Date; description: string; - hasVoted: boolean; id: string; numVotes: number; resumeId: string; @@ -18,4 +17,5 @@ export type ResumeComment = Readonly<{ name: string?; userId: string; }; + userVote: ResumesCommentVote | undefined; }>;