From 241f9ed144fe68f39821a22d74d502fcd479c8bf Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 21 Nov 2024 16:26:40 -0300 Subject: [PATCH] refactor: simplify product data handling in ProductDetails component feat: enhance loading state handling in ProductDetails component feat: implement offer fetching and aggregation functionality feat: add revalidation for static props in page component feat: update offer fetcher to use secure subdomain for API requests feat: update offer fetcher to use dynamic base URL for API requests feat: refactor ProductDetails component to improve data structure and validation handling feat: update fetcher to include credentials in API requests feat: update offer fetcher to use dynamic store URL and conditionally append workspace parameter feat: update revalidation settings to use dynamic configuration feat: enhance error handling in useOffer hook to return initial state on fetch errors feat: update useOffer hook to return error state on fetch failures feat: refactor offer fetching logic to separate URL generation and fetching functions feat: set fetch priority to high for offer URLs in page component feat: upgrade swr to version 2.2.5 and update offer fetching logic feat: remove fetcherOffer export and related preload calls in useProductLink feat: update product search URL to remove unnecessary versioning chore: update yarn.lock to remove deprecated dependencies and clean up versioning --- packages/api/src/index.ts | 16 +- packages/core/discovery.config.default.js | 1 + packages/core/package.json | 2 +- .../ProductDetails/ProductDetails.tsx | 141 +++++++++--------- packages/core/src/pages/[slug]/p.tsx | 21 ++- packages/core/src/sdk/offer/aggregate.ts | 52 +++++++ packages/core/src/sdk/offer/enhance.ts | 20 +++ packages/core/src/sdk/offer/fetcher.ts | 23 +++ packages/core/src/sdk/offer/index.ts | 45 ++++++ packages/core/src/sdk/offer/sort.ts | 28 ++++ .../core/src/sdk/product/useProductLink.ts | 4 +- turbo.json | 1 + yarn.lock | 16 +- 13 files changed, 279 insertions(+), 91 deletions(-) create mode 100644 packages/core/src/sdk/offer/aggregate.ts create mode 100644 packages/core/src/sdk/offer/enhance.ts create mode 100644 packages/core/src/sdk/offer/fetcher.ts create mode 100644 packages/core/src/sdk/offer/index.ts create mode 100644 packages/core/src/sdk/offer/sort.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 139aeb5155..cbd36882ef 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -24,11 +24,12 @@ const platforms = { }, } -const directives: Directive[] = [ - cacheControlDirective -] +const directives: Directive[] = [cacheControlDirective] -export const getTypeDefs = () => [typeDefs, ...directives.map(d => d.typeDefs)] +export const getTypeDefs = () => [ + typeDefs, + ...directives.map((d) => d.typeDefs), +] export const getResolvers = (options: Options) => platforms[options.platform].getResolvers(options) @@ -47,3 +48,10 @@ export const getSchema = async (options: Options) => { export * from './platforms/vtex/resolvers/root' export type { Resolver } from './platforms/vtex' + +export type { + CommertialOffer, + Item, + ProductSearchResult, + Seller, +} from './platforms/vtex/clients/search/types/ProductSearchResult' diff --git a/packages/core/discovery.config.default.js b/packages/core/discovery.config.default.js index 70aab9636f..1f63b07d41 100644 --- a/packages/core/discovery.config.default.js +++ b/packages/core/discovery.config.default.js @@ -102,5 +102,6 @@ module.exports = { noRobots: false, preact: false, enableRedirects: false, + revalidate: 300, // Revalidate every 5 minutes }, } diff --git a/packages/core/package.json b/packages/core/package.json index 245aee1c7d..183574181f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,7 +80,7 @@ "sass-loader": "^12.6.0", "sharp": "^0.32.6", "style-loader": "^3.3.1", - "swr": "^1.3.0", + "swr": "^2.2.5", "tsx": "^4.6.2", "typescript": "4.7.3" }, diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 29df9bf941..9af9983f82 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -214,82 +214,77 @@ function ProductDetails({ {...ImageGallery.props} images={productImages} /> -
-
- - {skuMatrix?.shouldDisplaySKUMatrix && - Object.keys(slugsMap).length > 1 && ( - <> -
- {skuMatrix.separatorButtonsText} -
- - - - {skuMatrix.triggerButtonLabel} - - - - - - )} + {isValidating ? ( +
+
+

Loading...

+
- - {!outOfStock && ( - +
- )} -
+ > + +
+ + {!outOfStock && ( + + )} +
+ )} {shouldDisplayProductDescription && ( + + + {/* SEO */} + +const withTax = ( + price: number, + tax: number = 0, + unitMultiplier: number = 1 +) => { + const unitTax = tax / unitMultiplier + return Math.round((price + unitTax) * 100) / 100 +} + +const getHighPrice = ( + offers: Root[], + options: { includeTaxes: boolean } = { includeTaxes: false } +) => { + const availableOffers = offers.filter(inStock) + const highOffer = availableOffers[availableOffers.length - 1] + const highPrice = highOffer ? price(highOffer) : 0 + if (!options.includeTaxes) { + return highPrice + } + + return withTax(highPrice, highOffer?.Tax, highOffer?.product?.unitMultiplier) +} + +const getLowPrice = ( + offers: Root[], + options: { includeTaxes: boolean } = { includeTaxes: false } +) => { + const [lowOffer] = offers.filter(inStock) + + const lowPrice = lowOffer ? price(lowOffer) : 0 + + if (!options.includeTaxes) { + return lowPrice + } + + return withTax(lowPrice, lowOffer?.Tax, lowOffer?.product?.unitMultiplier) +} + +export function aggregateOffer(offers: Root[]) { + return { + highPrice: getHighPrice(offers), + lowPrice: getLowPrice(offers), + lowPriceWithTaxes: getLowPrice(offers, { includeTaxes: true }), + offerCount: offers.length, + } +} diff --git a/packages/core/src/sdk/offer/enhance.ts b/packages/core/src/sdk/offer/enhance.ts new file mode 100644 index 0000000000..e3d83ec465 --- /dev/null +++ b/packages/core/src/sdk/offer/enhance.ts @@ -0,0 +1,20 @@ +import { CommertialOffer } from '@faststore/api' + +export type EnhancedCommercialOffer = CommertialOffer & { + seller: S + product: P +} + +export const enhanceCommercialOffer = ({ + offer, + seller, + product, +}: { + offer: CommertialOffer + seller: S + product: P +}): EnhancedCommercialOffer => ({ + ...offer, + product, + seller, +}) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts new file mode 100644 index 0000000000..39862981c5 --- /dev/null +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -0,0 +1,23 @@ +import { ProductSearchResult } from '@faststore/api' +import { api, storeUrl } from '../../../discovery.config' + +const IS_PROD = process.env.NODE_ENV === 'production' + +export function getUrl(skuId: string) { + const base = IS_PROD + ? storeUrl + : `https://${api.storeId}.${api.environment}.com.br` + const url = new URL(`${base}/api/intelligent-search/product_search`) + url.searchParams.append('query', `sku.id:${skuId}`) + if (IS_PROD) { + url.searchParams.append('workspace', 'chrs') + } + + return url.toString() +} + +export async function fetcher(skuId: string) { + return fetch(getUrl(skuId)).then((res) => + res.json() + ) as Promise +} diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts new file mode 100644 index 0000000000..ad086486bb --- /dev/null +++ b/packages/core/src/sdk/offer/index.ts @@ -0,0 +1,45 @@ +import useSWR from 'swr' +import { aggregateOffer } from './aggregate' +import { enhanceCommercialOffer } from './enhance' +import { fetcher } from './fetcher' +import { bestOfferFirst } from './sort' +export { getUrl as getOfferUrl } from './fetcher' + +const ERROR_DATA = { offers: {}, isValidating: false } + +export function useOffer(args: { skuId: string }) { + const { data, error, isValidating } = useSWR(args.skuId, fetcher) + + if (error || !data || data.products.length === 0) { + console.warn('Error or no data fetching offer to SKU', args.skuId, error) + return ERROR_DATA + } + + const product = data.products[0] + + if (!product || product.items.length === 0) { + console.warn('Product not found or has no items for SKU', args.skuId) + return ERROR_DATA + } + + const item = product.items.find((item) => item.itemId === args.skuId) + + if (!item) { + console.warn('Item not found for SKU', args.skuId) + return ERROR_DATA + } + + const sellers = item.sellers + .map((seller) => + enhanceCommercialOffer({ + offer: seller.commertialOffer, + seller, + product: item, + }) + ) + .sort(bestOfferFirst) + + const offers = aggregateOffer(sellers) + + return { offers, isValidating } +} diff --git a/packages/core/src/sdk/offer/sort.ts b/packages/core/src/sdk/offer/sort.ts new file mode 100644 index 0000000000..410530b0c1 --- /dev/null +++ b/packages/core/src/sdk/offer/sort.ts @@ -0,0 +1,28 @@ +import type { CommertialOffer } from '@faststore/api' + +export const inStock = (offer: Pick) => + offer.AvailableQuantity > 0 + +export const price = (offer: Pick) => + offer.spotPrice ?? 0 + +export const availability = (available: boolean) => + available ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock' + +export const bestOfferFirst = ( + a: Pick, + b: Pick +) => { + if (inStock(a) && !inStock(b)) { + return -1 + } + + if (!inStock(a) && inStock(b)) { + return 1 + } + + return price(a) - price(b) +} + +export const inStockOrderFormItem = (itemAvailability: string) => + itemAvailability === 'available' diff --git a/packages/core/src/sdk/product/useProductLink.ts b/packages/core/src/sdk/product/useProductLink.ts index 289bbcddc8..e810f96d70 100644 --- a/packages/core/src/sdk/product/useProductLink.ts +++ b/packages/core/src/sdk/product/useProductLink.ts @@ -1,8 +1,6 @@ import type { CurrencyCode, SelectItemEvent } from '@faststore/sdk' -import { useCallback } from 'react' - import type { ProductSummary_ProductFragment } from '@generated/graphql' - +import { useCallback } from 'react' import type { AnalyticsItem, SearchSelectItemEvent } from '../analytics/types' import { useSession } from '../session' diff --git a/turbo.json b/turbo.json index 2d6c20c26f..799820723d 100644 --- a/turbo.json +++ b/turbo.json @@ -22,6 +22,7 @@ "dependsOn": ["^build"] }, "lint": {}, + "serve": {}, "start": { "outputs": ["dist/**"] }, diff --git a/yarn.lock b/yarn.lock index d76a46d418..8da4f3458f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17287,10 +17287,13 @@ swap-case@^2.0.2: dependencies: tslib "^2.0.3" -swr@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz" - integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== +swr@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b" + integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" symbol-observable@^1.1.0: version "1.2.0" @@ -18254,6 +18257,11 @@ urlpattern-polyfill@^8.0.0: resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz" integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ== +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"