From 132245f4185dad9a4d68b79b5e0879b85ef8a33e Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Mon, 6 Apr 2026 14:00:58 -0700 Subject: [PATCH 01/11] commiting entire print-product-detail-sdk block --- .../components/Contexts.js | 72 + .../components/CustomizationInputs.js | 857 ++++++++++++ .../components/ProductComponents.js | 830 ++++++++++++ .../components/useSeo.js | 198 +++ .../print-product-detail-sdk.css | 1186 +++++++++++++++++ .../print-product-detail-sdk.js | 143 ++ .../print-product-detail-sdk/sdk/index.d.ts | 418 ++++++ .../print-product-detail-sdk/sdk/index.js | 6 + .../utilities/sample_state.json | 275 ++++ .../utilities/utility-functions.js | 152 +++ .../code/scripts/vendors/build-htm-preact.js | 34 + express/code/scripts/vendors/htm-preact.js | 1 + package.json | 3 +- 13 files changed, 4174 insertions(+), 1 deletion(-) create mode 100644 express/code/blocks/print-product-detail-sdk/components/Contexts.js create mode 100644 express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js create mode 100644 express/code/blocks/print-product-detail-sdk/components/ProductComponents.js create mode 100644 express/code/blocks/print-product-detail-sdk/components/useSeo.js create mode 100644 express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css create mode 100644 express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js create mode 100644 express/code/blocks/print-product-detail-sdk/sdk/index.d.ts create mode 100644 express/code/blocks/print-product-detail-sdk/sdk/index.js create mode 100644 express/code/blocks/print-product-detail-sdk/utilities/sample_state.json create mode 100644 express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js create mode 100644 express/code/scripts/vendors/build-htm-preact.js create mode 100644 express/code/scripts/vendors/htm-preact.js diff --git a/express/code/blocks/print-product-detail-sdk/components/Contexts.js b/express/code/blocks/print-product-detail-sdk/components/Contexts.js new file mode 100644 index 000000000..5c3066d3c --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/components/Contexts.js @@ -0,0 +1,72 @@ +import { html, createContext, useContext, useMemo, useSyncExternalStore, useEffect, useCallback, useState } from '../../../scripts/vendors/htm-preact.js'; + +export const StoreContext = createContext(null); + +export function StoreProvider({ children, sdkStore }) { + const state = useSyncExternalStore( + sdkStore.subscribe.bind(sdkStore), + sdkStore.getSnapshot.bind(sdkStore), + ); + + const actions = useMemo(() => sdkStore, [sdkStore]); + + const value = useMemo(() => ({ + state, + actions, + env: sdkStore.env, + sdk: sdkStore, + }), [state, actions, sdkStore]); + + return html` + <${StoreContext.Provider} value=${value}> + ${children} + + `; +} + +export function useStore() { + const value = useContext(StoreContext); + if (!value) { + throw new Error('useStore must be used within a StoreProvider'); + } + return value; +} +const DrawerContext = createContext(null); + +export function DrawerProvider({ children }) { + const [drawerState, setDrawerState] = useState({ open: false, type: null, payload: null }); + + const openDrawer = useCallback((nextState) => { + setDrawerState({ open: true, ...nextState }); + document.body.classList.add('disable-scroll'); + }, []); + + const closeDrawer = useCallback(() => { + setDrawerState({ open: false, type: null, payload: null }); + document.body.classList.remove('disable-scroll'); + }, []); + + const value = useMemo(() => ({ + state: drawerState, + openDrawer, + closeDrawer, + }), [drawerState, openDrawer, closeDrawer]); + + useEffect(() => () => { + document.body.classList.remove('disable-scroll'); + }, []); + + return html` + <${DrawerContext.Provider} value=${value}> + ${children} + + `; +} + +export function useDrawer() { + const ctx = useContext(DrawerContext); + if (!ctx) { + throw new Error('useDrawer must be used within a DrawerProvider'); + } + return ctx; +} diff --git a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js new file mode 100644 index 000000000..8fee59450 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js @@ -0,0 +1,857 @@ +import { + html, + useEffect, + useRef, +} from '../../../scripts/vendors/htm-preact.js'; +import { useStore } from './Contexts.js'; +import createSimpleCarousel from '../../../scripts/widgets/simple-carousel.js'; +import { createPicker } from '../../../scripts/widgets/picker.js'; +import { trackPrintAddonOptionSelect } from '../../../scripts/instrument.js'; +import { debounce } from '../../../scripts/utils/hofs.js'; + +const debouncedTrackOptionSelect = debounce((payload) => { + trackPrintAddonOptionSelect(payload).catch(() => { }); +}, 250); + +function toDomIdPart(value) { + return String(value || '') + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function positionTooltip(target, tooltipText) { + const pill = target.getBoundingClientRect(); + const pillTop = pill.top; + const tooltipWidth = (tooltipText.length * 3) + 12; + const pillCenter = pill.left + (pill.width / 2); + const drawer = target.closest('.pdpx-drawer'); + const drawerOffsetLeft = drawer ? drawer.getBoundingClientRect().left : 0; + target.style.setProperty('--tooltip-top', `${pillTop - 42}px`); + target.style.setProperty('--tooltip-left', `${pillCenter - tooltipWidth - drawerOffsetLeft}px`); + target.style.setProperty('--arrow-top', `${pillTop - 6}px`); + target.style.setProperty('--arrow-left', `${pillCenter - drawerOffsetLeft}px`); +} + +function getNextRadioIndex(currentIndex, key, maxIndex) { + switch (key) { + case 'ArrowRight': + case 'ArrowDown': + return currentIndex + 1 > maxIndex ? 0 : currentIndex + 1; + case 'ArrowLeft': + case 'ArrowUp': + return currentIndex - 1 < 0 ? maxIndex : currentIndex - 1; + case 'Home': + return 0; + case 'End': + return maxIndex; + default: + return currentIndex; + } +} + +export function CheckboxSelector({ attribute }) { + const { actions } = useStore(); + const { selector, selectedOptionValue, name } = attribute; + const isChecked = selectedOptionValue === selector.checkedValue; + const checkboxId = `pdpx-checkbox-${toDomIdPart(name)}`; + + const handleChange = () => { + const nextValue = isChecked + ? selector.uncheckedValue + : selector.checkedValue; + actions.selectOption(name, nextValue); + }; + + return html` +
+ +
+ `; +} + +export function DropdownSelector({ attribute, onRequestDrawer, productType }) { + const { actions } = useStore(); + const { selector, selectedOptionValue, title, helpLink } = attribute; + const pickerHostRef = useRef(null); + const pickerRef = useRef(null); + const pickerIdRef = useRef(`pdpx-picker-${attribute.name}`); + const options = selector.options || []; + const optionsSignature = options + .map((option) => `${option.value}:${option.title}:${option.priceDelta || ''}`) + .join('|'); + useEffect(() => { + let cancelled = false; + async function mountPicker() { + if (!pickerHostRef.current) { + return; + } + if (pickerRef.current?.destroy) { + pickerRef.current.destroy(); + pickerRef.current = null; + } + pickerHostRef.current.innerHTML = ''; + const pickerOptions = options.map((option) => ({ + value: option.value, + text: `${option.title}${option.priceDelta ? ` ${option.priceDelta}` : ''}`, + })); + const picker = await createPicker({ + id: pickerIdRef.current, + name: attribute.name, + label: title, + labelPosition: 'side', + options: pickerOptions, + defaultValue: selectedOptionValue, + onChange: (value) => { + actions.selectOption(attribute.name, value); + debouncedTrackOptionSelect({ + attributeName: attribute.name, + actionValue: value, + productType, + }); + }, + }); + if (cancelled || !pickerHostRef.current) { + picker?.destroy?.(); + return; + } + pickerHostRef.current.appendChild(picker); + pickerRef.current = picker; + } + mountPicker(); + return () => { + cancelled = true; + if (pickerRef.current?.destroy) { + pickerRef.current.destroy(); + pickerRef.current = null; + } + if (pickerHostRef.current) { + pickerHostRef.current.innerHTML = ''; + } + }; + }, [attribute.name, title, optionsSignature]); + useEffect(() => { + if (!pickerRef.current?.getPicker || !pickerRef.current?.setPicker) { + return; + } + if (String(pickerRef.current.getPicker()) !== String(selectedOptionValue)) { + pickerRef.current.setPicker(selectedOptionValue); + } + }, [selectedOptionValue]); + const hasDrawerLink = typeof onRequestDrawer === 'function' + && helpLink?.type === 'dialog' + && helpLink.dialogType; + + const triggerDrawer = () => { + if (hasDrawerLink) { + onRequestDrawer({ + type: helpLink.dialogType, + payload: { attribute, helpLink }, + }); + } + }; + + return html` +
+ + `; +} + +export function QuantitySelector() { + const { state, actions } = useStore(); + const pickerHostRef = useRef(null); + const pickerRef = useRef(null); + const pickerIdRef = useRef('pdpx-picker-qty'); + + if (!state) { + return null; + } + + const { quantity, quantityOptions, productType } = state; + const optionsSignature = quantityOptions + .map((option) => `${option.quantity}:${option.label}:${option.discount || ''}`) + .join('|'); + + useEffect(() => { + let cancelled = false; + + async function mountPicker() { + if (!pickerHostRef.current) { + return; + } + + if (pickerRef.current?.destroy) { + pickerRef.current.destroy(); + pickerRef.current = null; + } + + pickerHostRef.current.innerHTML = ''; + const pickerOptions = quantityOptions.map((option) => ({ + value: String(option.quantity), + text: `${option.label}${option.discount ? ` (Save ${option.discount})` : ''}`, + })); + + const picker = await createPicker({ + id: pickerIdRef.current, + name: 'qty', + label: 'Quantity', + labelPosition: 'side', + options: pickerOptions, + defaultValue: String(quantity), + onChange: (value) => { + const nextQuantity = parseInt(value, 10); + if (!Number.isNaN(nextQuantity)) { + actions.selectQuantity(nextQuantity); + debouncedTrackOptionSelect({ + attributeName: 'qty', + actionValue: value, + productType, + }); + } + }, + }); + + if (cancelled || !pickerHostRef.current) { + picker?.destroy?.(); + return; + } + + pickerHostRef.current.appendChild(picker); + pickerRef.current = picker; + } + + mountPicker(); + + return () => { + cancelled = true; + if (pickerRef.current?.destroy) { + pickerRef.current.destroy(); + pickerRef.current = null; + } + if (pickerHostRef.current) { + pickerHostRef.current.innerHTML = ''; + } + }; + }, [optionsSignature]); + + useEffect(() => { + if (!pickerRef.current?.getPicker || !pickerRef.current?.setPicker) { + return; + } + if (String(pickerRef.current.getPicker()) !== String(quantity)) { + pickerRef.current.setPicker(String(quantity)); + } + }, [quantity]); + + return html` +
+
+
+ `; +} + +export function RadioSelector({ attribute }) { + const { actions } = useStore(); + const { selector, selectedOptionValue, name, title } = attribute; + const groupLabelId = `pdpx-radio-group-label-${toDomIdPart(name)}`; + + const handleChange = (value) => { + if (value !== selectedOptionValue) { + actions.selectOption(name, value); + } + }; + + return html` +
+ +
+ ${selector.options.map( + (option) => html` + + `, + )} +
+
+ `; +} + +function updateImageUrl(url, maxDim = 54) { + try { + const urlObj = new URL(url); + urlObj.searchParams.set('max_dim', String(maxDim)); + return urlObj.toString(); + } catch { + return url; + } +} + +function flattenOptionGroups(selector) { + if (!selector.optionGroups || !Array.isArray(selector.optionGroups)) { + return []; + } + return selector.optionGroups.flatMap((group) => (group.options || []).map((option) => ({ + ...option, + groupTitle: group.title, + }))); +} + +function buildPillElement(option, isSelected, index, setSize, activeIndex, handlers) { + const { handleOptionClick, handleMiniPillKeyDown } = handlers; + const thumbnailUrl = updateImageUrl(option.imageUrl, 48); + + const pillContainer = document.createElement('div'); + pillContainer.className = 'pdpx-mini-pill-container'; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = `pdpx-mini-pill-image-container ${isSelected ? 'selected' : ''}`; + button.setAttribute('data-name', option.value); + button.setAttribute('data-title', option.title); + button.setAttribute('role', 'radio'); + button.setAttribute('aria-current', isSelected ? 'true' : 'false'); + button.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + button.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); + button.setAttribute('aria-posinset', String(index + 1)); + button.setAttribute('aria-setsize', String(setSize)); + button.setAttribute('aria-label', `${option.title}${option.priceDelta ? ` ${option.priceDelta}` : ''}`); + button.setAttribute('tabindex', index === activeIndex ? '0' : '-1'); + button.addEventListener('click', () => handleOptionClick(option)); + button.addEventListener('keydown', handleMiniPillKeyDown); + + const img = document.createElement('img'); + img.className = 'pdpx-mini-pill-image'; + img.src = thumbnailUrl; + img.alt = ''; + img.setAttribute('aria-hidden', 'true'); + button.appendChild(img); + + pillContainer.addEventListener('mouseenter', (event) => { + positionTooltip(event.currentTarget, option.title); + }); + + const textContainer = document.createElement('div'); + textContainer.className = 'pdpx-mini-pill-text-container'; + if (option.priceDelta) { + const priceSpan = document.createElement('span'); + priceSpan.className = 'pdpx-mini-pill-price'; + priceSpan.textContent = option.priceDelta; + textContainer.appendChild(priceSpan); + } + + pillContainer.appendChild(button); + pillContainer.appendChild(textContainer); + return pillContainer; +} + +/** + * Mini-pill carousel built imperatively to work with createSimpleCarousel. + * The carousel mutates the DOM (moves children into a platform, adds faders), + * which conflicts with Preact's reconciliation. By building the pills in useEffect + * and not rendering them via Preact, we avoid reconciliation conflicts. + */ +function MiniPillCarousel({ attribute, onRequestDrawer, productType }) { + const containerRef = useRef(null); + const carouselCleanupsRef = useRef([]); + const { actions } = useStore(); + const { selector, selectedOptionValue, title } = attribute; + let { helpLink } = attribute; + const allOptions = flattenOptionGroups(selector); + const isSegmented = selector.optionGroups?.length > 1; + const selectedOption = allOptions.find((option) => option.value === selectedOptionValue) + || allOptions[0]; + const selectedOptionTitle = isSegmented && selectedOption?.groupTitle + ? `${selectedOption.title} (${selectedOption.groupTitle})` + : (selectedOption?.title || ''); + const groupLabelId = `pdpx-mini-pill-label-${toDomIdPart(attribute.name)}`; + const groupValueId = `pdpx-mini-pill-selected-value-${toDomIdPart(attribute.name)}`; + const isBusinessCardMediaAttribute = attribute.name === 'media' && productType === 'zazzle_businesscard'; + if (isBusinessCardMediaAttribute) { + helpLink = { + type: 'dialog', + dialogType: 'paperType', + label: 'Compare Paper Types', + }; + } + const isShirtColorAttribute = attribute.name === 'color' + && (productType === 'zazzle_shirt' || productType === 'zazzle_hoodie'); + if (isShirtColorAttribute) { + helpLink = { + type: 'dialog', + dialogType: 'printingProcess', + label: 'Learn More', + }; + } + const handleOptionClick = (option) => { + actions.selectOption(attribute.name, option.value); + debouncedTrackOptionSelect({ + attributeName: attribute.name, + actionValue: option.value, + productType, + }); + }; + + const handleMiniPillKeyDown = (event) => { + const { key, currentTarget } = event; + if (!['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) { + return; + } + const buttons = Array.from( + containerRef.current?.querySelectorAll('.pdpx-mini-pill-image-container') || [], + ); + if (!buttons.length) { + return; + } + const currentIndex = buttons.indexOf(currentTarget); + if (currentIndex < 0) { + return; + } + event.preventDefault(); + const nextIndex = getNextRadioIndex(currentIndex, key, buttons.length - 1); + const nextButton = buttons[nextIndex]; + nextButton?.focus(); + nextButton?.click(); + }; + + const triggerDrawer = () => { + if (helpLink) { + onRequestDrawer({ + type: helpLink.dialogType, + payload: { attribute, helpLink }, + }); + } + }; + + // Build pills imperatively and initialize carousel (avoids Preact reconciliation conflict) + useEffect(() => { + if (!containerRef.current || !allOptions.length) { + return undefined; + } + + const container = containerRef.current; + container.innerHTML = ''; + const handlers = { handleOptionClick, handleMiniPillKeyDown }; + + if (isSegmented) { + const sectionsWrapper = document.createElement('div'); + sectionsWrapper.className = 'pdpx-mini-pill-sections-container'; + + const carouselTargets = []; + selector.optionGroups.forEach((group) => { + const sectionContainer = document.createElement('div'); + sectionContainer.className = 'pdpx-mini-pill-section-container'; + + if (group.title) { + const sectionLabel = document.createElement('span'); + sectionLabel.className = 'pdpx-pill-selector-section-label'; + sectionLabel.textContent = group.title; + sectionContainer.appendChild(sectionLabel); + } + + const sectionOptions = document.createElement('div'); + sectionOptions.className = 'pdpx-mini-pill-section-options-container'; + sectionOptions.setAttribute('role', 'radiogroup'); + sectionOptions.setAttribute('aria-orientation', 'horizontal'); + sectionOptions.setAttribute('aria-label', group.title || `${title} options`); + + const groupOptions = group.options || []; + const groupSelectedIndex = groupOptions.findIndex( + (option) => option.value === selectedOptionValue, + ); + const groupActiveIndex = groupSelectedIndex >= 0 ? groupSelectedIndex : 0; + + groupOptions.forEach((option, index) => { + const isSelected = option.value === selectedOptionValue; + const pill = buildPillElement( + option, + isSelected, + index, + groupOptions.length, + groupActiveIndex, + handlers, + ); + sectionOptions.appendChild(pill); + }); + + sectionContainer.appendChild(sectionOptions); + sectionsWrapper.appendChild(sectionContainer); + carouselTargets.push(sectionOptions); + }); + + container.appendChild(sectionsWrapper); + + Promise.all( + carouselTargets.map((target) => createSimpleCarousel( + '.pdpx-mini-pill-container', + target, + { ariaLabel: `${title} options`, centerActive: true, activeClass: 'selected' }, + )), + ).then((carousels) => { + carouselCleanupsRef.current = carousels.filter(Boolean).map((c) => c.cleanup); + }); + } else { + const selectedIndex = allOptions.findIndex( + (option) => option.value === selectedOptionValue, + ); + const activeIndex = selectedIndex >= 0 ? selectedIndex : 0; + + allOptions.forEach((option, index) => { + const isSelected = option.value === selectedOptionValue; + const pill = buildPillElement( + option, + isSelected, + index, + allOptions.length, + activeIndex, + handlers, + ); + container.appendChild(pill); + }); + + createSimpleCarousel('.pdpx-mini-pill-container', container, { + ariaLabel: `${title} options`, + centerActive: true, + activeClass: 'selected', + }).then((carousel) => { + if (carousel) { + carouselCleanupsRef.current = [carousel.cleanup]; + } + }); + } + + return () => { + carouselCleanupsRef.current.forEach((fn) => fn()); + carouselCleanupsRef.current = []; + }; + }, [ + attribute.name, + title, + selector.optionGroups?.map((g) => (g.options || []).map((o) => o.value).join(',')).join('|'), + ]); + + // Update selected state when selection changes + useEffect(() => { + if (!containerRef.current) return; + const buttons = containerRef.current.querySelectorAll('.pdpx-mini-pill-image-container'); + let hasSelected = false; + buttons.forEach((btn) => { + const value = btn.getAttribute('data-name'); + const isSelected = value === selectedOptionValue; + if (isSelected) { + hasSelected = true; + } + btn.classList.toggle('selected', isSelected); + btn.setAttribute('aria-current', isSelected ? 'true' : 'false'); + btn.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + btn.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); + btn.setAttribute('tabindex', isSelected ? '0' : '-1'); + }); + if (!hasSelected && buttons.length > 0) { + buttons[0].setAttribute('tabindex', '0'); + } + }, [selectedOptionValue]); + + return html` +
+
+
+ ${title}: + ${selectedOptionTitle} +
+ ${helpLink + && html` + + `} +
+
+
+ `; +} + +export function ThumbnailSelector({ attribute, onRequestDrawer, productType }) { + const { actions } = useStore(); + const { selector, selectedOptionValue, title, helpLink } = attribute; + const groupLabelId = `pdpx-pill-label-${toDomIdPart(attribute.name)}`; + + const allOptions = flattenOptionGroups(selector); + + if (!allOptions.length) { + return null; + } + + const isMiniPill = attribute.name === 'color' || attribute.name === 'media'; + + const handleOptionClick = (option) => { + if (option.value !== selectedOptionValue) { + actions.selectOption(attribute.name, option.value); + debouncedTrackOptionSelect({ + attributeName: attribute.name, + actionValue: option.value, + productType, + }); + } + }; + + const selectedIndex = allOptions.findIndex((option) => option.value === selectedOptionValue); + const activeIndex = selectedIndex >= 0 ? selectedIndex : 0; + const handleThumbnailKeyDown = (event) => { + const { key, currentTarget } = event; + if (!['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) { + return; + } + const currentIndex = Number(currentTarget.getAttribute('data-index')); + if (Number.isNaN(currentIndex) || !allOptions.length) { + return; + } + event.preventDefault(); + const nextIndex = getNextRadioIndex(currentIndex, key, allOptions.length - 1); + const nextOption = allOptions[nextIndex]; + if (!nextOption) { + return; + } + const radioGroup = currentTarget.closest('[role="radiogroup"]'); + const nextButton = radioGroup?.querySelector(`[data-index="${nextIndex}"]`); + handleOptionClick(nextOption); + nextButton?.focus(); + }; + + const triggerDrawer = () => { + if (helpLink && typeof onRequestDrawer === 'function') { + onRequestDrawer({ + type: helpLink.dialogType, + payload: { attribute, helpLink }, + }); + } + }; + + if (isMiniPill) { + return html`<${MiniPillCarousel} attribute=${attribute} onRequestDrawer=${onRequestDrawer} productType=${productType} />`; + } + + return html` +
+
+ ${title} + ${helpLink?.type === 'dialog' && helpLink.dialogType && html` + + `} +
+
+ ${selector.optionGroups?.map( + (group) => html` +
+ ${group.title + && html`
${group.title}
`} + ${(group.options || []).map((option) => { + const thumbnailUrl = updateImageUrl(option.imageUrl); + const isSelected = option.value === selectedOptionValue; + const optionIndex = allOptions.findIndex((candidate) => candidate.value === option.value); + return html` + + `; + })} +
+ `, + )} +
+ ${selector.preview + && html` +
+ ${selector.preview.optionTitle} +
+
+ `} +
+ `; +} + +function renderAttribute(attribute, onRequestDrawer, key, productType) { + switch (attribute.selector.type) { + case 'thumbnails': + return html`<${ThumbnailSelector} + key=${key} + attribute=${attribute} + onRequestDrawer=${onRequestDrawer} + productType=${productType} + />`; + case 'dropdown': + return html`<${DropdownSelector} + key=${key} + attribute=${attribute} + onRequestDrawer=${onRequestDrawer} + productType=${productType} + />`; + case 'radio': + return html`<${DropdownSelector} + key=${key} + attribute=${attribute} + onRequestDrawer=${onRequestDrawer} + productType=${productType} + />`; + case 'checkbox': + return html`<${DropdownSelector} + key=${key} + attribute=${attribute} + onRequestDrawer=${onRequestDrawer} + productType=${productType} + />`; + default: + return null; + } +} + +const ATTRIBUTE_ORDER = { + zazzle_businesscard: ['style', 'cornerstyle', 'media'], + zazzle_shirt: ['style', 'color', 'size'], +}; + +function sortAttributes(attributes, productType) { + const order = ATTRIBUTE_ORDER[productType]; + if (!order) return attributes; + return [...attributes].sort((a, b) => { + const indexA = order.indexOf(a.name); + const indexB = order.indexOf(b.name); + const posA = indexA >= 0 ? indexA : order.length; + const posB = indexB >= 0 ? indexB : order.length; + return posA - posB; + }); +} + +export function CustomizationInputs({ onRequestDrawer, productType }) { + const { state } = useStore(); + if (!state) { + return null; + } + const productAttributes = sortAttributes( + (state.attributes || []).filter((attribute) => attribute.name !== 'quantity'), + productType, + ); + return html` +
+
+ ${productAttributes.map((attribute) => renderAttribute( + attribute, + onRequestDrawer, + attribute.name, + productType, + ))} + <${QuantitySelector} /> +
+
+ `; +} diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js new file mode 100644 index 000000000..e88168722 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -0,0 +1,830 @@ +import { + html, + useEffect, + useRef, + useState, + Fragment, + useCallback, +} from '../../../scripts/vendors/htm-preact.js'; +import { useStore, useDrawer } from './Contexts.js'; +import axAccordionDecorate from '../../ax-accordion/ax-accordion.js'; +import { formatLargeNumberToK } from '../utilities/utility-functions.js'; +import createSimpleCarousel from '../../../scripts/widgets/simple-carousel.js'; + +function mapToAccordionFormat(descriptions) { + if (!descriptions || !Array.isArray(descriptions)) { + return []; + } + return descriptions.map((item) => ({ + title: item.attributeTitle, + content: item.descriptionHTML, + })); +} + +export function ProductDetails() { + const { state } = useStore(); + const accordionRef = useRef(null); + const previousDescriptionsRef = useRef(null); + useEffect(() => { + if (!state || !accordionRef.current) { + return; + } + const descriptions = state.descriptionComponents; + if (!descriptions) { + return; + } + const accordionData = mapToAccordionFormat(descriptions); + if (!accordionRef.current.accordionData) { + accordionRef.current.accordionData = accordionData; + axAccordionDecorate(accordionRef.current); + } else { + const previousDescriptions = previousDescriptionsRef.current; + if (previousDescriptions !== descriptions && accordionRef.current.updateAccordion) { + let forceExpandTitle = null; + if (previousDescriptions && Array.isArray(previousDescriptions)) { + const prevTitles = previousDescriptions.map((entry) => entry.attributeTitle); + const currentTitles = descriptions.map((entry) => entry.attributeTitle); + const changedIndex = currentTitles + .findIndex((title, index) => prevTitles[index] !== title); + if (changedIndex >= 0) { + forceExpandTitle = descriptions[changedIndex].attributeTitle; + } + } + accordionRef.current.updateAccordion(accordionData, forceExpandTitle); + } + } + previousDescriptionsRef.current = descriptions; + }, [state]); + + if (!state) { + return null; + } + + return html` +
+
+ Product Details +
+
+
+ `; +} + +export function ProductHeader() { + const { state } = useStore(); + const [tooltipVisible, setTooltipVisible] = useState(false); + const showTooltip = useCallback(() => setTooltipVisible(true), []); + const hideTooltip = useCallback(() => setTooltipVisible(false), []); + + if (!state) { + return null; + } + + const { title, pricing, shippingEstimate, reviewsStats } = state; + const reviewsCount = reviewsStats?.count || 0; + const reviewsRating = reviewsStats?.rating || 0; + const formattedRating = reviewsRating ? Math.round(reviewsRating * 10) / 10 : ''; + const formattedCount = reviewsCount ? formatLargeNumberToK(reviewsCount) : ''; + + return html` +
+
+
+
+

${title}

+
+ ${reviewsRating > 0 && html` +
+ +
+ +
+
+ +
+
+ `} +
+
+
+ ${pricing.totalPrice} + ${pricing.originalTotalPrice !== pricing.totalPrice && html` + ${pricing.originalTotalPrice} + `} + ${pricing.showCompValue && html` + Comp. value +
+ + ${tooltipVisible && html` + + `} +
+ `} +
+ ${pricing.discountLabel && html` + ${pricing.discountLabel} + `} +
+
+ ${shippingEstimate && html` +
+ + Estimated Delivery + ${shippingEstimate} +
+ `} +
+ `; +} + +function updateImageUrl(url, maxDim = 644) { + try { + const urlObj = new URL(url); + urlObj.searchParams.set('max_dim', String(maxDim)); + return urlObj.toString(); + } catch { + return url; + } +} + +export function ProductImages() { + const { state, actions } = useStore(); + const containerRef = useRef(null); + const carouselCleanupRef = useRef(null); + + if (!state || !state.selectedRealview) { + return null; + } + + const { realviews = [], selectedRealview } = state; + const heroImageUrl = updateImageUrl(selectedRealview.url, 644); + + const handleThumbnailClick = (realview) => { + actions.selectRealview(realview.id); + }; + + // Build thumbnails imperatively to work with createSimpleCarousel + useEffect(() => { + if (!containerRef.current || !realviews.length) return undefined; + + const container = containerRef.current; + container.innerHTML = ''; + + realviews.forEach((realview) => { + const thumbnailUrl = updateImageUrl(realview.url, 76); + const isSelected = realview.id === selectedRealview.id; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = `pdpx-image-thumbnail-carousel-item ${isSelected ? 'selected' : ''}`; + button.setAttribute('data-image-type', realview.id); + button.addEventListener('click', () => handleThumbnailClick(realview)); + + const img = document.createElement('img'); + img.className = 'pdpx-image-thumbnail-carousel-item-image'; + img.src = thumbnailUrl; + img.alt = realview.title; + img.setAttribute('data-image-type', realview.id); + button.appendChild(img); + + container.appendChild(button); + }); + + createSimpleCarousel('.pdpx-image-thumbnail-carousel-item', container, { + ariaLabel: 'Product image thumbnails', + centerActive: true, + activeClass: 'selected', + }).then((carousel) => { + if (carousel) { + carouselCleanupRef.current = carousel.cleanup; + } + }); + + return () => { + if (carouselCleanupRef.current) { + carouselCleanupRef.current(); + carouselCleanupRef.current = null; + } + }; + }, [realviews.map((r) => r.id).join(',')]); + + // Update selected state when selection changes + useEffect(() => { + if (!containerRef.current) return; + const buttons = containerRef.current.querySelectorAll('.pdpx-image-thumbnail-carousel-item'); + buttons.forEach((btn) => { + const id = btn.getAttribute('data-image-type'); + const isSelected = id === selectedRealview.id; + btn.classList.toggle('selected', isSelected); + }); + }, [selectedRealview.id]); + + return html` +
+
+ ${selectedRealview.title} +
+ +
+ `; +} + +const TASK_ID_MAP = { + zazzle_shirt: 'tshirt', + zazzle_businesscard: 'businesscard', +}; + +function buildCheckoutUrl(templateId, expressProductSettings, productType) { + const taskId = TASK_ID_MAP[productType] || ''; + const baseUrl = `https://new.express.adobe.com/design-remix/template/${templateId}`; + const params = new URLSearchParams({ + category: 'templates', + taskId, + loadPrintAddon: 'true', + print: 'true', + action: 'customize-and-print-zazzle-iframe', + source: 'a.com-print-and-deliver-seo', + entryPoint: 'a.com-print-and-deliver-seo', + mv: 'other', + url: 'express/print', + }); + const encodedProductSettings = encodeURIComponent(expressProductSettings); + return `${baseUrl}?${params.toString()}&productSettings=${encodedProductSettings}`; +} + +const VALID_COUNTRIES = ['us', 'gb']; +const OUT_OF_REGION_TEXT = 'Print with Adobe Express isn\u2019t available yet in your region. Check back soon!'; + +export function CheckoutButton({ templateId }) { + const { state } = useStore(); + const [outOfRegion, setOutOfRegion] = useState(null); + const promoBarRef = useRef(null); + + useEffect(() => { + let active = true; + import('../../../scripts/utils/location-utils.js') + .then(({ getCountry }) => getCountry()) + .then((country) => { + if (active) setOutOfRegion(!VALID_COUNTRIES.includes(country)); + }); + return () => { active = false; }; + }, []); + + useEffect(() => { + if (!outOfRegion || !promoBarRef.current) return; + const container = promoBarRef.current; + + const promoBar = document.createElement('div'); + promoBar.className = 'sticky-promo-bar rounded'; + + const textWrapper = document.createElement('div'); + const textSpan = document.createElement('span'); + textSpan.className = 'pdpx-checkout-button-cta-text-container'; + textSpan.textContent = OUT_OF_REGION_TEXT; + textWrapper.appendChild(textSpan); + promoBar.appendChild(textWrapper); + + container.appendChild(promoBar); + + import('../../../scripts/utils.js').then(({ getLibs }) => import(`${getLibs()}/utils/utils.js`)).then(({ loadStyle, getConfig }) => { + const { codeRoot } = getConfig(); + loadStyle(`${codeRoot}/blocks/sticky-promo-bar/sticky-promo-bar.css`, () => { + import('../../sticky-promo-bar/sticky-promo-bar.js').then(({ default: stickyPromoBar }) => { + stickyPromoBar(promoBar); + }); + }); + }).catch((error) => { + window.lana?.log(`Failed to load sticky-promo-bar: ${error}`, { tags: 'print-product-detail-sdk', severity: 'warning' }); + }); + }, [outOfRegion]); + + if (outOfRegion === null) return null; + + const defaultUrl = `https://new.express.adobe.com/design-remix/template/${templateId}`; + const checkoutUrl = state?.expressProductSettings + ? buildCheckoutUrl(templateId, state.expressProductSettings, state.productType) + : defaultUrl; + + if (outOfRegion) { + return html` +
+
+ `; + } + + return html` +
+ + + Customize and print it + +
+ powered by zazzle + Returns guaranteed + through 100% satisfaction promise. +
+
+ `; +} + +function SizeChartTable({ measurementTypes, attributeValues }) { + if (!measurementTypes?.length || !attributeValues?.length) { + return null; + } + + const headerLabel = measurementTypes[0]?.key?.startsWith('garment') + ? 'Garment (IN)' : 'Body (IN)'; + + return html` +
+ + + + + + ${measurementTypes.map((type) => html` + + `)} + + + + ${attributeValues.map((row) => html` + + + ${measurementTypes.map((type) => { + const measurement = row.measurements?.[type.key]; + return html` + + `; + })} + + `)} + +
${headerLabel} size chart
${headerLabel}${type.label}
${row.attributeValueLabel}${measurement?.imperial || '—'}
+
+ `; +} + +function SizeChartContent({ onClose }) { + const { actions } = useStore(); + const [chart, setChart] = useState(null); + const [error, setError] = useState(null); + const { fetchSizeChart } = actions; + useEffect(() => { + let active = true; + fetchSizeChart() + .then((data) => { + if (active) { + setChart(data); + } + }) + .catch((err) => { + if (active) { + setError(err); + } + window.reportError?.(err); + }); + return () => { + active = false; + }; + }, [fetchSizeChart]); + + if (error) { + return html`
Unable to load size chart. Please try again later.
`; + } + + if (!chart) { + return html`
`; + } + + const { measurementTypes = [], attributeValues = [] } = chart.sizeChart || {}; + const bodyTypes = measurementTypes.filter((t) => t.key.startsWith('body')); + const garmentTypes = measurementTypes.filter((t) => !t.key.startsWith('body')); + + return html` +
+

${chart.title || 'Size Chart'}

+
+
+ ${bodyTypes.length > 0 && html` + <${SizeChartTable} + measurementTypes=${bodyTypes} + attributeValues=${attributeValues} + /> + `} + ${garmentTypes.length > 0 && html` + <${SizeChartTable} + measurementTypes=${garmentTypes} + attributeValues=${attributeValues} + /> + `} +
+
+
+ ${bodyTypes.length > 0 && html` +
+

Body

+ ${bodyTypes.map((type) => html` +

+ ${type.extraDescription || type.label} +

+ `)} +
+ `} + ${garmentTypes.length > 0 && html` +
+

Garment

+ ${garmentTypes.map((type) => html` +

+ ${type.extraDescription || type.label} +

+ `)} +
+ `} + ${chart.fitStyle && html` +
+

Fit

+

${chart.fitStyle}

+
+ `} +
+
+ `; +} + +function PrintingProcessContent({ onClose }) { + return html` +
+
+
+
+ Classic Printing +
+
+ Classic printing: no underbase +

+ Best for lighter colored garments. The design is printed directly onto the garment using CMYK inks only. +

+
+ cyan + magenta + yellow + black + CMYK +
+
+
+
+
+ Vivid Printing +
+
+ Vivid printing: with underbase +

+ Best for darker colored garments. A white underbase is printed first, then the design is printed on top using CMYK inks for vivid color reproduction. +

+
+ cyan + magenta + yellow + black + white + CMYK + White +
+
+
+
+
+ `; +} + +function stripHtmlTags(str) { + let result = str; + let prev; + do { + prev = result; + result = result.replace(/<[^>]*>/g, ''); + } while (result !== prev); + return result; +} + +function flattenOptionGroups(selector) { + if (!selector?.optionGroups || !Array.isArray(selector.optionGroups)) { + return []; + } + return selector.optionGroups.flatMap( + (group) => (group.options || []).map((option) => ({ ...option, groupTitle: group.title })), + ); +} + +function PaperTypeContent({ onClose }) { + const { state, actions } = useStore(); + const { state: drawerState } = useDrawer(); + const pillContainerRef = useRef(null); + const carouselCleanupRef = useRef(null); + const attrName = drawerState.payload?.attribute?.name; + const attribute = (state?.attributes || []).find((a) => a.name === attrName); + if (!attribute) return null; + const { selector, selectedOptionValue } = attribute; + const { preview } = selector; + const allOptions = flattenOptionGroups(selector); + const selectedOption = allOptions.find((o) => o.value === selectedOptionValue) || allOptions[0]; + const heroImageUrl = preview?.imageUrl ? updateImageUrl(preview.imageUrl, 1000) : ''; + const isRecommended = selectedOptionValue === '175ptmatte'; + + // Build pills imperatively and initialize carousel (avoids Preact reconciliation conflict) + useEffect(() => { + if (!pillContainerRef.current || !allOptions.length) return undefined; + + const container = pillContainerRef.current; + container.innerHTML = ''; + + allOptions.forEach((option) => { + const isSelected = option.value === selectedOptionValue; + const thumbUrl = updateImageUrl(option.imageUrl, 48); + + const pillContainer = document.createElement('div'); + pillContainer.className = 'pdpx-mini-pill-container'; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = `pdpx-mini-pill-image-container ${isSelected ? 'selected' : ''}`; + button.setAttribute('data-name', option.value); + button.setAttribute('data-title', option.title); + button.setAttribute('aria-label', option.title); + button.setAttribute('aria-current', isSelected ? 'true' : 'false'); + button.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + button.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); + button.addEventListener('click', () => { + if (option.value !== selectedOptionValue) { + actions.selectOption(attribute.name, option.value); + } + }); + + const img = document.createElement('img'); + img.className = 'pdpx-mini-pill-image'; + img.src = thumbUrl; + img.alt = ''; + img.setAttribute('aria-hidden', 'true'); + button.appendChild(img); + + pillContainer.addEventListener('mouseenter', (event) => { + const pill = event.currentTarget.getBoundingClientRect(); + const tooltipWidth = (option.title.length * 3) + 12; + const pillCenter = pill.left + (pill.width / 2); + const drawer = event.currentTarget.closest('.pdpx-drawer'); + const drawerOffsetLeft = drawer ? drawer.getBoundingClientRect().left : 0; + event.currentTarget.style.setProperty('--tooltip-top', `${pill.top - 42}px`); + event.currentTarget.style.setProperty('--tooltip-left', `${pillCenter - tooltipWidth - drawerOffsetLeft}px`); + event.currentTarget.style.setProperty('--arrow-top', `${pill.top - 6}px`); + event.currentTarget.style.setProperty('--arrow-left', `${pillCenter - drawerOffsetLeft}px`); + }); + + const textContainer = document.createElement('div'); + textContainer.className = 'pdpx-mini-pill-text-container'; + if (option.priceDelta) { + const priceSpan = document.createElement('span'); + priceSpan.className = 'pdpx-mini-pill-price'; + priceSpan.textContent = option.priceDelta; + textContainer.appendChild(priceSpan); + } + + pillContainer.appendChild(button); + pillContainer.appendChild(textContainer); + container.appendChild(pillContainer); + }); + + createSimpleCarousel('.pdpx-mini-pill-container', container, { + ariaLabel: `${attribute.title} options`, + centerActive: true, + activeClass: 'selected', + }).then((carousel) => { + if (carousel) { + carouselCleanupRef.current = carousel.cleanup; + } + }); + + return () => { + if (carouselCleanupRef.current) { + carouselCleanupRef.current(); + carouselCleanupRef.current = null; + } + }; + }, [attribute.name, attribute.title, allOptions.map((o) => o.value).join(',')]); + + // Update selected state when selection changes + useEffect(() => { + if (!pillContainerRef.current) return; + const buttons = pillContainerRef.current.querySelectorAll('.pdpx-mini-pill-image-container'); + buttons.forEach((btn) => { + const value = btn.getAttribute('data-name'); + const isSelected = value === selectedOptionValue; + btn.classList.toggle('selected', isSelected); + btn.setAttribute('aria-current', isSelected ? 'true' : 'false'); + btn.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + btn.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); + }); + }, [selectedOptionValue]); + + return html` + <${Fragment}> +
+ ${heroImageUrl && html` +
+ ${selectedOption?.title || ''} +
+ `} +
+ ${selectedOption?.title || ''} + Recommended +
+ ${preview?.descriptionHTML && html` +
+ ${preview.descriptionHTML + .split(//i)[0] + .replace(/[<>]/g, '') + .split('/') + .filter(Boolean) + .map((spec) => html` +
+ + ${spec.trim()} +
+ `)} +
+ `} +
+
+
+ ${attribute.title}: + ${selectedOption?.title || ''} +
+
+
+
+ ${state?.descriptionComponents?.[1]?.descriptionHTML && html` +
+ `} +
+
+
+ ${selectedOption?.title || ''} +
+
${selectedOption?.title || ''}
+ ${selectedOption?.priceDelta && html` +
${selectedOption.priceDelta}
+ `} +
+
+ +
+ + `; +} + +export function Drawer() { + const { state, closeDrawer } = useDrawer(); + const drawerRef = useRef(null); + const triggerRef = useRef(null); + + useEffect(() => { + if (!state.open) return undefined; + + // Capture the element that triggered the drawer so we can return focus + triggerRef.current = document.activeElement; + + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + closeDrawer(); + return; + } + // Focus trap: keep Tab within the drawer + if (e.key === 'Tab' && drawerRef.current) { + const focusable = drawerRef.current.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ); + if (!focusable.length) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + document.addEventListener('keydown', handleKeyDown); + + // Move focus into the drawer + requestAnimationFrame(() => { + if (drawerRef.current) { + const firstFocusable = drawerRef.current.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (firstFocusable) firstFocusable.focus(); + } + }); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + // Return focus to the triggering element + if (triggerRef.current?.focus) { + triggerRef.current.focus(); + triggerRef.current = null; + } + }; + }, [state.open, closeDrawer]); + + const drawerLabels = { + sizeChart: state.payload?.helpLink?.label || 'Size Chart', + paperType: 'Select Paper Type', + printingProcess: 'Printing Process', + }; + const drawerLabel = drawerLabels[state.type] || ''; + + return html` + <${Fragment}> + + + + `; +} diff --git a/express/code/blocks/print-product-detail-sdk/components/useSeo.js b/express/code/blocks/print-product-detail-sdk/components/useSeo.js new file mode 100644 index 000000000..fce4c011a --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/components/useSeo.js @@ -0,0 +1,198 @@ +import { useEffect } from '../../../scripts/vendors/htm-preact.js'; +import { useStore } from './Contexts.js'; + +export function getCanonicalUrl() { + const existing = document.querySelector('link[rel="canonical"]'); + const href = existing?.getAttribute('href'); + return href && href.trim() ? href : window.location.href; +} + +export function getAuthoredOverrides(doc = document) { + const overrides = {}; + const titleEl = doc.querySelector('title'); + const metaDescEl = doc.querySelector('meta[name="description"]'); + const ogImgEl = doc.querySelector('meta[property="og:image"]'); + if (titleEl && titleEl.textContent && titleEl.textContent.trim()) { + overrides.name = titleEl.textContent.trim(); + } + if (metaDescEl && metaDescEl.content && metaDescEl.content.trim()) { + overrides.description = metaDescEl.content.trim(); + } + if (ogImgEl && ogImgEl.content && ogImgEl.content.trim()) { + overrides.image = ogImgEl.content.trim(); + } + return overrides; +} + +export function upsertLdJson(id, data) { + let script = document.getElementById(id); + if (!script) { + script = document.createElement('script'); + script.type = 'application/ld+json'; + script.id = id; + document.head.appendChild(script); + } + script.textContent = JSON.stringify(data); +} + +function stripHtml(input) { + if (!input) return ''; + const doc = new DOMParser().parseFromString(input, 'text/html'); + return (doc.body.textContent || '').replace(/\s+/g, ' ').trim(); +} + +async function getCurrencyCode() { + const { getCurrency } = await import('../../../scripts/utils/pricing.js'); + const { getCountry } = await import('../../../scripts/utils/location-utils.js'); + const country = await getCountry(); + const currency = await getCurrency(country); + return currency; +} + +export async function buildProductJsonLd(apiData, overrides, canonicalUrl) { + const name = overrides?.name || apiData.productTitle || ''; + const descriptionSource = overrides?.description + || (Array.isArray(apiData.productDescriptions) && apiData.productDescriptions[0]?.description) + || ''; + const description = stripHtml(descriptionSource); + const image = overrides?.image || apiData.heroImage || ''; + const sku = apiData.id || apiData.templateId || ''; + + const json = { + '@context': 'https://schema.org/', + '@type': 'Product', + name, + description, + image, + brand: { + '@type': 'Brand', + name: 'Adobe Express', + }, + }; + + if (sku) json.sku = String(sku); + + // Offers (if pricing available) + if (apiData.productPrice) { + const priceCurrency = await getCurrencyCode(); + json.offers = { + '@type': 'Offer', + price: Number(apiData.productPrice).toFixed(2), + priceCurrency, + availability: 'https://schema.org/InStock', + url: canonicalUrl, + }; + } + + return json; +} + +export function upsertTitleAndDescriptionRespectingAuthored(apiData) { + const authored = getAuthoredOverrides(document); + if (!authored.name && apiData.productTitle) { + let titleEl = document.querySelector('title'); + if (!titleEl) { + titleEl = document.createElement('title'); + document.head.appendChild(titleEl); + } + titleEl.textContent = apiData.productTitle; + } + if (!authored.description) { + const descriptionSource = (Array.isArray(apiData.productDescriptions) && apiData.productDescriptions[0]?.description) || ''; + const description = stripHtml(descriptionSource).slice(0, 160); + if (description) { + let metaDesc = document.querySelector('meta[name="description"]'); + if (!metaDesc) { + metaDesc = document.createElement('meta'); + metaDesc.setAttribute('name', 'description'); + document.head.appendChild(metaDesc); + } + metaDesc.setAttribute('content', description); + } + } +} + +export function buildBreadcrumbsJsonLdFromDom() { + const nav = document.querySelector('nav.feds-breadcrumbs'); + if (!nav) return null; + const items = []; + const lis = nav.querySelectorAll('ul > li'); + let position = 1; + lis.forEach((li) => { + const a = li.querySelector('a'); + const name = (a ? a.textContent : li.textContent) || ''; + const item = { '@type': 'ListItem', position, name: name.trim() }; + if (a && a.getAttribute('href')) { + try { + const href = new URL(a.getAttribute('href'), window.location.origin).toString(); + item.item = href; + } catch (e) { + // ignore malformed hrefs + } + } + items.push(item); + position += 1; + }); + if (!items.length) return null; + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: items, + }; +} + +function mapStateToSeoPayload(state, templateId) { + const descriptionComponents = Array.isArray(state.descriptionComponents) + ? state.descriptionComponents.map((item) => ({ description: item.descriptionHTML })) + : []; + + const numericPrice = (() => { + const priceString = state.pricing?.unitPrice || state.pricing?.totalPrice || ''; + const normalized = priceString.replace(/[^0-9.,]/g, '').replace(',', '.'); + const parsed = Number.parseFloat(normalized); + return Number.isFinite(parsed) ? parsed : undefined; + })(); + + return { + productTitle: state.title, + productDescriptions: descriptionComponents, + heroImage: state.selectedRealview?.url, + productPrice: numericPrice, + id: templateId, + templateId, + }; +} + +export default function useSeo(templateId) { + const { state } = useStore(); + + useEffect(() => { + if (!state) { + return undefined; + } + + let active = true; + const payload = mapStateToSeoPayload(state, templateId); + upsertTitleAndDescriptionRespectingAuthored(payload); + + const canonicalUrl = getCanonicalUrl(); + const overrides = getAuthoredOverrides(document); + + buildProductJsonLd(payload, overrides, canonicalUrl) + .then((json) => { + if (active && json) { + upsertLdJson('pdp-product-jsonld', json); + } + }) + .catch((error) => { + window.reportError?.(error); + }); + + const breadcrumbs = buildBreadcrumbsJsonLdFromDom(); + if (breadcrumbs) { + upsertLdJson('pdp-breadcrumbs-jsonld', breadcrumbs); + } + + return () => { active = false; }; + }, [state, templateId]); +} diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css new file mode 100644 index 000000000..57525f82c --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css @@ -0,0 +1,1186 @@ +.print-product-detail-sdk { + --standard-transition-hover-in-border-color: opacity 80ms ease-out 20ms; + --standard-transition-hover-out-border-color: opacity 80ms ease-out 220ms; + --standard-transition-hover-in-opacity: opacity 80ms ease-out 20ms; + --standard-transition-hover-out-opacity: opacity 80ms ease-out 220ms; + --global-container-margin: 16px; + --font-size-header: 24px; + + + + @media (min-width: 600px) { + --global-container-margin: 32px; + + } + @media (min-width: 1200px) { + --global-container-margin: 40px; + --font-size-header: 28px; + } + span { + font-family: var(--body-font-family) + } + .global-Typography-Size-Headings-Heading-L { + font-size: var(--font-size-header); + font-weight: var(--heading-font-weight-extra); + line-height: 104%; + color: var(--color-gray-950); + } + .picker-with-link { + width: fit-content; + } + .picker-container.side { + width: fit-content; + } + .picker-current-value { + min-width: 65px; + } + .picker-container:not(.picker-with-link .picker-container) { + margin-bottom: var(--spacing-300); + } +} + +.hidden { + display: none; + visibility: hidden; + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + z-index: -1; + overflow: hidden; + border: none; + outline: none; + box-shadow: none; + background: transparent; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +[data-skeleton='true'] { + background: linear-gradient(90deg, #eee 25%, #f5f5f5 37%, #eee 63%); + background-size: 400% 100%; + animation: skeleton-shimmer 1.2s ease-in-out infinite; + border-radius: 4px; + min-height: 16px; +} +@keyframes skeleton-shimmer { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } +} +.pdpx-global-container { + max-width: var(--block-wd-max-width); + margin: 0 auto; + padding: var(--global-container-margin); + display: flex; + flex-direction: column; + gap: var(--spacing-300); + align-items: flex-start; +} +.pdpx-product-info-wrapper { + display: contents; + flex-direction: column; + width: 100%; + order: 3; +} +.pdpx-product-info-section-container { + display: flex; + flex-direction: column; + width: 100%; + order: 3; + .pdpx-product-info-section { + width: 100%; + order: 3; + } +} +/* HEADER SECTION CONTENT */ +.pdpx-product-info-heading-section-wrapper { + width: 100%; + order: 1; +} +.pdpx-product-info-heading-section-container { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--spacing-100); + align-items: baseline; + margin-bottom: var(--spacing-200); +} +.pdpx-product-title { + font-size: var(--ax-heading-xl-size); +} +.pdpx-customization-inputs-container { + padding-bottom: var(--spacing-400); +} +.pdpx-price-info-container { + display: flex; + flex-direction: column; + .pdpx-price-info-row { + display: flex; + align-items: baseline; + gap: var(--spacing-75); + .pdpx-price-label { + font-size: var(--ax-body-l-size); + font-weight: var(--ax-detail-weight); + } + .pdpx-compare-price-label { + text-decoration: line-through; + font-size: var(--ax-body-xs-size); + } + .pdpx-compare-price-info-label { + font-size: var(--ax-body-xs-size); + text-align: end; + white-space: nowrap; + } + } + .pdpx-compare-price-info-icon-container { + position: relative; + .pdpx-compare-price-info-icon-button { + background: transparent; + border: none; + padding: 0; + margin: 0; + align-self: end; + cursor: pointer; + width: max-content; + .pdpx-compare-price-info-icon { + width: var(--spacing-200); + } + } + .pdpx-info-tooltip-content { + position: absolute; + display: none; + right: -100px; + background-color: #323232; + color: var(--color-white); + padding: 15px; + border-radius: 8px; + width: max-content; + max-width: 250px; + text-align: center; + z-index: 1; + .pdpx-info-tooltip-content-title { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-detail-weight); + color: var(--color-white); + margin: 0; + } + .pdpx-info-tooltip-content-description { + font-size: var(--ax-body-xxs-size); + margin: 0; + padding: 0; + padding-top: 10px; + } + } + } + .pdpx-savings-text { + font-size: var(--ax-body-xxs-size); + font-weight: var(--ax-detail-weight); + color: #05834e; + } +} +.pdpx-product-ratings-lockup-container { + display: flex; + flex-direction: row; + align-items: baseline; + gap: var(--spacing-75); + .pdpx-product-info-header-ratings-star { + width: 17px; + } + .pdpx-star-ratings { + display: flex; + align-self: anchor-center; + gap: 2px; + } + .pdpx-star-ratings .pdpx-star-icon { + width: 17px; + height: 17px; + display: block; + flex-shrink: 0; + } + .pdpx-star-ratings .pdpx-star-icon-wrapper { + width: 17px; + height: 17px; + flex-shrink: 0; + } + .pdpx-star-ratings .pdpx-star-icon-half-wrapper { + position: relative; + overflow: hidden; + } + .pdpx-star-ratings .pdpx-star-icon-half-filled { + position: absolute; + left: 0; + top: 0; + clip-path: inset(0 50% 0 0); + -webkit-clip-path: inset(0 50% 0 0); + } + .pdpx-star-ratings .pdpx-star-icon-half-empty { + position: absolute; + left: 0; + top: 0; + clip-path: inset(0 0 0 50%); + -webkit-clip-path: inset(0 0 0 50%); + } + .pdpx-ratings-number { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-detail-weight); + line-height: 1.5; + color: var(--body-color); + } + .pdpx-ratings-amount { + background: transparent; + font-size: var(--ax-body-xs-size); + line-height: 1.5; + font-weight: var(--ax-body-weight); + color: var(--body-color); + border: none; + padding: 0; + margin: 0; + cursor: default; + } +} +.pdpx-delivery-estimate-pill { + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--palette-indigo-200); + border-radius: 8px; + width: fit-content; + padding: var(--spacing-100); + gap: var(--spacing-75); + margin-bottom: 0; +} +/* END HEADER SECTION CONTENT */ +/* PRODUCT IMAGES SECTION CONTENT */ +.pdpx-product-images-container { + width: 100%; + order: 2; + .pdpx-product-hero-image-container { + margin-bottom: var(--spacing-100); + aspect-ratio: 1/1; + border-radius: var(--spacing-200); + overflow: hidden; + object-fit: contain; + object-position: center; + .pdpx-product-hero-image { + width: 100%; + border-radius: var(--spacing-200); + } + } + .pdpx-image-thumbnail-carousel-container { + position: relative; + width: 100%; + } + .pdpx-image-thumbnail-carousel-item { + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + width: var(--spacing-700); + height: var(--spacing-700); + border: 1px solid transparent; + background-color: transparent; + padding: 0; + cursor: pointer; + transition: border-color 0.2s ease; + } + .pdpx-image-thumbnail-carousel-item:hover { + border-color: var(--color-gray-300); + } + .pdpx-image-thumbnail-carousel-item.selected { + border: 1px solid var(--color-background-accent-default); + } + .pdpx-image-thumbnail-carousel-item-image { + width: 100%; + height: 100%; + object-fit: cover; + } +} +/* SIMPLE CAROUSEL */ +.simple-carousel-fader-left { + pointer-events: none; + width: 88px; + background: linear-gradient( + to right, + rgba(255, 255, 255, 1) 0%, + rgba(255, 255, 255, 1) 20%, + rgba(255, 255, 255, 0) 100% + ); + .simple-carousel-arrow-left { + pointer-events: all; + left: 5px; + } +} +.simple-carousel-fader-right { + pointer-events: none; + width: 88px; + background: linear-gradient( + to left, + rgba(255, 255, 255, 1) 0%, + rgba(255, 255, 255, 1) 20%, + rgba(255, 255, 255, 0) 100% + ); + .simple-carousel-arrow-right { + pointer-events: all; + } +} + +/* CHECKOUT BUTTON */ +.pdpx-checkout-button-container { + position: fixed; + bottom: -1px; + left: 0; + background: var(--color-white); + width: 100%; + margin-top: var(--spacing-200); + order: 4; + z-index: 18; + a.pdpx-checkout-button { + width: calc(100% - (var(--global-container-margin) * 2)); + margin-left: auto; + margin-right: auto; + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + margin-bottom: var(--spacing-100); + background-color: var(--color-background-accent-default); + color: var(--color-white); + border: none; + border-radius: 24px; + padding: 13px; + cursor: pointer; + } +} + +.pdpx-checkout-button-text { + font-size: var(--ax-body-m-size); + font-weight: var(--ax-detail-weight); +} +.pdpx-checkout-button-subhead { + width: calc(100% - (var(--global-container-margin) * 2)); + margin-left: auto; + margin-right: auto; + display: flex; + flex-direction: row; + align-items: end; + flex-wrap: wrap; + justify-content: center; + gap: var(--spacing-75); + font-size: var(--ax-body-xs-size); + margin-bottom: var(--spacing-300); +} +.pdpx-checkout-button-subhead-image { + margin-bottom: 1px; +} +main .pdpx-checkout-button-subhead a.pdpx-checkout-button-subhead-link { + white-space: nowrap; + color: var(--color-black); + font-weight: var(--ax-body-weight); + text-decoration: underline; +} +.pdpx-checkout-button-subhead-text { + text-align: right; +} +.pdpx-checkout-button-disabled a.pdpx-checkout-button { + background-color: var(--color-dark-gray); + border-radius: 10px; + cursor: not-allowed; + pointer-events: none; + font-weight: var(--ax-body-weight); + span.pdpx-checkout-button-text { + font-size: var(--ax-body-s-size); + font-weight: var(--ax-body-weight); + } +} +.pdpx-assurance-lockup-container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--spacing-300); + margin-bottom: var(--spacing-400); + .pdpx-assurance-lockup-item { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacing-100); + width: 100%; + justify-content: center; + padding: var(--spacing-100) 0; + background-color: var(--color-gray-100); + border-radius: 8px; + } +} +/* PILL SELECTOR */ +.pdpx-pill-selector-container { + margin-bottom: var(--spacing-400); + overflow: visible; + .pdpx-pill-selector-label-container { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: var(--spacing-100); + .pdpx-pill-selector-label-label { + font-weight: var(--ax-detail-weight); + } + } +} +.pdpx-standard-selector-container { + margin-bottom: 1rem; +} +.pdpx-pill-selector-label { + display: block; + font-weight: var(--ax-detail-weight); + margin-bottom: var(--spacing-100); +} +.pdpx-pill-selector-options-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--spacing-100); + overflow: visible; +} +.pdpx-pill-container { + position: relative; + display: flex; + flex-direction: row; + min-width: 150px; + background-color: white; + overflow: hidden; + border-radius: 8px; + border: 1px solid var(--color-gray-300-variant); + padding: 0px; + cursor: pointer; +} +.pdpx-pill-container.selected { + border: 1px solid var(--color-background-accent-default); +} +.pdpx-pill-image-container { + width: 54px; + height: 100%; + flex-shrink: 0; + aspect-ratio: 1/1; +} +.pdpx-pill-image { + width: 100%; + height: 100%; + object-fit: cover; +} +.pdpx-pill-text-container { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + min-width: 92px; + gap: var(--spacing-75); + padding: var(--spacing-100); + padding-right: var(--spacing-400); +} +.pdpx-pill-text-name { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-detail-weight); + color: black; + text-align: left; +} +.pdpx-pill-text-price { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-body-weight); + color: var(--color-dark-gray); +} +/* MINI PILL SELECTOR */ +.pdpx-mini-pill-container { + position: relative; + display: flex; + flex-direction: column; + gap: 0px; + overflow: visible; +} +.pdpx-pill-selector-label-compare-link { + background: transparent; + font-family: var(--body-font-family); + font-size: var(--ax-body-xs-size); + font-weight: 500; + border: none; + padding: 0; + margin: 0; + text-decoration: underline; + cursor: pointer; +} + +.pdpx-mini-pill-sections-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--spacing-200); + overflow: visible; + position: relative; +} +.pdpx-mini-pill-image-container { + border-radius: 4px; + border: 1px solid transparent; + width: 48px; + height: 48px; + padding: 0; + background-color: transparent; + display: inline-block; + overflow: hidden; + position: relative; + line-height: 0; + cursor: pointer; + transition: var(--standard-transition-hover-out-border-color); + font-family: var(--body-font-family); + font-weight: var(--ax-body-weight); + font-size: var(--ax-body-xs-size); + .pdpx-mini-pill-image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + display: block; + pointer-events: none; + } +} +.pdpx-mini-pill-price { + font-size: var(--ax-body-xxs-size); + font-weight: var(--ax-body-weight); + color: var(--color-gray-950); +} +.pdpx-mini-pill-image-container.selected { + border: 1px solid var(--color-background-accent-default); +} +.pdpx-mini-pill-image-container:hover:not(.selected), +.pdpx-image-thumbnail-carousel-item:hover:not(.selected) { + border-color: var(--color-black); + transition: var(--standard-transition-hover-in-border-color); +} +@media (hover: none), (pointer: coarse) { + .pdpx-mini-pill-image-container::after, + .pdpx-mini-pill-image-container::before { + display: none !important; + content: none; + } +} +.pdpx-mini-pill-image-container::after { + content: attr(aria-label); + position: fixed; + font-size: var(--ax-body-xs-size); + padding: var(--spacing-325) var(--spacing-200); + top: var(--tooltip-top); + left: var(--tooltip-left); + background: var(--color-gray-800); + color: var(--color-white); + border-radius: 6px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: var(--standard-transition-hover-out-opacity); + z-index: 200; +} +.pdpx-mini-pill-image-container::before { + content: ''; + position: fixed; + top: var(--arrow-top); + left: var(--arrow-left); + bottom: 100%; + transform: translate(-50%, -4px) rotate(45deg); + width: 8px; + height: 8px; + background: var(--color-gray-800); + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: var(--standard-transition-hover-out-opacity); + z-index: 199; +} +.pdpx-mini-pill-image-container:hover::after, +.pdpx-mini-pill-image-container:hover::before { + opacity: 1; + visibility: visible; + transition: var(--standard-transition-hover-in-opacity); +} +.pdpx-mini-pill-text-container { + display: none; +} +.pdpx-pill-selector-section-label { + display: block; + margin-bottom: var(--spacing-100); +} +.pdpx-mini-pill-section-container, +.pdpx-mini-pill-section-options-container { + position: relative; + width: 100%; +} +/* PRODUCT DETAILS SECTION */ +.pdpx-product-details-section { + margin-bottom: var(--spacing-400); +} +.pdpx-product-details-section-title-container { + padding-bottom: var(--spacing-300); +} +.pdpx-product-details-section-title { + font-size: var(--ax-body-s-size); + font-weight: var(--ax-detail-weight); +} +.pdpx-mini-pill-selector-options-container { + position: relative; +} +#pdpx-checkout-button-sentinel-div { + height: 1px; + width: 1px; + opacity: 0; + background-color: transparent; + visibility: hidden; + pointer-events: none; + position: absolute; + bottom: 0; + left: 0; +} +/*DRAWER*/ +.pdpx-drawer-body { + padding: var(--spacing-300) var(--spacing-300) 0 var(--spacing-300); + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; + overflow-x: visible; + scrollbar-width: thin; + +} +.pdpx-drawer-hero-image-container { + aspect-ratio: 3 / 2; + width: 100%; + min-height: 200px; + overflow: hidden; + flex-shrink: 0; + margin-bottom: var(--spacing-400); + .pdpx-drawer-hero-image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: bottom; + border-radius: 8px; + display: block; + transition: opacity 0.2s ease; + } +} +.pdpx-drawer-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-100); + .pdpx-drawer-title { + font-size: var(--ax-body-l-size); + font-weight: var(--ax-detail-weight); + } + .pdpx-recommended-badge { + font-size: var(--body-font-size-xs); + color: white; + padding: 4px 9px; + line-height: 1.3; + border-radius: 7px; + background-color: #3b63fb; + } +} +.pdpx-drawer-pills-container { + display: flex; + gap: var(--spacing-100); + margin-bottom: var(--spacing-300); + .pdpx-drawer-pill { + border-radius: 8px; + padding: var(--spacing-75) var(--spacing-100); + background-color: var(--color-gray-150); + display: flex; + align-items: center; + font-size: var(--body-font-size-s); + gap: 4px; + } +} +main .section .pdpx-drawer-description { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-body-weight); + margin-bottom: var(--spacing-300); + p { + margin: 0; + margin-top: 0px; + margin-bottom: 0px; + padding: 0; + } + p, ul, ol { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-body-weight); + } +} +.ax-accordion-item-description ul, +.ax-accordion-item-description ol, +.ax-accordion-item-description p { + font-size: var(--ax-body-s-size); +} +main .section .ax-accordion-item-description p { + margin-top: 0; + margin-bottom: 0; +} +.pdpx-drawer-foot { + display: flex; + border-top: 1px solid var(--color-gray-325); + padding: var(--spacing-200) var(--spacing-300); + justify-content: space-between; + background-color: var(--color-white); + flex-shrink: 0; + .pdpx-drawer-foot-info-container { + display: flex; + align-items: center; + gap: var(--spacing-200); + } + .pdpx-drawer-foot-info-container img { + width: 48px; + border-radius: 4px; + } + .pdpx-drawer-foot-info-name { + font-weight: var(--ax-detail-weight); + line-height: 1.3; + padding-bottom: 2px; + } + .pdpx-drawer-foot-info-price { + font-size: var(--body-font-size-s); + line-height: 1.3; + } + .pdpx-drawer-foot-select-button { + color: var(--color-white); + background-color: var(--color-background-accent-default); + padding: var(--spacing-200) var(--spacing-400); + border-radius: 24px; + border: none; + font-family: var(--body-font-family); + font-weight: var(--ax-detail-weight); + font-size: var(--body-font-size-l); + cursor: pointer; + } +} +.pdpx-printing-process-options-container { + display: flex; + flex-direction: column; + gap: var(--spacing-300); + .pdpx-printing-process-option-container { + display: flex; + flex-direction: row; + gap: var(--spacing-500); + .pdpx-printing-process-option-image-container { + max-width: 40%; + .pdpx-printing-process-option-image { + aspect-ratio: 1/1; + object-fit: cover; + object-position: center; + border-radius: 8px; + } + } + .pdpx-printing-process-option-info-container { + display: flex; + flex-direction: column; + gap: var(--spacing-300); + + .pdpx-printing-process-option-info-title { + font-size: var(--ax-body-l-size); + font-weight: var(--ax-detail-weight); + } + p.pdpx-printing-process-option-info-description { + font-size: var(--ax-body-xs-size); + font-weight: var(--ax-body-weight); + color: var(--color-gray-900); + line-height: 1.3; + margin: 0; + padding: 0; + } + .pdpx-printing-process-option-color-lockup { + display: flex; + align-items: center; + padding: var(--spacing-75) var(--spacing-100); + border-radius: 8px; + background-color: var(--color-gray-150); + font-size: var(--body-font-size-s); + width: fit-content; + img:last-of-type { + margin-right: 4px; + } + } + } + } +} +.pdpx-drawer { + position: fixed; + left: 0; + bottom: 0; + z-index: 102; + border-radius: 16px 16px 0 0; + background-color: var(--color-white); + height: 600px; + width: 100%; + transform: translateY(0); + transition: transform 300ms ease; + display: flex; + flex-direction: column; +} +.pdpx-drawer.hidden { + transform: translateY(100%); +} +.pdpx-drawer-head { + display: flex; + position: sticky; + top: 0; + z-index: 1; + justify-content: space-between; + border-radius: 16px 16px 0 0; + border-bottom: 1px solid var(--color-gray-325); + padding: var(--spacing-400) var(--spacing-300); + background-color: var(--color-white); +} +.pdpx-drawer-head-label { + font-size: var(--ax-body-xl-size); + font-weight: var(--ax-detail-weight); + line-height: 1.3; +} +.pdpx-drawer-head button { + cursor: pointer; + background: none; + border: none; +} +.pdpx-drawer-body .pdpx-mini-pill-sections-container { + overflow: visible; +} +.pdpx-curtain { + z-index: 101; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgb(0 0 0 / 40%); +} +/* SIZE CHART */ +.size-chart-product-name { + font-size: var(--body-font-size-l); + font-weight: var(--subheading-font-weight); + color: var(--color-gray-950); + margin: 0; + margin-bottom: var(--spacing-100); +} +.size-chart-table-container { + border-radius: 16px; + border: 1px solid var(--color-gray-325); + padding: var(--spacing-400); + color: var(--color-gray-900); + margin-bottom: var(--spacing-400); +} +.size-chart-table-section { + display: flex; + flex-direction: column; + gap: var(--spacing-100); +} +.size-chart-table thead .size-chart-table-header { + font-size: var(--body-font-size-s); + font-weight: var(--subheading-font-weight); + color: var(--color-gray-950); + text-align: left; + padding: var(--spacing-100); + white-space: nowrap; +} +.size-chart-table { + width: 100%; + border-collapse: separate; + border-spacing: 0 var(--spacing-100); + table-layout: fixed; +} +.size-chart-table thead th { + font-size: var(--body-font-size-s); + font-weight: normal; + color: var(--color-gray-800); + text-align: left; + padding: var(--spacing-100); + padding-top: 0px; +} +.size-chart-table thead th:nth-child(2), +.size-chart-table thead th:nth-child(3) { + padding-left: var(--spacing-400); + text-align: right; +} +.size-chart-table tbody tr { + background: var(--color-white); +} +.size-chart-table tbody tr:nth-child(odd) { + background: #ebebeb; +} +.size-chart-table tbody tr:nth-child(odd) td:first-child { + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; +} +.size-chart-table tbody tr:nth-child(odd) td:last-child { + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} +.size-chart-table tbody td { + font-size: var(--body-font-size-xs); + color: var(--color-gray-950); + padding: var(--spacing-100) var(--spacing-200); + word-break: break-word; + line-height: 1.3; +} +.size-chart-table tbody td:nth-child(2), +.size-chart-table tbody td:nth-child(3) { + padding-left: var(--spacing-400); + text-align: right; +} +.size-chart-instructions { + display: flex; + flex-direction: column; + gap: var(--spacing-300); + margin-bottom: 80px; +} +.size-chart-instruction-section h3 { + font-size: var(--body-font-size-m); + font-weight: var(--subheading-font-weight); + color: var(--color-gray-950); + margin: 0 0 var(--spacing-100) 0; +} +.size-chart-instructions .size-chart-instruction-text { + font-size: var(--body-font-size-s); + font-style: normal; + font-weight: var(--ax-body-weight); + line-height: 130%; + margin: 0; +} +.pdpx-checkout-button-container:not(.hide-gradient)::before { + content: ''; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + height: var(--spacing-900); + pointer-events: none; + background: linear-gradient(to top, white 20%, rgba(255, 255, 255, 0) 100%); +} + +/*TABLET*/ +@media (min-width: 600px) { + .pdpx-global-container { + flex-direction: row; + } + .pdpx-price-info-container { + .pdpx-compare-price-info-icon-container { + .pdpx-info-tooltip-content { + right: 0px; + width: 175px; + } + } + } + .pdpx-product-images-container { + order: 1; + width: calc(50% - 8px); + } + .pdpx-product-info-wrapper { + order: 2; + width: calc(50% - 8px); + display: flex; + flex-direction: column; + flex: 1 1 auto; + height: 75vh; + overflow-x: visible; + overflow-y: scroll; + scrollbar-gutter: stable; + padding-right: var(--spacing-400); + } + .pdpx-product-info-heading-section-wrapper { + order: 1; + } + .pdpx-product-info-section-container { + display: flex; + flex-direction: column; + order: 2; + } + .pdpx-product-info-heading-section-container { + flex-direction: column; + order: 1; + } + .pdpx-product-info-section { + order: 2; + } + .pdpx-checkout-button-subhead { + display: flex; + } + .pdpx-delivery-estimate-pill { + margin-bottom: var(--spacing-400); + } + .size-chart-tables { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-400); + } + .pdpx-mini-pill-text-container { + display: block; + } + .pdpx-drawer-body { + padding: var(--spacing-500); + } + .pdpx-drawer-foot { + padding: var(--spacing-200) var(--spacing-500); + } + .pdpx-checkout-button-container:not(.hide-gradient)::before { + content: ''; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + height: var(--spacing-900); + pointer-events: none; + background: linear-gradient(to top, white 20%, rgba(255, 255, 255, 0) 100%); + } + .pdpx-checkout-button-container { + position: sticky; + width: 100%; + a.pdpx-checkout-button { + width: 100%; + } + } +} +/*DESKTOP*/ +@media (min-width: 1200px) { + .pdpx-global-container { + gap: var(--spacing-400); + } + .pdpx-price-info-container { + .pdpx-compare-price-info-icon-container { + .pdpx-info-tooltip-content { + right: 0; + width: 250px; + } + } + } + .pdpx-product-info-heading-section-container { + flex-direction: row; + margin-bottom: var(--spacing-400); + gap: var(--spacing-400); + .pdpx-price-info-container { + align-items: flex-end; + } + } + .pdpx-title-and-ratings-container { + max-width: 64%; + } + .pdpx-product-images-container, + .pdpx-product-info-wrapper { + width: calc(50% - 12px); + } + .pdpx-savings-text { + text-align: end; + } + .pdpx-drawer { + bottom: initial; + top: 0; + right: 0; + left: initial; + width: 50%; + height: 100%; + border-radius: initial; + transform: translateX(0); + } + .pdpx-drawer.hidden { + transform: translateX(100%); + } + .pdpx-drawer-title { + font-size: var(--body-font-size-xl); + } + .pdpx-assurance-lockup-container { + flex-direction: row; + .pdpx-assurance-lockup-item { + width: 50%; + } + } + .pdpx-product-images-container .pdpx-product-hero-image-container { + margin-bottom: var(--spacing-200); + } + .pdpx-product-images-container .pdpx-image-thumbnail-carousel-item { + width: 76px; + height: 76px; + } + .pdpx-mini-pill-sections-container { + .simple-carousel-platform { + flex-wrap: wrap; + overflow: visible; + } + .simple-carousel-fader-left, + .simple-carousel-fader-right { + pointer-events: none; + display: none; + } + } + .pdpx-drawer-head { + padding: var(--spacing-400) var(--spacing-600); + } + .pdpx-drawer-body { + padding: var(--spacing-600) var(--spacing-600) 0 var(--spacing-600); + } + .pdpx-drawer-foot { + padding: var(--spacing-200) var(--spacing-600); + } +} + +/* Accessibility: screen-reader-only utility */ +.print-product-detail-sdk .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* Accessibility: focus indicators */ +.print-product-detail-sdk a:focus-visible, +.print-product-detail-sdk button:focus-visible, +.print-product-detail-sdk input:focus-visible, +.print-product-detail-sdk select:focus-visible { + outline: 3px solid var(--color-background-accent-default); + outline-offset: 2px; +} + +.print-product-detail-sdk .pdpx-mini-pill-image-container:focus-visible { + outline: 3px solid var(--color-background-accent-default); + outline-offset: 2px; + border-radius: 50%; +} + +.print-product-detail-sdk .pdpx-pill-container:focus-visible { + outline: 3px solid var(--color-background-accent-default); + outline-offset: 2px; + border-radius: var(--spacing-100); +} + +.print-product-detail-sdk a.pdpx-checkout-button:focus-visible { + outline: 3px solid var(--color-white); + outline-offset: -3px; +} + +/* Accessibility: reduced motion */ +@media (prefers-reduced-motion: reduce) { + .print-product-detail-sdk [data-skeleton='true'] { + animation: none; + } + + .print-product-detail-sdk .pdpx-curtain, + .print-product-detail-sdk .pdpx-drawer, + .print-product-detail-sdk .pdpx-mini-pill-image-container::after, + .print-product-detail-sdk .pdpx-mini-pill-image-container::before { + transition-duration: 0.01ms; + } +} diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js new file mode 100644 index 000000000..12cdde3b0 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js @@ -0,0 +1,143 @@ +import { createZazzleStore, extractTemplateId } from './utilities/utility-functions.js'; +import { getLibs } from '../../scripts/utils.js'; +import { + html, + render, + useEffect, + useRef, + Fragment, +} from '../../scripts/vendors/htm-preact.js'; +import { StoreProvider, useStore, DrawerProvider, useDrawer } from './components/Contexts.js'; +import { ProductImages, ProductDetails, ProductHeader, CheckoutButton, Drawer } from './components/ProductComponents.js'; +import { CustomizationInputs } from './components/CustomizationInputs.js'; +import { trackViewTemplatePage } from '../../scripts/instrument.js'; +import useSeo from './components/useSeo.js'; + +function LoadingSkeleton() { + return html` +
+
+
+
+
+
+

+
+
+
+
+
+
+
+ `; +} + +function PDPContent({ templateId }) { + const store = useStore(); + const { state, actions } = store; + const { openDrawer } = useDrawer(); + const { fetchProduct } = actions; + const containerRef = useRef(null); + const hasTrackedPageView = useRef(false); + + useSeo(templateId); + + useEffect(() => { + if (!templateId) { + return; + } + fetchProduct(templateId); + }, [templateId, fetchProduct]); + + useEffect(() => { + if (!state || hasTrackedPageView.current) return; + hasTrackedPageView.current = true; + try { + const attributeObject = Object.fromEntries( + (state.attributes || []).map((attr) => [attr.name, attr.selectedOptionValue]), + ); + trackViewTemplatePage( + 'pdp', + state.productType, + templateId, + 'print', + true, + attributeObject, + true, + ); + } catch (error) { + window.lana?.log(`Failed to track PDP pageload: ${error}`, { tags: 'print-product-detail-sdk', severity: 'warning' }); + } + }, [state, templateId]); + + useEffect(() => { + if (!containerRef.current || !state) return; + import(`${getLibs()}/martech/attributes.js`).then(({ decorateDefaultLinkAnalytics }) => { + import(`${getLibs()}/utils/utils.js`).then(({ getConfig }) => { + decorateDefaultLinkAnalytics(containerRef.current, getConfig()); + }); + }); + }, [state]); + + const handleDrawerRequest = (request) => { + if (!request) { + return; + } + openDrawer({ type: request.type, payload: request.payload }); + }; + + if (!state) { + return html` + <${Fragment}> + <${LoadingSkeleton} /> + <${Drawer} /> + + `; + } + + return html` +
+ <${ProductImages} /> +
+ <${ProductHeader} /> +
+
+ <${CustomizationInputs} onRequestDrawer=${handleDrawerRequest} productType=${state.productType} /> + <${ProductDetails} /> + <${Drawer} /> +
+ <${CheckoutButton} templateId=${templateId} /> +
+
+
+ `; +} + +export function PDPApp({ sdkStore, templateId }) { + return html` + <${StoreProvider} sdkStore=${sdkStore}> + <${DrawerProvider}> + <${PDPContent} templateId=${templateId} /> + + + `; +} + +export default async function decorate(block) { + const templateId = extractTemplateId(block); + block.innerHTML = ''; + const mountPoint = document.createElement('div'); + block.append(mountPoint); + const store = await createZazzleStore(); + render(html`<${PDPApp} sdkStore=${store.sdk} templateId=${templateId} />`, mountPoint); + const { loadStyle, getConfig } = await import(`${getLibs()}/utils/utils.js`); + const { codeRoot } = getConfig(); + await Promise.all([ + new Promise((resolve) => { + loadStyle(`${codeRoot}/scripts/widgets/simple-carousel.css`, resolve); + }), + new Promise((resolve) => { + loadStyle(`${codeRoot}/scripts/widgets/picker.css`, resolve); + }), + ]); +} diff --git a/express/code/blocks/print-product-detail-sdk/sdk/index.d.ts b/express/code/blocks/print-product-detail-sdk/sdk/index.d.ts new file mode 100644 index 000000000..bf0d53531 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/sdk/index.d.ts @@ -0,0 +1,418 @@ +export type PDPState = { + /** + * All data needed to render the product attribute UI (titles, selectors, values, etc.). + */ + attributes: Attribute[]; + /** + * The components that make up the product description, one for each attribute with a description. + */ + descriptionComponents: DescriptionComponent[]; + /** + * The `?productSettings` string to add to the Express URL, which facilitates state transfer to the add-on. + */ + expressProductSettings: string; + /** + * Pricing info for the currently selected options and quantity. + */ + pricing: PricingInfo; + /** + * The Zazzle-centric product type (e.g. `zazzle_shirt`). + */ + productType: AdobeKnownZazzleProductType; + /** + * The currently selected quantity. + */ + quantity: number; + /** + * The list of quantity options to show in the quantity selector. + */ + quantityOptions: QuantityOption[]; + /** + * All views (known to Zazzle as realviews) showcasing the product. Images and videos may be present. + * @see selectedRealview + * @see selectRealview + */ + realviews: Realview[]; + /** + * Overall reviews statistics. + */ + reviewsStats?: ReviewsStats | undefined; + /** + * The currently selected realview. + * @see realviews + * @see selectRealview + */ + selectedRealview: Realview; + /** + * The range of possible worst-case delivery dates (e.g. 'Nov 23 - 27') + */ + shippingEstimate: string; + /** + * The title of the product. + */ + title: string; + /** + * The unit label that's applicable to the current quantity (e.g. 'shirt' or 'shirts'). + * @see quantity + */ + unitLabel: string; +}; +export type Attribute = { + /** + * When present, render a help link alongside the attribute title, which will show more info when clicked. + */ + helpLink: AttributeHelpLink | undefined; + /** + * The attribute's internal name (e.g. 'color'). + * Use this as the first parameter when selecting an option. + * + * @see selectOption + */ + name: string; + /** The currently selected option's value, or internal name (e.g. 'red'). */ + selectedOptionValue: string; + /** The human readable title of the currently selected option (e.g. 'Red'). */ + selectedOptionTitle: string; + /** + * Data powering the actual UI selector (thumbnails, radio buttons, dropdown, etc.). + * Each selector type has its own data structure. See the associated types for details. + */ + selector: AttributeSelectorThumbnails | AttributeSelectorRadio | AttributeSelectorCheckbox | AttributeSelectorDropdown; + /** The human readable title of the attribute (e.g. 'Color'). */ + title: string; +}; +export type AttributeHelpLink = { + /** + * For now, size chart is the only supported help link dialog type. + * @see fetchSizeChart + */ + dialogType: 'sizeChart'; + /** The label of the link that should open the dialog. */ + label: string; + type: 'dialog'; +}; +/** This selector type doesn't appear to currently be in use :/ */ +export type AttributeSelectorCheckbox = { + /** + * The value, or internal name, of the option that represents the checked state. + * (Under the hood, a checkbox is really a semantic UI element that selects between two underlying options.) + * Use this as the second parameter when selecting an option. + * @see selectOption + */ + checkedValue: string; + /** Will only be set when configured to show and non-zero. */ + priceDelta?: string; + /** The human readable title of the checkbox (e.g. 'High Definition'). */ + title: string; + type: 'checkbox'; + /** + * The value, or internal name, of the option that represents the unchecked state. + * (Under the hood, a checkbox is really a semantic UI element that selects between two underlying options.) + * Use this as the second parameter when selecting an option. + * @see selectOption + */ + uncheckedValue: string; +}; +export type AttributeSelectorDropdown = { + /** An optional message to display beneath the dropdown (e.g. 'Unisex sizing') */ + message?: string; + options: { + /** Will only be set when configured to show and non-zero. */ + priceDelta?: string; + /** The human readable title of the option (e.g. 'Red'). */ + title: string; + /** + * The value, or internal name, of the option. + * Use this as the second parameter when selecting an option. + * @see selectOption + */ + value: string; + }[]; + type: 'dropdown'; +}; +export type AttributeSelectorRadio = { + options: { + /** Will only be set when configured to show and non-zero. */ + priceDelta?: string; + /** The human readable title of the option (e.g. 'Red'). */ + title: string; + /** + * The value, or internal name, of the option. + * Use this as the second parameter when selecting an option. + * @see selectOption + */ + value: string; + }[]; + type: 'radio'; +}; +export type AttributeSelectorThumbnails = { + /** + * In most cases, there will be only one group. + * But in cases like shirts, there will be multiple, with unique labels. + */ + optionGroups: { + options: { + /** Replace the `max_dim` param value with whatever resolution you'd like to use. */ + imageUrl: string; + /** Will only be set when configured to show and non-zero. */ + priceDelta?: string; + /** The human readable title of the option (e.g. 'Red'). */ + title: string; + /** + * The value, or internal name, of the option. + * Use this as the second parameter when selecting an option. + * @see selectOption + */ + value: string; + }[]; + /** The human readable title of the option group (e.g. 'Classic printing: no underbase'). */ + title?: string; + }[]; + /** + * When present, show a larger preview of the currently selected option, along with a description of the option. + */ + preview?: { + /** Will likely contain HTML elements, like `
  • `. */ + descriptionHTML: string; + /** Replace the `max_dim` param value with whatever resolution you'd like to use. */ + imageUrl: string; + /** + * The human readable title of the option, primarily for a11y (equivalent to `attribute.selectedOptionTitle`). + */ + optionTitle: string; + }; + type: 'thumbnails'; +}; +export type DescriptionComponent = { + /** The human readable title of the attribute (e.g. 'Color'). */ + attributeTitle: string; + /** + * The description contextual to the currently selected option. + * Will likely contain HTML elements, like `
  • `. + */ + descriptionHTML: string; + /** The human readable title of the currently selected option (e.g. 'Red'). */ + selectedValueTitle: string; +}; +export type QuantityOption = { + /** + * Will only be set when the discount on this quantity is better than the currently applied discount (e.g. '50%'). + */ + discount: string | undefined; + /** The label for the quantity option (e.g. '1 shirt') */ + label: string; + quantity: number; +}; +export type PricingInfo = { + /** + * The label for the currently applied discount. For example, + * 'You save 40% (LETZCELEBRATE)' or 'You save 55% (Volume Discount)' + */ + discountLabel: string | undefined; + /** The total price (respecting quantity), before any discounts */ + originalTotalPrice: string; + /** The unit price, before any discounts */ + originalUnitPrice: string; + /** The currently active promo code, iff that's what driving the current price */ + promoCode: string | undefined; + /** Whether to show the "Comp Value" UI */ + showCompValue: boolean; + /** The total price (respecting quantity), including any discounts */ + totalPrice: string; + /** The unit price, including any discounts */ + unitPrice: string; +}; +export type Realview = RealviewImage | RealviewVideo; +export type RealviewBase = { + /** The unique identifier for the realview. Use this for determining which realview is selected. */ + id: string; + title: string; + /** + * The URL for the image (or video thumbnnail). + * Replace the `max_dim` param value with whatever resolution you'd like to use. + */ + url: string; +}; +export type RealviewImage = RealviewBase & { + type: 'image'; +}; +export type RealviewVideo = RealviewBase & { + /** + * The source URL for the video in MP4 format (i.e. use `type="video/mp4"`). + * For some video types, there will be a `max_dim` param in the URL. If so, replace that value with whatever resolution you'd like to use. + */ + mp4Source: string; + type: 'video'; +}; +export type ReviewsStats = { + /** The total number of reviews for the product */ + count: number; + /** The average rating for the product, from 1 to 5 */ + rating: number; +}; +/** + * Zazzle product types that the Adobe world knows about. + */ +export type AdobeKnownZazzleProductType = 'mojo_throwpillow' | 'zazzle_bag' | 'zazzle_businesscard' | 'zazzle_flyer' | 'zazzle_foldedthankyoucard' | 'zazzle_invitation3' | 'zazzle_mug' | 'zazzle_print' | 'zazzle_shirt' | 'zazzle_sticker'; +export declare namespace SizeChart { + interface ISizeChart { + defaultToMetric?: boolean; + fitStyle?: string; + imageRealViewUrl?: string; + isUnisex: boolean; + modelInfo?: SizeChart.ISizeMeasurement[]; + sizeChart?: SizeChart.IChart; + title?: string; + } + interface IModelInfo { + bodyMeasurements: ISizeMeasurement[]; + wearingMeasurements: ISizeMeasurement[]; + } + interface IChart { + /** an array of measurement types in the size chart, defining the order of columns from left to right */ + measurementTypes: IMeasurementType[]; + /** an array of attribute value rows */ + attributeValues: IAttributeValueSizeMeasurement[]; + } + interface IMeasurementType { + label: string; + key: TMeasurementKey; + extraDescription: string; + } + type TMeasurementKey = 'bodyChest' | 'bodyWaist' | 'bodyHip' | 'bodyWeight' | 'bodyAge' | 'garmentWidth' | 'garmentHeight' | 'inseam'; + type TMeasurementTypeCategory = 'body' | 'garment'; + interface IAttributeValueSizeMeasurement { + attributeValueLabel: string; + measurements: { + [key in TMeasurementKey]: ISizeMeasurement; + }; + } + interface ISizeMeasurement { + label?: string; + metric: string; + imperial: string; + } +} +/** + * # createZazzlePDPStore + * + * Creates a Zazzle PDP store, which has several key responsibilities: + * 1) Abstract away Zazzle API calls, so you only have to deal with high-level methods like `fetchProduct` and `selectOption`. + * 2) Maintain an immutable data store representing the current state of the PDP. + * 3) Translate raw API data into a more convenient format for rendering the PDP. + * + * ## Usage + * + * 1) Create a store instance. + * ```ts + * const store = createZazzlePDPStore({ region: 'us', language: 'en' }); + * ``` + * 2) Fetch product data. + * ```ts + * await store.fetchProduct('template123'); + * ``` + * 3) Subscribe to data changes so you can update your UI. (Unsubscribe by calling the function returned from `subscribe`.) + * ```ts + * const unsubscribe = store.subscribe(() => { + * const newState = store.getSnapshot(); + * // Update your UI based on the new state + * }); + * ``` + * + * ## Reactive optimistic data model + * + * The store interaction/update loop assumes that the UI is fully driven by the state. User input responsiveness is solved with optimistic updates. + * As such, the UI should not be remembering any of the state on its own, and should instead fully defer to the state. + * + * For example, calling `selectOption('color', 'red')` will result in several state updates, all of which will trigger the subscription callback. This is the flow. + * 1) Update the state immediately with the newly selected color option (`attributes` will change). + * 2) Fetch the new state of the attributes from the API and update the state again (`attributes` will change, potentially with a different set of options). + * 3) Fetch new pricing and shipping estimate data concurrently, and update the state when each one returns. + * + * This approach results in immediate optimistic user feedback and async state updates as new data is received, just like on zazzle.com. + * + * ## Immutability + * + * The state is maintained immutably, so you can use `===` to compare previous and current state (at any level of the tree) to optimize re-renders. + * ```ts + * const shouldRerenderAttributesUi = previousState.attributes !== currentState.attributes; + * ``` + * + * ## useSyncExternalStore support + * + * This store directly integrates with the `useSyncExternalStore` API in UI frameworks like React and Preact. + * ```ts + * const [zazzlePDP] = useState(() => createZazzlePDPStore({ language: 'en', region: 'us' })); + * const pdpState = useSyncExternalStore(zazzlePDP.subscribe, zazzlePDP.getSnapshot); + * ``` + */ +export declare function createZazzlePDPStore(options: { + /** + * ISO 639-1 language code + * @example "en" + * + * If not provided, we use the default language for the given `region`. + */ + language?: string; + /** + * ISO 3166-1 country code + * @example "us" + */ + region: string; +}): { + /** Constants */ + env: { + /** + * ISO 4217 currency code + * @example "USD" + */ + currency: ZUIBase.ZCurrency; + /** + * ISO 639-1 language code + * @example "en" + * + * If not provided, we use the default language for the given `region`. + */ + language: "en" | "de" | "es" | "fr" | "ja" | "ko" | "nl" | "pt" | "sv"; + /** + * ISO 3166-1 country code + * @example "us" + */ + region: "at" | "br" | "us" | "au" | "ca" | "gb" | "nz" | "de" | "ch" | "es" | "fr" | "be" | "jp" | "kr" | "nl" | "pt" | "se"; + }; + /** + * Subscribes to changes in the data + * @param callback Function that will be called when the data changes + * @returns A function to unsubscribe the callback + */ + subscribe: (callback: () => void) => () => void; + /** + * Gets the current state (you likely want to call this inside a subscription callback) + * @see subscribe + */ + getSnapshot: () => Readonly; + /** + * Fetches product (and accompanying) data for the given `templateId`. + * This should be called once during page init, and then likely never again. + */ + fetchProduct(templateId: string): Promise; + /** + * Fetches size chart data for the current product state. + * (This data is not stored in the state, since it's only needed on user action, and will be stale with any option change.) + * @see AttributeHelpLink + */ + fetchSizeChart(): Promise; + /** + * Optimistically updates the selected option, then syncs with the API. + */ + selectOption(attributeName: string, value: string): Promise; + /** + * Optimistically updates the quantity, then syncs with the API. + */ + selectQuantity(quantity: number): Promise; + /** + * Optimistically updates the selected realview, then syncs with the API. + */ + selectRealview(realviewId: string): Promise; +}; +export default createZazzlePDPStore; diff --git a/express/code/blocks/print-product-detail-sdk/sdk/index.js b/express/code/blocks/print-product-detail-sdk/sdk/index.js new file mode 100644 index 000000000..8cf4e4490 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/sdk/index.js @@ -0,0 +1,6 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));function l(e){return Array.isArray(e)?e:Array.from(e)}function u(e){return Number.isSafeInteger(e)&&e>=0}function d(e){return e!=null&&typeof e!=`function`&&u(e.length)}function f(e,t=1){let n=[],r=Math.floor(t),i=(e,t)=>{for(let a=0;a0?E(e,{...t},n,r):w(e,t);default:return S(e)?typeof t==`string`?t===``:!0:w(e,t)}}function D(e,t,n,r){if(t==null)return!0;if(Array.isArray(t))return k(e,t,n,r);if(t instanceof Map)return O(e,t,n,r);if(t instanceof Set)return A(e,t,n,r);let i=Object.keys(t);if(e==null)return i.length===0;if(i.length===0)return!0;if(r&&r.has(t))return r.get(t)===e;r&&r.set(t,e);try{for(let a=0;avoid 0)}function M(e){return Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))}function N(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}var P=`[object RegExp]`,F=`[object String]`,I=`[object Number]`,L=`[object Boolean]`,R=`[object Arguments]`,z=`[object Symbol]`,B=`[object Date]`,V=`[object Map]`,H=`[object Set]`,U=`[object Array]`,ee=`[object ArrayBuffer]`,te=`[object Object]`,ne=`[object DataView]`,re=`[object Uint8Array]`,ie=`[object Uint8ClampedArray]`,ae=`[object Uint16Array]`,oe=`[object Uint32Array]`,se=`[object Int8Array]`,W=`[object Int16Array]`,G=`[object Int32Array]`,ce=`[object Float32Array]`,le=`[object Float64Array]`;function ue(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function de(e,t){return K(e,void 0,e,new Map,t)}function K(e,t,n,r=new Map,i=void 0){let a=i?.(e,t,n,r);if(a!==void 0)return a;if(C(e))return e;if(r.has(e))return r.get(e);if(Array.isArray(e)){let t=Array(e.length);r.set(e,t);for(let a=0;aj(t,e)}function he(e,t){return de(e,(n,r,i,a)=>{let o=t?.(n,r,i,a);if(o!==void 0)return o;if(typeof e==`object`)switch(Object.prototype.toString.call(e)){case I:case F:case L:{let t=new e.constructor(e?.valueOf());return q(t,e),t}case R:{let t={};return q(t,e),t.length=e.length,t[Symbol.iterator]=e[Symbol.iterator],t}default:return}})}function J(e){return he(e)}var ge=/^(?:0|[1-9]\d*)$/;function _e(e,t=2**53-1){switch(typeof e){case`number`:return Number.isInteger(e)&&e>=0&&e!t(e,n,r));return n===-1?[]:e.slice(n)}function De(e,t=p){return d(e)?Oe(l(e),t):[]}function Oe(e,t){switch(typeof t){case`function`:return X(e,(e,n,r)=>!!t(e,n,r));case`object`:if(Array.isArray(t)&&t.length===2){let n=t[0],r=t[1];return X(e,be(n,r))}else return X(e,me(t));case`number`:case`symbol`:case`string`:return X(e,x(t))}}function ke(e,t,n=1){if(t??(t=e,e=0),!Number.isInteger(n)||n===0)throw Error(`The step value must be a non-zero integer.`);let r=Math.max(Math.ceil((t-e)/n),0),i=Array(r);for(let t=0;t{for(let a=0;an.has(e))}function Re(e){return[...new Set(e)]}function ze(...e){if(e.length===0||!Se(e[0]))return[];let t=Re(Array.from(e[0]));for(let n=1;n{if(e!==t){let r=Ve(e),i=Ve(t);if(r===i&&r===0){if(et)return n===`desc`?-1:1}return n===`desc`?i-r:r-i}return 0},Ue=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,We=/^\w*$/;function Ge(e,t){return Array.isArray(e)?!1:typeof e==`number`||typeof e==`boolean`||e==null||Ce(e)?!0:typeof e==`string`&&(We.test(e)||!Ue.test(e))||t!=null&&Object.hasOwn(t,e)}function Ke(e,t,n,r){if(e==null)return[];n=r?void 0:n,Array.isArray(e)||(e=Object.values(e)),Array.isArray(t)||(t=t==null?[null]:[t]),t.length===0&&(t=[null]),Array.isArray(n)||(n=n==null?[]:[n]),n=n.map(e=>String(e));let i=(e,t)=>{let n=e;for(let e=0;et==null||e==null?t:typeof e==`object`&&`key`in e?Object.hasOwn(t,e.key)?t[e.key]:i(t,e.path):typeof e==`function`?e(t):Array.isArray(e)?i(t,e):typeof t==`object`?t[e]:t,o=t.map(e=>(Array.isArray(e)&&e.length===1&&(e=e[0]),e==null||typeof e==`function`||Array.isArray(e)||Ge(e)?e:{key:e,path:v(e)}));return e.map(e=>({original:e,criteria:o.map(t=>a(t,e))})).slice().sort((e,t)=>{for(let r=0;re.original)}function qe(e,t){if(e==null)return!0;switch(typeof t){case`symbol`:case`number`:case`object`:if(Array.isArray(t))return Je(e,t);if(typeof t==`number`?t=g(t):typeof t==`object`&&(t=Object.is(t?.valueOf(),-0)?`-0`:String(t)),m(t))return!1;if(e?.[t]===void 0)return!0;try{return delete e[t],!0}catch{return!1}case`string`:if(e?.[t]===void 0&&h(t))return Je(e,v(t));if(m(t))return!1;try{return delete e[t],!0}catch{return!1}}}function Je(e,t){let n=t.length===1?e:y(e,t.slice(0,-1)),r=t[t.length-1];if(n?.[r]===void 0)return!0;if(m(r))return!1;try{return delete n[r],!0}catch{return!1}}function Ye(e,t,n){return n==null?Math.min(e,t):Math.min(Math.max(e,t),n)}function Xe(e,t,n){return Number.isNaN(t)&&(t=0),Number.isNaN(n)&&(n=0),Ye(e,t,n)}function Ze(e){return e==null}function Qe(e,...t){let n=t.length;return n>1&&je(e,t[0],t[1])?t=[]:n>2&&je(t[0],t[1],t[2])&&(t=[t[0]]),Ke(e,f(t),[`asc`])}function $e(e){return e}var et=(e,t,n)=>{let r=e[t];(!(Object.hasOwn(e,t)&&w(r,n))||n===void 0&&!(t in e))&&(e[t]=n)};function tt(e,t,n,r){if(e==null&&!S(e))return e;let i=Ge(t,e)?[t]:Array.isArray(t)?t:typeof t==`string`?v(t):[t],a=e;for(let t=0;tn,()=>void 0)}function rt(){}function it(e){return typeof Buffer<`u`&&Buffer.isBuffer(e)}function at(e){let t=e?.constructor;return e===(typeof t==`function`?t.prototype:Object.prototype)}function ot(e){return ue(e)}function st(e,t){if(e=Ee(e),e<1||!Number.isSafeInteger(e))return[];let n=Array(e);for(let r=0;re!==`constructor`):t}function lt(e){let t=st(e.length,e=>`${e}`),n=new Set(t);return it(e)&&(n.add(`offset`),n.add(`parent`)),ot(e)&&(n.add(`buffer`),n.add(`byteLength`),n.add(`byteOffset`)),[...t,...Object.keys(e).filter(e=>!n.has(e))]}function ut(e){if(e==null)return[];switch(typeof e){case`object`:case`function`:return d(e)?pt(e):at(e)?ft(e):dt(e);default:return dt(Object(e))}}function dt(e){let t=[];for(let n in e)t.push(n);return t}function ft(e){return dt(e).filter(e=>e!==`constructor`)}function pt(e){let t=st(e.length,e=>`${e}`),n=new Set(t);return it(e)&&(n.add(`offset`),n.add(`parent`)),ot(e)&&(n.add(`buffer`),n.add(`byteLength`),n.add(`byteOffset`)),[...t,...dt(e).filter(e=>!n.has(e))]}function mt(e){if(typeof e!=`object`||!e)return!1;if(Object.getPrototypeOf(e)===null)return!0;if(Object.prototype.toString.call(e)!==`[object Object]`){let t=e[Symbol.toStringTag];return t==null||!Object.getOwnPropertyDescriptor(e,Symbol.toStringTag)?.writable?!1:e.toString()===`[object ${t}]`}let t=e;for(;Object.getPrototypeOf(t)!==null;)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t}function ht(e,...t){e=Object(e);for(let n=0;nArray.isArray(e)||h(e))?jt(e):At(e)}function At(e){let t={},n=[...ut(e),...Dt(e)];for(let r=0;r{if(!mt(e))return e})}return t}function Mt(e,...t){if(Ze(e))return{};let n={};for(let r=0;r`u`||!Buffer.isBuffer(e))&&!ot(e)&&!ve(e)?!1:e.length===0;if(typeof e==`object`){if(e instanceof Map||e instanceof Set)return e.size===0;let t=Object.keys(e);return at(e)?t.filter(e=>e!==`constructor`).length===0:t.length===0}return!0}function Ft(e){return N(e)===`[object Error]`}function It(e,t,n){return _(e).split(t,n)}var Lt=.1,Rt=1,zt=99999,Bt=1;function Vt(e,t,n,r){t??=Rt,n??=zt,r??=Bt;let i=Xe(e,t,n);return Math.round(Math.ceil(i/r)*r)}function Ht(e,t,n){let r=[];if(e=De(e,e=>!e.hasDiscount),e.length>0){if(n)r=Ut(e,t);else{let n;for(let i=0,a=0;i1){let e,t;for(let n=0;n({quantity:e}))}function Ut(e,t){let n=[];for(let r=0,i=0;ro.maxQuantity?i++:(n.push({quantity:a,discount:o}),r++)}return n}var Wt=o(((e,t)=>{(function(n){var r=typeof e==`object`&&e&&!e.nodeType&&e,i=typeof t==`object`&&t&&!t.nodeType&&t,a=typeof global==`object`&&global;(a.global===a||a.window===a||a.self===a)&&(n=a);var o,s=2147483647,c=36,l=1,u=26,d=38,f=700,p=72,m=128,h=`-`,g=/^xn--/,_=/[^\x20-\x7E]/,v=/[\x2E\u3002\uFF0E\uFF61]/g,y={overflow:`Overflow: input needs wider integers to process`,"not-basic":`Illegal input >= 0x80 (not a basic code point)`,"invalid-input":`Invalid input`},b=c-l,x=Math.floor,S=String.fromCharCode,C;function w(e){throw RangeError(y[e])}function T(e,t){for(var n=e.length,r=[];n--;)r[n]=t(e[n]);return r}function E(e,t){var n=e.split(`@`),r=``;n.length>1&&(r=n[0]+`@`,e=n[1]),e=e.replace(v,`.`);var i=T(e.split(`.`),t).join(`.`);return r+i}function D(e){for(var t=[],n=0,r=e.length,i,a;n=55296&&i<=56319&&n65535&&(e-=65536,t+=S(e>>>10&1023|55296),e=56320|e&1023),t+=S(e),t}).join(``)}function k(e){return e-48<10?e-22:e-65<26?e-65:e-97<26?e-97:c}function A(e,t){return e+22+75*(e<26)-((t!=0)<<5)}function j(e,t,n){var r=0;for(e=n?x(e/f):e>>1,e+=x(e/t);e>b*u>>1;r+=c)e=x(e/b);return x(r+(b+1)*e/(e+d))}function M(e){var t=[],n=e.length,r,i=0,a=m,o=p,d=e.lastIndexOf(h),f,g,_,v,y,b,S,C;for(d<0&&(d=0),f=0;f=128&&w(`not-basic`),t.push(e.charCodeAt(f));for(g=d>0?d+1:0;g=n&&w(`invalid-input`),b=k(e.charCodeAt(g++)),(b>=c||b>x((s-i)/v))&&w(`overflow`),i+=b*v,S=y<=o?l:y>=o+u?u:y-o,!(bx(s/C)&&w(`overflow`),v*=C;r=t.length+1,o=j(i-_,r,_==0),x(i/r)>s-a&&w(`overflow`),a+=x(i/r),i%=r,t.splice(i++,0,a)}return O(t)}function N(e){var t,n,r,i,a,o,d,f,g,_,v,y=[],b,C,T,E;for(e=D(e),b=e.length,t=m,n=0,a=p,o=0;o=t&&vx((s-n)/C)&&w(`overflow`),n+=(d-t)*C,t=d,o=0;os&&w(`overflow`),v==t){for(f=n,g=c;_=g<=a?l:g>=a+u?u:g-a,!(f<_);g+=c)E=f-_,T=c-_,y.push(S(A(_+E%T,0))),f=x(E/T);y.push(S(A(f,0))),a=j(n,C,r==i),n=0,++r}++n,++t}return y.join(``)}function P(e){return E(e,function(e){return g.test(e)?M(e.slice(4).toLowerCase()):e})}function F(e){return E(e,function(e){return _.test(e)?`xn--`+N(e):e})}if(o={version:`1.4.1`,ucs2:{decode:D,encode:O},decode:M,encode:N,toASCII:F,toUnicode:P},typeof define==`function`&&typeof define.amd==`object`&&define.amd)define(`punycode`,function(){return o});else if(r&&i)if(t.exports==r)i.exports=o;else for(C in o)o.hasOwnProperty(C)&&(r[C]=o[C]);else n.punycode=o})(e)})),Gt=o(((e,t)=>{t.exports=TypeError})),Kt=o(((e,t)=>{t.exports={}})),qt=o(((e,t)=>{var n=typeof Map==`function`&&Map.prototype,r=Object.getOwnPropertyDescriptor&&n?Object.getOwnPropertyDescriptor(Map.prototype,`size`):null,i=n&&r&&typeof r.get==`function`?r.get:null,a=n&&Map.prototype.forEach,o=typeof Set==`function`&&Set.prototype,s=Object.getOwnPropertyDescriptor&&o?Object.getOwnPropertyDescriptor(Set.prototype,`size`):null,c=o&&s&&typeof s.get==`function`?s.get:null,l=o&&Set.prototype.forEach,u=typeof WeakMap==`function`&&WeakMap.prototype?WeakMap.prototype.has:null,d=typeof WeakSet==`function`&&WeakSet.prototype?WeakSet.prototype.has:null,f=typeof WeakRef==`function`&&WeakRef.prototype?WeakRef.prototype.deref:null,p=Boolean.prototype.valueOf,m=Object.prototype.toString,h=Function.prototype.toString,g=String.prototype.match,_=String.prototype.slice,v=String.prototype.replace,y=String.prototype.toUpperCase,b=String.prototype.toLowerCase,x=RegExp.prototype.test,S=Array.prototype.concat,C=Array.prototype.join,w=Array.prototype.slice,T=Math.floor,E=typeof BigInt==`function`?BigInt.prototype.valueOf:null,D=Object.getOwnPropertySymbols,O=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?Symbol.prototype.toString:null,k=typeof Symbol==`function`&&typeof Symbol.iterator==`object`,A=typeof Symbol==`function`&&Symbol.toStringTag&&(typeof Symbol.toStringTag===k||`symbol`)?Symbol.toStringTag:null,j=Object.prototype.propertyIsEnumerable,M=(typeof Reflect==`function`?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(e){return e.__proto__}:null);function N(e,t){if(e===1/0||e===-1/0||e!==e||e&&e>-1e3&&e<1e3||x.call(/e/,t))return t;var n=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if(typeof e==`number`){var r=e<0?-T(-e):T(e);if(r!==e){var i=String(r),a=_.call(t,i.length+1);return v.call(i,n,`$&_`)+`.`+v.call(v.call(a,/([0-9]{3})/g,`$&_`),/_$/,``)}}return v.call(t,n,`$&_`)}var P=Kt(),F=P.custom,I=ae(F)?F:null,L={__proto__:null,double:`"`,single:`'`},R={__proto__:null,double:/(["\\])/g,single:/(['\\])/g};t.exports=function e(t,n,r,o){var s=n||{};if(W(s,`quoteStyle`)&&!W(L,s.quoteStyle))throw TypeError(`option "quoteStyle" must be "single" or "double"`);if(W(s,`maxStringLength`)&&(typeof s.maxStringLength==`number`?s.maxStringLength<0&&s.maxStringLength!==1/0:s.maxStringLength!==null))throw TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var u=W(s,`customInspect`)?s.customInspect:!0;if(typeof u!=`boolean`&&u!==`symbol`)throw TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(W(s,`indent`)&&s.indent!==null&&s.indent!==` `&&!(parseInt(s.indent,10)===s.indent&&s.indent>0))throw TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(W(s,`numericSeparator`)&&typeof s.numericSeparator!=`boolean`)throw TypeError('option "numericSeparator", if provided, must be `true` or `false`');var d=s.numericSeparator;if(t===void 0)return`undefined`;if(t===null)return`null`;if(typeof t==`boolean`)return t?`true`:`false`;if(typeof t==`string`)return me(t,s);if(typeof t==`number`){if(t===0)return 1/0/t>0?`0`:`-0`;var f=String(t);return d?N(t,f):f}if(typeof t==`bigint`){var m=String(t)+`n`;return d?N(t,m):m}var h=s.depth===void 0?5:s.depth;if(r===void 0&&(r=0),r>=h&&h>0&&typeof t==`object`)return H(t)?`[Array]`:`[Object]`;var g=ye(s,r);if(o===void 0)o=[];else if(le(o,t)>=0)return`[Circular]`;function y(t,n,i){if(n&&(o=w.call(o),o.push(n)),i){var a={depth:s.depth};return W(s,`quoteStyle`)&&(a.quoteStyle=s.quoteStyle),e(t,a,r+1,o)}return e(t,s,r+1,o)}if(typeof t==`function`&&!ee(t)){var x=ce(t),T=Y(t,y);return`[Function`+(x?`: `+x:` (anonymous)`)+`]`+(T.length>0?` { `+C.call(T,`, `)+` }`:``)}if(ae(t)){var D=k?v.call(String(t),/^(Symbol\(.*\))_[^)]*$/,`$1`):O.call(t);return typeof t==`object`&&!k?J(D):D}if(pe(t)){for(var F=`<`+b.call(String(t.nodeName)),R=t.attributes||[],V=0;V`,t.childNodes&&t.childNodes.length&&(F+=`...`),F+=``,F}if(H(t)){if(t.length===0)return`[]`;var se=Y(t,y);return g&&!ve(se)?`[`+be(se,g)+`]`:`[ `+C.call(se,`, `)+` ]`}if(te(t)){var he=Y(t,y);return!(`cause`in Error.prototype)&&`cause`in t&&!j.call(t,`cause`)?`{ [`+String(t)+`] `+C.call(S.call(`[cause]: `+y(t.cause),he),`, `)+` }`:he.length===0?`[`+String(t)+`]`:`{ [`+String(t)+`] `+C.call(he,`, `)+` }`}if(typeof t==`object`&&u){if(I&&typeof t[I]==`function`&&P)return P(t,{depth:h-r});if(u!==`symbol`&&typeof t.inspect==`function`)return t.inspect()}if(ue(t)){var xe=[];return a&&a.call(t,function(e,n){xe.push(y(n,t,!0)+` => `+y(e,t))}),_e(`Map`,i.call(t),xe,g)}if(q(t)){var Se=[];return l&&l.call(t,function(e){Se.push(y(e,t))}),_e(`Set`,c.call(t),Se,g)}if(de(t))return ge(`WeakMap`);if(fe(t))return ge(`WeakSet`);if(K(t))return ge(`WeakRef`);if(re(t))return J(y(Number(t)));if(oe(t))return J(y(E.call(t)));if(ie(t))return J(p.call(t));if(ne(t))return J(y(String(t)));if(typeof window<`u`&&t===window)return`{ [object Window] }`;if(typeof globalThis<`u`&&t===globalThis||typeof global<`u`&&t===global)return`{ [object globalThis] }`;if(!U(t)&&!ee(t)){var Ce=Y(t,y),we=M?M(t)===Object.prototype:t instanceof Object||t.constructor===Object,Te=t instanceof Object?``:`null prototype`,Ee=!we&&A&&Object(t)===t&&A in t?_.call(G(t),8,-1):Te?`Object`:``,X=(we||typeof t.constructor!=`function`?``:t.constructor.name?t.constructor.name+` `:``)+(Ee||Te?`[`+C.call(S.call([],Ee||[],Te||[]),`: `)+`] `:``);return Ce.length===0?X+`{}`:g?X+`{`+be(Ce,g)+`}`:X+`{ `+C.call(Ce,`, `)+` }`}return String(t)};function z(e,t,n){var r=L[n.quoteStyle||t];return r+e+r}function B(e){return v.call(String(e),/"/g,`"`)}function V(e){return!A||!(typeof e==`object`&&(A in e||e[A]!==void 0))}function H(e){return G(e)===`[object Array]`&&V(e)}function U(e){return G(e)===`[object Date]`&&V(e)}function ee(e){return G(e)===`[object RegExp]`&&V(e)}function te(e){return G(e)===`[object Error]`&&V(e)}function ne(e){return G(e)===`[object String]`&&V(e)}function re(e){return G(e)===`[object Number]`&&V(e)}function ie(e){return G(e)===`[object Boolean]`&&V(e)}function ae(e){if(k)return e&&typeof e==`object`&&e instanceof Symbol;if(typeof e==`symbol`)return!0;if(!e||typeof e!=`object`||!O)return!1;try{return O.call(e),!0}catch{}return!1}function oe(e){if(!e||typeof e!=`object`||!E)return!1;try{return E.call(e),!0}catch{}return!1}var se=Object.prototype.hasOwnProperty||function(e){return e in this};function W(e,t){return se.call(e,t)}function G(e){return m.call(e)}function ce(e){if(e.name)return e.name;var t=g.call(h.call(e),/^function\s*([\w$]+)/);return t?t[1]:null}function le(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0,r=e.length;nt.maxStringLength){var n=e.length-t.maxStringLength,r=`... `+n+` more character`+(n>1?`s`:``);return me(_.call(e,0,t.maxStringLength),t)+r}var i=R[t.quoteStyle||`single`];return i.lastIndex=0,z(v.call(v.call(e,i,`\\$1`),/[\x00-\x1f]/g,he),`single`,t)}function he(e){var t=e.charCodeAt(0),n={8:`b`,9:`t`,10:`n`,12:`f`,13:`r`}[t];return n?`\\`+n:`\\x`+(t<16?`0`:``)+y.call(t.toString(16))}function J(e){return`Object(`+e+`)`}function ge(e){return e+` { ? }`}function _e(e,t,n,r){var i=r?be(n,r):C.call(n,`, `);return e+` (`+t+`) {`+i+`}`}function ve(e){for(var t=0;t=0)return!1;return!0}function ye(e,t){var n;if(e.indent===` `)n=` `;else if(typeof e.indent==`number`&&e.indent>0)n=C.call(Array(e.indent+1),` `);else return null;return{base:n,prev:C.call(Array(t+1),n)}}function be(e,t){if(e.length===0)return``;var n=` +`+t.prev+t.base;return n+C.call(e,`,`+n)+` +`+t.prev}function Y(e,t){var n=H(e),r=[];if(n){r.length=e.length;for(var i=0;i{var n=qt(),r=Gt(),i=function(e,t,n){for(var r=e,i;(i=r.next)!=null;r=i)if(i.key===t)return r.next=i.next,n||(i.next=e.next,e.next=i),i},a=function(e,t){if(e){var n=i(e,t);return n&&n.value}},o=function(e,t,n){var r=i(e,t);r?r.value=n:e.next={key:t,next:e.next,value:n}},s=function(e,t){return e?!!i(e,t):!1},c=function(e,t){if(e)return i(e,t,!0)};t.exports=function(){var e,t={assert:function(e){if(!t.has(e))throw new r(`Side channel does not contain `+n(e))},delete:function(t){var n=e&&e.next,r=c(e,t);return r&&n&&n===r&&(e=void 0),!!r},get:function(t){return a(e,t)},has:function(t){return s(e,t)},set:function(t,n){e||={next:void 0},o(e,t,n)}};return t}})),Yt=o(((e,t)=>{t.exports=Object})),Xt=o(((e,t)=>{t.exports=Error})),Zt=o(((e,t)=>{t.exports=EvalError})),Qt=o(((e,t)=>{t.exports=RangeError})),$t=o(((e,t)=>{t.exports=ReferenceError})),en=o(((e,t)=>{t.exports=SyntaxError})),tn=o(((e,t)=>{t.exports=URIError})),nn=o(((e,t)=>{t.exports=Math.abs})),rn=o(((e,t)=>{t.exports=Math.floor})),an=o(((e,t)=>{t.exports=Math.max})),on=o(((e,t)=>{t.exports=Math.min})),sn=o(((e,t)=>{t.exports=Math.pow})),cn=o(((e,t)=>{t.exports=Math.round})),ln=o(((e,t)=>{t.exports=Number.isNaN||function(e){return e!==e}})),un=o(((e,t)=>{var n=ln();t.exports=function(e){return n(e)||e===0?e:e<0?-1:1}})),dn=o(((e,t)=>{t.exports=Object.getOwnPropertyDescriptor})),fn=o(((e,t)=>{var n=dn();if(n)try{n([],`length`)}catch{n=null}t.exports=n})),pn=o(((e,t)=>{var n=Object.defineProperty||!1;if(n)try{n({},`a`,{value:1})}catch{n=!1}t.exports=n})),mn=o(((e,t)=>{t.exports=function(){if(typeof Symbol!=`function`||typeof Object.getOwnPropertySymbols!=`function`)return!1;if(typeof Symbol.iterator==`symbol`)return!0;var e={},t=Symbol(`test`),n=Object(t);if(typeof t==`string`||Object.prototype.toString.call(t)!==`[object Symbol]`||Object.prototype.toString.call(n)!==`[object Symbol]`)return!1;var r=42;for(var i in e[t]=r,e)return!1;if(typeof Object.keys==`function`&&Object.keys(e).length!==0||typeof Object.getOwnPropertyNames==`function`&&Object.getOwnPropertyNames(e).length!==0)return!1;var a=Object.getOwnPropertySymbols(e);if(a.length!==1||a[0]!==t||!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if(typeof Object.getOwnPropertyDescriptor==`function`){var o=Object.getOwnPropertyDescriptor(e,t);if(o.value!==r||o.enumerable!==!0)return!1}return!0}})),hn=o(((e,t)=>{var n=typeof Symbol<`u`&&Symbol,r=mn();t.exports=function(){return typeof n!=`function`||typeof Symbol!=`function`||typeof n(`foo`)!=`symbol`||typeof Symbol(`bar`)!=`symbol`?!1:r()}})),gn=o(((e,t)=>{t.exports=typeof Reflect<`u`&&Reflect.getPrototypeOf||null})),_n=o(((e,t)=>{t.exports=Yt().getPrototypeOf||null})),vn=o(((e,t)=>{var n=`Function.prototype.bind called on incompatible `,r=Object.prototype.toString,i=Math.max,a=`[object Function]`,o=function(e,t){for(var n=[],r=0;r{var n=vn();t.exports=Function.prototype.bind||n})),bn=o(((e,t)=>{t.exports=Function.prototype.call})),xn=o(((e,t)=>{t.exports=Function.prototype.apply})),Sn=o(((e,t)=>{t.exports=typeof Reflect<`u`&&Reflect&&Reflect.apply})),Cn=o(((e,t)=>{var n=yn(),r=xn(),i=bn();t.exports=Sn()||n.call(i,r)})),wn=o(((e,t)=>{var n=yn(),r=Gt(),i=bn(),a=Cn();t.exports=function(e){if(e.length<1||typeof e[0]!=`function`)throw new r(`a function is required`);return a(n,i,e)}})),Tn=o(((e,t)=>{var n=wn(),r=fn(),i;try{i=[].__proto__===Array.prototype}catch(e){if(!e||typeof e!=`object`||!(`code`in e)||e.code!==`ERR_PROTO_ACCESS`)throw e}var a=!!i&&r&&r(Object.prototype,`__proto__`),o=Object,s=o.getPrototypeOf;t.exports=a&&typeof a.get==`function`?n([a.get]):typeof s==`function`?function(e){return s(e==null?e:o(e))}:!1})),En=o(((e,t)=>{var n=gn(),r=_n(),i=Tn();t.exports=n?function(e){return n(e)}:r?function(e){if(!e||typeof e!=`object`&&typeof e!=`function`)throw TypeError(`getProto: not an object`);return r(e)}:i?function(e){return i(e)}:null})),Dn=o(((e,t)=>{var n=Function.prototype.call,r=Object.prototype.hasOwnProperty;t.exports=yn().call(n,r)})),On=o(((e,t)=>{var n,r=Yt(),i=Xt(),a=Zt(),o=Qt(),s=$t(),c=en(),l=Gt(),u=tn(),d=nn(),f=rn(),p=an(),m=on(),h=sn(),g=cn(),_=un(),v=Function,y=function(e){try{return v(`"use strict"; return (`+e+`).constructor;`)()}catch{}},b=fn(),x=pn(),S=function(){throw new l},C=b?function(){try{return arguments.callee,S}catch{try{return b(arguments,`callee`).get}catch{return S}}}():S,w=hn()(),T=En(),E=_n(),D=gn(),O=xn(),k=bn(),A={},j=typeof Uint8Array>`u`||!T?n:T(Uint8Array),M={__proto__:null,"%AggregateError%":typeof AggregateError>`u`?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":typeof ArrayBuffer>`u`?n:ArrayBuffer,"%ArrayIteratorPrototype%":w&&T?T([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":A,"%AsyncGenerator%":A,"%AsyncGeneratorFunction%":A,"%AsyncIteratorPrototype%":A,"%Atomics%":typeof Atomics>`u`?n:Atomics,"%BigInt%":typeof BigInt>`u`?n:BigInt,"%BigInt64Array%":typeof BigInt64Array>`u`?n:BigInt64Array,"%BigUint64Array%":typeof BigUint64Array>`u`?n:BigUint64Array,"%Boolean%":Boolean,"%DataView%":typeof DataView>`u`?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":i,"%eval%":eval,"%EvalError%":a,"%Float16Array%":typeof Float16Array>`u`?n:Float16Array,"%Float32Array%":typeof Float32Array>`u`?n:Float32Array,"%Float64Array%":typeof Float64Array>`u`?n:Float64Array,"%FinalizationRegistry%":typeof FinalizationRegistry>`u`?n:FinalizationRegistry,"%Function%":v,"%GeneratorFunction%":A,"%Int8Array%":typeof Int8Array>`u`?n:Int8Array,"%Int16Array%":typeof Int16Array>`u`?n:Int16Array,"%Int32Array%":typeof Int32Array>`u`?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":w&&T?T(T([][Symbol.iterator]())):n,"%JSON%":typeof JSON==`object`?JSON:n,"%Map%":typeof Map>`u`?n:Map,"%MapIteratorPrototype%":typeof Map>`u`||!w||!T?n:T(new Map()[Symbol.iterator]()),"%Math%":Math,"%Number%":Number,"%Object%":r,"%Object.getOwnPropertyDescriptor%":b,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":typeof Promise>`u`?n:Promise,"%Proxy%":typeof Proxy>`u`?n:Proxy,"%RangeError%":o,"%ReferenceError%":s,"%Reflect%":typeof Reflect>`u`?n:Reflect,"%RegExp%":RegExp,"%Set%":typeof Set>`u`?n:Set,"%SetIteratorPrototype%":typeof Set>`u`||!w||!T?n:T(new Set()[Symbol.iterator]()),"%SharedArrayBuffer%":typeof SharedArrayBuffer>`u`?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":w&&T?T(``[Symbol.iterator]()):n,"%Symbol%":w?Symbol:n,"%SyntaxError%":c,"%ThrowTypeError%":C,"%TypedArray%":j,"%TypeError%":l,"%Uint8Array%":typeof Uint8Array>`u`?n:Uint8Array,"%Uint8ClampedArray%":typeof Uint8ClampedArray>`u`?n:Uint8ClampedArray,"%Uint16Array%":typeof Uint16Array>`u`?n:Uint16Array,"%Uint32Array%":typeof Uint32Array>`u`?n:Uint32Array,"%URIError%":u,"%WeakMap%":typeof WeakMap>`u`?n:WeakMap,"%WeakRef%":typeof WeakRef>`u`?n:WeakRef,"%WeakSet%":typeof WeakSet>`u`?n:WeakSet,"%Function.prototype.call%":k,"%Function.prototype.apply%":O,"%Object.defineProperty%":x,"%Object.getPrototypeOf%":E,"%Math.abs%":d,"%Math.floor%":f,"%Math.max%":p,"%Math.min%":m,"%Math.pow%":h,"%Math.round%":g,"%Math.sign%":_,"%Reflect.getPrototypeOf%":D};if(T)try{null.error}catch(e){M[`%Error.prototype%`]=T(T(e))}var N=function e(t){var n;if(t===`%AsyncFunction%`)n=y(`async function () {}`);else if(t===`%GeneratorFunction%`)n=y(`function* () {}`);else if(t===`%AsyncGeneratorFunction%`)n=y(`async function* () {}`);else if(t===`%AsyncGenerator%`){var r=e(`%AsyncGeneratorFunction%`);r&&(n=r.prototype)}else if(t===`%AsyncIteratorPrototype%`){var i=e(`%AsyncGenerator%`);i&&T&&(n=T(i.prototype))}return M[t]=n,n},P={__proto__:null,"%ArrayBufferPrototype%":[`ArrayBuffer`,`prototype`],"%ArrayPrototype%":[`Array`,`prototype`],"%ArrayProto_entries%":[`Array`,`prototype`,`entries`],"%ArrayProto_forEach%":[`Array`,`prototype`,`forEach`],"%ArrayProto_keys%":[`Array`,`prototype`,`keys`],"%ArrayProto_values%":[`Array`,`prototype`,`values`],"%AsyncFunctionPrototype%":[`AsyncFunction`,`prototype`],"%AsyncGenerator%":[`AsyncGeneratorFunction`,`prototype`],"%AsyncGeneratorPrototype%":[`AsyncGeneratorFunction`,`prototype`,`prototype`],"%BooleanPrototype%":[`Boolean`,`prototype`],"%DataViewPrototype%":[`DataView`,`prototype`],"%DatePrototype%":[`Date`,`prototype`],"%ErrorPrototype%":[`Error`,`prototype`],"%EvalErrorPrototype%":[`EvalError`,`prototype`],"%Float32ArrayPrototype%":[`Float32Array`,`prototype`],"%Float64ArrayPrototype%":[`Float64Array`,`prototype`],"%FunctionPrototype%":[`Function`,`prototype`],"%Generator%":[`GeneratorFunction`,`prototype`],"%GeneratorPrototype%":[`GeneratorFunction`,`prototype`,`prototype`],"%Int8ArrayPrototype%":[`Int8Array`,`prototype`],"%Int16ArrayPrototype%":[`Int16Array`,`prototype`],"%Int32ArrayPrototype%":[`Int32Array`,`prototype`],"%JSONParse%":[`JSON`,`parse`],"%JSONStringify%":[`JSON`,`stringify`],"%MapPrototype%":[`Map`,`prototype`],"%NumberPrototype%":[`Number`,`prototype`],"%ObjectPrototype%":[`Object`,`prototype`],"%ObjProto_toString%":[`Object`,`prototype`,`toString`],"%ObjProto_valueOf%":[`Object`,`prototype`,`valueOf`],"%PromisePrototype%":[`Promise`,`prototype`],"%PromiseProto_then%":[`Promise`,`prototype`,`then`],"%Promise_all%":[`Promise`,`all`],"%Promise_reject%":[`Promise`,`reject`],"%Promise_resolve%":[`Promise`,`resolve`],"%RangeErrorPrototype%":[`RangeError`,`prototype`],"%ReferenceErrorPrototype%":[`ReferenceError`,`prototype`],"%RegExpPrototype%":[`RegExp`,`prototype`],"%SetPrototype%":[`Set`,`prototype`],"%SharedArrayBufferPrototype%":[`SharedArrayBuffer`,`prototype`],"%StringPrototype%":[`String`,`prototype`],"%SymbolPrototype%":[`Symbol`,`prototype`],"%SyntaxErrorPrototype%":[`SyntaxError`,`prototype`],"%TypedArrayPrototype%":[`TypedArray`,`prototype`],"%TypeErrorPrototype%":[`TypeError`,`prototype`],"%Uint8ArrayPrototype%":[`Uint8Array`,`prototype`],"%Uint8ClampedArrayPrototype%":[`Uint8ClampedArray`,`prototype`],"%Uint16ArrayPrototype%":[`Uint16Array`,`prototype`],"%Uint32ArrayPrototype%":[`Uint32Array`,`prototype`],"%URIErrorPrototype%":[`URIError`,`prototype`],"%WeakMapPrototype%":[`WeakMap`,`prototype`],"%WeakSetPrototype%":[`WeakSet`,`prototype`]},F=yn(),I=Dn(),L=F.call(k,Array.prototype.concat),R=F.call(O,Array.prototype.splice),z=F.call(k,String.prototype.replace),B=F.call(k,String.prototype.slice),V=F.call(k,RegExp.prototype.exec),H=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,U=/\\(\\)?/g,ee=function(e){var t=B(e,0,1),n=B(e,-1);if(t===`%`&&n!==`%`)throw new c("invalid intrinsic syntax, expected closing `%`");if(n===`%`&&t!==`%`)throw new c("invalid intrinsic syntax, expected opening `%`");var r=[];return z(e,H,function(e,t,n,i){r[r.length]=n?z(i,U,`$1`):t||e}),r},te=function(e,t){var n=e,r;if(I(P,n)&&(r=P[n],n=`%`+r[0]+`%`),I(M,n)){var i=M[n];if(i===A&&(i=N(n)),i===void 0&&!t)throw new l(`intrinsic `+e+` exists, but is not available. Please file an issue!`);return{alias:r,name:n,value:i}}throw new c(`intrinsic `+e+` does not exist!`)};t.exports=function(e,t){if(typeof e!=`string`||e.length===0)throw new l(`intrinsic name must be a non-empty string`);if(arguments.length>1&&typeof t!=`boolean`)throw new l(`"allowMissing" argument must be a boolean`);if(V(/^%?[^%]*%?$/,e)===null)throw new c("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=ee(e),r=n.length>0?n[0]:``,i=te(`%`+r+`%`,t),a=i.name,o=i.value,s=!1,u=i.alias;u&&(r=u[0],R(n,L([0,1],u)));for(var d=1,f=!0;d=n.length){var g=b(o,p);f=!!g,o=f&&`get`in g&&!(`originalValue`in g.get)?g.get:o[p]}else f=I(o,p),o=o[p];f&&!s&&(M[a]=o)}}return o}})),kn=o(((e,t)=>{var n=On(),r=wn(),i=r([n(`%String.prototype.indexOf%`)]);t.exports=function(e,t){var a=n(e,!!t);return typeof a==`function`&&i(e,`.prototype.`)>-1?r([a]):a}})),An=o(((e,t)=>{var n=On(),r=kn(),i=qt(),a=Gt(),o=n(`%Map%`,!0),s=r(`Map.prototype.get`,!0),c=r(`Map.prototype.set`,!0),l=r(`Map.prototype.has`,!0),u=r(`Map.prototype.delete`,!0),d=r(`Map.prototype.size`,!0);t.exports=!!o&&function(){var e,t={assert:function(e){if(!t.has(e))throw new a(`Side channel does not contain `+i(e))},delete:function(t){if(e){var n=u(e,t);return d(e)===0&&(e=void 0),n}return!1},get:function(t){if(e)return s(e,t)},has:function(t){return e?l(e,t):!1},set:function(t,n){e||=new o,c(e,t,n)}};return t}})),jn=o(((e,t)=>{var n=On(),r=kn(),i=qt(),a=An(),o=Gt(),s=n(`%WeakMap%`,!0),c=r(`WeakMap.prototype.get`,!0),l=r(`WeakMap.prototype.set`,!0),u=r(`WeakMap.prototype.has`,!0),d=r(`WeakMap.prototype.delete`,!0);t.exports=s?function(){var e,t,n={assert:function(e){if(!n.has(e))throw new o(`Side channel does not contain `+i(e))},delete:function(n){if(s&&n&&(typeof n==`object`||typeof n==`function`)){if(e)return d(e,n)}else if(a&&t)return t.delete(n);return!1},get:function(n){return s&&n&&(typeof n==`object`||typeof n==`function`)&&e?c(e,n):t&&t.get(n)},has:function(n){return s&&n&&(typeof n==`object`||typeof n==`function`)&&e?u(e,n):!!t&&t.has(n)},set:function(n,r){s&&n&&(typeof n==`object`||typeof n==`function`)?(e||=new s,l(e,n,r)):a&&(t||=a(),t.set(n,r))}};return n}:a})),Mn=o(((e,t)=>{var n=Gt(),r=qt(),i=Jt(),a=An(),o=jn()||a||i;t.exports=function(){var e,t={assert:function(e){if(!t.has(e))throw new n(`Side channel does not contain `+r(e))},delete:function(t){return!!e&&e.delete(t)},get:function(t){return e&&e.get(t)},has:function(t){return!!e&&e.has(t)},set:function(t,n){e||=o(),e.set(t,n)}};return t}})),Nn=o(((e,t)=>{var n=String.prototype.replace,r=/%20/g,i={RFC1738:`RFC1738`,RFC3986:`RFC3986`};t.exports={default:i.RFC3986,formatters:{RFC1738:function(e){return n.call(e,r,`+`)},RFC3986:function(e){return String(e)}},RFC1738:i.RFC1738,RFC3986:i.RFC3986}})),Pn=o(((e,t)=>{var n=Nn(),r=Object.prototype.hasOwnProperty,i=Array.isArray,a=function(){for(var e=[],t=0;t<256;++t)e.push(`%`+((t<16?`0`:``)+t.toString(16)).toUpperCase());return e}(),o=function(e){for(;e.length>1;){var t=e.pop(),n=t.obj[t.prop];if(i(n)){for(var r=[],a=0;a=d?s.slice(l,l+d):s,f=[],p=0;p=48&&m<=57||m>=65&&m<=90||m>=97&&m<=122||o===n.RFC1738&&(m===40||m===41)){f[f.length]=u.charAt(p);continue}if(m<128){f[f.length]=a[m];continue}if(m<2048){f[f.length]=a[192|m>>6]+a[128|m&63];continue}if(m<55296||m>=57344){f[f.length]=a[224|m>>12]+a[128|m>>6&63]+a[128|m&63];continue}p+=1,m=65536+((m&1023)<<10|u.charCodeAt(p)&1023),f[f.length]=a[240|m>>18]+a[128|m>>12&63]+a[128|m>>6&63]+a[128|m&63]}c+=f.join(``)}return c},isBuffer:function(e){return!e||typeof e!=`object`?!1:!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return Object.prototype.toString.call(e)===`[object RegExp]`},maybeMap:function(e,t){if(i(e)){for(var n=[],r=0;r{var n=Mn(),r=Pn(),i=Nn(),a=Object.prototype.hasOwnProperty,o={brackets:function(e){return e+`[]`},comma:`comma`,indices:function(e,t){return e+`[`+t+`]`},repeat:function(e){return e}},s=Array.isArray,c=Array.prototype.push,l=function(e,t){c.apply(e,s(t)?t:[t])},u=Date.prototype.toISOString,d=i.default,f={addQueryPrefix:!1,allowDots:!1,allowEmptyArrays:!1,arrayFormat:`indices`,charset:`utf-8`,charsetSentinel:!1,commaRoundTrip:!1,delimiter:`&`,encode:!0,encodeDotInKeys:!1,encoder:r.encode,encodeValuesOnly:!1,filter:void 0,format:d,formatter:i.formatters[d],indices:!1,serializeDate:function(e){return u.call(e)},skipNulls:!1,strictNullHandling:!1},p=function(e){return typeof e==`string`||typeof e==`number`||typeof e==`boolean`||typeof e==`symbol`||typeof e==`bigint`},m={},h=function e(t,i,a,o,c,u,d,h,g,_,v,y,b,x,S,C,w,T){for(var E=t,D=T,O=0,k=!1;(D=D.get(m))!==void 0&&!k;){var A=D.get(t);if(O+=1,A!==void 0){if(A===O)throw RangeError(`Cyclic object value`);k=!0}D.get(m)===void 0&&(O=0)}if(typeof _==`function`?E=_(i,E):E instanceof Date?E=b(E):a===`comma`&&s(E)&&(E=r.maybeMap(E,function(e){return e instanceof Date?b(e):e})),E===null){if(u)return g&&!C?g(i,f.encoder,w,`key`,x):i;E=``}if(p(E)||r.isBuffer(E))return g?[S(C?i:g(i,f.encoder,w,`key`,x))+`=`+S(g(E,f.encoder,w,`value`,x))]:[S(i)+`=`+S(String(E))];var j=[];if(E===void 0)return j;var M;if(a===`comma`&&s(E))C&&g&&(E=r.maybeMap(E,g)),M=[{value:E.length>0?E.join(`,`)||null:void 0}];else if(s(_))M=_;else{var N=Object.keys(E);M=v?N.sort(v):N}var P=h?String(i).replace(/\./g,`%2E`):String(i),F=o&&s(E)&&E.length===1?P+`[]`:P;if(c&&s(E)&&E.length===0)return F+`[]`;for(var I=0;I0?b+y:``}})),In=o(((e,t)=>{var n=Pn(),r=Object.prototype.hasOwnProperty,i=Array.isArray,a={allowDots:!1,allowEmptyArrays:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:`utf-8`,charsetSentinel:!1,comma:!1,decodeDotInKeys:!1,decoder:n.decode,delimiter:`&`,depth:5,duplicates:`combine`,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictDepth:!1,strictNullHandling:!1,throwOnLimitExceeded:!1},o=function(e){return e.replace(/&#(\d+);/g,function(e,t){return String.fromCharCode(parseInt(t,10))})},s=function(e,t,n){if(e&&typeof e==`string`&&t.comma&&e.indexOf(`,`)>-1)return e.split(`,`);if(t.throwOnLimitExceeded&&n>=t.arrayLimit)throw RangeError(`Array limit exceeded. Only `+t.arrayLimit+` element`+(t.arrayLimit===1?``:`s`)+` allowed in an array.`);return e},c=`utf8=%26%2310003%3B`,l=`utf8=%E2%9C%93`,u=function(e,t){var u={__proto__:null},d=t.ignoreQueryPrefix?e.replace(/^\?/,``):e;d=d.replace(/%5B/gi,`[`).replace(/%5D/gi,`]`);var f=t.parameterLimit===1/0?void 0:t.parameterLimit,p=d.split(t.delimiter,t.throwOnLimitExceeded?f+1:f);if(t.throwOnLimitExceeded&&p.length>f)throw RangeError(`Parameter limit exceeded. Only `+f+` parameter`+(f===1?``:`s`)+` allowed.`);var m=-1,h,g=t.charset;if(t.charsetSentinel)for(h=0;h-1&&(x=i(x)?[x]:x);var S=r.call(u,b);S&&t.duplicates===`combine`?u[b]=n.combine(u[b],x):(!S||t.duplicates===`last`)&&(u[b]=x)}return u},d=function(e,t,r,i){var a=0;if(e.length>0&&e[e.length-1]===`[]`){var o=e.slice(0,-1).join(``);a=Array.isArray(t)&&t[o]?t[o].length:0}for(var c=i?t:s(t,r,a),l=e.length-1;l>=0;--l){var u,d=e[l];if(d===`[]`&&r.parseArrays)u=r.allowEmptyArrays&&(c===``||r.strictNullHandling&&c===null)?[]:n.combine([],c);else{u=r.plainObjects?{__proto__:null}:{};var f=d.charAt(0)===`[`&&d.charAt(d.length-1)===`]`?d.slice(1,-1):d,p=r.decodeDotInKeys?f.replace(/%2E/g,`.`):f,m=parseInt(p,10);!r.parseArrays&&p===``?u={0:c}:!isNaN(m)&&d!==p&&String(m)===p&&m>=0&&r.parseArrays&&m<=r.arrayLimit?(u=[],u[m]=c):p!==`__proto__`&&(u[p]=c)}c=u}return c},f=function(e,t,n,i){if(e){var a=n.allowDots?e.replace(/\.([^.[]+)/g,`[$1]`):e,o=/(\[[^[\]]*])/,s=/(\[[^[\]]*])/g,c=n.depth>0&&o.exec(a),l=c?a.slice(0,c.index):a,u=[];if(l){if(!n.plainObjects&&r.call(Object.prototype,l)&&!n.allowPrototypes)return;u.push(l)}for(var f=0;n.depth>0&&(c=s.exec(a))!==null&&f{var n=Fn(),r=In();t.exports={formats:Nn(),parse:r,stringify:n}})),Rn=o((e=>{var t=Wt();function n(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}var r=/^([a-z0-9.+-]+:)/i,i=/:[0-9]*$/,a=/^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/,o=[`'`,`{`,`}`,`|`,`\\`,`^`,"`",`<`,`>`,`"`,"`",` `,`\r`,` +`,` `],s=[`%`,`/`,`?`,`;`,`#`].concat(o),c=[`/`,`?`,`#`],l=255,u=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,f={javascript:!0,"javascript:":!0},p={javascript:!0,"javascript:":!0},m={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},h=Ln();function g(e,t,r){if(e&&typeof e==`object`&&e instanceof n)return e;var i=new n;return i.parse(e,t,r),i}n.prototype.parse=function(e,n,i){if(typeof e!=`string`)throw TypeError(`Parameter 'url' must be a string, not `+typeof e);var g=e.indexOf(`?`),_=g!==-1&&g127?N+=`x`:N+=M[P];if(!N.match(u)){var I=A.slice(0,T),L=A.slice(T+1),R=M.match(d);R&&(I.push(R[1]),L.unshift(R[2])),L.length&&(y=`/`+L.join(`.`)+y),this.hostname=I.join(`.`);break}}}this.hostname.length>l?this.hostname=``:this.hostname=this.hostname.toLowerCase(),k||(this.hostname=t.toASCII(this.hostname));var z=this.port?`:`+this.port:``;this.host=(this.hostname||``)+z,this.href+=this.host,k&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),y[0]!==`/`&&(y=`/`+y))}if(!f[S])for(var T=0,j=o.length;T0?r.host.split(`@`):!1;S&&(r.auth=S.shift(),r.hostname=S.shift(),r.host=r.hostname)}return r.search=e.search,r.query=e.query,(r.pathname!==null||r.search!==null)&&(r.path=(r.pathname?r.pathname:``)+(r.search?r.search:``)),r.href=r.format(),r}if(!b.length)return r.pathname=null,r.search?r.path=`/`+r.search:r.path=null,r.href=r.format(),r;for(var C=b.slice(-1)[0],w=(r.host||e.host||b.length>1)&&(C===`.`||C===`..`)||C===``,T=0,E=b.length;E>=0;E--)C=b[E],C===`.`?b.splice(E,1):C===`..`?(b.splice(E,1),T++):T&&(b.splice(E,1),T--);if(!v&&!y)for(;T--;)b.unshift(`..`);v&&b[0]!==``&&(!b[0]||b[0].charAt(0)!==`/`)&&b.unshift(``),w&&b.join(`/`).substr(-1)!==`/`&&b.push(``);var D=b[0]===``||b[0]&&b[0].charAt(0)===`/`;if(x){r.hostname=D?``:b.length?b.shift():``,r.host=r.hostname;var S=r.host&&r.host.indexOf(`@`)>0?r.host.split(`@`):!1;S&&(r.auth=S.shift(),r.hostname=S.shift(),r.host=r.hostname)}return v||=r.host&&b.length,v&&!D&&b.unshift(``),b.length>0?r.pathname=b.join(`/`):(r.pathname=null,r.path=null),(r.pathname!==null||r.search!==null)&&(r.path=(r.pathname?r.pathname:``)+(r.search?r.search:``)),r.auth=e.auth||r.auth,r.slashes=r.slashes||e.slashes,r.href=r.format(),r},n.prototype.parseHost=function(){var e=this.host,t=i.exec(e);t&&(t=t[0],t!==`:`&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)},e.parse=g,e.format=_})),zn=c(Rn());function Bn(e,t){if(Object.is(e,t))return!0;if(typeof e!=`object`||!e||typeof t!=`object`||!t)return!1;let n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(let r=0;re),t.length===0)return e;let n=zn.parse(e,!0),r=Object.assign({},n.query,...t);return n.query=Ot(r,Object.keys(r).filter(e=>{let t=r[e];return t==null||t===``})),delete n.search,zn.format(n)}function Hn(e){let t=zn.parse(e,!0).query;if(t){for(let e in t){let n=t[e];t[e]=Array.isArray(n)?n[n.length-1]:n}return t}}function Un(e){return Hn(`?${e}`)}function Wn(e,t=!1){if(t){let t=Object.keys(e).filter(t=>{let n=e[t];return n==null||n===``});e=Ot(e,t)}return zn.format({query:e}).slice(1)}function Q(e){return typeof DEBUG>`u`||!DEBUG||!e||typeof e!=`object`?e:(Object.getOwnPropertyNames(e).forEach(t=>{let n=e[t];typeof n==`object`&&n&&Q(n)}),Object.freeze(e),e)}var Gn=class e{static toString(e){let t=``;return e&&bt(e,function(e,n){e!=null&&(t.length>0&&(t+=`&`),t+=n+`=`+e.toString())}),t}static fromString(e){let t={};if(e)for(let n of It(e,`&`)){let e=It(n,`=`,2);t[e[0]]=e[1]}return t}static equals(t,n){return Bn(e.fromString(t),e.fromString(n))}};function Kn(e=0){return new Promise(t=>{setTimeout(t,e)})}const qn=e=>e!==void 0,Jn=e=>!!e;function Yn(e){let t=document.createElement(`div`);return t.innerHTML=e,t.querySelectorAll(`li`).forEach(e=>{e.textContent?.toLowerCase().includes(`designer tip`)&&e.remove()}),t.innerHTML}const Xn=new Set([`STANDARDWEEKEND`,`ZIP`,`ZIPPLUS`]);var $=!0,Zn=new Set;function Qn(e){if($!==e){$=e;for(let e of Zn)try{e($)}catch(e){setTimeout(()=>{throw e})}}}typeof window<`u`&&(document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&!$&&navigator.onLine&&Qn(!0)}),window.addEventListener(`online`,()=>{Qn(!0)}),window.addEventListener(`offline`,()=>{Qn(!1)}));function $n(e){return Zn.add(e),()=>{Zn.delete(e)}}async function er(){!$&&navigator.onLine&&Qn(!0),!$&&await new Promise(e=>{let t=$n(()=>{$&&(t(),e($))})})}var tr=o(((e,t)=>{function n(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.exports=function(e,t,r,i){t||=`&`,r||=`=`;var a={};if(typeof e!=`string`||e.length===0)return a;var o=/\+/g;e=e.split(t);var s=1e3;i&&typeof i.maxKeys==`number`&&(s=i.maxKeys);var c=e.length;s>0&&c>s&&(c=s);for(var l=0;l=0?(f=u.substr(0,d),p=u.substr(d+1)):(f=u,p=``),m=decodeURIComponent(f),h=decodeURIComponent(p),n(a,m)?Array.isArray(a[m])?a[m].push(h):a[m]=[a[m],h]:a[m]=h}return a}})),nr=o(((e,t)=>{var n=function(e){switch(typeof e){case`string`:return e;case`boolean`:return e?`true`:`false`;case`number`:return isFinite(e)?e:``;default:return``}};t.exports=function(e,t,r,i){return t||=`&`,r||=`=`,e===null&&(e=void 0),typeof e==`object`?Object.keys(e).map(function(i){var a=encodeURIComponent(n(i))+r;return Array.isArray(e[i])?e[i].map(function(e){return a+encodeURIComponent(n(e))}).join(t):a+encodeURIComponent(n(e[i]))}).filter(Boolean).join(t):i?encodeURIComponent(n(i))+r+encodeURIComponent(n(e)):``}})),rr=c(o((e=>{e.decode=e.parse=tr(),e.encode=e.stringify=nr()}))());function ir(e){"@babel/helpers - typeof";return ir=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},ir(e)}function ar(e,t){if(ir(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t||`default`);if(ir(r)!=`object`)return r;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function or(e){var t=ar(e,`string`);return ir(t)==`symbol`?t:t+``}function sr(e,t,n){return(t=or(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var cr=c(Rn()),lr=8e3,ur=8e3,dr=Q({credentials:`include`,enableCsrf:!1,fallbackToPostMethod:!0,headers:{},includeCacheVersion:!0,includeClientParam:!0,includeI18NParams:!1,method:`GET`,retryOnNetworkError:!1});function fr(...e){let t=[...e].reverse();return t.push(dr),ht({},...t)}var pr=class extends Error{constructor(e,t,n,r){t&&(e+=`\nmethod:${t}`),n&&(e+=`\nurl:${n}`),super(e),sr(this,`name`,`ZFetchError`),sr(this,`response`,void 0),this.response=r}};function mr(e){return typeof e==`object`&&!!e&&`error`in e}function hr(e){return mr(e)&&typeof e.error==`object`&&!!e.error&&`id`in e.error&&e.error.id===`auth_token_invalid`}function gr(e){return mr(e)&&typeof e.error==`object`&&!!e.error&&`csrf`in e.error}async function _r(e,t,n,r){n={enableCsrf:!0,...n,headers:{Accept:`application/json`,...n?.headers}},{url:e,data:t,init:n}=yr(e,t,n,r);let i,a=n.retryOnNetworkError?2:1;for(let t=1;t<=a;t++){let o,s;try{if(o=await vr(e,n),s=await o.json(),n.enableCsrf&&gr(s)&&r&&(r.csrf=s.error.csrf,n.headers||={},n.headers[`X-Csrf-Token`]=r.csrf,o=await vr(e,n),s=await o.json(),gr(s)))throw new pr(`Invalid CSRF token on fetch retry.`,n.method,e,o);if(r?.access_token&&hr(s))throw new pr(`Invalid access token`,n.method,e,o);if(n.throwSoftJsonErrors&&mr(s))throw new pr(JSON.stringify(s.error),n.method,e,o);i=s;break}catch(r){await br(r,n.method,e,o,t=200&&n.status<300)return n;throw new pr(`Unsuccessful status: ${n.status} - ${n.statusText}`,t.method,e,n)}function yr(e,t,n,r){n=fr(n);function i(n,r){typeof t==`string`||typeof Blob<`u`&&t instanceof Blob||typeof FormData<`u`&&t instanceof FormData?e=Z(e,{[n]:r}):t={...t,[n]:r}}let a={};if(r&&(r.access_token&&(a.access_token=r.access_token),n.includeI18NParams&&(r.currency&&(a.zcur=r.currency),r.language&&(a.lang=r.language),r.region&&(a.region=r.region)),r.sessionParams&&Object.assign(a,r.sessionParams),Pt(a)||(e=Z(e,a))),n.enableCsrf&&(n.headers||={},n.headers[`X-Csrf-Token`]=r?.csrf??``),n.includeClientParam&&i(`client`,`js`),n.includeCacheVersion&&n.method===`GET`&&r?.httpCacheVersion&&(e=Z(e,{cv:r.httpCacheVersion})),(n.method===`GET`||n.method===`DELETE`)&&(e=Z(e,t),delete n.body,n.fallbackToPostMethod)){let r=cr.parse(e,!0);if(e.length>lr||r.search&&r.search.length>ur){if(n.method=`POST`,Pt(a))t=r.query,delete r.query;else{let e=Object.keys(a);t=Ot(r.query,e),r.query=Mt(r.query,e)}delete r.search,e=cr.format(r)}}if(!(n.method===`GET`||n.method===`DELETE`))if(typeof t==`string`||t instanceof Blob||t instanceof FormData){if(n.body=t,DEBUG&&!(typeof t!=`string`||n.headers&&Object.keys(n.headers).find(e=>e.toLowerCase()===`content-type`)))throw new pr(`You provided a raw payload to zfetch but failed to specify a Content-Type header.`,n.method,e,void 0)}else if(n.postAsFormUrlEncoded){let e=Nt(t,e=>e!==void 0);n.body=rr.stringify(e),n.headers||={},n.headers[`Content-Type`]=`application/x-www-form-urlencoded; charset=UTF-8`}else n.headers||={},n.headers[`Content-Type`]=`application/json`,n.body=JSON.stringify(t);return{url:e,data:t,init:n}}async function br(e,t,n,r,i){let a=e;if(a instanceof pr)throw a;if(i&&xr(a))await Kn(1e3);else throw new pr(`${a.name}: ${a.message}`,t,n,r)}function xr(e){return Ft(e)&&e.name===`TypeError`&&typeof e.message==`string`&&Sr.has(e.message)}var Sr=new Set([`network error`,`Failed to fetch`,`The Internet connection appears to be offline.`,`Load failed`,`NetworkError when attempting to fetch resource.`,`Network request failed`]);function Cr(e,t,n,r){return wr(e,t,{method:`GET`,...n},r)}async function wr(e,t,n,r){return await er(),{init:n,env:r}=Tr(n,r),_r(e,t,n,r)}function Tr(e,t){let n=e?.method??`GET`;return e={...e,credentials:`omit`,enableCsrf:!1,retryOnNetworkError:n===`GET`},t?.region&&(e.includeI18NParams=!0),{init:e,env:t}}function Er(e){return{async wwwGetProductFromTemplate(t){return(await Cr(e.wwwPag(`svc/partner/adobeexpress/v1/getproductfromtemplate`),{templateId:t},{throwSoftJsonErrors:!0})).data},async wwwChangeOptions(t){return(await Cr(e.wwwPag(`svc/partner/adobeexpress/v1/changeoptions`),t,{throwSoftJsonErrors:!0})).data.product},async wwwGetHelpDialog(t){let n=await Cr(e.wwwPag(`svc/z3/product/productattributehelp/gethelpdialog`),t);if(!n.success)throw n.error;return n.data},async wwwGetPricing(t){return(await Cr(e.wwwPag(`svc/partner/adobeexpress/v1/getproductpricing`),t,{throwSoftJsonErrors:!0})).data},async wwwGetShippingEstimates(t){let n=await Cr(e.wwwPag(`svc/partner/adobeexpress/v1/getshippingestimates`),t,{throwSoftJsonErrors:!0});return n?.data?.estimates&&(n.data.estimates=n.data.estimates.filter(e=>!Xn.has(e.method))),n.data}}}const Dr={mojo_throwpillow:{},zazzle_bag:{},zazzle_businesscard:{attributeValueFilter:{"*":``,"en-au":`std={iso}`,"en-ca":`std={iso}`,"en-gb":`std={iso}`,"en-us":`std={na}`,"es-us":`std={na}`,"fr-ca":`std={iso}`}},zazzle_flyer:{attributeValueFilter:{"*":``,"en-au":`std={iso}`,"en-ca":`std={iso}`,"en-gb":`std={iso}`,"en-us":`std={na}`,"es-us":`std={na}`,"fr-ca":`std={iso}`}},zazzle_foldedthankyoucard:{attributeValueFilter:{"*":``,"en-au":`std={iso}`,"en-ca":`std={iso}`,"en-gb":`std={iso}`,"en-us":`std={na}`,"es-us":`std={na}`,"fr-ca":`std={iso}`}},zazzle_invitation3:{attributeValueFilter:{"*":``,"en-au":`std={iso}`,"en-ca":`std={iso}`,"en-gb":`std={iso}`,"en-us":`std={na}`,"es-us":`std={na}`,"fr-ca":`std={iso}`}},zazzle_mug:{},zazzle_print:{attributeValueFilter:{"*":``,"en-au":`std={iso}`,"en-ca":`*`,"en-gb":`*`,"en-us":`std={na}`,"es-us":`std={na}`,"fr-ca":`*`}},zazzle_shirt:{attributeValueFilter:{"en-gb":``}},zazzle_sticker:{}};var Or=class e{constructor(e){sr(this,`zMatchExpression`,void 0),this.zMatchExpression=(e||``).toLowerCase()}evaluate(e){if(!this.zMatchExpression)return 0;let t=``;typeof e==`string`?t=e:typeof e==`object`&&(t=Wn(e)||``),t=t.toLowerCase();let n=this.lookupEvaluation(this.zMatchExpression,t);if(n!==void 0)return n;let r=this.zMatchExpression.split(`&`).map(e=>this.scoreExpressionAtom(e,t)),i=r.includes(-1)?-1:r.reduce((e,t)=>e+t,0);return this.cacheEvaluation(this.zMatchExpression,t,i),i}matches(e){return this.evaluate(e)>-1}expressionAtomValueMatchesAttributeSetAtomValue(e,t){let n=this.getAtomValueAsSet(t),r=e.startsWith(`!`),i=ze(n,this.getAtomValueAsSet(e.replace(/^!/,``)));return r?i.length===0:i.length>0}getAtomValueAsSet(e){return e=e.replace(/[{\[}\]}]/g,``),e.split(`,`).filter(e=>!!e)}scoreExpressionAtom(e,t){let n=e.includes(`:=`)?`:=`:`=`,[r,i]=e.split(n),a=Un(t)||{},o=a[r];return Object.keys(a).includes(r)?this.expressionAtomValueMatchesAttributeSetAtomValue(i,o)?1:-1:n===`=`?0:-1}cacheEvaluation(t,n,r){let i=this.generateCacheKey(t,n);e.cache[i]=r}static clearCache(){e.cache={}}generateCacheKey(e,t){return[e,t].join(`|`)}lookupEvaluation(t,n){let r=this.generateCacheKey(t,n);if(r in e.cache)return e.cache[r]}};sr(Or,`cache`,{});function kr(e,t,n=!1){let r=new Or(e),i=[],a=[];return t.forEach(e=>{r.matches(e.properties)?a.push(e):i.push(e)}),n?[...a,...i]:a}function Ar(e,t,n){if(!t)return e.values;let r=[];if(r=t[n]?kr(t[n],e.values):t[`*`]?kr(t[`*`],e.values):e.values,!r.find(t=>t.name===e.value)){let t=e.values.find(t=>t.name===e.value);t&&r.unshift(t)}return r}function jr(e,t){let n={};switch(t){case`firstProductRealview`:n=e.firstProductRealviewParams;break;case`realview`:n=e.realviewParams;break;case`swatch`:n=e.swatchParams;break}return Pt(n)&&(n=e.realviewParams),n}const Mr={AISparkle:`쓑`,Account:``,AccountFill22:`쑟`,AccountStroke22:`쑠`,AccountWIZ:`쑜`,Add22:`쑰`,AddFill22:`쒇`,AddImage:`쎗`,AddShape:``,AddText:`쎓`,AddToList:``,AddedToList:``,AddressBook22:`쑸`,AddressBookWIZ:`쑕`,Advanced:``,Album:``,Albums:``,AlignLayersBottom:`쎺`,AlignLayersHorzCenter:`쎸`,AlignLayersLeft:`쎻`,AlignLayersRight:`쎼`,AlignLayersTop:`쎹`,AlignLayersVertCenter:`쎷`,AllWhites:`쏓`,Animation:`쒢`,AppsEndWIZ:`쑌`,AppsStartWIZ:`쑋`,AroundClock:``,Arrange22:`쑱`,ArrowDown:``,ArrowNw:``,ArrowRight:``,ArrowSw:``,ArtView:``,Asciicircum:`^`,AspectRatio:`쒝`,AspectRatio22:`쒜`,AttAccent:``,AttAccentOpen:``,AttAge:``,AttColorOpen:``,AttDesignSizeOpen:``,AttFeaturesOpen:``,AttImage:``,AttImageOpen:``,AttLaptopSizeOpen:``,AttMetalOpen:``,AttOptionsOpen:``,AttOrientationOpen:``,AttOutputOpen:``,AttSignedOpen:``,AttSizeOpen:``,AttStoneOpen:``,AttStyle:``,AttStyleOpen:``,AttText:``,AttTextOpen:``,AttUpholsteryFabricOpen:``,Background:`쏠`,BackgroundWhite:`쏕`,Boundaries:`쎔`,Burger22:`쒏`,BurgerEndWIZ:`쑐`,BurgerStartWIZ:`쑏`,BurgerWideStartWIZ:`쑑`,Camera:``,CameraStroke:``,CaretDown:``,CaretDownSmallZ4:`썠`,CaretDownZ4:`썖`,CaretUpSmallZ4:`썟`,CaretUpZ4:`썕`,CartEmpty:`Á`,CartFill22:`쑡`,CartFull:`Â`,CartStroke22:`쑢`,CartWIZ:``,CartWIZ2:`쑙`,Change22:`쑲`,CharacterSpacing:``,ChatFill22:`쑣`,ChatFilled:`쐙`,ChatMonostroke:`쐘`,ChatStroke22:`쑤`,ChatWIZ:`쑚`,Check:`✓`,CheckStamp:``,Checkmark:`쎕`,ChevronDown:``,ChevronUp:``,Circle:``,CircleOutline:``,ClipMaskOff:`쏗`,ClipMaskOn:`쏘`,ClockStroke:``,CloseStamp:``,CloseStroke:`╳`,Collections22:`쑹`,ColorDropper:``,ColorPickerDroplet:``,Connect:``,ContourDynamicSizing:`쏙`,Copy:``,Create22:`쒐`,CreateEndWIZ:`쑎`,CreateStartWIZ:`쑍`,CreateVertical:`쐛`,Crop:``,Crop22:`쑪`,CropZ4:`쏏`,Customers:``,Cut:``,DatePicker:``,Delete22:`쑴`,DeleteTrashcan:`쎤`,DeliveryTruck:``,Designers:``,Designs22:`쑺`,DirectionDown:`쒡`,DirectionLeft:`쒞`,DirectionRight:`쒠`,DirectionUp:`쒟`,DistributeHorz:``,DistributeVert:``,Download22:`쒙`,DownloadCircle22:`쒚`,DragDrop:`쏬`,Droplet:``,Duplicate22:`쑳`,Earnings22:`쑻`,EarningsWIZ:`쑘`,Edit:``,EditBox:``,EditText22:`쒈`,Editor22:`쑶`,EditorsPick:``,Elements:`쏢`,Email:``,Envelope:`쓒`,ErrorCA:`썢`,Expand:``,Expand22:`쑼`,ExpandWindow:``,EyeDropper:``,FacebookCircle:``,FacebookOutline:`썻`,FileBitmap:``,FilePdfai:``,FileScreenprint:``,FileStitch:``,FileVideo:``,Fill:`쏐`,Fill22:`쑮`,Filter:`쏑`,Filter22:`쑭`,FilterImage:``,Fit:`쏒`,Fit22:`쑩`,FitBest:``,FitFill:``,FitFit:``,Flag:``,Flip22:`쑬`,FlipX:`쐁`,FlipY:`쐀`,Followed22:`쑷`,FollowedDesignersWIZ:`쑖`,Font:``,Font22:`쒍`,Fraction:`⁄`,Friends22:`쑽`,GdArrowGetStarted:``,Gift22:`쒑`,GiftBox:``,GiftsEndWIZ:`쑞`,GiftsStartWIZ:`쑝`,Globe:``,GoogleCircle:``,Greater:`쎃`,Grid:`쐠`,GridView:``,Gridlines:`쎖`,Grippy:``,Group:`쐄`,Group24:`쓔`,GroupEdit:`쓓`,GroupLayers:`쏵`,Guillemotleft:`«`,Guillemotright:`»`,HamburgerMenuCaret:`Å`,Heart:``,Heart22:`쑾`,HeartFill22:`쒄`,HeartOpen:``,Help:`쏞`,Hidden:``,History:``,HolidayGift:`ñ`,Home:``,HorizontalText:`쏀`,Hotspot:``,IdeasVertical:`쐞`,Images22:`쑿`,ImagesVertical:`쐜`,Info:``,InfoBg:``,InfoStamp:``,InfoStamp24:`쓙`,InfoStampFill24:`쓚`,InstagramCircle:``,InstagramOutline:`썹`,IntercharacterSpacing:`쎱`,InviteShapes:``,IsolatedZ:``,LaptopStroke:``,LargerImage:``,Layout:`쐡`,Less:`쎂`,Line:``,LineWeight:``,Link:``,List:``,Live22:`쒒`,LiveEndWIZ:`쑊`,LiveStartWIZ:`쑉`,LiveVertical:`쐝`,Location:``,Lock:``,Logo:``,LogoBigZ:``,LogoLetterform:``,MagnifyDecrease:``,MagnifyIncrease:``,MakeTools:``,Maker22:`쒀`,MakerProCheckmark:``,MakerStroke:``,MakerVertical:`쐟`,MarketplaceVertical:`쐚`,Mask:`쓖`,MaskEdit:`쓗`,Message:``,MicrophoneOff:`쐤`,MicrophoneOn:`쐣`,MinimizeWindow:``,MobileClose:`쀁`,MobileDevice:``,More:`쏄`,MoveBackward:``,MoveBackwardZ4:`쎾`,MoveForward:``,MoveForwardZ4:`쎽`,MoveToBack:``,MoveToBackZ4:`쏼`,MoveToFront:``,MoveToFrontZ4:`쏻`,MultipleSelected:``,Multiply:`×`,MultiplyLight:`Ö`,Mute:``,MyAccountWIZ:`쑒`,MyEndWIZ:`쑈`,MyOrdersWIZ:`쑓`,MyStartWIZ:`쑇`,MyStoresWIZ:`쑗`,NoWhites:`쏔`,NotSelected:``,NotVisible:`썝`,NotificationFill22:`쑥`,NotificationStroke22:`쑦`,ObjectAlignBottom:``,ObjectAlignHorzCenter:``,ObjectAlignLeft:``,ObjectAlignRight:``,ObjectAlignTop:``,ObjectAlignVertCenter:``,OffCanvasLeft:`Ä`,OffCanvasRight:``,Orders22:`쒁`,PaintPalette:``,PaperStack:``,Paste:``,Pause:``,PaymentAmericanexpress:``,PaymentApplepay:`쐎`,PaymentBankTransfer:``,PaymentBankTransferJapan:``,PaymentBankeinzug:``,PaymentBoletoBancario:``,PaymentGiropay:``,PaymentGooglepay:`쐏`,PaymentIdeal:``,PaymentIncasso:``,PaymentMastercard:``,PaymentOverboeking:``,PaymentOverboekingBancare:``,PaymentPaypal:``,PaymentSofort:``,PaymentTransferenciaBancaria:``,PaymentTransferenciaBancaria2:``,PaymentUberweisung:``,PaymentVirementBancare:``,PaymentVisa:``,PaymentVorkasse:``,Pencil:``,Phone:``,PhoneOff:`쐨`,PhoneOn:`쐧`,PicFrame:``,PiggyBankStroke:``,Pin:``,PinterestCircle:``,PinterestOutline:`썺`,Play:``,PlayCircle:`Æ`,Plus:``,Popup:``,PositionBottom:``,PositionHorzCenter:``,PositionLeft:``,PositionRight:``,PositionTop:``,PositionVertCenter:``,Print:``,Prints22:`쒘`,Product22:`쒓`,ProductBox:``,Profile22:`쒂`,ProsellerBadge:``,QRCode:`쏣`,QRCodeLarge:`쏤`,QuestionStamp:``,Radius:`쏛`,Redo:``,RedoZ4:`쎘`,RemoveWhite:`쏖`,Resize:``,RetailerCart:``,RevisedErrorIcon:``,Robot:``,Rotate:``,Rotate22:`쑫`,RotateCcw:``,RotateCw:``,RotateZ4:`쎥`,RoundedRectangle:`쏚`,Save:``,SaveRibbon:``,Saved22:`쒔`,SavedDesignsWIZ:`쑔`,Scale22:`쑨`,Search:`Ã`,Sell22:`쒕`,SellStartWIZ:`쑛`,Send:`쓜`,SendFill:`쓛`,SettingsGear:`쐢`,ShapeFill:``,Share:``,Share22:`쒅`,ShareFill22:`쒆`,ShareUp22:`쒎`,Shared:`쏡`,ShoppingBag:`쒛`,Show:``,SizeDecrease:``,SizeIncrease:``,SizeNonApparel:`썐`,Sliders:`쒣`,Smiley:``,SocialBlogger:``,SocialEmail:``,SocialEmailIso:``,SocialFacebook:``,SocialFacebookIso:``,SocialFlickr:``,SocialFlickrSquare:``,SocialGoogleplus:``,SocialGoogleplusIso:``,SocialInstagram:``,SocialInstagramBare:``,SocialLinkedin:``,SocialLinkedinSquare:``,SocialPinterest:``,SocialPinterestBare:``,SocialPinterestIso:``,SocialRss:``,SocialShare:``,SocialTumblr:``,SocialTumblrIso:``,SocialTwitter:``,SocialTwitterIso:``,SocialWaneloIso:``,SocialWordpress:``,SocialYoutube:``,SocialYoutubeIso:``,SocialZazzleBlog:``,SocialZazzleForum:``,SourceAccount:``,SourceComputer:``,SourceGoogleDrive:``,SourceInstagram:``,SpaceEvenlyHorz:`쎵`,SpaceEvenlyVert:`쎶`,SpeechBubble:``,SquareOutline:``,Star:``,StickerShapes:``,Store22:`쒃`,StoreOpen:``,Stroke:``,TShirt:``,TabArea:``,TabFilter:``,TabFit:``,TabFonts:``,TabInfo:``,TabLayers:``,TabModify:``,TabMore:``,TabOptions:``,TabPersonalize:``,TabTemplates:``,TabTextalign:``,Templates:`썓`,Text:``,TextAlignCenter:``,TextAlignCenterZ4:`쎴`,TextAlignLeft:``,TextAlignLeftZ4:`쎲`,TextAlignRight:``,TextAlignRightZ4:`쎳`,TextAlignVertBottom:``,TextAlignVertCenter:``,TextAlignVertTop:``,TextCurve22:`쒉`,TextDropShadow:`쐇`,TextHeight22:`쒌`,TextLineHeight:`쏸`,TextOnPath:`쎿`,TextOrientation22:`쒊`,TextPathHorz:``,TextPathHorzCurveDown:``,TextPathHorzCurveUp:``,TextPathVert:``,TextSize:``,TextSizeZ4:`쎰`,TextSpacing22:`쒋`,TextStroke:`쐆`,ThumbsDown:``,ThumbsUp:``,TikTokStroke:`썾`,Tile22:`쑯`,Tiling:``,Tools22:`쒖`,Transfer22:`쑧`,Transparency:``,TransparencyMobile:``,Trash:``,Trending22:`쒗`,TrendingEndWIZ:`쑆`,TrendingStartWIZ:`쑅`,Triangle:`쐍`,TwitterCircle:``,TwitterOutline:`썼`,Undo:``,UndoZ4:`쎙`,Ungroup:`쐅`,Ungroup24:`쓕`,Unlink:``,UnlinkZ4:`쐃`,Unmask:`쓘`,Uploads:`쏟`,Verified22:`쑵`,VerifiedCollectionStar:``,VerticalText:`쏁`,VideoOff:`쐦`,VideoOn:`쐥`,ViewGrid:``,ViewImage:``,ViewList:``,Volume:``,WarehouseStroke:``,Warning:`썗`,WarningStamp:``,YoutubeCircle:``,YoutubeOutline:`썽`,ZazzleHeart:``,ZazzleSelect:`쐗`},Nr=e=>e,Pr=e=>e in Mr,Fr=Q({type:`thumbnails`,showTitle:!0,thumbnailSize:`m`}),Ir=Q({type:`thumbnails`,thumbnailRows:1}),Lr=Q({type:`thumbnailsWithPreview`,thumbnailRows:1}),Rr=Q({fullSelection:{type:`thumbnails`,enabled:!0},hideWhen:0,includeDesign:!0,includeDesignWhenCustomizing:!0,inlineSelection:{type:`none`},name:``,showPriceDelta:!0,shown:!0,thumbnailType:`realview`}),zr=Q({hideWhen:0,includeDesign:!0,includeDesignWhenCustomizing:!0,inlineSelection:{type:`thumbnails`},name:``,showPriceDelta:!0,shown:!0,thumbnailShape:`square`,thumbnailType:`realview`}),Br=Q({actionable:!1,astSlug:``,descriptionLabel:``,dismissable:!0,flair:!1,flairLabel:`product.ui.config.new[title]`,skuFilter:``,titleLabel:``}),Vr=Q({ads:[],attributeGroups:[{attributes:[]}],mobileAttributeGroups:[{attributes:[]}]});function Hr(e,t,n=!1,r=!1,i,a,o){let s,c=n?t.mobileAttributeGroups:t.attributeGroups,l={};if(c.forEach(e=>{e.attributes.forEach(e=>{l[e.name]=e})}),r){let r=[],o=[];function c(e){if(n){let n=l[e];if(!n){let r=Xr(t,e);n=Gr({name:e},r)}return Jr(n)}else{let t=l[e];return t||=Wr({name:e}),qr(t)}}let u={};(i||[]).forEach(e=>{switch(e.type){case`area`:o.push({attributes:[],defaulted:!0,vizLiteAreaGroupName:e.areaGroup});break;case`optiongroup`:if(e.attributes.length){let t=e.attributes.map(e=>(u[e]=!0,c(e))),n=t.filter(e=>e.forceSelection),i=t.filter(e=>!e.forceSelection),s=`product.annotation.${a}.optiongroup.${e.name}[title]`;n.length&&r.push({attributes:n,defaulted:!0,label:s}),i.length&&o.push({attributes:i,defaulted:!0,label:s})}break;case`option`:{u[e.attribute]=!0;let t=c(e.attribute),n={attributes:[t],defaulted:!0};t.forceSelection?r.push(n):o.push(n);break}}}),Ae(e,e=>{if(!u[e.name]){let t=c(e.name),n={attributes:[t],defaulted:!0};t.forceSelection?r.push(n):o.push(n)}});let d=[...r,...o];if(d.some(e=>!!e.label)){let t=[];d.forEach(n=>{n.label||n.vizLiteAreaGroupName?t.push(n):n.attributes.forEach(r=>{e[r.name]&&t.push({attributes:[r],defaulted:!0,isAccessoryGroup:n.isAccessoryGroup,label:e[r.name].title})})}),s=t}else s=d}else{let r=[...c],i=[];if(Ae(e,e=>{if(!l[e.name]){let r;if(n){let n=Xr(t,e.name);r=Gr({name:e.name},n)}else r=Wr({name:e.name});i.push(r)}}),i.length){let e={attributes:i,defaulted:!0};r.push(e)}s=r}return o&&(s=J(s),s.forEach(e=>{e.attributes.forEach(e=>{e.showPriceDelta=!1})})),s}function Ur(e,t,n=!1,r,i,a){return Hr(e,t,!1,n,r,i,a)}function Wr(e){let t=Et({},Rr,e,{defaulted:!0});e.heroThumbnailType||(t.heroThumbnailType=t.thumbnailType),t.name===`size`&&(e.heroThumbnailType||(t.heroThumbnailType=`zazzicon`),t.heroThumbnailType===`zazzicon`&&!e.heroThumbnailIconName&&(t.heroThumbnailIconName=Nr(`AttDesignSizeOpen`))),t.heroThumbnailIconName&&!Pr(t.heroThumbnailIconName)&&(t.heroThumbnailIconName=Nr(`Robot`));let n=t.inlineSelection;n.type===`thumbnails`?t.inlineSelection=n={...Ir,...n}:n.type===`thumbnailsWithPreview`&&(t.inlineSelection=n={...Lr,...n});let r=t.fullSelection;return n.type===`droplist`&&(!e.fullSelection||!e.fullSelection.type)&&(r.type=`radio`),r.type===`thumbnails`&&(t.fullSelection=r={...Fr,...r}),t.helpLink&&Zr(t.helpLink),t}function Gr(e,t){let n=Et({},zr,t,e,{defaulted:!0});n.helpLink&&Zr(n.helpLink);let r=t.inlineSelection&&t.inlineSelection.type===`radio`,i=!e||!e.inlineSelection||!e.inlineSelection.type;return r&&i&&(n.inlineSelection.type=`droplist`),n}function Kr(e){let t=Et({},Br,e,{defaulted:!0});return t.helpLink&&Zr(t.helpLink),t}function qr(e){let t={...e,heroThumbnailType:`swatch`,hideWhen:1,includeDesignWhenCustomizing:!1,thumbnailType:`swatch`};return t.fullSelection.type===`thumbnails`&&(t.fullSelection.thumbnailSize=`l`),t}function Jr(e){return{...e,hideWhen:1,includeDesignWhenCustomizing:!1,thumbnailType:`swatch`}}function Yr(e){e=Et({},Vr,e);let t=e.ads.map(Kr),n=e.attributeGroups.map(e=>{let t=(e.attributes||[]).map(Wr);return{...e,attributes:t,defaulted:!0}}),r=Ie(e.attributeGroups,e=>e.attributes),i=Ie(e.mobileAttributeGroups,e=>e.attributes),a=[{attributes:Be([...i.map(e=>e.name),...r.map(e=>e.name)]).map(e=>{let t=r.find(t=>t.name===e)??Wr({name:e});return Gr(i.find(t=>t.name===e)??{},t)}),defaulted:!0}];return{...e,ads:t,attributeGroups:n,defaulted:!0,mobileAttributeGroups:a}}function Xr(e,t){for(let n of e.attributeGroups)for(let e of n.attributes)if(e.name===t)return e;return Wr({name:t})}function Zr(e){e.iconName&&!Pr(e.iconName)&&(e.iconName=Nr(`Robot`))}function Qr(e,t,n){if(e.subtitle){let r=e.subtitle.map(e=>new Or(e.filter).evaluate(t.properties)),i=Math.max(...r);if(i>0){let t=r.findIndex(e=>e===i),a=e.subtitle[t];return n(a.label)}}}function $r(e){return e?.find(e=>e.discountIsApplied&&(e.isDiscountOnItem||e.isDiscountOnPrice))||void 0}function ei(e,t){let{discountProductItems:n=[]}=e;return n.find(e=>e.isDiscountOnPrice||e.discountIsApplied||!!t&&e.discountCode===t)}function ti(e,t){let n=ei(e,t);return n?n.priceAdjusted:e.unitPrice}function ni(e,t,n={},r=!1){return Z(t,r&&e.videoParams?e.videoParams:e.realviewParams,n)}const ri=(e,t)=>{if(!e?.length)return``;let n=Qe(e,e=>new Date(e.maxDeliveryDate)),r=new Date(n[0].maxDeliveryDate),i=new Date(n[n.length-1].maxDeliveryDate),a=``;if(r.getUTCMonth()===i.getUTCMonth())a=r.getUTCDate()===i.getUTCDate()?t(i,{day:`numeric`,month:`short`,timeZone:`UTC`}):`${t(r,{day:`numeric`,month:`short`,timeZone:`UTC`})} - ${t(i,{day:`numeric`,timeZone:`UTC`})}`;else{let e={day:`numeric`,month:`short`,timeZone:`UTC`};a=`${t(r,e)} - ${t(i,e)}`}return a};function ii(e){let t=e;function n(e){if(e===t)return t;t=e;for(let e of r)try{e()}catch(e){window.reportError(e)}return e}let r=new Set;function i(e){return r.add(e),()=>{r.delete(e)}}return{getSnapshot:()=>t,setState:n,subscribe:i}}function ai(e,t){let n=oi(e,t),r={},i=ii(r),a=ii(n(r));return i.subscribe(()=>{let e=n(i.getSnapshot());a.setState(e)}),{external:a,internal:i}}function oi(e,t){function n(t){let n=e.culture;n===`en-ca`?n=`en-us`:n===`fr-ca`&&(n=`fr-fr`);let r={style:`currency`,currency:e.currency};return t.toLocaleString(n,r)}function r(e){if(e!==0)return`${e>0?`+`:`-`}${n(Math.abs(e))}`}let i=Vn((e,t,n)=>{let r=Yr({...e,...Dr[t]});return{pbj:r,pbjAttributeGroups:Ur(n,r,!1,void 0,t,!1)}}),a=si(e=>{let{pbj:t,pbjAttributeGroups:n}=i(e.product.pbjOverrides,e.product.productType,e.product.attributes);return[t,n,e.entities.dbStrings,e.product.attributes]},(n,i,a,o)=>i.flatMap(e=>e.attributes).map(i=>{let s=o[i.name];if(!s||!i.shown)return;let c=Ar(s,n.attributeValueFilter,e.culture).filter(e=>e.isInStock||e.name===s.value),l=c.find(e=>e.name===s.value);if(c.length<=i.hideWhen)return;let u=(()=>{let e=i.helpLink;if(e?.type===`dialog`&&e.dialogType===`sizeChart`&&e.label&&a[e.label])return{dialogType:`sizeChart`,label:a[e.label],type:`dialog`}})(),d=(()=>{switch(i.inlineSelection.type){case`thumbnails`:case`thumbnailsWithPreview`:{let e=i.includeDesign&&(i.thumbnailType===`realview`||i.thumbnailType===`firstProductRealview`),n=i.inlineSelection,o=n.groups||[{filter:``}],s=o.map(e=>kr(e.filter,c,n.allowGroupBackfill));return{optionGroups:o.map((n,o)=>{let c=s[o];if(c.length===0)return;let l=n.label?a[n.label]:``;return{options:c.map(n=>{let a=jr(n,i.thumbnailType);return{imageUrl:Z(t.staticRealviewUrlBase(),{...a,max_dim:48},!e&&{design:void 0}),priceDelta:i.showPriceDelta&&n.priceDifferential?r(n.priceDifferential):void 0,title:n.title,value:n.name}}),title:l}}).filter(qn),preview:(()=>{if(i.inlineSelection.type!==`thumbnailsWithPreview`||!l)return;let n=jr(l,i.thumbnailType),r=Z(t.staticRealviewUrlBase(),{...n,max_dim:192},!e&&{design:void 0});return{descriptionHTML:l.descriptionBrief,imageUrl:r,optionTitle:l.title}})(),type:`thumbnails`}}case`radio`:return{options:c.map(e=>({priceDelta:i.showPriceDelta?r(e.priceDifferential):void 0,title:e.title,value:e.name})),type:`radio`};case`toggle`:{let e=s.values.indexOf(l),t=e===0?1:0,n=s.values[t].priceDifferential;return{checkedValue:s.values[e].name,priceDelta:i.showPriceDelta?r(n):void 0,title:s.values[1].title,type:`checkbox`,uncheckedValue:s.values[t].name}}}return{message:(l?Qr(i.inlineSelection,l,e=>a[e]):``)||void 0,options:c.map(e=>({title:e.title,value:e.name,priceDelta:i.showPriceDelta?r(e.priceDifferential):void 0})),type:`dropdown`}})();return{helpLink:u,name:s.name,selectedOptionValue:s.value,selectedOptionTitle:l?.title??``,selector:d,title:s.title}}).filter(qn)),o=si(e=>[e.product.attributes],e=>Object.values(e).map(e=>{let t=e.values.find(t=>t.name===e.value)??e.values[0];if(t?.description)return{attributeTitle:e.title,descriptionHTML:Yn(t.description),selectedValueTitle:t.titleLong}}).filter(Jn));function s(e){let t={designAreasSizes:e.designAreasSizes,qty:1,...St(e.product.attributes,e=>e.value)};return JSON.stringify(t)}let c=si(e=>[e.product.quantity,e.product.pricing],(t,r)=>{let i=r.unitPrice,a=i*t,o=ti(r),s=o*t,c=ei(r),l=c?.discountCode;return{discountLabel:c?.discountString,originalTotalPrice:n(a),originalUnitPrice:n(i),promoCode:l,showCompValue:e.region===`us`,totalPrice:n(s),unitPrice:n(o)}});function l(e){return e.product.productType}function u(e){return e.product.quantity}let d=si(e=>[e.product.pricing.discountProductItems,e.product.pricing,e.product.quantities,e.product.hasForcedQuantities,e.product.singularUnitLabel,e.product.pluralUnitLabel],(e,t,n,r,i,a)=>{let o=$r(e)?.discountPercent??0;return Ht(t.volumeDiscountTiers,n,r).map(e=>{let t=e.discount?.discountPercent&&(!o||o[e.product.realviews],e=>e.filter(e=>e.type===`Product`&&(e.media===`image`||e.media===`video`)).map(e=>{let n={id:e.id,title:e.title,url:ni(e,t.staticRealviewUrlBase(),{max_dim:128})};if(e.media===`image`)return{...n,type:`image`};{let r=(()=>{if(e.videoId)return Z(t.videoPag(`${e.videoId}_hd.mp4`));if(e.videoParams)return ni(e,t.staticRealviewUrlBase(),{max_dim:128},!0)})();return r?{...n,mp4Source:r,type:`video`}:void 0}}).filter(qn)),p=si(e=>[e.reviews?.stats.totalReviews,e.reviews?.stats.averageRating],(e,t)=>{if(!(e===void 0||t===void 0))return{count:e,rating:t}});function m(e){let t=f(e);return t.find(t=>t.id===e.product.preferredRealviewId)??t[0]}function h(t){return t.shippingEstimates?ri(t.shippingEstimates.estimates,(t,n)=>t.toLocaleDateString(e.culture,n)):``}function g(e){return e.product.title}function _(e){let t=e.product;return t.quantity===1?t.singularUnitLabel:t.pluralUnitLabel}return Vn(e=>{if(e.product)return{attributes:a(e),descriptionComponents:o(e),expressProductSettings:s(e),pricing:c(e),productType:l(e),quantity:u(e),quantityOptions:d(e),realviews:f(e),reviewsStats:p(e),selectedRealview:m(e),shippingEstimate:h(e),title:g(e),unitLabel:_(e)}})}function si(e,t){let n=Vn(t);return(...t)=>n(...e(...t))}var ci={at:`.at`,au:`.com.au`,be:`.be`,br:`.com.br`,ca:`.ca`,ch:`.ch`,de:`.de`,es:`.es`,fr:`.fr`,gb:`.co.uk`,jp:`.co.jp`,kr:`.co.kr`,nl:`.nl`,nz:`.co.nz`,pt:`.pt`,se:`.se`,us:`.com`},li={at:`EUR`,au:`AUD`,be:`EUR`,br:`BRL`,ca:`CAD`,ch:`CHF`,de:`EUR`,es:`EUR`,fr:`EUR`,gb:`GBP`,jp:`JPY`,kr:`KRW`,nl:`EUR`,nz:`NZD`,pt:`EUR`,se:`SEK`,us:`USD`},ui={at:`de`,au:`en`,be:`fr`,br:`pt`,ca:`en`,ch:`de`,de:`de`,es:`de`,fr:`fr`,gb:`en`,jp:`ja`,kr:`ko`,nl:`nl`,nz:`en`,pt:`pt`,se:`sv`,us:`en`},di={at:`Europe/Vienna`,au:`Australia/Sydney`,be:`Europe/Brussels`,br:`America/Sao_Paulo`,ca:`America/Toronto`,ch:`Europe/Zurich`,de:`Europe/Berlin`,es:`Europe/Madrid`,fr:`Europe/Paris`,gb:`Europe/London`,jp:`Asia/Tokyo`,kr:`Asia/Seoul`,nl:`Europe/Amsterdam`,nz:`Pacific/Auckland`,pt:`Europe/Lisbon`,se:`Europe/Stockholm`,us:`America/Los_Angeles`},fi={at:`metric`,au:`metric`,be:`metric`,br:`metric`,ca:`metric`,ch:`metric`,de:`metric`,es:`metric`,fr:`metric`,gb:`metric`,jp:`metric`,kr:`metric`,nl:`metric`,nz:`metric`,pt:`metric`,se:`metric`,us:`imperial`},pi=[`en-us`,`en-au`,`en-ca`,`en-gb`,`en-nz`,`de-at`,`de-ch`,`de-de`,`es-es`,`es-us`,`fr-be`,`fr-ca`,`fr-ch`,`fr-fr`,`ja-jp`,`nl-be`,`nl-nl`,`pt-br`,`pt-pt`,`sv-se`],mi=`en`,hi=`us`;function gi(e,t){let n=e?e.toLowerCase():hi,r=t?t.toLowerCase():mi,i=`${r}-${n}`;if(vi(i))return i;if(_i(n)){let e=ui[n];if(e){let t=`${e}-${n}`.toLowerCase();if(vi(t))return t}}return i=`${r}-${hi}`.toLowerCase(),vi(i)?i:`en-us`}function _i(e){return e in ui}function vi(e){return pi.includes(e)}function yi(e,t){return e.replace(`.*`,`${ci[t]}`)}function bi(e,t){let n=gi(e,t),[r,i]=n.split(`-`),a=yi(`rlv.zcache.*`,i),o=yi(`asset.zcache.*/vcc`,i),s=yi(`asset.zcache.*/assets/graphics`,i),c=yi(`www.zazzle.*`,i);return{culture:n,currency:li[i],language:r,region:i,staticRlvUrlBase:a,timezone:di[i],unit:fi[i],videoUrlBase:o,wwwAstBase:s,wwwUrlBase:c}}function xi(e){return{zcur:e.currency,lang:e.language,region:e.region}}function Si(e){function t(t,n,r,i=!0){return n=Ci(n),Z(`https://${t}/${n}`,{...i?xi(e):void 0,...r})}let n=(e=!0)=>o(`svc/getimage`,void 0,e),r=(e=!0)=>a(`svc/view`,void 0,e),i=(e=!0)=>o(`svc/view`,void 0,e);function a(e,t,n=!0){return e=Ci(e),c(`rlv/${e}`,t,n)}function o(n,r,i=!0){return t(e.staticRlvUrlBase,n,r,i)}function s(n,r,i=!0){return t(e.wwwAstBase,n,r,i)}function c(n,r,i=!0){return t(e.wwwUrlBase,n,r,i)}function l(n,r){return t(e.videoUrlBase,n,r,!1)}return{dynamicRealviewUrlBase:r,rlvPag:a,sRlvPag:o,staticRealviewGetImageBase:n,staticRealviewUrlBase:i,videoPag:l,wwwAst:s,wwwPag:c}}function Ci(e){return e.startsWith(`/`)?e.substring(1):e}function wi(e){let t=bi(e.region,e.language),n=Si(t),r=Er(n),i=ai(t,n),a=()=>i.internal.getSnapshot(),o=e=>i.internal.setState(e);return{env:{currency:t.currency,language:t.language,region:t.region},subscribe:e=>i.external.subscribe(e),getSnapshot:()=>i.external.getSnapshot(),async fetchProduct(e){let{designAreasSizes:t,entities:n,product:i}=await r.wwwGetProductFromTemplate(e);i.pricing=await r.wwwGetPricing({productId:i.id,productOptions:i.productOption}),o({designAreasSizes:t,entities:n,product:i,shippingEstimates:await r.wwwGetShippingEstimates({productId:i.id,productOptions:i.productOption,qty:i.quantity})})},async fetchSizeChart(){let e=a();if(!e.product)throw Error(`Product data is not available`);return await r.wwwGetHelpDialog({att:`size`,dialogType:`sizeChart`,po:e.product.productOption,pt:e.product.productType})},async selectOption(e,n){let i=a();if(!i.product)throw Error(`Product data is not available`);let s=i.product.attributes[e];if(!s||s.value===n||!s.values.some(e=>e.name===n))return;i=o({...i,product:{...i.product,attributes:{...i.product.attributes,[e]:{...s,value:n}}}});let c={...Gn.fromString(i.product.productOption),[e]:n},l=await r.wwwChangeOptions({preferredViewId:i.product.preferredRealviewId,productId:i.product.id,productOptions:Gn.toString(c)});i=a(),i=o({...i,product:{...i.product,...l}}),await Promise.all([(async()=>{let e=await r.wwwGetPricing({productId:i.product.id,productOptions:i.product.productOption,qty:i.product.quantity}),t=a();o({...t,product:{...t.product,pricing:e}})})(),(async()=>{if(!(t.region!==`us`&&t.region!==`gb`))try{let e=await r.wwwGetShippingEstimates({productId:i.product.rootProductId,productOptions:i.product.productOption,qty:i.product.quantity,zip:``});o({...a(),shippingEstimates:e})}catch(e){window.reportError(e)}})()])},async selectQuantity(e){let t=a();if(!t.product)throw Error(`Product data is not available`);e=Vt(e,t.product.minQuantity,t.product.maxQuantity,t.product.quantityIncrement);let{product:n}=t,i=n.pricing.volumeDiscountTiers?.find(e=>e.minQuantity<=n.quantity&&n.quantity<=e.maxQuantity),s=n.pricing.volumeDiscountTiers?.find(t=>t.minQuantity<=e&&e<=t.maxQuantity);(i===s||!i?.hasDiscount&&!s?.hasDiscount)&&(t=o({...t,product:{...t.product,quantity:e}}));let c=await r.wwwGetPricing({productId:t.product.id,productOptions:t.product.productOption,qty:e});t=a(),o({...t,product:{...t.product,pricing:c,quantity:e}})},async selectRealview(e){let t=a();if(!t.product)throw Error(`Product data is not available`);if(t.product.preferredRealviewId===e||!t.product.realviews.some(t=>t.id===e))return;t=a(),t=o({...t,product:{...t.product,preferredRealviewId:e}});let n=await r.wwwChangeOptions({preferredViewId:t.product.preferredRealviewId,productId:t.product.id,productOptions:t.product.productOption});t=a(),o({...t,product:{...t.product,...n}})}}}var Ti=wi;export{wi as createZazzlePDPStore,Ti as default}; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/express/code/blocks/print-product-detail-sdk/utilities/sample_state.json b/express/code/blocks/print-product-detail-sdk/utilities/sample_state.json new file mode 100644 index 000000000..71c5360a3 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/utilities/sample_state.json @@ -0,0 +1,275 @@ +{ + "state": { + "attributes": [ + { + "name": "cornerstyle", + "selectedOptionValue": "normal", + "selectedOptionTitle": "Squared", + "selector": { + "optionGroups": [ + { + "options": [ + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113474916875736696&cornerstyle=normal&max_dim=48", + "title": "Squared", + "value": "normal" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113474916875736696&cornerstyle=allrounded&max_dim=48", + "priceDelta": "+$5.35", + "title": "Rounded", + "value": "allrounded" + } + ], + "title": "" + } + ], + "type": "thumbnails" + }, + "title": "Corner Style" + }, + { + "name": "media", + "selectedOptionValue": "175ptmatte", + "selectedOptionTitle": "Signature Matte", + "selector": { + "optionGroups": [ + { + "options": [ + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=175ptmatte&max_dim=48", + "title": "Signature Matte", + "value": "175ptmatte" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptsemi&max_dim=48", + "title": "Standard Semi-Gloss", + "value": "16ptsemi" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=18ptsemi_gloss&max_dim=48", + "priceDelta": "+$4.50", + "title": "Signature UV Gloss", + "value": "18ptsemi_gloss" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=18ptsemi_matte&max_dim=48", + "priceDelta": "+$4.50", + "title": "Signature UV Matte", + "value": "18ptsemi_matte" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptcream&max_dim=48", + "priceDelta": "+$4.50", + "title": "Signature Cream", + "value": "16ptcream" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=18ptsemi_softtouch&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Silk", + "value": "18ptsemi_softtouch" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptlinen&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Linen", + "value": "16ptlinen" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptpearl&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Pearl", + "value": "16ptpearl" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=18ptkraft&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Kraft", + "value": "18ptkraft" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptpewtergray&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Grey", + "value": "16ptpewtergray" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=16ptblack&max_dim=48", + "priceDelta": "+$8.95", + "title": "Premium Black", + "value": "16ptblack" + }, + { + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=32ptultrathick&max_dim=48", + "priceDelta": "+$16.90", + "title": "Premium Thick", + "value": "32ptultrathick" + } + ], + "title": "" + } + ], + "preview": { + "descriptionHTML": "

    18 pt thickness / 120 lb weight
    Light eggshell white, uncoated matte finish

    ", + "imageUrl": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&rlvnet=1&realview=113055797571919632&media=175ptmatte&max_dim=192", + "optionTitle": "Signature Matte" + }, + "type": "thumbnails" + }, + "title": "Paper" + } + ], + "descriptionComponents": [ + { + "attributeTitle": "Size", + "descriptionHTML": "

    When it comes to your business, don't wait for opportunity, create it! Make a lasting impression with quality cards that WOW.

    \n
      \n
    • Dimensions: 3.5\" x 2.0\"
    • \n
    • Full color CMYK print process
    • \n
    • Double sided printing for no additional cost
    • \n
    • 100% satisfaction guarantee
    • \n
    ", + "selectedValueTitle": "Standard, 3.5\" x 2.0\"" + }, + { + "attributeTitle": "Paper", + "descriptionHTML": "

    Signature Matte is our best-selling paper—timeless, elegant, and crafted for a premium feel. With its softly textured, uncoated matte finish and sturdy 18 point thickness, it delivers a refined look and lasting impression. Made exclusively for Zazzle by Neenah, it blends print clarity, sustainability, and elevated style.

    • Light white, uncoated matte finish with an eggshell texture
    • Thick, premium-quality stock with a substantial feel (18 pt)
    • Ideal for writing—won't smudge or smear
    • Made with 30% post-consumer waste
    • Made and printed in the USA
    ", + "selectedValueTitle": "Signature Matte" + } + ], + "expressProductSettings": "{\"designAreasSizes\":\"[3.5,2,3.5,2]\",\"qty\":1,\"style\":\"3.5x2\",\"cornerstyle\":\"normal\",\"media\":\"175ptmatte\",\"printquality\":\"4color\",\"envelopes\":\"none\"}", + "pricing": { + "discountLabel": "You save 15%", + "originalTotalPrice": "$23.15", + "originalUnitPrice": "$23.15", + "promoCode": "ZCLASSOF2026", + "showCompValue": true, + "totalPrice": "$19.68", + "unitPrice": "$19.68" + }, + "productType": "zazzle_businesscard", + "quantity": 1, + "quantityOptions": [ + { + "label": "1 pack of 100", + "quantity": 1 + }, + { + "discount": "20%", + "label": "2 packs of 100", + "quantity": 2 + }, + { + "discount": "30%", + "label": "3 packs of 100", + "quantity": 3 + }, + { + "discount": "40%", + "label": "4 packs of 100", + "quantity": 4 + }, + { + "discount": "50%", + "label": "5 packs of 100", + "quantity": 5 + }, + { + "discount": "55%", + "label": "6 packs of 100", + "quantity": 6 + }, + { + "discount": "65%", + "label": "10 packs of 100", + "quantity": 10 + }, + { + "discount": "68%", + "label": "20 packs of 100", + "quantity": 20 + }, + { + "discount": "68%", + "label": "50 packs of 100", + "quantity": 50 + }, + { + "discount": "68%", + "label": "100 packs of 100", + "quantity": 100 + }, + { + "discount": "68%", + "label": "200 packs of 100", + "quantity": 200 + }, + { + "discount": "68%", + "label": "500 packs of 100", + "quantity": 500 + } + ], + "realviews": [ + { + "id": "113335724526596936", + "title": "Front", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113335724526596936&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&max_dim=128&zattribution=none", + "type": "image" + }, + { + "id": "113466622742179623", + "title": "Back", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113466622742179623&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&max_dim=128&zattribution=none", + "type": "image" + }, + { + "id": "113337099578326473", + "title": "Standing Front", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113337099578326473&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&max_dim=128&zattribution=none", + "type": "image" + }, + { + "id": "113908836188368045", + "title": "Front/Back", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113908836188368045&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&max_dim=128&zattribution=none", + "type": "image" + }, + { + "id": "113643097329661494", + "title": "Front/Back In Situ", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113643097329661494&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&istype=lightwood_pprclip_black&max_dim=128&zattribution=none", + "type": "image" + }, + { + "id": "113099207059102382", + "title": "Paper Corner", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113099207059102382&design=62f85318-d828-459c-9801-c31232b624b5&rlvnet=1&max_dim=128", + "type": "image" + } + ], + "selectedRealview": { + "id": "113335724526596936", + "title": "Front", + "url": "https://rlv.zcache.com/svc/view?zcur=USD&lang=en®ion=us&realview=113335724526596936&design=62f85318-d828-459c-9801-c31232b624b5&style=3.5x2&media=175ptmatte&cornerstyle=normal&envelopes=none&max_dim=128&zattribution=none", + "type": "image" + }, + "shippingEstimate": "Apr 6 - 10", + "title": "Black and Gold Designer Business Card", + "unitLabel": "pack of 100" + }, + "actions": { + "env": { + "currency": "USD", + "language": "en", + "region": "us" + } + }, + "env": { + "currency": "USD", + "language": "en", + "region": "us" + }, + "sdk": { + "env": { + "currency": "USD", + "language": "en", + "region": "us" + } + } +} diff --git a/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js new file mode 100644 index 000000000..90d32f520 --- /dev/null +++ b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js @@ -0,0 +1,152 @@ +import { getLibs, createTag } from '../../../scripts/utils.js'; + +export function formatPaperThickness(thickness) { + const thicknessFormatted = `${thickness.replace('_', '.')}pt thickness`; + return thicknessFormatted; +} + +export function formatPaperWeight(weight) { + const [weightValue, gsmValue] = weight.split('lb'); + const weightFormatted = `${weightValue}lb weight`; + const gsmFormatted = gsmValue?.replace('gsm', ' GSM'); + return { weight: weightFormatted, gsm: gsmFormatted }; +} + +export function extractTemplateId(block) { + const templateIdBlock = block.children[0].children[1].textContent; + const urlParams = new URLSearchParams(window.location.search); + const templateIdURL = urlParams.get('templateId'); + const templateId = templateIdURL || templateIdBlock; + return templateId; +} + +export function formatDeliveryEstimateDateRange(minDate, maxDate) { + const options = { month: 'short', day: 'numeric' }; + const minFormatted = new Date(minDate).toLocaleDateString('en-US', options); + const maxFormatted = new Date(maxDate).toLocaleDateString('en-US', options); + return `${minFormatted} - ${maxFormatted}`; +} + +export function formatLargeNumberToK(totalReviews) { + if (totalReviews > 1000) { + const hundreds = Math.round((totalReviews % 1000) / 100); + if (hundreds === 0) { + return `${Math.round(totalReviews / 1000)}k`; + } + return `${Math.round(totalReviews / 1000)}.${Math.round((totalReviews % 1000) / 100)}k`; + } + return String(totalReviews); +} + +export function exchangeRegionForTopLevelDomain(region) { + const urlParams = new URLSearchParams(window.location.search); + const regionURL = urlParams.get('region'); + const regionFinal = regionURL || region; + const regionToTopLevelDomainMap = { + 'en-GB': 'co.uk', + 'en-US': 'com', + 'en-CA': 'ca', + 'en-AU': 'au', + 'en-NZ': 'nz', + }; + const topLevelDomain = regionToTopLevelDomainMap[regionFinal] || 'com'; + return topLevelDomain; +} + +export async function formatPriceZazzle(price, differential = false) { + const { getCountry } = await import('../../../scripts/utils/location-utils.js'); + const country = await getCountry(); + const { getCurrency, formatPrice } = await import('../../../scripts/utils/pricing.js'); + const currency = await getCurrency(country); + const urlParams = new URLSearchParams(window.location.search); + const region = urlParams.get('region'); + const currencyMap = { + 'en-GB': 'GBP', + 'en-US': 'USD', + 'en-CA': 'CAD', + 'en-AU': 'AUD', + 'en-NZ': 'NZD', + }; + const currencyFinal = currencyMap[region] || currency; + let priceDifferentialOperator; + const localizedPrice = await formatPrice(price, currencyFinal); + if (differential) { + priceDifferentialOperator = price >= 0 ? '+' : ''; + } else { + priceDifferentialOperator = ''; + } + const formattedPrice = priceDifferentialOperator + localizedPrice; + return formattedPrice; +} + +export function formatStringSnakeCase(string) { + const normalizedString = string.replace(/[^a-zA-Z0-9\s]/g, '_'); + const formattedString = normalizedString.trim().toLowerCase().replace(/ /g, '_'); + return formattedString; +} + +export async function addPrefetchLinks() { + const { getConfig } = await import(`${getLibs()}/utils/utils.js`); + const { ietf } = getConfig().locale; + const topLevelDomain = exchangeRegionForTopLevelDomain(ietf); + const prefetchLink1 = createTag('link', { + rel: 'dns-prefetch', + href: `https://www.zazzle.${topLevelDomain}`, + }); + const prefetchLink2 = createTag('link', { + rel: 'dns-prefetch', + href: `https://rlv.zcache.${topLevelDomain}`, + }); + + const preconnectLink1 = createTag('link', { + rel: 'preconnect', + href: `https://www.zazzle.${topLevelDomain}`, + }); + const preconnectLink2 = createTag('link', { + rel: 'preconnect', + href: `https://rlv.zcache.${topLevelDomain}`, + }); + document.head.appendChild(prefetchLink1); + document.head.appendChild(prefetchLink2); + document.head.appendChild(preconnectLink1); + document.head.appendChild(preconnectLink2); +} + +function normalizeLocale(ietf) { + const SUPPORTED_REGIONS = new Set(['at', 'br', 'us', 'au', 'ca', 'gb', 'nz', 'de', 'ch', 'es', 'fr', 'be', 'jp', 'kr', 'nl', 'pt', 'se']); + const SUPPORTED_LANGUAGES = new Set(['en', 'de', 'es', 'fr', 'ja', 'ko', 'nl', 'pt', 'sv']); + if (!ietf) { + return { language: 'en', region: 'us' }; + } + + const [languageRaw = 'en', regionRaw = 'us'] = ietf.split('-'); + const language = languageRaw.toLowerCase(); + const region = regionRaw.toLowerCase(); + + return { + language: SUPPORTED_LANGUAGES.has(language) ? language : 'en', + region: SUPPORTED_REGIONS.has(region) ? region : 'us', + }; +} + +let storePromise = null; +export async function createZazzleStore() { + if (storePromise) return storePromise; + storePromise = (async () => { + const [{ createZazzlePDPStore }, { getConfig }] = await Promise.all([ + import('../sdk/index.js'), + import(`${getLibs()}/utils/utils.js`), + ]); + + const { locale } = getConfig(); + const { language, region } = normalizeLocale(locale?.ietf); + + const store = createZazzlePDPStore({ language, region }); + + return { + env: store.env, + sdk: store, + }; + })(); + return storePromise; +} diff --git a/express/code/scripts/vendors/build-htm-preact.js b/express/code/scripts/vendors/build-htm-preact.js new file mode 100644 index 000000000..17ee07f66 --- /dev/null +++ b/express/code/scripts/vendors/build-htm-preact.js @@ -0,0 +1,34 @@ +/* eslint-disable import/extensions */ +/* eslint-disable max-len */ +// Used to create custom htm-preact dependency file + +import { h, Component, createContext, createRef, render, Fragment } from 'preact'; +import { + useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary, useId, +} from 'preact/hooks'; +import htm from 'htm'; + +function useSyncExternalStore(subscribe, getSnapshot) { + const [state, setState] = useState(() => getSnapshot()); + + useLayoutEffect(() => { + function checkForUpdates() { + const next = getSnapshot(); + setState((prev) => (Object.is(prev, next) ? prev : next)); + } + + checkForUpdates(); + + const unsubscribe = subscribe(checkForUpdates); + return unsubscribe; + }, [subscribe, getSnapshot]); + + useDebugValue(state); + return state; +} + +const html = htm.bind(h); + +export { + h, html, render, Component, createContext, createRef, useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary, useId, useSyncExternalStore, Fragment, +}; diff --git a/express/code/scripts/vendors/htm-preact.js b/express/code/scripts/vendors/htm-preact.js new file mode 100644 index 000000000..3b8422e76 --- /dev/null +++ b/express/code/scripts/vendors/htm-preact.js @@ -0,0 +1 @@ +var _,n,e,t,o,r,u,l,i,c,f,s,a={},p=[],h=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,d=Array.isArray;function v(_,n){for(var e in n)_[e]=n[e];return _}function m(_){_&&_.parentNode&&_.parentNode.removeChild(_)}function y(n,e,t){var o,r,u,l={};for(u in e)"key"==u?o=e[u]:"ref"==u?r=e[u]:l[u]=e[u];if(arguments.length>2&&(l.children=arguments.length>3?_.call(arguments,2):t),"function"==typeof n&&null!=n.defaultProps)for(u in n.defaultProps)void 0===l[u]&&(l[u]=n.defaultProps[u]);return g(n,l,o,r,null)}function g(_,t,o,r,u){var l={type:_,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==u?++e:u,__i:-1,__u:0};return null==u&&null!=n.vnode&&n.vnode(l),l}function b(){return{current:null}}function k(_){return _.children}function w(_,n){this.props=_,this.context=n}function C(_,n){if(null==n)return _.__?C(_.__,_.__i+1):null;for(var e;n<_.__k.length;n++)if(null!=(e=_.__k[n])&&null!=e.__e)return e.__e;return"function"==typeof _.type?C(_):null}function x(_){var n,e;if(null!=(_=_.__)&&null!=_.__c){for(_.__e=_.__c.base=null,n=0;n<_.__k.length;n++)if(null!=(e=_.__k[n])&&null!=e.__e){_.__e=_.__c.base=e.__e;break}return x(_)}}function H(_){(!_.__d&&(_.__d=!0)&&t.push(_)&&!E.__r++||o!=n.debounceRendering)&&((o=n.debounceRendering)||r)(E)}function E(){for(var _,e,o,r,l,i,c,f=1;t.length;)t.length>f&&t.sort(u),_=t.shift(),f=t.length,_.__d&&(o=void 0,r=void 0,l=(r=(e=_).__v).__e,i=[],c=[],e.__P&&((o=v({},r)).__v=r.__v+1,n.vnode&&n.vnode(o),M(e.__P,o,r,e.__n,e.__P.namespaceURI,32&r.__u?[l]:null,i,null==l?C(r):l,!!(32&r.__u),c),o.__v=r.__v,o.__.__k[o.__i]=o,W(i,o,c),r.__e=r.__=null,o.__e!=l&&x(o)));E.__r=0}function S(_,n,e,t,o,r,u,l,i,c,f){var s,h,v,m,y,b,w,x=t&&t.__k||p,H=n.length;for(i=function(_,n,e,t,o){var r,u,l,i,c,f=e.length,s=f,a=0;for(_.__k=new Array(o),r=0;r0?u=_.__k[r]=g(u.type,u.props,u.key,u.ref?u.ref:null,u.__v):_.__k[r]=u,i=r+a,u.__=_,u.__b=_.__b+1,l=null,-1!=(c=u.__i=U(u,e,i,s))&&(s--,(l=e[c])&&(l.__u|=2)),null==l||null==l.__v?(-1==c&&(o>f?a--:oi?a--:a++,u.__u|=4))):_.__k[r]=null;if(s)for(r=0;r(f?1:0))for(o=e-1,r=e+1;o>=0||r=0?o--:r++])&&!(2&c.__u)&&l==c.key&&i==c.type)return u;return-1}function N(_,n,e){"-"==n[0]?_.setProperty(n,null==e?"":e):_[n]=null==e?"":"number"!=typeof e||h.test(n)?e:e+"px"}function D(_,n,e,t,o){var r,u;_:if("style"==n)if("string"==typeof e)_.style.cssText=e;else{if("string"==typeof t&&(_.style.cssText=t=""),t)for(n in t)e&&n in e||N(_.style,n,"");if(e)for(n in e)t&&e[n]==t[n]||N(_.style,n,e[n])}else if("o"==n[0]&&"n"==n[1])r=n!=(n=n.replace(l,"$1")),u=n.toLowerCase(),n=u in _||"onFocusOut"==n||"onFocusIn"==n?u.slice(2):n.slice(2),_.l||(_.l={}),_.l[n+r]=e,e?t?e.u=t.u:(e.u=i,_.addEventListener(n,r?f:c,r)):_.removeEventListener(n,r?f:c,r);else{if("http://www.w3.org/2000/svg"==o)n=n.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!=n&&"height"!=n&&"href"!=n&&"list"!=n&&"form"!=n&&"tabIndex"!=n&&"download"!=n&&"rowSpan"!=n&&"colSpan"!=n&&"role"!=n&&"popover"!=n&&n in _)try{_[n]=null==e?"":e;break _}catch(_){}"function"==typeof e||(null==e||!1===e&&"-"!=n[4]?_.removeAttribute(n):_.setAttribute(n,"popover"==n&&1==e?"":e))}}function T(_){return function(e){if(this.l){var t=this.l[e.type+_];if(null==e.t)e.t=i++;else if(e.t0?_:d(_)?_.map(A):v({},_)}function L(e,t,o,r,u,l,i,c,f){var s,p,h,v,y,g,b,k=o.props||a,w=t.props,x=t.type;if("svg"==x?u="http://www.w3.org/2000/svg":"math"==x?u="http://www.w3.org/1998/Math/MathML":u||(u="http://www.w3.org/1999/xhtml"),null!=l)for(s=0;s=e.__.length&&e.__.push({}),e.__[_]}function t_(_){return G=1,o_(b_,_)}function o_(_,n,e){var t=e_(q++,2);if(t.t=_,!t.__c&&(t.__=[e?e(n):b_(void 0,n),function(_){var n=t.__N?t.__N[0]:t.__[0],e=t.t(n,_);n!==e&&(t.__N=[e,t.__[1]],t.__c.setState({}))}],t.__c=B,!B.__f)){var o=function(_,n,e){if(!t.__c.__H)return!0;var o=t.__c.__H.__.filter(function(_){return!!_.__c});if(o.every(function(_){return!_.__N}))return!r||r.call(this,_,n,e);var u=t.__c.props!==_;return o.forEach(function(_){if(_.__N){var n=_.__[0];_.__=_.__N,_.__N=void 0,n!==_.__[0]&&(u=!0)}}),r&&r.call(this,_,n,e)||u};B.__f=!0;var r=B.shouldComponentUpdate,u=B.componentWillUpdate;B.componentWillUpdate=function(_,n,e){if(this.__e){var t=r;r=void 0,o(_,n,e),r=t}u&&u.call(this,_,n,e)},B.shouldComponentUpdate=o}return t.__N||t.__}function r_(_,n){var e=e_(q++,3);!K.__s&&g_(e.__H,n)&&(e.__=_,e.u=n,B.__H.__h.push(e))}function u_(_,n){var e=e_(q++,4);!K.__s&&g_(e.__H,n)&&(e.__=_,e.u=n,B.__h.push(e))}function l_(_){return G=5,i_(function(){return{current:_}},[])}function i_(_,n){var e=e_(q++,7);return g_(e.__H,n)&&(e.__=_(),e.__H=n,e.__h=_),e.__}function c_(_,n){return G=8,i_(function(){return _},n)}function f_(_){var n=B.context[_.__c],e=e_(q++,9);return e.c=_,n?(null==e.__&&(e.__=!0,n.sub(B)),n.props.value):_.__}function s_(_,n){K.useDebugValue&&K.useDebugValue(n?n(_):_)}function a_(_){var n=e_(q++,10),e=t_();return n.__=_,B.componentDidCatch||(B.componentDidCatch=function(_,t){n.__&&n.__(_,t),e[1](_)}),[e[0],function(){e[1](void 0)}]}function p_(){var _=e_(q++,11);if(!_.__){for(var n=B.__v;null!==n&&!n.__m&&null!==n.__;)n=n.__;var e=n.__m||(n.__m=[0,0]);_.__="P"+e[0]+"-"+e[1]++}return _.__}function h_(){for(var _;_=J.shift();)if(_.__P&&_.__H)try{_.__H.__h.forEach(m_),_.__H.__h.forEach(y_),_.__H.__h=[]}catch(n){_.__H.__h=[],K.__e(n,_.__v)}}K.__b=function(_){B=null,Q&&Q(_)},K.__=function(_,n){_&&n.__k&&n.__k.__m&&(_.__m=n.__k.__m),n_&&n_(_,n)},K.__r=function(_){X&&X(_),q=0;var n=(B=_.__c).__H;n&&(V===B?(n.__h=[],B.__h=[],n.__.forEach(function(_){_.__N&&(_.__=_.__N),_.u=_.__N=void 0})):(n.__h.forEach(m_),n.__h.forEach(y_),n.__h=[],q=0)),V=B},K.diffed=function(_){Y&&Y(_);var n=_.__c;n&&n.__H&&(n.__H.__h.length&&(1!==J.push(n)&&z===K.requestAnimationFrame||((z=K.requestAnimationFrame)||v_)(h_)),n.__H.__.forEach(function(_){_.u&&(_.__H=_.u),_.u=void 0})),V=B=null},K.__c=function(_,n){n.some(function(_){try{_.__h.forEach(m_),_.__h=_.__h.filter(function(_){return!_.__||y_(_)})}catch(e){n.some(function(_){_.__h&&(_.__h=[])}),n=[],K.__e(e,_.__v)}}),Z&&Z(_,n)},K.unmount=function(_){__&&__(_);var n,e=_.__c;e&&e.__H&&(e.__H.__.forEach(function(_){try{m_(_)}catch(_){n=_}}),e.__H=void 0,n&&K.__e(n,e.__v))};var d_="function"==typeof requestAnimationFrame;function v_(_){var n,e=function(){clearTimeout(t),d_&&cancelAnimationFrame(n),setTimeout(_)},t=setTimeout(e,35);d_&&(n=requestAnimationFrame(e))}function m_(_){var n=B,e=_.__c;"function"==typeof e&&(_.__c=void 0,e()),B=n}function y_(_){var n=B;_.__c=_.__(),B=n}function g_(_,n){return!_||_.length!==n.length||n.some(function(n,e){return n!==_[e]})}function b_(_,n){return"function"==typeof n?n(_):n}var k_=function(_,n,e,t){var o;n[0]=0;for(var r=1;rn());return u_(()=>{function e(){const _=n();t(n=>Object.is(n,_)?n:_)}return e(),_(e)},[_,n]),s_(e),e}const x_=function(_){var n=w_.get(this);return n||(n=new Map,w_.set(this,n)),(n=k_(this,n.get(_)||(n.set(_,n=function(_){for(var n,e,t=1,o="",r="",u=[0],l=function(_){1===t&&(_||(o=o.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?u.push(0,_,o):3===t&&(_||o)?(u.push(3,_,o),t=2):2===t&&"..."===o&&_?u.push(4,_,0):2===t&&o&&!_?u.push(5,0,!0,o):t>=5&&((o||!_&&5===t)&&(u.push(t,0,o,e),t=6),_&&(u.push(t,_,0,e),t=6)),o=""},i=0;i<_.length;i++){i&&(1===t&&l(),l(i));for(var c=0;c<_[i].length;c++)n=_[i][c],1===t?"<"===n?(l(),u=[u],t=3):o+=n:4===t?"--"===o&&">"===n?(t=1,o=""):o=n+o[0]:r?n===r?r="":o+=n:'"'===n||"'"===n?r=n:">"===n?(l(),t=1):t&&("="===n?(t=5,e=o,o=""):"/"===n&&(t<5||">"===_[i][c+1])?(l(),3===t&&(u=u[0]),t=u,(u=u[0]).push(2,0,t),t=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(l(),t=2):o+=n),3===t&&"!--"===o&&(t=4,u=u[0])}return l(),u}(_)),n),arguments,[])).length>1?n:n[0]}.bind(y);export{w as Component,k as Fragment,j as createContext,b as createRef,y as h,x_ as html,O as render,c_ as useCallback,f_ as useContext,s_ as useDebugValue,r_ as useEffect,a_ as useErrorBoundary,p_ as useId,u_ as useLayoutEffect,i_ as useMemo,o_ as useReducer,l_ as useRef,t_ as useState,C_ as useSyncExternalStore}; diff --git a/package.json b/package.json index 5017e3e2c..7b7756901 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lint:js": "eslint .", "lint:css": "stylelint 'blocks/**/*.css' 'express/code/styles/*.css'", "build:spectrum": "node express/code/scripts/widgets/spectrum/build.mjs", - "build:spectrum:icons-catalog": "ICONS_CATALOG=1 node express/code/scripts/widgets/spectrum/build.mjs" + "build:spectrum:icons-catalog": "ICONS_CATALOG=1 node express/code/scripts/widgets/spectrum/build.mjs", + "build:htm-preact": "microbundle express/code/scripts/vendors/build-htm-preact.js -o express/code/scripts/vendors/htm-preact.js -f modern --no-sourcemap --target web; mv express/code/scripts/vendors/htm-preact.modern.js express/code/scripts/vendors/htm-preact.js" }, "repository": { "type": "git", From 0a4082a20179a2cc3a226fb577c3ec55ab2c5e81 Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Mon, 6 Apr 2026 14:23:46 -0700 Subject: [PATCH 02/11] updating filenames and references to avoid linting errors --- .../code/blocks/print-product-detail-sdk/components/Contexts.js | 2 +- .../print-product-detail-sdk/components/CustomizationInputs.js | 2 +- .../print-product-detail-sdk/components/ProductComponents.js | 2 +- .../code/blocks/print-product-detail-sdk/components/useSeo.js | 2 +- .../blocks/print-product-detail-sdk/print-product-detail-sdk.js | 2 +- .../print-product-detail-sdk/sdk/{index.js => index.min.js} | 0 .../print-product-detail-sdk/utilities/utility-functions.js | 2 +- .../code/scripts/vendors/{htm-preact.js => htm-preact.min.js} | 0 package.json | 2 +- 9 files changed, 7 insertions(+), 7 deletions(-) rename express/code/blocks/print-product-detail-sdk/sdk/{index.js => index.min.js} (100%) rename express/code/scripts/vendors/{htm-preact.js => htm-preact.min.js} (100%) diff --git a/express/code/blocks/print-product-detail-sdk/components/Contexts.js b/express/code/blocks/print-product-detail-sdk/components/Contexts.js index 5c3066d3c..49c90f51c 100644 --- a/express/code/blocks/print-product-detail-sdk/components/Contexts.js +++ b/express/code/blocks/print-product-detail-sdk/components/Contexts.js @@ -1,4 +1,4 @@ -import { html, createContext, useContext, useMemo, useSyncExternalStore, useEffect, useCallback, useState } from '../../../scripts/vendors/htm-preact.js'; +import { html, createContext, useContext, useMemo, useSyncExternalStore, useEffect, useCallback, useState } from '../../../scripts/vendors/htm-preact.min.js'; export const StoreContext = createContext(null); diff --git a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js index 8fee59450..3b30bdca7 100644 --- a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js +++ b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js @@ -2,7 +2,7 @@ import { html, useEffect, useRef, -} from '../../../scripts/vendors/htm-preact.js'; +} from '../../../scripts/vendors/htm-preact.min.js'; import { useStore } from './Contexts.js'; import createSimpleCarousel from '../../../scripts/widgets/simple-carousel.js'; import { createPicker } from '../../../scripts/widgets/picker.js'; diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index e88168722..bd0dcae05 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -5,7 +5,7 @@ import { useState, Fragment, useCallback, -} from '../../../scripts/vendors/htm-preact.js'; +} from '../../../scripts/vendors/htm-preact.min.js'; import { useStore, useDrawer } from './Contexts.js'; import axAccordionDecorate from '../../ax-accordion/ax-accordion.js'; import { formatLargeNumberToK } from '../utilities/utility-functions.js'; diff --git a/express/code/blocks/print-product-detail-sdk/components/useSeo.js b/express/code/blocks/print-product-detail-sdk/components/useSeo.js index fce4c011a..8f558fa6e 100644 --- a/express/code/blocks/print-product-detail-sdk/components/useSeo.js +++ b/express/code/blocks/print-product-detail-sdk/components/useSeo.js @@ -1,4 +1,4 @@ -import { useEffect } from '../../../scripts/vendors/htm-preact.js'; +import { useEffect } from '../../../scripts/vendors/htm-preact.min.js'; import { useStore } from './Contexts.js'; export function getCanonicalUrl() { diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js index 12cdde3b0..e244217ed 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js @@ -6,7 +6,7 @@ import { useEffect, useRef, Fragment, -} from '../../scripts/vendors/htm-preact.js'; +} from '../../scripts/vendors/htm-preact.min.js'; import { StoreProvider, useStore, DrawerProvider, useDrawer } from './components/Contexts.js'; import { ProductImages, ProductDetails, ProductHeader, CheckoutButton, Drawer } from './components/ProductComponents.js'; import { CustomizationInputs } from './components/CustomizationInputs.js'; diff --git a/express/code/blocks/print-product-detail-sdk/sdk/index.js b/express/code/blocks/print-product-detail-sdk/sdk/index.min.js similarity index 100% rename from express/code/blocks/print-product-detail-sdk/sdk/index.js rename to express/code/blocks/print-product-detail-sdk/sdk/index.min.js diff --git a/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js index 90d32f520..21c1e4e6e 100644 --- a/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js +++ b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js @@ -134,7 +134,7 @@ export async function createZazzleStore() { if (storePromise) return storePromise; storePromise = (async () => { const [{ createZazzlePDPStore }, { getConfig }] = await Promise.all([ - import('../sdk/index.js'), + import('../sdk/index.min.js'), import(`${getLibs()}/utils/utils.js`), ]); diff --git a/express/code/scripts/vendors/htm-preact.js b/express/code/scripts/vendors/htm-preact.min.js similarity index 100% rename from express/code/scripts/vendors/htm-preact.js rename to express/code/scripts/vendors/htm-preact.min.js diff --git a/package.json b/package.json index 7b7756901..2da9acf95 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:css": "stylelint 'blocks/**/*.css' 'express/code/styles/*.css'", "build:spectrum": "node express/code/scripts/widgets/spectrum/build.mjs", "build:spectrum:icons-catalog": "ICONS_CATALOG=1 node express/code/scripts/widgets/spectrum/build.mjs", - "build:htm-preact": "microbundle express/code/scripts/vendors/build-htm-preact.js -o express/code/scripts/vendors/htm-preact.js -f modern --no-sourcemap --target web; mv express/code/scripts/vendors/htm-preact.modern.js express/code/scripts/vendors/htm-preact.js" + "build:htm-preact": "microbundle express/code/scripts/vendors/build-htm-preact.js -o express/code/scripts/vendors/htm-preact.min.js -f modern --no-sourcemap --target web; mv express/code/scripts/vendors/htm-preact.min.modern.js express/code/scripts/vendors/htm-preact.min.js" }, "repository": { "type": "git", From b65bf99441a1cfa9d2ee620c684e814c49f356c4 Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Mon, 6 Apr 2026 14:56:01 -0700 Subject: [PATCH 03/11] fixed lint errors related to this block --- .../components/CustomizationInputs.js | 24 ++++++++--------- .../components/ProductComponents.js | 26 ++++++------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js index 3b30bdca7..2ac993009 100644 --- a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js +++ b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js @@ -74,9 +74,7 @@ export function CheckboxSelector({ attribute }) { onChange=${handleChange} /> ${selector.title}${selector.priceDelta - ? ` ${selector.priceDelta}` - : ''}${selector.title}${selector.priceDelta ? ` ${selector.priceDelta}` : ''}
  • @@ -307,8 +305,8 @@ export function RadioSelector({ attribute }) { /> ${option.title}${option.priceDelta - ? ` ${option.priceDelta}` - : ''} `, @@ -707,12 +705,12 @@ export function ThumbnailSelector({ attribute, onRequestDrawer, productType }) { aria-label="${group.title || `${title} options`}" > ${group.title - && html`
    ${group.title}
    `} + && html`
    ${group.title}
    `} ${(group.options || []).map((option) => { - const thumbnailUrl = updateImageUrl(option.imageUrl); - const isSelected = option.value === selectedOptionValue; - const optionIndex = allOptions.findIndex((candidate) => candidate.value === option.value); - return html` + const thumbnailUrl = updateImageUrl(option.imageUrl); + const isSelected = option.value === selectedOptionValue; + const optionIndex = allOptions.findIndex((candidate) => candidate.value === option.value); + return html` `; - })} + })}
    `, )} @@ -764,8 +762,8 @@ export function ThumbnailSelector({ attribute, onRequestDrawer, productType }) { />
    `} diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index bd0dcae05..de995cf2b 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -406,7 +406,7 @@ function SizeChartTable({ measurementTypes, attributeValues }) { `; } -function SizeChartContent({ onClose }) { +function SizeChartContent() { const { actions } = useStore(); const [chart, setChart] = useState(null); const [error, setError] = useState(null); @@ -493,7 +493,7 @@ function SizeChartContent({ onClose }) { `; } -function PrintingProcessContent({ onClose }) { +function PrintingProcessContent() { return html`
    @@ -547,16 +547,6 @@ function PrintingProcessContent({ onClose }) { `; } -function stripHtmlTags(str) { - let result = str; - let prev; - do { - prev = result; - result = result.replace(/<[^>]*>/g, ''); - } while (result !== prev); - return result; -} - function flattenOptionGroups(selector) { if (!selector?.optionGroups || !Array.isArray(selector.optionGroups)) { return []; @@ -693,11 +683,11 @@ function PaperTypeContent({ onClose }) { ${preview?.descriptionHTML && html`
    ${preview.descriptionHTML - .split(//i)[0] - .replace(/[<>]/g, '') - .split('/') - .filter(Boolean) - .map((spec) => html` + .split(//i)[0] + .replace(/[<>]/g, '') + .split('/') + .filter(Boolean) + .map((spec) => html`
    ${spec.trim()} @@ -720,7 +710,7 @@ function PaperTypeContent({ onClose }) { ${state?.descriptionComponents?.[1]?.descriptionHTML && html`
    `}
    From 6088f88779fd73fe995dc10c8a20072e08b2941a Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Mon, 6 Apr 2026 14:58:10 -0700 Subject: [PATCH 04/11] fixed lint errors related to this block --- .../code/scripts/vendors/build-htm-preact.js | 34 ------------------- package.json | 5 ++- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 express/code/scripts/vendors/build-htm-preact.js diff --git a/express/code/scripts/vendors/build-htm-preact.js b/express/code/scripts/vendors/build-htm-preact.js deleted file mode 100644 index 17ee07f66..000000000 --- a/express/code/scripts/vendors/build-htm-preact.js +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable import/extensions */ -/* eslint-disable max-len */ -// Used to create custom htm-preact dependency file - -import { h, Component, createContext, createRef, render, Fragment } from 'preact'; -import { - useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary, useId, -} from 'preact/hooks'; -import htm from 'htm'; - -function useSyncExternalStore(subscribe, getSnapshot) { - const [state, setState] = useState(() => getSnapshot()); - - useLayoutEffect(() => { - function checkForUpdates() { - const next = getSnapshot(); - setState((prev) => (Object.is(prev, next) ? prev : next)); - } - - checkForUpdates(); - - const unsubscribe = subscribe(checkForUpdates); - return unsubscribe; - }, [subscribe, getSnapshot]); - - useDebugValue(state); - return state; -} - -const html = htm.bind(h); - -export { - h, html, render, Component, createContext, createRef, useState, useReducer, useEffect, useLayoutEffect, useRef, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary, useId, useSyncExternalStore, Fragment, -}; diff --git a/package.json b/package.json index 2da9acf95..ba30389f0 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "lint:js": "eslint .", "lint:css": "stylelint 'blocks/**/*.css' 'express/code/styles/*.css'", "build:spectrum": "node express/code/scripts/widgets/spectrum/build.mjs", - "build:spectrum:icons-catalog": "ICONS_CATALOG=1 node express/code/scripts/widgets/spectrum/build.mjs", - "build:htm-preact": "microbundle express/code/scripts/vendors/build-htm-preact.js -o express/code/scripts/vendors/htm-preact.min.js -f modern --no-sourcemap --target web; mv express/code/scripts/vendors/htm-preact.min.modern.js express/code/scripts/vendors/htm-preact.min.js" + "build:spectrum:icons-catalog": "ICONS_CATALOG=1 node express/code/scripts/widgets/spectrum/build.mjs" }, "repository": { "type": "git", @@ -89,4 +88,4 @@ "@spectrum-web-components/tags": "^1.11.0", "@spectrum-web-components/toast": "^1.11.0" } -} +} \ No newline at end of file From 81e8f83254ec527c87ebb31938af4db8cc2e182d Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 12:11:48 -0700 Subject: [PATCH 05/11] fixed bug with image thumbnail carousel not updating --- .../print-product-detail-sdk/components/ProductComponents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index de995cf2b..cf76aec5e 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -229,7 +229,7 @@ export function ProductImages() { carouselCleanupRef.current = null; } }; - }, [realviews.map((r) => r.id).join(',')]); + }, [realviews.map((r) => `${r.id}:${r.url}`).join(',')]); // Update selected state when selection changes useEffect(() => { From ee2ceb40118f4bbcf6c9708eca4c0e59b8ed2cbc Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 12:29:41 -0700 Subject: [PATCH 06/11] solved issue with mini pills in drawer default option not being select-able --- .../print-product-detail-sdk/components/ProductComponents.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index cf76aec5e..95738d133 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -595,9 +595,7 @@ function PaperTypeContent({ onClose }) { button.setAttribute('aria-checked', isSelected ? 'true' : 'false'); button.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); button.addEventListener('click', () => { - if (option.value !== selectedOptionValue) { - actions.selectOption(attribute.name, option.value); - } + actions.selectOption(attribute.name, option.value); }); const img = document.createElement('img'); From 6b6acc6a9e057938cbd9a242dbcb1c7be2dacb09 Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 12:46:57 -0700 Subject: [PATCH 07/11] fixed z-index issue with dropdown menus and checkout button --- .../print-product-detail-sdk/print-product-detail-sdk.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css index 57525f82c..dcdcedddb 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css @@ -119,6 +119,10 @@ .pdpx-customization-inputs-container { padding-bottom: var(--spacing-400); } +.pdpx-customization-inputs-container .picker-container.opened { + position: relative; + z-index: 19; +} .pdpx-price-info-container { display: flex; flex-direction: column; From eed873050f4efdf266b78cbfdfeb09389f68a601 Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 14:43:10 -0700 Subject: [PATCH 08/11] fixed small issue with dropdown menus getting cut off at bottom edge of product-info-container --- .../print-product-detail-sdk.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css index dcdcedddb..abbf35069 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css @@ -123,6 +123,15 @@ position: relative; z-index: 19; } +.pdpx-customization-inputs-container .picker-container.opened .picker-options-wrapper::after { + content: ''; + position: absolute; + bottom: -16px; + left: 0; + width: 1px; + height: 16px; + pointer-events: none; +} .pdpx-price-info-container { display: flex; flex-direction: column; @@ -972,6 +981,7 @@ main .section .ax-accordion-item-description p { background: linear-gradient(to top, white 20%, rgba(255, 255, 255, 0) 100%); } + /*TABLET*/ @media (min-width: 600px) { .pdpx-global-container { From eab5370520ef6f8b9b0a22a540cccaccf4ebb25e Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 15:27:48 -0700 Subject: [PATCH 09/11] small bug fixes / imrovements as per PR feedback --- .../components/ProductComponents.js | 13 +-- .../print-product-detail-sdk.css | 81 +++++++++---------- 2 files changed, 45 insertions(+), 49 deletions(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index 95738d133..564b1454f 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -764,14 +764,15 @@ export function Drawer() { document.addEventListener('keydown', handleKeyDown); // Move focus into the drawer - requestAnimationFrame(() => { - if (drawerRef.current) { - const firstFocusable = drawerRef.current.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); - if (firstFocusable) firstFocusable.focus(); - } - }); + let active = true; + setTimeout(() => { + if (!active || !drawerRef.current) return; + const firstFocusable = drawerRef.current.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (firstFocusable) firstFocusable.focus(); + }, 0); return () => { + active = false; document.removeEventListener('keydown', handleKeyDown); // Return focus to the triggering element if (triggerRef.current?.focus) { diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css index abbf35069..f638df3c6 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css @@ -575,51 +575,46 @@ main .pdpx-checkout-button-subhead a.pdpx-checkout-button-subhead-link { border-color: var(--color-black); transition: var(--standard-transition-hover-in-border-color); } -@media (hover: none), (pointer: coarse) { - .pdpx-mini-pill-image-container::after, +@media (hover: hover) { + .pdpx-mini-pill-image-container::after { + content: attr(aria-label); + position: fixed; + font-size: var(--ax-body-xs-size); + padding: var(--spacing-325) var(--spacing-200); + top: var(--tooltip-top); + left: var(--tooltip-left); + background: var(--color-gray-800); + color: var(--color-white); + border-radius: 6px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: var(--standard-transition-hover-out-opacity); + z-index: 200; + } .pdpx-mini-pill-image-container::before { - display: none !important; - content: none; + content: ''; + position: fixed; + top: var(--arrow-top); + left: var(--arrow-left); + bottom: 100%; + transform: translate(-50%, -4px) rotate(45deg); + width: 8px; + height: 8px; + background: var(--color-gray-800); + pointer-events: none; + opacity: 0; + visibility: hidden; + transition: var(--standard-transition-hover-out-opacity); + z-index: 199; + } + .pdpx-mini-pill-image-container:hover::after, + .pdpx-mini-pill-image-container:hover::before { + opacity: 1; + visibility: visible; + transition: var(--standard-transition-hover-in-opacity); } -} -.pdpx-mini-pill-image-container::after { - content: attr(aria-label); - position: fixed; - font-size: var(--ax-body-xs-size); - padding: var(--spacing-325) var(--spacing-200); - top: var(--tooltip-top); - left: var(--tooltip-left); - background: var(--color-gray-800); - color: var(--color-white); - border-radius: 6px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - visibility: hidden; - transition: var(--standard-transition-hover-out-opacity); - z-index: 200; -} -.pdpx-mini-pill-image-container::before { - content: ''; - position: fixed; - top: var(--arrow-top); - left: var(--arrow-left); - bottom: 100%; - transform: translate(-50%, -4px) rotate(45deg); - width: 8px; - height: 8px; - background: var(--color-gray-800); - pointer-events: none; - opacity: 0; - visibility: hidden; - transition: var(--standard-transition-hover-out-opacity); - z-index: 199; -} -.pdpx-mini-pill-image-container:hover::after, -.pdpx-mini-pill-image-container:hover::before { - opacity: 1; - visibility: visible; - transition: var(--standard-transition-hover-in-opacity); } .pdpx-mini-pill-text-container { display: none; From 4d344518d1c5af4c95283202df07069153eb8411 Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 15:34:21 -0700 Subject: [PATCH 10/11] more small bug fixes / imrovements as per PR feedback --- .../components/CustomizationInputs.js | 65 ++++++++++--------- .../components/ProductComponents.js | 8 ++- .../print-product-detail-sdk.css | 40 ++++++------ .../print-product-detail-sdk.js | 4 +- 4 files changed, 62 insertions(+), 55 deletions(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js index 2ac993009..df5658af7 100644 --- a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js +++ b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js @@ -393,6 +393,7 @@ function buildPillElement(option, isSelected, index, setSize, activeIndex, handl function MiniPillCarousel({ attribute, onRequestDrawer, productType }) { const containerRef = useRef(null); const carouselCleanupsRef = useRef([]); + const handlersRef = useRef({}); const { actions } = useStore(); const { selector, selectedOptionValue, title } = attribute; let { helpLink } = attribute; @@ -422,35 +423,36 @@ function MiniPillCarousel({ attribute, onRequestDrawer, productType }) { label: 'Learn More', }; } - const handleOptionClick = (option) => { - actions.selectOption(attribute.name, option.value); - debouncedTrackOptionSelect({ - attributeName: attribute.name, - actionValue: option.value, - productType, - }); - }; - - const handleMiniPillKeyDown = (event) => { - const { key, currentTarget } = event; - if (!['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) { - return; - } - const buttons = Array.from( - containerRef.current?.querySelectorAll('.pdpx-mini-pill-image-container') || [], - ); - if (!buttons.length) { - return; - } - const currentIndex = buttons.indexOf(currentTarget); - if (currentIndex < 0) { - return; - } - event.preventDefault(); - const nextIndex = getNextRadioIndex(currentIndex, key, buttons.length - 1); - const nextButton = buttons[nextIndex]; - nextButton?.focus(); - nextButton?.click(); + handlersRef.current = { + handleOptionClick: (option) => { + actions.selectOption(attribute.name, option.value); + debouncedTrackOptionSelect({ + attributeName: attribute.name, + actionValue: option.value, + productType, + }); + }, + handleMiniPillKeyDown: (event) => { + const { key, currentTarget } = event; + if (!['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key)) { + return; + } + const buttons = Array.from( + containerRef.current?.querySelectorAll('.pdpx-mini-pill-image-container') || [], + ); + if (!buttons.length) { + return; + } + const currentIndex = buttons.indexOf(currentTarget); + if (currentIndex < 0) { + return; + } + event.preventDefault(); + const nextIndex = getNextRadioIndex(currentIndex, key, buttons.length - 1); + const nextButton = buttons[nextIndex]; + nextButton?.focus(); + nextButton?.click(); + }, }; const triggerDrawer = () => { @@ -470,7 +472,10 @@ function MiniPillCarousel({ attribute, onRequestDrawer, productType }) { const container = containerRef.current; container.innerHTML = ''; - const handlers = { handleOptionClick, handleMiniPillKeyDown }; + const handlers = { + handleOptionClick: (option) => handlersRef.current.handleOptionClick(option), + handleMiniPillKeyDown: (event) => handlersRef.current.handleMiniPillKeyDown(event), + }; if (isSegmented) { const sectionsWrapper = document.createElement('div'); diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index 564b1454f..ad1575314 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -104,7 +104,7 @@ export function ProductHeader() {
    - + ${formattedCount}
    `} @@ -561,6 +561,8 @@ function PaperTypeContent({ onClose }) { const { state: drawerState } = useDrawer(); const pillContainerRef = useRef(null); const carouselCleanupRef = useRef(null); + const actionsRef = useRef(actions); + actionsRef.current = actions; const attrName = drawerState.payload?.attribute?.name; const attribute = (state?.attributes || []).find((a) => a.name === attrName); if (!attribute) return null; @@ -595,7 +597,7 @@ function PaperTypeContent({ onClose }) { button.setAttribute('aria-checked', isSelected ? 'true' : 'false'); button.setAttribute('aria-pressed', isSelected ? 'true' : 'false'); button.addEventListener('click', () => { - actions.selectOption(attribute.name, option.value); + actionsRef.current.selectOption(attribute.name, option.value); }); const img = document.createElement('img'); @@ -801,7 +803,7 @@ export function Drawer() { class="pdpx-drawer ${state.open ? '' : 'hidden'}" id="pdp-x-drawer" role="dialog" - aria-modal="${state.open ? 'true' : 'false'}" + aria-modal="true" aria-label="${drawerLabel}" >
    diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css index f638df3c6..1427da1dd 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.css @@ -39,7 +39,7 @@ } } -.hidden { +.print-product-detail-sdk .hidden { display: none; visibility: hidden; position: absolute; @@ -60,7 +60,7 @@ -moz-appearance: none; appearance: none; } -[data-skeleton='true'] { +.print-product-detail-sdk [data-skeleton='true'] { background: linear-gradient(90deg, #eee 25%, #f5f5f5 37%, #eee 63%); background-size: 400% 100%; animation: skeleton-shimmer 1.2s ease-in-out infinite; @@ -874,26 +874,26 @@ main .section .ax-accordion-item-description p { background: rgb(0 0 0 / 40%); } /* SIZE CHART */ -.size-chart-product-name { +.print-product-detail-sdk .size-chart-product-name { font-size: var(--body-font-size-l); font-weight: var(--subheading-font-weight); color: var(--color-gray-950); margin: 0; margin-bottom: var(--spacing-100); } -.size-chart-table-container { +.print-product-detail-sdk .size-chart-table-container { border-radius: 16px; border: 1px solid var(--color-gray-325); padding: var(--spacing-400); color: var(--color-gray-900); margin-bottom: var(--spacing-400); } -.size-chart-table-section { +.print-product-detail-sdk .size-chart-table-section { display: flex; flex-direction: column; gap: var(--spacing-100); } -.size-chart-table thead .size-chart-table-header { +.print-product-detail-sdk .size-chart-table thead .size-chart-table-header { font-size: var(--body-font-size-s); font-weight: var(--subheading-font-weight); color: var(--color-gray-950); @@ -901,13 +901,13 @@ main .section .ax-accordion-item-description p { padding: var(--spacing-100); white-space: nowrap; } -.size-chart-table { +.print-product-detail-sdk .size-chart-table { width: 100%; border-collapse: separate; border-spacing: 0 var(--spacing-100); table-layout: fixed; } -.size-chart-table thead th { +.print-product-detail-sdk .size-chart-table thead th { font-size: var(--body-font-size-s); font-weight: normal; color: var(--color-gray-800); @@ -915,50 +915,50 @@ main .section .ax-accordion-item-description p { padding: var(--spacing-100); padding-top: 0px; } -.size-chart-table thead th:nth-child(2), -.size-chart-table thead th:nth-child(3) { +.print-product-detail-sdk .size-chart-table thead th:nth-child(2), +.print-product-detail-sdk .size-chart-table thead th:nth-child(3) { padding-left: var(--spacing-400); text-align: right; } -.size-chart-table tbody tr { +.print-product-detail-sdk .size-chart-table tbody tr { background: var(--color-white); } -.size-chart-table tbody tr:nth-child(odd) { +.print-product-detail-sdk .size-chart-table tbody tr:nth-child(odd) { background: #ebebeb; } -.size-chart-table tbody tr:nth-child(odd) td:first-child { +.print-product-detail-sdk .size-chart-table tbody tr:nth-child(odd) td:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; } -.size-chart-table tbody tr:nth-child(odd) td:last-child { +.print-product-detail-sdk .size-chart-table tbody tr:nth-child(odd) td:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; } -.size-chart-table tbody td { +.print-product-detail-sdk .size-chart-table tbody td { font-size: var(--body-font-size-xs); color: var(--color-gray-950); padding: var(--spacing-100) var(--spacing-200); word-break: break-word; line-height: 1.3; } -.size-chart-table tbody td:nth-child(2), -.size-chart-table tbody td:nth-child(3) { +.print-product-detail-sdk .size-chart-table tbody td:nth-child(2), +.print-product-detail-sdk .size-chart-table tbody td:nth-child(3) { padding-left: var(--spacing-400); text-align: right; } -.size-chart-instructions { +.print-product-detail-sdk .size-chart-instructions { display: flex; flex-direction: column; gap: var(--spacing-300); margin-bottom: 80px; } -.size-chart-instruction-section h3 { +.print-product-detail-sdk .size-chart-instruction-section h3 { font-size: var(--body-font-size-m); font-weight: var(--subheading-font-weight); color: var(--color-gray-950); margin: 0 0 var(--spacing-100) 0; } -.size-chart-instructions .size-chart-instruction-text { +.print-product-detail-sdk .size-chart-instructions .size-chart-instruction-text { font-size: var(--body-font-size-s); font-style: normal; font-weight: var(--ax-body-weight); diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js index e244217ed..33624714e 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js @@ -15,7 +15,7 @@ import useSeo from './components/useSeo.js'; function LoadingSkeleton() { return html` -
    +
    @@ -96,7 +96,7 @@ function PDPContent({ templateId }) { } return html` -
    +
    <${ProductImages} />
    <${ProductHeader} /> From 14b65c71b657fcda8523b034394791430d0af40d Mon Sep 17 00:00:00 2001 From: Max Nelson Date: Fri, 10 Apr 2026 15:47:47 -0700 Subject: [PATCH 11/11] more small bug fixes / imrovements as per PR feedback --- .../components/CustomizationInputs.js | 11 +++---- .../components/ProductComponents.js | 8 +++-- .../print-product-detail-sdk.js | 17 +++++++++- .../utilities/utility-functions.js | 31 +++++++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js index df5658af7..4aed7fd16 100644 --- a/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js +++ b/express/code/blocks/print-product-detail-sdk/components/CustomizationInputs.js @@ -8,6 +8,7 @@ import createSimpleCarousel from '../../../scripts/widgets/simple-carousel.js'; import { createPicker } from '../../../scripts/widgets/picker.js'; import { trackPrintAddonOptionSelect } from '../../../scripts/instrument.js'; import { debounce } from '../../../scripts/utils/hofs.js'; +import { sanitizeHtml } from '../utilities/utility-functions.js'; const debouncedTrackOptionSelect = debounce((payload) => { trackPrintAddonOptionSelect(payload).catch(() => { }); @@ -767,7 +768,7 @@ export function ThumbnailSelector({ attribute, onRequestDrawer, productType }) { />
    @@ -793,18 +794,14 @@ function renderAttribute(attribute, onRequestDrawer, key, productType) { productType=${productType} />`; case 'radio': - return html`<${DropdownSelector} + return html`<${RadioSelector} key=${key} attribute=${attribute} - onRequestDrawer=${onRequestDrawer} - productType=${productType} />`; case 'checkbox': - return html`<${DropdownSelector} + return html`<${CheckboxSelector} key=${key} attribute=${attribute} - onRequestDrawer=${onRequestDrawer} - productType=${productType} />`; default: return null; diff --git a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js index ad1575314..ac95e9f7f 100644 --- a/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js +++ b/express/code/blocks/print-product-detail-sdk/components/ProductComponents.js @@ -8,7 +8,7 @@ import { } from '../../../scripts/vendors/htm-preact.min.js'; import { useStore, useDrawer } from './Contexts.js'; import axAccordionDecorate from '../../ax-accordion/ax-accordion.js'; -import { formatLargeNumberToK } from '../utilities/utility-functions.js'; +import { formatLargeNumberToK, sanitizeHtml } from '../utilities/utility-functions.js'; import createSimpleCarousel from '../../../scripts/widgets/simple-carousel.js'; function mapToAccordionFormat(descriptions) { @@ -303,6 +303,10 @@ export function CheckoutButton({ templateId }) { .then(({ getCountry }) => getCountry()) .then((country) => { if (active) setOutOfRegion(!VALID_COUNTRIES.includes(country)); + }) + .catch((err) => { + window.lana?.log(`print-product-detail-sdk: country detection failed: ${err.message}`, { tags: 'print-product-detail-sdk', severity: 'warning' }); + if (active) setOutOfRegion(false); }); return () => { active = false; }; }, []); @@ -710,7 +714,7 @@ function PaperTypeContent({ onClose }) { ${state?.descriptionComponents?.[1]?.descriptionHTML && html`
    `}
    diff --git a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js index 33624714e..c952f2d24 100644 --- a/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js +++ b/express/code/blocks/print-product-detail-sdk/print-product-detail-sdk.js @@ -5,6 +5,7 @@ import { render, useEffect, useRef, + useState, Fragment, } from '../../scripts/vendors/htm-preact.min.js'; import { StoreProvider, useStore, DrawerProvider, useDrawer } from './components/Contexts.js'; @@ -39,6 +40,7 @@ function PDPContent({ templateId }) { const { fetchProduct } = actions; const containerRef = useRef(null); const hasTrackedPageView = useRef(false); + const [fetchError, setFetchError] = useState(false); useSeo(templateId); @@ -46,7 +48,10 @@ function PDPContent({ templateId }) { if (!templateId) { return; } - fetchProduct(templateId); + fetchProduct(templateId).catch((err) => { + window.lana?.log(`print-product-detail-sdk: fetchProduct failed: ${err.message}`, { tags: 'print-product-detail-sdk', severity: 'error' }); + setFetchError(true); + }); }, [templateId, fetchProduct]); useEffect(() => { @@ -86,6 +91,16 @@ function PDPContent({ templateId }) { openDrawer({ type: request.type, payload: request.payload }); }; + if (fetchError) { + return html` +
    +
    +

    Something went wrong loading this product. Please try refreshing the page.

    +
    +
    + `; + } + if (!state) { return html` <${Fragment}> diff --git a/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js index 21c1e4e6e..0687d6563 100644 --- a/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js +++ b/express/code/blocks/print-product-detail-sdk/utilities/utility-functions.js @@ -79,6 +79,37 @@ export async function formatPriceZazzle(price, differential = false) { return formattedPrice; } +const ALLOWED_TAGS = new Set([ + 'p', 'br', 'strong', 'em', 'b', 'i', 'u', 'ul', 'ol', 'li', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', +]); + +function cleanNode(node, doc) { + const fragment = doc.createDocumentFragment(); + node.childNodes.forEach((child) => { + if (child.nodeType === Node.TEXT_NODE) { + fragment.appendChild(doc.createTextNode(child.textContent)); + } else if (child.nodeType === Node.ELEMENT_NODE) { + if (ALLOWED_TAGS.has(child.tagName.toLowerCase())) { + const el = doc.createElement(child.tagName.toLowerCase()); + el.appendChild(cleanNode(child, doc)); + fragment.appendChild(el); + } else { + fragment.appendChild(cleanNode(child, doc)); + } + } + }); + return fragment; +} + +export function sanitizeHtml(html) { + if (!html) return ''; + const doc = new DOMParser().parseFromString(html, 'text/html'); + const cleaned = cleanNode(doc.body, document); + const wrapper = document.createElement('div'); + wrapper.appendChild(cleaned); + return wrapper.innerHTML; +} + export function formatStringSnakeCase(string) { const normalizedString = string.replace(/[^a-zA-Z0-9\s]/g, '_'); const formattedString = normalizedString.trim().toLowerCase().replace(/ /g, '_');