mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-02-03 02:24:47 +08:00
[resumes][feat] delete comment (#537)
* [resumes][feat] add delete form * [resumes][feat] add delete comment Co-authored-by: Terence Ho <>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "ResumesComment" DROP CONSTRAINT "ResumesComment_parentId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ResumesComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -187,7 +187,7 @@ model ResumesComment {
|
||||
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
|
||||
votes ResumesCommentVote[]
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
parent ResumesComment? @relation("parentComment", fields: [parentId], references: [id])
|
||||
parent ResumesComment? @relation("parentComment", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children ResumesComment[] @relation("parentComment")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useState } from 'react';
|
||||
|
||||
import ResumeCommentDeleteForm from './comment/ResumeCommentDeleteForm';
|
||||
import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
|
||||
import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm';
|
||||
import ResumeCommentVoteButtons from './comment/ResumeCommentVoteButtons';
|
||||
@@ -22,6 +23,7 @@ export default function ResumeCommentListItem({
|
||||
const isCommentOwner = userId === comment.user.userId;
|
||||
const [isEditingComment, setIsEditingComment] = useState(false);
|
||||
const [isReplyingComment, setIsReplyingComment] = useState(false);
|
||||
const [isDeletingComment, setIsDeletingComment] = useState(false);
|
||||
const [showReplies, setShowReplies] = useState(true);
|
||||
|
||||
return (
|
||||
@@ -73,7 +75,7 @@ export default function ResumeCommentListItem({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upvote and edit */}
|
||||
{/* Upvote and actions (edit, reply, delete) */}
|
||||
<div className="mt-1 flex h-6 items-center">
|
||||
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
|
||||
{/* Action buttons; only present for authenticated user when not editing/replying */}
|
||||
@@ -84,6 +86,7 @@ export default function ResumeCommentListItem({
|
||||
onClick={() => {
|
||||
setIsReplyingComment(!isReplyingComment);
|
||||
setIsEditingComment(false);
|
||||
setIsDeletingComment(false);
|
||||
}}>
|
||||
Reply
|
||||
</button>
|
||||
@@ -111,10 +114,33 @@ export default function ResumeCommentListItem({
|
||||
onClick={() => {
|
||||
setIsEditingComment(!isEditingComment);
|
||||
setIsReplyingComment(false);
|
||||
setIsDeletingComment(false);
|
||||
}}>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isCommentOwner && (
|
||||
<button
|
||||
className="-my-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-red-600"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsDeletingComment(!isDeletingComment);
|
||||
setIsEditingComment(false);
|
||||
setIsReplyingComment(false);
|
||||
}}>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Delete comment form */}
|
||||
{isDeletingComment && (
|
||||
<ResumeCommentDeleteForm
|
||||
id={comment.id}
|
||||
isDeletingComment={isDeletingComment}
|
||||
setIsDeletingComment={setIsDeletingComment}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reply Form */}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Button, Dialog } from '@tih/ui';
|
||||
|
||||
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
type ResumeCommentDeleteFormProps = Readonly<{
|
||||
id: string;
|
||||
isDeletingComment: boolean;
|
||||
setIsDeletingComment: (value: boolean) => void;
|
||||
}>;
|
||||
|
||||
export default function ResumeCommentDeleteForm({
|
||||
id,
|
||||
isDeletingComment,
|
||||
setIsDeletingComment,
|
||||
}: ResumeCommentDeleteFormProps) {
|
||||
const { event: gaEvent } = useGoogleAnalytics();
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const commentDeleteMutation = trpc.useMutation(
|
||||
'resumes.comments.user.delete',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Comments updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.list']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onDelete = async () => {
|
||||
return commentDeleteMutation.mutate(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsDeletingComment(false);
|
||||
|
||||
gaEvent({
|
||||
action: 'resumes.comment_delete',
|
||||
category: 'engagement',
|
||||
label: 'Delete comment',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
setIsDeletingComment(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isShown={isDeletingComment}
|
||||
primaryButton={
|
||||
<Button
|
||||
disabled={commentDeleteMutation.isLoading}
|
||||
display="block"
|
||||
isLoading={commentDeleteMutation.isLoading}
|
||||
label="Delete"
|
||||
variant="danger"
|
||||
onClick={onDelete}
|
||||
/>
|
||||
}
|
||||
secondaryButton={
|
||||
<Button
|
||||
disabled={commentDeleteMutation.isLoading}
|
||||
display="block"
|
||||
label="Cancel"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
}
|
||||
title="Are you sure?"
|
||||
onClose={() => setIsDeletingComment(false)}>
|
||||
<div>
|
||||
Note that deleting this comment will delete all its replies as well.
|
||||
This action is also irreversible! Please check before confirming!
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -99,4 +99,16 @@ export const resumesCommentsUserRouter = createProtectedRouter()
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
.mutation('delete', {
|
||||
input: z.object({ id: z.string() }),
|
||||
async resolve({ ctx, input }) {
|
||||
const { id } = input;
|
||||
|
||||
return await ctx.prisma.resumesComment.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user