From 1eab9dfe737f6856b718d8b128155b9e2b05dad7 Mon Sep 17 00:00:00 2001 From: Christian Mutti <34144667+chrsmutti@users.noreply.github.com> Date: Fri, 27 Dec 2019 11:01:17 -0300 Subject: [PATCH] Add second query with catalog integration (#15) * add autcomplete component * remove font-awesome and add append history function * add Attribute as a component * remove hard coded color * use FormattedMessage instead of formatMessage function * add ordinal number to intl * Add DidYouMean, Banner and Suggestions * update changelog an remove dependencies * Added SearchQuery and changed biggy-to-vtex conversion into SearchQuery component * PropTypes and convertOrderBy * Fix query and searchResult path * Added search-graphql client * Remove store from all queries * Catalog product in autocomplete * Added unstableProductOrigin count * Fix suggestionSearches * Added cacheControl * Minor fix to compatibility-layer and add suggestions, banners and corrections to searchQuery * Minor fix to show show-more Co-authored-by: Hiago Lucas Cardeal --- CHANGELOG.md | 4 + docs/README.md | 7 +- graphql/schema.graphql | 311 +-------------- graphql/types/Benefits.graphql | 24 ++ graphql/types/Biggy.graphql | 273 +++++++++++++ graphql/types/Brand.graphql | 34 ++ graphql/types/Category.graphql | 20 + graphql/types/Facets.graphql | 72 ++++ graphql/types/ItemMetadata.graphql | 75 ++++ graphql/types/PageType.graphql | 12 + graphql/types/Product.graphql | 363 ++++++++++++++++++ graphql/types/ProductSearch.graphql | 17 + graphql/types/Suggestions.graphql | 14 + manifest.json | 3 + messages/context.json | 2 +- node/clients.ts | 84 ---- node/clients/biggy-search.ts | 119 ++++++ node/clients/index.ts | 14 + node/clients/search-graphql.ts | 127 ++++++ node/commons/compatibility-layer.ts | 91 +++++ node/commons/inputs.ts | 8 - node/index.ts | 16 +- node/resolvers/autocomplete.ts | 30 +- node/resolvers/extra-info.ts | 13 + node/resolvers/products.ts | 41 ++ node/resolvers/search.ts | 28 +- react/SearchContext.js | 216 +++-------- react/SearchWrapper.js | 47 +-- react/api/api.js | 10 - react/api/log.js | 19 - .../components/TileList/TileList.tsx | 5 +- react/components/Autocomplete/index.tsx | 39 +- react/components/SearchQuery.js | 127 ++++++ react/{ => components}/useRedirect.js | 0 react/graphql/productsById.gql | 48 --- react/graphql/searchResult.gql | 189 +++++---- react/graphql/suggestionProducts.gql | 153 +++++--- react/graphql/suggestionSearches.gql | 4 +- react/graphql/topSearches.gql | 4 +- react/index.ts | 2 - react/utils/biggy-client.ts | 19 +- react/utils/compatibility-layer.ts | 159 ++++++++ react/withAccount.js | 10 - 43 files changed, 1953 insertions(+), 900 deletions(-) create mode 100644 graphql/types/Benefits.graphql create mode 100644 graphql/types/Biggy.graphql create mode 100644 graphql/types/Brand.graphql create mode 100644 graphql/types/Category.graphql create mode 100644 graphql/types/Facets.graphql create mode 100644 graphql/types/ItemMetadata.graphql create mode 100644 graphql/types/PageType.graphql create mode 100644 graphql/types/Product.graphql create mode 100644 graphql/types/ProductSearch.graphql create mode 100644 graphql/types/Suggestions.graphql delete mode 100644 node/clients.ts create mode 100644 node/clients/biggy-search.ts create mode 100644 node/clients/index.ts create mode 100644 node/clients/search-graphql.ts create mode 100644 node/commons/compatibility-layer.ts create mode 100644 node/resolvers/extra-info.ts create mode 100644 node/resolvers/products.ts delete mode 100644 react/api/api.js delete mode 100644 react/api/log.js create mode 100644 react/components/SearchQuery.js rename react/{ => components}/useRedirect.js (100%) delete mode 100644 react/graphql/productsById.gql create mode 100644 react/utils/compatibility-layer.ts delete mode 100644 react/withAccount.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c153bf..c7cddf66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- `__unstableProductOrigin` prop into generic app. + ## [0.3.6] - 2019-12-26 ### Added diff --git a/docs/README.md b/docs/README.md index caee1370..e9abcb83 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ a more complete search experience. - [Autocomplete](#autocomplete) - [Order Options](#order-options) - [Enhanced Search Result](#enhanced-search-result) + - [Plug & Play](#plug--play) - [Catalog Integration](#catalog-integration) - [Blocks API](#blocks-api) - [Configuration](#configuration) @@ -124,9 +125,9 @@ passing them to the `hiddenOptions` prop on the `order-by` component. This app has three new components to improve the search result experience. They are: -- [`did-you-mean`](DidYouMean.md). A possible misspelling correction for the current query. -- [`search-suggestion`](Suggestions.md). A list of search terms similar to the query. -- [`search-banner`](Banner.md). A banner that can be configured by query. +- [`did-you-mean`](DidYouMean.md). A possible misspelling correction for the current query. +- [`search-suggestion`](Suggestions.md). A list of search terms similar to the query. +- [`search-banner`](Banner.md). A banner that can be configured by query. To add these components to your search-result page, you need to use the `search-result-layout.desktop.enhanced` and `search-result-layout.mobile.enhanced` instead of `search-result-layout.desktop` and `search-result-layout.mobile`. Here is an implementation example: diff --git a/graphql/schema.graphql b/graphql/schema.graphql index d826edbc..1014c86b 100755 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -1,45 +1,39 @@ type Query { - getConfig: GetConfigResponse @cacheControl(scope: PUBLIC, maxAge: MEDIUM) - """ Get top searches for a given store. - - Using paidNavigation filter you'll be able to filter out words that shouldn't be shown - when the user comes from a paid navigation (e.g. google ads, bing ads). """ - topSearches(store: String!, paidNavigation: Boolean): TopSearchesOutput + topSearches: TopSearchesOutput @cacheControl(scope: PUBLIC, maxAge: SHORT) """ Get suggestion searches for a given search term. """ - suggestionSearches(store: String!, term: String!): SuggestionSearchesOutput + suggestionSearches(term: String!): SuggestionSearchesOutput + @cacheControl(scope: PUBLIC, maxAge: SHORT) """ Get suggestion products for a given search term. """ suggestionProducts( - store: String! term: String! attributeKey: String attributeValue: String - ): SuggestionProductsOutput + ): SuggestionProductsOutput @cacheControl(scope: PUBLIC, maxAge: SHORT) """ Get attribute keys for a store. """ - attributeKeys(store: String!): [AttributeResponseKey]! + attributeKeys: [AttributeResponseKey]! + @cacheControl(scope: PUBLIC, maxAge: SHORT) """ Get attribute values for a given attribute. """ attributeValues( - store: String! attribute: String! filters: [ResultRequestAttributeText] - ): [AttributeResponseValue]! + ): [AttributeResponseValue]! @cacheControl(scope: PUBLIC, maxAge: SHORT) searchResult( - store: String! attributePath: String query: String page: Int @@ -48,211 +42,12 @@ type Query { operator: String fuzzy: Int leap: Boolean - ): ResultResponse -} - -type GetConfigResponse { - apiKey: String - store: String -} - -""" -Attribute response key. -""" -type AttributeResponseKey { - key: String! - label: String! - - """ - Attribute type, could be either 'text' or 'number'. - """ - type: String - visible: Boolean - values: [AttributeResponseValue] - minValue: Float - maxValue: Float - active: Boolean - activeFrom: String - activeTo: String - templateUrl: String - proxyUrl: String -} - -""" -Attribute response value. -""" -type AttributeResponseValue { - count: Int - active: Boolean - proxyUrl: String - - """ - Only for text attributes. - """ - key: String - - """ - Only for text attributes. - """ - label: String - - """ - Only for number attributes. - """ - from: String - - """ - Only for number attributes. - """ - to: String -} - -enum CacheControlScope { - PUBLIC - PRIVATE -} - -""" -A Search product, contains all the info about the product. Used in SearchResult. -""" -type ElasticProduct { - name: String - id: String - product: String - url: String - oldPrice: Float - price: Float - oldPriceText: String - priceText: String - brand: String - stock: Float - customSort: Float - availableSpecsCount: Int - rating: ElasticProductRating - installment: ElasticProductInstallment - categories: [String] - textAttributes: [ElasticProductText] - numberAttributes: [ElasticProductNumber] - skus: [ElasticProductSku] - images: [ElasticProductImage] - stickers: [ElasticProductSticker] - extraInfo: [ExtraInfo] -} - -""" -A product's seller. -""" -type ElasticProductSeller { - id: String! - oldPrice: Float - price: Float - stock: Float - installment: ElasticProductInstallment -} - -""" -A product's image. -""" -type ElasticProductImage { - name: String - value: String! -} - -""" -A product's installment information. -""" -type ElasticProductInstallment { - count: Int! - value: Float! - interest: Boolean! - valueText: String -} - -""" -A key, value interface containing a label version for the key that is suited for UI, the value is a Number. -""" -type ElasticProductNumber { - key: String - value: Float - labelKey: String -} - -""" -A product's rating information. -""" -type ElasticProductRating { - value: Float - count: Int -} - -type ElasticProductSku { - id: String - name: String - icon: String - image: String - url: String - stock: Float - oldPrice: Float - price: Float - oldPriceText: String - priceText: String - attributes: [ElasticProductSkuAttribute] - sellers: [ElasticProductSeller] -} - -type ElasticProductSkuAttribute { - key: String - value: String -} - -""" -A product's sticker information. -""" -type ElasticProductSticker { - location: String - name: String - image: String - target: String -} - -""" -A key, value interface containing a label version for both that is better suited for UI. -""" -type ElasticProductText { - key: String! - value: String! - labelKey: String! - labelValue: String! -} - -""" -Extra information provided for a product. -""" -type ExtraInfo { - key: String! - value: String! -} - -type PhraseSuggestion { - text: String - highlighted: String - misspelled: Boolean - correction: Boolean + ): ResultResponse @cacheControl(scope: PUBLIC, maxAge: SHORT) } -type ElasticBanner { - id: String - name: String - area: String - html: String -} - -""" -Result Request Attribute Text. -""" -input ResultRequestAttributeText { - name: String! - value: String! +enum Origin { + BIGGY + VTEX } """ @@ -262,7 +57,7 @@ type ResultResponse { query: String operator: String total: Int - products: [ElasticProduct] + products(origin: Origin = BIGGY): [Product] pagination: ResultResponsePagination options: SearchOptions attributes: [AttributeResponseKey] @@ -272,90 +67,10 @@ type ResultResponse { redirect: String } -type ResultResponsePagination { - count: Int - current: ResultResponsePaginationItem - before: [ResultResponsePaginationItem] - after: [ResultResponsePaginationItem] - next: ResultResponsePaginationItem - previous: ResultResponsePaginationItem - first: ResultResponsePaginationItem - last: ResultResponsePaginationItem -} - -type ResultResponsePaginationItem { - index: Int - proxyUrl: String -} - -type SearchOptions { - sorts: [SearchSort] - counts: [SearchOptionsCount] -} - -type SearchOptionsCount { - count: Int - active: Boolean - proxyUrl: String -} - -""" -A Search product, contains all the info about the product. Used in Autocomplete. -""" -type SearchProduct { - id: String! - name: String! - url: String! - images: [ElasticProductImage] - oldPrice: Float - price: Float - oldPriceText: String - priceText: String - installment: ElasticProductInstallment - attributes: [ElasticProductText] - extraInfo: [ExtraInfo] - brand: String - categories: [String] - product: String - skus: [ElasticProductSku] -} - -type SearchSort { - field: String - order: String - active: Boolean - proxyUrl: String -} - """ Result of a SuggestionProducts query, a list of suggestion products. """ type SuggestionProductsOutput { count: Int! - products: [SearchProduct]! -} - -""" -Suggestion query object, with the term, how many times it was searched and attributes for it. -""" -type SuggestionQueryResponseSearch { - term: String! - count: Int! - attributes: [ElasticProductText] -} - -""" -Result of a SuggestionSearches query, a list of suggestion searches. -""" -type SuggestionSearchesOutput { - searches: [SuggestionQueryResponseSearch]! -} - -""" -Result of a TopSearches query, a list of the top searches at a given time. - -The amount of queries returned is determined by the Store's search settings. -""" -type TopSearchesOutput { - searches: [SuggestionQueryResponseSearch]! + products(origin: Origin = BIGGY): [Product]! } diff --git a/graphql/types/Benefits.graphql b/graphql/types/Benefits.graphql new file mode 100644 index 00000000..0ed035ae --- /dev/null +++ b/graphql/types/Benefits.graphql @@ -0,0 +1,24 @@ +""" Benefit of a Product """ +type Benefit { + """ Flag which indicates if the benefit is featured or not """ + featured: Boolean + """ Id of the product which the benefit is associated """ + id: String + """ Name of the benefit """ + name: String + """ Items of the benefit """ + items: [BenefitItem] + """ Type of benefit """ + teaserType: String +} + +type BenefitItem { + """ Product itself """ + benefitProduct: Product + """ IDs of the SKU Items that are taking part in the benefit """ + benefitSKUIds: [String] + """ Discount applied to the benefit product """ + discount: Float + """ Minimum quantity of the benefit product that is required to validate the benefit """ + minQuantity: Int +} \ No newline at end of file diff --git a/graphql/types/Biggy.graphql b/graphql/types/Biggy.graphql new file mode 100644 index 00000000..767899cb --- /dev/null +++ b/graphql/types/Biggy.graphql @@ -0,0 +1,273 @@ +""" +Attribute response key. +""" +type AttributeResponseKey { + key: String! + label: String! + + """ + Attribute type, could be either 'text' or 'number'. + """ + type: String + visible: Boolean + values: [AttributeResponseValue] + minValue: Float + maxValue: Float + active: Boolean + activeFrom: String + activeTo: String + templateUrl: String + proxyUrl: String +} + +""" +Attribute response value. +""" +type AttributeResponseValue { + count: Int + active: Boolean + proxyUrl: String + + """ + Only for text attributes. + """ + key: String + + """ + Only for text attributes. + """ + label: String + + """ + Only for number attributes. + """ + from: String + + """ + Only for number attributes. + """ + to: String +} + +""" +A Search product, contains all the info about the product. Used in SearchResult. +""" +type ElasticProduct { + name: String + id: String + product: String + url: String + oldPrice: Float + price: Float + oldPriceText: String + priceText: String + brand: String + stock: Float + customSort: Float + availableSpecsCount: Int + rating: ElasticProductRating + installment: ElasticProductInstallment + categories: [String] + textAttributes: [ElasticProductText] + numberAttributes: [ElasticProductNumber] + skus: [ElasticProductSku] + images: [ElasticProductImage] + stickers: [ElasticProductSticker] + extraInfo: [ExtraInfo] +} + +""" +A product's seller. +""" +type ElasticProductSeller { + id: String! + oldPrice: Float + price: Float + stock: Float + installment: ElasticProductInstallment +} + +""" +A product's image. +""" +type ElasticProductImage { + name: String + value: String! +} + +""" +A product's installment information. +""" +type ElasticProductInstallment { + count: Int! + value: Float! + interest: Boolean! + valueText: String +} + +""" +A key, value interface containing a label version for the key that is suited for UI, the value is a Number. +""" +type ElasticProductNumber { + key: String + value: Float + labelKey: String +} + +""" +A product's rating information. +""" +type ElasticProductRating { + value: Float + count: Int +} + +type ElasticProductSku { + id: String + name: String + icon: String + image: String + url: String + stock: Float + oldPrice: Float + price: Float + oldPriceText: String + priceText: String + attributes: [ElasticProductSkuAttribute] + sellers: [ElasticProductSeller] +} + +type ElasticProductSkuAttribute { + key: String + value: String +} + +""" +A product's sticker information. +""" +type ElasticProductSticker { + location: String + name: String + image: String + target: String +} + +""" +A key, value interface containing a label version for both that is better suited for UI. +""" +type ElasticProductText { + key: String! + value: String! + labelKey: String! + labelValue: String! +} + +""" +Extra information provided for a product. +""" +type ExtraInfo { + key: String! + value: String! +} + +type PhraseSuggestion { + text: String + highlighted: String + misspelled: Boolean + correction: Boolean +} + +type ElasticBanner { + id: String + name: String + area: String + html: String +} + +""" +Result Request Attribute Text. +""" +input ResultRequestAttributeText { + name: String! + value: String! +} + +type ResultResponsePagination { + count: Int + current: ResultResponsePaginationItem + before: [ResultResponsePaginationItem] + after: [ResultResponsePaginationItem] + next: ResultResponsePaginationItem + previous: ResultResponsePaginationItem + first: ResultResponsePaginationItem + last: ResultResponsePaginationItem +} + +type ResultResponsePaginationItem { + index: Int + proxyUrl: String +} + +type SearchOptions { + sorts: [SearchSort] + counts: [SearchOptionsCount] +} + +type SearchOptionsCount { + count: Int + active: Boolean + proxyUrl: String +} + +""" +A Search product, contains all the info about the product. Used in Autocomplete. +""" +type SearchProduct { + id: String! + name: String! + url: String! + images: [ElasticProductImage] + oldPrice: Float + price: Float + oldPriceText: String + priceText: String + installment: ElasticProductInstallment + attributes: [ElasticProductText] + extraInfo: [ExtraInfo] + brand: String + categories: [String] + product: String + skus: [ElasticProductSku] +} + +type SearchSort { + field: String + order: String + active: Boolean + proxyUrl: String +} + +""" +Suggestion query object, with the term, how many times it was searched and attributes for it. +""" +type SuggestionQueryResponseSearch { + term: String! + count: Int! + attributes: [ElasticProductText] +} + +""" +Result of a SuggestionSearches query, a list of suggestion searches. +""" +type SuggestionSearchesOutput { + searches: [SuggestionQueryResponseSearch]! +} + +""" +Result of a TopSearches query, a list of the top searches at a given time. + +The amount of queries returned is determined by the Store's search settings. +""" +type TopSearchesOutput { + searches: [SuggestionQueryResponseSearch]! +} diff --git a/graphql/types/Brand.graphql b/graphql/types/Brand.graphql new file mode 100644 index 00000000..2a8da6e8 --- /dev/null +++ b/graphql/types/Brand.graphql @@ -0,0 +1,34 @@ +type Brand { + """ + slug is used as cacheId + """ + cacheId: ID + """ + Brand id + """ + id: Int + """ + Brand logo + """ + imageUrl: String @toVtexAssets + """ + Text link + """ + slug: String + """ + Name of brand + """ + name: String @translatableV2 + """ + Title used by html tag + """ + titleTag: String @translatableV2 + """ + Description used by html tag + """ + metaTagDescription: String @translatableV2 + """ + Brand is active + """ + active: Boolean +} diff --git a/graphql/types/Category.graphql b/graphql/types/Category.graphql new file mode 100644 index 00000000..b7e4d811 --- /dev/null +++ b/graphql/types/Category.graphql @@ -0,0 +1,20 @@ +type Category { + """ id is used as cacheId """ + cacheId: ID + """ URI of category """ + href: String + """ Category text link """ + slug: String + """ Category ID """ + id: Int + """ Category name """ + name: String @translatableV2 + """ Title used by html tag""" + titleTag: String @translatableV2 + """ Description used by html tag""" + hasChildren: Boolean + """ Has children categories """ + metaTagDescription: String @translatableV2 + """ Categories children """ + children: [Category] +} diff --git a/graphql/types/Facets.graphql b/graphql/types/Facets.graphql new file mode 100644 index 00000000..cd2617a9 --- /dev/null +++ b/graphql/types/Facets.graphql @@ -0,0 +1,72 @@ +type Facets { + departments: [DepartmentFacet] + brands: [BrandFacet] + specificationFilters: [FilterFacets] + categoriesTrees: [CategoriesTreeFacet] + priceRanges: [PriceRangesFacet] + recordsFiltered: Int +} + +type DepartmentFacet { + id: ID! + quantity: Int! + name: String @translatableV2 + link: String! + linkEncoded: String! + map: String + value: String! + selected: Boolean! +} + +type BrandFacet { + id: ID! + quantity: Int! + name: String! + link: String! + linkEncoded: String! + map: String + value: String! + selected: Boolean! +} + +type PriceRangesFacet { + quantity: Int! + name: String @translatableV2 + link: String! + linkEncoded: String! + map: String + value: String! + selected: Boolean! + slug: String +} + +type CategoriesTreeFacet { + id: ID! + quantity: Int! + name: String @translatableV2 + link: String! + linkEncoded: String! + """ + Contains slugified links according to the store structure. /:department/d, /:category/:subcategory, etc + """ + href: String! + map: String + value: String! + children: [CategoriesTreeFacet] + selected: Boolean! +} + +type FilterFacet { + quantity: Int! + name: String @translatableV2 + link: String! + linkEncoded: String! + map: String + value: String! + selected: Boolean! +} + +type FilterFacets { + name: String + facets: [FilterFacet] +} diff --git a/graphql/types/ItemMetadata.graphql b/graphql/types/ItemMetadata.graphql new file mode 100644 index 00000000..e70e9b45 --- /dev/null +++ b/graphql/types/ItemMetadata.graphql @@ -0,0 +1,75 @@ +type ItemMetadata { + items: [ItemMetadataUnit] + priceTable: [ItemPriceTable] +} + +type ItemPriceTable { + type: String + values: [PriceTableItem] +} + +type PriceTableItem { + id: String + assemblyId: String + price: Int +} + +type ItemMetadataUnit { + id: ID + name: String + skuName: String + productId: String + refId: String + ean: String + imageUrl: String @toVtexAssets + detailUrl: String + seller: String + assemblyOptions: [AssemblyOption] +} + +type AssemblyOption { + id: ID + name: String + required: Boolean + composition: CompositionType + inputValues: [InputValue] +} + +scalar StringOrBoolean + +type InputValue { + label: String + maxLength: Int + type: InputValueType + defaultValue: StringOrBoolean + domain: [String] +} + +enum InputValueType { + TEXT + BOOLEAN + OPTIONS +} + +type CompositionType { + minQuantity: Int + maxQuantity: Int + items: [CompositionItem] +} + +type CompositionItem { + id: ID + minQuantity: Int + maxQuantity: Int + initialQuantity: Int + priceTable: String + seller: String +} + +input AssemblyOptionInput { + id: ID! + quantity: Int! + assemblyId: String! + seller: String! + options: [AssemblyOptionInput!] +} diff --git a/graphql/types/PageType.graphql b/graphql/types/PageType.graphql new file mode 100644 index 00000000..b38ffb1b --- /dev/null +++ b/graphql/types/PageType.graphql @@ -0,0 +1,12 @@ +type PageType { + id: String + type: PageEntityIdentifier +} + +enum PageEntityIdentifier { + brand + department + category + subcategory + search +} diff --git a/graphql/types/Product.graphql b/graphql/types/Product.graphql new file mode 100644 index 00000000..df0bc601 --- /dev/null +++ b/graphql/types/Product.graphql @@ -0,0 +1,363 @@ +type Product { + """ + Brand of the product + """ + brand: String @translatableV2 + """ + Id of the brand of the product + """ + brandId: Int + """ + linkText is used as cacheId + """ + cacheId: ID + categoryId: ID + """ + Categories of the product + """ + categories: [String] + @deprecated( + reason: "Use 'categoryTree' field for internationalization support" + ) + """ + Product's categories + """ + categoryTree: [Category] + """ + List of related products + """ + clusterHighlights: [ClusterHighlight] + productClusters: [ProductClusters] + """ + Product description + """ + description: String @translatableV2 + """ + SKU objects of the product + """ + items(filter: ItemsFilter): [SKU] + """ + Product URL + """ + link: String + """ + Product slug + """ + linkText: String + """ + Product ID + """ + productId: ID + """ + Product name + """ + productName: String @translatableV2 + """ + Array of product properties + """ + properties: [Property] + """ + Array of product properties + """ + propertyGroups: [PropertyGroup] + """ + Product reference + """ + productReference: String + """ + Title used by html tag + """ + titleTag: String @translatableV2 + """ + Description used by html tag + """ + metaTagDescription: String @translatableV2 + """ + Related Products + """ + recommendations: Recommendation + """ + JSON specification of the product + """ + jsonSpecifications: String + """ + List of benefits associated with this product + """ + benefits: [Benefit] + itemMetadata: ItemMetadata + """ + Array of product SpecificationGroup + """ + specificationGroups: [SpecificationGroup] + + """ + Returns highest and lowest prices for available SKUs in product. + """ + priceRange: ProductPriceRange +} + +enum ItemsFilter { + """ + Returns all items, same as no filter. + """ + ALL + """ + Returns only the first available item. Returns first if no item is available. + """ + FIRST_AVAILABLE + """ + Returns all available items. Returns first if no item is available. + """ + ALL_AVAILABLE +} + +type ProductPriceRange { + sellingPrice: PriceRange + listPrice: PriceRange +} + +type PriceRange { + highPrice: Float + lowPrice: Float +} + +type OnlyProduct { + brand: String + categoryId: ID + categoryTree: [Category] + clusterHighlights: [ClusterHighlight] + productClusters: [ProductClusters] + description: String + link: String + linkText: String + productId: ID + productName: String + properties: [Property] + propertyGroups: [PropertyGroup] + productReference: String + recommendations: Recommendation + jsonSpecifications: String +} + +type ProductClusters { + id: ID + name: String +} + +type ClusterHighlight { + id: ID + name: String +} + +type Seller { + sellerId: ID + sellerName: String + addToCartLink: String + sellerDefault: Boolean + commertialOffer: Offer +} + +type Recommendation { + buy: [Product] + view: [Product] + similars: [Product] +} + +type SKU { + itemId: ID + name: String @translatableV2 + nameComplete: String @translatableV2 + complementName: String + ean: String + referenceId: [Reference] + measurementUnit: String + unitMultiplier: Float + kitItems: [KitItem] + images(quantity: Int = 10): [Image] + videos: [Video] + sellers: [Seller] + variations: [Property] + attachments: [Attachment] @deprecated(reason: "Use itemMetaData instead") +} + +type skuSpecification { + fieldName: String @translatableV2 + fieldValues: [String] @translatableV2 +} + +type productSpecification { + fieldName: String @translatableV2 + fieldValues: [String] @translatableV2 +} + +input ProductUniqueIdentifier { + field: ProductUniqueIdentifierField! + value: ID! +} + +enum ProductUniqueIdentifierField { + id + slug + ean + reference + sku +} + +type KitItem { + itemId: ID + amount: Int + product: OnlyProduct + sku: SKU +} + +type Attachment { + id: ID + name: String + required: Boolean + domainValues: [DomainValues] +} + +type DomainValues { + FieldName: String + MaxCaracters: String + DomainValues: String +} + +enum InstallmentsCriteria { + MAX + MIN + ALL +} + +type Offer { + Installments( + criteria: InstallmentsCriteria = ALL + rates: Boolean = true + ): [Installment] + Price: Float + ListPrice: Float + PriceWithoutDiscount: Float + RewardValue: Float + PriceValidUntil: String + AvailableQuantity: Float + Tax: Float + CacheVersionUsedToCallCheckout: String + DeliverySlaSamples: [DeliverySlaSamples] + """ + List of discount highlights + """ + discountHighlights: [Discount!] + teasers: [Teaser!] + """ + List of gifts associated with the product + """ + giftSkuIds: [String] +} + +type Teaser { + name: String + conditions: TeaserCondition + effects: TeaserEffects +} + +type TeaserCondition { + minimumQuantity: Int + parameters: [TeaserValue] +} + +type TeaserEffects { + parameters: [TeaserValue] +} + +type TeaserValue { + name: String + value: String +} + +""" +Discount object +""" +type Discount { + """ + Discount name + """ + name: String +} + +type DeliverySlaSamples { + DeliverySlaPerTypes: [DeliverySlaPerTypes] + Region: Region +} + +type DeliverySlaPerTypes { + TypeName: String + Price: Float + EstimatedTimeSpanToDelivery: String +} + +type Region { + IsPersisted: Boolean + IsRemoved: Boolean + Id: ID + Name: String + CountryCode: String + ZipCode: String + CultureInfoName: String +} + +type Image { + cacheId: ID + imageId: ID + imageLabel: String + imageTag: String + imageUrl: String @toVtexAssets + imageText: String +} + +type Video { + videoUrl: String +} + +type SpecificationGroup { + name: String @translatableV2 + specifications: [SpecificationGroupProperty] +} + +type SpecificationGroupProperty { + name: String @translatableV2 + values: [String] @translatableV2 +} + +type Property { + name: String + values: [String] +} + +type PropertyGroup { + name: String + properties: [String] +} + +type Installment { + Value: Float + InterestRate: Float + TotalValuePlusInterestRate: Float + NumberOfInstallments: Int + PaymentSystemName: String + PaymentSystemGroupName: String + Name: String +} + +type Reference { + Key: String + Value: String +} + +enum CrossSelingInputEnum { + buy + similars + view + viewAndBought + accessories + suggestions +} diff --git a/graphql/types/ProductSearch.graphql b/graphql/types/ProductSearch.graphql new file mode 100644 index 00000000..5453f42d --- /dev/null +++ b/graphql/types/ProductSearch.graphql @@ -0,0 +1,17 @@ +type ProductSearch { + products: [Product] + recordsFiltered: Int + titleTag: String + metaTagDescription: String + breadcrumb: [SearchBreadcrumb] +} + +type SearchBreadcrumb { + name: String @translatableV2 + href: String +} + +type SearchMetadata { + titleTag: String + metaTagDescription: String +} diff --git a/graphql/types/Suggestions.graphql b/graphql/types/Suggestions.graphql new file mode 100644 index 00000000..05ce5b64 --- /dev/null +++ b/graphql/types/Suggestions.graphql @@ -0,0 +1,14 @@ +type Suggestions { + """ searchTerm from Query autocomplete is used as cacheId """ + cacheId: ID + itemsReturned: [Items] +} + +type Items { + thumb: String + name: String @translatableV2 + href: String + criteria: String + slug: String + productId: String +} diff --git a/manifest.json b/manifest.json index f9ef99bc..98bd1610 100644 --- a/manifest.json +++ b/manifest.json @@ -41,6 +41,9 @@ { "name": "vbase-read-write" }, + { + "name": "vtex.search-graphql:resolve-graphql" + }, { "name": "outbound-access", "attrs": { diff --git a/messages/context.json b/messages/context.json index 923b8f18..6fb4d24f 100644 --- a/messages/context.json +++ b/messages/context.json @@ -8,4 +8,4 @@ "store/ordinalNumber": "ordinalNumber", "store/didYouMean": "didYouMean", "store/searchSuggestions": "searchSuggestions" -} \ No newline at end of file +} diff --git a/node/clients.ts b/node/clients.ts deleted file mode 100644 index 43c749d5..00000000 --- a/node/clients.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - ExternalClient, - InstanceOptions, - IOClients, - IOContext, -} from "@vtex/api"; -import { - SearchResultInput, - SuggestionProductsInput, - SuggestionSearchesInput, - TopSearchesInput, -} from "./commons/inputs"; - -// Extend the default IOClients implementation with our own custom clients. -export class Clients extends IOClients { - public get biggySearch() { - return this.getOrSet("biggySearch", BiggySearchClient); - } -} - -export class BiggySearchClient extends ExternalClient { - constructor(context: IOContext, options?: InstanceOptions) { - super("http://search.biggylabs.com.br/search-api/v1/", context, options); - } - - public async topSearches({ store }: TopSearchesInput): Promise { - return this.http.get(`${store}/api/top_searches`, { - metric: "top-searches", - }); - } - - public async suggestionSearches({ - store, - term, - }: SuggestionSearchesInput): Promise { - return this.http.get(`${store}/api/suggestion_searches`, { - params: { - term, - }, - metric: "suggestion-searches", - }); - } - - public async suggestionProducts({ - store, - term, - attributeKey, - attributeValue, - }: SuggestionProductsInput): Promise { - return this.http.get(`${store}/api/suggestion_products`, { - params: { - term, - key: attributeKey, - value: attributeValue, - }, - metric: "suggestion-products", - }); - } - - public async searchResult({ - store, - attributePath, - query, - page, - count, - sort, - operator, - fuzzy, - leap, - }: SearchResultInput): Promise { - return this.http.get(`${store}/api/search/${attributePath || ""}`, { - params: { - query, - page, - count, - sort, - operator, - fuzzy, - bgy_leap: leap ? true : undefined, - }, - metric: "search-result", - }); - } -} diff --git a/node/clients/biggy-search.ts b/node/clients/biggy-search.ts new file mode 100644 index 00000000..a0b682c0 --- /dev/null +++ b/node/clients/biggy-search.ts @@ -0,0 +1,119 @@ +import { path } from "ramda"; +import { ExternalClient, InstanceOptions, IOContext } from "@vtex/api"; +import { + SuggestionSearchesInput, + SuggestionProductsInput, + SearchResultInput, +} from "../commons/inputs"; + +export class BiggySearchClient extends ExternalClient { + private store: string; + + constructor(context: IOContext, options?: InstanceOptions) { + super("http://search.biggylabs.com.br/search-api/v1/", context, options); + + const { account } = context; + this.store = account; + } + + public async topSearches(): Promise { + try { + const result = await this.http.get( + `${this.store}/api/top_searches`, + { + metric: "top-searches", + }, + ); + + return result || { searches: [] }; + } catch (err) { + // TODO: Add logging + return { searches: [] }; + } + } + + public async suggestionSearches({ + term, + }: SuggestionSearchesInput): Promise { + try { + const result = await this.http.get( + `${this.store}/api/suggestion_searches`, + { + params: { + term, + }, + metric: "suggestion-searches", + }, + ); + + return result || { searches: [] }; + } catch (err) { + // TODO: Add logging + return { searches: [] }; + } + } + + public async suggestionProducts({ + term, + attributeKey, + attributeValue, + }: SuggestionProductsInput): Promise { + try { + const result = await this.http.get( + `${this.store}/api/suggestion_products`, + { + params: { + term, + key: attributeKey, + value: attributeValue, + }, + metric: "suggestion-products", + }, + ); + + return result || { count: 0, products: [] }; + } catch (err) { + // TODO: Add logging + return { count: 0, products: [] }; + } + } + + public async searchResult({ + attributePath, + query, + page, + count, + sort, + operator, + fuzzy, + leap, + }: SearchResultInput): Promise { + try { + const result = await this.http.get( + `${this.store}/api/search/${attributePath || ""}`, + { + params: { + query, + page, + count, + sort, + operator, + fuzzy, + bgy_leap: leap ? true : undefined, + }, + metric: "search-result", + }, + ); + + return result || { products: [] }; + } catch (err) { + if (path(["response", "status"], err) === 302) { + const redirect = path(["response", "headers", "location"], err); + return { redirect, products: [] }; + } + + // TODO: Add logging + return { products: [] }; + } + } +} diff --git a/node/clients/index.ts b/node/clients/index.ts new file mode 100644 index 00000000..e293ceb2 --- /dev/null +++ b/node/clients/index.ts @@ -0,0 +1,14 @@ +import { IOClients } from "@vtex/api"; +import { BiggySearchClient } from "./biggy-search"; +import { SearchGraphQL } from "./search-graphql"; + +// Extend the default IOClients implementation with our own custom clients. +export class Clients extends IOClients { + public get biggySearch() { + return this.getOrSet("biggySearch", BiggySearchClient); + } + + public get searchGraphQL() { + return this.getOrSet("searchGraphQL", SearchGraphQL); + } +} diff --git a/node/clients/search-graphql.ts b/node/clients/search-graphql.ts new file mode 100644 index 00000000..d9f23353 --- /dev/null +++ b/node/clients/search-graphql.ts @@ -0,0 +1,127 @@ +import { AppGraphQLClient, IOContext, InstanceOptions } from "@vtex/api"; +import { pathOr } from "ramda"; + +export class SearchGraphQL extends AppGraphQLClient { + constructor(context: IOContext, options?: InstanceOptions) { + super("vtex.search-graphql", context, options); + } + + public productsById = async (ids: string[]) => { + try { + const result = await this.graphql.query({ + inflight: true, + variables: { ids }, + query: PRODUCTS_BY_ID_QUERY, + }); + + return pathOr([], ["data", "productsByIdentifier"], result); + } catch (err) { + return []; + } + }; +} + +const PRODUCTS_BY_ID_QUERY = ` + query ProductsByReference($ids: [ID!]) { + productsByIdentifier(field: id, values: $ids) { + cacheId + productId + description + productName + productReference + linkText + brand + brandId + link + categories + priceRange { + sellingPrice { + highPrice + lowPrice + } + listPrice { + highPrice + lowPrice + } + } + specificationGroups { + name + specifications { + name + values + } + } + items(filter: ALL_AVAILABLE) { + itemId + name + nameComplete + complementName + ean + variations { + name + values + } + referenceId { + Key + Value + } + measurementUnit + unitMultiplier + images { + cacheId + imageId + imageLabel + imageTag + imageUrl + imageText + } + sellers { + sellerId + sellerName + commertialOffer { + discountHighlights { + name + } + teasers { + name + conditions { + minimumQuantity + parameters { + name + value + } + } + effects { + parameters { + name + value + } + } + } + Installments(criteria: MAX) { + Value + InterestRate + TotalValuePlusInterestRate + NumberOfInstallments + Name + } + Price + ListPrice + PriceWithoutDiscount + RewardValue + PriceValidUntil + AvailableQuantity + } + } + } + productClusters { + id + name + } + properties { + name + values + } + } + } +`; diff --git a/node/commons/compatibility-layer.ts b/node/commons/compatibility-layer.ts new file mode 100644 index 00000000..37278d30 --- /dev/null +++ b/node/commons/compatibility-layer.ts @@ -0,0 +1,91 @@ +import { propOr } from "ramda"; + +export const convertBiggyProduct = (product: any) => { + const categories: string[] = product.categories + ? product.categories.map((_: any, index: number) => { + const subArray = product.categories.slice(0, index); + return `/${subArray.join("/")}/`; + }) + : []; + + const skus = propOr([], "skus", product).map( + convertSKU(product), + ); + + return { + categories, + cacheId: product.name.replace(" ", "-"), + productId: product.product || product.id, + productName: product.name, + productReference: product.product || product.id, + linkText: slugifyUrl(product.url), + brand: + product.brand || + product.extraInfo["marca"] || + product.extraInfo["brand"] || + "", + link: product.url, + description: product.url, + items: skus, + sku: skus.find(sku => sku.sellers && sku.sellers.length > 0), + }; +}; + +const slugifyUrl = (url: string) => { + const parts = url.replace(/\/p$/, "").split("/"); + return parts[parts.length - 1]; +}; + +const convertSKU = (product: any) => (sku: any) => { + const image = { + cacheId: product.product || product.id, + imageId: product.product || product.id, + imageLabel: "principal", + imageUrl: product.images[0].value, + imageText: "principal", + }; + + const sellers = propOr([], "sellers", sku).map( + (seller: any) => { + const price = seller.price || sku.price || product.price; + const oldPrice = seller.oldPrice || sku.oldPrice || product.oldPrice; + const installment = + seller.installment || sku.installment || product.installment; + + return { + sellerId: seller.id, + sellerName: "", + commertialOffer: { + AvailableQuantity: 10000, + discountHighlights: [], + teasers: [], + Installments: installment + ? [ + { + Value: installment.value, + InterestRate: 0, + TotalValuePlusInterestRate: price, + NumberOfInstallments: installment.count, + Name: "", + }, + ] + : null, + Price: price, + ListPrice: oldPrice, + PriceWithoutDiscount: price, + }, + }; + }, + ); + + return { + sellers, + image, + seller: sellers[0], + itemId: sku.id, + name: product.name, + nameComplete: product.name, + complementName: product.name, + images: [image], + }; +}; diff --git a/node/commons/inputs.ts b/node/commons/inputs.ts index e1f2b93a..6d7399c8 100644 --- a/node/commons/inputs.ts +++ b/node/commons/inputs.ts @@ -1,22 +1,14 @@ -export interface TopSearchesInput { - store: string; - paidNavigation?: boolean; -} - export interface SuggestionSearchesInput { - store: string; term: string; } export interface SuggestionProductsInput { - store: string; term: string; attributeKey?: string; attributeValue?: string; } export interface SearchResultInput { - store: string; attributePath: string; query: string; page: number; diff --git a/node/index.ts b/node/index.ts index c84af4ca..9ad87699 100755 --- a/node/index.ts +++ b/node/index.ts @@ -1,7 +1,11 @@ import { ClientsConfig, Service, IOContext } from "@vtex/api"; -import { Clients, BiggySearchClient } from "./clients"; +import { Clients } from "./clients"; import { autocomplete } from "./resolvers/autocomplete"; import { search } from "./resolvers/search"; +import { extraInfo } from "./resolvers/extra-info"; +import { products } from "./resolvers/products"; +import { BiggySearchClient } from "./clients/biggy-search"; +import { SearchGraphQL } from "./clients/search-graphql"; const FIFTEEN_SECOND_MS = 15 * 1000; @@ -23,6 +27,15 @@ export default new Service({ ...autocomplete, ...search, }, + ResultResponse: { + ...products, + }, + SuggestionProductsOutput: { + ...products, + }, + SearchProduct: { + ...extraInfo, + }, }, }, }); @@ -31,5 +44,6 @@ export interface IContext { vtex: IOContext; clients: { biggySearch: BiggySearchClient; + searchGraphQL: SearchGraphQL; }; } diff --git a/node/resolvers/autocomplete.ts b/node/resolvers/autocomplete.ts index 3b99d0f8..e8087b95 100644 --- a/node/resolvers/autocomplete.ts +++ b/node/resolvers/autocomplete.ts @@ -1,18 +1,14 @@ import { SuggestionProductsInput, SuggestionSearchesInput, - TopSearchesInput, } from "../commons/inputs"; import { IContext } from ".."; export const autocomplete = { - topSearches: async (_: any, args: TopSearchesInput, ctx: IContext) => { + topSearches: async (_: any, __: any, ctx: IContext) => { const { biggySearch } = ctx.clients; - const result = (await biggySearch.topSearches(args)) || {}; - result.searches = result.searches || []; - - return result; + return await biggySearch.topSearches(); }, suggestionSearches: async ( @@ -22,10 +18,7 @@ export const autocomplete = { ) => { const { biggySearch } = ctx.clients; - const result = (await biggySearch.suggestionSearches(args)) || {}; - result.searches = result.searches || []; - - return result; + return await biggySearch.suggestionSearches(args); }, suggestionProducts: async ( @@ -35,21 +28,6 @@ export const autocomplete = { ) => { const { biggySearch } = ctx.clients; - const result = (await biggySearch.suggestionProducts(args)) || { count: 0 }; - result.products = result.products || []; - - result.products.forEach((product: any) => { - const mapInfo = product.extraInfo || {}; - const extraInfo: { key: string; value: string }[] = []; - - // Transform ExtraInfo from Map to Array. - for (const key of Object.keys(mapInfo)) { - extraInfo.push({ key, value: mapInfo[key] }); - } - - product.extraInfo = extraInfo; - }); - - return result; + return await biggySearch.suggestionProducts(args); }, }; diff --git a/node/resolvers/extra-info.ts b/node/resolvers/extra-info.ts new file mode 100644 index 00000000..49a1d313 --- /dev/null +++ b/node/resolvers/extra-info.ts @@ -0,0 +1,13 @@ +export const extraInfo = { + extraInfo: (product: any) => { + const mapInfo = product.extraInfo || {}; + const extraInfo: { key: string; value: string }[] = []; + + // Transform ExtraInfo from Map to Array. + for (const key of Object.keys(mapInfo)) { + extraInfo.push({ key, value: mapInfo[key] }); + } + + return extraInfo; + }, +}; diff --git a/node/resolvers/products.ts b/node/resolvers/products.ts new file mode 100644 index 00000000..750592ef --- /dev/null +++ b/node/resolvers/products.ts @@ -0,0 +1,41 @@ +import { convertBiggyProduct } from "../commons/compatibility-layer"; +import { map, prop, isEmpty, sort, indexOf } from "ramda"; +import { IContext } from ".."; + +enum Origin { + BIGGY, + VTEX, +} + +export const products = { + products: async ( + searchResult: any, + { origin }: { origin: Origin }, + ctx: IContext, + ) => { + if (origin === Origin.BIGGY) { + return searchResult.products.map(convertBiggyProduct); + } + + const { searchGraphQL } = ctx.clients; + + let products: any[] = searchResult.products; + const productIds = map((product: any) => { + return prop("product", product) || prop("id", product) || ""; + }, products); + + if (!isEmpty(productIds)) { + // Get products' model from VTEX search API + products = await searchGraphQL.productsById(productIds); + + // Maintain biggySearch's order. + products = sort( + (a, b) => + indexOf(a.productId, productIds) - indexOf(b.productId, productIds), + products, + ); + } + + return products; + }, +}; diff --git a/node/resolvers/search.ts b/node/resolvers/search.ts index 55a65380..c05a8893 100644 --- a/node/resolvers/search.ts +++ b/node/resolvers/search.ts @@ -1,36 +1,10 @@ -import { path } from "ramda"; import { SearchResultInput } from "../commons/inputs"; import { IContext } from ".."; export const search = { searchResult: async (_: any, args: SearchResultInput, ctx: IContext) => { const { biggySearch } = ctx.clients; - - let result: any = {}; - try { - result = (await biggySearch.searchResult(args)) || {}; - } catch (err) { - if (path(["response", "status"], err) === 302) { - const redirect = path(["response", "headers", "location"], err); - return { redirect, products: [] }; - } - - throw err; - } - - result.products = result.products || []; - - result.products.forEach((product: any) => { - const mapInfo = product.extraInfo || {}; - const extraInfo: { key: string; value: string }[] = []; - - // Transform ExtraInfo from Map to Array. - for (const key of Object.keys(mapInfo)) { - extraInfo.push({ key, value: mapInfo[key] }); - } - - product.extraInfo = extraInfo; - }); + const result = await biggySearch.searchResult(args); return result; }, diff --git a/react/SearchContext.js b/react/SearchContext.js index 538969a7..792834be 100644 --- a/react/SearchContext.js +++ b/react/SearchContext.js @@ -1,185 +1,89 @@ import React, { useMemo } from "react"; -import { Query } from "react-apollo"; -import { useRuntime } from "vtex.render-runtime"; -import { onSearchResult, SearchClickPixel } from "vtex.sae-analytics"; -import searchResultQuery from "./graphql/searchResult.gql"; -import { vtexOrderToBiggyOrder } from "./utils/vtex-utils"; -import VtexSearchResult from "./models/vtex-search-result"; -import logError from "./api/log"; -import useRedirect from "./useRedirect"; -import BiggyClient from "./utils/biggy-client"; +import PropTypes from "prop-types"; +import { prop } from "ramda"; +import { SearchClickPixel } from "vtex.sae-analytics"; -const saveTermInHistory = term => { - new BiggyClient().prependSearchHistory(term); -}; - -const getUrlByAttributePath = ( - attributePath, - map, - priceRange, - priceRangeKey, -) => { - const facets = attributePath ? attributePath.split("/") : []; - const apiUrlTerms = map - ? map - .split(",") - .slice(1) - .map((item, index) => `${item}/${facets[index].replace(/^z/, "")}`) - : []; - - const url = apiUrlTerms.join("/"); - - if (priceRange && priceRangeKey) { - const [from, to] = priceRange.split(" TO "); - return `${url}/${priceRangeKey}/${from}:${to}`; - } - - return url; -}; +import { + convertOrderBy, + convertURLToAttributePath, +} from "./utils/compatibility-layer.ts"; +import useRedirect from "./components/useRedirect"; +import SearchQuery from "./components/SearchQuery"; const SearchContext = props => { - const { account, workspace, route } = useRuntime(); const { setRedirect } = useRedirect(); const { + children, + priceRangeKey, + maxItemsPerPage, + __unstableProductOrigin: productOrigin, params: { path: attributePath }, query: { _query, map, order, operator, fuzzy, priceRange, bgy_leap: leap }, } = props; const url = useMemo( () => - getUrlByAttributePath( - attributePath, - map, - priceRange, - props.priceRangeKey, - ), + convertURLToAttributePath(attributePath, map, priceRange, priceRangeKey), [attributePath, map, priceRange], ); - const initialVariables = { + const variables = { operator, fuzzy, + productOrigin, query: _query, page: 1, - store: account, attributePath: url, - sort: vtexOrderToBiggyOrder(order), - count: props.maxItemsPerPage, + sort: convertOrderBy(order), + count: maxItemsPerPage, leap: !!leap, }; - const onFetchMoreFunction = fetchMore => ({ variables, updateQuery }) => { - const { to } = variables; - const page = parseInt(to / props.maxItemsPerPage, 10) + 1; - - return ( - fetchMore({ - variables: { ...variables, page }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult || page === 1) return prev; - - return { - ...fetchMoreResult, - searchResult: { - ...fetchMoreResult.searchResult, - products: [ - ...prev.searchResult.products, - ...fetchMoreResult.searchResult.products, - ], - }, - }; - }, - }) - /* If the object from updateQuery is not returned, search-result gets an infinite loading. - * A PR to search-result project is required - */ - .then(() => - updateQuery( - { productSearch: { products: [] } }, - { - fetchMoreResult: { - productSearch: { products: [] }, - }, - }, - ), - ) - ); - }; - - try { - if (!_query) throw new Error("Empty search is not allowed"); - - return ( - { - saveTermInHistory(initialVariables.query); - onSearchResult(data); - }} - > - {({ loading, error, data, fetchMore }) => { - if (error) { - logError(account, workspace, route.path, error); - } - - if (data && data.searchResult && data.searchResult.redirect) { - setRedirect(data.searchResult.redirect); - } - - const vtexSearchResult = - error || !data - ? VtexSearchResult.emptySearch() - : new VtexSearchResult( - _query, - 1, - props.maxItemsPerPage, - order, - attributePath, - map, - priceRange, - onFetchMoreFunction(fetchMore), - data, - loading, - !!props.priceRangeKey, - ); - - return ( - <> - - - {React.cloneElement(props.children, { - searchResult: - error || !data - ? { - query: props.params.query, - } - : data.searchResult, - ...props, - ...vtexSearchResult, - })} - - ); - }} - - ); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - - logError(account, workspace, route.path, error); + if (!_query) throw new Error("Empty search is not allowed"); + + return ( + + {result => { + const redirect = prop("redirect", result); + + if (redirect) { + setRedirect(redirect); + } + + return ( + <> + + + {React.cloneElement(children, { + ...result, + })} + + ); + }} + + ); +}; - const vtexSearchResult = VtexSearchResult.emptySearch(); +SearchContext.propTypes = { + children: PropTypes.arrayOf(PropTypes.node).isRequired, + priceRangeKey: PropTypes.string, + maxItemsPerPage: PropTypes.number, + params: PropTypes.shape({ path: PropTypes.string.isRequired }).isRequired, + // eslint-disable-next-line react/forbid-prop-types + query: PropTypes.any.isRequired, + __unstableProductOrigin: PropTypes.oneOf(["BIGGY", "VTEX"]), +}; - return React.cloneElement(props.children, { - searchResult: { - query: _query, - }, - ...props, - ...vtexSearchResult, - }); - } +SearchContext.defaultProps = { + priceRangeKey: undefined, + maxItemsPerPage: 20, + __unstableProductOrigin: "BIGGY", }; export default SearchContext; diff --git a/react/SearchWrapper.js b/react/SearchWrapper.js index 859e314c..be0a13f2 100644 --- a/react/SearchWrapper.js +++ b/react/SearchWrapper.js @@ -1,35 +1,38 @@ import React from "react"; import { Helmet, useRuntime } from "vtex.render-runtime"; import PropTypes from "prop-types"; +import { isEmpty, reject, prop } from "ramda"; -const SearchWrapper = props => { - const { getSettings } = useRuntime(); - const { storeName, metaTagKeywords } = getSettings("vtex.store") || {}; +const getCanonicalHost = () => + // eslint-disable-next-line no-underscore-dangle + window.__hostname__ || prop("hostname", window.location); - const { searchResult } = props; +const SearchWrapper = ({ children, searchResult: { query } }) => { + const { route, getSettings } = useRuntime(); + const { storeName, metaTagKeywords } = getSettings("vtex.store") || {}; - const title = - searchResult && searchResult.query - ? `${searchResult.query} - ${storeName}` - : storeName; + const title = reject(isEmpty, [query, storeName]).join(" - "); - const { children } = props; return ( <> {children} @@ -37,13 +40,13 @@ const SearchWrapper = props => { }; SearchWrapper.propTypes = { - searchResult: PropTypes.shape({ - query: PropTypes.string.isRequired, - }).isRequired, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node, ]).isRequired, + searchResult: PropTypes.shape({ + query: PropTypes.string, + }).isRequired, }; export default SearchWrapper; diff --git a/react/api/api.js b/react/api/api.js deleted file mode 100644 index 53368ad0..00000000 --- a/react/api/api.js +++ /dev/null @@ -1,10 +0,0 @@ -import axios from "axios"; - -const axionsInstance = axios.create({ - baseURL: "https://search.biggylabs.com.br/search-api/v1", - headers: { - "Content-Type": "application/json", - }, -}); - -export default axionsInstance; diff --git a/react/api/log.js b/react/api/log.js deleted file mode 100644 index 0070fd9c..00000000 --- a/react/api/log.js +++ /dev/null @@ -1,19 +0,0 @@ -import axionsInstance from "./api"; - -const logError = (storeSlug, workspace, attributePath, error) => { - const browser = - typeof navigator !== "undefined" - ? navigator.userAgent - : "server-side-error"; - - const message = `Workspace: ${workspace}\nBrowser: ${browser}\nMessage: ${ - error.message - }\n${error.stack != null ? error.stack : ""}`; - - axionsInstance.post(`/${storeSlug}/log`, { - message, - url: attributePath, - }); -}; - -export default logError; diff --git a/react/components/Autocomplete/components/TileList/TileList.tsx b/react/components/Autocomplete/components/TileList/TileList.tsx index fcdd21c9..9ba5725b 100644 --- a/react/components/Autocomplete/components/TileList/TileList.tsx +++ b/react/components/Autocomplete/components/TileList/TileList.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { ExtensionPoint } from "vtex.render-runtime"; import styles from "./styles"; -import { Product } from "../../../../models/product"; import { CustomListItem } from "../CustomListItem/CustomListItem"; import { ProductLayout } from "../.."; import { Spinner } from "vtex.styleguide"; @@ -11,7 +10,7 @@ import { FormattedMessage } from "react-intl"; interface TileListProps { term: string; title: string | JSX.Element; - products: Product[]; + products: any[]; showTitle: boolean; shelfProductCount: number; totalProducts: number; @@ -54,7 +53,7 @@ export class TileList extends React.Component { > {this.props.products.map(product => { const productSummary = ProductSummary.mapCatalogProductToProductSummary( - product.toSummary(), + product, ); return ( diff --git a/react/components/Autocomplete/index.tsx b/react/components/Autocomplete/index.tsx index b9708377..26353c60 100644 --- a/react/components/Autocomplete/index.tsx +++ b/react/components/Autocomplete/index.tsx @@ -8,9 +8,7 @@ import { } from "./components/ItemList/types"; import { TileList } from "./components/TileList/TileList"; import stylesCss from "./styles.css"; -import { withRuntime } from "../../utils/withRuntime"; import BiggyClient from "../../utils/biggy-client"; -import { Product } from "../../models/product"; import { FormattedMessage } from "react-intl"; import { IconClose, IconClock } from "vtex.styleguide"; import { withDevice } from "vtex.device-detector"; @@ -38,13 +36,14 @@ interface AutoCompleteProps { hideTitles: boolean; historyFirst: boolean; isMobile: boolean; + __unstableProductOrigin: "BIGGY" | "VTEX"; } interface AutoCompleteState { topSearchedItems: Item[]; suggestionItems: Item[]; history: Item[]; - products: Product[]; + products: any[]; totalProducts: number; queryFromHover: { key?: string; value?: string }; dynamicTerm: string; @@ -130,10 +129,7 @@ class AutoComplete extends React.Component< } async updateSuggestions() { - const result = await this.client.suggestionSearches( - this.props.runtime.account, - this.props.inputValue, - ); + const result = await this.client.suggestionSearches(this.props.inputValue); const { searches } = result.data.suggestionSearches; const { maxSuggestedTerms = MAX_SUGGESTED_TERMS_DEFAULT } = this.props; @@ -168,6 +164,7 @@ class AutoComplete extends React.Component< async updateProducts() { const term = this.state.dynamicTerm; + const { __unstableProductOrigin = "BIGGY" } = this.props; const { queryFromHover } = this.state; if (!term) { @@ -183,10 +180,10 @@ class AutoComplete extends React.Component< }); const result = await this.client.suggestionProducts( - this.props.runtime.account, term, queryFromHover ? queryFromHover.key : undefined, queryFromHover ? queryFromHover.value : undefined, + __unstableProductOrigin, ); this.setState({ @@ -199,26 +196,7 @@ class AutoComplete extends React.Component< maxSuggestedProducts = MAX_SUGGESTED_PRODUCTS_DEFAULT, } = this.props; - const products = suggestionProducts.products - .slice(0, maxSuggestedProducts) - .map(currentProduct => { - return new Product( - currentProduct.id, - currentProduct.name, - currentProduct.brand, - currentProduct.url, - currentProduct.price, - currentProduct.priceText, - currentProduct.installment, - currentProduct.images && currentProduct.images.length > 0 - ? currentProduct.images[0].value - : "", - currentProduct.oldPrice, - currentProduct.oldPriceText, - currentProduct.categories, - currentProduct.skus, - ); - }); + const products = suggestionProducts.products.slice(0, maxSuggestedProducts); this.setState({ products, @@ -227,7 +205,7 @@ class AutoComplete extends React.Component< } async updateTopSearches() { - const result = await this.client.topSearches(this.props.runtime.account); + const result = await this.client.topSearches(); const { searches } = result.data.topSearches; const { maxTopSearches = MAX_TOP_SEARCHES_DEFAULT } = this.props; @@ -426,5 +404,4 @@ class AutoComplete extends React.Component< } } -// TO DO: usar compose -export default withDevice(withApollo(withRuntime(AutoComplete))); +export default withDevice(withApollo(AutoComplete)); diff --git a/react/components/SearchQuery.js b/react/components/SearchQuery.js new file mode 100644 index 00000000..70cfdc56 --- /dev/null +++ b/react/components/SearchQuery.js @@ -0,0 +1,127 @@ +import { useQuery } from "react-apollo"; +import PropTypes from "prop-types"; +import { path, pathOr, isEmpty, reject } from "ramda"; +import { onSearchResult } from "vtex.sae-analytics"; +import BiggyClient from "../utils/biggy-client.ts"; +import { + makeFetchMore, + fromAttributesToFacets, +} from "../utils/compatibility-layer.ts"; + +import searchResultQuery from "../graphql/searchResult.gql"; + +const saveTermInHistory = term => { + new BiggyClient().prependSearchHistory(term); +}; + +const SearchQuery = ({ + children, + priceRangeKey, + map, + attributePath, + variables, +}) => { + const searchResult = useQuery(searchResultQuery, { + variables, + ssr: false, + fetchPolicy: "network-only", + onCompleted: data => { + saveTermInHistory(variables.query); + onSearchResult(data); + }, + }); + + const redirect = path(["data", "searchResult", "redirect"], searchResult); + searchResult.loading = searchResult.loading || redirect; + + const products = path(["data", "searchResult", "products"], searchResult); + + const fetchMore = makeFetchMore(searchResult.fetchMore, variables.count); + const recordsFiltered = pathOr( + 0, + ["data", "searchResult", "total"], + searchResult, + ); + + const facets = pathOr( + [], + ["data", "searchResult", "attributes"], + searchResult, + ) + .filter(facet => facet.visible) + .map(attr => fromAttributesToFacets(attr)); + + const searchQuery = {}; + searchQuery.data = { + productSearch: { + products, + breadcrumb: [ + { name: variables.query, href: `/search?_query=${variables.query}` }, + ], + recordsFiltered, + }, + facets: { + departments: [], + brands: [], + specificationFilters: facets.filter(facet => facet.map !== "priceRange"), + categoriesTrees: [], + priceRanges: priceRangeKey + ? facets.filter(facet => facet.map === "priceRange") + : [], + }, + recordsFiltered, + }; + + searchQuery.variables = { + withFacets: true, + query: reject(isEmpty, ["search", attributePath]).join("/"), + map: map || "s", + orderBy: "", + from: 0, + to: variables.count * variables.page - 1, + facetQuery: "search", + facetMap: "b", + }; + + searchQuery.loading = searchResult.loading; + searchQuery.fetchMore = fetchMore; + searchQuery.refetch = () => searchResult.refetch(); + + return children({ + ...searchQuery.variables, + ...searchQuery, + ...variables, + redirect, + searchResult: { + ...searchResult, + ...path(["data", "searchResult"], searchResult), + }, + searchQuery: { + ...path(["data", "searchResult"], searchResult), // Suggestions, banners, etc. + ...searchQuery, + }, + maxItemsPerPage: variables.count, + pagination: "show-more", + params: { term: variables.query }, + query: variables.query, + }); +}; + +SearchQuery.propTypes = { + children: PropTypes.func.isRequired, + priceRangeKey: PropTypes.string, + map: PropTypes.string, + attributePath: PropTypes.string, + variables: PropTypes.shape({ + operator: PropTypes.string, + query: PropTypes.string, + page: PropTypes.number, + attributePath: PropTypes.string, + sort: PropTypes.string, + count: PropTypes.number, + leap: PropTypes.bool, + fuzzy: PropTypes.bool, + }).isRequired, +}; + +export default SearchQuery; diff --git a/react/useRedirect.js b/react/components/useRedirect.js similarity index 100% rename from react/useRedirect.js rename to react/components/useRedirect.js diff --git a/react/graphql/productsById.gql b/react/graphql/productsById.gql deleted file mode 100644 index ae5f845c..00000000 --- a/react/graphql/productsById.gql +++ /dev/null @@ -1,48 +0,0 @@ -query ProductsByReference($ids: [ID!]) { - productsByIdentifier(field: id, values: $ids) - @context(provider: "vtex.search-graphql") { - cacheId - productId - productName - productReference - description - categories - categoryTree { - name - href - } - link - linkText - brand - items { - name - itemId - referenceId { - Value - } - images { - imageUrl - imageTag - } - sellers { - sellerId - commertialOffer { - Installments { - Value - InterestRate - TotalValuePlusInterestRate - NumberOfInstallments - Name - } - AvailableQuantity - Price - ListPrice - } - } - } - productClusters { - id - name - } - } -} diff --git a/react/graphql/searchResult.gql b/react/graphql/searchResult.gql index caf648d8..ecfda966 100644 --- a/react/graphql/searchResult.gql +++ b/react/graphql/searchResult.gql @@ -1,5 +1,105 @@ +fragment Product on Product { + cacheId + productId + description + productName + productReference + linkText + brand + brandId + link + categories + priceRange { + sellingPrice { + highPrice + lowPrice + } + listPrice { + highPrice + lowPrice + } + } + specificationGroups { + name + specifications { + name + values + } + } + items { + itemId + name + nameComplete + complementName + ean + variations { + name + values + } + referenceId { + Key + Value + } + measurementUnit + unitMultiplier + images { + cacheId + imageId + imageLabel + imageTag + imageUrl + imageText + } + sellers { + sellerId + sellerName + commertialOffer { + discountHighlights { + name + } + teasers { + name + conditions { + minimumQuantity + parameters { + name + value + } + } + effects { + parameters { + name + value + } + } + } + Installments { + Value + InterestRate + TotalValuePlusInterestRate + NumberOfInstallments + Name + } + Price + ListPrice + PriceWithoutDiscount + RewardValue + PriceValidUntil + AvailableQuantity + } + } + } + productClusters { + id + name + } + properties { + name + values + } +} + query searchResult( - $store: String! $attributePath: String $query: String $page: Int @@ -8,9 +108,9 @@ query searchResult( $operator: String $fuzzy: Int $leap: Boolean + $productOrigin: Origin ) { searchResult( - store: $store attributePath: $attributePath query: $query page: $page @@ -20,92 +120,13 @@ query searchResult( fuzzy: $fuzzy leap: $leap ) { + products(origin: $productOrigin) { + ...Product + } query operator total redirect - products { - id - product - name - brand - url - oldPrice - price - oldPriceText - priceText - images { - name - value - } - categories - skus { - id - sellers { - id - oldPrice - price - installment { - count - value - } - } - } - installment { - count - value - interest - valueText - } - extraInfo { - key - value - } - } - pagination { - count - current { - index - proxyUrl - } - before { - index - proxyUrl - } - after { - index - proxyUrl - } - next { - index - proxyUrl - } - previous { - index - proxyUrl - } - first { - index - proxyUrl - } - last { - index - proxyUrl - } - } - options { - sorts { - field - order - active - proxyUrl - } - counts { - count - active - proxyUrl - } - } attributes { key label diff --git a/react/graphql/suggestionProducts.gql b/react/graphql/suggestionProducts.gql index 5e822fbc..c70fe741 100644 --- a/react/graphql/suggestionProducts.gql +++ b/react/graphql/suggestionProducts.gql @@ -1,63 +1,118 @@ +fragment Product on Product { + cacheId + productId + description + productName + productReference + linkText + brand + brandId + link + categories + priceRange { + sellingPrice { + highPrice + lowPrice + } + listPrice { + highPrice + lowPrice + } + } + specificationGroups { + name + specifications { + name + values + } + } + items { + itemId + name + nameComplete + complementName + ean + variations { + name + values + } + referenceId { + Key + Value + } + measurementUnit + unitMultiplier + images { + cacheId + imageId + imageLabel + imageTag + imageUrl + imageText + } + sellers { + sellerId + sellerName + commertialOffer { + discountHighlights { + name + } + teasers { + name + conditions { + minimumQuantity + parameters { + name + value + } + } + effects { + parameters { + name + value + } + } + } + Installments { + Value + InterestRate + TotalValuePlusInterestRate + NumberOfInstallments + Name + } + Price + ListPrice + PriceWithoutDiscount + RewardValue + PriceValidUntil + AvailableQuantity + } + } + } + productClusters { + id + name + } + properties { + name + values + } +} + query suggestionProducts( - $store: String! $term: String! $attributeKey: String $attributeValue: String + $productOrigin: Origin ) { suggestionProducts( - store: $store term: $term attributeKey: $attributeKey attributeValue: $attributeValue ) { count - products { - id - name - url - oldPrice - price - oldPriceText - priceText - images { - name - value - } - installment { - count - value - interest - valueText - } - attributes { - key - value - labelKey - labelValue - } - extraInfo { - key - value - } - skus { - id - name - image - url - oldPrice - price - sellers { - id - oldPrice - price - installment { - count - value - interest - valueText - } - } - } + products(origin: $productOrigin) { + ...Product } } } diff --git a/react/graphql/suggestionSearches.gql b/react/graphql/suggestionSearches.gql index ffc3c74b..bdc655bc 100644 --- a/react/graphql/suggestionSearches.gql +++ b/react/graphql/suggestionSearches.gql @@ -1,5 +1,5 @@ -query suggestionSearches($store: String!, $term: String!) { - suggestionSearches(store: $store, term: $term) { +query suggestionSearches($term: String!) { + suggestionSearches(term: $term) { searches { term attributes { diff --git a/react/graphql/topSearches.gql b/react/graphql/topSearches.gql index 89be663d..589866a5 100644 --- a/react/graphql/topSearches.gql +++ b/react/graphql/topSearches.gql @@ -1,5 +1,5 @@ -query topSearches($store: String!, $paidNavigation: Boolean) { - topSearches(store: $store, paidNavigation: $paidNavigation) { +query topSearches { + topSearches { searches { term } diff --git a/react/index.ts b/react/index.ts index 85dc478d..1c3c8aa8 100644 --- a/react/index.ts +++ b/react/index.ts @@ -1,7 +1,5 @@ -import { Product } from "./models/product"; import BiggyClient from "./utils/biggy-client"; export default { BiggyClient, - Product, }; diff --git a/react/utils/biggy-client.ts b/react/utils/biggy-client.ts index 5d15deb7..dbd27c0f 100644 --- a/react/utils/biggy-client.ts +++ b/react/utils/biggy-client.ts @@ -12,45 +12,38 @@ export default class BiggyClient { constructor(private client: ApolloClient) {} - public async topSearches( - store: string, - paidNavigation?: boolean, - ): Promise> { + public async topSearches(): Promise< + ApolloQueryResult<{ topSearches: ISearchesOutput }> + > { return this.client.query({ query: topSearches, - variables: { - store, - paidNavigation, - }, }); } public async suggestionSearches( - store: string, term: string, ): Promise> { return this.client.query({ query: suggestionSearches, variables: { - store, term, }, }); } public async suggestionProducts( - store: string, term: string, attributeKey?: string, attributeValue?: string, + productOrigin: "BIGGY" | "VTEX" = "BIGGY", ): Promise> { return this.client.query({ query: suggestionProducts, variables: { - store, term, attributeKey, attributeValue, + productOrigin, }, }); } @@ -77,7 +70,6 @@ export default class BiggyClient { } async searchResult( - store: string, attributePath: string, query: string, page: number, @@ -89,7 +81,6 @@ export default class BiggyClient { return this.client.query({ query: searchResult, variables: { - store, attributePath, query, page, diff --git a/react/utils/compatibility-layer.ts b/react/utils/compatibility-layer.ts new file mode 100644 index 00000000..2ad3364c --- /dev/null +++ b/react/utils/compatibility-layer.ts @@ -0,0 +1,159 @@ +/* + * Functions designed to provide compatibility between our search components + * and store-components components. + */ + +type UpdateQuery = (prev: any, options: { fetchMoreResult: any }) => void; +type FetchMoreOptions = { + variables: { to: number; page?: number }; + updateQuery: UpdateQuery; +}; +type FetchMore = (options: FetchMoreOptions) => Promise; + +/** + * Our Query depends on a `page` variable, but store-components' SearchContext + * works with `from` and `to` variables. This methods provides a layer when + * fetchMore is called to transform `to` into `page`. + * + * @param fetchMore Apollo's fetchMore function for our query. + */ +export const makeFetchMore = ( + fetchMore: FetchMore, + maxItemsPerPage: number, +): FetchMore => async ({ variables, updateQuery = () => {} }) => { + const { to } = variables; + const page = variables.page + ? variables.page + : Math.floor(to / maxItemsPerPage) + 1; + + await fetchMore({ + updateQuery: makeUpdateQuery(page), + variables: { ...variables, page }, + }); + + return updateQuery( + { productSearch: { products: [] } }, + { + fetchMoreResult: { + productSearch: { products: [] }, + }, + }, + ); +}; + +/** + * UpdateQuery factory for our own query. + * + * @param page Page to search for. + */ +const makeUpdateQuery: (page: number) => UpdateQuery = page => ( + prev, + { fetchMoreResult }, +) => { + if (!fetchMoreResult || page === 1) return prev; + + return { + ...fetchMoreResult, + searchResult: { + ...fetchMoreResult.searchResult, + products: [ + ...prev.searchResult.products, + ...fetchMoreResult.searchResult.products, + ], + }, + }; +}; + +/** + * Convert Biggy attributes into VTEX Catalog facets. + * + * @export + * @param {*} attribute A searchResult attribute. + * @returns A Catalog facet. + */ +export function fromAttributesToFacets(attribute: any) { + if (attribute.type === "number") { + return { + map: "priceRange", + name: attribute.label, + slug: `de-${attribute.minValue}-a-${attribute.maxValue}`, + }; + } + + return { + name: attribute.label, + facets: attribute.values.map((value: any) => { + return { + quantity: value.count, + name: unescape(value.label), + link: value.proxyUrl, + linkEncoded: value.proxyUrl, + map: attribute.key, + selected: false, + value: `z${value.key}`, + }; + }), + }; +} + +type OrderBy = + | "OrderByPriceDESC" + | "OrderByPriceASC" + | "OrderByTopSaleDESC" + | "OrderByReviewRateDESC" + | "OrderByNameDESC" + | "OrderByNameASC" + | "OrderByReleaseDateDESC" + | "OrderByBestDiscountDESC"; + +/** + * Convert from VTEX OrderBy into Biggy's sort. + * + * @export + * @param {OrderBy} orderBy VTEX OrderBy. + * @returns {string} Biggy's sort. + */ +export function convertOrderBy(orderBy: OrderBy): string { + switch (orderBy) { + case "OrderByPriceDESC": + return "price:desc"; + case "OrderByPriceASC": + return "price:asc"; + case "OrderByTopSaleDESC": + return "orders:desc"; + case "OrderByReviewRateDESC": + return ""; // TODO: Not Supported + case "OrderByNameDESC": + return "name:desc"; + case "OrderByNameASC": + return "name:asc"; + case "OrderByReleaseDateDESC": + return "fields.release:desc"; + case "OrderByBestDiscountDESC": + return ""; // TODO: Not Supported + default: + return ""; + } +} + +export function convertURLToAttributePath( + attributePath: string, + map: string, + priceRange: string, + priceRangeKey: string, +) { + const facets = (attributePath || "").split("/"); + const apiUrlTerms = (map || "") + .split(",") + .slice(1) + .map((item, index) => `${item}/${facets[index].replace(/^z/, "")}`); + + const url = apiUrlTerms.join("/"); + + if (priceRange && priceRangeKey) { + const [from, to] = priceRange.split(" TO "); + return `${url}/${priceRangeKey}/${from}:${to}`; + } + + return url; +} diff --git a/react/withAccount.js b/react/withAccount.js deleted file mode 100644 index cdb37ecd..00000000 --- a/react/withAccount.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { useRuntime } from "vtex.render-runtime"; - -export default function withAccount(WrappedComponent) { - return props => { - const { account } = useRuntime(); - - return ; - }; -}