Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b02410d
feat: create product thumbnail component
artursantiago Feb 5, 2025
6487abe
fix: add data-attribute
artursantiago Feb 21, 2025
c4c9e1a
feat: use children instead of title
artursantiago Feb 21, 2025
6815f62
style: add spaces to improve readability
artursantiago Feb 21, 2025
5cb36a0
feat: remove overflow hidden
artursantiago Feb 27, 2025
bf05bee
refactor(ProductThumbnail): move it inside the ReviewModal
artursantiago Mar 11, 2025
385a7ee
feat(AddReviewModal): create composable AddReviewModal component
artursantiago Feb 19, 2025
c499dfa
fix: adjust global components
artursantiago Feb 27, 2025
b18feef
feat: add create product review types to gql generated
artursantiago Feb 27, 2025
37f27e0
feat: create hook useAddReview
artursantiago Feb 27, 2025
c3a45d9
feat: add addReviewModal cms settings
artursantiago Feb 27, 2025
9166248
feat: add utils function isUUID
artursantiago Feb 27, 2025
c1ba945
feat: create add review modal form
artursantiago Feb 27, 2025
e8f95de
feat: passing product to add review modal
artursantiago Feb 27, 2025
2a5fee0
feat: add default values to modal
artursantiago Feb 27, 2025
3c9efe0
feat(ReviewsAndRatings): adjust to use ReviewModal and ProductThumbna…
artursantiago Mar 11, 2025
ac54c87
feat(ReviewModal): set label props as optional
artursantiago Mar 12, 2025
637acd2
fix: review modal imports
artursantiago Mar 14, 2025
2d7ea41
fix: default text to review headline field
artursantiago Mar 20, 2025
be966b4
feat: remove unused pushToast
artursantiago Mar 20, 2025
5347c10
feat: use productReviewId as alias for CreateProductReviewMutation
artursantiago Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export interface CheckboxFieldProps extends CheckboxProps {
* Control the vertical alignment of the checkbox in relation to the label (center, top, bottom).
*/
alignment?: 'center' | 'top' | 'bottom'
/**
* Checkbox Component's ref.
*/
checkboxRef?: React.MutableRefObject<HTMLInputElement>
}

const CheckboxField = forwardRef<HTMLDivElement, CheckboxFieldProps>(
Expand All @@ -29,6 +33,7 @@ const CheckboxField = forwardRef<HTMLDivElement, CheckboxFieldProps>(
error,
disabled,
alignment = 'center',
checkboxRef,
...otherProps
},
ref
Expand All @@ -49,6 +54,7 @@ const CheckboxField = forwardRef<HTMLDivElement, CheckboxFieldProps>(
name={name}
defaultChecked={checked}
disabled={disabled}
ref={checkboxRef}
{...otherProps}
/>
<div data-fs-checkbox-field-content>
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/molecules/Rating/Rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ const Rating = forwardRef<HTMLUListElement, RatingProps>(function Rating(
onMouseEnter={() => setHover(tempIndex)}
onMouseLeave={() => setHover(value)}
disabled={disabled}
type="button"
/>
) : (
<>
Expand Down
8 changes: 8 additions & 0 deletions packages/core/@generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const documents = {
types.ClientProductGalleryQueryDocument,
'\n query ClientProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ClientProduct\n product(locator: $locator) {\n ...ProductDetailsFragment_product\n }\n }\n':
types.ClientProductQueryDocument,
'\n mutation CreateProductReview($data: ICreateProductReview!) {\n productReviewId: createProductReview(data: $data)\n }\n':
types.CreateProductReviewDocument,
'\n query ClientSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n products {\n ...ProductSummary_product\n }\n }\n products {\n pageInfo {\n totalCount\n }\n }\n metadata {\n ...SearchEvent_metadata\n }\n }\n }\n':
types.ClientSearchSuggestionsQueryDocument,
'\n query ClientTopSearchSuggestionsQuery(\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]\n ) {\n ...ClientTopSearchSuggestions\n search(first: 5, term: $term, selectedFacets: $selectedFacets) {\n suggestions {\n terms {\n value\n }\n }\n }\n }\n':
Expand Down Expand Up @@ -182,6 +184,12 @@ export function gql(
export function gql(
source: '\n query ClientProductQuery($locator: [IStoreSelectedFacet!]!) {\n ...ClientProduct\n product(locator: $locator) {\n ...ProductDetailsFragment_product\n }\n }\n'
): typeof import('./graphql').ClientProductQueryDocument
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(
source: '\n mutation CreateProductReview($data: ICreateProductReview!) {\n productReviewId: createProductReview(data: $data)\n }\n'
): typeof import('./graphql').CreateProductReviewDocument
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
15 changes: 15 additions & 0 deletions packages/core/@generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,12 @@ export type ClientProductQueryQuery = {
}
}

export type CreateProductReviewMutationVariables = Exact<{
data: ICreateProductReview
}>

export type CreateProductReviewMutation = { productReviewId: string }

export type ClientSearchSuggestionsQueryQueryVariables = Exact<{
term: Scalars['String']['input']
selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>
Expand Down Expand Up @@ -2439,6 +2445,15 @@ export const ClientProductQueryDocument = {
ClientProductQueryQuery,
ClientProductQueryQueryVariables
>
export const CreateProductReviewDocument = {
__meta__: {
operationName: 'CreateProductReview',
operationHash: '446f171f0f6ed728b011ae9218d726897f27641f',
},
} as unknown as TypedDocumentString<
CreateProductReviewMutation,
CreateProductReviewMutationVariables
>
export const ClientSearchSuggestionsQueryDocument = {
__meta__: {
operationName: 'ClientSearchSuggestionsQuery',
Expand Down
95 changes: 95 additions & 0 deletions packages/core/cms/faststore/sections.json
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,86 @@
"type": "string",
"default": "Close Review Modal"
},
"ratingField": {
"title": "Rating input field",
"type": "object",
"properties": {
"label": {
"title": "Rating input field label",
"type": "string",
"default": "Rate the product from 1 to 5 stars"
},
"requiredErrorMessage": {
"title": "Input field error message",
"type": "string",
"default": "This field is required"
}
}
},
"reviewTitleField": {
"title": "Review title input field",
"type": "object",
"properties": {
"label": {
"title": "Review title input field label",
"type": "string",
"default": "Review headline"
},
"requiredErrorMessage": {
"title": "Review title field error message",
"type": "string",
"default": "This field is required"
}
}
},
"reviewerNameField": {
"title": "Reviewer name input field",
"type": "object",
"properties": {
"label": {
"title": "Reviewer name input field label",
"type": "string",
"default": "Name"
},
"requiredErrorMessage": {
"title": "Reviewer name field error message",
"type": "string",
"default": "This field is required"
}
}
},
"reviewTextField": {
"title": "Reviewer text input field",
"type": "object",
"properties": {
"label": {
"title": "Reviewer text input field label",
"type": "string",
"default": "Share your thoughts about the product. How would you describe its quality?"
},
"requiredErrorMessage": {
"title": "Reviewer text field error message",
"type": "string",
"default": "This field is required"
}
}
},
"privacyPolicyCheckboxField": {
"title": "Privacy Policy Checkbox Field",
"type": "object",
"properties": {
"label": {
"title": "Privacy policy checkbox field label",
"type": "string",
"default": "I confirm that I agree to the Privacy Policy, Terms of Use, and Terms of Service. I acknowledge that my review may be used for marketing purposes by the company or its partners. I understand that my rating and review may be visible publicly, may include a “Verified buyer” badge, and that my data may be associated with my review."
},
"requiredErrorMessage": {
"title": "Privacy policy checkbox field error message",
"type": "string",
"default": "This field is required"
}
}
},
"cancelButtonLabel": {
"title": "Cancel button label",
"type": "string",
Expand All @@ -1043,6 +1123,21 @@
"title": "Submit review button label",
"type": "string",
"default": "Submit your review"
},
"successTitle": {
"title": "Success title",
"type": "string",
"default": "Success!"
},
"successSubtitle": {
"title": "Success subtitle",
"type": "string",
"default": "Your review has been submitted."
},
"successButtonLabel": {
"title": "Success button label",
"type": "string",
"default": "Back to reviews"
}
}
}
Expand Down

This file was deleted.

129 changes: 90 additions & 39 deletions packages/core/src/components/reviews/ReviewModal/ReviewModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import {
Button as UIButton,
type ModalProps as UIModalProps,
type ModalHeaderProps as UIModalHeaderProps,
type ModalBodyProps as UIModalBodyProps,
type ModalFooterProps as UIModalFooterProps,
useUI,
} from '@faststore/ui'

import styles from './section.module.scss'

import dynamic from 'next/dynamic'
import { useCallback, useEffect, useState } from 'react'
import { useAddReview } from 'src/sdk/reviews/useAddReview'

import isUUID from 'src/utils/isUUID'
import type {
ReviewModalFormData,
ReviewModalFormProps,
} from './ReviewModalForm'
import type { ReviewModalSuccessProps } from './ReviewModalSuccess'

const UIModal = dynamic<UIModalProps>(
() =>
Expand All @@ -27,23 +33,32 @@ const UIModalHeader = dynamic<UIModalHeaderProps>(
{ ssr: false }
)

const UIModalBody = dynamic<UIModalBodyProps>(
const ReviewModalSuccess = dynamic(
() =>
import(/* webpackChunkName: "UIModalBody" */ '@faststore/ui').then(
(module) => module.ModalBody
import(
/* webpackChunkName: "ReviewModalSuccess" */ 'src/components/reviews/ReviewModal/ReviewModalSuccess'
),
{ ssr: false }
)

const UIModalFooter = dynamic<UIModalFooterProps>(
const ReviewModalForm = dynamic(
() =>
import(/* webpackChunkName: "UIModalFooter" */ '@faststore/ui').then(
(module) => module.ModalFooter
import(
/* webpackChunkName: "ReviewModalForm" */ 'src/components/reviews/ReviewModal/ReviewModalForm'
),
{ ssr: false }
)

export interface ReviewModalProps {
const UIIcon = dynamic(
() => import('@faststore/ui').then((module) => module.Icon),
{
ssr: false,
}
)

export interface ReviewModalProps
extends Omit<ReviewModalFormProps, 'onSubmit' | 'onCancel'>,
ReviewModalSuccessProps {
/**
* The review modal's title.
*/
Expand All @@ -52,37 +67,68 @@ export interface ReviewModalProps {
* Close button aria-label.
*/
closeButtonAriaLabel?: string
/**
* Cancel button label.
*/
cancelButtonLabel?: string
/**
* Submit button label.
*/
submitButtonLabel?: string
}

function ReviewModal({
title = 'Add a review',
closeButtonAriaLabel = 'Close Review Modal',
cancelButtonLabel = 'Cancel',
submitButtonLabel = 'Submit your review',
product,
successTitle,
successSubtitle,
successButtonLabel,
...formProps
}: ReviewModalProps) {
const { closeReviewModal } = useUI()
const { pushToast, closeReviewModal } = useUI()
const { createProductReview, data, error, loading } = useAddReview()
const [submittedReview, setSubmittedReview] =
useState<ReviewModalFormData | null>(null)
const [isSuccess, setIsSuccess] = useState(false)

const handleSubmit = async () => {
// TODO: send review
function pushErrorToast(message = 'Something went wrong.') {
pushToast({
title: 'Oops.',
message,
status: 'ERROR',
icon: <UIIcon name="CircleWavyWarning" width={30} height={30} />,
})
}

function handleOnClose() {
closeReviewModal()
}
const handleSubmit = useCallback(
(formData: ReviewModalFormData) => {
createProductReview({
data: {
productId: product.id,
rating: formData.rating,
title: formData.title,
text: formData.text,
reviewerName: formData.reviewerName,
},
})
.then(() => {
setSubmittedReview(formData)
})
.catch((error) => {
pushErrorToast(error.message)
})
},
[product.id, createProductReview]
)

useEffect(() => {
if (submittedReview) {
if (data?.productReviewId && isUUID(data.productReviewId)) {
setIsSuccess(true)
} else {
pushErrorToast(data?.productReviewId)
}
}
}, [submittedReview, data, error])

return (
<UIModal
data-fs-review-modal
title={title}
onTransitionEnd={(_, fade) => fade === 'out' && handleOnClose()}
onTransitionEnd={(_, fade) => fade === 'out' && closeReviewModal()}
overlayProps={{
className: `section ${styles.section} section-review-modal`,
}}
Expand All @@ -99,18 +145,23 @@ function ReviewModal({
'aria-label': closeButtonAriaLabel,
}}
/>
<UIModalBody data-fs-review-modal-body>
{/* TODO: ReviewModal form will go here in another PR */}
body
</UIModalBody>
<UIModalFooter data-fs-review-modal-footer>
<UIButton variant="secondary" onClick={fadeOut}>
{cancelButtonLabel}
</UIButton>
<UIButton variant="primary" onClick={handleSubmit}>
{submitButtonLabel}
</UIButton>
</UIModalFooter>
{isSuccess ? (
<ReviewModalSuccess
successTitle={successTitle}
successSubtitle={successSubtitle}
successButtonLabel={successButtonLabel}
close={fadeOut}
review={submittedReview}
/>
) : (
<ReviewModalForm
{...formProps}
onSubmit={handleSubmit}
onCancel={fadeOut}
product={product}
loading={loading}
/>
)}
</>
)}
</UIModal>
Expand Down
Loading
Loading