From 4913f6b5d9a20672bc4975137909ed4479f48fb7 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 21 Nov 2024 16:26:40 -0300 Subject: [PATCH 01/20] refactor: simplify product data handling in ProductDetails component --- .../components/sections/ProductDetails/ProductDetails.tsx | 8 ++++---- packages/core/src/pages/[slug]/p.tsx | 6 ++---- packages/core/src/sdk/overrides/PageProvider.tsx | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index cd77ef01ff..4a772cdf1d 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -89,10 +89,10 @@ function ProductDetails({ } = useOverrideComponents<'ProductDetails'>() const { currency } = useSession() const context = usePDP() - const { product, isValidating } = context?.data + const { data, isValidating } = context const [quantity, setQuantity] = useState(1) - if (!product) { + if (!data.product) { throw new Error('NotFound') } @@ -111,7 +111,7 @@ function ProductDetails({ lowPrice, lowPriceWithTaxes, }, - } = product + } = data.product useEffect(() => { import('@faststore/sdk').then(({ sendAnalyticsEvent }) => { @@ -209,7 +209,7 @@ function ProductDetails({ // This decision can be reviewed later if needed quantity={quantity} setQuantity={setQuantity} - product={product} + product={data.product} isValidating={isValidating} taxesConfiguration={taxesConfiguration} /> diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index e7791cb9e3..a058f78b02 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -77,10 +77,8 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { }) const context = { - data: { - ...deepmerge(server, client, { arrayMerge: overwriteMerge }), - isValidating, - }, + data: deepmerge(server, client, { arrayMerge: overwriteMerge }), + isValidating, } as PDPContext return ( diff --git a/packages/core/src/sdk/overrides/PageProvider.tsx b/packages/core/src/sdk/overrides/PageProvider.tsx index d3b50e0980..3c4814cbdf 100644 --- a/packages/core/src/sdk/overrides/PageProvider.tsx +++ b/packages/core/src/sdk/overrides/PageProvider.tsx @@ -10,8 +10,8 @@ import { createContext, useContext, useMemo } from 'react' import { SearchPageContextType } from 'src/pages/s' export interface PDPContext { - data?: ServerProductQueryQuery & - ClientProductQueryQuery['product'] & { isValidating?: boolean } + data?: ServerProductQueryQuery & ClientProductQueryQuery['product'] + isValidating?: boolean } export interface PLPContext { From 145d4f6e3ea11d1361f69329a8095e2335ea38c8 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 21 Nov 2024 16:41:13 -0300 Subject: [PATCH 02/20] feat: enhance loading state handling in ProductDetails component --- .../ProductDetails/ProductDetails.tsx | 122 ++++++++++-------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 4a772cdf1d..53267951da 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -192,61 +192,77 @@ function ProductDetails({ {...ImageGallery.props} images={productImages} /> -
-
- -
- {!outOfStock && ( - +
+

Loading...

+
+
+ ) : ( +
+
- )} -
+ > + +
+ + {!outOfStock && ( + + )} + + )} {shouldDisplayProductDescription && ( Date: Tue, 26 Nov 2024 10:21:52 -0300 Subject: [PATCH 03/20] feat: implement offer fetching and aggregation functionality --- packages/api/src/index.ts | 16 ++++++-- packages/core/src/pages/[slug]/p.tsx | 10 ++--- packages/core/src/sdk/offer/aggregate.ts | 52 ++++++++++++++++++++++++ packages/core/src/sdk/offer/enhance.ts | 20 +++++++++ packages/core/src/sdk/offer/fetcher.ts | 14 +++++++ packages/core/src/sdk/offer/index.ts | 47 +++++++++++++++++++++ packages/core/src/sdk/offer/sort.ts | 28 +++++++++++++ turbo.json | 1 + 8 files changed, 178 insertions(+), 10 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/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index a058f78b02..b3940d5d3c 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -30,8 +30,8 @@ import { GlobalSectionsData, getGlobalSectionsData, } from 'src/components/cms/GlobalSections' +import { useOffer } from 'src/sdk/offer' import PageProvider, { PDPContext } from 'src/sdk/overrides/PageProvider' -import { useProductQuery } from 'src/sdk/product/useProductQuery' import { PDPContentType, getPDP } from 'src/server/cms/pdp' /** @@ -71,14 +71,12 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { const { currency } = useSession() const titleTemplate = storeConfig?.seo?.titleTemplate ?? '' - // Stale while revalidate the product for fetching the new price etc - const { data: client, isValidating } = useProductQuery(product.id, { - product: product, - }) + const offer = useOffer({ skuId: product.sku }) + const client = { product: { offers: offer.offers } } const context = { data: deepmerge(server, client, { arrayMerge: overwriteMerge }), - isValidating, + isValidating: offer.isValidating, } as PDPContext return ( diff --git a/packages/core/src/sdk/offer/aggregate.ts b/packages/core/src/sdk/offer/aggregate.ts new file mode 100644 index 0000000000..29b34b6c9b --- /dev/null +++ b/packages/core/src/sdk/offer/aggregate.ts @@ -0,0 +1,52 @@ +import { Item, Seller } from '@faststore/api' +import { EnhancedCommercialOffer } from './enhance' +import { inStock, price } from './sort' + +type Root = EnhancedCommercialOffer + +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..199ff728bd --- /dev/null +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -0,0 +1,14 @@ +import { ProductSearchResult } from '@faststore/api' +import { api } from '../../../discovery.config' + +export async function fetcher(skuId: string) { + const url = new URL( + `https://${api.storeId}.${api.environment}.com.br/api/io/_v/api/intelligent-search/product_search` + ) + url.searchParams.append('query', `sku.id:${skuId}`) + url.searchParams.append('workspace', 'chrs') + + return fetch(url.toString()).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..bd45e812bf --- /dev/null +++ b/packages/core/src/sdk/offer/index.ts @@ -0,0 +1,47 @@ +import useSWR from 'swr' +import { aggregateOffer } from './aggregate' +import { enhanceCommercialOffer } from './enhance' +import { fetcher } from './fetcher' +import { bestOfferFirst } from './sort' + +export function useOffer(args: { skuId: string }) { + const { data, error, isValidating } = useSWR(args.skuId, fetcher) + + if (error) { + console.warn('Error fetching offer to SKU', args.skuId, error) + } + + if (!data) { + return { offers: {}, isValidating } + } + + if (data.products.length === 0) { + console.warn('Product not found for SKU', args.skuId) + } + + const [product] = data.products + + if (product.items.length === 0) { + console.warn('Product has no items', product) + } + + const item = product.items.find((item) => item.itemId === args.skuId) + + if (!item) { + console.warn('Item not found for SKU', args.skuId) + } + + 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/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/**"] }, From 9dbb5cf75111f9a18e85e55cca64b00dad59502e Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 10:59:03 -0300 Subject: [PATCH 04/20] feat: add revalidation for static props in page component --- packages/core/src/pages/[slug]/p.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index b3940d5d3c..8e228a5e4b 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -265,6 +265,7 @@ export const getStaticProps: GetStaticProps< globalSections, key: seo.canonical, }, + revalidate: 300, // Revalidate every 5 minutes } } From 322158443ce503c23309ace4dde6b69b7b37b914 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 11:41:03 -0300 Subject: [PATCH 05/20] feat: update offer fetcher to use secure subdomain for API requests --- packages/core/src/sdk/offer/fetcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index 199ff728bd..a2a61df46c 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -1,9 +1,9 @@ import { ProductSearchResult } from '@faststore/api' -import { api } from '../../../discovery.config' +import { secureSubdomain } from '../../../discovery.config' export async function fetcher(skuId: string) { const url = new URL( - `https://${api.storeId}.${api.environment}.com.br/api/io/_v/api/intelligent-search/product_search` + `${secureSubdomain}/api/io/_v/api/intelligent-search/product_search` ) url.searchParams.append('query', `sku.id:${skuId}`) url.searchParams.append('workspace', 'chrs') From 6dcead878713b4985015a9d2a34b2da7a29e1be8 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 11:42:21 -0300 Subject: [PATCH 06/20] feat: update offer fetcher to use dynamic base URL for API requests --- packages/core/src/sdk/offer/fetcher.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index a2a61df46c..e99a505689 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -1,10 +1,12 @@ import { ProductSearchResult } from '@faststore/api' -import { secureSubdomain } from '../../../discovery.config' +import { api, secureSubdomain } from '../../../discovery.config' export async function fetcher(skuId: string) { - const url = new URL( - `${secureSubdomain}/api/io/_v/api/intelligent-search/product_search` - ) + const base = + process.env.NODE_ENV === 'development' + ? `https://${api.storeId}.${api.environment}.com.br` + : secureSubdomain + const url = new URL(`${base}/api/io/_v/api/intelligent-search/product_search`) url.searchParams.append('query', `sku.id:${skuId}`) url.searchParams.append('workspace', 'chrs') From a43fbf3e405ba8d5fdb5800fe223451554656553 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 12:08:59 -0300 Subject: [PATCH 07/20] feat: refactor ProductDetails component to improve data structure and validation handling --- .../components/sections/ProductDetails/ProductDetails.tsx | 8 ++++---- packages/core/src/pages/[slug]/p.tsx | 6 ++++-- packages/core/src/sdk/overrides/PageProvider.tsx | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 53267951da..fe80b4d79e 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -89,10 +89,10 @@ function ProductDetails({ } = useOverrideComponents<'ProductDetails'>() const { currency } = useSession() const context = usePDP() - const { data, isValidating } = context + const { product, isValidating } = context?.data const [quantity, setQuantity] = useState(1) - if (!data.product) { + if (!product) { throw new Error('NotFound') } @@ -111,7 +111,7 @@ function ProductDetails({ lowPrice, lowPriceWithTaxes, }, - } = data.product + } = product useEffect(() => { import('@faststore/sdk').then(({ sendAnalyticsEvent }) => { @@ -222,7 +222,7 @@ function ProductDetails({ // This decision can be reviewed later if needed quantity={quantity} setQuantity={setQuantity} - product={data.product} + product={product} isValidating={isValidating} taxesConfiguration={taxesConfiguration} /> diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index 8e228a5e4b..c5a6b15808 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -75,8 +75,10 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { const client = { product: { offers: offer.offers } } const context = { - data: deepmerge(server, client, { arrayMerge: overwriteMerge }), - isValidating: offer.isValidating, + data: { + ...deepmerge(server, client, { arrayMerge: overwriteMerge }), + isValidating: offer.isValidating, + }, } as PDPContext return ( diff --git a/packages/core/src/sdk/overrides/PageProvider.tsx b/packages/core/src/sdk/overrides/PageProvider.tsx index 3c4814cbdf..d3b50e0980 100644 --- a/packages/core/src/sdk/overrides/PageProvider.tsx +++ b/packages/core/src/sdk/overrides/PageProvider.tsx @@ -10,8 +10,8 @@ import { createContext, useContext, useMemo } from 'react' import { SearchPageContextType } from 'src/pages/s' export interface PDPContext { - data?: ServerProductQueryQuery & ClientProductQueryQuery['product'] - isValidating?: boolean + data?: ServerProductQueryQuery & + ClientProductQueryQuery['product'] & { isValidating?: boolean } } export interface PLPContext { From c2811d93537af08fe63a72296d1bf14e62b9abb5 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 17:10:06 -0300 Subject: [PATCH 08/20] feat: update fetcher to include credentials in API requests --- packages/core/src/sdk/offer/fetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index e99a505689..361c406a63 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -10,7 +10,7 @@ export async function fetcher(skuId: string) { url.searchParams.append('query', `sku.id:${skuId}`) url.searchParams.append('workspace', 'chrs') - return fetch(url.toString()).then((res) => + return fetch(url.toString(), { credentials: 'include' }).then((res) => res.json() ) as Promise } From de787bf6ac829ad912a0270bead632e5fa503ea9 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 17:16:51 -0300 Subject: [PATCH 09/20] feat: update offer fetcher to use dynamic store URL and conditionally append workspace parameter --- packages/core/src/sdk/offer/fetcher.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index 361c406a63..b30d218d1a 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -1,16 +1,19 @@ import { ProductSearchResult } from '@faststore/api' -import { api, secureSubdomain } from '../../../discovery.config' +import { api, storeUrl } from '../../../discovery.config' + +const IS_PROD = process.env.NODE_ENV === 'production' export async function fetcher(skuId: string) { - const base = - process.env.NODE_ENV === 'development' - ? `https://${api.storeId}.${api.environment}.com.br` - : secureSubdomain + const base = IS_PROD + ? storeUrl + : `https://${api.storeId}.${api.environment}.com.br` const url = new URL(`${base}/api/io/_v/api/intelligent-search/product_search`) url.searchParams.append('query', `sku.id:${skuId}`) - url.searchParams.append('workspace', 'chrs') + if (IS_PROD) { + url.searchParams.append('workspace', 'chrs') + } - return fetch(url.toString(), { credentials: 'include' }).then((res) => + return fetch(url.toString()).then((res) => res.json() ) as Promise } From 1def59c3e798aad85f1632d674145af2701a8e40 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 17:22:52 -0300 Subject: [PATCH 10/20] feat: update revalidation settings to use dynamic configuration --- packages/core/discovery.config.default.js | 1 + packages/core/src/pages/[slug]/p.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index c5a6b15808..65868b2a6f 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -267,7 +267,7 @@ export const getStaticProps: GetStaticProps< globalSections, key: seo.canonical, }, - revalidate: 300, // Revalidate every 5 minutes + revalidate: storeConfig.experimental.revalidate, } } From 6978522334a96b3d266cc5870e3e43ab37f2ffee Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 17:55:42 -0300 Subject: [PATCH 11/20] feat: enhance error handling in useOffer hook to return initial state on fetch errors --- packages/core/src/sdk/offer/index.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts index bd45e812bf..45cdf4c205 100644 --- a/packages/core/src/sdk/offer/index.ts +++ b/packages/core/src/sdk/offer/index.ts @@ -4,31 +4,28 @@ import { enhanceCommercialOffer } from './enhance' import { fetcher } from './fetcher' import { bestOfferFirst } from './sort' +const INITIAL = { offers: {}, isValidating: true } + export function useOffer(args: { skuId: string }) { const { data, error, isValidating } = useSWR(args.skuId, fetcher) - if (error) { - console.warn('Error fetching offer to SKU', args.skuId, error) - } - - if (!data) { - return { offers: {}, isValidating } - } - - if (data.products.length === 0) { - console.warn('Product not found for SKU', args.skuId) + if (error || !data || data.products.length === 0) { + console.warn('Error or no data fetching offer to SKU', args.skuId, error) + return INITIAL } - const [product] = data.products + const product = data.products[0] - if (product.items.length === 0) { - console.warn('Product has no items', product) + if (!product || product.items.length === 0) { + console.warn('Product not found or has no items for SKU', args.skuId) + return INITIAL } const item = product.items.find((item) => item.itemId === args.skuId) if (!item) { console.warn('Item not found for SKU', args.skuId) + return INITIAL } const sellers = item.sellers From 8c44707709bcb90571b260c7598fbeca263ecadb Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Tue, 26 Nov 2024 18:04:51 -0300 Subject: [PATCH 12/20] feat: update useOffer hook to return error state on fetch failures --- packages/core/src/sdk/offer/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts index 45cdf4c205..12022df95f 100644 --- a/packages/core/src/sdk/offer/index.ts +++ b/packages/core/src/sdk/offer/index.ts @@ -4,28 +4,28 @@ import { enhanceCommercialOffer } from './enhance' import { fetcher } from './fetcher' import { bestOfferFirst } from './sort' -const INITIAL = { offers: {}, isValidating: true } +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 INITIAL + 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 INITIAL + 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 INITIAL + return ERROR_DATA } const sellers = item.sellers From 2daa84c6cc263ce0952a3c92418b8bafe90631ce Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 28 Nov 2024 13:31:19 -0300 Subject: [PATCH 13/20] feat: refactor offer fetching logic to separate URL generation and fetching functions --- packages/core/src/pages/[slug]/p.tsx | 11 ++++++++++- packages/core/src/sdk/offer/fetcher.ts | 8 ++++++-- packages/core/src/sdk/offer/index.ts | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index 65868b2a6f..9726216c78 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -3,6 +3,7 @@ import type { Locator } from '@vtex/client-cms' import deepmerge from 'deepmerge' import type { GetStaticPaths, GetStaticProps } from 'next' import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo' +import Head from 'next/head' import type { ComponentType } from 'react' import { gql } from '@generated' @@ -30,7 +31,7 @@ import { GlobalSectionsData, getGlobalSectionsData, } from 'src/components/cms/GlobalSections' -import { useOffer } from 'src/sdk/offer' +import { getOfferUrl, useOffer } from 'src/sdk/offer' import PageProvider, { PDPContext } from 'src/sdk/overrides/PageProvider' import { PDPContentType, getPDP } from 'src/server/cms/pdp' @@ -83,6 +84,14 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { return ( <> + + + {/* SEO */} + 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 index 12022df95f..ad086486bb 100644 --- a/packages/core/src/sdk/offer/index.ts +++ b/packages/core/src/sdk/offer/index.ts @@ -3,6 +3,7 @@ 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 } From 3a5ab8364f94b9e2666b78eeb5f337e90c7da577 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 28 Nov 2024 14:08:58 -0300 Subject: [PATCH 14/20] 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 --- packages/core/package.json | 2 +- packages/core/src/pages/[slug]/p.tsx | 1 + packages/core/src/sdk/offer/index.ts | 2 +- packages/core/src/sdk/product/useProductLink.ts | 12 +++++++++--- yarn.lock | 16 ++++++++++++---- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 4eefe3c11d..11986a687e 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/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index 9726216c78..c860bd7b00 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -90,6 +90,7 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { href={getOfferUrl(product.sku)} as="fetch" crossOrigin="anonymous" + fetchPriority="high" /> {/* SEO */} diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts index ad086486bb..016f48a6e3 100644 --- a/packages/core/src/sdk/offer/index.ts +++ b/packages/core/src/sdk/offer/index.ts @@ -3,7 +3,7 @@ import { aggregateOffer } from './aggregate' import { enhanceCommercialOffer } from './enhance' import { fetcher } from './fetcher' import { bestOfferFirst } from './sort' -export { getUrl as getOfferUrl } from './fetcher' +export { fetcher as fetcherOffer, getUrl as getOfferUrl } from './fetcher' const ERROR_DATA = { offers: {}, isValidating: false } diff --git a/packages/core/src/sdk/product/useProductLink.ts b/packages/core/src/sdk/product/useProductLink.ts index 289bbcddc8..17beff2a9f 100644 --- a/packages/core/src/sdk/product/useProductLink.ts +++ b/packages/core/src/sdk/product/useProductLink.ts @@ -1,9 +1,9 @@ import type { CurrencyCode, SelectItemEvent } from '@faststore/sdk' -import { useCallback } from 'react' - import type { ProductSummary_ProductFragment } from '@generated/graphql' - +import { useCallback } from 'react' +import { preload } from 'swr' import type { AnalyticsItem, SearchSelectItemEvent } from '../analytics/types' +import { fetcherOffer } from '../offer' import { useSession } from '../session' export type ProductLinkOptions = { @@ -65,6 +65,12 @@ export const useProductLink = ({ return { href: `/${slug}/p`, onClick, + onMouseDown: () => { + preload(product.sku, fetcherOffer) + }, + onTouchStart: () => { + preload(product.sku, fetcherOffer) + }, 'data-testid': 'product-link', } } diff --git a/yarn.lock b/yarn.lock index c732799755..1ebed786d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17303,10 +17303,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" @@ -18270,6 +18273,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" From cd543fcc1ee30a985343250bcf057c8fdaa4f344 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Thu, 28 Nov 2024 14:37:41 -0300 Subject: [PATCH 15/20] feat: remove fetcherOffer export and related preload calls in useProductLink --- packages/core/src/sdk/offer/index.ts | 2 +- packages/core/src/sdk/product/useProductLink.ts | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts index 016f48a6e3..ad086486bb 100644 --- a/packages/core/src/sdk/offer/index.ts +++ b/packages/core/src/sdk/offer/index.ts @@ -3,7 +3,7 @@ import { aggregateOffer } from './aggregate' import { enhanceCommercialOffer } from './enhance' import { fetcher } from './fetcher' import { bestOfferFirst } from './sort' -export { fetcher as fetcherOffer, getUrl as getOfferUrl } from './fetcher' +export { getUrl as getOfferUrl } from './fetcher' const ERROR_DATA = { offers: {}, isValidating: false } diff --git a/packages/core/src/sdk/product/useProductLink.ts b/packages/core/src/sdk/product/useProductLink.ts index 17beff2a9f..e810f96d70 100644 --- a/packages/core/src/sdk/product/useProductLink.ts +++ b/packages/core/src/sdk/product/useProductLink.ts @@ -1,9 +1,7 @@ import type { CurrencyCode, SelectItemEvent } from '@faststore/sdk' import type { ProductSummary_ProductFragment } from '@generated/graphql' import { useCallback } from 'react' -import { preload } from 'swr' import type { AnalyticsItem, SearchSelectItemEvent } from '../analytics/types' -import { fetcherOffer } from '../offer' import { useSession } from '../session' export type ProductLinkOptions = { @@ -65,12 +63,6 @@ export const useProductLink = ({ return { href: `/${slug}/p`, onClick, - onMouseDown: () => { - preload(product.sku, fetcherOffer) - }, - onTouchStart: () => { - preload(product.sku, fetcherOffer) - }, 'data-testid': 'product-link', } } From 1ed7859c68ad642d3c265e7100eca9f235f71d4b Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Fri, 6 Dec 2024 11:19:54 -0300 Subject: [PATCH 16/20] feat: update product search URL to remove unnecessary versioning --- packages/core/src/sdk/offer/fetcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index 8a2dcac0cc..39862981c5 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -7,7 +7,7 @@ export function getUrl(skuId: string) { const base = IS_PROD ? storeUrl : `https://${api.storeId}.${api.environment}.com.br` - const url = new URL(`${base}/api/io/_v/api/intelligent-search/product_search`) + 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') From 18b0940133eb986cc183989d189ca8880fdda6c3 Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Fri, 10 Jan 2025 13:30:31 -0300 Subject: [PATCH 17/20] chore: update yarn.lock to remove deprecated dependencies and clean up versioning --- yarn.lock | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1ebed786d1..8da4f3458f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16869,7 +16869,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16887,15 +16887,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" @@ -16990,7 +16981,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17018,13 +17009,6 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -18709,7 +18693,7 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -18743,15 +18727,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" From 182d580de97719eadff00cd4098c2972cd5b958e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Feij=C3=B3?= Date: Thu, 12 Dec 2024 16:49:07 -0300 Subject: [PATCH 18/20] test: Order form ID from cookie --- packages/api/src/platforms/vtex/resolvers/validateCart.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/api/src/platforms/vtex/resolvers/validateCart.ts b/packages/api/src/platforms/vtex/resolvers/validateCart.ts index fb7d226057..4cf53123ae 100644 --- a/packages/api/src/platforms/vtex/resolvers/validateCart.ts +++ b/packages/api/src/platforms/vtex/resolvers/validateCart.ts @@ -333,9 +333,10 @@ export const validateCart = async ( { cart: { order }, session }: MutationValidateCartArgs, ctx: Context ) => { - const orderNumber = order?.orderNumber - ? order.orderNumber - : getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com') + console.log('GET COOKIE', getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com')) + const orderFormIdFromCookie = getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com') + const orderNumber = orderFormIdFromCookie !== '' ? orderFormIdFromCookie : order?.orderNumber + console.log('ORDER NUMBER', orderNumber) const { acceptedOffer, shouldSplitItem } = order const { From a9287ef75546cd121f780e1436aaeff1b6f58523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Feij=C3=B3?= Date: Thu, 2 Jan 2025 11:24:39 -0300 Subject: [PATCH 19/20] chore: Remove console logs --- packages/api/src/platforms/vtex/resolvers/validateCart.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/api/src/platforms/vtex/resolvers/validateCart.ts b/packages/api/src/platforms/vtex/resolvers/validateCart.ts index 4cf53123ae..843833a331 100644 --- a/packages/api/src/platforms/vtex/resolvers/validateCart.ts +++ b/packages/api/src/platforms/vtex/resolvers/validateCart.ts @@ -333,10 +333,8 @@ export const validateCart = async ( { cart: { order }, session }: MutationValidateCartArgs, ctx: Context ) => { - console.log('GET COOKIE', getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com')) const orderFormIdFromCookie = getCookieCheckoutOrderNumber(ctx.headers.cookie, 'checkout.vtex.com') const orderNumber = orderFormIdFromCookie !== '' ? orderFormIdFromCookie : order?.orderNumber - console.log('ORDER NUMBER', orderNumber) const { acceptedOffer, shouldSplitItem } = order const { From d652941abe6567bee1bc8079a43855d1af0cdf3c Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Fri, 10 Jan 2025 14:27:07 -0300 Subject: [PATCH 20/20] feat: remove workspace parameter from product search URL in production --- packages/core/src/sdk/offer/fetcher.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts index 39862981c5..cab669ba47 100644 --- a/packages/core/src/sdk/offer/fetcher.ts +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -9,10 +9,6 @@ export function getUrl(skuId: string) { : `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() }