From d9880dbff1c0163a3b7d55a4700a1ec6c88cfa7b Mon Sep 17 00:00:00 2001 From: Terence <45381509+Vielheim@users.noreply.github.com> Date: Sat, 8 Oct 2022 00:42:27 +0800 Subject: [PATCH] [resumes][feat] fetch comments from database (#320) * [resumes][feat] Add resume-comments type * [resumes][feat] Add resume-comments type * [resumes][feat] Filter comments * [resumes][feat] Add comments render * [resumes][refactor] rename variables * [resumes][refactor] update invalidateQueries * [resumes][refactor] Use resumeId in [resumeId].tsx * [resumes][fix] fix invalidateQuery Co-authored-by: Terence Ho <> --- .../resumes/comments/CommentsForm.tsx | 28 +++++--- .../resumes/comments/CommentsList.tsx | 26 ++++++-- .../resumes/comments/comment/Comment.tsx | 18 ++++++ .../resumes/comments/comment/CommentBody.tsx | 64 +++++++++++++++++++ .../resumes/comments/comment/CommentCard.tsx | 22 +++++++ apps/portal/src/pages/resumes/[resumeId].tsx | 3 +- .../server/router/resumes-reviews-router.ts | 32 +++++++++- apps/portal/src/types/resume-comments.d.ts | 21 ++++++ 8 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 apps/portal/src/components/resumes/comments/comment/Comment.tsx create mode 100644 apps/portal/src/components/resumes/comments/comment/CommentBody.tsx create mode 100644 apps/portal/src/components/resumes/comments/comment/CommentCard.tsx create mode 100644 apps/portal/src/types/resume-comments.d.ts diff --git a/apps/portal/src/components/resumes/comments/CommentsForm.tsx b/apps/portal/src/components/resumes/comments/CommentsForm.tsx index 98ff8d82..c7b7dc6a 100644 --- a/apps/portal/src/components/resumes/comments/CommentsForm.tsx +++ b/apps/portal/src/components/resumes/comments/CommentsForm.tsx @@ -39,17 +39,29 @@ export default function CommentsForm({ skills: '', }, }); - const reviewCreateMutation = trpc.useMutation('resumes.reviews.user.create'); + + const trpcContext = trpc.useContext(); + const reviewCreateMutation = trpc.useMutation('resumes.reviews.user.create', { + onSuccess: () => { + // New review added, invalidate query to trigger refetch + trpcContext.invalidateQueries(['resumes.reviews.list']); + }, + }); // TODO: Give a feedback to the user if the action succeeds/fails const onSubmit: SubmitHandler = async (data) => { - await reviewCreateMutation.mutate({ - resumeId, - ...data, - }); - - // Redirect back to comments section - setShowCommentsForm(false); + return await reviewCreateMutation.mutate( + { + resumeId, + ...data, + }, + { + onSuccess: () => { + // Redirect back to comments section + setShowCommentsForm(false); + }, + }, + ); }; const onCancel = () => { diff --git a/apps/portal/src/components/resumes/comments/CommentsList.tsx b/apps/portal/src/components/resumes/comments/CommentsList.tsx index 0b1d2d35..54843bca 100644 --- a/apps/portal/src/components/resumes/comments/CommentsList.tsx +++ b/apps/portal/src/components/resumes/comments/CommentsList.tsx @@ -1,8 +1,10 @@ +import { useSession } from 'next-auth/react'; import { useState } from 'react'; import { Tabs } from '@tih/ui'; import { trpc } from '~/utils/trpc'; +import Comment from './comment/Comment'; import CommentsListButton from './CommentsListButton'; import { COMMENTS_SECTIONS } from './constants'; @@ -16,12 +18,15 @@ export default function CommentsList({ setShowCommentsForm, }: CommentsListProps) { const [tab, setTab] = useState(COMMENTS_SECTIONS[0].value); + const { data: session } = useSession(); - const commentsQuery = trpc.useQuery(['resumes.reviews.list', { resumeId }]); + // Fetch the most updated comments to render + const commentsQuery = trpc.useQuery([ + 'resumes.reviews.list', + { resumeId, section: tab }, + ]); - /* eslint-disable no-console */ - console.log(commentsQuery.data); - /* eslint-enable no-console */ + // TODO: Add loading prompt return (
@@ -32,7 +37,18 @@ export default function CommentsList({ value={tab} onChange={(value) => setTab(value)} /> - {/* TODO: Add comments lists */} + +
+ {commentsQuery.data?.map((comment) => { + return ( + + ); + })} +
); } diff --git a/apps/portal/src/components/resumes/comments/comment/Comment.tsx b/apps/portal/src/components/resumes/comments/comment/Comment.tsx new file mode 100644 index 00000000..cb08480b --- /dev/null +++ b/apps/portal/src/components/resumes/comments/comment/Comment.tsx @@ -0,0 +1,18 @@ +import CommentBody from './CommentBody'; +import CommentCard from './CommentCard'; + +import type { ResumeComment } from '~/types/resume-comments'; + +type CommentProps = { + comment: ResumeComment; + userId?: string; +}; + +export default function Comment({ comment, userId }: CommentProps) { + const isCommentOwner = userId === comment.user.userId; + return ( + + + + ); +} diff --git a/apps/portal/src/components/resumes/comments/comment/CommentBody.tsx b/apps/portal/src/components/resumes/comments/comment/CommentBody.tsx new file mode 100644 index 00000000..69da7418 --- /dev/null +++ b/apps/portal/src/components/resumes/comments/comment/CommentBody.tsx @@ -0,0 +1,64 @@ +import { + ArrowDownCircleIcon, + ArrowUpCircleIcon, +} from '@heroicons/react/20/solid'; +import { FaceSmileIcon } from '@heroicons/react/24/outline'; + +import type { ResumeComment } from '~/types/resume-comments'; + +type CommentBodyProps = { + comment: ResumeComment; + isCommentOwner?: boolean; +}; + +export default function CommentBody({ + comment, + isCommentOwner, +}: CommentBodyProps) { + return ( +
+ {comment.user.image ? ( + {comment.user.name + ) : ( + + )} + +
+ {/* Name and creation time */} +
+
+ {comment.user.name ?? 'Reviewer ABC'} +
+
+ {comment.createdAt.toLocaleString('en-US', { + dateStyle: 'medium', + timeStyle: 'short', + })} +
+
+ + {/* Description */} +
{comment.description}
+ + {/* Upvote and edit */} +
+ {/* TODO: Implement upvote */} + +
{comment.numVotes}
+ + + {/* TODO: Implement edit */} + {isCommentOwner ? ( +
+ Edit +
+ ) : null} +
+
+
+ ); +} diff --git a/apps/portal/src/components/resumes/comments/comment/CommentCard.tsx b/apps/portal/src/components/resumes/comments/comment/CommentCard.tsx new file mode 100644 index 00000000..bbe0f840 --- /dev/null +++ b/apps/portal/src/components/resumes/comments/comment/CommentCard.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react'; + +type CommentCardProps = { + children: ReactNode; + isCommentOwner?: boolean; +}; + +export default function CommentCard({ + isCommentOwner, + children, +}: CommentCardProps) { + // Used two different
to allow customisation of owner comments + return isCommentOwner ? ( +
+ {children} +
+ ) : ( +
+ {children} +
+ ); +} diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx index 5a13a1c3..7103655b 100644 --- a/apps/portal/src/pages/resumes/[resumeId].tsx +++ b/apps/portal/src/pages/resumes/[resumeId].tsx @@ -137,8 +137,7 @@ export default function ResumeReviewPage() {
- {/* TODO: Update resumeId */} - +
diff --git a/apps/portal/src/server/router/resumes-reviews-router.ts b/apps/portal/src/server/router/resumes-reviews-router.ts index 8e681326..8219edce 100644 --- a/apps/portal/src/server/router/resumes-reviews-router.ts +++ b/apps/portal/src/server/router/resumes-reviews-router.ts @@ -1,19 +1,23 @@ import { z } from 'zod'; +import { ResumesSection } from '@prisma/client'; import { createRouter } from './context'; +import type { ResumeComment } from '~/types/resume-comments'; + export const resumeReviewsRouter = createRouter().query('list', { input: z.object({ resumeId: z.string(), + section: z.nativeEnum(ResumesSection), }), async resolve({ ctx, input }) { const userId = ctx.session?.user?.id; - const { resumeId } = input; + const { resumeId, section } = input; // For this resume, we retrieve every comment's information, along with: // The user's name and image to render // Number of votes, and whether the user (if-any) has voted - return await ctx.prisma.resumesComment.findMany({ + const comments = await ctx.prisma.resumesComment.findMany({ include: { _count: { select: { @@ -38,7 +42,31 @@ export const resumeReviewsRouter = createRouter().query('list', { }, where: { resumeId, + section, }, }); + + return comments.map((data) => { + const hasVoted = data.votes.length > 0; + const numVotes = data._count.votes; + + const comment: ResumeComment = { + createdAt: data.createdAt, + description: data.description, + hasVoted, + id: data.id, + numVotes, + resumeId: data.resumeId, + section: data.section, + updatedAt: data.updatedAt, + user: { + image: data.user.image, + name: data.user.name, + userId: data.userId, + }, + }; + + return comment; + }); }, }); diff --git a/apps/portal/src/types/resume-comments.d.ts b/apps/portal/src/types/resume-comments.d.ts new file mode 100644 index 00000000..5a6dfff8 --- /dev/null +++ b/apps/portal/src/types/resume-comments.d.ts @@ -0,0 +1,21 @@ +import type { ResumesSection } from '@prisma/client'; + +/** + * Returned by `resumeReviewsRouter` (query for 'resumes.reviews.list') and received as prop by `Comment` in `CommentsList` + * frontend-friendly representation of the query + */ +export type ResumeComment = { + createdAt: Date; + description: string; + hasVoted: boolean; + id: string; + numVotes: number; + resumeId: string; + section: ResumesSection; + updatedAt: Date; + user: { + image: string?; + name: string?; + userId: string; + }; +};