From adad825611a4bda25fc2e8202afb514f4640dc8a Mon Sep 17 00:00:00 2001 From: "Mark J. Becker" Date: Mon, 29 Jan 2024 14:52:51 +0100 Subject: [PATCH 01/10] Add urlKey parameter to route config --- dev-template.html | 2 +- src/components/ProductItem/MockData.ts | 3 +++ src/components/ProductItem/ProductItem.tsx | 2 +- src/components/ProductList/MockData.ts | 2 ++ src/types/interface.ts | 3 ++- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dev-template.html b/dev-template.html index ef62690e..bb0bab1c 100644 --- a/dev-template.html +++ b/dev-template.html @@ -75,7 +75,7 @@ apiKey: '', environmentType: '', // searchQuery: 'search_query', // Optional: providing searchQuery will override 'q' query param - // route: ({ sku }) => { + // route: ({ sku, urlKey }) => { // const storeConfig = JSON.parse( // document // .querySelector("meta[name='store-config']") diff --git a/src/components/ProductItem/MockData.ts b/src/components/ProductItem/MockData.ts index 3deacd69..2d6dbb78 100644 --- a/src/components/ProductItem/MockData.ts +++ b/src/components/ProductItem/MockData.ts @@ -123,6 +123,7 @@ export const sampleProductNoImage: Product = { }, gift_message_available: null, url: 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html', + urlKey: 'sprite-foam-yoga-brick', media_gallery: null, custom_attributes: null, add_to_cart_allowed: null, @@ -294,6 +295,7 @@ export const sampleProductDiscounted: Product = { }, gift_message_available: null, url: 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html', + urlKey: 'sprite-foam-yoga-brick', media_gallery: null, custom_attributes: null, add_to_cart_allowed: null, @@ -465,6 +467,7 @@ export const sampleProductNotDiscounted: Product = { }, gift_message_available: null, url: 'http://master-7rqtwti-eragxvhtzr4am.us-4.magentosite.cloud/sprite-foam-yoga-brick.html', + urlKey: 'sprite-foam-yoga-brick', media_gallery: null, custom_attributes: null, add_to_cart_allowed: null, diff --git a/src/components/ProductItem/ProductItem.tsx b/src/components/ProductItem/ProductItem.tsx index 036cb839..ed54538e 100644 --- a/src/components/ProductItem/ProductItem.tsx +++ b/src/components/ProductItem/ProductItem.tsx @@ -82,7 +82,7 @@ export const ProductItem: FunctionComponent = ({ }; const productUrl = setRoute - ? setRoute({ sku: productView?.sku }) + ? setRoute({ sku: productView?.sku, urlKey: productView?.urlKey }) : product?.canonical_url; return ( diff --git a/src/components/ProductList/MockData.ts b/src/components/ProductList/MockData.ts index be450c8b..1793aa13 100644 --- a/src/components/ProductList/MockData.ts +++ b/src/components/ProductList/MockData.ts @@ -13,6 +13,7 @@ const SimpleProduct = { sku: '24-WG088', name: 'Sprite Foam Roller', url: 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/sprite-foam-roller.html', + urlKey: 'sprite-foam-roller', images: [ { label: 'Image', @@ -49,6 +50,7 @@ const ComplexProduct = { sku: 'MSH06', name: 'Lono Yoga Short', url: 'http://master-7rqtwti-grxawiljl6f4y.us-4.magentosite.cloud/lono-yoga-short.html', + urlKey: 'lono-yoga-short', images: [ { label: '', diff --git a/src/types/interface.ts b/src/types/interface.ts index 70d2e0c3..ced39b53 100644 --- a/src/types/interface.ts +++ b/src/types/interface.ts @@ -50,7 +50,7 @@ export type BucketTypename = | 'StatsBucket' | 'CategoryView'; -export type RedirectRouteFunc = ({ sku }: { sku: string }) => string; +export type RedirectRouteFunc = ({ sku, urlKey }: { sku: string, urlKey: null | string }) => string; export interface MagentoHeaders { environmentId: string; @@ -212,6 +212,7 @@ export interface Product { }; gift_message_available: null | string; url: null | string; + urlKey: null | string; media_gallery: null | ProductViewMedia; custom_attributes: null | CustomAttribute; add_to_cart_allowed: null | boolean; From e3ce857d481a4b053e73ec2e52d554a630029f18 Mon Sep 17 00:00:00 2001 From: "Mark J. Becker" Date: Tue, 30 Jan 2024 13:39:14 +0100 Subject: [PATCH 02/10] Add image optimization feature --- README.md | 2 + dev-template.html | 2 + .../ProductItem/ProductItem.test.tsx | 34 +++++++++++---- src/components/ProductItem/ProductItem.tsx | 22 +++++----- src/types/interface.ts | 2 + src/utils/getProductImage.ts | 42 ++++++++++++++++++- 6 files changed, 85 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 87b96ef8..64234ce0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ const storeDetails = { displaySearchBox: true, displayOutOfStock: true, allowAllProducts: false, + optimizeImages: true, + imageBaseWidth: 200, }, context: { customerGroup: 'CUSTOMER_GROUP_CODE', diff --git a/dev-template.html b/dev-template.html index ef62690e..55109cb2 100644 --- a/dev-template.html +++ b/dev-template.html @@ -68,6 +68,8 @@ displaySearchBox: true, // display search box displayOutOfStock: true, allowAllProducts: false, + optimizeImages: true, + imageBaseWidth: 200, }, context: { customerGroup: '', diff --git a/src/components/ProductItem/ProductItem.test.tsx b/src/components/ProductItem/ProductItem.test.tsx index b7272f0b..2532cfd1 100644 --- a/src/components/ProductItem/ProductItem.test.tsx +++ b/src/components/ProductItem/ProductItem.test.tsx @@ -9,19 +9,39 @@ it. import { render } from '@testing-library/preact'; +import { StoreContextProvider } from '../../context'; import { sampleProductNotDiscounted } from './MockData'; import ProductItem from './ProductItem'; describe('WidgetSDK - UIKit/ProductItem', () => { test('renders', () => { + const context = { + environmentId: '', + environmentType: '', + websiteCode: '', + storeCode: '', + storeViewCode: '', + apiUrl: '', + apiKey: '', + config: { + optimizeImages: true, + imageBaseWidth: 200, + }, + context: {}, + route: undefined, + searchQuery: 'q', + }; + const { container } = render( - {}} - /> + + {}} + /> + ); const elem = container.querySelector('.ds-sdk-product-item'); diff --git a/src/components/ProductItem/ProductItem.tsx b/src/components/ProductItem/ProductItem.tsx index 036cb839..9a39a748 100644 --- a/src/components/ProductItem/ProductItem.tsx +++ b/src/components/ProductItem/ProductItem.tsx @@ -10,6 +10,7 @@ it. import { FunctionComponent } from 'preact'; import { useState } from 'preact/hooks'; +import { useStore } from '../../context'; import NoImage from '../../icons/NoImage.svg'; import { Product, @@ -18,7 +19,7 @@ import { RefinedProduct, } from '../../types/interface'; import { SEARCH_UNIT_ID } from '../../utils/constants'; -import { getProductImageURL } from '../../utils/getProductImage'; +import { generateOptimizedImages, getProductImageURL } from '../../utils/getProductImage'; import { htmlStringDecode } from '../../utils/htmlStringDecode'; import { SwatchButtonGroup } from '../SwatchButtonGroup'; import ProductPrice from './ProductPrice'; @@ -42,6 +43,7 @@ export const ProductItem: FunctionComponent = ({ const [selectedSwatch, setSelectedSwatch] = useState(''); const [productImages, setImages] = useState(); const [refinedProduct, setRefinedProduct] = useState(); + const { config: { optimizeImages, imageBaseWidth } } = useStore(); const handleSelection = async (optionIds: string[], sku: string) => { const data = await refineProduct(optionIds, sku); @@ -55,9 +57,14 @@ export const ProductItem: FunctionComponent = ({ return selected; }; - const productImage = getProductImageURL( + let productImageSrc = getProductImageURL( productImages ? productImages ?? [] : productView.images ?? [] ); // get image for PLP + let productImageSrcset : string[] = []; + + if (optimizeImages) { + [productImageSrc, productImageSrcset] = generateOptimizedImages(productImageSrc, imageBaseWidth ?? 200); + }; // will have to figure out discount logic for amount_off and percent_off still const discount: boolean = refinedProduct @@ -94,16 +101,11 @@ export const ProductItem: FunctionComponent = ({ >
- {/* - NOTE: - we could use for breakpoint based img file - in future for better performance - */} - {productImage ? ( + {productImageSrc ? (
{productView.name} { ''; return imageUrl ? `${protocol}//${imageUrl}` : ''; -}; +} + +export interface ResolveImageUrlOptions { + width: number; + height?: number; + auto?: string; + quality?: number; + crop?: boolean; + fit?: string; +} + +const resolveImageUrl = (url: string, opts: ResolveImageUrlOptions) : string => { + const [base, query] = url.split('?'); + const params = new URLSearchParams(query); + + Object.entries(opts).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.set(key, String(value)); + } + }); + + return `${base}?${params.toString()}`; +} + +const generateOptimizedImages = (imageUrl: string, baseImageWidth: number): [string, string[]] => { + const baseOptions = { + fit: 'cover', + crop: false, + dpi: 1, + }; + + const src = resolveImageUrl(imageUrl, { ...baseOptions, width: baseImageWidth }); + const dpiSet = [1, 2, 3]; + const srcset = dpiSet.map((dpi) => { + return `${resolveImageUrl(imageUrl, { ...baseOptions, auto: 'webp', quality: 80, width: baseImageWidth * dpi })} ${dpi}x`; + }); + + return [src, srcset]; +} -export { getProductImageURL }; +export { getProductImageURL, generateOptimizedImages }; From 719a2d6fe11b6277790e006cf6db80a9d956d6c3 Mon Sep 17 00:00:00 2001 From: "Mark J. Becker" Date: Mon, 11 Mar 2024 17:43:08 +0100 Subject: [PATCH 03/10] Replace MSE SDK with direct access to ACDL --- dev-template.html | 70 ++++++------ src/api/search.ts | 22 ++-- src/components/ProductItem/ProductItem.tsx | 14 ++- src/context/events.tsx | 123 ++++++++++----------- src/types/globals.d.ts | 4 +- 5 files changed, 113 insertions(+), 120 deletions(-) diff --git a/dev-template.html b/dev-template.html index 93a5f19b..adba2697 100644 --- a/dev-template.html +++ b/dev-template.html @@ -13,6 +13,11 @@ referrerpolicy="no-referrer" /> + + ``` @@ -90,71 +90,81 @@ Most of these will be passed with the extension if you have your storefront setu #### Store Variables needed: -``` - ENVIRONMENT_ID - WEBSITE_CODE - STORE_CODE - STORE_VIEW_CODE - CUSTOMER_GROUP_CODE - API_KEY - SANDBOX_KEY // input this key into webpack.dev.js & webpack.prod.js +```sh +ENVIRONMENT_ID +WEBSITE_CODE +STORE_CODE +STORE_VIEW_CODE +CUSTOMER_GROUP_CODE +API_KEY +SANDBOX_KEY # input this key into webpack.dev.js & webpack.prod.js ``` - To set up sandbox keys please see here: https://experienceleague.adobe.com/docs/commerce-merchant-services/catalog-service/installation.html?lang=en #### insert into store details config -``` +```ts const storeDetails = { - environmentId: 'ENVIRONMENT_ID', - websiteCode: 'WEBSITE_CODE', - storeCode: 'STORE_CODE', - storeViewCode: 'STORE_VIEW_CODE', - config: { - minQueryLength: '2', - pageSize: 8, - perPageConfig: { - pageSizeOptions: '12,24,36', - defaultPageSizeOption: '24', - }, - currencySymbol: '$', - currencyRate: '1', - displaySearchBox: true, - displayOutOfStock: true, - allowAllProducts: false, - currentCategoryUrlPath?: string; - categoryName: '', // name of category to display - displaySearchBox: false, // display search box - displayOutOfStock: '', // "1" will return from php escapeJs and boolean is returned if called from data-service-graphql - displayMode: '', // "" for search || "PAGE" for category search - locale: '', //add locale for translations - priceSlider: false, //enable slider for price - EXPERIMENTAL, default is false - imageCarousel: false, //enable multiple image carousel - EXPERIMENTAL, default is false - listview: false; //add listview as an option - EXPERIMENTAL, default is false - optimizeImages: true, // optimize images with Fastly - imageBaseWidth: 200, - resolveCartId?: resolveCartId // Luma specific addToCart method. Enabled with the extension - refreshCart?: refreshCart // Luma specific addToCart method. Enabled with the extension - addToCart?: (sku, options, quantity)=>{} // custom add to cart callback function. Called on addToCart action - }, - context: { - customerGroup: 'CUSTOMER_GROUP_CODE', - }, - apiKey: 'API_KEY', - }; + environmentId: 'ENVIRONMENT_ID', + websiteCode: 'WEBSITE_CODE', + storeCode: 'STORE_CODE', + storeViewCode: 'STORE_VIEW_CODE', + config: { + minQueryLength: '2', + pageSize: 8, + perPageConfig: { + pageSizeOptions: '12,24,36', + defaultPageSizeOption: '24', + }, + currencySymbol: '$', + currencyRate: '1', + allowAllProducts: false, + currentCategoryUrlPath: "string", + /* name of category to display */ + categoryName: '', + /* display search box */ + displaySearchBox: false, + /* '1' will return from php escapeJs and boolean is returned if called from data-service-graphql */ + displayOutOfStock: '', + /* "" for search || "PAGE" for category search */ + displayMode: '', + /* add locale for translations */ + locale: '', + /* enable slider for price - EXPERIMENTAL, default is false */ + priceSlider: false, + /* enable multiple image carousel - EXPERIMENTAL, default is false */ + imageCarousel: false, + /* add listview as an option - EXPERIMENTAL, default is false */ + listview: false, + /* optimize images with Fastly */ + optimizeImages: true, + imageBaseWidth: 200, + /* Luma specific addToCart method. Enabled with the extension */ + resolveCartId: resolveCartId(), + /* Luma specific addToCart method. Enabled with the extension */ + refreshCart: refreshCart(), + /* custom add to cart callback function. Called on addToCart action */ + addToCart: (sku, options, quantity) => {}, + }, + context: { + customerGroup: 'CUSTOMER_GROUP_CODE', + }, + apiKey: 'API_KEY', +}; ``` Append LiveSearchPLP to the storefront window: -``` +```js const root = document.querySelector('div.search-plp-root'); setTimeout(async () => { - while (typeof window.LiveSearchPLP !== 'function') { - console.log('waiting for window.LiveSearchPLP to be available'); - await new Promise((resolve) => setTimeout(resolve, 500)); - } - window.LiveSearchPLP({ storeDetails, root }); + while (typeof window.LiveSearchPLP !== 'function') { + console.log('waiting for window.LiveSearchPLP to be available'); + await new Promise((resolve) => setTimeout(resolve, 500)); + } + window.LiveSearchPLP({ storeDetails, root }); }, 1000); ``` @@ -172,7 +182,7 @@ So how do we use CSS variables to style our components? Great question 😄 Let's say as if I want to style an element with the theme's primary color. Normally, in CSS we would have done the following: -``` +```tsx