acinguiux-preact-components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +56 -0
- package/src/content/themes/theme-acinguiux-amg/theme-acinguiux-amg.css +23 -0
- package/src/content/themes/theme-acinguiux-cafe/theme-acinguiux-cafe.css +47 -0
- package/src/content/themes/theme-acinguiux-energy/theme-acinguiux-energy.css +45 -0
- package/src/content/themes/theme-acinguiux-livewire/theme-acinguiux-livewire.css +22 -0
- package/src/content/themes/theme-acinguiux-livewire-italy/theme-acinguiux-livewire-italy.css +22 -0
- package/src/content/themes/theme-acinguiux-recharge/theme-acinguiux-recharge.css +49 -0
- package/src/content/themes/theme-allon/theme-allon.css +25 -0
- package/src/content/themes/theme-atlas/theme-atlas.css +31 -0
- package/src/content/themes/theme-aurvana/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.svg +13 -0
- package/src/content/themes/theme-aurvana/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-aurvana/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-aurvana/theme-aurvana.css +49 -0
- package/src/content/themes/theme-base/theme-base.css +49 -0
- package/src/content/themes/theme-base2/resources/favicon/android-chrome-192x192.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/android-chrome-512x512.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-16x16.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-32x32.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.svg +9 -0
- package/src/content/themes/theme-base2/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-base2/resources/favicon/site.webmanifest +1 -0
- package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-base2/resources/fonts/acinguiux-typeface-la-heavy-221208.woff2 +0 -0
- package/src/content/themes/theme-base2/theme-base2.css +47 -0
- package/src/content/themes/theme-eco-marathon/theme-eco-marathon.css +22 -0
- package/src/content/themes/theme-energy-transition-campus-amsterdam/theme-energy-transition-campus-amsterdam.css +26 -0
- package/src/content/themes/theme-evpass/theme-evpass.css +46 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.svg +9 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-nam-2025/theme-nam-2025.css +47 -0
- package/src/content/themes/theme-pennzoil/theme-pennzoil.css +36 -0
- package/src/content/themes/theme-quaker-state/theme-quaker-state.css +63 -0
- package/src/content/themes/theme-tafawoq/theme-tafawoq.css +26 -0
- package/src/content/themes/theme-vegetable/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.svg +13 -0
- package/src/content/themes/theme-vegetable/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-vegetable/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-vegetable/theme-vegetable.css +49 -0
- package/src/content/themes/theme-zeolyst/resources/fonts/type-ar-medium.woff2 +0 -0
- package/src/content/themes/theme-zeolyst/theme-zeolyst.css +29 -0
- package/src/main/atoms/audio.js +16 -0
- package/src/main/atoms/box.js +5 -0
- package/src/main/atoms/button.js +40 -0
- package/src/main/atoms/card.js +22 -0
- package/src/main/atoms/form.js +30 -0
- package/src/main/atoms/heading.js +17 -0
- package/src/main/atoms/icon.js +24 -0
- package/src/main/atoms/img.js +131 -0
- package/src/main/atoms/input.js +55 -0
- package/src/main/atoms/link-text.js +21 -0
- package/src/main/atoms/link.js +60 -0
- package/src/main/atoms/list.js +12 -0
- package/src/main/atoms/logo.js +9 -0
- package/src/main/atoms/menu.js +10 -0
- package/src/main/atoms/message.js +5 -0
- package/src/main/atoms/nav-link.js +49 -0
- package/src/main/atoms/popup.js +47 -0
- package/src/main/atoms/rich-text.js +128 -0
- package/src/main/atoms/scroller.js +224 -0
- package/src/main/atoms/svg.js +65 -0
- package/src/main/atoms/table.js +32 -0
- package/src/main/atoms/textarea.js +10 -0
- package/src/main/atoms/time.js +12 -0
- package/src/main/atoms/video.js +100 -0
- package/src/main/export-main.js +12 -0
- package/src/main/export-matter.js +86 -0
- package/src/main/export-preact-hooks.js +1 -0
- package/src/main/export-preact.js +1 -0
- package/src/main/index.js +13 -0
- package/src/main/molecules/asset.js +23 -0
- package/src/main/molecules/glossary.js +44 -0
- package/src/main/molecules/links.js +23 -0
- package/src/main/molecules/promo-text.js +27 -0
- package/src/main/molecules/tags.js +15 -0
- package/src/main/molecules/tree.js +51 -0
- package/src/main/organisms/accordion-item.js +106 -0
- package/src/main/organisms/author.js +29 -0
- package/src/main/organisms/breadcrumb.js +69 -0
- package/src/main/organisms/call-to-action.js +24 -0
- package/src/main/organisms/carousel.js +178 -0
- package/src/main/organisms/cart-item.js +156 -0
- package/src/main/organisms/cart.js +162 -0
- package/src/main/organisms/contact-form.js +141 -0
- package/src/main/organisms/container/ab-test.js +47 -0
- package/src/main/organisms/container/default.js +6 -0
- package/src/main/organisms/container/filtered-section.js +293 -0
- package/src/main/organisms/container/footer.js +12 -0
- package/src/main/organisms/container/grid.js +44 -0
- package/src/main/organisms/container/header.js +13 -0
- package/src/main/organisms/container/list.js +7 -0
- package/src/main/organisms/container/main.js +6 -0
- package/src/main/organisms/container/raw.js +7 -0
- package/src/main/organisms/container/section.js +28 -0
- package/src/main/organisms/container.js +29 -0
- package/src/main/organisms/content-owner.js +15 -0
- package/src/main/organisms/date-entry.js +56 -0
- package/src/main/organisms/external-search.js +73 -0
- package/src/main/organisms/filtered-item.js +163 -0
- package/src/main/organisms/footer-item.js +17 -0
- package/src/main/organisms/image-gallery.js +164 -0
- package/src/main/organisms/last-modified.js +20 -0
- package/src/main/organisms/legal-footer.js +16 -0
- package/src/main/organisms/list-item.js +48 -0
- package/src/main/organisms/metadata.js +11 -0
- package/src/main/organisms/navigation.js +232 -0
- package/src/main/organisms/notification.js +87 -0
- package/src/main/organisms/order-tracker.js +203 -0
- package/src/main/organisms/page-header-banner.js +26 -0
- package/src/main/organisms/page-header.js +33 -0
- package/src/main/organisms/page-tags.js +14 -0
- package/src/main/organisms/page.js +260 -0
- package/src/main/organisms/press-release.js +24 -0
- package/src/main/organisms/product-admin.js +204 -0
- package/src/main/organisms/promo-banner.js +28 -0
- package/src/main/organisms/promo-bottom.js +23 -0
- package/src/main/organisms/promo-button.js +8 -0
- package/src/main/organisms/promo-card-cover.js +35 -0
- package/src/main/organisms/promo-card.js +33 -0
- package/src/main/organisms/promo-full.js +20 -0
- package/src/main/organisms/promo-image.js +22 -0
- package/src/main/organisms/promo-lure.js +22 -0
- package/src/main/organisms/promo-product-card.js +187 -0
- package/src/main/organisms/promo-product-full.js +293 -0
- package/src/main/organisms/promo-simple.js +23 -0
- package/src/main/organisms/quote.js +21 -0
- package/src/main/organisms/search-form.js +42 -0
- package/src/main/organisms/search-nav.js +66 -0
- package/src/main/organisms/search-result.js +53 -0
- package/src/main/organisms/slider.js +26 -0
- package/src/main/organisms/standalone-asset.js +22 -0
- package/src/main/organisms/tabs.js +277 -0
- package/src/main/organisms/topbar.js +83 -0
- package/src/main/organisms/web-component.js +53 -0
- package/src/main/routing/annotation.js +9 -0
- package/src/main/routing/component.js +138 -0
- package/src/main/routing/empty.js +5 -0
- package/src/main/routing/error-handler.js +64 -0
- package/src/main/routing/placeholder-image.svg +5 -0
- package/src/main/routing/router.js +219 -0
- package/src/main/shared/analytics.js +677 -0
- package/src/main/shared/bubble-event.js +11 -0
- package/src/main/shared/custom-element.js +21 -0
- package/src/main/shared/deep-selector.js +28 -0
- package/src/main/shared/disable-transparency.js +10 -0
- package/src/main/shared/format-time.js +8 -0
- package/src/main/shared/get-id.js +5 -0
- package/src/main/shared/get-meta.js +3 -0
- package/src/main/shared/get-size-class.js +3 -0
- package/src/main/shared/get-size.js +11 -0
- package/src/main/shared/h.js +88 -0
- package/src/main/shared/hash-jump.js +33 -0
- package/src/main/shared/icons/arrow-back.svg +1 -0
- package/src/main/shared/icons/arrow-down.svg +1 -0
- package/src/main/shared/icons/arrow-next.svg +1 -0
- package/src/main/shared/icons/arrow-tail-right.svg +1 -0
- package/src/main/shared/icons/arrow-tail-up.svg +1 -0
- package/src/main/shared/icons/arrow-up.svg +1 -0
- package/src/main/shared/icons/asset-download.svg +1 -0
- package/src/main/shared/icons/logo.svg +5 -0
- package/src/main/shared/icons/low-carbon-placeholder.svg +9 -0
- package/src/main/shared/icons/media-pause.svg +1 -0
- package/src/main/shared/icons/media-play.svg +1 -0
- package/src/main/shared/icons/navigation-burger.svg +1 -0
- package/src/main/shared/icons/navigation-close.svg +1 -0
- package/src/main/shared/icons/navigation-link.svg +1 -0
- package/src/main/shared/icons/navigation-refresh.svg +1 -0
- package/src/main/shared/icons/navigation-search.svg +1 -0
- package/src/main/shared/icons/navigation-share.svg +1 -0
- package/src/main/shared/icons/toggle-newwindow.svg +1 -0
- package/src/main/shared/icons.js +18 -0
- package/src/main/shared/id-from-string.js +5 -0
- package/src/main/shared/mark-selection.js +19 -0
- package/src/main/shared/register.js +26 -0
- package/src/main/shared/renderer.js +43 -0
- package/src/main/shared/simple-consent-api.js +70 -0
- package/src/main/shared/split-links.js +11 -0
- package/src/main/shared/t.js +60 -0
- package/src/main/shared/twind.js +837 -0
- package/src/main/shared/update-head.js +34 -0
- package/src/main/shared/update-scrollbar-width.js +30 -0
- package/src/main/shared/use-link.js +151 -0
- package/src/main/shared/use-persistent-state.js +42 -0
- package/src/main/shared/wait-for-dom-ready.js +6 -0
- package/src/main/shared/wcm-mode.js +4 -0
- package/src/wcs/components/acinguiux-preact-doc/acinguiux-preact-doc.js +207 -0
- package/src/wcs/components/admin-dashboard/admin-dashboard.js +487 -0
- package/src/wcs/components/admin-login/admin-login.js +91 -0
- package/src/wcs/components/bazaar-voice/bazaar-voice.js +56 -0
- package/src/wcs/components/chatbot-koreai/chatbot-koreai.js +176 -0
- package/src/wcs/components/chatbot-koreai/koreai-transport.js +217 -0
- package/src/wcs/components/chatbot-ms/chatbot-ms.js +210 -0
- package/src/wcs/components/chatbot-test/chatbot-test.js +44 -0
- package/src/wcs/components/comparison-chart/comparison-chart.js +111 -0
- package/src/wcs/components/consent-banner/consent-banner.js +248 -0
- package/src/wcs/components/consent-banner/icons/ccpa.svg +6 -0
- package/src/wcs/components/consent-banner/icons/info.svg +1 -0
- package/src/wcs/components/consent-banner/provider-onetrust.js +131 -0
- package/src/wcs/components/decision-tree/arrow-back.svg +3 -0
- package/src/wcs/components/decision-tree/badges.js +37 -0
- package/src/wcs/components/decision-tree/decision-tree.js +162 -0
- package/src/wcs/components/dynamic-contact-details/dynamic-contact-details.js +111 -0
- package/src/wcs/components/example-accordion/example-accordion.js +10 -0
- package/src/wcs/components/example-asset/example-asset.js +12 -0
- package/src/wcs/components/example-form/example-form.js +59 -0
- package/src/wcs/components/example-nested/example-nested.js +10 -0
- package/src/wcs/components/example-routing/example-routing.js +51 -0
- package/src/wcs/components/example-rtl/example-rtl.js +28 -0
- package/src/wcs/components/example-tabs/example-tabs.js +12 -0
- package/src/wcs/components/example-web-component/example-web-component.js +34 -0
- package/src/wcs/components/floating-button/floating-button.js +17 -0
- package/src/wcs/components/formstack-form/fields/address.js +38 -0
- package/src/wcs/components/formstack-form/fields/checkbox.js +42 -0
- package/src/wcs/components/formstack-form/fields/date.js +22 -0
- package/src/wcs/components/formstack-form/fields/description.js +8 -0
- package/src/wcs/components/formstack-form/fields/input.js +8 -0
- package/src/wcs/components/formstack-form/fields/name.js +39 -0
- package/src/wcs/components/formstack-form/fields/radio.js +24 -0
- package/src/wcs/components/formstack-form/fields/rating.js +53 -0
- package/src/wcs/components/formstack-form/fields/section.js +8 -0
- package/src/wcs/components/formstack-form/fields/select.js +10 -0
- package/src/wcs/components/formstack-form/fields/textarea.js +8 -0
- package/src/wcs/components/formstack-form/fields/wrapper.js +11 -0
- package/src/wcs/components/formstack-form/formstack-form.js +280 -0
- package/src/wcs/components/fuel-prices/fuel-prices.js +45 -0
- package/src/wcs/components/furniture-overview/furniture-overview.js +115 -0
- package/src/wcs/components/gauge-value/gauge-value.js +65 -0
- package/src/wcs/components/help-centre/api.js +150 -0
- package/src/wcs/components/help-centre/help-centre.js +400 -0
- package/src/wcs/components/help-centre/icon-search.svg +1 -0
- package/src/wcs/components/image-gen/admin-panel.js +248 -0
- package/src/wcs/components/image-gen/image-gen.js +385 -0
- package/src/wcs/components/image-gen/labels.js +37 -0
- package/src/wcs/components/image-gen/use-api.js +392 -0
- package/src/wcs/components/inspired-gallery/inspired-gallery.js +118 -0
- package/src/wcs/components/launch-container/launch-container.js +95 -0
- package/src/wcs/components/launch-container/ledger.js +140 -0
- package/src/wcs/components/lng-map/lng-map.js +44 -0
- package/src/wcs/components/mouseflow-analytics/mouseflow-analytics.js +39 -0
- package/src/wcs/components/msds-search/msds-search.js +127 -0
- package/src/wcs/components/msds-search/navigation-search.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-back.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-cart.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-close.svg +3 -0
- package/src/wcs/components/product-catalogue/product-catalogue.js +215 -0
- package/src/wcs/components/product-links/icon-cart.svg +3 -0
- package/src/wcs/components/product-links/product-links.js +43 -0
- package/src/wcs/components/rio-iframe/rio-iframe.js +137 -0
- package/src/wcs/components/salsify-products/filter-tools.js +60 -0
- package/src/wcs/components/salsify-products/icon-cart.svg +3 -0
- package/src/wcs/components/salsify-products/process-products.js +53 -0
- package/src/wcs/components/salsify-products/route-tools.js +54 -0
- package/src/wcs/components/salsify-products/salsify-products.js +281 -0
- package/src/wcs/components/shout-out/shout-out.js +51 -0
- package/src/wcs/components/simple-chart/simple-chart.js +53 -0
- package/src/wcs/components/single-stat/single-stat.js +85 -0
- package/src/wcs/components/site-feedback/site-feedback.js +56 -0
- package/src/wcs/components/skds-search/navigation-search.svg +3 -0
- package/src/wcs/components/skds-search/skds-search.js +103 -0
- package/src/wcs/components/smart-banner/smart-banner.js +104 -0
- package/src/wcs/components/standalone-table/arrow-up-down.svg +3 -0
- package/src/wcs/components/standalone-table/arrow-up.svg +3 -0
- package/src/wcs/components/standalone-table/standalone-table.js +440 -0
- package/src/wcs/components/station-locator/station-locator.js +49 -0
- package/src/wcs/components/store-badges/badges.js +60 -0
- package/src/wcs/components/store-badges/store-badges.js +93 -0
- package/src/wcs/components/topbar-button/person.svg +1 -0
- package/src/wcs/components/topbar-button/topbar-button.js +22 -0
- package/src/wcs/components/universal-gallery/universal-gallery.js +308 -0
- package/src/wcs/components/zendesk-chat/zendesk-chat.js +133 -0
- package/src/wcs/shared/chat-bot/README.md +61 -0
- package/src/wcs/shared/chat-bot/chat-bot.js +216 -0
- package/src/wcs/shared/chat-bot/resources/arrow-next.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/navigation-close.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/person.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/upload.svg +1 -0
- package/src/wcs/shared/filtered-data/README.md +52 -0
- package/src/wcs/shared/filtered-data/fetch-data.js +33 -0
- package/src/wcs/shared/filtered-data/filtered-data.js +337 -0
- package/src/wcs/shared/promo-with-popup/icon-close.svg +3 -0
- package/src/wcs/shared/promo-with-popup/icon-next.svg +3 -0
- package/src/wcs/shared/promo-with-popup/icon-prev.svg +3 -0
- package/src/wcs/shared/promo-with-popup/promo-with-popup.js +93 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { h, register, preactHooks } = globalThis.ami
|
|
2
|
+
const { useState, useEffect, useRef, useMemo } = preactHooks
|
|
3
|
+
|
|
4
|
+
const LOCALE = (document.documentElement.lang || navigator.language).replace('-', '_').toLowerCase()
|
|
5
|
+
const HASH_PARAM = 'iframe'
|
|
6
|
+
const POLLING_INTERVAL = 200
|
|
7
|
+
const DEFAULT_HEIGHT = 800
|
|
8
|
+
const DEFAULT_ALLOW = 'geolocation'
|
|
9
|
+
const ALLOWED_PROTOCOLS = new Set(['http:', 'https:'])
|
|
10
|
+
const PARAMS_TO_PASS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid']
|
|
11
|
+
|
|
12
|
+
function RIOIframe ({ src, height: defaultHeight = DEFAULT_HEIGHT, title }) {
|
|
13
|
+
const id = useMemo(() => `_${globalThis.crypto.randomUUID()}`, [])
|
|
14
|
+
const ref = useRef(null)
|
|
15
|
+
const [height, setHeight] = useState(defaultHeight)
|
|
16
|
+
const [url, setUrl] = useState({})
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!ref.current) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const iframe = ref.current
|
|
24
|
+
const isFirst = document.querySelector('rio-iframe') === iframe.parentNode.host
|
|
25
|
+
|
|
26
|
+
const currUrl = getUrl(src, isFirst)
|
|
27
|
+
setUrl(currUrl)
|
|
28
|
+
|
|
29
|
+
const postMessage = () => iframe.contentWindow.postMessage({ id, locale: LOCALE, index: 0 }, currUrl?.origin)
|
|
30
|
+
|
|
31
|
+
const onMessage = event => {
|
|
32
|
+
// Ignore messages from other iframes (by id).
|
|
33
|
+
if (event?.data?.id !== id) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Set the height.
|
|
38
|
+
if (event.data.height) {
|
|
39
|
+
setHeight(event.data.height)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update the URL (first iframe only).
|
|
43
|
+
if (isFirst && event.data.url) {
|
|
44
|
+
// Only Unicode characters will be encoded in the event.data.url.
|
|
45
|
+
// Make sure the entire URL is encoded in the hash without double-encoding any potential Unicode characters.
|
|
46
|
+
location.hash = `${HASH_PARAM}=${toBase64(decodeURI(event.data.url))}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Poll iframe when loaded.
|
|
51
|
+
const onLoad = () => {
|
|
52
|
+
postMessage()
|
|
53
|
+
const intervalRef = setInterval(() => iframe.contentWindow ? postMessage() : clearInterval(intervalRef),
|
|
54
|
+
POLLING_INTERVAL)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
window.addEventListener('message', onMessage)
|
|
58
|
+
iframe.addEventListener('load', onLoad)
|
|
59
|
+
|
|
60
|
+
// Unbind event listeners on unmount.
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener('message', onMessage)
|
|
63
|
+
iframe.removeEventListener('load', onLoad)
|
|
64
|
+
}
|
|
65
|
+
}, [src])
|
|
66
|
+
|
|
67
|
+
return h('iframe', {
|
|
68
|
+
id,
|
|
69
|
+
height: url?.href ? height : 0,
|
|
70
|
+
className: 'block w-full',
|
|
71
|
+
src: url?.href,
|
|
72
|
+
ref,
|
|
73
|
+
title,
|
|
74
|
+
allow: DEFAULT_ALLOW,
|
|
75
|
+
allowfullscreen: 'allowfullscreen',
|
|
76
|
+
loading: 'lazy'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getUrl (src, isFirst) {
|
|
81
|
+
let url = sanitizeUrl(src)
|
|
82
|
+
if (!url) {
|
|
83
|
+
return url
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isFirst) {
|
|
87
|
+
url = deepLink(url)
|
|
88
|
+
}
|
|
89
|
+
updateParams(url)
|
|
90
|
+
return url
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sanitizeUrl (url) {
|
|
94
|
+
if (!url) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const urlObj = new URL(url, globalThis.location.href)
|
|
99
|
+
|
|
100
|
+
if (!ALLOWED_PROTOCOLS.has(urlObj.protocol)) {
|
|
101
|
+
console.error(`Unsupported iframe protocol: ${url}`)
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return urlObj
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function deepLink (urlObj) {
|
|
109
|
+
const hashParams = new URLSearchParams(location.hash.substring(1))
|
|
110
|
+
if (hashParams.has(HASH_PARAM)) {
|
|
111
|
+
return new URL(fromBase64(hashParams.get(HASH_PARAM)), urlObj)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return urlObj
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function updateParams (urlObj) {
|
|
118
|
+
const pageParams = new URLSearchParams(location.search)
|
|
119
|
+
|
|
120
|
+
for (const param of PARAMS_TO_PASS) {
|
|
121
|
+
pageParams.has(param) && urlObj.searchParams.set(param, pageParams.get(param))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toBase64 (str) {
|
|
126
|
+
const textEncoder = new globalThis.TextEncoder()
|
|
127
|
+
// eslint-disable-next-line unicorn/prefer-code-point -- fromCharCode is required to match charCodeAt in fromBase64
|
|
128
|
+
return btoa(String.fromCharCode(...textEncoder.encode(str)))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function fromBase64 (str) {
|
|
132
|
+
const textDecoder = new globalThis.TextDecoder()
|
|
133
|
+
// eslint-disable-next-line unicorn/prefer-code-point -- fromCharCode is required to match fromCharCode in toBase64
|
|
134
|
+
return textDecoder.decode(Uint8Array.from([...atob(str)].map(c => c.charCodeAt(0))))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
register(RIOIframe, 'rio-iframe')
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getFilters (allProducts, filter1, filter2) {
|
|
2
|
+
return [filter1, filter2]
|
|
3
|
+
.filter(Boolean)
|
|
4
|
+
.map(f => {
|
|
5
|
+
const [title = '', field = ''] = f.split(',')
|
|
6
|
+
|
|
7
|
+
// Get a list of unique values for the field existing in all products.
|
|
8
|
+
const options = [...(allProducts.reduce((acc, p) => new Set([...acc, p[field]]), new Set()))]
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.map(v => ({ name: v, value: v }))
|
|
11
|
+
|
|
12
|
+
// Sort values.
|
|
13
|
+
options.sort((a, b) => {
|
|
14
|
+
if (isNumber(a.value) && isNumber(b.value)) {
|
|
15
|
+
return Number.parseFloat(a.value) - Number.parseFloat(b.value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return a.value.localeCompare(b.value)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return { name: title, field, options }
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function updateFiltersWithCounts (products, filters, appliedFilters) {
|
|
26
|
+
// For each filter, compute counts for its options in one pass over products.
|
|
27
|
+
// This reduces complexity by avoiding a per-option scan of the product list.
|
|
28
|
+
for (const filter of filters) {
|
|
29
|
+
const counts = computeCountsForFilter(products, filter, appliedFilters)
|
|
30
|
+
filter.options = filter.options.map(o => ({ name: `${o.value} (${counts.get(o.value) || 0})`, value: o.value }))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Compute option counts for a filter given the searchedProducts and applied filters.
|
|
35
|
+
function computeCountsForFilter (products, filter, appliedFilters) {
|
|
36
|
+
const counts = new Map()
|
|
37
|
+
const otherApplied = appliedFilters.filter(af => af.field !== filter.field)
|
|
38
|
+
|
|
39
|
+
for (const p of products) {
|
|
40
|
+
let ok = true
|
|
41
|
+
for (const af of otherApplied) {
|
|
42
|
+
if (af.value && p[af.field] !== af.value) {
|
|
43
|
+
ok = false
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!ok) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const val = p[filter.field]
|
|
52
|
+
counts.set(val, (counts.get(val) || 0) + 1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return counts
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isNumber (value) {
|
|
59
|
+
return !Number.isNaN(Number.parseFloat(value))
|
|
60
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.20731 4.42397C4.16978 4.18006 3.95991 4 3.71312 4H2.5C2.22386 4 2 3.77614 2 3.5V2.5C2 2.22386 2.22386 2 2.5 2H4.57104C5.3114 2 5.94102 2.54017 6.0536 3.27191L6.2591 4.60767H20.5105C20.8335 4.60767 21.0717 4.90937 20.9968 5.22359L19.2352 12.6149C19.1815 12.8401 18.9803 12.999 18.7488 12.999H7.55007L7.79269 14.576C7.83022 14.8199 8.04009 15 8.28688 15H18.5C18.7761 15 19 15.2239 19 15.5V16.5C19 16.7761 18.7761 17 18.5 17H7.42896C6.6886 17 6.05898 16.4598 5.9464 15.7281L4.20731 4.42397ZM6.58786 6.60767L7.26279 10.999H17.4458C17.5153 10.999 17.5756 10.9513 17.5917 10.8838L18.5669 6.79244C18.5893 6.69818 18.5179 6.60767 18.421 6.60767H6.58786ZM10 20C10 21.1046 9.10457 22 8 22C6.89543 22 6 21.1046 6 20C6 18.8954 6.89543 18 8 18C9.10457 18 10 18.8954 10 20ZM19 20C19 21.1046 18.1046 22 17 22C15.8954 22 15 21.1046 15 20C15 18.8954 15.8954 18 17 18C18.1046 18 19 18.8954 19 20Z" fill="#292929"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const RENDITION = 'c_limit,cs_srgb,h_500,w_500'
|
|
2
|
+
const DEFAULT_LOCALE = 'en-US'
|
|
3
|
+
const LOCALE = document.documentElement.lang ?? DEFAULT_LOCALE
|
|
4
|
+
|
|
5
|
+
export default function processProducts (products) {
|
|
6
|
+
const processedProducts = []
|
|
7
|
+
for (const product of products) {
|
|
8
|
+
let image = get(product, 'Main Image')
|
|
9
|
+
if (image) {
|
|
10
|
+
const index = image.lastIndexOf('/')
|
|
11
|
+
image = `${image.slice(0, index + 1)}${RENDITION}/${image.slice(index + 1)}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const links = []
|
|
15
|
+
const features = []
|
|
16
|
+
for (const key of Object.keys(product)) {
|
|
17
|
+
if (key.startsWith('Buy Now Label')) {
|
|
18
|
+
const name = get(product, key)
|
|
19
|
+
const value = get(product, key.replace('Label', 'Link'))
|
|
20
|
+
links.push({ name, value })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (key.startsWith('Key Feature')) {
|
|
24
|
+
const value = get(product, key)
|
|
25
|
+
value && features.push(value)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Randomize links order.
|
|
30
|
+
// eslint-disable-next-line sonarjs/pseudo-random
|
|
31
|
+
links.sort(() => Math.random() - 0.5) // NOSONAR
|
|
32
|
+
|
|
33
|
+
processedProducts.push({
|
|
34
|
+
id: get(product, 'Salsify Unique ID'),
|
|
35
|
+
name: get(product, 'Product Name'),
|
|
36
|
+
description: get(product, 'Product Description'),
|
|
37
|
+
category: get(product, 'Product Category'),
|
|
38
|
+
subcategory: get(product, 'Product Subcategory'),
|
|
39
|
+
viscosity: get(product, 'Viscosity'),
|
|
40
|
+
'product-line': get(product, 'Product Line'),
|
|
41
|
+
image,
|
|
42
|
+
features,
|
|
43
|
+
links
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
processedProducts.sort((a, b) => a.id.localeCompare(b.id))
|
|
48
|
+
return processedProducts
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function get (product, key) {
|
|
52
|
+
return product[key]?.[LOCALE] ?? product[key]?.[DEFAULT_LOCALE]
|
|
53
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function getSuffix () {
|
|
2
|
+
const url = getUrl()
|
|
3
|
+
return url.pathname.split('.html/')[1]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function updateSuffix (suffix) {
|
|
7
|
+
const url = getUrl()
|
|
8
|
+
const currentSuffix = getSuffix()
|
|
9
|
+
|
|
10
|
+
url.pathname = `${url.pathname.split('.html')[0]}.html`
|
|
11
|
+
|
|
12
|
+
if (suffix) {
|
|
13
|
+
url.pathname += `/${suffix}`
|
|
14
|
+
url.hash = ''
|
|
15
|
+
} else {
|
|
16
|
+
url.hash = `#_${currentSuffix}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return url.toString()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setParams (params) {
|
|
23
|
+
const url = new URL(location.href)
|
|
24
|
+
url.hash = ''
|
|
25
|
+
|
|
26
|
+
for (const [name, value] of Object.entries(params)) {
|
|
27
|
+
value
|
|
28
|
+
? url.searchParams.set(name, value)
|
|
29
|
+
: url.searchParams.delete(name)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
route(url)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getParam (name) {
|
|
36
|
+
const url = new URL(location.href)
|
|
37
|
+
return url.searchParams.get(name)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function route (url) {
|
|
41
|
+
history.pushState({}, '', url)
|
|
42
|
+
globalThis.dispatchEvent(new Event('popstate'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getUrl () {
|
|
46
|
+
const url = new URL(location.href)
|
|
47
|
+
|
|
48
|
+
// Hack for homepage.
|
|
49
|
+
if (url.pathname === '/') {
|
|
50
|
+
url.pathname = '/_jcr_content.html'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return url
|
|
54
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { route, getParam, setParams, getSuffix, updateSuffix } from './route-tools.js'
|
|
2
|
+
import processProducts from './process-products.js'
|
|
3
|
+
import { getFilters, updateFiltersWithCounts } from './filter-tools.js'
|
|
4
|
+
import ICON_CART from './icon-cart.svg'
|
|
5
|
+
|
|
6
|
+
const { h, register, matter, preactHooks, t } = globalThis.ami
|
|
7
|
+
const { useState, useLayoutEffect, useMemo, useRef } = preactHooks
|
|
8
|
+
const { Card, Box, Heading, Input, List, Button, Link } = matter
|
|
9
|
+
|
|
10
|
+
const LOAD_MORE_LIMIT = 12
|
|
11
|
+
const BASE_SEARCH_FIELDS = ['name', 'description']
|
|
12
|
+
const SEARCH_DEBOUNCE = 300
|
|
13
|
+
const SEARCH_PARAM = 'search' // Do not use `q` as it is one of the forwarded params in router.js.
|
|
14
|
+
|
|
15
|
+
function SalsifyProducts ({ src, filter1, filter2, 'disable-filters': disableFilters, columns = 4 }) {
|
|
16
|
+
const areFiltersDisabled = disableFilters === 'true'
|
|
17
|
+
const [state, setState] = useState({})
|
|
18
|
+
|
|
19
|
+
// Update state based on the current URL.
|
|
20
|
+
const updateState = (allProducts, allFilters) => {
|
|
21
|
+
// Resolve selected product based on the URL suffix.
|
|
22
|
+
const productId = getSuffix()
|
|
23
|
+
const selectedProduct = productId ? allProducts.find(p => p.id === productId) : null
|
|
24
|
+
|
|
25
|
+
// Apply search.
|
|
26
|
+
let searchedProducts = allProducts
|
|
27
|
+
const searchTerm = getParam(SEARCH_PARAM)
|
|
28
|
+
if (searchTerm) {
|
|
29
|
+
const searchableFields = [...BASE_SEARCH_FIELDS, ...allFilters.map(f => f.field)]
|
|
30
|
+
searchedProducts = allProducts.filter(product => searchableFields.some(
|
|
31
|
+
field => product[field]?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
32
|
+
))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Mark selected filters.
|
|
36
|
+
const appliedFilters = []
|
|
37
|
+
for (const filter of allFilters) {
|
|
38
|
+
filter.selected = getParam(filter.field) || null
|
|
39
|
+
if (filter.selected) {
|
|
40
|
+
appliedFilters.push({ field: filter.field, value: filter.selected })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
updateFiltersWithCounts(searchedProducts, allFilters, appliedFilters)
|
|
45
|
+
|
|
46
|
+
// Filter products.
|
|
47
|
+
const filteredProducts = searchedProducts.filter(product =>
|
|
48
|
+
appliedFilters.every(filter => !filter.value || product[filter.field] === filter.value)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Limit products.
|
|
52
|
+
const page = Number.parseInt(getParam('page'), 10) || 1
|
|
53
|
+
const limit = page * LOAD_MORE_LIMIT
|
|
54
|
+
const limitedProducts = filteredProducts.slice(0, limit)
|
|
55
|
+
|
|
56
|
+
setState({
|
|
57
|
+
products: limitedProducts,
|
|
58
|
+
selectedProduct,
|
|
59
|
+
filters: allFilters,
|
|
60
|
+
searchTerm,
|
|
61
|
+
hasMore: filteredProducts.length > limit
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
let cleanup
|
|
67
|
+
(async () => {
|
|
68
|
+
const res = await fetch(src)
|
|
69
|
+
const data = await res.json()
|
|
70
|
+
const allProducts = processProducts(data)
|
|
71
|
+
const allFilters = getFilters(allProducts, filter1, filter2)
|
|
72
|
+
|
|
73
|
+
updateState(allProducts, allFilters)
|
|
74
|
+
const handler = () => updateState(allProducts, allFilters)
|
|
75
|
+
globalThis.addEventListener('popstate', handler)
|
|
76
|
+
cleanup = () => globalThis.removeEventListener('popstate', handler)
|
|
77
|
+
})().catch(console.error)
|
|
78
|
+
return () => cleanup?.()
|
|
79
|
+
}, [src, filter1, filter2])
|
|
80
|
+
|
|
81
|
+
if (!state.products) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config = {
|
|
86
|
+
areFiltersDisabled,
|
|
87
|
+
filters: state.filters,
|
|
88
|
+
hasMore: state.hasMore,
|
|
89
|
+
searchTerm: state.searchTerm,
|
|
90
|
+
columns: Number.parseInt(columns, 10)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return state.selectedProduct
|
|
94
|
+
? h(ProductDetails, { product: state.selectedProduct, config })
|
|
95
|
+
: h(ProductList, { products: state.products, config })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ProductList ({ products, config }) {
|
|
99
|
+
const listId = useMemo(() => `_${crypto.randomUUID()}`, [])
|
|
100
|
+
const searchTimeoutRef = useRef(null)
|
|
101
|
+
const hasFilters = config.filters.length > 0 && !config.areFiltersDisabled
|
|
102
|
+
|
|
103
|
+
return h('div', { className: 'grid grid-cols-1 gap-5 animate-fade' },
|
|
104
|
+
hasFilters && h('div',
|
|
105
|
+
h(Box, { className: 'p-6 col-span-3 bg-bga text-txa rounded-2xl' },
|
|
106
|
+
h('div', { className: 'flex gap-5' },
|
|
107
|
+
h('label', { className: 'flex-1' },
|
|
108
|
+
h('span', { className: 'font-bold' }, t('Search')),
|
|
109
|
+
h(Input, {
|
|
110
|
+
'aria-controls': listId,
|
|
111
|
+
type: 'search',
|
|
112
|
+
name: t('Search'),
|
|
113
|
+
value: config.searchTerm || '',
|
|
114
|
+
onInput: event => {
|
|
115
|
+
// Capture primitive values synchronously. Do NOT rely on the
|
|
116
|
+
// event object inside async callbacks — it may be retargeted
|
|
117
|
+
// (shadow DOM) or otherwise unavailable later.
|
|
118
|
+
const query = event.target?.value ?? ''
|
|
119
|
+
|
|
120
|
+
// Avoid flooding updates when user is typing.
|
|
121
|
+
clearTimeout(searchTimeoutRef.current)
|
|
122
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
123
|
+
// Use captured `query` instead of `event.target` here.
|
|
124
|
+
const params = query ? { [SEARCH_PARAM]: query, page: null } : { [SEARCH_PARAM]: null, page: null }
|
|
125
|
+
setParams(params)
|
|
126
|
+
}, SEARCH_DEBOUNCE)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
),
|
|
130
|
+
config.filters.map(filter => h(Filter, { ariaControls: listId, filter }))
|
|
131
|
+
),
|
|
132
|
+
h('p',
|
|
133
|
+
h(Link, {
|
|
134
|
+
'aria-controls': listId,
|
|
135
|
+
_variant: 'underline',
|
|
136
|
+
_model: { name: t('Reset filters'), value: '#' },
|
|
137
|
+
onClick: e => {
|
|
138
|
+
e.preventDefault()
|
|
139
|
+
history.pushState({}, '', location.pathname)
|
|
140
|
+
globalThis.dispatchEvent(new Event('popstate'))
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
),
|
|
146
|
+
h('div', { id: listId, 'aria-live': 'polite' },
|
|
147
|
+
products.length === 0 && h('div', { className: 'text-center' }, t('No results found.')),
|
|
148
|
+
products.length > 0 && h('div', {
|
|
149
|
+
className: `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${config.columns} gap-4`,
|
|
150
|
+
children: products.map(product =>
|
|
151
|
+
h('div', { className: 'animate-fade' },
|
|
152
|
+
h(Card, {
|
|
153
|
+
id: `_${product.id}`,
|
|
154
|
+
_model: { value: updateSuffix(product.id) },
|
|
155
|
+
children: [
|
|
156
|
+
h(Box,
|
|
157
|
+
h('div', { className: 'rounded-lg overflow-hidden p-6 border-1 border-txa/10', style: 'background: white' },
|
|
158
|
+
h(ProductImage, { product })
|
|
159
|
+
),
|
|
160
|
+
h(Heading, { _level: 3 }, product.name),
|
|
161
|
+
h(Labels, { product, config })
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
),
|
|
169
|
+
config.hasMore && h('div', { className: 'flex justify-center' },
|
|
170
|
+
h(Button, {
|
|
171
|
+
'aria-controls': listId,
|
|
172
|
+
className: 'w-auto',
|
|
173
|
+
onClick: () => {
|
|
174
|
+
const page = Number.parseInt(getParam('page'), 10) || 1
|
|
175
|
+
setParams({ page: page + 1 })
|
|
176
|
+
},
|
|
177
|
+
_model: {
|
|
178
|
+
name: t('Load more'),
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ProductDetails ({ product, config }) {
|
|
186
|
+
useLayoutEffect(() => {
|
|
187
|
+
window.scrollTo(0, 0)
|
|
188
|
+
}, [])
|
|
189
|
+
|
|
190
|
+
return h('div', {
|
|
191
|
+
className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 animate-fade',
|
|
192
|
+
itemtype: 'https://schema.org/Product',
|
|
193
|
+
itemscope: '',
|
|
194
|
+
children: [
|
|
195
|
+
h(Box, { className: 'lg:col-span-3 md:col-span-2 p-6 rounded-2xl bg-bga text-txa' },
|
|
196
|
+
h(Link, {
|
|
197
|
+
onClick: event => {
|
|
198
|
+
event.preventDefault()
|
|
199
|
+
route(event.currentTarget.href)
|
|
200
|
+
dispatchEvent(new Event('hashchange')) // Force hash jump.
|
|
201
|
+
},
|
|
202
|
+
_variant: 'simple',
|
|
203
|
+
_model: { name: t('View product list'), value: updateSuffix(null) },
|
|
204
|
+
}),
|
|
205
|
+
h(Heading, { _level: 2, itemprop: 'name' }, product.name)
|
|
206
|
+
),
|
|
207
|
+
h('div', { className: 'flex flex-col gap-5' },
|
|
208
|
+
h('div', { className: 'rounded-2xl overflow-hidden' },
|
|
209
|
+
h(ProductImage, { product })
|
|
210
|
+
),
|
|
211
|
+
h(StoreLinks, { product })
|
|
212
|
+
),
|
|
213
|
+
h(Box, { className: 'lg:col-span-2 bg-bga text-txa rounded-2xl', itemprop: 'description' },
|
|
214
|
+
product.description && [
|
|
215
|
+
h(Heading, { _level: 3 }, t('Description')),
|
|
216
|
+
h('p', product.description),
|
|
217
|
+
],
|
|
218
|
+
h(Labels, { product, config }),
|
|
219
|
+
product.features.length > 0 && [
|
|
220
|
+
h(Heading, { _level: 3 }, t('Features')),
|
|
221
|
+
h(List, product.features.map(f =>
|
|
222
|
+
h('li', f)
|
|
223
|
+
))
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function ProductImage ({ product }) {
|
|
231
|
+
return h('div', { className: 'aspect-square' },
|
|
232
|
+
h('img', {
|
|
233
|
+
itemprop: 'image',
|
|
234
|
+
className: 'block object-contain h-full',
|
|
235
|
+
alt: product.name,
|
|
236
|
+
src: product.image
|
|
237
|
+
})
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function Filter ({ ariaControls, filter }) {
|
|
242
|
+
return h('label', { className: 'flex-1' },
|
|
243
|
+
h('span', { className: 'font-bold' }, filter.name),
|
|
244
|
+
h(Input, {
|
|
245
|
+
defaultValue: filter.selected || '',
|
|
246
|
+
selectedValue: filter.selected,
|
|
247
|
+
'aria-controls': ariaControls,
|
|
248
|
+
type: 'select',
|
|
249
|
+
onChange: event => {
|
|
250
|
+
setParams({ [filter.field]: event.target.value, page: null })
|
|
251
|
+
},
|
|
252
|
+
children: [
|
|
253
|
+
h('option', { value: '' }, ''),
|
|
254
|
+
filter.options.map(o => h('option', { value: o.value, selected: o.value === filter.selected }, o.name))
|
|
255
|
+
]
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function Labels ({ product, config }) {
|
|
261
|
+
return [
|
|
262
|
+
h('hr', { className: 'mb-2 w-6 h-1 bg-bgb' }),
|
|
263
|
+
h('div', { className: 'space-y-1' },
|
|
264
|
+
config.filters.map(filter => h('dl', { className: 'text-sm flex gap-2' },
|
|
265
|
+
h('dt', { className: 'font-bold basis-24 grow-0 shrink-0' }, filter.name),
|
|
266
|
+
h('dd', product[filter.field])
|
|
267
|
+
))
|
|
268
|
+
)
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function StoreLinks ({ product }) {
|
|
273
|
+
return product.links.length > 0 && h(Box, { className: 'p-6 text-txa bg-bga rounded-xl' },
|
|
274
|
+
h(Heading, { _level: 3 }, t('Buy now')),
|
|
275
|
+
product.links.map(link =>
|
|
276
|
+
h(Button, { _wide: true, _model: { ...link, icon: ICON_CART } })
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
register(SalsifyProducts, 'salsify-products')
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { register, h, matter, preactHooks } = globalThis.ami
|
|
2
|
+
const { Box, SVG, Heading, RichText } = matter
|
|
3
|
+
const { useEffect, useState } = preactHooks
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ICON_SIZE_MAPPING = { s: 6, m: 8, l: 12, xl: 16 }
|
|
6
|
+
const DEFAULT_ICON_SIZE = DEFAULT_ICON_SIZE_MAPPING.s
|
|
7
|
+
|
|
8
|
+
function ShoutOut ({ src }) {
|
|
9
|
+
const [data, setData] = useState()
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
(async () => {
|
|
12
|
+
const res = await fetch(src)
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`Error fetching ${src}. Status: ${res.status}`)
|
|
15
|
+
}
|
|
16
|
+
const json = await res.json()
|
|
17
|
+
setData(json)
|
|
18
|
+
})().catch(console.error)
|
|
19
|
+
}, [src])
|
|
20
|
+
|
|
21
|
+
if (!data) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const iconSize = DEFAULT_ICON_SIZE_MAPPING[data.customModel.elements.iconSize.value] || DEFAULT_ICON_SIZE
|
|
26
|
+
const title = data.customModel.elements.title.value
|
|
27
|
+
|
|
28
|
+
return h(Box, { className: 'bg-bga text-txa h-full rounded-2xl' },
|
|
29
|
+
h('div', { className: 'flex gap-3' },
|
|
30
|
+
h('div', { className: 'shrink' },
|
|
31
|
+
h('div', { className: `block w-${iconSize} h-${iconSize}` },
|
|
32
|
+
h(SVG, {
|
|
33
|
+
_style: id => `
|
|
34
|
+
${id} path, ${id} polygon {
|
|
35
|
+
fill: currentColor;
|
|
36
|
+
}`,
|
|
37
|
+
_model: { src: data.customModel.elements.icon.value }
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
),
|
|
41
|
+
title && h('div', { className: 'shrink' },
|
|
42
|
+
h(Heading, { _level: 3 }, title)
|
|
43
|
+
),
|
|
44
|
+
h('div', { className: 'flex items-center' },
|
|
45
|
+
h(RichText, data.customModel.elements.description.value)
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
register(ShoutOut, 'shout-out')
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Chart from 'chart.js/auto'
|
|
2
|
+
|
|
3
|
+
const { h, register, preactHooks, matter } = globalThis.ami
|
|
4
|
+
const { Box, Heading } = matter
|
|
5
|
+
const { useRef, useEffect, useState, useLayoutEffect } = preactHooks
|
|
6
|
+
|
|
7
|
+
function SimpleChart ({ src, title, subtitle }) {
|
|
8
|
+
const [data, setData] = useState()
|
|
9
|
+
const chart = useRef(null)
|
|
10
|
+
const canvasRef = useRef(null)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
(async () => {
|
|
14
|
+
const res = await fetch(src)
|
|
15
|
+
setData(await res.json())
|
|
16
|
+
})().catch(console.error)
|
|
17
|
+
}, [src])
|
|
18
|
+
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
if (!data || !canvasRef.current) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
chart.current?.destroy()
|
|
25
|
+
chart.current = new Chart(canvasRef.current, {
|
|
26
|
+
...data,
|
|
27
|
+
options: {
|
|
28
|
+
...data.options,
|
|
29
|
+
animation: {
|
|
30
|
+
...data.options?.animation,
|
|
31
|
+
onComplete: () => canvasRef.current?.setAttribute('data-complete', '')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}, [data])
|
|
36
|
+
|
|
37
|
+
if (!data) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return h(Box, { className: 'bg-bga text-txa rounded-2xl' },
|
|
42
|
+
h(Heading, { _level: 3 }, title),
|
|
43
|
+
h('canvas', {
|
|
44
|
+
className: 'w-full',
|
|
45
|
+
ref: canvasRef,
|
|
46
|
+
'aria-label': subtitle ? `${title}: ${subtitle}` : title,
|
|
47
|
+
role: 'img'
|
|
48
|
+
}),
|
|
49
|
+
subtitle ? h('p', subtitle) : null
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
register(SimpleChart, 'simple-chart')
|