From a846caf6927154b3f2b99cd749ec2aa92147f577 Mon Sep 17 00:00:00 2001 From: gutchenzo Date: Thu, 20 Feb 2025 12:11:11 -0300 Subject: [PATCH] feat: implements new organism component "RatingSummary" --- packages/components/src/index.ts | 5 + .../RatingSummary/RatingDistribution.tsx | 44 +++++++ .../RatingSummary/RatingDistributionItem.tsx | 50 +++++++ .../organisms/RatingSummary/RatingSummary.tsx | 124 ++++++++++++++++++ .../src/organisms/RatingSummary/index.ts | 1 + packages/core/@generated/gql.ts | 4 +- packages/core/@generated/graphql.ts | 47 ++++++- packages/core/cms/faststore/sections.json | 43 ++++++ .../ProductDetails/ProductDetails.tsx | 7 + .../ReviewsAndRatings/section.module.scss | 4 +- .../ReviewsAndRatings/ReviewsAndRatings.tsx | 16 +++ .../components/molecules/Rating/styles.scss | 2 + .../organisms/RatingSummary/styles.scss | 121 +++++++++++++++++ packages/ui/src/styles/components.scss | 1 + 14 files changed, 461 insertions(+), 8 deletions(-) create mode 100644 packages/components/src/organisms/RatingSummary/RatingDistribution.tsx create mode 100644 packages/components/src/organisms/RatingSummary/RatingDistributionItem.tsx create mode 100644 packages/components/src/organisms/RatingSummary/RatingSummary.tsx create mode 100644 packages/components/src/organisms/RatingSummary/index.ts create mode 100644 packages/ui/src/components/organisms/RatingSummary/styles.scss diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 746ca7b222..12254489fc 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -382,3 +382,8 @@ export type { SKUMatrixTriggerProps, SKUMatrixSidebarProps, } from './organisms/SKUMatrix' + +export { + default as RatingSummary, + RatingSummaryProps, +} from './organisms/RatingSummary' diff --git a/packages/components/src/organisms/RatingSummary/RatingDistribution.tsx b/packages/components/src/organisms/RatingSummary/RatingDistribution.tsx new file mode 100644 index 0000000000..f7ea129f17 --- /dev/null +++ b/packages/components/src/organisms/RatingSummary/RatingDistribution.tsx @@ -0,0 +1,44 @@ +import React, { forwardRef, type HTMLAttributes } from 'react' +import RatingDistributionItem from './RatingDistributionItem' + +export interface RatingDistributionProps + extends HTMLAttributes { + /** + * The rating distribution + */ + distribution: { + starsOne: number + starsTwo: number + starsThree: number + starsFour: number + starsFive: number + } + /** + * Optional test ID for testing. + */ + testId?: string +} + +export const RatingDistribution = forwardRef< + HTMLOListElement, + RatingDistributionProps +>(function ProgressStatus( + { + distribution: { starsFive, starsFour, starsThree, starsTwo, starsOne }, + testId = 'fs-rating-distribution', + ...props + }, + ref +) { + return ( +
    + + + + + +
+ ) +}) + +export default RatingDistribution diff --git a/packages/components/src/organisms/RatingSummary/RatingDistributionItem.tsx b/packages/components/src/organisms/RatingSummary/RatingDistributionItem.tsx new file mode 100644 index 0000000000..3e8b1343d5 --- /dev/null +++ b/packages/components/src/organisms/RatingSummary/RatingDistributionItem.tsx @@ -0,0 +1,50 @@ +import React, { forwardRef, type HTMLAttributes } from 'react' +import Icon from '../../atoms/Icon' + +export interface RatingDistributionItemProps + extends HTMLAttributes { + ratingIndex: number + /* Current value of the progress */ + value: number + /* Optional test ID for testing*/ + testId?: string +} + +const ItemStar = ({ ratingIndex }: { ratingIndex: number }) => ( + +

{ratingIndex}

+ +
+) + +const ItemProgressBar = ({ value }: { value: number }) => ( +
+
+
+
+
+) + +const ItemPercentage = ({ percentage }: { percentage: number }) => ( +

{percentage}%

+) + +const RatingDistributionItem = forwardRef< + HTMLLIElement, + RatingDistributionItemProps +>(function RatingDistributionItem( + { ratingIndex, value, testId = 'fs-rating-distribution-item' }, + ref +) { + return ( +
  • + + + +
  • + ) +}) +export default RatingDistributionItem diff --git a/packages/components/src/organisms/RatingSummary/RatingSummary.tsx b/packages/components/src/organisms/RatingSummary/RatingSummary.tsx new file mode 100644 index 0000000000..ed80906403 --- /dev/null +++ b/packages/components/src/organisms/RatingSummary/RatingSummary.tsx @@ -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 { + /** + * 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 ( +
    +

    {formattedAverage}

    + +

    {totalCountText}

    +
    + ) +} + +export const RatingSummary = forwardRef( + 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 ( +
    + + + {totalCount > 0 && ( + + )} +
    + ) + } +) + +export default RatingSummary diff --git a/packages/components/src/organisms/RatingSummary/index.ts b/packages/components/src/organisms/RatingSummary/index.ts new file mode 100644 index 0000000000..e23be2117c --- /dev/null +++ b/packages/components/src/organisms/RatingSummary/index.ts @@ -0,0 +1 @@ +export { default, type RatingSummaryProps } from './RatingSummary' diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts index 79bc1e0df2..0e08a53088 100644 --- a/packages/core/@generated/gql.ts +++ b/packages/core/@generated/gql.ts @@ -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, @@ -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. diff --git a/packages/core/@generated/graphql.ts b/packages/core/@generated/graphql.ts index 32b2e27439..e44c1b8c0c 100644 --- a/packages/core/@generated/graphql.ts +++ b/packages/core/@generated/graphql.ts @@ -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 = { @@ -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 + } + } } } @@ -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 + } + } } } @@ -2065,6 +2095,13 @@ export const ProductDetailsFragment_ProductFragmentDoc = rating { average totalCount + distribution { + starsOne + starsTwo + starsThree + starsFour + starsFive + } } ...CartProductItem } @@ -2342,7 +2379,7 @@ export const ServerCollectionPageQueryDocument = { export const ServerProductQueryDocument = { __meta__: { operationName: 'ServerProductQuery', - operationHash: '0a3f449b2a88dc1f692fe1ae981370be53a02cce', + operationHash: '312acab1a14a3b35d6c70887b5cf289b5cf6cf76', }, } as unknown as TypedDocumentString< ServerProductQueryQuery, @@ -2396,7 +2433,7 @@ export const ClientProductGalleryQueryDocument = { export const ClientProductQueryDocument = { __meta__: { operationName: 'ClientProductQuery', - operationHash: 'e1599e2efe3664aad09c026919c1c104b4085f00', + operationHash: 'e678f7fc4d59a3e4cbf61295fc1e669f44724464', }, } as unknown as TypedDocumentString< ClientProductQueryQuery, diff --git a/packages/core/cms/faststore/sections.json b/packages/core/cms/faststore/sections.json index c82caca8c7..7e571925f2 100644 --- a/packages/core/cms/faststore/sections.json +++ b/packages/core/cms/faststore/sections.json @@ -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" + } + } } } } diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 01bc82bdc5..76f090aa7a 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -384,6 +384,13 @@ export const fragment = gql(` rating { average totalCount + distribution { + starsOne + starsTwo + starsThree + starsFour + starsFive + } } # Contains necessary info to add this item to cart diff --git a/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss b/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss index 9303d73f18..1ccc4a8fdb 100644 --- a/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss +++ b/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss @@ -1,6 +1,8 @@ @layer components { .section { // TODO: Ajustar esses componentes para a nova section ReviewsAndRatings - // @import '@faststore/ui/src/components/atoms/Ratings/styles'; + @import "@faststore/ui/src/components/molecules/Rating/styles"; + @import "@faststore/ui/src/components/organisms/RatingSummary/styles"; + @import "@faststore/ui/src/components/atoms/Button/styles"; } } diff --git a/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx b/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx index 989f518873..e1dafa359b 100644 --- a/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx +++ b/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx @@ -1,11 +1,27 @@ +import { RatingSummary } from '@faststore/ui' +import { usePDP } from 'src/sdk/overrides/PageProvider' +import useScreenResize from 'src/sdk/ui/useScreenResize' + export type ReviewsAndRatingsProps = { title: string } function ReviewsAndRatings({ title, ...otherProps }: ReviewsAndRatingsProps) { + const context = usePDP() + const { isDesktop } = useScreenResize() + + const { + product: { rating }, + } = context?.data + return ( <>

    {title}

    +
    + {(isDesktop || rating.totalCount > 0) && ( + + )} +
    ) } diff --git a/packages/ui/src/components/molecules/Rating/styles.scss b/packages/ui/src/components/molecules/Rating/styles.scss index 709699eb9b..3c4f2a1df5 100644 --- a/packages/ui/src/components/molecules/Rating/styles.scss +++ b/packages/ui/src/components/molecules/Rating/styles.scss @@ -30,6 +30,8 @@ width: var(--fs-rating-icon-width); height: var(--fs-rating-icon-height); color: var(--fs-rating-color); + color: var(--fs-rating-color); + fill: var(--fs-rating-color); } [data-fs-rating-button] { diff --git a/packages/ui/src/components/organisms/RatingSummary/styles.scss b/packages/ui/src/components/organisms/RatingSummary/styles.scss new file mode 100644 index 0000000000..cbfd59ba8e --- /dev/null +++ b/packages/ui/src/components/organisms/RatingSummary/styles.scss @@ -0,0 +1,121 @@ +[data-fs-rating-summary] { + // -------------------------------------------------------- + // Design Tokens for Rating Summary + // -------------------------------------------------------- + + --fs-rating-summary-width : 30%; + --fs-rating-summary-min-height : 15rem; + --fs-rating-summary-line-height : 1; + --fs-rating-summary-gap : var(--fs-spacing-4); + --fs-rating-summary-padding : var(--fs-spacing-5); + --fs-rating-summary-font-size : var(--fs-text-size-0); + --fs-rating-summary-font-weight : var(--fs-text-weight-regular); + --fs-rating-summary-border-radius : var(--fs-border-radius); + --fs-rating-summary-background-color : var(--fs-color-neutral-1); + + --fs-rating-header-vertical-gap : var(--fs-spacing-0); + --fs-rating-header-padding-top : var(--fs-spacing-0); + --fs-rating-header-average-font-size : var(--fs-text-size-7); + --fs-rating-header-average-font-weight : var(--fs-text-weight-semibold); + --fs-rating-header-total-count-color : var(--fs-color-text-light); + + --fs-rating-distribution-vertical-gap : var(--fs-spacing-1); + + --fs-rating-distribution-item-horizontal-gap : var(--fs-spacing-2); + --fs-rating-distribution-item-star-horizontal-gap : var(--fs-spacing-0); + --fs-rating-distribution-item-star-icon-size : var(--fs-spacing-2); + + --fs-progress-bar-height : var(--fs-spacing-0); + --fs-progress-bar-radius : var(--fs-border-radius-pill); + --fs-progress-bar-track-color : var(--fs-color-neutral-2); + --fs-progress-bar-fill-color : var(--fs-color-main-2); + --fs-progress-bar-transition-function : var(--fs-transition-function); + --fs-progress-bar-transition-property : var(--fs-transition-property); + --fs-progress-bar-transition-timing : var(--fs-transition-timing); + + // -------------------------------------------------------- + // Structural Styles + // -------------------------------------------------------- + + display: flex; + flex-direction: column; + gap: var(--fs-rating-summary-gap); + align-items: center; + justify-content: center; + width: var(--fs-rating-summary-width); + min-height: var(--fs-rating-summary-min-height); + padding: var(--fs-rating-summary-padding); + font-size: var(--fs-rating-summary-font-size); + font-weight: var(--fs-rating-summary-font-weight); + line-height: var(--fs-rating-summary-line-height); + background-color: var(--fs-rating-summary-background-color); + border-radius: var(--fs-rating-summary-border-radius); + + [data-fs-rating-summary-header] { + display: flex; + flex-direction: column; + gap: var(--fs-rating-header-vertical-gap); + align-items: center; + padding-top: var(--fs-rating-header-padding-top); + + [data-fs-rating-summary-header-average] { + font-size: var(--fs-rating-header-average-font-size); + font-weight: var(--fs-rating-header-average-font-weight); + } + + [data-fs-rating-summary-header-total-count] { + color: var(--fs-rating-header-total-count-color); + } + } + + [data-fs-rating-summary-distribution] { + display: flex; + flex-direction: column; + gap: var(--fs-rating-distribution-vertical-gap); + width: 100%; + + [data-fs-distribution-item] { + display: grid; + grid-template-columns: var(--fs-spacing-5) 1fr var(--fs-spacing-5); + grid-gap: var(--fs-rating-distribution-item-horizontal-gap); + align-items: center; + + [data-fs-rating-distribution-item-star] { + display: flex; + gap: var(--fs-rating-distribution-item-star-horizontal-gap); + align-items: center; + text-align: left; + + [data-fs-rating-distribution-item-star-icon] { + width: var(--fs-rating-distribution-item-star-icon-size); + height: var(--fs-rating-distribution-item-star-icon-size); + } + } + + [data-fs-rating-distribution-item-progress-bar] { + width: 100%; + height: var(--fs-progress-bar-height); + + [data-fs-rating-distribution-item-progress-bar-track] { + height: 100%; + overflow: hidden; + background-color: var(--fs-progress-bar-track-color); + border-radius: var(--fs-progress-bar-radius); + } + + [data-fs-rating-distribution-item-progress-bar-fill] { + height: 100%; + background-color: var(--fs-progress-bar-fill-color); + transition: + var(--fs-progress-bar-transition-property) + var(--fs-progress-bar-transition-timing) + var(--fs-progress-bar-transition-function); + } + } + + [data-fs-rating-distribution-item-percentage] { + text-align: right; + } + } + } +} diff --git a/packages/ui/src/styles/components.scss b/packages/ui/src/styles/components.scss index a693c501d2..4936a6b08c 100644 --- a/packages/ui/src/styles/components.scss +++ b/packages/ui/src/styles/components.scss @@ -83,6 +83,7 @@ @import "../components/organisms/ProductGallery/styles"; @import "../components/organisms/ProductGrid/styles"; @import "../components/organisms/ProductShelf/styles"; +@import "../components/organisms/RatingSummary/styles"; @import "../components/organisms/RegionModal/styles"; @import "../components/organisms/SearchInput/styles"; @import "../components/organisms/SKUMatrix/styles";