From d74c6240b2bf0d61dbafc1a7697b861525f118b4 Mon Sep 17 00:00:00 2001 From: celebitolga Date: Mon, 17 Jul 2023 17:33:13 +0300 Subject: [PATCH 1/8] [ADD] Urls to utils --- ikas-theme/src/utils/urls.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 ikas-theme/src/utils/urls.ts diff --git a/ikas-theme/src/utils/urls.ts b/ikas-theme/src/utils/urls.ts new file mode 100644 index 0000000..656c054 --- /dev/null +++ b/ikas-theme/src/utils/urls.ts @@ -0,0 +1,15 @@ +const urls = { + base: "/", + cart: "/cart", + search: "/search", + blog: "/blog", + account: "/account", + accountFavorite: "/account/favorite-products", + accountAddresses: "/account/addresses", + accountOrders: "/account/orders", + login: "/account/login", + register: "/account/register", + forgotPassword: "/account/forgot-password", +}; + +export default urls; From 7a62fc74405d7d0087f860438d12418f68707c98 Mon Sep 17 00:00:00 2001 From: celebitolga Date: Mon, 17 Jul 2023 17:33:28 +0300 Subject: [PATCH 2/8] [ADD] im-start-empty and im-star-full icons added to svg's folder --- .../src/components/svg/im-star-empty.tsx | 19 +++++++++++++++++++ .../src/components/svg/im-star-full.tsx | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 ikas-theme/src/components/svg/im-star-empty.tsx create mode 100644 ikas-theme/src/components/svg/im-star-full.tsx diff --git a/ikas-theme/src/components/svg/im-star-empty.tsx b/ikas-theme/src/components/svg/im-star-empty.tsx new file mode 100644 index 0000000..d7ea380 --- /dev/null +++ b/ikas-theme/src/components/svg/im-star-empty.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +const svg = ({ className }: { className?: string }) => ( + + + +); + +export default svg; diff --git a/ikas-theme/src/components/svg/im-star-full.tsx b/ikas-theme/src/components/svg/im-star-full.tsx new file mode 100644 index 0000000..5919d4d --- /dev/null +++ b/ikas-theme/src/components/svg/im-star-full.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +const svg = ({ className }: { className?: string }) => ( + + + +); + +export default svg; From acca7fddf3520e89ca4b20132346a124e0c7f61e Mon Sep 17 00:00:00 2001 From: celebitolga Date: Mon, 17 Jul 2023 17:34:00 +0300 Subject: [PATCH 3/8] [ADD] Style prop added to alert comp --- ikas-theme/src/components/components/alert/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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} From 0d69ba440a5a47befd403dd2f7b7a9bd8225c0f1 Mon Sep 17 00:00:00 2001 From: celebitolga Date: Mon, 17 Jul 2023 17:35:13 +0300 Subject: [PATCH 4/8] [REFACTOR] Textarea's status prop can be undefined --- ikas-theme/src/components/components/textarea/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) => { From 750b3425d80a7d49622743489b8cfe829e44efcc Mon Sep 17 00:00:00 2001 From: celebitolga Date: Mon, 17 Jul 2023 17:35:25 +0300 Subject: [PATCH 5/8] [ADD] ProductReviews component added --- .../public/locales/en/product-reviews.json | 23 + .../src/components/__generated__/editor.tsx | 3 +- .../__generated__/pages/[slug]/index.ts | 3 +- .../src/components/__generated__/types.ts | 4 + .../src/components/product-reviews/index.tsx | 169 +++++ .../product-reviews/review-form/index.tsx | 142 ++++ .../product-reviews/review-form/style.ts | 21 + .../product-reviews/review/index.tsx | 58 ++ .../product-reviews/review/style.ts | 56 ++ .../product-reviews/stars/index.tsx | 125 ++++ .../components/product-reviews/stars/style.ts | 45 ++ .../src/components/product-reviews/style.ts | 61 ++ ikas-theme/src/pages/api/getConfig.ts | 4 + ikas-theme/src/theme.json | 696 +++++++++++++++--- 14 files changed, 1320 insertions(+), 90 deletions(-) create mode 100644 ikas-theme/public/locales/en/product-reviews.json create mode 100644 ikas-theme/src/components/product-reviews/index.tsx create mode 100644 ikas-theme/src/components/product-reviews/review-form/index.tsx create mode 100644 ikas-theme/src/components/product-reviews/review-form/style.ts create mode 100644 ikas-theme/src/components/product-reviews/review/index.tsx create mode 100644 ikas-theme/src/components/product-reviews/review/style.ts create mode 100644 ikas-theme/src/components/product-reviews/stars/index.tsx create mode 100644 ikas-theme/src/components/product-reviews/stars/style.ts create mode 100644 ikas-theme/src/components/product-reviews/style.ts create mode 100644 ikas-theme/src/pages/api/getConfig.ts 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..c1d6f8d --- /dev/null +++ b/ikas-theme/public/locales/en/product-reviews.json @@ -0,0 +1,23 @@ +{ + "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", + "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/product-reviews/index.tsx b/ikas-theme/src/components/product-reviews/index.tsx new file mode 100644 index 0000000..8916dd4 --- /dev/null +++ b/ikas-theme/src/components/product-reviews/index.tsx @@ -0,0 +1,169 @@ +import React, { useState, useRef } from "react"; +import { observer } from "mobx-react-lite"; +import { + IkasBaseStore, + IkasCustomerReviewList, + useTranslation, +} from "@ikas/storefront"; +import { useRouter } from "next/router"; + +// Types +import { ProductReviewsProps } from "../__generated__/types"; + +// Components +import { Container } from "src/components/components/container"; +import Pagination from "../components/pagination"; +import Button from "../components/button"; +import ReviewForm from "./review-form"; +import Review from "./review"; +import Stars, { type StarType } from "./stars"; + +// Utils +import urls from "src/utils/urls"; + +// Styles +import * as S from "./style"; + +const ProductReviews = (props: ProductReviewsProps) => { + const { productDetail } = props; + + const { t } = useTranslation(); + const store = IkasBaseStore.getInstance(); + const router = useRouter(); + const namespace = "product-reviews"; + + // States + const [isFormVisible, setFormVisible] = useState(false); + const [isWriteReviewButtonHidden, setHiddenWriteReviewButton] = + useState(false); + const [customerReviewList, setCustomerReviewList] = + useState(null); + + // Refs + const productReviewRef = useRef(null); + + const getCustomerReviews = async () => { + try { + const result = await productDetail.getCustomerReviews({ limit: 6 }); + setCustomerReviewList(result); + } catch (error) { + console.log("error", error); + } + }; + + const onWriteReviewButtonClick = () => { + if ( + productDetail.isCustomerReviewLoginRequired && + !store.customerStore.customer + ) { + const route = decodeURIComponent( + urls.login + "?redirect=" + productDetail.href + ); + + router.push(route); + } else { + setFormVisible((prev) => !prev); + } + }; + + const onReviewFormSubmitSuccess = () => { + setHiddenWriteReviewButton(true); + }; + + const onPageChange = async (page: number) => { + await customerReviewList?.getPage(page); + + window.scrollTo({ + top: productReviewRef.current?.offsetTop ?? 0, + behavior: "smooth", + }); + }; + + React.useEffect(() => { + setFormVisible(false); + getCustomerReviews(); + }, [productDetail]); + + return ( + + + + {t(`${namespace}:title`)} + + + {customerReviewList && ( + <> + {customerReviewList && customerReviewList.data?.length ? ( + + + + + {t(`${namespace}:basedOnXReviews`, { + x: productDetail.reviewCount || "0", + })} + + + ) : ( + + {t(`${namespace}:emptyReview`)} + + )} + + )} + {!isWriteReviewButtonHidden && + productDetail.isCustomerReviewEnabled && ( + + )} + + +
+ {isFormVisible && ( + + )} + {customerReviewList && customerReviewList.data?.length > 0 && ( + + {customerReviewList.data.map((review, index) => ( + + ))} + + )} +
+ {customerReviewList && ( + + )} +
+
+
+
+
+ ); +}; + +export default observer(ProductReviews); diff --git a/ikas-theme/src/components/product-reviews/review-form/index.tsx b/ikas-theme/src/components/product-reviews/review-form/index.tsx new file mode 100644 index 0000000..49fa503 --- /dev/null +++ b/ikas-theme/src/components/product-reviews/review-form/index.tsx @@ -0,0 +1,142 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { + IkasProduct, + useTranslation, + CustomerReviewForm, +} from "@ikas/storefront"; + +// Compontents +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"; + +// Styles +import * as S from "./style"; + +const REVIEW_TITLE_MAX_LENGTH = 64; +const REVIEW_COMMENT_MAX_LENGTH = 256; + +function ReviewForm({ + namespace, + product, + onSubmitSuccess, +}: { + namespace: string; + product: IkasProduct; + onSubmitSuccess: () => void; +}) { + const { t } = useTranslation(); + + // States + const [responseStatus, setResponseStatus] = useState< + "success" | "error" | undefined + >(); + const [isPending, setPending] = useState(false); + const [form] = useState( + new CustomerReviewForm({ + productId: product.id, + message: { starRule: t(`${namespace}: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); + } + }; + + return ( + + + {t(`${namespace}: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); + }} + /> + + +