[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/docs/README.md from the search-result VTEX app.

[Assistant]
Here's the implementation:

```markdown
📢 Use this project, [contribute](https://github.com/vtex-apps/search-result) to it or open issues to help evolve it using [Store Discussion](https://github.com/vtex-apps/store-discussion).

# Search Result

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

The Search Result app handles the results returned by the [VTEX Search API](https://developers.vtex.com/docs/guides/search-overview) and displays them to users.

The app exports all store blocks expected on a search results page, such as the filters and the product gallery.

![search-result](https://user-images.githubusercontent.com/52087100/77557721-d96b6580-6e98-11ea-9178-77c8c4a6408e.png)

## Configuration

To configure the VTEX Search Result, follow the instructions below.

### Adding the Search Result app to your theme's dependencies

In your theme's `manifest.json`, add the Search Result app as a dependency:

```json
"dependencies": {
    "vtex.search-result": "3.x"
}
```

Now, you can use all the blocks exported by the `search-result` app. See the complete list below:

#### `search-result` blocks

| Block name | Description |
|---|---|
| `search-result-layout` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Builds the search result page using its three children blocks: `search-result-layout desktop`, `search-result-layout.mobile`, and `search-not-found-layout`. Use it in the `store.search` template, as it uses context from the VTEX Search API. |
| `search-result-layout.customQuery` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Use this block instead of `search-result-layout` when the search result is declared in a template that doesn't fetch Search context, such as Home. It accepts a `querySchema` prop that executes search custom queries. It also supports three children blocks: `search-result-layout.desktop`, `search-result-layout.mobile`, and `search-not-found-layout`. |
| `search-result-layout.desktop` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Builds the search result page structure for desktop mode. |
| `search-result-layout.mobile` | Builds the search result page structure for mobile mode. If `search-result-layout.mobile` isn't provided, `search-result-layout.desktop` is used. |
| `search-layout-switcher` | Enables mobile users to switch between layout modes. |
| `search-not-found-layout` | Builds the entire search results page structure for scenarios where no result was found. It renders when users search for a term that doesn't return a product. |
| `gallery` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Displays the gallery with all the products found in the search. |
| `gallery-layout-switcher` | Allows users to switch between available `gallery` layouts. Learn more in the guide [Building a search results page with multiple layouts](https://developers.vtex.com/vtex-developer-docs/docs/vtex-io-documentation-building-a-search-results-page-with-multiple-layouts). |
| `gallery-layout-option` | Defines how each layout option renders for users. Learn more in the guide [Building a search results page with multiple layouts](https://developers.vtex.com/vtex-developer-docs/docs/vtex-io-documentation-building-a-search-results-page-with-multiple-layouts). |
| `not-found` | Contains text and a description for a page not found in the search. Declare it as a child of `search-not-found-layout`. |
| `search-content` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Decides, behind the scenes, which block to display: either the `gallery` block (if products are found) or the `not-found` (if selected filters lead to an empty search results page). This means both `gallery` and `not-found` must be declared `search-content` children. |
| `store.not-found#search` | When configured, it displays a `404` error message when the server can’t return what the browser's request is, or isn’t configured to handle that request. |
| `search-products-count-per-page` | Displays the total number of products on the search results page. |
| `search-products-progress-bar` | Displays a progress bar of products on the search results page. |
| `order-by.v2` | Allows users to choose the product order on the search results page. |
| `filter-navigator.v3` | Allows users to apply different filters to the search. On mobile, it renders a button that, when clicked, displays all available filters in a sidebar. |
| `total-products.v2` | Displays the total number of products found for that search. |
| `search-title.v2` | Displays a title for the performed search. |
| `search-fetch-more` | Displays the **Show more** button. This button doesn't render when the user is on the last page. |
| `search-fetch-previous` | Displays the **Show previous** button. This button doesn't render when the user is on the first page. |
| `sidebar-close-button` | Displays an `X` button on the filter sidebar on mobile. |

## Adding the Search Result to page templates

Search Result app data usually displays on search pages (`store.search`), but you can add it to any other page.

When adding to the search page, use the `search-result-layout`, which fetches data from the template's current search context. To add the app to another page, use the [`search-result-layout.customQuery`](#search-result-blocks) block.

On the desired store page, add the `search-result-layout` block or the `search-result-layout.customQuery` to the correct template blocks list. Check both codes below as examples.

On search pages:

```json
"store.search": {
 "blocks": ["search-result-layout"]
}
```

On other pages:

```diff
"store.home": {
    "blocks": [
        "carousel#home",
        "shelf#home",
   + "search-result-layout.customQuery#home"
    ]
}
```

### Defining how the search query data should be fetched

Before declaring the blocks into the search results layout, define how the search results will be fetched.

You should define these results through a custom query on the homepage. On the search template, use the already provided context.

If you use `search-result-layout`, the blocks define the fetched data from the `context`. If you use `search-result-layout.customQuery`, send the props through the `querySchema` to configure the custom query.

Using `search-result-layout`:

```json
{
  "store.search": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  }
}
```

Using `search-result-layout.customQuery`:

```json
{
  "store.home": {
    "blocks": [
      "carousel#home",
      "shelf#home",
      "search-result-layout.customQuery#home"
    ]
  },
  "search-result-layout.customQuery#home": {
    "props": {
      "querySchema": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  }
}
```

Check all props to configure your search data in the table below.

### The `context` and `querySchema` props

| Prop name | Type | Description | Default value |
|---|---|---|---|
| `queryField` | `string` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Query string of the search URL that defines the results to fetch in the custom query. Example: `Blue`. This prop only works if `mapField` is also declared. | `undefined` |
| `mapField` | `string` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Search URL's `map` parameter to define which results to fetch in the custom query. Example: `specificationFilter_100`. This prop only works if the `queryField` prop is also declared. | `undefined` |
| `maxItemsPerPage` | `number` |Maximum number of items per search page. The maximum value of this prop is `50`. If a higher number is added, the query will fail. | `10` |
| `orderByField` | `enum` | Determines the order products follow when displayed. Possible values are named after the sorting type: `OrderByReleaseDateDESC`, `OrderByBestDiscountDESC`, `OrderByPriceDESC`, `OrderByPriceASC`, `OrderByNameASC`, `OrderByNameDESC`, or `OrderByTopSaleDESC`. <br/><br/>`ASC` and `DESC` stand for ascending order and descending order, respectively, based on the position of each value's corresponding code in the [ASCII table](http://www.asciitable.com/). <br/><br/>`OrderByTopSaleDESC` considers the number of units sold of the product in the past 90 days, taking into account only ecommerce orders (no physical store orders) from `order-placed` events (Example: Without checking if the payment was approved). If the store has an app, it's possible to consider events from the app — provided they're implemented on the store's side, as they aren't enabled by default. If the shopper has an ad-blocking extension or a browser restriction that disables sending events, their navigation won't be counted.<br/><br/>If not set to any of the mentioned values, the fallback behavior is sorting by [relevance settings](https://help.vtex.com/tracks/vtex-intelligent-search--19wrbB7nEQcmwzDPl1l4Cb/1qlObWIib6KqgrfX1FCOXS). <br/><br/>`OrderByScoreDESC` is **not** a valid value for this prop. | `""` |
| `hideUnavailableItems` | `boolean` | Determines whether the search result should hide unavailable items (`true`) or not (`false`). This prop only hides items that are unavailable according to indexed information, without taking into account `simulationBehavior`. | `false` |
| `facetsBehavior` | `string` | Defines the filters' behavior. When set to `Dynamic`, results are restricted according to the filters the user selected. If set to `Static`, all filters continue to display to the user, even if there are no results. | `Static` |
| `skusFilter` | `enum` | Refines the SKUs returned for each product in the query. Fewer returned SKUs result in a more performant shelf query. Available value: `FIRST_AVAILABLE` (returns only the first available SKU), `ALL_AVAILABLE` (returns all available SKUs), and `ALL` (returns all product SKUs). | `ALL_AVAILABLE` |
| `simulationBehavior` | `enum` | Defines whether the search data will be up-to-date (`default`) or fetched using the cache (`skip`). Use the last option only if you prefer faster queries over the most up-to-date prices or inventory. | `default` |
| `installmentCriteria` | `enum` | Defines which price to display when different installments are available. Possible values: `MAX_WITHOUT_INTEREST` (displays the maximum installment option with no interest attached) or ´MAX_WITH_INTEREST` (displays the maximum installment option, whether it has interest attached or not). | `"MAX_WITHOUT_INTEREST"` |
| `excludedPaymentSystems` | `string` | List of payment systems not to consider when displaying installment options to users. This prop works only if `installmentCriteria` is also declared. Otherwise, all available payment systems display regardless. | `undefined` |
| `includedPaymentSystems` | `string` | List of payment systems to consider when displaying installment options to users. This prop works only if `installmentCriteria` is also declared. Otherwise, all available payment systems display regardless. | `undefined` |

>ℹ Pagination doesn't display results after page 50. You can configure it to display more products per page by increasing the number of products on each page using the `maxItemsPerPage` prop.

>⚠ When the `simulationBehavior` prop is set as `skip`, it defines that the search data should only be fetched using the store's cache. This may impact the content displayed on store pages, as cache storage changes based on user interaction on each page.

You must define the query for the following search pages:

* Brand
* Department
* Category
* Subcategory

This allows you to define custom behaviors for your store's search pages. For example:

```json
{
  "store.search": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  },
  "store.search#category": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  },
  "store.search#brand": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  },
  "store.search#department": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  },
  "store.search#subcategory": {
    "blocks": ["search-result-layout"],
    "props": {
      "context": {
        "skusFilter": "FIRST_AVAILABLE",
        "simulationBehavior": "default"
      }
    }
  }
}
```

### Defining your search results page's layout and behavior

Now you can structure the `search-result-layout` or the `search-result-layout.customQuery` blocks. Both require the `search-result-layout.desktop` as a child. You can also provide props to other children, such as `search-result-layout.mobile` and `search-not-found-layout`.

Depending on your store's scenario, structure the `search-result-layout` or the `search-result-layout.customQuery` by declaring their children and then configuring them using the [Flex Layout](https://developers.vtex.com/docs/guides/vtex-flex-layout) blocks and their props. For example:

```json
{
  "search-result-layout": {
    "blocks": [
      "search-result-layout.desktop",
      "search-result-layout.mobile",
      "search-not-found-layout"
    ]
  },
  "search-result-layout.desktop": {
    "children": [
      "flex-layout.row#searchbread",
      "flex-layout.row#searchtitle",
      "flex-layout.row#result"
    ],
    "props": {
      "preventRouteChange": true
    }
  }
}
```

#### The `search-result-layout.desktop`, `search-result-layout.mobile`, and `search-not-found-layout` props

| Prop name                 | Type      | Description              | Default value   |
|--------------------------|-----------|---------------------------|-----------------|
| `hiddenFacets`            | `object`  | Indicates which filters should be hidden. The possible values are in [this table](#the-hiddenfacets-object).                                                                                | `undefined`     |
| `showFacetQuantity`       | `boolean` |Determines whether the resulting amount in each filter should appear beside its name on the `filter-navigator.v3` block (`true`) or not (`false`).                                                                                | `false`         |
| `showFacetTitle`          | `boolean` | Determines whether the facet title should appear on the selected filters section on the `filter-navigator.v3` block (`true`) or not (`false`).                                                                                | `false`         |
| `blockClass`              | `string`  | Unique block ID for [CSS customization](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-css-handles-for-store-customization#using-the-blockclass-property).                                                          | `undefined`     |
| `trackingId`              | `string`  | ID for Google Analytics to track store metrics based on the Search Result block.                                                                                | `Search result` |
| `mobileLayout`            | `object`  | Controls how the search results page displays to users using the mobile layout. Possible values are in [this table](#the-mobilelayout-object).                                                                                | `undefined`     |
| `defaultGalleryLayout`    | `string`  | Determines the default name of the gallery layout on the search results page. This prop is required when the `gallery` block explicitly defines several layouts. This prop's value must match the layout name defined in the `name` prop from `layouts` object.                          | `undefined`     |
| `thresholdForFacetSearch` | `number`  | Specifies the minimum number of facets that must be displayed for the search bar to appear. If you declare `0`, the search bar always displays.                                                                                | `undefined`     |
| `preventRouteChange`      | `boolean` | Keeps page customizations even when the user applies new filters. This prop only changes the URL's query string rather than the entire URL, preventing a full page reload when filters are applied.                                                                                | `false`         |
| `showShippingFacet`       | `boolean` | Determines whether Shipping filters should display (`true`) or not (`false`).                                                                                | `false`         |

#### The `mobileLayout` object

| Prop name | Type   | Description                                                                                | Default value |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `mode1`   | `enum` | Defines the default layout for the mobile search results page. Possible values: `normal`, `small`, or `inline`.                                                           | `normal`      |
| `mode2`   | `enum` | Defines which layout sets for the mobile search results page when users click the layout selector button. Possible values are also `normal`, `small`, or `inline`. | `small`       |

#### The `hiddenFacets` object

| Prop name              | Type      | Description                                                                                | Default value |
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `brands`               | `boolean` | Determines whether Brand filters should be hidden (`true`) or not (`false`).                                                       | `false`       |
| `categories`           | `boolean` | Determines whether Category filters should be hidden (`true`) or not (`false`).                                                    | `false`       |
| `priceRange`           | `boolean` | Determines whether Price filters should be hidden (`true`) or not (`false`).                                                       | `false`       |
| `specificationFilters` | `object`  | Indicates which specification filters should be hidden. Possible values are in [this table](#the-specificationfilters-object). | `undefined`   |

#### The `specificationFilters` object

| Prop name       | Type      | Description                                                                                | Default value |
| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `hideAll`       | `boolean` | Determines whether specification filters should be hidden (`true`) or not (`false`).                                             | `false`       |
| `hiddenFilters` | `object`  | Object array of specification filters that should be hidden. Possible values are in [this table](#the-hiddenfilters-object). | `undefined`   |

#### The `hiddenFilters` object

| Prop name | Type     | Description                                             | Default value |
| --------- | -------- | ------------------------------------------------------- | ------------- |
| `name`    | `string` | Defines the name of the specification filter that you want to hide. | `undefined`   |

### Using the Flex Layout to build your search results page

You can build your search results page with the [Flex Layout](https://developers.vtex.com/docs/guides/vtex-flex-layout) app and the other blocks exported by the Search Results app, such as the `gallery`.

Below are the available blocks to build your store's search results page and their existing props.

#### The `gallery` block

The `gallery` block defines how fetched items display on the store's search results page.

When declared with no props, it expects the [`product-summary.shelf`](https://developers.vtex.com/docs/guides/vtex-product-summary) as a child and consequently the block structure inherited from it.

However, you can use the `layouts` prop to provide several layouts for the page, allowing your store to arrange items that best fit your users' needs.

In a scenario where multiple layouts are provided, your store users will be able to shift between them according to their needs using the `gallery-layout-switcher` block, described in [the table below](#the-gallery-layout-switcher-props). The `gallery` will then render the component provided by the currently selected layout.

Learn more in [Building a search results page with multiple layouts](https://developers.vtex.com/docs/guides/vtex-io-documentation-building-a-search-results-page-with-multiple-layouts).

##### The `gallery-layout-switcher` props

| Prop name               | Type               | Description                                                                                | Default value       |
| ----------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `layouts`               | `object`           | Arranges and displays items on the search results page. If no value is provided, the `gallery` block must receive a `product-summary-shelf` block as a child. Check [this table](#the-layouts-object) for props of this block.                                                                                | `undefined`         |
| `undefined`             | `block`            | Defines which blocks should reder per layout. The prop name is not `undefined`, you must include the value passed on the `component` prop. This prop's value must match the block name you choose to render in that specific layout.                                                                                | `undefined`         |
| `customSummaryInterval` | `number`           | Defines the item interval at which the **Gallery** should render a custom `product-summary` block. For example, declaring `5` renders a custom block at every four items rendered, as shown [on this image](https://user-images.githubusercontent.com/1207017/101687291-0cff1780-3a49-11eb-9c00-678b70001c8a.jpg). This prop doesn't support `layouts` yet. | `undefined`         |
| `CustomSummary`         | `block`            | Defines a block to render according to the interval defined by the `customSummaryInterval` prop.                                                                                | `undefined`         |
| `preferredSKU`          | `PreferredSKUEnum` | Controls which SKU will initially be selected in the product summary.                                                                                | `"FIRST_AVAILABLE"` |

For `PreferredSKUEnum`:

| Name            | Value             | Description                                                    |
| --------------- | ----------------- | -------------------------------------------------------------- |
| First Available | `FIRST_AVAILABLE` | First available SKU in stock found or first SKU without stock. |
| Last Available  | `LAST_AVAILABLE`  | Last available SKU in stock found or last SKU without stock.   |
| Cheapest        | `PRICE_ASC`       | Cheapest SKU in stock found or first SKU without stock.        |
| Most Expensive  | `PRICE_DESC`      | Most expensive SKU in stock found or first SKU without stock.  |

>⚠ To select which SKU should take preference over this prop, you can create a product specification (field) and, for each product, assign the value of the desired SKU to be initially selected. If the specification doesn't exist or the value is empty, it will use the `preferredSKU` prop as a fallback. Learn more in [Recipes](https://developers.vtex.com/docs/guides/storefront-implementation).

##### The `layouts` object

| Prop name     | Type                | Description                                                                                | Default value |
| ------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `name`        | `string`            | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Specifies the layout name. This value must be unique. For example, not equal to other layout names declared in the `gallery` block.                                                                                | `undefined`   |
| `component`   | `string`            | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Names the `undefined` prop from the `gallery` block, which declares the block to render in this layout. This prop's value can be anything as long as it's PascalCased, meaning the first letter of each word in its name is capitalized. **Caution**: For this to work, the chosen value must be named after the `gallery` block's `undefined` prop. _Don’t use the `component` prop's value to directly pass the desired block name itself_. See the example below to understand the underlying logic behind this prop. | `undefined`   |
| `itemsPerRow` | `number` / `object` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Specifies the number of items to display in each row of this layout. This prop works with [responsive values](https://developers.vtex.com/docs/apps/vtex.responsive-values/), so it also accepts an object with different numbers for desktop, tablet, or phone screen sizes (_see the table below_).                                                                                | `undefined`   |

##### The `itemsPerRow` object

| Prop name | Type     | Description                                      | Default value |
| --------- | -------- | ------------------------------------------------ | ------------- |
| `desktop` | `number` | Number of slides to show on desktop devices. | `undefined`   |
| `tablet`  | `number` | Number of slides to show on tablet devices.  | `undefined`   |
| `phone`   | `number` | Number of slides to show on phone devices.   | `undefined`   |

##### Example of a `gallery` block

```json
{
  "gallery": {
    "props": {
      "layouts": [
        {
          "name": "whole",
          "component": "OneOrTwoLineSummary",
          "itemsPerRow": 1
        },
        {
          "name": "two",
          "component": "OneOrTwoLineSummary",
          "itemsPerRow": 2
        },
        {
          "name": "many",
          "component": "ManyByLineSummary",
          "itemsPerRow": {
            "desktop": 5,
            "mobile": 1
          }
        }
      ],
      "OneOrTwoLineSummary": "product-summary.shelf",
      "ManyByLineSummary": "product-summary.shelf"
    }
  }
}
```

#### The `gallery-layout-switcher` block

The `gallery-layout-switcher` block is a logical block that allows users to switch between the available `gallery` layouts.

It receives no props and expects [the `gallery-layout-option` block](#the-gallery-layout-option-block) as a child. For the accessibility features to work correctly, define the options in the same order as the layouts.

#### The `gallery-layout-option` block

This block defines how each layout option renders for users.

| Prop name | Type     | Description                                                                                | Default value |
| --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `name`    | `string` | ![https://img.shields.io/badge/-Mandatory-red](https://img.shields.io/badge/-Mandatory-red)<br/><br/>Specifies the name of the layout option. This prop's value must match the one passed to the [`name` prop](#the-layouts-object). | `undefined`   |

##### Example of the `gallery-layout-switcher` and the `gallery-layout-option` blocks

```jsonc
{
  "gallery-layout-switcher": {
    "children": [
      //It follows the same whole -> two -> many order
      "gallery-layout-option#whole",
      "gallery-layout-option#two",
      "gallery-layout-option#many"
    ]
  },
  "gallery-layout-option#whole": {
    "props": {
      "name": "whole"
    },
    "children": ["icon-single-grid", "rich-text#option-whole"]
  },
  "gallery-layout-option#two": {
    "props": {
      "name": "two"
    },
    "children": ["icon-inline-grid", "rich-text#option-two"]
  },
  "gallery-layout-option#many": {
    "props": {
      "name": "many"
    },
    "children": ["icon-menu", "rich-text#option-many"]
  }
}
```

#### `filter-navigator.v3` block

This block renders a filter selector for the fetched results.

| Prop name | Type | Description | Default value |
|---|---|---|---|
| `categoryFiltersMode` | `enum` | Determines whether the category filters use the `href` attribute with category page URLs (`href`) or not (`default`). By default, the filters use HTML divs with `role="link"`. Setting this prop's value to `href` creates a link, which can improve the SEO ranking of your category pages. | `default` |
| `layout` | `enum` | Determines whether the **Filter Navigator** layout is responsive (`responsive`) or not (`desktop`). You can use `desktop` when the **Filter Navigator** is configured to display in a [drawer](https://developers.vtex.com/docs/guides/vtex-store-drawer). | `responsive` |
| `maxItemsDepartment` | `number` | Specifies the maximum number of departments to display before the **See more** button triggers. | `8` |
| `maxItemsCategory` | `number` | Specifies the maximum number of category items to display before the **See more** button triggers. | `8` |
| `initiallyCollapsed` | `boolean` | Makes the search filters start collapsed (`true`) or open (`false`). | `false` |
| `openFiltersMode` | `enum` | Defines how many filters can open simultaneously on the **Filter Navigator** component. Possible values are `many` (more than one filter can open simultaneously) and `one` (only one filter can open). If `one` is declared, all filters collapse before user interaction, regardless of the `initiallyCollapsed` prop's value. | `many` |
| `filtersTitleHtmlTag` | `string` | Specifies the HTML tag for the filter's title. | `h5` |
| `scrollToTop` | `enum` | Scrolls the page to the top (`auto` or `smooth`) or not (`none`) when selecting a facet. | `none` |
| `truncateFilters` | `boolean` | Determines whether a filter selector with more than 10 filter options shortens the list and displays a **See more** button (`true`) or not (`false`). | `false` |
| `closeOnOutsideClick` | `boolean` | Determines whether the **Filter Navigator** component closes when users click outside of it (`true`) or not (`false`). This prop only works if the `openFiltersMode` prop is set to `one`. | `false` |
| `appliedFiltersOverview` | `enum` | Determines whether an overview of the applied filters displays (`show`) or not (`hide`). | `hide` |
| `totalProductsOnMobile` | `enum` | Determines whether the **Filter Navigator** displays the total number of products on mobile devices (`show`) or not (`hide`). | `hide` |
| `fullWidthOnMobile` | `boolean` | Determines whether the `filter-navigator.v3` renders on mobile using the full screen width (`true`) or not (`false`). | `false` |
| `navigationTypeOnMobile` | `enum` | Defines how mobile users should navigate on the filter selector component. Possible values are `page` (only one list of options can be seen at a time) or `collapsible` (all lists of options can be seen simultaneously). | `page` |
| `updateOnFilterSelectionOnMobile` | `boolean` | Determines whether search results on mobile update based on the filter selection (`true`) or not (`false`). This prop only works if the `preventRouteChange` prop is declared as `true`. | `false` |
| `drawerDirectionMobile` | `Enum` | Determines whether search filters on mobile open to the left (`drawerLeft`) or to the right (`drawerRight`). | `drawerLeft` |
| `showClearByFilter` | `boolean` | Determines whether a clear button (which erases all selected filter options) displays alongside the filter name (`true`) or not (`false`). | `false` |
| `showClearAllFiltersOnDesktop` | `boolean` | Determines whether a clear button displays (`true`) or not (`false`). This button resets all selected filters. | `false` |
| `priceRangeLayout` | `enum` | Determines whether a text field for entering the desired price range displays (`inputAndSlider`) or not (`slider`). | `slider` |
| `facetOrdering` | `array` | Applies custom sorting rules for filters. The default behavior sorts items by quantity, in descending order. | `undefined` |
| `showQuantityBadgeOnMobile` | `boolean` | Displays a badge for mobile users indicating how many active filters there are. | `false` |

- **`facetOrdering` object:**

| Prop name | Type | Description | Default value |
|---|---|---|---|
| `key` | `string` | Specifies the facet key to sort. Possible values are `category-1`, `category-2`, `category-3` (for department, category, and subcategory), `brand`, or a product specification name. | `undefined` |
| `orderBy` | `enum` | Specifies the field from facets to use when sorting entries. Possible values are `name` and `quantity`. | `undefined` |
| `order` | `enum` | Determines whether the filter should sort by ascending (`ASC`) or descending (`DESC`) order. | `ASC` |

For example:

```jsonc
{
  "filter-navigator.v3": {
    "props": {
      "facetOrdering": [
        {
          "key": "brand",
          "orderBy": "name",
          "order": "ASC"
        }
      ]
    }
  }
}
```

>⚠ The `facetOrdering` prop conflicts with the `enableFiltersFetchOptimization` flag on `vtex.store`, as it returns only the top filter values ordered by count. To achieve the desired outcome with `facetOrdering`, set `enableFiltersFetchOptimization` as `false` in your `vtex.store` Admin settings.

#### The `order-by` block

The `order-by` block renders a dropdown button with [sorting options](#the-sorting-options-for-the-order-by-block) to display the fetched results. See the block props in the table below.

##### The `order-by` props

| Prop name              | Type       | Description                                                                                | Default value |
| ---------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `specificationOptions` | `[object]` | Indicates which sorting options by specification will be displayed. This only works for stores using `vtex.search-resolver@1.x` | `undefined`   |
| `hiddenOptions`        | `[string]` | Indicates which sorting options will be hidden. Example: `["OrderByNameASC", "OrderByNameDESC"]`                                  | `undefined`   |
| `showOrderTitle`       | `boolean`  | Determines whether the selected order value (example: `Relevance`) displays (`true`) or not (`false`).                     | `true`        |

- **`specificationOptions` Object:**

  | Prop name | Type   | Description                                                                                | Default value |
  | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
  | value     | string | Value sent for sorting in the API. It must be in the format `{specification key}:{asc/desc}`. Example: `"size:desc"` or `"priceByUnit:asc"`. | `undefined`   |
  | label     | string | Label that displays in the sorting options. Example: `"Price by unit, ascending"`                                                                       | `undefined`   |

##### Sorting options for the `order-by` block

| Sorting option           | Value                       |
| ------------------------ | --------------------------- |
| Relevance                | `""`                        |
| Top Sales Descending     | `"OrderByTopSaleDESC"`      |
| Release Date Descending  | `"OrderByReleaseDateDESC"`  |
| Best Discount Descending | `"OrderByBestDiscountDESC"` |
| Price Descending         | `"OrderByPriceDESC"`        |
| Price Ascending          | `"OrderByPriceASC"`         |
| Name Ascending           | `"OrderByNameASC"`          |
| Name Descending          | `"OrderByNameDESC"`         |
| Collection               | `"OrderByCollection"`       |

#### The `search-fetch-more` block

The `search-fetch-more` block renders a `Show more` button that loads the following search results page. See the block props in the table below.

>ℹ This block doesn't render if there is no next page.

##### The `search-fetch-more` prop

| Prop name              | Type   | Description                                                                                | Default value |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `htmlElementForButton` | `enum` | Specifies which HTML element displays for the `Show more` button component. Possible values: `a` (displays a `<a>` element with `href` and `rel` attributes) or `button` (displays a `<button>` element without `href` and `rel` attributes). | `button`      |

#### The `search-fetch-previous` block

The `search-fetch-previous` block renders a `Show previous` button that loads the previous search results page. See the block props in the table below.

>ℹ This block doesn't render if there is no previous page.

##### The `search-fetch-previous` prop

| Prop name              | Type   | Description                                                                                | Default value |
| ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `htmlElementForButton` | `enum` | Specifies which HTML element displays for `Show previous` button component. Possible values: `a` (displays a `<a>` element with `href` and `rel` attributes) or `button` (displays a `<button>` element without `href` and `rel` attributes). | `button`      |

#### The `search-products-count-per-page` block

The `search-products-count-per-page` block shows the product count per search page. When declared, this block doesn't need any props.

#### The `search-products-progress-bar` block

The `search-products-progress-bar` block shows a progress bar of search results. When declared, this block doesn't need any props.

#### The `sidebar-close-button` block

The `sidebar-close-button` block is the `Close` button rendered on the top right of the mobile filter sidebar.

##### The `sidebar-close-button` props

| Prop name | Type     | Description              | Default value |
| --------- | -------- | ------------------------ | ------------- |
| `size`    | `number` | Size of the button icon. | `30`          |
| `type`    | `string` | Type of the button icon. | `line`        |

## Customization

To apply CSS customization in this and other blocks, follow the instructions given in the recipe on [Using CSS Handles for store customization](https://developers.vtex.com/docs/guides/vtex-io-documentation-building-a-search-results-page-with-multiple-layouts).

| CSS handles                            |
| -------------------------------------- |
| `accordionFilter`                      |
| `accordionFilterContainer`             |
| `accordionFilterContent`               |
| `accordionFilterItemActive`            |
| `accordionFilterItemBox`               |
| `accordionFilterItemHidden`            |
| `accordionFilterItemIcon`              |
| `accordionFilterItemOptions`           |
| `accordionFilterItemTitle`             |
| `accordionFilterItem`                  |
| `accordionFilterOpen`                  |
| `accordionSelectedFilters`             |
| `border`                               |
| `breadcrumb`                           |
| `buttonShowMore`                       |
| `categoriesContainer`                  |
| `categoryGroup`                        |
| `categoryParent`                       |
| `clearAllFilters`                      |
| `container`                            |
| `dropdownMobile`                       |
| `filter`                               |
| `filterAccordionBreadcrumbs`           |
| `filterBreadcrumbsContent`             |
| `filterBreadcrumbsText`                |
| `filterBreadcrumbsItem`                |
| `filterBreadcrumbsItemName`            |
| `filterAccordionItemBox--{facetValue}` |
| `filterApplyButtonWrapper`             |
| `filterAvailable`                      |
| `filterIsOpen`                         |
| `filterButtonsBox`                     |
| `filterClearButtonWrapper`             |
| `filterContainer--{facetType}`         |
| `filterContainer--b`                   |
| `filterContainer--c`                   |
| `filterContainer--priceRange`          |
| `filterContainer--{selectedFilters}`   |
| `filterContainer--{title}`             |
| `filterContainer`                      |
| `filterIcon`                           |
| `filterItem--{facetValue}`             |
| `filterItem--selected`                 |
| `filterItem`                           |
| `filterMessage`                        |
| `filterPopup`                          |
| `filterPopupArrowIcon`                 |
| `filterPopupButton`                    |
| `filterPopupContent`                   |
| `filterPopupContentContainer`          |
| `filterPopupContentContainerOpen`      |
| `filterPopupFooter`                    |
| `filterPopupOpen`                      |
| `filterPopupTitle`                     |
| `filterSelected`                       |
| `filterSelectedFilters`                |
| `filterTotalProducts`                  |
| `filtersWrapper`                       |
| `filtersWrapperMobile`                 |
| `filterTemplateOverflow`               |
| `filterTitle`                          |
| `filterTitleSpan`                      |
| `footerButton`                         |
| `galleryItem`                          |
| `galleryItem--custom`                  |
| `galleryItem--{displayMode}`           |
| `galleryTitle`                         |
| `gallery`                              |
| `galleryLayoutSwitcher`                |
| `galleryLayoutOptionButton`            |
| `layoutSwitcher`                       |
| `loadingOverlay`                       |
| `loadingSpinnerInnerContainer`         |
| `loadingSpinnerOuterContainer`         |
| `orderByButton`                        |
| `orderByDropdown`                      |
| `orderByOptionItem`                    |
| `orderByOptionItem--selected`          |
| `orderByOptionsContainer`              |
| `orderByText`                          |
| `orderBy`                              |
| `productCount`                         |
| `progressBarContainer`                 |
| `progressBar`                          |
| `progressBarFiller`                    |
| `resultGallery`                        |
| `searchNotFoundInfo`                   |
| `searchNotFoundOops`                   |
| `searchNotFoundTerm`                   |
| `searchNotFoundTextListLine`           |
| `searchNotFoundWhatDoIDo`              |
| `searchNotFoundWhatToDoDotsContainer`  |
| `searchNotFoundWhatToDoDots`           |
| `searchNotFound`                       |
| `searchResultContainer`                |
| `seeMoreButton`                        |
| `selectedFilterItem`                   |
| `showingProductsContainer`             |
| `showingProductsCount`                 |
| `showingProducts`                      |
| `switch`                               |
| `totalProductsMessage`                 |
| `totalProducts`                        |

## Contributors ✨

Thanks goes out to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tr>
    <td align="center"><a href="https://github.com/grupo-exito-ecommerce"><img src="https://avatars2.githubusercontent.com/u/46934781?v=4?s=100" width="100px;" alt=""/><br /><sub><b>grupo-exito-ecommerce</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=grupo-exito-ecommerce" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/ygorneves10"><img src="https://avatars1.githubusercontent.com/u/39542011?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ygor Neves</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=ygorneves10" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/marcosewbank"><img src="https://avatars3.githubusercontent.com/u/27689698?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Marcos André Suarez Ewbank</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=marcosewbank" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/BeatrizMiranda"><img src="https://avatars2.githubusercontent.com/u/28959326?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Beatriz Miranda</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=BeatrizMiranda" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/felipeireslan"><img src="https://avatars3.githubusercontent.com/u/47363947?v=4?s=100" width="100px;" alt=""/><br /><sub><b>felipeireslan</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=felipeireslan" title="Code">💻</a></td>
    <td align="center"><a href="https://juliomoreira.pro"><img src="https://avatars2.githubusercontent.com/u/1207017?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Julio Moreira</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=juliomoreira" title="Code">💻</a></td>
    <td align="center"><a href="https://github.com/anto90fg"><img src="https://avatars.githubusercontent.com/u/73878310?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Antonio Cervelione</b></sub></a><br /><a href="https://github.com/vtex-apps/search-result/commits?author=anto90fg" title="Code">💻</a></td>
  </tr>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/SearchResultFlexibleMobile.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'

import SearchResultFlexible from './SearchResultFlexible'

const SearchResultFlexibleMobile = props => {
  return <SearchResultFlexible {...props} />
}

SearchResultFlexibleMobile.schema = {
  title: 'admin/editor.search-result-mobile.title',
}

export default SearchResultFlexibleMobile

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/Gallery.tsx from the search-result VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { Fragment } from 'react'
import { ProductList as ProductListStructuredData } from 'vtex.structured-data'
import { useRuntime } from 'vtex.render-runtime'

import GalleryLayout from './GalleryLayout'
import type { GalleryLayoutProps, Slots } from './GalleryLayout'
import type { GalleryProps as GalleryLegacyProps } from './GalleryLegacy'
import GalleryLegacy from './GalleryLegacy'

/*
 * This type receives Slots directly, instead of using the 'slots' prop to do it.
 * Is the equivalent of adding '[key: string]: ComponentType' at the end of GalleryLayoutProps
 * and removing the 'slots' prop.
 */
type GalleryLayoutPropsWithSlots = Omit<GalleryLayoutProps, 'slots'> & Slots

const Gallery: React.FC<
  GalleryLegacyProps | GalleryLayoutPropsWithSlots
> = props => {
  const {
    route: { routeId },
  } = useRuntime()

  if ('layouts' in props && props.layouts.length > 0) {
    const {
      layouts,
      lazyItemsRemaining,
      products,
      showingFacets,
      summary,
      preferredSKU,
      ...slots
    } = props as GalleryLayoutPropsWithSlots

    return (
      <Fragment>
        {!routeId.includes('store.search') && (
          <ProductListStructuredData products={products} />
        )}
        <GalleryLayout
          layouts={layouts}
          lazyItemsRemaining={lazyItemsRemaining}
          products={products}
          showingFacets={showingFacets}
          summary={summary}
          slots={slots}
          preferredSKU={preferredSKU}
        />
      </Fragment>
    )
  }

  return <GalleryLegacy {...(props as GalleryLegacyProps)} />
}

export interface Product {
  /** Product's id. */
  productId: string
  /** Product's cache id. */
  cacheId: string
  /** Product's name. */
  productName: string
  /** Product's description. */
  description: string
  /** Product's categories. */
  categories: unknown[]
  /** Product's link. */
  link?: string
  /** Product's link text. */
  linkText: string
  /** Product's brand. */
  brand?: string
  /** Product's SKU items. */
  items: ProductItem[]
  specification: string
}

interface ProductItemReference {
  Value: string
}

interface ProductItemImage {
  /** Images's imageUrl. */
  imageUrl: string
  /** Images's imageTag. */
  imageTag: string
}

interface ProductItemSeller {
  /** Sellers's commertialOffer. */
  commertialOffer: {
    /** CommertialOffer's price. */
    Price: number
    /** CommertialOffer's list price. */
    ListPrice: number
  }
}

interface ProductItem {
  /** SKU's id. */
  itemId: string
  /** SKU's name. */
  name: string
  /** SKU's referenceId. */
  referenceId?: ProductItemReference[]
  /** SKU's images. */
  images: ProductItemImage[]
  /** SKU's sellers. */
  sellers: ProductItemSeller[]
}

export default Gallery

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/SearchResultLayout.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import { useChildBlock, ExtensionPoint } from 'vtex.render-runtime'
import { useDevice } from 'vtex.device-detector'
import { path, compose, equals, pathOr, isEmpty, isNil } from 'ramda'
import { useAds } from '@vtex/ads-react'

import OldSearchResult from './index'
import useMergeResults from './hooks/useMergeResults'
import { removeTreePath } from './utils/removeTreePath'

const noProducts = compose(
  isEmpty,
  pathOr([], ['data', 'productSearch', 'products'])
)

const isFtOnly = compose(equals('ft'), path(['variables', 'map']))

const noRedirect = compose(isNil, path(['data', 'productSearch', 'redirect']))

const foundNothing = searchQuery => {
  const { loading } = searchQuery || {}

  return isFtOnly(searchQuery) && !loading && noProducts(searchQuery)
}

const SearchResultLayout = props => {
  const { searchQuery } = props
  const hasMobileBlock = !!useChildBlock({ id: 'search-result-layout.mobile' })
  const hasCustomNotFound = !!useChildBlock({ id: 'search-not-found-layout' })
  const { isMobile } = useDevice()

  const sponsoredSearchResult = useAds({
    placement: 'top_search',
    type: 'product',
    amount: props?.sponsoredCount ?? 3,
    term: searchQuery?.variables?.query ?? '',
    selectedFacets: searchQuery?.variables?.selectedFacets ?? [],
  })

  const newProps = useMergeResults({ props, sponsoredSearchResult })

  if (
    foundNothing(searchQuery) &&
    hasCustomNotFound &&
    noRedirect(searchQuery)
  ) {
    return (
      <ExtensionPoint
        id="search-not-found-layout"
        {...removeTreePath(newProps)}
      />
    )
  }

  if (hasMobileBlock && isMobile) {
    return (
      <ExtensionPoint
        id="search-result-layout.mobile"
        {...removeTreePath(newProps)}
      />
    )
  }

  return (
    <ExtensionPoint
      id="search-result-layout.desktop"
      {...removeTreePath(newProps)}
    />
  )
}

SearchResultLayout.getSchema = () => {
  const { description, ...schema } = OldSearchResult.getSchema({
    searchQuery: true,
  })

  return {
    ...schema,
    title: 'admin/editor.search-result-layout.title',
    properties: {
      ...schema?.properties,
      sponsoredCount: {
        type: 'number',
        title: 'Sponsored count',
        default: 3,
      },
    },
  }
}

export default SearchResultLayout

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/searchResult.css from the search-result VTEX app.

[Assistant]
Here's the implementation:

```css
.container {
  display: grid;
  display: -ms-grid;
  grid-template-columns: 230px repeat(4, auto) 306px;
  -ms-grid-columns: 230px (1fr) [4] 306px;
  grid-template-rows: auto auto auto 60px auto;
  grid-template-areas:
    'breadcrumb breadcrumb breadcrumb breadcrumb breadcrumb breadcrumb'
    'richTitle richTitle richTitle richTitle richTitle richTitle'
    'title title title title title title'
    'filters quantity . . . orderby'
    'filters gallery gallery gallery gallery gallery'
    '. pagination pagination pagination pagination pagination';
}

.container a {
  outline: 0;
}

.buttonShowMore {
  -ms-grid-row: 3;
  -ms-grid-column: 2;
  -ms-grid-column-span: 5;
  grid-column: pagination;
  grid-row: pagination;
  justify-self: end;
  align-self: center;
}
.filterTitleSpan {
  overflow: hidden;
}
.clearAllFilters {
  background: transparent;
  border: none;
  outline: none;
  cursor: pointer;
}

.switch {
  -ms-grid-row: 1;
  -ms-grid-column: 6;
  grid-column: switch;
  grid-row: switch;
}

.breadcrumb {
  -ms-grid-row: 1;
  -ms-grid-column: 1;
  -ms-grid-column-span: 3;
  grid-column: breadcrumb;
  grid-row: breadcrumb;
  align-self: center;
  justify-self: stretch;
}

.richTitle {
  grid-column: richTitle;
  grid-row: richTitle;
}

.filters {
  -ms-grid-row: 2;
  -ms-grid-column: 1;
  grid-column: filters;
  grid-row: filters;
}

.filter__container {
}

.resultGallery {
  -ms-grid-row: 2;
  -ms-grid-column: 2;
  -ms-grid-column-span: 5;
  grid-column-start: filters;
  grid-column-end: gallery;
  grid-row: gallery;
}

.filters ~ .resultGallery {
  grid-column-start: gallery;
}

.gallery {
}

.galleryTitle {
  grid-column: title;
  grid-row: title;
}

.filterPopupButton {
  background: none;
  height: 3.3rem;
  border-top: none;
  border-left: none;
  border-right: none;
}

.accordionFilter {
  color: #828282;
}

.filterAccordionItemBox {
  min-height: 3.75rem;
}

.filterAccordionBreadcrumbs {
  height: 4.25rem;
}

.filterButtonsBox {
  height: 4rem;
}

.filterPopupFooter {
  margin-top: auto;
}

.accordionFilterItemOptions {
}

.dropdownMobile {
  margin-top: -2px;
}

.accordionFilterItemActive svg path {
  fill: #4f4f4f;
}

.totalProducts {
  -ms-grid-row: 1;
  -ms-grid-column: 5;
  grid-row: quantity;
  justify-self: start;
  align-self: center;
}

.filters ~ .totalProducts {
  grid-column: quantity;
  grid-row: quantity;
}

.orderBy {
  -ms-grid-row: 1;
  -ms-grid-column: 6;
  grid-column: orderby;
  grid-row: orderby;
  align-self: center;
  justify-self: stretch;
  display: flex;
}

.filterPopupTitle {
}

.filterPopupArrowIcon {
}

.filterPopup {
}

.filterPopupOpen {
}

.filterPopupContent {
}

.filterPopupContentContainer {
}

.filterPopupContentContainerOpen {
}

.orderByOptionsContainer {
  min-width: 180px;
}

.orderByOptionItem {
  min-width: 180px;
}

@media only screen and (max-width: 64.1em) {
  .container {
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-areas:
      'filters orderby switch'
      'breadcrumb breadcrumb breadcrumb'
      'richTitle richTitle richTitle'
      'title title title'
      'quantity quantity quantity'
      'gallery gallery gallery';
  }

  .totalProducts {
    border-top-style: solid;
    border-top-width: 2px;
    justify-self: stretch;
    grid-column: quantity;
  }
}

@media only screen and (min-width: 40em) {
  .sidebar {
    width: 16rem;
  }
}

@media only screen and (max-width: 40em) {
  .container {
    -ms-grid-columns: 2fr 1.5fr 0.2fr;
    grid-template-columns: 2fr 1.5fr 0.2fr;
    grid-template-areas:
      'orderby filters switch'
      'breadcrumb breadcrumb breadcrumb'
      'richTitle richTitle richTitle'
      'quantity quantity quantity'
      'gallery gallery gallery';
    grid-template-rows: auto;
  }

  .resultGallery {
    grid-column-start: gallery;
    grid-column-end: gallery;
  }

  .filterTitle {
    font-weight: 300;
  }
}

.filterPopupOpen {
  overflow: hidden;
}

.filterQuantityBadgeDefault {
  top: -0.7rem;
  right: -0.8rem;
}

.filters--layout {
}

.buttonShowMore--layout {
}

.galleryTitle--layout {
}

.totalProducts--layout {
}

.orderBy--layout {
}

.container--layout {
}

.notFound--layout {
}

.shippingActionButton {
  border: 0;
  padding: 0;
  font-size: 14px;
  background: unset;
  cursor: pointer;
  text-align: left;
  font-weight: 500;
  color: #134CD8;
}

.scrim, .sidebar {
  z-index: 999;
}

.filterContent {
  padding-right: 2px;
}
```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/ProductsProgressBar.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import { path } from 'ramda'
import { useSearchPage } from 'vtex.search-page-context/SearchPageContext'
import { useCssHandles } from 'vtex.css-handles'

const CSS_HANDLES = ['progressBarContainer', 'progressBar', 'progressBarFiller']

const ProductsProgressBar = props => {
  const { searchQuery } = useSearchPage()
  const handles = useCssHandles(CSS_HANDLES)
  const products =
    path(['data', 'productSearch', 'products'], searchQuery) ||
    props.products ||
    []

  const recordsFiltered =
    path(['data', 'productSearch', 'recordsFiltered'], searchQuery) ||
    props.recordsFiltered ||
    0

  const productsLoadedPercentage = Math.round(
    (100 * products.length) / recordsFiltered
  )

  if (products.length === 0) {
    return null
  }

  return (
    <div className={handles.progressBarContainer}>
      <div
        className={`${handles.progressBar} relative flex bg-action-secondary center mv4 br-pill w-50 w-30-m w-25-l h1`}
      >
        <span
          className={`${handles.progressBarFiller} absolute br-pill bg-action-primary h1`}
          style={{ width: `${productsLoadedPercentage}%` }}
        />
      </div>
    </div>
  )
}

export default ProductsProgressBar

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/GalleryLegacy.tsx from the search-result VTEX app.

[Assistant]
Here's the implementation:

```tsx
import type { ComponentType } from 'react'
import React, { useMemo, useContext } from 'react'
import classNames from 'classnames'
// eslint-disable-next-line no-restricted-imports
import { splitEvery } from 'ramda'
import { useDevice } from 'vtex.device-detector'
import { ProductListContext } from 'vtex.product-list-context'
import { useResponsiveValue } from 'vtex.responsive-values'
import { useCssHandles } from 'vtex.css-handles'
import { useRuntime } from 'vtex.render-runtime'
import { Spinner } from 'vtex.styleguide'
import type { MaybeResponsiveInput } from 'vtex.responsive-values'

import withResizeDetector from './components/withResizeDetector'
import GalleryRow from './components/GalleryRow'
import ProductListEventCaller from './utils/ProductListEventCaller'
import SettingsContext from './components/SettingsContext'
import type { Product } from './Gallery'
import type { PreferredSKU } from './GalleryLayout'
import { useBreadcrumb } from './hooks/useBreadcrumb'
import { useSearchTitle } from './hooks/useSearchTitle'

/** Layout with one column */
const ONE_COLUMN_LAYOUT = 1

const LAZY_RENDER_THRESHOLD = 2

const CSS_HANDLES = ['gallery'] as const

const { ProductListProvider } = ProductListContext

export type MobileLayoutMode = 'normal' | 'small' | 'inline'

export interface GalleryProps {
  products: Product[]
  mobileLayoutMode: MobileLayoutMode
  maxItemsPerRow: MaybeResponsiveInput<number>
  minItemWidth: number
  width: number
  summary: any
  showingFacets: any
  lazyItemsRemaining: number
  customSummaryInterval?: number
  CustomSummary?: ComponentType
  /** Logic to enable which SKU will be the selected item */
  preferredSKU?: PreferredSKU
}

/**
 * Canonical gallery that displays a list of given products.
 */
const Gallery: React.FC<GalleryProps> = ({
  products = [],
  mobileLayoutMode = 'normal',
  maxItemsPerRow = {
    desktop: 5,
    tablet: 3,
    phone: 2,
  },
  minItemWidth = 240,
  width,
  summary,
  showingFacets,
  lazyItemsRemaining,
  customSummaryInterval,
  CustomSummary,
  preferredSKU,
}) => {
  const { isMobile } = useDevice()
  const { trackingId } = useContext(SettingsContext) || {}
  const handles = useCssHandles(CSS_HANDLES)
  const { getSettings } = useRuntime()
  const responsiveMaxItemsPerRow = useResponsiveValue(maxItemsPerRow)
  const breadcrumb = useBreadcrumb()
  const searchTitle = useSearchTitle(breadcrumb ?? [], { matchFt: true }).trim()

  // Not using ?? operator because trackingId and searchTitle can be ''
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  const listName = trackingId || searchTitle || 'Search result'

  const layoutMode = isMobile ? mobileLayoutMode : 'normal'

  const getItemsPerRow = () => {
    const maxItemsPerMinWidth = Math.floor(width / minItemWidth)

    if (isMobile) {
      if (layoutMode === 'normal') {
        return ONE_COLUMN_LAYOUT
      }

      const maxItemsOnMobile =
        maxItemsPerMinWidth >= 2 ? maxItemsPerMinWidth : 2

      return responsiveMaxItemsPerRow <= maxItemsOnMobile
        ? responsiveMaxItemsPerRow
        : maxItemsOnMobile
    }

    return responsiveMaxItemsPerRow <= maxItemsPerMinWidth
      ? responsiveMaxItemsPerRow
      : maxItemsPerMinWidth
  }

  const itemsPerRow = getItemsPerRow() || responsiveMaxItemsPerRow

  const rows = useMemo(
    () => splitEvery(itemsPerRow, products),
    [itemsPerRow, products]
  )

  const galleryClasses = classNames(
    handles.gallery,
    'flex flex-row flex-wrap items-stretch bn ph1 na4',
    {
      'justify-center': !showingFacets,
      'pl9-l': showingFacets,
    }
  )

  const isLazyRenderEnabled =
    getSettings('vtex.store')?.enableSearchRenderingOptimization

  return (
    <ProductListProvider listName={listName}>
      <div className={galleryClasses}>
        {rows.map((rowProducts, index) => (
          <GalleryRow
            key={index}
            products={rowProducts}
            lazyRender={isLazyRenderEnabled && index >= LAZY_RENDER_THRESHOLD}
            summary={summary}
            displayMode={layoutMode}
            itemsPerRow={itemsPerRow}
            rowIndex={index}
            listName={listName}
            customSummaryInterval={customSummaryInterval}
            CustomSummary={CustomSummary}
            preferredSKU={preferredSKU}
          />
        ))}
        {typeof lazyItemsRemaining === 'number' && lazyItemsRemaining > 0 && (
          <div
            style={{
              width: '100%',
              // Approximate number, just to add scroll leeway
              height: 300 * Math.ceil(lazyItemsRemaining / itemsPerRow),
            }}
            className="flex justify-center pt10"
          >
            <Spinner />
          </div>
        )}
      </div>
      <ProductListEventCaller />
    </ProductListProvider>
  )
}

export default withResizeDetector<GalleryProps>(Gallery)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/TotalProducts.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import { useIntl } from 'react-intl'
import React from 'react'
import PropTypes from 'prop-types'
import { useCssHandles } from 'vtex.css-handles'
import { formatIOMessage } from 'vtex.native-types'

import styles from './searchResult.css'

const CSS_HANDLES = ['totalProductsMessage']

const TotalProducts = ({
  message = 'store/search.total-products-2',
  recordsFiltered,
  wrapperClass = styles.totalProducts,
}) => {
  const handles = useCssHandles(CSS_HANDLES)
  const intl = useIntl()
  const values = {
    recordsFiltered,
    // eslint-disable-next-line react/display-name
    span: (...chunks) => (
      <span className={`${handles.totalProductsMessage} c-muted-2`}>
        {chunks}
      </span>
    ),
  }

  const totalProductsMessage = formatIOMessage({ id: message, intl }, values)

  return (
    <div
      className={`${wrapperClass} pv5 ph9 bn-ns bt-s b--muted-5 tc-s tl t-action--small`}
    >
      <span>{totalProductsMessage}</span>
    </div>
  )
}

TotalProducts.propTypes = {
  /** Total of records filtered */
  recordsFiltered: PropTypes.number.isRequired,
  wrapperClass: PropTypes.string,
  message: PropTypes.string,
}

export default TotalProducts

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/SearchQuery.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import SearchQuery from './components/SearchQuery'

export default SearchQuery

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/SearchResultFlexible.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { useMemo, useEffect, useRef } from 'react'
import {
  SearchPageContext,
  SearchPageStateContext,
  SearchPageStateDispatch,
  useSearchPageStateReducer,
} from 'vtex.search-page-context/SearchPageContext'
import { useCssHandles } from 'vtex.css-handles'
import { useRuntime } from 'vtex.render-runtime'
import { generateBlockClass } from '@vtex/css-handles'
import { pathOr, isEmpty } from 'ramda'

import SearchResultContainer from './components/SearchResultContainer'
import ContextProviders from './components/ContextProviders'
import getFilters from './utils/getFilters'
import LoadingOverlay from './components/LoadingOverlay'
import { PAGINATION_TYPE } from './constants/paginationType'
import styles from './searchResult.css'

const emptyFacets = {
  brands: [],
  priceRanges: [],
  specificationFilters: [],
  categoriesTrees: [],
}

const CSS_HANDLES = ['loadingOverlay']

const useShowContentLoader = (searchQuery, dispatch) => {
  const loadingRef = useRef(searchQuery.loading)
  const previousLoading = loadingRef.current
  const isLoading = searchQuery && searchQuery.loading

  useEffect(() => {
    if (previousLoading && !isLoading) {
      loadingRef.current = false
      dispatch({ type: 'HIDE_CONTENT_LOADER' })
    }
  }, [dispatch, isLoading, previousLoading])
}

const SearchResultFlexible = ({
  children,
  hiddenFacets,
  pagination = PAGINATION_TYPE.SHOW_MORE,
  mobileLayout = { mode1: 'normal' },
  defaultGalleryLayout,
  showProductsCount,
  blockClass,
  preventRouteChange = false,
  showFacetQuantity = false,
  showFacetTitle = false,
  showShippingFacet = false,
  availableShippingValues = [],
  // Below are set by SearchContext
  searchQuery,
  maxItemsPerPage,
  map,
  params,
  priceRange,
  orderBy,
  page,
  facetsLoading,
  trackingId,
  thresholdForFacetSearch,
  lazyItemsRemaining,
  sponsoredCount,
}) => {
  // This makes infinite scroll unavailable.
  // Infinite scroll was deprecated and we have
  // removed it since the flexible search release
  if (pagination === PAGINATION_TYPE.INFINITE_SCROLL) {
    pagination = PAGINATION_TYPE.SHOW_MORE
    console.warn('Infinite scroll cannot be used in flexible search')
  }

  pagination =
    pagination === PAGINATION_TYPE.INFINITE_SCROLL
      ? PAGINATION_TYPE.SHOW_MORE
      : pagination
  const facets = pathOr(emptyFacets, ['data', 'facets'], searchQuery)
  const {
    brands,
    brandsQuantity,
    priceRanges,
    specificationFilters,
    categoriesTrees,
    deliveries,
  } = facets

  const { query: runtimeQuery } = useRuntime()

  const filters = useMemo(
    () =>
      getFilters({
        specificationFilters,
        priceRanges,
        brands,
        brandsQuantity,
        hiddenFacets,
        deliveries,
        showShippingFacet,
        availableShippingValues,
      }),
    [
      brands,
      hiddenFacets,
      priceRanges,
      specificationFilters,
      brandsQuantity,
      deliveries,
      showShippingFacet,
      availableShippingValues,
    ]
  )

  const handles = useCssHandles(CSS_HANDLES)

  const hideFacets = !map
  const showCategories =
    hiddenFacets &&
    hiddenFacets.categories === false &&
    categoriesTrees &&
    categoriesTrees.length > 0

  const showFacets = showCategories || (!hideFacets && !isEmpty(filters))
  const [state, dispatch] = useSearchPageStateReducer({
    mobileLayout: mobileLayout.mode1,
    showContentLoader: searchQuery.loading,
    selectedGalleryLayout: runtimeQuery.layout || defaultGalleryLayout,
  })

  useShowContentLoader(searchQuery, dispatch)

  const settings = useMemo(
    () => ({
      hiddenFacets,
      pagination,
      mobileLayout,
      showFacetQuantity,
      showFacetTitle,
      trackingId,
      thresholdForFacetSearch,
      sponsoredCount,
    }),
    [
      hiddenFacets,
      mobileLayout,
      pagination,
      showFacetQuantity,
      showFacetTitle,
      trackingId,
      thresholdForFacetSearch,
      sponsoredCount,
    ]
  )

  const context = useMemo(() => {
    const selectedFacets = filters.flatMap(filter =>
      filter.facets.filter(facet => facet.selected)
    )

    return {
      hiddenFacets,
      pagination,
      mobileLayout,
      searchQuery,
      page,
      maxItemsPerPage,
      map,
      params,
      priceRange,
      orderBy,
      showFacets,
      filters,
      showProductsCount,
      preventRouteChange,
      facetsLoading,
      lazyItemsRemaining,
      selectedFacets,
      showShippingFacet,
    }
  }, [
    hiddenFacets,
    pagination,
    mobileLayout,
    searchQuery,
    page,
    maxItemsPerPage,
    map,
    params,
    priceRange,
    orderBy,
    showFacets,
    filters,
    showProductsCount,
    preventRouteChange,
    facetsLoading,
    lazyItemsRemaining,
    showShippingFacet,
  ])

  const showLoading = searchQuery.loading && !state.isFetchingMore

  const { searchId, redirect } = searchQuery?.data?.productSearch || {}
  const shouldAddAFAttr = searchId && !redirect

  return (
    <SearchPageContext.Provider value={context}>
      <SearchPageStateContext.Provider value={state}>
        <SearchPageStateDispatch.Provider value={dispatch}>
          <ContextProviders
            queryVariables={searchQuery.variables}
            settings={settings}
          >
            <SearchResultContainer
              searchQuery={searchQuery}
              maxItemsPerPage={maxItemsPerPage}
              pagination={pagination}
              mobileLayout={mobileLayout}
              map={map}
              params={params}
              priceRange={priceRange}
              hiddenFacets={hiddenFacets}
              orderBy={orderBy}
              page={page}
              facetsLoading={facetsLoading}
              lazyItemsRemaining={lazyItemsRemaining}
            >
              <LoadingOverlay loading={showLoading}>
                <div
                  data-af-element={
                    shouldAddAFAttr ? 'search-result' : undefined
                  }
                  data-af-onimpression={shouldAddAFAttr ? true : undefined}
                  data-af-search-id={shouldAddAFAttr ? searchId : undefined}
                  className={`${
                    handles.loadingOverlay
                  } w-100 flex flex-column flex-grow-1 ${generateBlockClass(
                    styles['container--layout'],
                    blockClass
                  )}`}
                >
                  {children}
                </div>
              </LoadingOverlay>
            </SearchResultContainer>
          </ContextProviders>
        </SearchPageStateDispatch.Provider>
      </SearchPageStateContext.Provider>
    </SearchPageContext.Provider>
  )
}

SearchResultFlexible.schema = {
  title: 'admin/editor.search-result-desktop.title',
}

export default SearchResultFlexible

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/ProductCountPerPage.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import { path } from 'ramda'
import { FormattedMessage, defineMessages } from 'react-intl'
import { useSearchPage } from 'vtex.search-page-context/SearchPageContext'
import { useCssHandles } from 'vtex.css-handles'

const CSS_HANDLES = ['showingProducts', 'showingProductsCount']

const showingAllProductsMessages = defineMessages({
  showingProducts: {
    id: 'store/search-result.showing-all-products',
  },
  showingProductsCount: {
    id: 'store/search-result.showing-all-products-count',
  },
})

const showingProductsMessages = defineMessages({
  showingProducts: {
    id: 'store/search-result.showing-products',
  },
  showingProductsCount: {
    id: 'store/search-result.showing-products-count',
  },
})

const ProductCountPerPage = () => {
  const { searchQuery } = useSearchPage()

  const handles = useCssHandles(CSS_HANDLES)
  const products =
    path(['data', 'productSearch', 'products'], searchQuery) || []

  const recordsFiltered = path(
    ['data', 'productSearch', 'recordsFiltered'],
    searchQuery
  )

  if (products.length === 0) {
    return null
  }

  const messages =
    products.length === recordsFiltered
      ? showingAllProductsMessages
      : showingProductsMessages

  return (
    <div
      className={`
        ${handles.showingProducts} tc t-small pt3 c-muted-2
      `}
    >
      <FormattedMessage
        id={messages.showingProducts.id}
        tagName="span"
        values={{
          value: (
            <span className={`${handles.showingProductsCount} b`}>
              <FormattedMessage
                id={messages.showingProductsCount.id}
                values={{
                  productsLoaded: products.length,
                  total: recordsFiltered,
                }}
              />
            </span>
          ),
        }}
      />
    </div>
  )
}

export default ProductCountPerPage

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/LayoutModeSwitcher.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import LayoutModeSwitcher from './components/LayoutModeSwitcher'

export default LayoutModeSwitcher

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/FilterNavigatorContext.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import FilterNavigatorContext from './components/FilterNavigatorContext'

export default FilterNavigatorContext

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/NotFoundLayout.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { useMemo } from 'react'
import { SearchPageContext } from 'vtex.search-page-context/SearchPageContext'
import { generateBlockClass } from '@vtex/css-handles'

import styles from './searchResult.css'

const NotFoundLayout = ({
  children,
  searchQuery,
  maxItemsPerPage,
  map,
  params,
  priceRange,
  orderBy,
  blockClass,
}) => {
  const context = useMemo(
    () => ({
      searchQuery,
      maxItemsPerPage,
      map,
      params,
      priceRange,
      orderBy,
    }),
    [map, maxItemsPerPage, orderBy, params, priceRange, searchQuery]
  )

  const { searchId, redirect } = searchQuery?.data?.productSearch || {}
  const shouldAddAFAttr = searchId && !redirect

  return (
    <SearchPageContext.Provider value={context}>
      <div
        className={`${generateBlockClass(
          styles['notFound--layout'],
          blockClass
        )} flex flex-column flex-grow-1`}
        data-af-element={shouldAddAFAttr ? 'search-result' : undefined}
        data-af-onimpression={shouldAddAFAttr ? true : undefined}
        data-af-search-id={shouldAddAFAttr ? searchId : undefined}
      >
        {children}
      </div>
    </SearchPageContext.Provider>
  )
}

export default NotFoundLayout

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/TotalProductsFlexible.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React from 'react'
import { useSearchPage } from 'vtex.search-page-context/SearchPageContext'
import { path } from 'ramda'

import TotalProducts from './TotalProducts'
import styles from './searchResult.css'

const withSearchPageContextProps =
  Component =>
  // eslint-disable-next-line react/display-name
  ({ message }) => {
    const { searchQuery } = useSearchPage()
    const recordsFiltered = path(
      ['data', 'productSearch', 'recordsFiltered'],
      searchQuery
    )

    return (
      <Component
        message={message}
        recordsFiltered={recordsFiltered}
        wrapperClass={styles['totalProducts--layout']}
      />
    )
  }

const TotalProductsFlexible = withSearchPageContextProps(TotalProducts)

TotalProductsFlexible.schema = {
  title: 'admin/editor.search-result.total-products',
}

export default TotalProductsFlexible

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/index.js from the search-result VTEX app.

[Assistant]
Here's the implementation:

```javascript
import React, { useMemo } from 'react'
import { useRuntime } from 'vtex.render-runtime'
import { SearchPageContext } from 'vtex.search-page-context/SearchPageContext'

import SearchResultContainer from './components/SearchResultContainer'
import { SORT_OPTIONS } from './OrderBy'
import LocalQuery from './components/LocalQuery'
import { LAYOUT_MODE } from './components/LayoutModeSwitcher'
import ContextProviders from './components/ContextProviders'
import { PAGINATION_TYPES } from './constants/paginationType'

const DEFAULT_MAX_ITEMS_PER_PAGE = 10

/**
 * Search Result Query Loader Component.
 * This Component queries the search if the search-result doesn't receive it already
 */
const trimStartingSlash = value => value && value.replace(/^\//, '')

const SearchResult = props => {
  const {
    querySchema,
    hiddenFacets,
    pagination,
    mobileLayout,
    searchQuery,
    trackingId,
  } = props

  const { query } = useRuntime()
  const settings = useMemo(
    () => ({
      hiddenFacets,
      pagination,
      mobileLayout,
      trackingId,
    }),
    [hiddenFacets, mobileLayout, pagination, trackingId]
  )

  const fieldsFromQueryString = {
    mapField: query.map,
    queryField: trimStartingSlash(query.query),
  }

  const areFieldsFromQueryStringValid = !!(
    fieldsFromQueryString.mapField && fieldsFromQueryString.queryField
  )

  return !searchQuery || (querySchema && querySchema.enableCustomQuery) ? (
    <LocalQuery
      {...props}
      {...querySchema}
      {...(areFieldsFromQueryStringValid ? fieldsFromQueryString : {})}
      render={localQueryProps => (
        <SearchPageContext.Provider
          value={{ searchQuery: localQueryProps.searchQuery }}
        >
          <ContextProviders
            queryVariables={localQueryProps.searchQuery.variables}
            settings={settings}
          >
            <SearchResultContainer {...localQueryProps} />
          </ContextProviders>
        </SearchPageContext.Provider>
      )}
    />
  ) : (
    <SearchPageContext.Provider value={{ searchQuery }}>
      <ContextProviders
        queryVariables={searchQuery.variables}
        settings={settings}
      >
        <SearchResultContainer {...props} />
      </ContextProviders>
    </SearchPageContext.Provider>
  )
}

SearchResult.defaultProps = {
  orderBy: SORT_OPTIONS[0].value,
}

SearchResult.getSchema = props => {
  const querySchema = !props.searchQuery
    ? {
        querySchema: {
          title: 'admin/editor.search-result.query',
          description: 'admin/editor.search-result.query.description',
          type: 'object',
          properties: {
            maxItemsPerPage: {
              title: 'admin/editor.search-result.query.maxItemsPerPage',
              type: 'number',
              default: DEFAULT_MAX_ITEMS_PER_PAGE,
            },
            queryField: {
              title: 'Query',
              type: 'string',
            },
            mapField: {
              title: 'Map',
              type: 'string',
            },
            priceRangeField: {
              title: 'PriceRange',
              type: 'string',
              description: 'e.g., "10 TO 233"',
            },
            orderByField: {
              title: 'Order by field',
              type: 'string',
              default: '',
              enum: SORT_OPTIONS.map(opt => opt.value),
              enumNames: SORT_OPTIONS.map(opt => opt.label),
            },
            hideUnavailableItems: {
              title: 'admin/editor.search-result.query.hideUnavailableItems',
              type: 'boolean',
              default: false,
            },
            skusFilter: {
              title: 'admin/editor.search-result.query.skusFilter',
              description:
                'admin/editor.search-result.query.skusFilter.description',
              type: 'string',
              default: 'ALL_AVAILABLE',
              enum: ['ALL_AVAILABLE', 'ALL', 'FIRST_AVAILABLE'],
              enumNames: [
                'admin/editor.search-result.query.skusFilter.all-available',
                'admin/editor.search-result.query.skusFilter.none',
                'admin/editor.search-result.query.skusFilter.first-available',
              ],
            },
            simulationBehavior: {
              title: 'admin/editor.search-result.query.simulationBehavior',
              description:
                'admin/editor.search-result.query.simulationBehavior.description',
              type: 'string',
              default: 'default',
              enum: ['default', 'skip'],
              enumNames: [
                'admin/editor.search-result.query.simulationBehavior.default',
                'admin/editor.search-result.query.simulationBehavior.skip',
              ],
            },
            installmentCriteria: {
              title:
                'admin/editor.search-result.query.installmentCriteria.title',
              description:
                'admin/editor.search-result.query.installmentCriteria.description',
              type: 'string',
              default: 'MAX_WITHOUT_INTEREST',
              enum: ['MAX_WITHOUT_INTEREST', 'MAX_WITH_INTEREST'],
              enumNames: [
                'admin/editor.search-result.query.installmentCriteria.max-without-interest',
                'admin/editor.search-result.query.installmentCriteria.max-with-interest',
              ],
            },
          },
        },
      }
    : {}

  return {
    title: 'admin/editor.search-result.title',
    description: 'admin/editor.search-result.description',
    type: 'object',
    dependencies: {
      advancedSettings: {
        oneOf: [
          {
            properties: {
              advancedSettings: {
                enum: [true],
              },
              trackingId: {
                title:
                  'admin/editor.search-result.advanced-settings.trackingId.title',
                description:
                  'admin/editor.search-result.advanced-settings.trackingId.description',
                type: 'string',
              },
            },
          },
        ],
      },
    },
    properties: {
      ...querySchema,
      mobileLayout: {
        title: 'admin/editor.search-result.mobileLayout',
        type: 'object',
        isLayout: true,
        properties: {
          mode1: {
            title: 'admin/editor.search-result.mobileLayout.mode1',
            type: 'string',
            default: LAYOUT_MODE[0].value,
            enum: LAYOUT_MODE.map(opt => opt.value),
            enumNames: LAYOUT_MODE.map(opt => opt.label),
            isLayout: true,
          },
          mode2: {
            title: 'admin/editor.search-result.mobileLayout.mode2',
            type: 'string',
            default: LAYOUT_MODE[1].value,
            enum: LAYOUT_MODE.map(opt => opt.value),
            enumNames: LAYOUT_MODE.map(opt => opt.label),
            isLayout: true,
          },
        },
      },
      hiddenFacets: {
        title: 'admin/editor.search-result.hiddenFacets',
        type: 'object',
        isLayout: true,
        properties: {
          brands: {
            title: 'admin/editor.search-result.hiddenFacets.brands',
            type: 'boolean',
            isLayout: true,
          },
          categories: {
            title: 'admin/editor.search-result.hiddenFacets.categories',
            type: 'boolean',
            isLayout: true,
          },
          priceRange: {
            title: 'admin/editor.search-result.hiddenFacets.priceRange',
            type: 'boolean',
            isLayout: true,
          },
          specificationFilters: {
            title:
              'admin/editor.search-result.hiddenFacets.specificationFilters',
            type: 'object',
            isLayout: true,
            properties: {
              hideAll: {
                title:
                  'admin/editor.search-result.hiddenFacets.specificationFilters.hideAll',
                type: 'boolean',
                isLayout: true,
              },
              hiddenFilters: {
                type: 'array',
                isLayout: true,
                items: {
                  title:
                    'admin/editor.search-result.hiddenFacets.specificationFilters.hiddenFilter',
                  type: 'object',
                  isLayout: true,
                  properties: {
                    name: {
                      title:
                        'admin/editor.search-result.hiddenFacets.specificationFilters.hiddenFilter.name',
                      type: 'string',
                      isLayout: true,
                    },
                  },
                },
              },
            },
          },
        },
      },
      pagination: {
        type: 'string',
        title: 'admin/editor.search-result.pagination.title',
        default: 'infinite-scroll',
        enum: PAGINATION_TYPES,
        enumNames: [
          'admin/editor.search-result.pagination.show-more',
          'admin/editor.search-result.pagination.infinite-scroll',
        ],
        isLayout: true,
      },
      advancedSettings: {
        title: 'admin/editor.search-result.advanced-settings.title',
        type: 'boolean',
      },
    },
  }
}

export default SearchResult

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of search-result/react/GalleryLayout.tsx from the search-result VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { useContext, useEffect, useMemo } from 'react'
import type { ComponentType } from 'react'
import classNames from 'classnames'
import { ProductListContext } from 'vtex.product-list-context'
import { Spinner } from 'vtex.styleguide'
import { useCssHandles, applyModifiers } from 'vtex.css-handles'
import { useResponsiveValue } from 'vtex.responsive-values'
import type { MaybeResponsiveInput } from 'vtex.responsive-values'
import { useRuntime } from 'vtex.render-runtime'
import { SearchPageContext } from 'vtex.search-page-context'

import GalleryLayoutRow from './components/GalleryLayoutRow'
import SettingsContext from './components/SettingsContext'
import ProductListEventCaller from './utils/ProductListEventCaller'
import type { Product } from './Gallery'
import {
  SET_GALLERY_LAYOUTS_TYPE,
  SWITCH_GALLERY_LAYOUT_TYPE,
} from './constants'
import { useBreadcrumb } from './hooks/useBreadcrumb'
import { useSearchTitle } from './hooks/useSearchTitle'

const LAZY_RENDER_THRESHOLD = 2

const CSS_HANDLES = ['gallery'] as const

const { ProductListProvider } = ProductListContext
const { useSearchPageState, useSearchPageStateDispatch } = SearchPageContext

interface LayoutOption {
  name: string
  component: string
  itemsPerRow: MaybeResponsiveInput<number>
}

export type Slots = Record<string, ComponentType>

export interface GalleryLayoutProps {
  layouts: LayoutOption[]
  lazyItemsRemaining: number
  products: Product[]
  showingFacets: boolean
  summary: unknown
  /** Logic to enable which SKU will be the selected item */
  preferredSKU?: PreferredSKU
  slots: Slots
}

export type PreferredSKU =
  | 'FIRST_AVAILABLE'
  | 'LAST_AVAILABLE'
  | 'PRICE_ASC'
  | 'PRICE_DESC'

const GalleryLayout: React.FC<GalleryLayoutProps> = ({
  layouts,
  lazyItemsRemaining,
  products,
  showingFacets,
  summary,
  preferredSKU,
  slots,
}) => {
  const { trackingId } = useContext(SettingsContext) || {}
  const handles = useCssHandles(CSS_HANDLES)
  const { getSettings } = useRuntime()
  const { selectedGalleryLayout } = useSearchPageState()
  const searchPageStateDispatch = useSearchPageStateDispatch()

  const breadcrumb = useBreadcrumb()
  const searchTitle = useSearchTitle(breadcrumb ?? [], { matchFt: true }).trim()

  // Not using ?? operator because trackingId and searchTitle can be ''
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
  const listName = trackingId || searchTitle || 'Search result'

  useEffect(() => {
    searchPageStateDispatch({
      type: SET_GALLERY_LAYOUTS_TYPE,
      args: { galleryLayouts: layouts },
    })
  }, [layouts, searchPageStateDispatch])

  const currentLayoutOption = useMemo(() => {
    let layoutOption

    if (selectedGalleryLayout) {
      layoutOption = layouts.find(
        layout => layout.name === selectedGalleryLayout
      )
    } else {
      console.error(
        'No default gallery layout defined. Set it in search-result-layout by using the defaultGalleryLayout prop.'
      )
    }

    if (!layoutOption) {
      layoutOption = layouts[0]

      searchPageStateDispatch({
        type: SWITCH_GALLERY_LAYOUT_TYPE,
        args: {
          selectedGalleryLayout: layouts[0].name,
          focus: false,
        },
      })
    }

    return layoutOption
  }, [selectedGalleryLayout, layouts, searchPageStateDispatch])

  const itemsPerRow = useResponsiveValue(currentLayoutOption.itemsPerRow)

  const galleryRows = useMemo(() => {
    const rows = []

    let i = 0

    while (i * itemsPerRow < products.length) {
      const start = i * itemsPerRow
      const end = (i + 1) * itemsPerRow

      rows.push(products.slice(start, end))
      i++
    }

    return rows
  }, [itemsPerRow, products])

  if (!layouts || layouts.length === 0) {
    return null
  }

  if (!slots[currentLayoutOption.component]) {
    console.error(
      `Define a Slot with name "${currentLayoutOption.component}" for the layout "${currentLayoutOption.name}".`
    )

    return null
  }

  const galleryClasses = classNames(
    applyModifiers(handles.gallery, currentLayoutOption.name),
    'flex flex-row flex-wrap items-stretch bn ph1 na4',
    {
      'justify-center': !showingFacets,
      'pl9-l': showingFacets,
    }
  )

  const isLazyRenderEnabled =
    getSettings('vtex.store')?.enableSearchRenderingOptimization

  return (
    <ProductListProvider listName={listName as string}>
      <div id="gallery-layout-container" className={galleryClasses}>
        {galleryRows.map((rowProducts, index) => (
          <GalleryLayoutRow
            key={`${currentLayoutOption.name}-${index}`}
            products={rowProducts}
            lazyRender={!!isLazyRenderEnabled && index >= LAZY_RENDER_THRESHOLD}
            summary={summary}
            displayMode="normal"
            itemsPerRow={itemsPerRow}
            currentLayoutName={currentLayoutOption.name}
            rowIndex={index}
            listName={listName}
            preferredSKU={preferredSKU}
            GalleryItemComponent={slots[currentLayoutOption.component]}
          />
        ))}
        {typeof lazyItemsRemaining === 'number' && lazyItemsRemaining > 0 && (
          <div
            style={{
              width: '100%',
              // Approximate number, just to add scroll leeway
              height: 300 * Math.ceil(lazyItemsRemaining / itemsPerRow),
            }}
            className="flex justify-center pt10"
          >
            <Spinner />
          </div>
        )}
      </div>
      <ProductListEventCaller />
    </ProductListProvider>
  )
}

export default GalleryLayout

```