From 46042ff0c81b34c6120154a52573c73a70f57985 Mon Sep 17 00:00:00 2001 From: wnhlee <2wheeh@gmail.com> Date: Mon, 11 Mar 2024 19:13:46 +0900 Subject: [PATCH 1/2] feat: implement CreateReviewButton --- .../review/client/create-review-button.tsx | 76 +++++++++++++++++++ ui/src/lib/utils/common/error.ts | 12 +++ 2 files changed, 88 insertions(+) create mode 100644 ui/src/components/review/client/create-review-button.tsx create mode 100644 ui/src/lib/utils/common/error.ts diff --git a/ui/src/components/review/client/create-review-button.tsx b/ui/src/components/review/client/create-review-button.tsx new file mode 100644 index 00000000..337a6e1b --- /dev/null +++ b/ui/src/components/review/client/create-review-button.tsx @@ -0,0 +1,76 @@ +'use client'; + +import clsx from 'clsx'; +import { useRouter } from 'next/navigation'; + +import Text from '@/components/common/server/text'; + +import { useToast } from '@/context/common/toast-context'; +import { useReview } from '@/context/review/review-context'; +import { useEditorRef } from '@/context/editor/editor-ref-context'; + +import { createReview } from '@/lib/apis/review/client'; +import { validateReviewFields } from '@/lib/utils/review/validate-review-fields'; +import { isErrorWithMessage } from '@/lib/utils/common/error'; + +export function CreateReviewButton() { + const router = useRouter(); + const { emitToast } = useToast(); + + const { title, movieName, movieNameRef, titleRef, disabled, setDisabled } = useReview(); + const { editorRef } = useEditorRef() ?? {}; + + const handleClick = () => { + setDisabled(true); + + const editorState = editorRef?.current?.getEditorState(); + + if (editorState) { + const validatedFields = validateReviewFields({ title, movieName, editorState }); + + if (!validatedFields.success) { + // Order to check should be aligned with the order of elements in the DOM + // title -> movieName -> editor + if (validatedFields.errors.title) { + titleRef.current?.focus(); + emitToast(validatedFields.errors.title, 'error'); + } else if (validatedFields.errors.movieName) { + movieNameRef.current?.focus(); + emitToast(validatedFields.errors.movieName, 'error'); + } else { + editorRef?.current?.focus(); + emitToast(validatedFields.errors.content!, 'error'); + } + + setDisabled(false); + return; + } + + void createReview(validatedFields.data) + .then(({ review }) => { + emitToast('리뷰 등록 완료', 'success'); + router.push(`/review/${review.id}`); + }) + .catch((error) => { + if (isErrorWithMessage(error)) { + emitToast(error.message, 'error'); + } else { + // TODO: throw new Error and move to global error handler + console.error(error); + emitToast('알 수 없는 에러가 발생했습니다. 다시 시도해주세요.', 'error'); + } + }) + .finally(() => setDisabled(false)); + } + }; + + const className = clsx(' rounded-full border bg-white px-2 py-1', { + 'pointer-events-none': disabled, + }); + + return ( + + ); +} diff --git a/ui/src/lib/utils/common/error.ts b/ui/src/lib/utils/common/error.ts new file mode 100644 index 00000000..0d5ead46 --- /dev/null +++ b/ui/src/lib/utils/common/error.ts @@ -0,0 +1,12 @@ +export interface ErrorWithMessage { + message: string; +} + +export function isErrorWithMessage(error: unknown): error is ErrorWithMessage { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof error.message === 'string' + ); +} From 9e0166b9e5f90a8e7298ee87beb68c6aeedddc8e Mon Sep 17 00:00:00 2001 From: wnhlee <2wheeh@gmail.com> Date: Mon, 11 Mar 2024 23:31:05 +0900 Subject: [PATCH 2/2] fixup: CreateReviewButton --- .../review/client/create-review-button.tsx | 108 +++++++++++------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/ui/src/components/review/client/create-review-button.tsx b/ui/src/components/review/client/create-review-button.tsx index 337a6e1b..50e1418a 100644 --- a/ui/src/components/review/client/create-review-button.tsx +++ b/ui/src/components/review/client/create-review-button.tsx @@ -20,56 +20,80 @@ export function CreateReviewButton() { const { title, movieName, movieNameRef, titleRef, disabled, setDisabled } = useReview(); const { editorRef } = useEditorRef() ?? {}; - const handleClick = () => { - setDisabled(true); + const validateAndGetData = () => { + if (!editorRef?.current) { + return; + } + + const editorState = editorRef.current.getEditorState(); + + const validatedFields = validateReviewFields({ title, movieName, editorState }); - const editorState = editorRef?.current?.getEditorState(); - - if (editorState) { - const validatedFields = validateReviewFields({ title, movieName, editorState }); - - if (!validatedFields.success) { - // Order to check should be aligned with the order of elements in the DOM - // title -> movieName -> editor - if (validatedFields.errors.title) { - titleRef.current?.focus(); - emitToast(validatedFields.errors.title, 'error'); - } else if (validatedFields.errors.movieName) { - movieNameRef.current?.focus(); - emitToast(validatedFields.errors.movieName, 'error'); - } else { - editorRef?.current?.focus(); - emitToast(validatedFields.errors.content!, 'error'); - } - - setDisabled(false); - return; + if (!validatedFields.success) { + // Order to check should be aligned with the order of elements in the DOM + // title -> movieName -> editor + if (validatedFields.errors.title) { + titleRef.current?.focus(); + emitToast(validatedFields.errors.title, 'error'); + } else if (validatedFields.errors.movieName) { + movieNameRef.current?.focus(); + emitToast(validatedFields.errors.movieName, 'error'); + } else { + editorRef?.current?.focus(); + emitToast(validatedFields.errors.content!, 'error'); } - void createReview(validatedFields.data) - .then(({ review }) => { - emitToast('리뷰 등록 완료', 'success'); - router.push(`/review/${review.id}`); - }) - .catch((error) => { - if (isErrorWithMessage(error)) { - emitToast(error.message, 'error'); - } else { - // TODO: throw new Error and move to global error handler - console.error(error); - emitToast('알 수 없는 에러가 발생했습니다. 다시 시도해주세요.', 'error'); - } - }) - .finally(() => setDisabled(false)); + setDisabled(false); + return; + } + + return validatedFields.data; + }; + + const handleApiError = (error: unknown) => { + if (isErrorWithMessage(error)) { + emitToast(error.message, 'error'); + } else { + // TODO: throw new Error and move to global error handler + console.error(error); + emitToast('알 수 없는 에러가 발생했습니다. 다시 시도해주세요.', 'error'); + } + }; + + const handleCreateReview = async () => { + setDisabled(true); + + const data = validateAndGetData(); + + if (!data) { + return; + } + + try { + const { review } = await createReview(data); + + emitToast('리뷰 등록 완료', 'success'); + router.push(`/review/${review.id}`); + } catch (error) { + handleApiError(error); + } finally { + setDisabled(false); } }; - const className = clsx(' rounded-full border bg-white px-2 py-1', { - 'pointer-events-none': disabled, - }); + const handleClick = () => { + void handleCreateReview(); + }; return ( - );