mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 12:43:12 +08:00
[resumes][feat] add comment edit (#386)
* [resumes][feat] add comment edit * [resumes][fix] use react-hook-form validation Co-authored-by: Terence Ho <>
This commit is contained in:
@ -1,8 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from '@heroicons/react/20/solid';
|
||||
import { FaceSmileIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, TextArea } from '@tih/ui';
|
||||
|
||||
import { trpc } from '~/utils/trpc';
|
||||
|
||||
import ResumeExpandableText from '../shared/ResumeExpandableText';
|
||||
|
||||
@ -13,11 +19,63 @@ type ResumeCommentListItemProps = {
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
type ICommentInput = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function ResumeCommentListItem({
|
||||
comment,
|
||||
userId,
|
||||
}: ResumeCommentListItemProps) {
|
||||
const isCommentOwner = userId === comment.user.userId;
|
||||
const [isEditingComment, setIsEditingComment] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors, isDirty },
|
||||
reset,
|
||||
} = useForm<ICommentInput>({
|
||||
defaultValues: {
|
||||
description: comment.description,
|
||||
},
|
||||
});
|
||||
|
||||
const trpcContext = trpc.useContext();
|
||||
const commentUpdateMutation = trpc.useMutation(
|
||||
'resumes.comments.user.update',
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Comment updated, invalidate query to trigger refetch
|
||||
trpcContext.invalidateQueries(['resumes.comments.list']);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const onCancel = () => {
|
||||
reset({ description: comment.description });
|
||||
setIsEditingComment(false);
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<ICommentInput> = async (data) => {
|
||||
const { id } = comment;
|
||||
return await commentUpdateMutation.mutate(
|
||||
{
|
||||
id,
|
||||
...data,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsEditingComment(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setFormValue = (value: string) => {
|
||||
setValue('description', value.trim(), { shouldDirty: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-primary-300 w-3/4 rounded-md border-2 bg-white p-2 drop-shadow-md">
|
||||
@ -54,7 +112,45 @@ export default function ResumeCommentListItem({
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<ResumeExpandableText text={comment.description} />
|
||||
{isEditingComment ? (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex-column mt-1 space-y-2">
|
||||
<TextArea
|
||||
{...(register('description', {
|
||||
required: 'Comments cannot be empty!',
|
||||
}),
|
||||
{})}
|
||||
defaultValue={comment.description}
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
errorMessage={errors.description?.message}
|
||||
label="Edit comment"
|
||||
placeholder="Leave your comment here"
|
||||
onChange={setFormValue}
|
||||
/>
|
||||
|
||||
<div className="flex-row space-x-2">
|
||||
<Button
|
||||
disabled={commentUpdateMutation.isLoading}
|
||||
label="Cancel"
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={!isDirty || commentUpdateMutation.isLoading}
|
||||
isLoading={commentUpdateMutation.isLoading}
|
||||
label="Confirm"
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<ResumeExpandableText text={comment.description} />
|
||||
)}
|
||||
|
||||
{/* Upvote and edit */}
|
||||
<div className="flex flex-row space-x-1 pt-1 align-middle">
|
||||
@ -63,12 +159,14 @@ export default function ResumeCommentListItem({
|
||||
<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">
|
||||
{isCommentOwner && !isEditingComment && (
|
||||
<a
|
||||
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
|
||||
href="#"
|
||||
onClick={() => setIsEditingComment(true)}>
|
||||
Edit
|
||||
</div>
|
||||
) : null}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,9 +10,8 @@ type ResumeCommentInput = Readonly<{
|
||||
userId: string;
|
||||
}>;
|
||||
|
||||
export const resumesCommentsUserRouter = createProtectedRouter().mutation(
|
||||
'create',
|
||||
{
|
||||
export const resumesCommentsUserRouter = createProtectedRouter()
|
||||
.mutation('create', {
|
||||
input: z.object({
|
||||
education: z.string(),
|
||||
experience: z.string(),
|
||||
@ -50,5 +49,22 @@ export const resumesCommentsUserRouter = createProtectedRouter().mutation(
|
||||
data: comments,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
})
|
||||
.mutation('update', {
|
||||
input: z.object({
|
||||
description: z.string(),
|
||||
id: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { id, description } = input;
|
||||
|
||||
return await ctx.prisma.resumesComment.update({
|
||||
data: {
|
||||
description,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user