Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rating summary #2682

Open
wants to merge 1 commit into
base: feat/reviews-and-ratings
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,8 @@ export type {
SKUMatrixTriggerProps,
SKUMatrixSidebarProps,
} from './organisms/SKUMatrix'

export {
default as RatingSummary,
RatingSummaryProps,
} from './organisms/RatingSummary'
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { forwardRef, type HTMLAttributes } from 'react'
import RatingDistributionItem from './RatingDistributionItem'

export interface RatingDistributionProps
extends HTMLAttributes<HTMLOListElement> {
/**
* The rating distribution
*/
distribution: {
starsOne: number
starsTwo: number
starsThree: number
starsFour: number
starsFive: number
}
/**
* Optional test ID for testing.
*/
Comment on lines +16 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to keep the pattern

Suggested change
/**
* Optional test ID for testing.
*/
/**
* ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
*/

testId?: string
}

export const RatingDistribution = forwardRef<
HTMLOListElement,
RatingDistributionProps
>(function ProgressStatus(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be RatingDistribution ?

Suggested change
>(function ProgressStatus(
>(function RatingDistribution(

{
distribution: { starsFive, starsFour, starsThree, starsTwo, starsOne },
testId = 'fs-rating-distribution',
...props
},
ref
) {
return (
<ol ref={ref} data-fs-rating-distribution data-testid={testId} {...props}>
<RatingDistributionItem ratingIndex={5} value={starsFive} />
<RatingDistributionItem ratingIndex={4} value={starsFour} />
<RatingDistributionItem ratingIndex={3} value={starsThree} />
<RatingDistributionItem ratingIndex={2} value={starsTwo} />
<RatingDistributionItem ratingIndex={1} value={starsOne} />
</ol>
)
})

export default RatingDistribution
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { forwardRef, type HTMLAttributes } from 'react'
import Icon from '../../atoms/Icon'

export interface RatingDistributionItemProps
extends HTMLAttributes<HTMLLIElement> {
ratingIndex: number
/* Current value of the progress */
value: number
/* Optional test ID for testing*/
testId?: string
}

const ItemStar = ({ ratingIndex }: { ratingIndex: number }) => (
<span data-fs-rating-distribution-item-star>
<p>{ratingIndex}</p>
<Icon data-fs-rating-distribution-item-star-icon name="Star" />
</span>
)

const ItemProgressBar = ({ value }: { value: number }) => (
<div data-fs-rating-distribution-item-progress-bar>
<div data-fs-rating-distribution-item-progress-bar-track>
<div
data-fs-rating-distribution-item-progress-bar-fill
style={{ width: `${value}%` }}
/>
</div>
</div>
)

const ItemPercentage = ({ percentage }: { percentage: number }) => (
<p data-fs-rating-distribution-item-percentage>{percentage}%</p>
)

const RatingDistributionItem = forwardRef<
HTMLLIElement,
RatingDistributionItemProps
>(function RatingDistributionItem(
{ ratingIndex, value, testId = 'fs-rating-distribution-item' },
ref
) {
return (
<li ref={ref} data-fs-distribution-item data-testid={testId}>
<ItemStar ratingIndex={ratingIndex} />
<ItemProgressBar value={value} />
<ItemPercentage percentage={value} />
</li>
)
})
export default RatingDistributionItem
124 changes: 124 additions & 0 deletions packages/components/src/organisms/RatingSummary/RatingSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { forwardRef, type HTMLAttributes } from 'react'
import Rating from '../../molecules/Rating'
import Button from '../../atoms/Button'
import RatingDistribution from './RatingDistribution'

export interface RatingSummaryProps extends HTMLAttributes<HTMLDivElement> {
/**
* The average rating of the product
*/
average: number
/**
* The total number of reviews of the product
*/
totalCount: number
/**
* The distribution of the ratings
*/
distribution: {
starsOne: number
starsTwo: number
starsThree: number
starsFour: number
starsFive: number
}
textLabels?: {
ratingCounter?: {
noReviewsText?: string
singleReviewText?: string
multipleReviewsText?: string
}
createReviewButton?: {
noReviewsText?: string
defaultText?: string
}
}
/**
* Optional test ID for testing.
*/
testId?: string
}

const RatingSummaryHeader = ({
average,
totalCount,
noReviewsText,
singleReviewText,
multipleReviewsText,
}: {
average: number
totalCount: number
noReviewsText: string
singleReviewText: string
multipleReviewsText: string
}) => {
const formattedAverage = average > 0 ? average.toPrecision(2) : ''
const totalCountText =
totalCount > 0
? `${totalCount} ${totalCount === 1 ? singleReviewText : multipleReviewsText}`
: noReviewsText

return (
<div data-fs-rating-summary-header>
<h2 data-fs-rating-summary-header-average>{formattedAverage}</h2>
<Rating value={average} />
<p data-fs-rating-summary-header-total-count>{totalCountText}</p>
</div>
)
}

export const RatingSummary = forwardRef<HTMLDivElement, RatingSummaryProps>(
function RatingSummary(
{
average,
totalCount,
distribution,
textLabels: {
ratingCounter: {
noReviewsText: ratingCounterNoReviewsText = 'No reviews yet',
singleReviewText: ratingCounterSingleReviewText = 'Review',
multipleReviewsText: ratingCounterMultipleReviewsText = 'Reviews',
} = {},
createReviewButton: {
noReviewsText:
createReviewButtonNoReviewsText = 'Write the first review',
defaultText: createReviewButtonDefaultText = 'Write a review',
} = {},
} = {},
testId = 'fs-rating-summary',
...props
},
ref
) {
const buttonText =
totalCount > 0
? createReviewButtonDefaultText
: createReviewButtonNoReviewsText

return (
<div ref={ref} data-fs-rating-summary data-testid={testId} {...props}>
<RatingSummaryHeader
average={average}
totalCount={totalCount}
noReviewsText={ratingCounterNoReviewsText}
singleReviewText={ratingCounterSingleReviewText}
multipleReviewsText={ratingCounterMultipleReviewsText}
/>
<Button
variant="secondary"
onClick={() => alert('Write a review button clicked!')}
>
{buttonText}
</Button>
{totalCount > 0 && (
<RatingDistribution
data-fs-rating-summary-distribution
distribution={distribution}
/>
)}
</div>
)
}
)

export default RatingSummary
1 change: 1 addition & 0 deletions packages/components/src/organisms/RatingSummary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, type RatingSummaryProps } from './RatingSummary'
4 changes: 2 additions & 2 deletions packages/core/@generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const documents = {
types.ProductSummary_ProductFragmentDoc,
'\n fragment Filter_facets on StoreFacet {\n ... on StoreFacetRange {\n key\n label\n\n min {\n selected\n absolute\n }\n\n max {\n selected\n absolute\n }\n\n __typename\n }\n ... on StoreFacetBoolean {\n key\n label\n values {\n label\n value\n selected\n quantity\n }\n\n __typename\n }\n }\n':
types.Filter_FacetsFragmentDoc,
'\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n rating {\n average\n totalCount\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n':
'\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n rating {\n average\n totalCount\n distribution {\n starsOne\n starsTwo\n starsThree\n starsFour\n starsFive\n }\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n':
types.ProductDetailsFragment_ProductFragmentDoc,
'\n fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {\n id: productID\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n\t\t\t\t\tsku\n name\n image {\n url\n alternateName\n }\n offers {\n highPrice\n lowPrice\n lowPriceWithTaxes\n offerCount\n priceCurrency\n offers {\n listPrice\n listPriceWithTaxes\n sellingPrice\n priceCurrency\n price\n priceWithTaxes\n priceValidUntil\n itemCondition\n availability\n quantity\n }\n }\n additionalProperty {\n propertyID\n value\n name\n valueReference\n }\n }\n }\n }\n }\n':
types.ProductSkuMatrixSidebarFragment_ProductFragmentDoc,
Expand Down Expand Up @@ -78,7 +78,7 @@ export function gql(
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(
source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n rating {\n average\n totalCount\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n'
source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n rating {\n average\n totalCount\n distribution {\n starsOne\n starsTwo\n starsThree\n starsFour\n starsFive\n }\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n'
): typeof import('./graphql').ProductDetailsFragment_ProductFragmentDoc
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
Expand Down
47 changes: 42 additions & 5 deletions packages/core/@generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,7 +1324,17 @@ export type ProductDetailsFragment_ProductFragment = {
value: any
valueReference: any
}>
rating: { average: number; totalCount: number }
rating: {
average: number
totalCount: number
distribution: {
starsOne: number
starsTwo: number
starsThree: number
starsFour: number
starsFive: number
}
}
}

export type ProductSkuMatrixSidebarFragment_ProductFragment = {
Expand Down Expand Up @@ -1462,7 +1472,17 @@ export type ServerProductQueryQuery = {
value: any
valueReference: any
}>
rating: { average: number; totalCount: number }
rating: {
average: number
totalCount: number
distribution: {
starsOne: number
starsTwo: number
starsThree: number
starsFour: number
starsFive: number
}
}
}
}

Expand Down Expand Up @@ -1763,7 +1783,17 @@ export type ClientProductQueryQuery = {
value: any
valueReference: any
}>
rating: { average: number; totalCount: number }
rating: {
average: number
totalCount: number
distribution: {
starsOne: number
starsTwo: number
starsThree: number
starsFour: number
starsFive: number
}
}
}
}

Expand Down Expand Up @@ -2065,6 +2095,13 @@ export const ProductDetailsFragment_ProductFragmentDoc =
rating {
average
totalCount
distribution {
starsOne
starsTwo
starsThree
starsFour
starsFive
}
}
...CartProductItem
}
Expand Down Expand Up @@ -2342,7 +2379,7 @@ export const ServerCollectionPageQueryDocument = {
export const ServerProductQueryDocument = {
__meta__: {
operationName: 'ServerProductQuery',
operationHash: '0a3f449b2a88dc1f692fe1ae981370be53a02cce',
operationHash: '312acab1a14a3b35d6c70887b5cf289b5cf6cf76',
},
} as unknown as TypedDocumentString<
ServerProductQueryQuery,
Expand Down Expand Up @@ -2396,7 +2433,7 @@ export const ClientProductGalleryQueryDocument = {
export const ClientProductQueryDocument = {
__meta__: {
operationName: 'ClientProductQuery',
operationHash: 'e1599e2efe3664aad09c026919c1c104b4085f00',
operationHash: 'e678f7fc4d59a3e4cbf61295fc1e669f44724464',
},
} as unknown as TypedDocumentString<
ClientProductQueryQuery,
Expand Down
43 changes: 43 additions & 0 deletions packages/core/cms/faststore/sections.json
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,49 @@
"title": "Title",
"type": "string",
"default": "Reviews"
},
"ratingSummary": {
"title": "Rating Summary",
"type": "object",
"properties": {
"ratingCounter": {
"title": "Rating Counter",
"type": "object",
"properties": {
"noReviewsText": {
"title": "No reviews text",
"type": "string",
"default": "No reviews yet"
},
"multipleReviewsText": {
"title": "Multiple reviews text",
"type": "string",
"default": "Reviews"
},
"singleReviewText": {
"title": "Single review text",
"type": "string",
"default": "Review"
}
}
}
}
},
"createReviewButton": {
"title": "Create Review Button",
"type": "object",
"properties": {
"noReviewsText": {
"title": "No reviews Text",
"type": "string",
"default": "Write the first review"
},
"defaultText": {
"title": "Default Text",
"type": "string",
"default": "Write a review"
}
}
}
}
}
Expand Down
Loading
Loading