From 33db057e4ebfde6b577870b7237ffd299878be94 Mon Sep 17 00:00:00 2001 From: Bruno Guilera Gutchenzo Date: Wed, 19 Feb 2025 14:32:21 -0300 Subject: [PATCH] feat: basic rating display (#2653) ## What's the purpose of this pull request? To add rating stars to existing components: - `` - `` ## How it works? If the feature flag for reviews and ratings, defined at `discovery.config`, it's turned on, then everywhere a `` is used then it will display the product star rating ## How to test it? [preview](https://starter-git-feat-basic-rating-display-vtex.vercel.app/) ## References [JIRA TASK: SFS-2063](https://vtex-dev.atlassian.net/browse/SFS-2063) [JIRA TASK: SFS-2065](https://vtex-dev.atlassian.net/browse/SFS-2065) ![image](https://github.com/user-attachments/assets/379da3f2-22cb-4e86-a3ac-3cf049fb0c0e) ![image](https://github.com/user-attachments/assets/05df5d00-6a78-44b1-bf59-6b8f46ef4c4d) ![image](https://github.com/user-attachments/assets/721c3121-8382-427f-bb2a-bfb5adb091ba) ![image](https://github.com/user-attachments/assets/85875b3a-53d6-4a0a-bbd2-9e28826fb227) ## Checklist You may erase this after checking them all :wink: **PR Description** - [ ] Displays rating at `Just Arrived` - [ ] Displays rating at `Most Wanted` - [ ] Displays rating at `Deals & Promotions` - [ ] Displays rating at `Product Details` - [ ] Displays rating at `Product Gallery` --- .../ProductCard/ProductCardContent.tsx | 2 +- .../molecules/ProductTitle/ProductTitle.tsx | 4 ++-- packages/core/@generated/gql.ts | 8 +++---- packages/core/@generated/graphql.ts | 22 +++++++++++++++---- packages/core/api/index.ts | 1 + packages/core/discovery.config.default.js | 1 + .../product/ProductCard/ProductCard.tsx | 9 +++++++- .../ProductDetails/ProductDetails.tsx | 10 +++++++++ .../ProductGallery/section.module.scss | 1 + 9 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/components/src/molecules/ProductCard/ProductCardContent.tsx b/packages/components/src/molecules/ProductCard/ProductCardContent.tsx index b13a8be039..f5d9ccb34f 100644 --- a/packages/components/src/molecules/ProductCard/ProductCardContent.tsx +++ b/packages/components/src/molecules/ProductCard/ProductCardContent.tsx @@ -128,7 +128,7 @@ const ProductCardContent = forwardRef( {includeTaxes && ( )} - {ratingValue && ( + {ratingValue !== undefined && ( } /> )} diff --git a/packages/components/src/molecules/ProductTitle/ProductTitle.tsx b/packages/components/src/molecules/ProductTitle/ProductTitle.tsx index 090b4369d2..f6c46c4614 100644 --- a/packages/components/src/molecules/ProductTitle/ProductTitle.tsx +++ b/packages/components/src/molecules/ProductTitle/ProductTitle.tsx @@ -56,9 +56,9 @@ const ProductTitle = forwardRef( {!!label && label} - {(refNumber || ratingValue) && ( + {(refNumber || ratingValue !== undefined) && (
- {ratingValue && } + {ratingValue !== undefined && } {refNumber && ( <> {refTag} {refNumber} diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts index 8fcec3cc26..79bc1e0df2 100644 --- a/packages/core/@generated/gql.ts +++ b/packages/core/@generated/gql.ts @@ -12,11 +12,11 @@ import * as types from './graphql' * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - '\n fragment ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\n isVariantOf {\n productGroupID\n name\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 listPrice\n listPriceWithTaxes\n quantity\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n }\n': + '\n fragment ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\n isVariantOf {\n productGroupID\n name\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 listPrice\n listPriceWithTaxes\n quantity\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n rating {\n average\n totalCount\n }\n }\n': 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 # 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 }\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, @@ -66,7 +66,7 @@ const documents = { * 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 ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\n isVariantOf {\n productGroupID\n name\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 listPrice\n listPriceWithTaxes\n quantity\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n }\n' + source: '\n fragment ProductSummary_product on StoreProduct {\n id: productID\n slug\n sku\n brand {\n brandName: name\n }\n name\n gtin\n\n isVariantOf {\n productGroupID\n name\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 listPrice\n listPriceWithTaxes\n quantity\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n advertisement {\n adId\n adResponseId\n }\n\n rating {\n average\n totalCount\n }\n }\n' ): typeof import('./graphql').ProductSummary_ProductFragmentDoc /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. @@ -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 # 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 }\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 e3ae3b190f..1c884a72aa 100644 --- a/packages/core/@generated/graphql.ts +++ b/packages/core/@generated/graphql.ts @@ -1245,6 +1245,7 @@ export type ProductSummary_ProductFragment = { valueReference: any }> advertisement: { adId: string; adResponseId: string } | null + rating: { average: number; totalCount: number } } type Filter_Facets_StoreFacetBoolean_Fragment = { @@ -1307,6 +1308,7 @@ export type ProductDetailsFragment_ProductFragment = { value: any valueReference: any }> + rating: { average: number; totalCount: number } } export type ProductSkuMatrixSidebarFragment_ProductFragment = { @@ -1444,6 +1446,7 @@ export type ServerProductQueryQuery = { value: any valueReference: any }> + rating: { average: number; totalCount: number } } } @@ -1650,6 +1653,7 @@ export type ClientManyProductsQueryQuery = { valueReference: any }> advertisement: { adId: string; adResponseId: string } | null + rating: { average: number; totalCount: number } } }> } @@ -1743,6 +1747,7 @@ export type ClientProductQueryQuery = { value: any valueReference: any }> + rating: { average: number; totalCount: number } } } @@ -1783,6 +1788,7 @@ export type ClientSearchSuggestionsQueryQuery = { valueReference: any }> advertisement: { adId: string; adResponseId: string } | null + rating: { average: number; totalCount: number } }> } products: { pageInfo: { totalCount: number } } @@ -1922,6 +1928,10 @@ export const ProductSummary_ProductFragmentDoc = new TypedDocumentString( adId adResponseId } + rating { + average + totalCount + } } `, { fragmentName: 'ProductSummary_product' } @@ -2036,6 +2046,10 @@ export const ProductDetailsFragment_ProductFragmentDoc = value valueReference } + rating { + average + totalCount + } ...CartProductItem } fragment CartProductItem on StoreProduct { @@ -2312,7 +2326,7 @@ export const ServerCollectionPageQueryDocument = { export const ServerProductQueryDocument = { __meta__: { operationName: 'ServerProductQuery', - operationHash: '46103bee661405bde706d72126fdbf9b0a0c9e6e', + operationHash: '0a3f449b2a88dc1f692fe1ae981370be53a02cce', }, } as unknown as TypedDocumentString< ServerProductQueryQuery, @@ -2348,7 +2362,7 @@ export const ClientAllVariantProductsQueryDocument = { export const ClientManyProductsQueryDocument = { __meta__: { operationName: 'ClientManyProductsQuery', - operationHash: '14148671fbf53498fad5c600ee87765920145019', + operationHash: 'e1ccf9e73ec6c0b8580c6e789d8a2af7618fb1eb', }, } as unknown as TypedDocumentString< ClientManyProductsQueryQuery, @@ -2366,7 +2380,7 @@ export const ClientProductGalleryQueryDocument = { export const ClientProductQueryDocument = { __meta__: { operationName: 'ClientProductQuery', - operationHash: '7d121ef8d4dc99174e64e4429a9b977b8bbebed8', + operationHash: 'e1599e2efe3664aad09c026919c1c104b4085f00', }, } as unknown as TypedDocumentString< ClientProductQueryQuery, @@ -2375,7 +2389,7 @@ export const ClientProductQueryDocument = { export const ClientSearchSuggestionsQueryDocument = { __meta__: { operationName: 'ClientSearchSuggestionsQuery', - operationHash: '47e48eaee91d16a4237eb2c1241bc2ed3e2ad9bb', + operationHash: '3599746571e06012a61a20f92d30ede456564c4b', }, } as unknown as TypedDocumentString< ClientSearchSuggestionsQueryQuery, diff --git a/packages/core/api/index.ts b/packages/core/api/index.ts index 3060957adf..4aa6a03b9f 100644 --- a/packages/core/api/index.ts +++ b/packages/core/api/index.ts @@ -12,6 +12,7 @@ export type { StoreProductGroupRoot, StoreProductRoot, StoreOrganizationRoot, + StoreProductRating, } from '@faststore/api' export * from '../@generated/graphql' diff --git a/packages/core/discovery.config.default.js b/packages/core/discovery.config.default.js index 68776f8f7e..d87a0b098d 100644 --- a/packages/core/discovery.config.default.js +++ b/packages/core/discovery.config.default.js @@ -34,6 +34,7 @@ module.exports = { hideUnavailableItems: false, showSponsored: false, incrementAddress: true, + reviewsAndRatings: true, }, // Default session diff --git a/packages/core/src/components/product/ProductCard/ProductCard.tsx b/packages/core/src/components/product/ProductCard/ProductCard.tsx index f5f7555ce6..3fc14f0a08 100644 --- a/packages/core/src/components/product/ProductCard/ProductCard.tsx +++ b/packages/core/src/components/product/ProductCard/ProductCard.tsx @@ -12,6 +12,7 @@ import NextLink from 'next/link' import { Image } from 'src/components/ui/Image' import { useFormattedPrice } from 'src/sdk/product/useFormattedPrice' import { useProductLink } from 'src/sdk/product/useProductLink' +import { api as apiConfig } from 'discovery.config' type Variant = 'wide' | 'default' @@ -88,6 +89,7 @@ function ProductCard({ lowPriceWithTaxes, offers: [{ listPrice: listPriceBase, availability, listPriceWithTaxes }], }, + rating, } = product const linkProps = { @@ -146,7 +148,7 @@ function ProductCard({ listPrice: listPrice, formatter: useFormattedPrice, }} - ratingValue={ratingValue} + ratingValue={apiConfig.reviewsAndRatings ? rating.average : undefined} outOfStock={outOfStock} onButtonClick={onButtonClick} linkProps={linkProps} @@ -211,6 +213,11 @@ export const fragment = gql(` adId adResponseId } + + rating { + average + totalCount + } } `) diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 7f89b7e0e5..259f1a6c29 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -16,6 +16,7 @@ import { getOverridableSection } from '../../../sdk/overrides/getOverriddenSecti import { useOverrideComponents } from '../../../sdk/overrides/OverrideContext' import { usePDP } from '../../../sdk/overrides/PageProvider' import { ProductDetailsDefaultComponents } from './DefaultComponents' +import { api as apiConfig } from 'discovery.config' type StoreConfig = typeof storeConfig & { experimental: { @@ -144,6 +145,7 @@ function ProductDetails({ lowPrice, lowPriceWithTaxes, }, + rating, } = product useEffect(() => { @@ -197,6 +199,9 @@ function ProductDetails({ // Maybe now it's worth to make title always a h1 and receive only the name, as it would be easier for users to override. title={

{name}

} {...ProductTitle.props} + ratingValue={ + apiConfig.reviewsAndRatings ? rating.average : undefined + } label={ showDiscountBadge && (