[resumes][fix] Fix bugs in comments section (#363)

* [resumes][fix] left-align all comments

* [resumes][fix] add comment owner OP tag

* [resumes][fix] render multi-line text in comments

* [resumes][feat] add see more/less for overflow comments

* [resumes][refactor] prefix comments section with Resume

* [resumes][refactor] Refactor routers from reviews -> comments

* [resumes][refactor] use Vote enum in ResumesCommentVote

* [resumes][refactor] add comment count to tabs

* [resumes][refactor] combine resume-card and resume-body into resume-list-item

Co-authored-by: Terence Ho <>
This commit is contained in:
Terence
2022-10-13 11:57:45 +08:00
committed by GitHub
parent ffd7539179
commit fd67a20a2b
18 changed files with 265 additions and 233 deletions

View File

@ -0,0 +1,9 @@
/*
Warnings:
- Changed the type of `value` on the `ResumesCommentVote` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- AlterTable
ALTER TABLE "ResumesCommentVote" DROP COLUMN "value",
ADD COLUMN "value" "Vote" NOT NULL;

View File

@ -159,7 +159,7 @@ model ResumesCommentVote {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
commentId String commentId String
value Int value Vote
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade) comment ResumesComment @relation(fields: [commentId], references: [id], onDelete: Cascade)

View File

@ -1,35 +0,0 @@
import { useSession } from 'next-auth/react';
import { Spinner } from '@tih/ui';
import Comment from './comment/Comment';
import type { ResumeComment } from '~/types/resume-comments';
type Props = Readonly<{
comments: Array<ResumeComment>;
isLoading: boolean;
}>;
export default function CommentListItems({ comments, isLoading }: Props) {
const { data: session } = useSession();
if (isLoading) {
return (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
);
}
return (
<div className="m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-3 overflow-y-auto">
{comments.map((comment) => (
<Comment
key={comment.id}
comment={comment}
userId={session?.user?.id}
/>
))}
</div>
);
}

View File

@ -1,54 +0,0 @@
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Tabs } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import CommentListItems from './CommentListItems';
import { COMMENTS_SECTIONS } from './constants';
import ResumeSignInButton from '../shared/ResumeSignInButton';
type CommentsListProps = Readonly<{
resumeId: string;
setShowCommentsForm: (show: boolean) => void;
}>;
export default function CommentsList({
resumeId,
setShowCommentsForm,
}: CommentsListProps) {
const { data: sessionData } = useSession();
const [tab, setTab] = useState(COMMENTS_SECTIONS[0].value);
const commentsQuery = trpc.useQuery(['resumes.reviews.list', { resumeId }]);
const renderButton = () => {
if (sessionData === null) {
return <ResumeSignInButton text="to join discussion" />;
}
return (
<Button
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
return (
<div className="space-y-3">
{renderButton()}
<Tabs
label="comments"
tabs={COMMENTS_SECTIONS}
value={tab}
onChange={(value) => setTab(value)}
/>
<CommentListItems
comments={commentsQuery.data?.filter((c) => c.section === tab) ?? []}
isLoading={commentsQuery.isFetching}
/>
</div>
);
}

View File

@ -0,0 +1,77 @@
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
import ResumeExpandableText from '../shared/ResumeExpandableText';
import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentListItemProps = {
comment: ResumeComment;
userId?: string;
};
export default function ResumeCommentListItem({
comment,
userId,
}: ResumeCommentListItemProps) {
const isCommentOwner = userId === comment.user.userId;
return (
<div className="border-primary-300 w-3/4 rounded-md border-2 bg-white p-2 drop-shadow-md">
<div className="flex w-full flex-row space-x-2 p-1 align-top">
{comment.user.image ? (
<img
alt={comment.user.name ?? 'Reviewer'}
className="mt-1 h-8 w-8 rounded-full"
src={comment.user.image!}
/>
) : (
<FaceSmileIcon className="h-8 w-8 rounded-full" />
)}
<div className="flex w-full flex-col space-y-1">
{/* Name and creation time */}
<div className="flex flex-row justify-between">
<div className="flex flex-row items-center space-x-1">
<div className="font-medium">
{comment.user.name ?? 'Reviewer ABC'}
</div>
<div className="text-primary-800 text-xs font-medium">
{isCommentOwner ? '(Me)' : ''}
</div>
</div>
<div className="text-xs text-gray-600">
{comment.createdAt.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
})}
</div>
</div>
{/* Description */}
<ResumeExpandableText>{comment.description}</ResumeExpandableText>
{/* 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" />
<div className="text-xs">{comment.numVotes}</div>
<ArrowDownCircleIcon className="h-4 w-4 fill-gray-400" />
{/* TODO: Implement edit */}
{isCommentOwner ? (
<div className="text-primary-800 hover:text-primary-400 px-1 text-xs">
Edit
</div>
) : null}
</div>
</div>
</div>
</div>
);
}

View File

@ -5,7 +5,7 @@ import { Button, Dialog, TextArea } from '@tih/ui';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
type CommentsFormProps = Readonly<{ type ResumeCommentsFormProps = Readonly<{
resumeId: string; resumeId: string;
setShowCommentsForm: (show: boolean) => void; setShowCommentsForm: (show: boolean) => void;
}>; }>;
@ -20,10 +20,10 @@ type IFormInput = {
type InputKeys = keyof IFormInput; type InputKeys = keyof IFormInput;
export default function CommentsForm({ export default function ResumeCommentsForm({
resumeId, resumeId,
setShowCommentsForm, setShowCommentsForm,
}: CommentsFormProps) { }: ResumeCommentsFormProps) {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const { const {
register, register,
@ -41,16 +41,19 @@ export default function CommentsForm({
}); });
const trpcContext = trpc.useContext(); const trpcContext = trpc.useContext();
const reviewCreateMutation = trpc.useMutation('resumes.reviews.user.create', { const commentCreateMutation = trpc.useMutation(
'resumes.comments.user.create',
{
onSuccess: () => { onSuccess: () => {
// New review added, invalidate query to trigger refetch // New Comment added, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.reviews.list']); trpcContext.invalidateQueries(['resumes.comments.list']);
}, },
}); },
);
// TODO: Give a feedback to the user if the action succeeds/fails // TODO: Give a feedback to the user if the action succeeds/fails
const onSubmit: SubmitHandler<IFormInput> = async (data) => { const onSubmit: SubmitHandler<IFormInput> = async (data) => {
return await reviewCreateMutation.mutate( return await commentCreateMutation.mutate(
{ {
resumeId, resumeId,
...data, ...data,
@ -89,7 +92,7 @@ export default function CommentsForm({
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<TextArea <TextArea
{...(register('general'), {})} {...(register('general'), {})}
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="General" label="General"
placeholder="General comments about the resume" placeholder="General comments about the resume"
onChange={(value) => onValueChange('general', value)} onChange={(value) => onValueChange('general', value)}
@ -97,7 +100,7 @@ export default function CommentsForm({
<TextArea <TextArea
{...(register('education'), {})} {...(register('education'), {})}
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="Education" label="Education"
placeholder="Comments about the Education section" placeholder="Comments about the Education section"
onChange={(value) => onValueChange('education', value)} onChange={(value) => onValueChange('education', value)}
@ -105,7 +108,7 @@ export default function CommentsForm({
<TextArea <TextArea
{...(register('experience'), {})} {...(register('experience'), {})}
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="Experience" label="Experience"
placeholder="Comments about the Experience section" placeholder="Comments about the Experience section"
onChange={(value) => onValueChange('experience', value)} onChange={(value) => onValueChange('experience', value)}
@ -113,7 +116,7 @@ export default function CommentsForm({
<TextArea <TextArea
{...(register('projects'), {})} {...(register('projects'), {})}
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="Projects" label="Projects"
placeholder="Comments about the Projects section" placeholder="Comments about the Projects section"
onChange={(value) => onValueChange('projects', value)} onChange={(value) => onValueChange('projects', value)}
@ -121,7 +124,7 @@ export default function CommentsForm({
<TextArea <TextArea
{...(register('skills'), {})} {...(register('skills'), {})}
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="Skills" label="Skills"
placeholder="Comments about the Skills section" placeholder="Comments about the Skills section"
onChange={(value) => onValueChange('skills', value)} onChange={(value) => onValueChange('skills', value)}
@ -130,7 +133,7 @@ export default function CommentsForm({
<div className="flex justify-end space-x-2 pt-4"> <div className="flex justify-end space-x-2 pt-4">
<Button <Button
disabled={reviewCreateMutation.isLoading} disabled={commentCreateMutation.isLoading}
label="Cancel" label="Cancel"
type="button" type="button"
variant="tertiary" variant="tertiary"
@ -138,8 +141,8 @@ export default function CommentsForm({
/> />
<Button <Button
disabled={!isDirty || reviewCreateMutation.isLoading} disabled={!isDirty || commentCreateMutation.isLoading}
isLoading={reviewCreateMutation.isLoading} isLoading={commentCreateMutation.isLoading}
label="Submit" label="Submit"
type="submit" type="submit"
variant="primary" variant="primary"

View File

@ -0,0 +1,86 @@
import { useSession } from 'next-auth/react';
import { useState } from 'react';
import { Spinner, Tabs } from '@tih/ui';
import { Button } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import { RESUME_COMMENTS_SECTIONS } from './resumeCommentConstants';
import ResumeCommentListItem from './ResumeCommentListItem';
import ResumeSignInButton from '../shared/ResumeSignInButton';
import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentsListProps = Readonly<{
resumeId: string;
setShowCommentsForm: (show: boolean) => void;
}>;
export default function ResumeCommentsList({
resumeId,
setShowCommentsForm,
}: ResumeCommentsListProps) {
const { data: sessionData } = useSession();
const [tab, setTab] = useState(RESUME_COMMENTS_SECTIONS[0].value);
const [tabs, setTabs] = useState(RESUME_COMMENTS_SECTIONS);
const commentsQuery = trpc.useQuery(['resumes.comments.list', { resumeId }], {
onSuccess: (data: Array<ResumeComment>) => {
const updatedTabs = RESUME_COMMENTS_SECTIONS.map(({ label, value }) => {
const count = data.filter(({ section }) => section === value).length;
const updatedLabel = count > 0 ? `${label} (${count})` : label;
return {
label: updatedLabel,
value,
};
});
setTabs(updatedTabs);
},
});
const renderButton = () => {
if (sessionData === null) {
return <ResumeSignInButton text="to join discussion" />;
}
return (
<Button
display="block"
label="Add your review"
variant="tertiary"
onClick={() => setShowCommentsForm(true)}
/>
);
};
return (
<div className="space-y-3">
{renderButton()}
<Tabs
label="comments"
tabs={tabs}
value={tab}
onChange={(value) => setTab(value)}
/>
{commentsQuery.isFetching ? (
<div className="col-span-10 pt-4">
<Spinner display="block" size="lg" />
</div>
) : (
<div className="m-2 flow-root h-[calc(100vh-20rem)] w-full flex-col space-y-3 overflow-y-auto">
{(commentsQuery.data?.filter((c) => c.section === tab) ?? []).map(
(comment) => (
<ResumeCommentListItem
key={comment.id}
comment={comment}
userId={sessionData?.user?.id}
/>
),
)}
</div>
)}
</div>
);
}

View File

@ -1,13 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import CommentsForm from './CommentsForm'; import ResumeCommentsForm from './ResumeCommentsForm';
import CommentsList from './CommentsList'; import ResumeCommentsList from './ResumeCommentsList';
type ICommentsSectionProps = { type CommentsSectionProps = {
resumeId: string; resumeId: string;
}; };
export default function CommentsSection({ resumeId }: ICommentsSectionProps) { export default function ResumeCommentsSection({
resumeId,
}: CommentsSectionProps) {
const [showCommentsForm, setShowCommentsForm] = useState(false); const [showCommentsForm, setShowCommentsForm] = useState(false);
return ( return (
@ -18,17 +20,17 @@ export default function CommentsSection({ resumeId }: ICommentsSectionProps) {
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="bg-gray-50 px-3 text-lg font-medium text-gray-900"> <span className="bg-gray-50 px-3 text-lg font-medium text-gray-900">
Comments Reviews
</span> </span>
</div> </div>
</div> </div>
{showCommentsForm ? ( {showCommentsForm ? (
<CommentsForm <ResumeCommentsForm
resumeId={resumeId} resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm} setShowCommentsForm={setShowCommentsForm}
/> />
) : ( ) : (
<CommentsList <ResumeCommentsList
resumeId={resumeId} resumeId={resumeId}
setShowCommentsForm={setShowCommentsForm} setShowCommentsForm={setShowCommentsForm}
/> />

View File

@ -1,18 +0,0 @@
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 (
<CommentCard isCommentOwner={isCommentOwner}>
<CommentBody comment={comment} isCommentOwner={isCommentOwner} />
</CommentCard>
);
}

View File

@ -1,64 +0,0 @@
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 (
<div className="flex w-full flex-row space-x-2 p-1 align-top">
{comment.user.image ? (
<img
alt={comment.user.name ?? 'Reviewer'}
className="mt-1 h-8 w-8 rounded-full"
src={comment.user.image!}
/>
) : (
<FaceSmileIcon className="h-8 w-8 rounded-full" />
)}
<div className="flex w-full flex-col space-y-1">
{/* Name and creation time */}
<div className="flex flex-row justify-between">
<div className="font-medium">
{comment.user.name ?? 'Reviewer ABC'}
</div>
<div className="text-xs text-gray-600">
{comment.createdAt.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
})}
</div>
</div>
{/* Description */}
<div className="text-sm">{comment.description}</div>
{/* 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" />
<div className="text-xs">{comment.numVotes}</div>
<ArrowDownCircleIcon className="h-4 w-4 fill-gray-400" />
{/* TODO: Implement edit */}
{isCommentOwner ? (
<div className="text-primary-800 hover:text-primary-400 px-1 text-xs">
Edit
</div>
) : null}
</div>
</div>
</div>
);
}

View File

@ -1,22 +0,0 @@
import type { ReactNode } from 'react';
type CommentCardProps = {
children: ReactNode;
isCommentOwner?: boolean;
};
export default function CommentCard({
isCommentOwner,
children,
}: CommentCardProps) {
// Used two different <div> to allow customisation of owner comments
return isCommentOwner ? (
<div className="border-primary-300 float-right w-3/4 rounded-md border-2 bg-white p-2 drop-shadow-md">
{children}
</div>
) : (
<div className="border-primary-300 float-left w-3/4 rounded-md border-2 bg-white p-2 drop-shadow-md">
{children}
</div>
);
}

View File

@ -1,6 +1,6 @@
import { ResumesSection } from '@prisma/client'; import { ResumesSection } from '@prisma/client';
export const COMMENTS_SECTIONS = [ export const RESUME_COMMENTS_SECTIONS = [
{ {
label: 'General', label: 'General',
value: ResumesSection.GENERAL, value: ResumesSection.GENERAL,

View File

@ -0,0 +1,48 @@
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { useLayoutEffect, useRef, useState } from 'react';
type ResumeExpandableTextProps = Readonly<{
children: ReactNode;
}>;
export default function ResumeExpandableText({
children,
}: ResumeExpandableTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const [descriptionOverflow, setDescriptionOverflow] = useState(false);
useLayoutEffect(() => {
if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
setDescriptionOverflow(true);
}
}, [ref]);
const onSeeActionClicked = () => {
setDescriptionExpanded(!descriptionExpanded);
};
return (
<>
<span
ref={ref}
className={clsx(
'whitespace-pre-wrap text-sm',
'line-clamp-3',
descriptionExpanded ? 'line-clamp-none' : '',
)}>
{children}
</span>
{descriptionOverflow && (
<div className="flex flex-row">
<div
className="text-xs text-indigo-500 hover:text-indigo-300"
onClick={onSeeActionClicked}>
{descriptionExpanded ? 'See Less' : 'See More'}
</div>
</div>
)}
</>
);
}

View File

@ -15,7 +15,7 @@ import {
} from '@heroicons/react/20/solid'; } from '@heroicons/react/20/solid';
import { Spinner } from '@tih/ui'; import { Spinner } from '@tih/ui';
import CommentsSection from '~/components/resumes/comments/CommentsSection'; import ResumeCommentsSection from '~/components/resumes/comments/ResumeCommentsSection';
import ResumePdf from '~/components/resumes/ResumePdf'; import ResumePdf from '~/components/resumes/ResumePdf';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
@ -174,11 +174,11 @@ export default function ResumeReviewPage() {
</div> </div>
)} )}
<div className="flex w-full flex-col py-4 lg:flex-row"> <div className="flex w-full flex-col py-4 lg:flex-row">
<div className="w-full lg:w-[800px]"> <div className="w-full lg:w-[780px]">
<ResumePdf url={detailsQuery.data.url} /> <ResumePdf url={detailsQuery.data.url} />
</div> </div>
<div className="mx-8 grow"> <div className="mx-8 grow">
<CommentsSection resumeId={resumeId as string} /> <ResumeCommentsSection resumeId={resumeId as string} />
</div> </div>
</div> </div>
</main> </main>

View File

@ -9,10 +9,10 @@ import { questionsAnswerCommentRouter } from './questions-answer-comment-router'
import { questionsAnswerRouter } from './questions-answer-router'; import { questionsAnswerRouter } from './questions-answer-router';
import { questionsQuestionCommentRouter } from './questions-question-comment-router'; import { questionsQuestionCommentRouter } from './questions-question-comment-router';
import { questionsQuestionRouter } from './questions-question-router'; import { questionsQuestionRouter } from './questions-question-router';
import { resumeCommentsRouter } from './resumes/resumes-comments-router';
import { resumesCommentsUserRouter } from './resumes/resumes-comments-user-router';
import { resumesRouter } from './resumes/resumes-resume-router'; import { resumesRouter } from './resumes/resumes-resume-router';
import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router'; import { resumesResumeUserRouter } from './resumes/resumes-resume-user-router';
import { resumeReviewsRouter } from './resumes/resumes-reviews-router';
import { resumesReviewsUserRouter } from './resumes/resumes-reviews-user-router';
import { resumesStarUserRouter } from './resumes/resumes-star-user-router'; import { resumesStarUserRouter } from './resumes/resumes-star-user-router';
import { todosRouter } from './todos'; import { todosRouter } from './todos';
import { todosUserRouter } from './todos-user-router'; import { todosUserRouter } from './todos-user-router';
@ -29,8 +29,8 @@ export const appRouter = createRouter()
.merge('resumes.resume.', resumesRouter) .merge('resumes.resume.', resumesRouter)
.merge('resumes.resume.user.', resumesResumeUserRouter) .merge('resumes.resume.user.', resumesResumeUserRouter)
.merge('resumes.resume.', resumesStarUserRouter) .merge('resumes.resume.', resumesStarUserRouter)
.merge('resumes.reviews.', resumeReviewsRouter) .merge('resumes.comments.', resumeCommentsRouter)
.merge('resumes.reviews.user.', resumesReviewsUserRouter) .merge('resumes.comments.user.', resumesCommentsUserRouter)
.merge('questions.answers.comments.', questionsAnswerCommentRouter) .merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter) .merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter) .merge('questions.questions.comments.', questionsQuestionCommentRouter)

View File

@ -4,7 +4,7 @@ import { createRouter } from '../context';
import type { ResumeComment } from '~/types/resume-comments'; import type { ResumeComment } from '~/types/resume-comments';
export const resumeReviewsRouter = createRouter().query('list', { export const resumeCommentsRouter = createRouter().query('list', {
input: z.object({ input: z.object({
resumeId: z.string(), resumeId: z.string(),
}), }),

View File

@ -3,14 +3,14 @@ import { ResumesSection } from '@prisma/client';
import { createProtectedRouter } from '../context'; import { createProtectedRouter } from '../context';
type IResumeCommentInput = Readonly<{ type ResumeCommentInput = Readonly<{
description: string; description: string;
resumeId: string; resumeId: string;
section: ResumesSection; section: ResumesSection;
userId: string; userId: string;
}>; }>;
export const resumesReviewsUserRouter = createProtectedRouter().mutation( export const resumesCommentsUserRouter = createProtectedRouter().mutation(
'create', 'create',
{ {
input: z.object({ input: z.object({
@ -27,7 +27,7 @@ export const resumesReviewsUserRouter = createProtectedRouter().mutation(
input; input;
// For each section, convert them into ResumesComment model if provided // For each section, convert them into ResumesComment model if provided
const comments: Array<IResumeCommentInput> = [ const comments: Array<ResumeCommentInput> = [
{ description: education, section: ResumesSection.EDUCATION }, { description: education, section: ResumesSection.EDUCATION },
{ description: experience, section: ResumesSection.EXPERIENCE }, { description: experience, section: ResumesSection.EXPERIENCE },
{ description: general, section: ResumesSection.GENERAL }, { description: general, section: ResumesSection.GENERAL },

View File

@ -1,10 +1,10 @@
import type { ResumesSection } from '@prisma/client'; import type { ResumesSection } from '@prisma/client';
/** /**
* Returned by `resumeReviewsRouter` (query for 'resumes.reviews.list') and received as prop by `Comment` in `CommentsList` * Returned by `resumeCommentsRouter` (query for 'resumes.comments.list') and received as prop by `Comment` in `CommentsList`
* frontend-friendly representation of the query * frontend-friendly representation of the query
*/ */
export type ResumeComment = { export type ResumeComment = Readonly<{
createdAt: Date; createdAt: Date;
description: string; description: string;
hasVoted: boolean; hasVoted: boolean;
@ -18,4 +18,4 @@ export type ResumeComment = {
name: string?; name: string?;
userId: string; userId: string;
}; };
}; }>;