diff --git a/spec/components/CioPlp/CioPlp.server.test.jsx b/spec/components/CioPlp/CioPlp.server.test.jsx index 93bda3a4..c46da3bf 100644 --- a/spec/components/CioPlp/CioPlp.server.test.jsx +++ b/spec/components/CioPlp/CioPlp.server.test.jsx @@ -57,7 +57,7 @@ describe('CioPlp React Server-Side Rendering', () => { , ); expect(html).toContain( - '
{"cioClient":null,"cioClientOptions":{},"staticRequestConfigs":{},"itemFieldGetters":{},"formatters":{},"callbacks":{},"urlHelpers":{"defaultQueryStringMap":{"query":"q","page":"page","offset":"offset","resultsPerPage":"numResults","filters":"filters","sortBy":"sortBy","sortOrder":"sortOrder","section":"section"}}}
', + '
{"cioClient":null,"cioClientOptions":{},"staticRequestConfigs":{},"customConfigs":{},"itemFieldGetters":{},"formatters":{},"callbacks":{},"urlHelpers":{"defaultQueryStringMap":{"query":"q","page":"page","offset":"offset","resultsPerPage":"numResults","filters":"filters","sortBy":"sortBy","sortOrder":"sortOrder","section":"section"}}}
', ); }); }); diff --git a/spec/hooks/useProductInfo/useProductInfo.test.js b/spec/hooks/useProductInfo/useProductInfo.test.js index 4b98c326..da0b70a7 100644 --- a/spec/hooks/useProductInfo/useProductInfo.test.js +++ b/spec/hooks/useProductInfo/useProductInfo.test.js @@ -22,15 +22,14 @@ describe('Testing Hook: useProductInfo', () => { const transformedItem = transformResultItem(mockItem); - it('Should return productSwatch, itemId, itemName, itemImageUrl, itemUrl, itemPrice', async () => { + it('Should return itemId, itemName, itemImageUrl, itemUrl, itemPrice', async () => { const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem })); await waitFor(() => { const { - current: { productSwatch, itemName, itemImageUrl, itemUrl, itemPrice, itemId }, + current: { itemName, itemImageUrl, itemUrl, itemPrice, itemId }, } = result; - expect(productSwatch).not.toBeNull(); expect(itemId).toEqual(transformedItem.itemId); expect(itemName).toEqual(transformedItem.itemName); expect(itemImageUrl).toEqual(transformedItem.imageUrl); @@ -46,37 +45,67 @@ describe('Testing Hook: useProductInfo', () => { getPrice: () => {}, getSwatches: () => {}, getSwatchPreview: () => {}, + getName: () => {}, + getItemUrl: () => {}, + getImageUrl: () => {}, }, }, }); await waitFor(() => { const { - current: { productSwatch, itemName, itemImageUrl, itemUrl, itemPrice }, + current: { itemName, itemImageUrl, itemUrl, itemPrice }, } = result; - expect(productSwatch).not.toBeNull(); - expect(itemName).toEqual(transformedItem.itemName); - expect(itemImageUrl).toEqual(transformedItem.imageUrl); - expect(itemUrl).toEqual(transformedItem.url); + expect(itemName).toBeUndefined(); + expect(itemImageUrl).toBeUndefined(); + expect(itemUrl).toBeUndefined(); expect(itemPrice).toBeUndefined(); }); }); - it('Should return correctly after different variation is selected', async () => { - const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem })); + it('Should return properly with getters that override defaults', async () => { + const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem }), { + initialProps: { + itemFieldGetters: { + getPrice: () => 'override', + getSwatches: () => [], + getSwatchPreview: () => 'override', + getName: () => 'override', + getItemUrl: () => 'override', + getImageUrl: () => 'override', + }, + }, + }); + + await waitFor(() => { + const { + current: { itemName, itemImageUrl, itemUrl, itemPrice }, + } = result; + + expect(itemName).toEqual('override'); + expect(itemUrl).toEqual('override'); + expect(itemImageUrl).toEqual('override'); + expect(itemPrice).toEqual('override'); + }); + }); + + it('Should return image properly with overridden baseUrl', async () => { + const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem }), { + initialProps: { + customConfigs: { imageBaseUrl: 'test.com' }, + }, + }); await waitFor(() => { const { - current: { productSwatch, itemName, itemImageUrl, itemUrl, itemPrice }, + current: { itemName, itemImageUrl, itemUrl, itemPrice }, } = result; - const { selectVariation, swatchList } = productSwatch; - selectVariation(swatchList[1]); - expect(itemName).toEqual(swatchList[1].itemName); - expect(itemImageUrl).toEqual(swatchList[1].imageUrl || transformedItem.imageUrl); - expect(itemUrl).toEqual(swatchList[1].url || transformedItem.url); - expect(itemPrice).toEqual(swatchList[1].price || transformedItem.data.price); + expect(itemName).toEqual(transformedItem.itemName); + expect(itemImageUrl).toEqual(`test.com${transformedItem.imageUrl}`); + expect(itemUrl).toEqual(transformedItem.url); + expect(itemPrice).toEqual(transformedItem.data.price); }); }); @@ -99,14 +128,27 @@ describe('Testing Hook: useProductInfo', () => { await waitFor(() => { const { - current: { productSwatch, itemName, itemImageUrl, itemUrl, itemPrice }, + current: { itemName, itemImageUrl, itemUrl, itemPrice }, } = result; - expect(productSwatch).not.toBeNull(); expect(itemName).toEqual(transformedItem.itemName); expect(itemImageUrl).toEqual(transformedItem.imageUrl); expect(itemUrl).toEqual(transformedItem.url); expect(itemPrice).toBeUndefined(); }); }); + + it('should merge product info fields with selectedVariation when provided', async () => { + const transformedItem = transformResultItem(mockItem); + const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem, selectedVariation: transformedItem.variations[0] })); + + console.log(transformedItem.variations[0]) + await waitFor(() => { + const { current: { itemName, itemPrice, itemImageUrl, itemUrl } } = result; + expect(itemName).toEqual(transformedItem.variations[0].itemName); + expect(itemPrice).toEqual(transformedItem.variations[0].data.price); + expect(itemImageUrl).toEqual(transformedItem.variations[0].imageUrl); + expect(itemUrl).toEqual(transformedItem.variations[0].url); + }); + }); }); diff --git a/spec/hooks/useProductSwatch/useProductSwatch.test.js b/spec/hooks/useProductSwatch/useProductSwatch.test.js index 88d06ba8..2db34de3 100644 --- a/spec/hooks/useProductSwatch/useProductSwatch.test.js +++ b/spec/hooks/useProductSwatch/useProductSwatch.test.js @@ -18,7 +18,7 @@ describe('Testing Hook: useProductSwatch', () => { }); const transformedItem = transformResultItem(mockItem); - const expectedSwatch = getSwatches(transformedItem, getPrice, getSwatchPreview); + const expectedSwatch = getSwatches(transformedItem, getSwatchPreview); it('Should throw error if called outside of PlpContext', () => { expect(() => renderHook(() => useProductSwatch())).toThrow(); @@ -73,7 +73,7 @@ describe('Testing Hook: useProductSwatch', () => { } = result; expect(typeof selectVariation).toBe('function'); - expect(selectedVariation).toBeUndefined(); + expect(selectedVariation).toBe(transformedItem.variations[0]); expect(swatchList.length).toBe(0); }); }); @@ -101,7 +101,7 @@ describe('Testing Hook: useProductSwatch', () => { } = result; expect(typeof selectVariation).toBe('function'); - expect(selectedVariation).toBeUndefined(); + expect(selectedVariation).toBe(transformedItem.variations[0]); expect(swatchList.length).toBe(0); }); }); diff --git a/spec/local_examples/item.json b/spec/local_examples/item.json index c0c26782..acd94180 100644 --- a/spec/local_examples/item.json +++ b/spec/local_examples/item.json @@ -32,6 +32,8 @@ "variation_id": "BKT00110DG1733LR", "swatchPreview": "#e04062", "price": 90, + "image_url": "https://constructorio-integrations.s3.amazonaws.com/tikus-threads/2022-06-29/Casual-Shirts_Washed-Poplin-Shirts_19145-MTX64_40_category-outfitter.jpg", + "url": "https://constructorio-integrations.s3.amazonaws.com/tikus-threads/2022-06-29/Casual-Shirts_Washed-Poplin-Shirts_19145-MTX64_40_category-outfitter.jpg", "facets": [ { "name": "Color", diff --git a/spec/utils.test.tsx b/spec/utils.test.tsx index f98d1e75..b38129be 100644 --- a/spec/utils.test.tsx +++ b/spec/utils.test.tsx @@ -9,7 +9,7 @@ const transformedItem = transformResultItem(mockItem); describe('Testing Utils, getProductCardCnstrcDataAttributes', () => { test('Should return relevant data attributes for Product Card', async () => { - const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem })); + const { result } = renderHookWithCioPlp(() => useProductInfo({ item: transformedItem, selectedVariation: { variationId: 'BKT00110DG1733LR', swatchPreview: '#FFFFFF' }})); await waitFor(() => { const dataAttributes = getProductCardCnstrcDataAttributes(result.current); diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx index 16a65f82..d27043cb 100644 --- a/src/components/ProductCard/ProductCard.tsx +++ b/src/components/ProductCard/ProductCard.tsx @@ -4,6 +4,7 @@ import { useOnAddToCart, useOnProductCardClick } from '../../hooks/callbacks'; import { CnstrcData, IncludeRenderProps, Item, ProductInfoObject } from '../../types'; import ProductSwatch from '../ProductSwatch'; import useProductInfo from '../../hooks/useProduct'; +import useProductSwatch from '../../hooks/useProductSwatch'; import { getProductCardCnstrcDataAttributes } from '../../utils'; interface Props { @@ -55,8 +56,10 @@ export type ProductCardProps = IncludeRenderProps export default function ProductCard(props: ProductCardProps) { const { item, children } = props; const state = useCioPlpContext(); - const productInfo = useProductInfo({ item }); - const { productSwatch, itemName, itemPrice, itemImageUrl, itemUrl } = productInfo; + const productSwatch = useProductSwatch({ item }); + const { selectedVariation } = productSwatch; + const productInfo = useProductInfo({ item, selectedVariation }); + const { itemName, itemPrice, itemImageUrl, itemUrl } = productInfo; if (!state) { throw new Error('This component is meant to be used within the CioPlp provider.'); diff --git a/src/hooks/useCioPlpProvider.ts b/src/hooks/useCioPlpProvider.ts index 44a85a97..f41fd4ef 100644 --- a/src/hooks/useCioPlpProvider.ts +++ b/src/hooks/useCioPlpProvider.ts @@ -15,6 +15,7 @@ export default function useCioPlpProvider( itemFieldGetters, urlHelpers, staticRequestConfigs = {}, + customConfigs = {}, cioClient: customCioClient, cioClientOptions: customCioClientOptions = {}, } = props; @@ -28,6 +29,7 @@ export default function useCioPlpProvider( cioClientOptions, setCioClientOptions, staticRequestConfigs, + customConfigs, itemFieldGetters: { ...defaultGetters, ...itemFieldGetters }, formatters: { ...defaultFormatters, ...formatters }, callbacks: { ...callbacks }, @@ -41,6 +43,7 @@ export default function useCioPlpProvider( callbacks, urlHelpers, staticRequestConfigs, + customConfigs, ], ); diff --git a/src/hooks/useProduct.ts b/src/hooks/useProduct.ts index d9a9f8bd..efd3dc72 100644 --- a/src/hooks/useProduct.ts +++ b/src/hooks/useProduct.ts @@ -1,32 +1,38 @@ -import useProductSwatch from './useProductSwatch'; import { useCioPlpContext } from './useCioPlpContext'; -import { UseProductInfo } from '../types'; import { tryCatchify } from '../utils'; +import { Item, SwatchItem } from '../types'; -const useProductInfo: UseProductInfo = ({ item }) => { +interface UseProductInfoArgs { + item: Item; + selectedVariation?: SwatchItem; +} + +const useProductInfo = ({ item, selectedVariation }: UseProductInfoArgs) => { const state = useCioPlpContext(); - const productSwatch = useProductSwatch({ item }); if (!item.data || !item.itemId || !item.itemName) { throw new Error('data, itemId, or itemName are required.'); } const getPrice = tryCatchify(state?.itemFieldGetters?.getPrice); + const getImageUrl = tryCatchify(state?.itemFieldGetters?.getImageUrl); + const getItemUrl = tryCatchify(state?.itemFieldGetters?.getItemUrl); + const getName = tryCatchify(state?.itemFieldGetters?.getName); - const itemName = productSwatch?.selectedVariation?.itemName || item.itemName; - const itemPrice = productSwatch?.selectedVariation?.price || getPrice(item); - const itemImageUrl = productSwatch?.selectedVariation?.imageUrl || item.imageUrl; - const itemUrl = productSwatch?.selectedVariation?.url || item.url; - const variationId = productSwatch?.selectedVariation?.variationId; + const itemName = getName(item, selectedVariation); + const itemPrice = getPrice(item, selectedVariation); + const itemImageUrl = getImageUrl(item, selectedVariation, { + imageBaseUrl: state.customConfigs.imageBaseUrl, + }); + const itemUrl = getItemUrl(item, selectedVariation); const { itemId } = item; return { - productSwatch, itemName, itemPrice, itemImageUrl, itemUrl, - variationId, + variationId: selectedVariation?.variationId, itemId, }; }; diff --git a/src/hooks/useProductSwatch.ts b/src/hooks/useProductSwatch.ts index 8ed9eec8..b117d3b4 100644 --- a/src/hooks/useProductSwatch.ts +++ b/src/hooks/useProductSwatch.ts @@ -1,43 +1,42 @@ import { useEffect, useState } from 'react'; import { useCioPlpContext } from './useCioPlpContext'; -import { SwatchItem, UseProductSwatch } from '../types'; +import { SwatchItem, UseProductSwatch, Variation } from '../types'; import { getSwatches as defaultGetSwatches, - getPrice as defaultGetPrice, getSwatchPreview as defaultGetSwatchPreview, } from '../utils/itemFieldGetters'; const useProductSwatch: UseProductSwatch = ({ item }) => { - const [selectedVariation, setSelectedVariation] = useState(); + const [selectedVariation, setSelectedVariation] = useState(); const [swatchList, setSwatchList] = useState([]); const state = useCioPlpContext(); const getSwatches = state?.itemFieldGetters?.getSwatches || defaultGetSwatches; - const getPrice = state?.itemFieldGetters?.getPrice || defaultGetPrice; const getSwatchPreview = state?.itemFieldGetters?.getSwatchPreview || defaultGetSwatchPreview; useEffect(() => { if (item?.variations) { try { - setSwatchList(getSwatches(item, getPrice, getSwatchPreview) || []); + const swatches = getSwatches(item, getSwatchPreview); + setSwatchList(swatches || []); } catch (e) { // do nothing } } - }, [item, getSwatches, getPrice, getSwatchPreview]); + }, [item, getSwatches, getSwatchPreview]); useEffect(() => { if (item?.variations) { - const initialSwatch = swatchList?.find((swatch) => swatch?.variationId === item?.variationId); - if (initialSwatch) { - setSelectedVariation(initialSwatch); + const initialVariation = item?.variations?.[0]; + if (initialVariation) { + setSelectedVariation(initialVariation); } } }, [swatchList, item]); - const selectVariation = (swatch: SwatchItem) => { - setSelectedVariation(swatch); + const selectVariation = (variation: Variation) => { + setSelectedVariation(variation); }; return { diff --git a/src/stories/components/CioPlp/CioPlpProps.md b/src/stories/components/CioPlp/CioPlpProps.md index 12981f2c..d84fc9fe 100644 --- a/src/stories/components/CioPlp/CioPlpProps.md +++ b/src/stories/components/CioPlp/CioPlpProps.md @@ -31,9 +31,12 @@ Callbacks will be composed with the library's internal tracking calls for a give ItemFieldGetters maps the fields sent in the catalog feeds to the fields the libary expects for rendering -| property | type | description | -| -------- | ------------------------ | ------------------ | -| getPrice | `(item: Item) => number` | Get price funciton | +| property | type | description | +| ----------- | -----------------------------------------------| ---------------------- | +| getPrice | `(item: Item, variation: Variation) => number` | Get price funciton | +| getImageUrl | `(item: Item, variation: Variation) => string` | Get image url funciton | +| getItemUrl | `(item: Item, variation: Variation) => stirng` | Get href url funciton | +| getName | `(item: Item, variation: Variation) => string` | Get item name funciton |
diff --git a/src/types.ts b/src/types.ts index 2f1b7edc..ed702e82 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,11 +39,13 @@ export interface ApiHierarchicalFacetOption extends ApiFacetOption { export type CioClientOptions = Omit; export interface ItemFieldGetters { - getPrice: (item: Item | Variation) => number; + getPrice: (item: Item, variation?: Variation) => number | undefined; + getItemUrl: (item: Item, variation?: Variation) => string | undefined; + getImageUrl: (item: Item, variation?: Variation) => string | undefined; + getName: (item: Item, variation?: Variation) => string; getSwatchPreview: (variation: Variation) => string; getSwatches: ( item: Item, - retrievePrice: ItemFieldGetters['getPrice'], retrieveSwatchPreview: ItemFieldGetters['getSwatchPreview'], ) => SwatchItem[] | undefined; } @@ -109,6 +111,10 @@ export interface UrlHelpers { defaultQueryStringMap: Readonly; } +export interface CustomConfigs { + imageBaseUrl?: string; +} + export interface RequestConfigs { // Search query?: string; @@ -137,6 +143,7 @@ export interface PlpContextValue { cioClientOptions: CioClientOptions; setCioClientOptions: React.Dispatch; staticRequestConfigs: RequestConfigs; + customConfigs: CustomConfigs; itemFieldGetters: ItemFieldGetters; formatters: Formatters; callbacks: Callbacks; @@ -197,6 +204,7 @@ export interface SwatchItem { price?: number; swatchPreview: string; variationId?: string; + variation?: Variation; } export interface PlpBrowseData { @@ -219,6 +227,7 @@ export interface CioPlpProviderProps { initialSearchResponse?: SearchResponse; initialBrowseResponse?: GetBrowseResultsResponse; staticRequestConfigs?: Partial; + customConfigs?: Partial; } export type UseSortReturn = { @@ -229,8 +238,8 @@ export type UseSortReturn = { export interface ProductSwatchObject { swatchList: SwatchItem[] | undefined; - selectedVariation: SwatchItem | undefined; - selectVariation: (swatch: SwatchItem) => void; + selectedVariation: Variation | undefined; + selectVariation: (variation: Variation) => void; } export type UseProductSwatchProps = { @@ -240,7 +249,6 @@ export type UseProductSwatchProps = { export type UseProductSwatch = (props: UseProductSwatchProps) => ProductSwatchObject; export interface ProductInfoObject { - productSwatch?: ProductSwatchObject; itemName: string; itemId: string; itemPrice?: number; diff --git a/src/utils/itemFieldGetters.ts b/src/utils/itemFieldGetters.ts index d310229e..c2219a58 100644 --- a/src/utils/itemFieldGetters.ts +++ b/src/utils/itemFieldGetters.ts @@ -1,13 +1,28 @@ import { ItemFieldGetters, Item, SwatchItem, Variation } from '../types'; -// eslint-disable-next-line import/prefer-default-export -export function getPrice(item: Item | Variation): number { - return item.data.price; +export function getPrice(item: Item, variation?: Variation): number { + return variation?.data?.price || item?.data?.price; +} + +export function getImageUrl(item: Item, variation?: Variation, options?: any): string | undefined { + const { imageBaseUrl } = options; + + if (imageBaseUrl) { + return `${imageBaseUrl}${variation?.imageUrl || item?.imageUrl}`; + } + return variation?.imageUrl || item?.imageUrl; +} + +export function getItemUrl(item: Item, variation?: Variation): string | undefined { + return variation?.url || item.url; +} + +export function getName(item: Item, variation?: Variation): string { + return variation?.itemName || item.itemName; } export function getSwatches( item: Item, - retrievePrice: ItemFieldGetters['getPrice'], retrieveSwatchPreview: ItemFieldGetters['getSwatchPreview'], ): SwatchItem[] | undefined { const swatchList: SwatchItem[] = []; @@ -19,8 +34,8 @@ export function getSwatches( url: variation?.url || item?.url, imageUrl: variation?.url, variationId: variation?.variationId, - price: retrievePrice(variation), swatchPreview: retrieveSwatchPreview(variation), + variation, }); } });