diff --git a/ikas-theme/public/locales/en/product-reviews.json b/ikas-theme/public/locales/en/product-reviews.json new file mode 100644 index 0000000..19da6d1 --- /dev/null +++ b/ikas-theme/public/locales/en/product-reviews.json @@ -0,0 +1,24 @@ +{ + "title": "Customer Reviews", + "writeAReview": "Write a review", + "closeReviewForm": "Close review form", + "emptyReview": "No reviews for this product", + "formTitle": "Write a review", + "basedOnXReviews": "Based on {{x}} reviews", + "xStar": "{{x}} star", + "purchased": "Purchased", + "noComment": "No comment", + "xReview": "{{x}} Review", + "form": { + "name": "Name", + "email": "Email", + "rating": "Rating", + "reviewStarts": "Raiting", + "reviewTitle": "Review Title", + "bodyOfReview": "Body of Review", + "requiredRule": "This field is required", + "submitReview": "Submit Review", + "successText": "Thank you for submitting a review!", + "errorText": "An error occurred on submiting. Please try again" + } +} diff --git a/ikas-theme/src/components/__generated__/editor.tsx b/ikas-theme/src/components/__generated__/editor.tsx index d986f90..09ef20d 100644 --- a/ikas-theme/src/components/__generated__/editor.tsx +++ b/ikas-theme/src/components/__generated__/editor.tsx @@ -19,10 +19,11 @@ const Component13 = dynamic(() => import("../page-404"), { loading: () => import("../page-blogs"), { loading: () => }); const Component15 = dynamic(() => import("../page-blog"), { loading: () => }); const Component16 = dynamic(() => import("../page-blog-category"), { loading: () => }); +const Component17 = dynamic(() => import("../product-reviews"), { loading: () => }); const Components = { - "8b5d6278-a490-4f0a-8308-00bf888b79b0": Component0,"ae2074dc-577e-40a1-a9aa-6104aaacdb40": Component1,"a1ea193f-50da-48a3-baa7-5fa8b9ec5b41": Component2,"4fd7e253-29f0-41d7-924f-cbfa1c85c74f": Component3,"58251b96-18ee-42bc-bf7a-75532385f30d": Component4,"2b6b4787-34f6-482e-a7ba-ba1401644295": Component5,"b382e9ea-6d5f-4e16-91de-1c5ba47bec02": Component6,"4018269a-2cae-44fb-8747-978ee92e1bdb": Component7,"c8853ae6-208e-4937-93a5-36a025cb44e3": Component8,"5dfdb8fe-de7b-4413-a00a-ad3af9f887ad": Component9,"9b971632-face-4723-bbdf-e3db79974eb2": Component10,"f2e59aaa-6e1c-4dba-9ae0-3d62d08ee2c0": Component11,"d1d2ba23-1e0d-4708-926b-bc3210bca6b1": Component12,"db6ef045-291c-4e06-92a8-61f6644d8f2c": Component13,"df5b16c2-f104-41f8-b1a2-7c4d7147b8e8": Component14,"0d74b684-3bbf-4521-be78-1ea152b66b3b": Component15,"743a6ac1-7598-4b7b-b695-d4a50a313bba": Component16 + "8b5d6278-a490-4f0a-8308-00bf888b79b0": Component0,"ae2074dc-577e-40a1-a9aa-6104aaacdb40": Component1,"a1ea193f-50da-48a3-baa7-5fa8b9ec5b41": Component2,"4fd7e253-29f0-41d7-924f-cbfa1c85c74f": Component3,"58251b96-18ee-42bc-bf7a-75532385f30d": Component4,"2b6b4787-34f6-482e-a7ba-ba1401644295": Component5,"b382e9ea-6d5f-4e16-91de-1c5ba47bec02": Component6,"4018269a-2cae-44fb-8747-978ee92e1bdb": Component7,"c8853ae6-208e-4937-93a5-36a025cb44e3": Component8,"5dfdb8fe-de7b-4413-a00a-ad3af9f887ad": Component9,"9b971632-face-4723-bbdf-e3db79974eb2": Component10,"f2e59aaa-6e1c-4dba-9ae0-3d62d08ee2c0": Component11,"d1d2ba23-1e0d-4708-926b-bc3210bca6b1": Component12,"db6ef045-291c-4e06-92a8-61f6644d8f2c": Component13,"df5b16c2-f104-41f8-b1a2-7c4d7147b8e8": Component14,"0d74b684-3bbf-4521-be78-1ea152b66b3b": Component15,"743a6ac1-7598-4b7b-b695-d4a50a313bba": Component16,"4fb4f19d-5889-4ef3-84c3-a0871eab784d": Component17 }; export default Components; \ No newline at end of file diff --git a/ikas-theme/src/components/__generated__/pages/[slug]/index.ts b/ikas-theme/src/components/__generated__/pages/[slug]/index.ts index 61ef45d..fa65a7d 100644 --- a/ikas-theme/src/components/__generated__/pages/[slug]/index.ts +++ b/ikas-theme/src/components/__generated__/pages/[slug]/index.ts @@ -3,9 +3,10 @@ import Component1 from "src/components/footer"; import Component2 from "src/components/product-detail"; import Component3 from "src/components/page-brand"; import Component4 from "src/components/page-category"; +import Component5 from "src/components/product-reviews"; const Components = { - "ae2074dc-577e-40a1-a9aa-6104aaacdb40": Component0,"a1ea193f-50da-48a3-baa7-5fa8b9ec5b41": Component1,"4fd7e253-29f0-41d7-924f-cbfa1c85c74f": Component2,"c8853ae6-208e-4937-93a5-36a025cb44e3": Component3,"5dfdb8fe-de7b-4413-a00a-ad3af9f887ad": Component4 + "ae2074dc-577e-40a1-a9aa-6104aaacdb40": Component0,"a1ea193f-50da-48a3-baa7-5fa8b9ec5b41": Component1,"4fd7e253-29f0-41d7-924f-cbfa1c85c74f": Component2,"c8853ae6-208e-4937-93a5-36a025cb44e3": Component3,"5dfdb8fe-de7b-4413-a00a-ad3af9f887ad": Component4,"4fb4f19d-5889-4ef3-84c3-a0871eab784d": Component5 }; export default Components; \ No newline at end of file diff --git a/ikas-theme/src/components/__generated__/types.ts b/ikas-theme/src/components/__generated__/types.ts index 6d201a4..f89c6a2 100644 --- a/ikas-theme/src/components/__generated__/types.ts +++ b/ikas-theme/src/components/__generated__/types.ts @@ -115,3 +115,7 @@ export type PageBlogCategoryProps = { imageAspectRatio: ImageAspectRatio; }; +export type ProductReviewsProps = { + productDetail: IkasProduct; +}; + diff --git a/ikas-theme/src/components/components/alert/index.tsx b/ikas-theme/src/components/components/alert/index.tsx index 6fc22d9..20d6363 100644 --- a/ikas-theme/src/components/components/alert/index.tsx +++ b/ikas-theme/src/components/components/alert/index.tsx @@ -11,6 +11,7 @@ export type FormAlertType = { type AlertComponentProps = FormAlertType & { closable?: boolean; onClose?: () => void; + style?: React.CSSProperties; }; const AlertComponent = (props: AlertComponentProps) => { @@ -24,7 +25,7 @@ const AlertComponent = (props: AlertComponentProps) => { if (!isVisible) return null; return ( - + {props.title && {props.title}} {props.text} {props.closable && x} diff --git a/ikas-theme/src/components/components/textarea/index.tsx b/ikas-theme/src/components/components/textarea/index.tsx index b840007..b30a571 100644 --- a/ikas-theme/src/components/components/textarea/index.tsx +++ b/ikas-theme/src/components/components/textarea/index.tsx @@ -3,7 +3,7 @@ import { FormItemStatus } from "../form/form-item"; import * as S from "./style"; type Props = { - status: FormItemStatus; + status?: FormItemStatus; } & JSX.IntrinsicElements["textarea"]; const Textarea = (props: Props) => { diff --git a/ikas-theme/src/components/product-reviews/detail/index.tsx b/ikas-theme/src/components/product-reviews/detail/index.tsx new file mode 100644 index 0000000..5bd180e --- /dev/null +++ b/ikas-theme/src/components/product-reviews/detail/index.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { observer } from "mobx-react-lite"; + +import { ProductReviewsProps } from "src/components/__generated__/types"; + +import Reviews from "./reviews"; +import ReviewsSummary from "./review-summary"; +import Pagination from "src/components/components/pagination"; + +import useProductReviews from "../useProductReviews"; + +const Detail = (props: ProductReviewsProps) => { + const { productDetail } = props; + + const { + isFormVisible, + customerReviewList, + onPageChange, + onWriteReviewButtonClick, + reviewsElementRef, + } = useProductReviews({ productDetail }); + + return ( + <> + + +
+ +
+ + {customerReviewList && ( + + )} + + ); +}; + +export default observer(Detail); diff --git a/ikas-theme/src/components/product-reviews/detail/review-form/index.tsx b/ikas-theme/src/components/product-reviews/detail/review-form/index.tsx new file mode 100644 index 0000000..546514c --- /dev/null +++ b/ikas-theme/src/components/product-reviews/detail/review-form/index.tsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import { + IkasProduct, + useTranslation, + CustomerReviewForm, +} from "@ikas/storefront"; + +import AlertComponent from "src/components/components/alert"; +import FormItem from "src/components/components/form/form-item"; +import Form from "src/components/components/form"; +import Input from "src/components/components/input"; +import TextArea from "src/components/components/textarea"; +import Button from "src/components/components/button"; +import Stars from "../stars"; + +import { NS } from "src/components/product-reviews"; + +import * as S from "./style"; + +const REVIEW_TITLE_MAX_LENGTH = 64; +const REVIEW_COMMENT_MAX_LENGTH = 256; + +type Props = { + product: IkasProduct; + onSubmitSuccess: () => void; + visible: boolean; +}; + +const ReviewForm = (props: Props) => { + const { product, onSubmitSuccess, visible } = props; + const { t } = useTranslation(); + + // States + const [responseStatus, setResponseStatus] = useState< + "success" | "error" | undefined + >(); + const [isPending, setPending] = useState(false); + const [form, setForm] = useState( + new CustomerReviewForm({ + productId: product.id, + message: { starRule: t(`${NS}:form.requiredRule`) }, + }) + ); + + const onSubmit = async () => { + try { + setPending(true); + const result = await form.submit(); + + if (result.isFormError) return; + + if (result.isSuccess) { + setResponseStatus("success"); + onSubmitSuccess(); + return; + } + if (!result.isSuccess) { + setResponseStatus("error"); + } + } catch (error) { + setResponseStatus("error"); + } finally { + setPending(false); + } + }; + + useEffect(() => { + if (visible) { + setPending(false); + setResponseStatus(undefined); + setForm( + new CustomerReviewForm({ + productId: product.id, + message: { starRule: t(`${NS}:form.requiredRule`) }, + }) + ); + } + }, [visible]); + + if (!visible) return null; + + return ( + + + {t(`${NS}:formTitle`)} + + {responseStatus === "success" && ( + + )} + + {responseStatus !== "success" && ( + <> + {responseStatus === "error" && ( + + )} + +
+ + form.onStarChange(star)} + /> + + + { + if (event.target.value.length > REVIEW_TITLE_MAX_LENGTH) + return; + + form.onTitleChange(event.target.value); + }} + /> + + +