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 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/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index cd77ef01ff..fe80b4d79e 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 && ( + + + {/* 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 c732799755..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" @@ -17303,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" @@ -18270,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" @@ -18701,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== @@ -18735,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"