Skip to content

Commit

Permalink
refactor: simplify product data handling in ProductDetails component
Browse files Browse the repository at this point in the history
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
  • Loading branch information
emersonlaurentino committed Jan 22, 2025
1 parent 63d9790 commit 0695f89
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 99 deletions.
16 changes: 12 additions & 4 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'
1 change: 1 addition & 0 deletions packages/core/discovery.config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,6 @@ module.exports = {
noRobots: false,
preact: false,
enableRedirects: false,
revalidate: 300, // Revalidate every 5 minutes
},
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,61 +192,77 @@ function ProductDetails({
{...ImageGallery.props}
images={productImages}
/>
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<ProductDetailsSettings.Component
buyButtonTitle={buyButtonTitle}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
}
useUnitMultiplier={quantitySelector?.useUnitMultiplier ?? false}
{...ProductDetailsSettings.props}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
quantity={quantity}
setQuantity={setQuantity}
product={product}
isValidating={isValidating}
taxesConfiguration={taxesConfiguration}
/>
</section>

{!outOfStock && (
<ShippingSimulation.Component
{isValidating ? (
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<p>Loading...</p>
</section>
</section>
) : (
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={shippingSimulatorTitle ?? ShippingSimulation.props.title}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
/>
)}
</section>
>
<ProductDetailsSettings.Component
buyButtonTitle={buyButtonTitle}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
}
useUnitMultiplier={
quantitySelector?.useUnitMultiplier ?? false
}
{...ProductDetailsSettings.props}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
quantity={quantity}
setQuantity={setQuantity}
product={product}
isValidating={isValidating}
taxesConfiguration={taxesConfiguration}
/>
</section>

{!outOfStock && (
<ShippingSimulation.Component
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={
shippingSimulatorTitle ?? ShippingSimulation.props.title
}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
/>
)}
</section>
)}

{shouldDisplayProductDescription && (
<ProductDescription.Component
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/pages/[slug]/p.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -30,8 +31,8 @@ import {
GlobalSectionsData,
getGlobalSectionsData,
} from 'src/components/cms/GlobalSections'
import { getOfferUrl, 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'

/**
Expand Down Expand Up @@ -71,20 +72,27 @@ 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 (
<>
<Head>
<link
rel="preload"
href={getOfferUrl(product.sku)}
as="fetch"
crossOrigin="anonymous"
fetchPriority="high"
/>
</Head>
{/* SEO */}
<NextSeo
title={meta.title}
Expand Down Expand Up @@ -269,6 +277,7 @@ export const getStaticProps: GetStaticProps<
globalSections,
key: seo.canonical,
},
revalidate: storeConfig.experimental.revalidate,
}
}

Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/sdk/offer/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Item, Seller } from '@faststore/api'
import { EnhancedCommercialOffer } from './enhance'
import { inStock, price } from './sort'

type Root = EnhancedCommercialOffer<Seller, Item>

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,
}
}
20 changes: 20 additions & 0 deletions packages/core/src/sdk/offer/enhance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommertialOffer } from '@faststore/api'

export type EnhancedCommercialOffer<S, P> = CommertialOffer & {
seller: S
product: P
}

export const enhanceCommercialOffer = <S, P>({
offer,
seller,
product,
}: {
offer: CommertialOffer
seller: S
product: P
}): EnhancedCommercialOffer<S, P> => ({
...offer,
product,
seller,
})
23 changes: 23 additions & 0 deletions packages/core/src/sdk/offer/fetcher.ts
Original file line number Diff line number Diff line change
@@ -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<ProductSearchResult>
}
45 changes: 45 additions & 0 deletions packages/core/src/sdk/offer/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
Loading

0 comments on commit 0695f89

Please sign in to comment.