[resumes][feat] Add upvote/downvote

pull/389/head
Terence Ho 3 years ago
parent 612bef14ad
commit 2900dbeb1a

@ -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<ICommentInput> = 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 (
<div className="border-primary-300 w-11/12 min-w-fit rounded-md border-2 bg-white p-2 drop-shadow-md">
<div className="flex flex-row space-x-2 p-1 align-top">
@ -154,18 +187,53 @@ export default function ResumeCommentListItem({
{/* Upvote and edit */}
<div className="flex flex-row space-x-1 pt-1 align-middle">
{/* TODO: Implement upvote */}
<ArrowUpCircleIcon className="h-4 w-4 fill-gray-400" />
<button
disabled={
!userId ||
commentUpvoteUpsertMutation.isLoading ||
commentUpvoteDeleteMutation.isLoading
}
type="button"
onClick={() => onVote(Vote.UPVOTE)}>
<ArrowUpCircleIcon
className={clsx(
'h-4 w-4',
comment.userVote?.value === Vote.UPVOTE
? 'fill-indigo-500'
: 'fill-gray-400',
userId && 'hover:fill-indigo-500',
)}
/>
</button>
<div className="text-xs">{comment.numVotes}</div>
<ArrowDownCircleIcon className="h-4 w-4 fill-gray-400" />
<button
disabled={
!userId ||
commentUpvoteUpsertMutation.isLoading ||
commentUpvoteDeleteMutation.isLoading
}
type="button"
onClick={() => onVote(Vote.DOWNVOTE)}>
<ArrowDownCircleIcon
className={clsx(
'h-4 w-4',
comment.userVote?.value === Vote.DOWNVOTE
? 'fill-red-500'
: 'fill-gray-400',
userId && 'hover:fill-red-500',
)}
/>
</button>
{isCommentOwner && !isEditingComment && (
<a
<button
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
href="#"
type="button"
onClick={() => setIsEditingComment(true)}>
Edit
</a>
</button>
)}
</div>
</div>

@ -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)

@ -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;

@ -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 },
},
});
},
});

@ -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;
}>;

Loading…
Cancel
Save