Skip to content

Commit

Permalink
Merge pull request #1 from ikascom/product-reviews
Browse files Browse the repository at this point in the history
Product reviews
  • Loading branch information
dizefurkan authored Jul 18, 2023
2 parents befbb72 + 2444916 commit a41bb7d
Show file tree
Hide file tree
Showing 24 changed files with 1,469 additions and 92 deletions.
24 changes: 24 additions & 0 deletions ikas-theme/public/locales/en/product-reviews.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 2 additions & 1 deletion ikas-theme/src/components/__generated__/editor.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ikas-theme/src/components/__generated__/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ikas-theme/src/components/components/alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type FormAlertType = {
type AlertComponentProps = FormAlertType & {
closable?: boolean;
onClose?: () => void;
style?: React.CSSProperties;
};

const AlertComponent = (props: AlertComponentProps) => {
Expand All @@ -24,7 +25,7 @@ const AlertComponent = (props: AlertComponentProps) => {

if (!isVisible) return null;
return (
<S.AlertWrapper $status={props.status}>
<S.AlertWrapper $status={props.status} style={props.style}>
{props.title && <S.AlertTitle>{props.title}</S.AlertTitle>}
<S.AlertText>{props.text}</S.AlertText>
{props.closable && <S.CloseButton onClick={onClose}>x</S.CloseButton>}
Expand Down
2 changes: 1 addition & 1 deletion ikas-theme/src/components/components/textarea/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
51 changes: 51 additions & 0 deletions ikas-theme/src/components/product-reviews/detail/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ReviewsSummary
productDetail={productDetail}
customerReviewList={customerReviewList}
isFormVisible={isFormVisible}
onWriteReviewButtonClick={onWriteReviewButtonClick}
/>

<div ref={reviewsElementRef}>
<Reviews customerReviewList={customerReviewList} />
</div>

{customerReviewList && (
<Pagination
pageCount={customerReviewList.pageCount}
page={customerReviewList.page}
loading={customerReviewList.isLoading}
hasPrev={customerReviewList.hasPrev}
hasNext={customerReviewList.hasNext}
getPage={onPageChange}
count={customerReviewList.count}
/>
)}
</>
);
};

export default observer(Detail);
153 changes: 153 additions & 0 deletions ikas-theme/src/components/product-reviews/detail/review-form/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomerReviewForm>(
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 (
<S.ReviewForm>
<S.Wrapper>
<S.Title>{t(`${NS}:formTitle`)}</S.Title>

{responseStatus === "success" && (
<AlertComponent status="success" text={t(`${NS}:form.successText`)} />
)}

{responseStatus !== "success" && (
<>
{responseStatus === "error" && (
<AlertComponent
status="error"
text={t(`${NS}:form.errorText`)}
style={{
marginBottom: "1rem",
}}
/>
)}

<Form onSubmit={onSubmit}>
<FormItem
status={form.starErrorMessage ? "error" : undefined}
help={form.starErrorMessage}
label={t(`${NS}:form.reviewStarts`)}
>
<Stars
star={form.star as 1 | 2 | 3 | 4 | 5}
onClick={(star) => form.onStarChange(star)}
/>
</FormItem>
<FormItem label={t(`${NS}:form.reviewTitle`)}>
<Input
maxLength={REVIEW_TITLE_MAX_LENGTH}
value={form.title}
onChange={(event) => {
if (event.target.value.length > REVIEW_TITLE_MAX_LENGTH)
return;

form.onTitleChange(event.target.value);
}}
/>
</FormItem>
<FormItem label={t(`${NS}:form.bodyOfReview`)}>
<TextArea
maxLength={REVIEW_COMMENT_MAX_LENGTH}
value={form.comment}
onChange={(event) => {
if (event.target.value.length > REVIEW_COMMENT_MAX_LENGTH)
return;

form.onCommentChange(event.target.value);
}}
style={{
minHeight: "150px",
}}
/>
</FormItem>
<Button size="small" loading={isPending} disabled={isPending}>
{t(`${NS}:form.submitReview`)}
</Button>
</Form>
</>
)}
</S.Wrapper>
</S.ReviewForm>
);
};

export default observer(ReviewForm);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styled from "styled-components";

import breakpoints from "src/styles/breakpoints";

export const ReviewForm = styled.div`
margin-top: 1rem;
padding-block: 2rem;
border-top: 1px solid ${(props) => props.theme.color.border};
`;

export const Wrapper = styled.div`
max-width: ${breakpoints.md};
margin-left: auto;
margin-right: auto;
`;

export const Title = styled.h3`
margin-bottom: 1.25rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState } from "react";
import { observer } from "mobx-react-lite";
import { useTranslation } from "@ikas/storefront";
import { ProductReviewsProps } from "src/components/__generated__/types";

import ReviewForm from "../review-form";
import Stars, { type StarType } from "../stars";
import Button from "src/components/components/button";

import useProductReviews from "src/components/product-reviews/useProductReviews";

import { NS } from "src/components/product-reviews";

import * as S from "./style";

// prettier-ignore
type Props = ProductReviewsProps & {
isFormVisible: ReturnType<typeof useProductReviews>["isFormVisible"];
customerReviewList: ReturnType<typeof useProductReviews>["customerReviewList"];
onWriteReviewButtonClick: ReturnType<typeof useProductReviews>["onWriteReviewButtonClick"];
};

const ReviewsSummary = (props: Props) => {
const {
productDetail,
customerReviewList,
isFormVisible,
onWriteReviewButtonClick,
} = props;

const { t } = useTranslation();

const [isWriteReviewButtonHidden, setHiddenWriteReviewButton] =
useState(false);

const isWriteReviewButtonVisible =
!isWriteReviewButtonHidden && productDetail.isCustomerReviewEnabled;

const isPreviewVisible =
customerReviewList && customerReviewList.data?.length > 0;

return (
<S.ReviewsSummary>
<S.ReviewsHeader>
{isPreviewVisible ? (
<S.Preview>
<Stars
title={t(`${NS}:xStar`, {
x: productDetail.averageRating || "0" })}
editable={false}
size="24px"
star={(productDetail.averageRating as StarType) || 0}
/>

<S.PreviewDesciption>
{t(`${NS}:basedOnXReviews`, {
x: productDetail.reviewCount || "0" })}
</S.PreviewDesciption>
</S.Preview>
) : (
<S.PreviewEmpty>{t(`${NS}:emptyReview`)}</S.PreviewEmpty>
)}

{isWriteReviewButtonVisible && (
<Button onClick={onWriteReviewButtonClick}>
{isFormVisible
? t(`${NS}:closeReviewForm`)
: t(`${NS}:writeAReview`)}
</Button>
)}
</S.ReviewsHeader>

<ReviewForm
product={productDetail}
onSubmitSuccess={() => setHiddenWriteReviewButton(true)}
visible={isFormVisible}
/>
</S.ReviewsSummary>
);
};

export default observer(ReviewsSummary);
Loading

0 comments on commit a41bb7d

Please sign in to comment.