Repository: vtex/faststore
Pull request #2724: feat: create review list
Author: login: artursantiago
Created: 2025-03-17T19:28:21Z | Merged: 2025-03-28T13:08:06Z

>⚠️ THIS PR DEPENDS ON:  ⚠️
> - https://github.com/vtex/faststore/pull/2705
> - https://github.com/vtex/faststore/pull/2713

## What's the purpose of this pull request?

This PR adds the `ReviewList` with its listing functionality, loading state, filters, and sorting.

## How it works?

- By default, the list will display 6 reviews. If more reviews are available, a "Load More" button will appear. Clicking it will load 6 additional reviews at a time.
- If there are no reviews for the product, a message will be displayed along with a button to open the ReviewModal.
- If a filter is applied but no reviews match the selected criteria, a different message will appear with a button to reset the filters.

## How to test it?

- Test scenarios where reviews exist and where they do not.
- Test cases with more than 6 reviews and fewer than 6 reviews.
- Apply filters that result in no matching reviews.
- Verify that sorting works correctly.

### Starters Deploy Preview

<!--- Add a link to a deploy preview from `starter.store` with this branch being used. --->

[Preview](https://starter-git-feat-review-list-vtex.vercel.app/)

## References

[JIRA SFS-2072](https://vtex-dev.atlassian.net/browse/SFS-2072)
[Figma](https://www.figma.com/design/YXU6IX2htN2yg7udpCumZv/Reviews-%26-Ratings?node-id=130-32955&t=5KOdS0Qx4MBzhrNq-4)
[FEATURE BRANCH](https://github.com/vtex/faststore/pull/2676)

![image](https://github.com/user-attachments/assets/87eed9ef-9250-4191-b610-33377e06b8fa)
![image](https://github.com/user-attachments/assets/84ad9ab5-1dd5-4245-8d6e-8aa2f11ddec3)
![image](https://github.com/user-attachments/assets/ec4a4715-7df4-4750-ada4-aeebcc12fbb6)
![image](https://github.com/user-attachments/assets/1fe1c002-333c-4db7-9e41-b6866023e39c)




Diff:
diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts
index 1f05ce51de..e09ccaad83 100644
--- a/packages/core/@generated/gql.ts
+++ b/packages/core/@generated/gql.ts
@@ -54,6 +54,8 @@ const documents = {
     types.ClientProductQueryDocument,
   '\n  mutation CreateProductReview($data: ICreateProductReview!) {\n    productReviewId: createProductReview(data: $data)\n  }\n':
     types.CreateProductReviewDocument,
+  '\n  query ClientProductReviews (\n    $productId: String!\n    $after: Int\n    $first: Int\n    $sort: StoreProductListReviewsSort\n    $rating: Int\n  ) {\n    reviews(\n      productId: $productId\n      after: $after\n      first: $first\n      sort: $sort\n      rating: $rating\n    ) {\n      data {\n        id\n        productId\n        rating\n        title\n        text\n        reviewDateTime\n        reviewerName\n        verifiedPurchaser\n      }\n      range {\n        from\n        to\n        total\n      }\n    }\n  }\n':
+    types.ClientProductReviewsDocument,
   '\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':
@@ -190,6 +192,12 @@ export function gql(
 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.
+ */
+export function gql(
+  source: '\n  query ClientProductReviews (\n    $productId: String!\n    $after: Int\n    $first: Int\n    $sort: StoreProductListReviewsSort\n    $rating: Int\n  ) {\n    reviews(\n      productId: $productId\n      after: $after\n      first: $first\n      sort: $sort\n      rating: $rating\n    ) {\n      data {\n        id\n        productId\n        rating\n        title\n        text\n        reviewDateTime\n        reviewerName\n        verifiedPurchaser\n      }\n      range {\n        from\n        to\n        total\n      }\n    }\n  }\n'
+): typeof import('./graphql').ClientProductReviewsDocument
 /**
  * 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 cb03304483..b7374726af 100644
--- a/packages/core/@generated/graphql.ts
+++ b/packages/core/@generated/graphql.ts
@@ -1803,6 +1803,30 @@ export type CreateProductReviewMutationVariables = Exact<{
 
 export type CreateProductReviewMutation = { productReviewId: string }
 
+export type ClientProductReviewsQueryVariables = Exact<{
+  productId: Scalars['String']['input']
+  after: InputMaybe<Scalars['Int']['input']>
+  first: InputMaybe<Scalars['Int']['input']>
+  sort: InputMaybe<StoreProductListReviewsSort>
+  rating: InputMaybe<Scalars['Int']['input']>
+}>
+
+export type ClientProductReviewsQuery = {
+  reviews: {
+    data: Array<{
+      id: string
+      productId: string
+      rating: number
+      title: string
+      text: string
+      reviewDateTime: string
+      reviewerName: string | null
+      verifiedPurchaser: boolean
+    }>
+    range: { from: number; to: number; total: number }
+  } | null
+}
+
 export type ClientSearchSuggestionsQueryQueryVariables = Exact<{
   term: Scalars['String']['input']
   selectedFacets: InputMaybe<Array<IStoreSelectedFacet> | IStoreSelectedFacet>
@@ -2454,6 +2478,15 @@ export const CreateProductReviewDocument = {
   CreateProductReviewMutation,
   CreateProductReviewMutationVariables
 >
+export const ClientProductReviewsDocument = {
+  __meta__: {
+    operationName: 'ClientProductReviews',
+    operationHash: '01c803b3d22cf1db7d30794ea581073c7ccc0859',
+  },
+} as unknown as TypedDocumentString<
+  ClientProductReviewsQuery,
+  ClientProductReviewsQueryVariables
+>
 export const ClientSearchSuggestionsQueryDocument = {
   __meta__: {
     operationName: 'ClientSearchSuggestionsQuery',
diff --git a/packages/core/cms/faststore/sections.json b/packages/core/cms/faststore/sections.json
index 2ffee587b8..c812211df8 100644
--- a/packages/core/cms/faststore/sections.json
+++ b/packages/core/cms/faststore/sections.json
@@ -1229,6 +1229,48 @@
                   }
                 }
               }
+            },
+            "emptyFilter": {
+              "title": "No reviews found message when a filter is applied",
+              "type": "object",
+              "properties": {
+                "title": {
+                  "title": "Title",
+                  "type": "string",
+                  "default": "No reviews found for this filter"
+                },
+                "subtitle": {
+                  "title": "Subtitle",
+                  "type": "string",
+                  "default": "Try adjusting your selection and check again"
+                },
+                "buttonLabel": {
+                  "title": "Button Label",
+                  "type": "string",
+                  "default": "See all reviews"
+                }
+              }
+            },
+            "emptyList": {
+              "title": "No reviews available message when there are no reviews",
+              "type": "object",
+              "properties": {
+                "title": {
+                  "title": "Title",
+                  "type": "string",
+                  "default": "There are no reviews for this item yet"
+                },
+                "subtitle": {
+                  "title": "Subtitle",
+                  "type": "string",
+                  "default": "Be the first one to do it"
+                },
+                "buttonLabel": {
+                  "title": "Button Label",
+                  "type": "string",
+                  "default": "Write the first review"
+                }
+              }
             }
           }
         }
diff --git a/packages/core/public/icons.svg b/packages/core/public/icons.svg
index f0c616e74d..d82c7f0e9b 100644
--- a/packages/core/public/icons.svg
+++ b/packages/core/public/icons.svg
@@ -125,4 +125,8 @@
   <symbol id="Stripe" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 222.5"><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z" fill="#635bff"/></symbol>
 
   <symbol id="Visa" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path d="M10.405 15.8656H8.44718L9.67054 8.13848H11.6283L10.405 15.8656Z" fill="#0066B3"/><path d="M6.79891 8.13848L4.93167 13.4532L4.71034 12.308L4.05238 8.83512C4.05238 8.83512 3.9719 8.13848 3.12279 8.13848H0.0362179L0 8.26871C0 8.26871 0.943678 8.47129 2.04832 9.1514L3.75057 15.8656H5.79084L8.90558 8.13848H6.79891Z" fill="#0066B3"/><path d="M22.2012 15.8656H24L22.4326 8.13848H20.8571C20.1307 8.13848 19.9536 8.71522 19.9536 8.71522L17.0341 15.8677H19.0743L19.4828 14.7204H21.9718L22.2012 15.8656ZM20.0462 13.1328L21.0744 10.2408L21.6539 13.1328H20.0462Z" fill="#0066B3"/><path d="M17.1874 9.9969L17.4671 8.33695C17.4671 8.33695 16.6039 8 15.7065 8C14.7347 8 12.4268 8.43618 12.4268 10.5592C12.4268 12.5561 15.1351 12.5809 15.1351 13.6289C15.1351 14.677 12.7065 14.491 11.9036 13.8295L11.6119 15.5638C11.6119 15.5638 12.4871 16 13.8232 16C15.1592 16 17.1754 15.2889 17.1754 13.354C17.1754 11.3447 14.4429 11.1587 14.4429 10.2842C14.4409 9.40982 16.3484 9.52145 17.1874 9.9969Z" fill="#0066B3"/></symbol>
+
+  <symbol id="ChatDots" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 56" fill="none">
+    <path d="M19.2114 25.6667C19.8725 25.6667 20.4267 25.4431 20.8739 24.9959C21.3211 24.5487 21.5448 23.9945 21.5448 23.3334C21.5448 22.6723 21.3211 22.1181 20.8739 21.6709C20.4267 21.2237 19.8725 21.0001 19.2114 21.0001C18.5503 21.0001 17.9961 21.2237 17.5489 21.6709C17.1017 22.1181 16.8781 22.6723 16.8781 23.3334C16.8781 23.9945 17.1017 24.5487 17.5489 24.9959C17.9961 25.4431 18.5503 25.6667 19.2114 25.6667ZM28.5448 25.6667C29.2059 25.6667 29.76 25.4431 30.2073 24.9959C30.6545 24.5487 30.8781 23.9945 30.8781 23.3334C30.8781 22.6723 30.6545 22.1181 30.2073 21.6709C29.76 21.2237 29.2059 21.0001 28.5448 21.0001C27.8836 21.0001 27.3295 21.2237 26.8823 21.6709C26.435 22.1181 26.2114 22.6723 26.2114 23.3334C26.2114 23.9945 26.435 24.5487 26.8823 24.9959C27.3295 25.4431 27.8836 25.6667 28.5448 25.6667ZM37.8781 25.6667C38.5392 25.6667 39.0934 25.4431 39.5406 24.9959C39.9878 24.5487 40.2114 23.9945 40.2114 23.3334C40.2114 22.6723 39.9878 22.1181 39.5406 21.6709C39.0934 21.2237 38.5392 21.0001 37.8781 21.0001C37.217 21.0001 36.6628 21.2237 36.2156 21.6709C35.7684 22.1181 35.5448 22.6723 35.5448 23.3334C35.5448 23.9945 35.7684 24.5487 36.2156 24.9959C36.6628 25.4431 37.217 25.6667 37.8781 25.6667ZM5.21143 51.3334V9.33341C5.21143 8.05008 5.66837 6.95147 6.58226 6.03758C7.49615 5.12369 8.59476 4.66675 9.87809 4.66675H47.2114C48.4948 4.66675 49.5934 5.12369 50.5073 6.03758C51.4211 6.95147 51.8781 8.05008 51.8781 9.33341V37.3334C51.8781 38.6167 51.4211 39.7154 50.5073 40.6292C49.5934 41.5431 48.4948 42.0001 47.2114 42.0001H14.5448L5.21143 51.3334ZM12.5614 37.3334H47.2114V9.33341H9.87809V39.9584L12.5614 37.3334Z" fill="#D6D6D6"/>
+</symbol>
 </svg>
diff --git a/packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx b/packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
new file mode 100644
index 0000000000..234bc8c2b8
--- /dev/null
+++ b/packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
@@ -0,0 +1,34 @@
+import { Button as UIButton, Icon as UIIcon } from '@faststore/ui'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'There are no reviews for this item yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <UIIcon name="ChatDots" width={56} height={56} />
+        <h5 data-fs-empty-review-list-title>{title}</h5>
+        <p data-fs-empty-review-list-subtitle>{subtitle}</p>
+      </div>
+      <UIButton variant="primary" onClick={() => onButtonClick?.(productId)}>
+        {buttonLabel}
+      </UIButton>
+    </div>
+  )
+}
+
+export default EmptyReviewList
diff --git a/packages/core/src/components/reviews/EmptyReviewList/index.ts b/packages/core/src/components/reviews/EmptyReviewList/index.ts
new file mode 100644
index 0000000000..5a8507e6fe
--- /dev/null
+++ b/packages/core/src/components/reviews/EmptyReviewList/index.ts
@@ -0,0 +1 @@
+export { default } from './EmptyReviewList'
diff --git a/packages/core/src/components/reviews/FilterProductReviews/index.ts b/packages/core/src/components/reviews/FilterProductReviews/index.ts
index 8afe327efd..37c8b9f51f 100644
--- a/packages/core/src/components/reviews/FilterProductReviews/index.ts
+++ b/packages/core/src/components/reviews/FilterProductReviews/index.ts
@@ -1,2 +1,5 @@
-export { default as FilterProductReviews } from './FilterProductReviews'
-export type { FilterProductReviewsProps } from './FilterProductReviews'
+export { default } from './FilterProductReviews'
+export type {
+  FilterProductReviewsProps,
+  FilterProductListReview,
+} from './FilterProductReviews'
diff --git a/packages/core/src/components/reviews/ReviewList/ReviewList.tsx b/packages/core/src/components/reviews/ReviewList/ReviewList.tsx
index 6c9f88bcb9..a21e68ae5c 100644
--- a/packages/core/src/components/reviews/ReviewList/ReviewList.tsx
+++ b/packages/core/src/components/reviews/ReviewList/ReviewList.tsx
@@ -1,9 +1,64 @@
-import { FilterProductReviews } from '../FilterProductReviews'
-import { SortProductReviews } from '../SortProductReviews'
+import { useUI } from '@faststore/ui'
+import type { FilterProductListReview } from '../FilterProductReviews'
 import type { StoreProductListReviewsSort } from '@generated/graphql'
-import type { FilterProductListReview } from '../FilterProductReviews/FilterProductReviews'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
 
-export interface ReviewListProps {
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
   sortSelect?: {
     label: string
     options: Record<StoreProductListReviewsSort, string>
@@ -12,22 +67,62 @@ export interface ReviewListProps {
     label: string
     options: Record<FilterProductListReview, string>
   }
-  currentSort?: StoreProductListReviewsSort
-  currentFilter?: FilterProductListReview
-  onSortChange: (sort: StoreProductListReviewsSort) => void
-  onFilterChange: (filter: FilterProductListReview) => void
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
 }
 
 function ReviewList({
   sortSelect,
   filterSelect,
-  currentSort,
-  currentFilter,
-  onSortChange,
-  onFilterChange,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
 }: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+        {...props}
+      />
+    )
+  }
+
   return (
-    <div data-fs-review-list>
+    <div data-fs-review-list {...props}>
       <div data-fs-review-list-header>
         <SortProductReviews
           id="fs-sort-product-reviews-select"
@@ -44,7 +139,44 @@ function ReviewList({
           onChange={onFilterChange}
         />
       </div>
-      <div data-fs-review-list-content></div>
+      {isFirstLoading ? (
+        <ReviewCardSkeleton />
+      ) : reviews.length > 0 ? (
+        <UIList>
+          {reviews.map((review) => (
+            <li key={review.id}>
+              <UIReviewCard
+                title={review.title}
+                text={review.text}
+                rating={review.rating}
+                author={review.reviewerName}
+                isVerified={review.verifiedPurchaser}
+                date={new Date(review.reviewDateTime)}
+              />
+            </li>
+          ))}
+        </UIList>
+      ) : (
+        <EmptyReviewList
+          data-fs-empty-review-filter
+          productId={productId}
+          title={emptyFilter?.title}
+          subtitle={emptyFilter?.subtitle}
+          buttonLabel={emptyFilter?.buttonLabel}
+          onButtonClick={resetFilter}
+        />
+      )}
+
+      {hasMore && !isFirstLoading && (
+        <UIButton
+          data-fs-review-list-has-more
+          onClick={loadMore}
+          loading={loadingMore}
+          variant="secondary"
+        >
+          {loadMoreLabel}
+        </UIButton>
+      )}
     </div>
   )
 }
diff --git a/packages/core/src/components/reviews/SortProductReviews/index.ts b/packages/core/src/components/reviews/SortProductReviews/index.ts
index 046fc86c75..c7fea2b622 100644
--- a/packages/core/src/components/reviews/SortProductReviews/index.ts
+++ b/packages/core/src/components/reviews/SortProductReviews/index.ts
@@ -1,2 +1,2 @@
-export { default as SortProductReviews } from './SortProductReviews'
+export { default } from './SortProductReviews'
 export type { SortProductReviewsProps } from './SortProductReviews'
diff --git a/packages/core/src/components/sections/ReviewsAndRatings/DefaultComponents.ts b/packages/core/src/components/sections/ReviewsAndRatings/DefaultComponents.ts
index 1a64907e00..7dfbce972a 100644
--- a/packages/core/src/components/sections/ReviewsAndRatings/DefaultComponents.ts
+++ b/packages/core/src/components/sections/ReviewsAndRatings/DefaultComponents.ts
@@ -5,18 +5,18 @@ const UIRatingSummary = dynamic(() =>
   import('@faststore/ui').then((module) => module.RatingSummary)
 )
 
-const ReviewModal = dynamic(
-  () =>
-    /* webpackChunkName: "ReviewModal" */
-    import('src/components/reviews/ReviewModal')
-)
-
 const RatingSummarySkeleton = dynamic(
   () =>
     /* webpackChunkName: "RatingSummarySkeleton" */
     import('src/components/skeletons/RatingSummarySkeleton')
 )
 
+const ReviewModal = dynamic(
+  () =>
+    /* webpackChunkName: "ReviewModal" */
+    import('src/components/reviews/ReviewModal')
+)
+
 const ReviewList = dynamic(
   () =>
     /* webpackChunkName: "ReviewList" */
diff --git a/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss b/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss
index ae061c4e79..100ddb5153 100644
--- a/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss
+++ b/packages/core/src/components/sections/ReviewsAndRatings/section.module.scss
@@ -1,11 +1,15 @@
 @layer components {
   .section {
     @import "@faststore/ui/src/components/atoms/Icon/styles.scss";
+    @import "@faststore/ui/src/components/atoms/Loader/styles";
     @import "@faststore/ui/src/components/atoms/Button/styles";
     @import "@faststore/ui/src/components/atoms/Link/styles";
     @import "@faststore/ui/src/components/atoms/Select/styles";
     @import "@faststore/ui/src/components/molecules/Rating/styles";
+    @import "@faststore/ui/src/components/molecules/ReviewCard/styles";
     @import "@faststore/ui/src/components/molecules/SelectField/styles";
+    @import "@faststore/ui/src/components/organisms/ReviewList/styles";
+    @import "@faststore/ui/src/components/organisms/EmptyReviewList/styles";
     @import "@faststore/ui/src/components/organisms/RatingSummary/styles";
 
     [data-fs-content="review-and-ratings"] {
@@ -20,26 +24,6 @@
           align-self: flex-start;
         }
       }
-
-      [data-fs-review-list] {
-        flex: 1;
-
-        [data-fs-review-list-header] {
-          --fs-review-list-header-border-color                                 : var(--fs-border-color-light);
-          --fs-review-list-header-border-width                                 : var(--fs-border-width);
-
-          display: flex;
-          flex-wrap: wrap;
-          gap: var(--fs-spacing-3);
-          align-items: center;
-          padding-bottom: var(--fs-spacing-0);
-          border-bottom: var(--fs-review-list-header-border-width) solid var(--fs-review-list-header-border-color);
-
-          [data-fs-select] {
-            --fs-select-min-height                                             : 2.375rem; // 38px
-          }
-        }
-      }
     }
   }
 }
diff --git a/packages/core/src/components/skeletons/ReviewCardSkeleton/ReviewCardSkeleton.tsx b/packages/core/src/components/skeletons/ReviewCardSkeleton/ReviewCardSkeleton.tsx
new file mode 100644
index 0000000000..e7f2b46b5e
--- /dev/null
+++ b/packages/core/src/components/skeletons/ReviewCardSkeleton/ReviewCardSkeleton.tsx
@@ -0,0 +1,50 @@
+import { Skeleton as UISkeleton } from '@faststore/ui'
+
+const ReviewCardSkeleton = () => {
+  return (
+    <div data-fs-review-card data-fs-review-card-skeleton>
+      <div data-fs-review-card-header>
+        <UISkeleton size={{ width: '80px', height: '20px' }} border="pill" />
+        <UISkeleton
+          size={{ width: '100px', height: '16px' }}
+          border="pill"
+          data-fs-review-card-author="desktop"
+        />
+        <UISkeleton
+          size={{ width: '80px', height: '16px' }}
+          data-fs-review-card-date="mobile"
+        />
+      </div>
+
+      <div data-fs-review-card-text>
+        <div data-fs-review-card-text-header>
+          <UISkeleton size={{ width: '60%', height: '24px' }} />
+          <UISkeleton
+            size={{ width: '80px', height: '16px' }}
+            data-fs-review-card-date="desktop"
+          />
+        </div>
+
+        <div
+          data-fs-review-card-text-content
+          style={{ display: 'flex', flexDirection: 'column', gap: 4 }}
+        >
+          <UISkeleton
+            size={{ width: '100%', height: '16px' }}
+            shimmer={false}
+          />
+          <UISkeleton size={{ width: '95%', height: '16px' }} shimmer={false} />
+          <UISkeleton size={{ width: '90%', height: '16px' }} shimmer={false} />
+        </div>
+      </div>
+
+      <UISkeleton
+        size={{ width: '100px', height: '16px' }}
+        border="pill"
+        data-fs-review-card-author="mobile"
+      />
+    </div>
+  )
+}
+
+export default ReviewCardSkeleton
diff --git a/packages/core/src/components/skeletons/ReviewCardSkeleton/index.ts b/packages/core/src/components/skeletons/ReviewCardSkeleton/index.ts
new file mode 100644
index 0000000000..3f1a0550b0
--- /dev/null
+++ b/packages/core/src/components/skeletons/ReviewCardSkeleton/index.ts
@@ -0,0 +1 @@
+export { default } from './ReviewCardSkeleton'
diff --git a/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx b/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx
index 103ddb5b04..490314bebb 100644
--- a/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx
+++ b/packages/core/src/components/ui/ReviewsAndRatings/ReviewsAndRatings.tsx
@@ -13,8 +13,10 @@ export type ReviewsAndRatingsProps = {
   }
   reviewModal: Omit<ReviewModalProps, 'product'>
   reviewList: {
-    filterSelect: ReviewListProps['filterSelect']
     sortSelect: ReviewListProps['sortSelect']
+    filterSelect: ReviewListProps['filterSelect']
+    emptyFilter: ReviewListProps['emptyFilter']
+    emptyList: ReviewListProps['emptyList']
   }
 }
 
@@ -35,7 +37,7 @@ function ReviewsAndRatings({
   const { isDesktop } = useScreenResize()
   const { product, isValidating } = context.data
 
-  const rating = context?.data?.product?.rating
+  const rating = product?.rating
 
   return (
     <>
@@ -57,7 +59,13 @@ function ReviewsAndRatings({
           )
         )}
 
-        <ReviewList.Component {...ReviewList.props} {...reviewList} />
+        <ReviewList.Component
+          {...ReviewList.props}
+          {...reviewList}
+          // Dynamic props shouldn't be overridable
+          // This decision can be reviewed later if needed
+          productId={product.id}
+        />
       </div>
 
       {displayReviewModal && !isValidating && (
diff --git a/packages/core/src/sdk/reviews/useProductReviews.ts b/packages/core/src/sdk/reviews/useProductReviews.ts
new file mode 100644
index 0000000000..2def1f7d44
--- /dev/null
+++ b/packages/core/src/sdk/reviews/useProductReviews.ts
@@ -0,0 +1,103 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort | undefined>(
+    initialSort
+  )
+  const [rating, setRating] = useState<number | undefined>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : undefined)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading } = useProductReviewsQuery({
+    productId,
+    first: REVIEWS_PER_PAGE,
+    after: page * REVIEWS_PER_PAGE,
+    sort,
+    rating: rating || undefined,
+  })
+
+  const loadMore = useCallback(() => {
+    if (isLoading) return
+    setLoadingMore(true)
+    setPage((prevPage) => prevPage + 1)
+  }, [isLoading])
+
+  const resetFilter = useCallback(() => {
+    setPage(0)
+    setRating(undefined)
+  }, [])
+
+  useEffect(() => {
+    if (!data?.reviews) return
+
+    // If it's the first page, replace reviews, otherwise append
+    if (page === 0) {
+      setReviews(data.reviews.data)
+    } else {
+      setLoadingMore(false)
+      setReviews((prev) => [...prev, ...data.reviews.data])
+    }
+
+    // Update total count
+    if (data.reviews.range) {
+      setTotalCount(data.reviews.range.total)
+    }
+  }, [data, page])
+
+  // Check if there are more reviews to load
+  const hasMore = reviews.length < totalCount
+
+  return {
+    reviews,
+    page,
+    totalCount,
+    loading: isLoading,
+    loadingMore,
+    hasMore,
+    currentSort: sort,
+    currentFilter: rating ? (String(rating) as FilterProductListReview) : 'all',
+    loadMore,
+    onSortChange: handleSortChange,
+    onFilterChange: handleFilterChange,
+    resetFilter,
+  }
+}
diff --git a/packages/core/src/sdk/reviews/useProductReviewsQuery.ts b/packages/core/src/sdk/reviews/useProductReviewsQuery.ts
new file mode 100644
index 0000000000..94d5a000bc
--- /dev/null
+++ b/packages/core/src/sdk/reviews/useProductReviewsQuery.ts
@@ -0,0 +1,53 @@
+import { gql } from '@generated/gql'
+import { type QueryOptions, useQuery } from '../graphql/useQuery'
+import type {
+  ClientProductReviewsQuery as Query,
+  ClientProductReviewsQueryVariables as Variables,
+} from '@generated/graphql'
+
+export const query = gql(`
+  query ClientProductReviews (
+    $productId: String!
+    $after: Int
+    $first: Int
+    $sort: StoreProductListReviewsSort
+    $rating: Int
+  ) {
+    reviews(
+      productId: $productId
+      after: $after
+      first: $first
+      sort: $sort
+      rating: $rating
+    ) {
+      data {
+        id
+        productId
+        rating
+        title
+        text
+        reviewDateTime
+        reviewerName
+        verifiedPurchaser
+      }
+      range {
+        from
+        to
+        total
+      }
+    }
+  }
+`)
+
+export const useProductReviewsQuery = (
+  variables: Variables,
+  options?: QueryOptions
+) => {
+  const { data, error, isLoading } = useQuery<Query, Variables>(
+    query,
+    variables,
+    { ...options }
+  )
+
+  return { data, isLoading, error }
+}
diff --git a/packages/ui/src/components/molecules/ReviewCard/styles.scss b/packages/ui/src/components/molecules/ReviewCard/styles.scss
index 3810a66b25..babb9a5d61 100644
--- a/packages/ui/src/components/molecules/ReviewCard/styles.scss
+++ b/packages/ui/src/components/molecules/ReviewCard/styles.scss
@@ -139,6 +139,7 @@
   [data-fs-review-card-text] {
     display: flex;
     flex-direction: column;
+    flex-grow: 1;
     gap: var(--fs-review-card-text-gap);
   }
 
diff --git a/packages/ui/src/components/organisms/EmptyReviewList/styles.scss b/packages/ui/src/components/organisms/EmptyReviewList/styles.scss
new file mode 100644
index 0000000000..e37b8a8922
--- /dev/null
+++ b/packages/ui/src/components/organisms/EmptyReviewList/styles.scss
@@ -0,0 +1,40 @@
+[data-fs-empty-review-list] {
+  // --------------------------------------------------------
+  // Structural Styles
+  // --------------------------------------------------------
+
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  gap: var(--fs-spacing-5);
+  align-items: center;
+  justify-content: center;
+  padding: var(--fs-spacing-6) 0;
+
+  [data-fs-empty-review-list-text] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--fs-spacing-0);
+    align-items: center;
+  }
+
+  [data-fs-empty-review-list-title] {
+    font-family: var(--fs-text-face-title);
+    font-size: var(--fs-text-size-3);
+    font-weight: var(--fs-text-weight-semibold);
+    line-height: var(--fs-text-size-5);
+    color: var(--fs-color-disabled-text);
+    text-align: center;
+    letter-spacing: -4%;
+  }
+
+  [data-fs-empty-review-list-subtitle] {
+    font-family: var(--fs-text-face-title);
+    font-size: var(--fs-text-size-2);
+    font-weight: var(--fs-text-weight-regular);
+    line-height: var(--fs-text-size-4);
+    color: var(--fs-color-disabled-text);
+    text-align: center;
+    letter-spacing: -2%;
+  }
+}
diff --git a/packages/ui/src/components/organisms/ReviewList/styles.scss b/packages/ui/src/components/organisms/ReviewList/styles.scss
new file mode 100644
index 0000000000..c81e38e7b1
--- /dev/null
+++ b/packages/ui/src/components/organisms/ReviewList/styles.scss
@@ -0,0 +1,25 @@
+[data-fs-review-list] {
+  // --------------------------------------------------------
+  // Structural Styles
+  // --------------------------------------------------------
+
+  flex: 1;
+
+  [data-fs-review-list-header] {
+    display: flex;
+    flex-wrap: wrap;
+    gap: var(--fs-spacing-3);
+    align-items: center;
+    padding-bottom: var(--fs-spacing-0);
+    border-bottom: var(--fs-border-width) solid var(--fs-border-color-light);
+
+    [data-fs-select] {
+      --fs-select-min-height: 2.375rem; // 38px
+    }
+  }
+
+  [data-fs-review-list-has-more] {
+    min-width: 13.75rem; // 220px
+    margin: var(--fs-spacing-5) auto 0;
+  }
+}


Reviews:
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T21:39:50Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:20:18Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:49:13Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:36:54Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:38:10Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:41:06Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:41:55Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:42:27Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:43:44Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:49:50Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:51:45Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T00:08:41Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T00:11:58Z
user: login: hellofanny
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T00:13:22Z
user: login: lucasfp13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T01:03:06Z
user: login: lucasfp13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T01:05:38Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:23:29Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:23:43Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:23:50Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:24:10Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:24:24Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:24:50Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:25:06Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:25:24Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:25:36Z
user: login: hellofanny
body: LGTM! 🚀 

6 reviews & Load More working as expected ✅ 
<img width="851" alt="image" src="https://github.com/user-attachments/assets/74fedd40-01b3-4f19-9dc3-94c7de0c8c20" />

Filter & Empty List ✅ 
<img width="1026" alt="image" src="https://github.com/user-attachments/assets/b1be3e4e-52da-4fe0-867a-c7a2e6e857d9" />
<img width="1005" alt="image" src="https://github.com/user-attachments/assets/26e1bcbf-b997-4fd5-8761-3c6e58185a8f" />

No reviews on mobile ✅ 
<img width="326" alt="image" src="https://github.com/user-attachments/assets/5bdf126d-9964-4439-b864-e60e5d903f11" />

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:24:40Z
user: login: lucasfp13
body: Left some suggestions 💬 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:04:45Z
user: login: lucasfp13
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T13:26:34Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:55:29Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:55:34Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:55:39Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:55:44Z
user: login: artursantiago
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T12:55:49Z

Review comments:
diff_hunk: @@ -0,0 +1,50 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        {/* TODO: Replace this SVG with the corresponding icon from the new library when it's available */}
+        <svg
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: nice! I'll create a task for it.
But I think we should  add this svg icon into `src/public/icons.svg`
It's a little bit tricky, if you need assist with it, lemme know.
created_at: 2025-03-21T21:39:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        {/* TODO: Replace this SVG with the corresponding icon from the new library when it's available */}
+        <svg
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: @artursantiago we have a guide here: https://github.com/vtex/faststore/tree/main/packages/core#managing-svg-icons

tell me if you need to help with it 🙏 
created_at: 2025-03-24T14:20:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        {/* TODO: Replace this SVG with the corresponding icon from the new library when it's available */}
+        <svg
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/86e53d76b3043a8444049169e3150119641e4b4b
created_at: 2025-03-24T14:49:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: ```suggestion
import { Button as UIButton, Icon as UIIcon} from '@faststore/ui'
```

**todo**: just to follow the patten 
created_at: 2025-03-24T23:36:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: ```suggestion
  title = 'There are no reviews for this item yet',
```

suggestion 
created_at: 2025-03-24T23:38:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <Icon name="ChatDots" width={56} height={56} />
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: ```suggestion
        <UIIcon name="ChatDots" width={56} height={56} />
```
created_at: 2025-03-24T23:41:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <Icon name="ChatDots" width={56} height={56} />
+        <h5 data-fs-empty-review-list-title>{title}</h5>
+        <p data-fs-empty-review-list-subtitle>{subtitle}</p>
+      </div>
+      <Button
+        data-fs-empty-review-list-write-review-button
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: ```suggestion
      <UIButton
        data-fs-empty-review-list-add-review-button
```
created_at: 2025-03-24T23:41:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <Icon name="ChatDots" width={56} height={56} />
+        <h5 data-fs-empty-review-list-title>{title}</h5>
+        <p data-fs-empty-review-list-subtitle>{subtitle}</p>
+      </div>
+      <Button
+        data-fs-empty-review-list-write-review-button
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: hellofanny
body: actually, I think we can remove this data-attr. 
created_at: 2025-03-24T23:42:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const PAGE_SIZE = 6
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: hellofanny
body: ```suggestion
const REVIEWS_PER_PAGE = 6
```

suggestion: Rename to be more descriptive
created_at: 2025-03-24T23:43:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const PAGE_SIZE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading, error } = useProductReviewsQuery({
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: hellofanny
body: are we going to use the `error`? 🤔 
created_at: 2025-03-24T23:49:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: hellofanny
body: ```suggestion
        productId={productId}
        onButtonClick={openReviewModal}
        title={emptyList?.title}
        subtitle={emptyList?.subtitle}
        buttonLabel={emptyList?.buttonLabel}
         {...props}
```

**nitpick**: Can we add this spread in the end 🙈
created_at: 2025-03-24T23:51:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+      />
+    )
+  }
+
+  return (
+    <div data-fs-review-list {...props}>
+      <div data-fs-review-list-header>
+        <SortProductReviews
+          id="fs-sort-product-reviews-select"
+          value={currentSort}
+          label={sortSelect?.label}
+          options={sortSelect?.options}
+          onChange={onSortChange}
+        />
+        <FilterProductReviews
+          id="fs-filter-product-reviews-select"
+          value={currentFilter}
+          label={filterSelect?.label}
+          options={filterSelect?.options}
+          onChange={onFilterChange}
+        />
+      </div>
+      <div data-fs-review-list-content>
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: hellofanny
body: I'm not sure if we need this div as wrapper. I think we can remove it.
created_at: 2025-03-25T00:08:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+      />
+    )
+  }
+
+  return (
+    <div data-fs-review-list {...props}>
+      <div data-fs-review-list-header>
+        <SortProductReviews
+          id="fs-sort-product-reviews-select"
+          value={currentSort}
+          label={sortSelect?.label}
+          options={sortSelect?.options}
+          onChange={onSortChange}
+        />
+        <FilterProductReviews
+          id="fs-filter-product-reviews-select"
+          value={currentFilter}
+          label={filterSelect?.label}
+          options={filterSelect?.options}
+          onChange={onFilterChange}
+        />
+      </div>
+      <div data-fs-review-list-content>
+        <UIList className="review-list-items">
+          {isFirstLoading ? (
+            <ReviewCardSkeleton />
+          ) : reviews.length > 0 ? (
+            reviews.map((review) => (
+              <li key={review.id} className="review-list-item">
+                <UIReviewCard
+                  title={review.title}
+                  text={review.text}
+                  rating={review.rating}
+                  author={review.reviewerName}
+                  isVerified={review.verifiedPurchaser}
+                  date={new Date(review.reviewDateTime)}
+                />
+              </li>
+            ))
+          ) : (
+            <EmptyReviewList
+              data-fs-empty-review-filter
+              productId={productId}
+              title={emptyFilter?.title}
+              subtitle={emptyFilter?.subtitle}
+              buttonLabel={emptyFilter?.buttonLabel}
+              onButtonClick={resetFilter}
+            />
+          )}
+        </UIList>
+      </div>
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: hellofanny
body: ```suggestion
  
      { isFirstLoading ? (
        <ReviewCardSkeleton />) : (
          reviews.length > 0 ? (
            <UIList className="review-list-items">
              {reviews.map((review) => (
                <li key={review.id} className="review-list-item">
                  <UIReviewCard
                    title={review.title}
                    text={review.text}
                    rating={review.rating}
                    author={review.reviewerName}
                    isVerified={review.verifiedPurchaser}
                    date={new Date(review.reviewDateTime)}
                  />
                </li>
              ))}
            </UIList>
          ):(
            <EmptyReviewList
            data-fs-empty-review-filter
            productId={productId}
            title={emptyFilter?.title}
            subtitle={emptyFilter?.subtitle}
            buttonLabel={emptyFilter?.buttonLabel}
            onButtonClick={resetFilter}
            />
          )
        )
      }
```
avoid rendering the `EmptyReviewList` inside the list.
created_at: 2025-03-25T00:11:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+[data-fs-review-list] {
+  // --------------------------------------------------------
+  // Structural Styles
+  // --------------------------------------------------------
+
+  flex: 1;
+
+  [data-fs-review-list-header] {
+    display: flex;
+    flex-wrap: wrap;
+    gap: var(--fs-spacing-3);
+    align-items: center;
+    padding-bottom: var(--fs-spacing-0);
+    border-bottom: var(--fs-border-width) solid var(--fs-border-color-light);
+
+    [data-fs-select] {
+      --fs-select-min-height                                             : 2.375rem; // 38px
path: packages/ui/src/components/organisms/ReviewList/styles.scss
user: login: hellofanny
body: ```suggestion
    [data-fs-select] {
      --fs-select-min-height: 2.375rem; // 38px
```
created_at: 2025-03-25T00:13:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const PAGE_SIZE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading, error } = useProductReviewsQuery({
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: If won't, use `error: _`
created_at: 2025-03-25T01:03:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+      />
+    )
+  }
+
+  return (
+    <div data-fs-review-list {...props}>
+      <div data-fs-review-list-header>
+        <SortProductReviews
+          id="fs-sort-product-reviews-select"
+          value={currentSort}
+          label={sortSelect?.label}
+          options={sortSelect?.options}
+          onChange={onSortChange}
+        />
+        <FilterProductReviews
+          id="fs-filter-product-reviews-select"
+          value={currentFilter}
+          label={filterSelect?.label}
+          options={filterSelect?.options}
+          onChange={onFilterChange}
+        />
+      </div>
+      <div data-fs-review-list-content>
+        <UIList className="review-list-items">
+          {isFirstLoading ? (
+            <ReviewCardSkeleton />
+          ) : reviews.length > 0 ? (
+            reviews.map((review) => (
+              <li key={review.id} className="review-list-item">
+                <UIReviewCard
+                  title={review.title}
+                  text={review.text}
+                  rating={review.rating}
+                  author={review.reviewerName}
+                  isVerified={review.verifiedPurchaser}
+                  date={new Date(review.reviewDateTime)}
+                />
+              </li>
+            ))
+          ) : (
+            <EmptyReviewList
+              data-fs-empty-review-filter
+              productId={productId}
+              title={emptyFilter?.title}
+              subtitle={emptyFilter?.subtitle}
+              buttonLabel={emptyFilter?.buttonLabel}
+              onButtonClick={resetFilter}
+            />
+          )}
+        </UIList>
+      </div>
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: lucasfp13
body: @artursantiago if you accept the suggestion above just remember to check for the blank spaces to be according with linter format locally.
created_at: 2025-03-25T01:05:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+      />
+    )
+  }
+
+  return (
+    <div data-fs-review-list {...props}>
+      <div data-fs-review-list-header>
+        <SortProductReviews
+          id="fs-sort-product-reviews-select"
+          value={currentSort}
+          label={sortSelect?.label}
+          options={sortSelect?.options}
+          onChange={onSortChange}
+        />
+        <FilterProductReviews
+          id="fs-filter-product-reviews-select"
+          value={currentFilter}
+          label={filterSelect?.label}
+          options={filterSelect?.options}
+          onChange={onFilterChange}
+        />
+      </div>
+      <div data-fs-review-list-content>
+        <UIList className="review-list-items">
+          {isFirstLoading ? (
+            <ReviewCardSkeleton />
+          ) : reviews.length > 0 ? (
+            reviews.map((review) => (
+              <li key={review.id} className="review-list-item">
+                <UIReviewCard
+                  title={review.title}
+                  text={review.text}
+                  rating={review.rating}
+                  author={review.reviewerName}
+                  isVerified={review.verifiedPurchaser}
+                  date={new Date(review.reviewDateTime)}
+                />
+              </li>
+            ))
+          ) : (
+            <EmptyReviewList
+              data-fs-empty-review-filter
+              productId={productId}
+              title={emptyFilter?.title}
+              subtitle={emptyFilter?.subtitle}
+              buttonLabel={emptyFilter?.buttonLabel}
+              onButtonClick={resetFilter}
+            />
+          )}
+        </UIList>
+      </div>
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/97ff83407748e5b837bed65aefcd82376781e46e
created_at: 2025-03-25T12:23:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
+      />
+    )
+  }
+
+  return (
+    <div data-fs-review-list {...props}>
+      <div data-fs-review-list-header>
+        <SortProductReviews
+          id="fs-sort-product-reviews-select"
+          value={currentSort}
+          label={sortSelect?.label}
+          options={sortSelect?.options}
+          onChange={onSortChange}
+        />
+        <FilterProductReviews
+          id="fs-filter-product-reviews-select"
+          value={currentFilter}
+          label={filterSelect?.label}
+          options={filterSelect?.options}
+          onChange={onFilterChange}
+        />
+      </div>
+      <div data-fs-review-list-content>
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/97ff83407748e5b837bed65aefcd82376781e46e
created_at: 2025-03-25T12:23:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,186 @@
+import { useUI } from '@faststore/components'
+import type { FilterProductListReview } from '../FilterProductReviews'
+import type { StoreProductListReviewsSort } from '@generated/graphql'
+import { useProductReviews } from 'src/sdk/reviews/useProductReviews'
+import dynamic from 'next/dynamic'
+import type { HTMLAttributes } from 'react'
+
+const UIReviewCard = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIReviewCard" */ '@faststore/ui').then(
+      (module) => module.ReviewCard
+    ),
+  { ssr: false }
+)
+
+const UIList = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIList" */ '@faststore/ui').then(
+      (module) => module.List
+    ),
+  { ssr: false }
+)
+const UIButton = dynamic(
+  () =>
+    import(/* webpackChunkName: "UIButton" */ '@faststore/ui').then(
+      (module) => module.Button
+    ),
+  { ssr: false }
+)
+
+const ReviewCardSkeleton = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "ReviewCardSkeleton" */ 'src/components/skeletons/ReviewCardSkeleton'
+    ),
+  { ssr: false }
+)
+
+const EmptyReviewList = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "EmptyReviewList" */ 'src/components/reviews/EmptyReviewList'
+    ),
+  { ssr: false }
+)
+const SortProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "SortProductReviews" */ 'src/components/reviews/SortProductReviews'
+    ),
+  { ssr: false }
+)
+const FilterProductReviews = dynamic(
+  () =>
+    import(
+      /* webpackChunkName: "FilterProductReviews" */ 'src/components/reviews/FilterProductReviews'
+    ),
+  { ssr: false }
+)
+
+export interface ReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  sortSelect?: {
+    label: string
+    options: Record<StoreProductListReviewsSort, string>
+  }
+  filterSelect?: {
+    label: string
+    options: Record<FilterProductListReview, string>
+  }
+  emptyList?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  emptyFilter?: {
+    title: string
+    subtitle: string
+    buttonLabel: string
+  }
+  loadMoreLabel?: string
+  productId: string
+}
+
+function ReviewList({
+  sortSelect,
+  filterSelect,
+  emptyFilter,
+  emptyList,
+  productId,
+  loadMoreLabel = 'Load more',
+  ...props
+}: ReviewListProps) {
+  const { openReviewModal } = useUI()
+  const {
+    reviews,
+    loading,
+    loadingMore,
+    hasMore,
+    currentSort,
+    currentFilter,
+    loadMore,
+    onSortChange,
+    onFilterChange,
+    resetFilter,
+  } = useProductReviews({
+    productId: productId,
+  })
+
+  const isFirstLoading = loading && !loadingMore
+
+  if (!loading && reviews.length <= 0 && currentFilter === 'all') {
+    return (
+      <EmptyReviewList
+        {...props}
+        productId={productId}
+        onButtonClick={openReviewModal}
+        title={emptyList?.title}
+        subtitle={emptyList?.subtitle}
+        buttonLabel={emptyList?.buttonLabel}
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/97ff83407748e5b837bed65aefcd82376781e46e
created_at: 2025-03-25T12:23:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const PAGE_SIZE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading, error } = useProductReviewsQuery({
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/3fdc6f83288bf45ec63bef2d264b28e61db18d1d
created_at: 2025-03-25T12:24:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const PAGE_SIZE = 6
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/d6029af45e10085552ef3bcffa44dea65e3ebd66
created_at: 2025-03-25T12:24:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <Icon name="ChatDots" width={56} height={56} />
+        <h5 data-fs-empty-review-list-title>{title}</h5>
+        <p data-fs-empty-review-list-subtitle>{subtitle}</p>
+      </div>
+      <Button
+        data-fs-empty-review-list-write-review-button
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/2c58148c2cbcc7b82770130db287a2c5255a5e71
created_at: 2025-03-25T12:24:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/b84c976f4edb9daab568f1fc5000b79514b8af2e
created_at: 2025-03-25T12:25:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/13c73f19265e1275acafb18a358ea0a032de4917
created_at: 2025-03-25T12:25:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+import { Button, Icon } from '@faststore/components'
+import type { HTMLAttributes } from 'react'
+
+export interface EmptyReviewListProps extends HTMLAttributes<HTMLDivElement> {
+  productId: string
+  title?: string
+  subtitle?: string
+  buttonLabel?: string
+  onButtonClick?: (productId: string) => void
+}
+
+function EmptyReviewList({
+  productId,
+  title = 'This item does not have any reviews yet',
+  subtitle = 'Be the first one to do it',
+  buttonLabel = 'Write the first review',
+  onButtonClick,
+  ...props
+}: EmptyReviewListProps) {
+  return (
+    <div data-fs-empty-review-list {...props}>
+      <div data-fs-empty-review-list-text>
+        <Icon name="ChatDots" width={56} height={56} />
path: packages/core/src/components/reviews/EmptyReviewList/EmptyReviewList.tsx
user: login: artursantiago
body: Done in https://github.com/vtex/faststore/pull/2724/commits/13c73f19265e1275acafb18a358ea0a032de4917
created_at: 2025-03-25T12:25:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +139,44 @@ function ReviewList({
           onChange={onFilterChange}
         />
       </div>
-      <div data-fs-review-list-content></div>
+      {isFirstLoading ? (
+        <ReviewCardSkeleton />
+      ) : reviews.length > 0 ? (
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: lucasfp13
body: **suggestion**: I'm still thinking about this nested conditionals here... what do you think about using `Suspense` here, as fallback we would use the skeleton?
created_at: 2025-03-26T12:51:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: We're already defining `initialRating` as optional.

```suggestion
  initialRating?: number
```
created_at: 2025-03-26T12:59:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: ```suggestion
  initialRating = undefined,
```
created_at: 2025-03-26T12:59:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: ```suggestion
  const [rating, setRating] = useState<number>(initialRating)
```
created_at: 2025-03-26T13:00:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: ```suggestion
    setRating(newRating >= 1 && newRating <= 5 ? newRating : undefined)
```
created_at: 2025-03-26T13:01:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading } = useProductReviewsQuery({
+    productId,
+    first: REVIEWS_PER_PAGE,
+    after: page * REVIEWS_PER_PAGE,
+    sort,
+    rating: rating || undefined,
+  })
+
+  const loadMore = useCallback(() => {
+    if (isLoading) return
+    setLoadingMore(true)
+    setPage((prevPage) => prevPage + 1)
+  }, [isLoading])
+
+  const resetFilter = useCallback(() => {
+    setPage(0)
+    setRating(null)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: lucasfp13
body: ```suggestion
    setRating(undefined)
```
created_at: 2025-03-26T13:03:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +139,44 @@ function ReviewList({
           onChange={onFilterChange}
         />
       </div>
-      <div data-fs-review-list-content></div>
+      {isFirstLoading ? (
+        <ReviewCardSkeleton />
+      ) : reviews.length > 0 ? (
path: packages/core/src/components/reviews/ReviewList/ReviewList.tsx
user: login: lucasfp13
body: I've tried the `Suspense` approach and it worked, but we still need both conditions to represent both states (empty and the list itself). We can improve it later by turning the `ReviewCardSkeleton` and even the `RatingSummarySkeleton` to receive children and work with loading states, like we have in `ProductGallery`.
created_at: 2025-03-26T13:26:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
+    setPage(0)
+    setReviews([])
+  }, [])
+
+  const handleSortChange = useCallback(
+    (newSort: StoreProductListReviewsSort) => {
+      setSort(newSort)
+      setPage(0)
+      setReviews([])
+    },
+    []
+  )
+
+  // Fetch reviews with the current filter, sort, and pagination
+  const { data, isLoading } = useProductReviewsQuery({
+    productId,
+    first: REVIEWS_PER_PAGE,
+    after: page * REVIEWS_PER_PAGE,
+    sort,
+    rating: rating || undefined,
+  })
+
+  const loadMore = useCallback(() => {
+    if (isLoading) return
+    setLoadingMore(true)
+    setPage((prevPage) => prevPage + 1)
+  }, [isLoading])
+
+  const resetFilter = useCallback(() => {
+    setPage(0)
+    setRating(null)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: https://github.com/vtex/faststore/pull/2724/commits/44d1ab58afb9932858ca93c8689f8ce2d9d84222
created_at: 2025-03-28T12:55:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
+  const [reviews, setReviews] = useState<ProductReview[]>([])
+  const [totalCount, setTotalCount] = useState(0)
+  const [loadingMore, setLoadingMore] = useState(false)
+
+  const handleFilterChange = useCallback((filter: FilterProductListReview) => {
+    const newRating = Number(filter)
+    setRating(newRating >= 1 && newRating <= 5 ? newRating : null)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: https://github.com/vtex/faststore/pull/2724/commits/44d1ab58afb9932858ca93c8689f8ce2d9d84222
created_at: 2025-03-28T12:55:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
+}: Props) {
+  const [page, setPage] = useState(0)
+  const [sort, setSort] = useState<StoreProductListReviewsSort>(initialSort)
+  const [rating, setRating] = useState<number | null>(initialRating)
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: https://github.com/vtex/faststore/pull/2724/commits/44d1ab58afb9932858ca93c8689f8ce2d9d84222
created_at: 2025-03-28T12:55:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
+}
+
+export function useProductReviews({
+  productId,
+  initialSort = 'reviewDateTime_desc',
+  initialRating = null,
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: https://github.com/vtex/faststore/pull/2724/commits/44d1ab58afb9932858ca93c8689f8ce2d9d84222
created_at: 2025-03-28T12:55:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+import { useState, useCallback, useEffect } from 'react'
+import type {
+  ClientProductReviewsQuery,
+  StoreProductListReviewsSort,
+} from '@generated/graphql'
+import { useProductReviewsQuery } from './useProductReviewsQuery'
+import type { FilterProductListReview } from 'src/components/reviews/FilterProductReviews/FilterProductReviews'
+
+type ProductReview = ClientProductReviewsQuery['reviews']['data'][number]
+
+const REVIEWS_PER_PAGE = 6
+
+interface Props {
+  productId: string
+  initialSort?: StoreProductListReviewsSort
+  initialRating?: number | null
path: packages/core/src/sdk/reviews/useProductReviews.ts
user: login: artursantiago
body: https://github.com/vtex/faststore/pull/2724/commits/44d1ab58afb9932858ca93c8689f8ce2d9d84222
created_at: 2025-03-28T12:55:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: codesandbox-ci[bot]
created_at: 2025-03-17T19:30:27Z
body: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/vtex/faststore/pr/2724/builds/602052) or the icon next to each commit SHA.
author_association: NONE
user: login: hellofanny
created_at: 2025-03-21T21:40:42Z
body: @artursantiago we need to rebase this branch 🙈 A lot of changes outdated. Can you help me updating it again? Thanks!
author_association: CONTRIBUTOR
user: login: hellofanny
created_at: 2025-03-25T00:23:43Z
body: I think it's probably missing some top/bottom margin on mobile. It looks squeezed. WDYT?

<img width="412" alt="image" src="https://github.com/user-attachments/assets/02de834f-f732-4090-9704-e822de00a543" />


author_association: CONTRIBUTOR