@salesforce/retail-react-app 1.0.0-preview.0
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/.eslintignore +7 -0
- package/.eslintrc.js +25 -0
- package/.prettierignore +4 -0
- package/.prettierrc.yaml +7 -0
- package/CHANGELOG.md +173 -0
- package/LICENSE +14 -0
- package/README.md +48 -0
- package/app/assets/svg/account.svg +3 -0
- package/app/assets/svg/alert.svg +3 -0
- package/app/assets/svg/basket.svg +3 -0
- package/app/assets/svg/brand-logo.svg +10 -0
- package/app/assets/svg/cc-amex.svg +7 -0
- package/app/assets/svg/cc-cvv.svg +8 -0
- package/app/assets/svg/cc-discover.svg +14 -0
- package/app/assets/svg/cc-mastercard.svg +8 -0
- package/app/assets/svg/cc-visa.svg +11 -0
- package/app/assets/svg/check-circle.svg +3 -0
- package/app/assets/svg/check.svg +3 -0
- package/app/assets/svg/chevron-down.svg +3 -0
- package/app/assets/svg/chevron-left.svg +3 -0
- package/app/assets/svg/chevron-right.svg +3 -0
- package/app/assets/svg/chevron-up.svg +3 -0
- package/app/assets/svg/close.svg +3 -0
- package/app/assets/svg/dashboard.svg +4 -0
- package/app/assets/svg/figma-logo.svg +14 -0
- package/app/assets/svg/file.svg +3 -0
- package/app/assets/svg/filter.svg +3 -0
- package/app/assets/svg/flag-ca.svg +5 -0
- package/app/assets/svg/flag-cn.svg +19 -0
- package/app/assets/svg/flag-fr.svg +19 -0
- package/app/assets/svg/flag-gb.svg +16 -0
- package/app/assets/svg/flag-it.svg +29 -0
- package/app/assets/svg/flag-jp.svg +10 -0
- package/app/assets/svg/flag-us.svg +7 -0
- package/app/assets/svg/github-logo.svg +40 -0
- package/app/assets/svg/hamburger.svg +8 -0
- package/app/assets/svg/heart-solid.svg +7 -0
- package/app/assets/svg/heart.svg +3 -0
- package/app/assets/svg/info.svg +3 -0
- package/app/assets/svg/like.svg +4 -0
- package/app/assets/svg/location.svg +3 -0
- package/app/assets/svg/lock.svg +3 -0
- package/app/assets/svg/paypal.svg +19 -0
- package/app/assets/svg/plug.svg +3 -0
- package/app/assets/svg/plus.svg +3 -0
- package/app/assets/svg/receipt.svg +3 -0
- package/app/assets/svg/search.svg +8 -0
- package/app/assets/svg/signout.svg +3 -0
- package/app/assets/svg/social-facebook.svg +3 -0
- package/app/assets/svg/social-instagram.svg +3 -0
- package/app/assets/svg/social-pinterest.svg +4 -0
- package/app/assets/svg/social-twitter.svg +3 -0
- package/app/assets/svg/social-youtube.svg +3 -0
- package/app/assets/svg/user.svg +3 -0
- package/app/assets/svg/visibility-off.svg +5 -0
- package/app/assets/svg/visibility.svg +3 -0
- package/app/components/_app/index.jsx +401 -0
- package/app/components/_app/index.test.js +85 -0
- package/app/components/_app/partials/above-header.jsx +10 -0
- package/app/components/_app-config/index.jsx +125 -0
- package/app/components/_app-config/index.test.js +77 -0
- package/app/components/_error/index.jsx +142 -0
- package/app/components/_error/index.test.js +25 -0
- package/app/components/action-card/index.jsx +75 -0
- package/app/components/address-display/index.jsx +30 -0
- package/app/components/basic-tile/index.jsx +65 -0
- package/app/components/basic-tile/index.test.js +23 -0
- package/app/components/breadcrumb/index.jsx +67 -0
- package/app/components/breadcrumb/index.test.js +30 -0
- package/app/components/confirmation-modal/index.jsx +111 -0
- package/app/components/confirmation-modal/index.test.js +98 -0
- package/app/components/drawer-menu/index.jsx +405 -0
- package/app/components/drawer-menu/index.test.js +33 -0
- package/app/components/dynamic-image/index.jsx +56 -0
- package/app/components/field/index.jsx +161 -0
- package/app/components/footer/index.jsx +269 -0
- package/app/components/footer/index.test.js +22 -0
- package/app/components/forms/address-fields.jsx +49 -0
- package/app/components/forms/credit-card-fields.jsx +149 -0
- package/app/components/forms/form-action-buttons.jsx +55 -0
- package/app/components/forms/login-fields.jsx +31 -0
- package/app/components/forms/password-requirements.jsx +99 -0
- package/app/components/forms/post-checkout-registration-fields.jsx +43 -0
- package/app/components/forms/profile-fields.jsx +36 -0
- package/app/components/forms/promo-code-fields.jsx +43 -0
- package/app/components/forms/registration-fields.jsx +42 -0
- package/app/components/forms/reset-password-fields.jsx +31 -0
- package/app/components/forms/state-province-options.jsx +75 -0
- package/app/components/forms/update-password-fields.jsx +49 -0
- package/app/components/forms/useAddressFields.jsx +196 -0
- package/app/components/forms/useCreditCardFields.jsx +146 -0
- package/app/components/forms/useLoginFields.jsx +52 -0
- package/app/components/forms/useProfileFields.jsx +95 -0
- package/app/components/forms/usePromoCodeFields.jsx +39 -0
- package/app/components/forms/useRegistrationFields.jsx +136 -0
- package/app/components/forms/useResetPasswordFields.jsx +40 -0
- package/app/components/forms/useUpdatePasswordFields.jsx +89 -0
- package/app/components/header/index.jsx +290 -0
- package/app/components/header/index.test.js +217 -0
- package/app/components/hero/index.jsx +84 -0
- package/app/components/hero/index.test.js +40 -0
- package/app/components/icons/index.jsx +158 -0
- package/app/components/icons/index.test.js +20 -0
- package/app/components/image-gallery/index.jsx +176 -0
- package/app/components/image-gallery/index.test.js +485 -0
- package/app/components/item-variant/index.jsx +33 -0
- package/app/components/item-variant/item-attributes.jsx +107 -0
- package/app/components/item-variant/item-image.jsx +73 -0
- package/app/components/item-variant/item-name.jsx +28 -0
- package/app/components/item-variant/item-price.jsx +117 -0
- package/app/components/link/index.jsx +32 -0
- package/app/components/link/index.test.js +72 -0
- package/app/components/links-list/index.jsx +89 -0
- package/app/components/links-list/index.test.js +62 -0
- package/app/components/list-menu/index.jsx +280 -0
- package/app/components/list-menu/index.test.js +44 -0
- package/app/components/loading-spinner/index.jsx +46 -0
- package/app/components/locale-selector/index.jsx +124 -0
- package/app/components/locale-selector/index.test.js +37 -0
- package/app/components/locale-text/index.jsx +97 -0
- package/app/components/locale-text/index.test.js +36 -0
- package/app/components/login/index.jsx +96 -0
- package/app/components/nested-accordion/index.jsx +185 -0
- package/app/components/nested-accordion/index.test.js +98 -0
- package/app/components/offline-banner/index.jsx +40 -0
- package/app/components/offline-banner/index.test.js +15 -0
- package/app/components/offline-boundary/index.jsx +104 -0
- package/app/components/offline-boundary/index.test.js +123 -0
- package/app/components/order-summary/index.jsx +331 -0
- package/app/components/page-action-placeholder/index.jsx +50 -0
- package/app/components/pagination/index.jsx +134 -0
- package/app/components/pagination/index.test.js +25 -0
- package/app/components/product-item/index.jsx +146 -0
- package/app/components/product-item/index.test.js +38 -0
- package/app/components/product-scroller/index.jsx +172 -0
- package/app/components/product-scroller/index.test.js +98 -0
- package/app/components/product-tile/index.jsx +195 -0
- package/app/components/product-tile/index.test.js +96 -0
- package/app/components/product-view/index.jsx +538 -0
- package/app/components/product-view/index.test.js +224 -0
- package/app/components/product-view-modal/index.jsx +48 -0
- package/app/components/product-view-modal/index.test.js +72 -0
- package/app/components/promo-code/index.jsx +162 -0
- package/app/components/promo-popover/index.jsx +83 -0
- package/app/components/quantity-picker/index.jsx +58 -0
- package/app/components/radio-card/index.jsx +75 -0
- package/app/components/recommended-products/index.jsx +227 -0
- package/app/components/register/index.jsx +114 -0
- package/app/components/reset-password/index.jsx +87 -0
- package/app/components/responsive/index.jsx +29 -0
- package/app/components/scroll-to-top/index.jsx +24 -0
- package/app/components/scroll-to-top/index.test.js +46 -0
- package/app/components/search/index.jsx +279 -0
- package/app/components/search/index.test.js +127 -0
- package/app/components/search/partials/recent-searches.jsx +76 -0
- package/app/components/search/partials/search-suggestions.jsx +45 -0
- package/app/components/search/partials/suggestions.jsx +43 -0
- package/app/components/section/index.jsx +68 -0
- package/app/components/seo/index.jsx +33 -0
- package/app/components/social-icons/index.jsx +101 -0
- package/app/components/social-icons/index.test.js +30 -0
- package/app/components/swatch-group/index.jsx +77 -0
- package/app/components/swatch-group/index.test.js +136 -0
- package/app/components/swatch-group/swatch.jsx +94 -0
- package/app/components/toggle-card/index.jsx +97 -0
- package/app/components/with-registration/index.jsx +58 -0
- package/app/components/with-registration/index.test.js +85 -0
- package/app/constants.js +109 -0
- package/app/contexts/index.js +92 -0
- package/app/hooks/einstein-mock-data.js +916 -0
- package/app/hooks/index.js +17 -0
- package/app/hooks/use-add-to-cart-modal.js +344 -0
- package/app/hooks/use-add-to-cart-modal.test.js +625 -0
- package/app/hooks/use-auth-modal.js +337 -0
- package/app/hooks/use-auth-modal.test.js +365 -0
- package/app/hooks/use-currency.js +20 -0
- package/app/hooks/use-currency.test.js +41 -0
- package/app/hooks/use-current-basket.js +39 -0
- package/app/hooks/use-current-customer.js +29 -0
- package/app/hooks/use-derived-product.js +77 -0
- package/app/hooks/use-derived-product.test.js +69 -0
- package/app/hooks/use-einstein.js +512 -0
- package/app/hooks/use-einstein.test.js +224 -0
- package/app/hooks/use-intersection-observer.js +64 -0
- package/app/hooks/use-limit-urls.js +31 -0
- package/app/hooks/use-limit-urls.test.js +40 -0
- package/app/hooks/use-multi-site.js +36 -0
- package/app/hooks/use-multi-site.test.js +53 -0
- package/app/hooks/use-navigation.js +37 -0
- package/app/hooks/use-navigation.test.js +109 -0
- package/app/hooks/use-page-urls.js +35 -0
- package/app/hooks/use-page-urls.test.js +39 -0
- package/app/hooks/use-pdp-search-params.js +16 -0
- package/app/hooks/use-pdp-search-params.test.js +52 -0
- package/app/hooks/use-previous.js +17 -0
- package/app/hooks/use-product-view-modal.js +93 -0
- package/app/hooks/use-product-view-modal.test.js +172 -0
- package/app/hooks/use-search-params.js +96 -0
- package/app/hooks/use-search-params.test.js +91 -0
- package/app/hooks/use-sort-urls.js +33 -0
- package/app/hooks/use-sort-urls.test.js +42 -0
- package/app/hooks/use-toast.js +68 -0
- package/app/hooks/use-toast.test.js +58 -0
- package/app/hooks/use-variant.js +32 -0
- package/app/hooks/use-variant.test.js +81 -0
- package/app/hooks/use-variation-attributes.js +138 -0
- package/app/hooks/use-variation-attributes.test.js +119 -0
- package/app/hooks/use-variation-params.js +31 -0
- package/app/hooks/use-variation-params.test.js +73 -0
- package/app/hooks/use-wish-list.js +42 -0
- package/app/main.jsx +14 -0
- package/app/mocks/basket-with-suit.js +146 -0
- package/app/mocks/empty-basket.js +39 -0
- package/app/mocks/mock-data.js +5632 -0
- package/app/mocks/product-set-winter-lookM.js +1224 -0
- package/app/mocks/searchResults.js +144 -0
- package/app/mocks/variant-750518699578M.js +434 -0
- package/app/page-designer/README.md +102 -0
- package/app/page-designer/assets/image-tile/index.jsx +51 -0
- package/app/page-designer/assets/image-tile/index.test.js +30 -0
- package/app/page-designer/assets/image-with-text/index.jsx +140 -0
- package/app/page-designer/assets/image-with-text/index.test.js +38 -0
- package/app/page-designer/assets/index.js +9 -0
- package/app/page-designer/index.js +10 -0
- package/app/page-designer/layouts/carousel/index.jsx +222 -0
- package/app/page-designer/layouts/carousel/index.test.js +43 -0
- package/app/page-designer/layouts/index.js +14 -0
- package/app/page-designer/layouts/mobileGrid1r1c/index.jsx +36 -0
- package/app/page-designer/layouts/mobileGrid1r1c/index.test.js +35 -0
- package/app/page-designer/layouts/mobileGrid2r1c/index.jsx +37 -0
- package/app/page-designer/layouts/mobileGrid2r1c/index.test.js +47 -0
- package/app/page-designer/layouts/mobileGrid2r2c/index.jsx +37 -0
- package/app/page-designer/layouts/mobileGrid2r2c/index.test.js +71 -0
- package/app/page-designer/layouts/mobileGrid2r3c/index.jsx +37 -0
- package/app/page-designer/layouts/mobileGrid2r3c/index.test.js +95 -0
- package/app/page-designer/layouts/mobileGrid3r1c/index.jsx +37 -0
- package/app/page-designer/layouts/mobileGrid3r1c/index.test.js +59 -0
- package/app/page-designer/layouts/mobileGrid3r2c/index.jsx +37 -0
- package/app/page-designer/layouts/mobileGrid3r2c/index.test.js +95 -0
- package/app/page-designer/utils.js +14 -0
- package/app/pages/account/addresses.jsx +382 -0
- package/app/pages/account/addresses.test.js +120 -0
- package/app/pages/account/constant.js +57 -0
- package/app/pages/account/index.jsx +237 -0
- package/app/pages/account/index.test.js +188 -0
- package/app/pages/account/order-detail.jsx +397 -0
- package/app/pages/account/order-history.jsx +264 -0
- package/app/pages/account/orders.jsx +30 -0
- package/app/pages/account/orders.test.js +95 -0
- package/app/pages/account/profile.jsx +357 -0
- package/app/pages/account/wishlist/index.jsx +195 -0
- package/app/pages/account/wishlist/index.mock.js +1481 -0
- package/app/pages/account/wishlist/index.test.js +56 -0
- package/app/pages/account/wishlist/partials/wishlist-primary-action.jsx +170 -0
- package/app/pages/account/wishlist/partials/wishlist-primary-action.mock.js +1623 -0
- package/app/pages/account/wishlist/partials/wishlist-primary-action.test.js +99 -0
- package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.jsx +120 -0
- package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.test.js +391 -0
- package/app/pages/cart/index.jsx +476 -0
- package/app/pages/cart/index.test.js +481 -0
- package/app/pages/cart/partials/cart-cta.jsx +46 -0
- package/app/pages/cart/partials/cart-secondary-button-group.jsx +135 -0
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +103 -0
- package/app/pages/cart/partials/cart-skeleton.jsx +93 -0
- package/app/pages/cart/partials/cart-title.jsx +27 -0
- package/app/pages/cart/partials/empty-cart.jsx +86 -0
- package/app/pages/checkout/confirmation.jsx +541 -0
- package/app/pages/checkout/confirmation.mock.js +450 -0
- package/app/pages/checkout/confirmation.test.js +114 -0
- package/app/pages/checkout/index.jsx +169 -0
- package/app/pages/checkout/index.test.js +582 -0
- package/app/pages/checkout/partials/cc-radio-group.jsx +122 -0
- package/app/pages/checkout/partials/checkout-footer.jsx +140 -0
- package/app/pages/checkout/partials/checkout-footer.test.js +16 -0
- package/app/pages/checkout/partials/checkout-header.jsx +54 -0
- package/app/pages/checkout/partials/checkout-header.test.js +16 -0
- package/app/pages/checkout/partials/checkout-skeleton.jsx +52 -0
- package/app/pages/checkout/partials/contact-info.jsx +251 -0
- package/app/pages/checkout/partials/contact-info.test.js +43 -0
- package/app/pages/checkout/partials/payment-form.jsx +97 -0
- package/app/pages/checkout/partials/payment.jsx +276 -0
- package/app/pages/checkout/partials/shipping-address-selection.jsx +377 -0
- package/app/pages/checkout/partials/shipping-address.jsx +132 -0
- package/app/pages/checkout/partials/shipping-options.jsx +232 -0
- package/app/pages/checkout/util/checkout-context.js +94 -0
- package/app/pages/home/data.js +134 -0
- package/app/pages/home/index.jsx +301 -0
- package/app/pages/home/index.test.js +23 -0
- package/app/pages/login/index.jsx +123 -0
- package/app/pages/login/index.test.js +229 -0
- package/app/pages/login-redirect/index.jsx +23 -0
- package/app/pages/login-redirect/index.test.js +16 -0
- package/app/pages/page-not-found/index.jsx +90 -0
- package/app/pages/page-not-found/index.test.js +31 -0
- package/app/pages/product-detail/index.jsx +394 -0
- package/app/pages/product-detail/index.mock.js +197 -0
- package/app/pages/product-detail/index.test.js +162 -0
- package/app/pages/product-detail/partials/information-accordion.jsx +121 -0
- package/app/pages/product-list/index.jsx +735 -0
- package/app/pages/product-list/index.test.js +180 -0
- package/app/pages/product-list/partials/above-page-header.jsx +10 -0
- package/app/pages/product-list/partials/checkbox-refinements.jsx +41 -0
- package/app/pages/product-list/partials/checkbox-refinements.test.js +53 -0
- package/app/pages/product-list/partials/color-refinements.jsx +88 -0
- package/app/pages/product-list/partials/empty-results.jsx +118 -0
- package/app/pages/product-list/partials/link-refinements.jsx +38 -0
- package/app/pages/product-list/partials/page-header.jsx +42 -0
- package/app/pages/product-list/partials/radio-refinements.jsx +60 -0
- package/app/pages/product-list/partials/refinements.jsx +144 -0
- package/app/pages/product-list/partials/selected-refinements.jsx +100 -0
- package/app/pages/product-list/partials/size-refinements.jsx +55 -0
- package/app/pages/registration/index.jsx +87 -0
- package/app/pages/registration/index.test.jsx +132 -0
- package/app/pages/reset-password/index.jsx +112 -0
- package/app/pages/reset-password/index.test.jsx +141 -0
- package/app/request-processor.js +118 -0
- package/app/request-processor.test.js +23 -0
- package/app/routes.jsx +111 -0
- package/app/routes.test.js +13 -0
- package/app/ssr.js +70 -0
- package/app/static/ico/favicon.ico +0 -0
- package/app/static/img/global/app-icon-192.png +0 -0
- package/app/static/img/global/app-icon-512.png +0 -0
- package/app/static/img/global/apple-touch-icon.png +0 -0
- package/app/static/img/hero.png +0 -0
- package/app/static/manifest.json +19 -0
- package/app/static/robots.txt +2 -0
- package/app/theme/components/base/accordion.js +21 -0
- package/app/theme/components/base/alert.js +17 -0
- package/app/theme/components/base/badge.js +25 -0
- package/app/theme/components/base/button.js +77 -0
- package/app/theme/components/base/checkbox.js +30 -0
- package/app/theme/components/base/container.js +17 -0
- package/app/theme/components/base/drawer.js +26 -0
- package/app/theme/components/base/formLabel.js +13 -0
- package/app/theme/components/base/icon.js +13 -0
- package/app/theme/components/base/input.js +44 -0
- package/app/theme/components/base/modal.js +11 -0
- package/app/theme/components/base/popover.js +61 -0
- package/app/theme/components/base/radio.js +33 -0
- package/app/theme/components/base/select.js +15 -0
- package/app/theme/components/base/skeleton.js +12 -0
- package/app/theme/components/base/tooltip.js +19 -0
- package/app/theme/components/project/_app.js +25 -0
- package/app/theme/components/project/breadcrumb.js +25 -0
- package/app/theme/components/project/checkout-footer.js +35 -0
- package/app/theme/components/project/drawer-menu.js +66 -0
- package/app/theme/components/project/footer.js +84 -0
- package/app/theme/components/project/header.js +84 -0
- package/app/theme/components/project/image-gallery.js +59 -0
- package/app/theme/components/project/links-list.js +43 -0
- package/app/theme/components/project/list-menu.js +91 -0
- package/app/theme/components/project/locale-selector.js +42 -0
- package/app/theme/components/project/nested-accordion.js +26 -0
- package/app/theme/components/project/offline-banner.js +25 -0
- package/app/theme/components/project/pagination.js +22 -0
- package/app/theme/components/project/product-tile.js +32 -0
- package/app/theme/components/project/social-icons.js +52 -0
- package/app/theme/components/project/swatch-group.js +115 -0
- package/app/theme/foundations/colors.js +170 -0
- package/app/theme/foundations/gradients.js +9 -0
- package/app/theme/foundations/layerStyles.js +41 -0
- package/app/theme/foundations/shadows.js +9 -0
- package/app/theme/foundations/sizes.js +18 -0
- package/app/theme/foundations/space.js +9 -0
- package/app/theme/foundations/styles.js +21 -0
- package/app/theme/index.js +104 -0
- package/app/utils/cc-utils.js +112 -0
- package/app/utils/cc-utils.test.js +41 -0
- package/app/utils/image-groups-utils.js +62 -0
- package/app/utils/image-groups-utils.test.js +65 -0
- package/app/utils/locale.js +78 -0
- package/app/utils/locale.test.js +112 -0
- package/app/utils/password-utils.js +21 -0
- package/app/utils/phone-utils.js +22 -0
- package/app/utils/phone-utils.test.js +15 -0
- package/app/utils/product-utils.js +35 -0
- package/app/utils/product-utils.test.js +51 -0
- package/app/utils/responsive-image.js +198 -0
- package/app/utils/responsive-image.test.js +170 -0
- package/app/utils/routes-utils.js +111 -0
- package/app/utils/routes-utils.test.js +291 -0
- package/app/utils/site-utils.js +222 -0
- package/app/utils/site-utils.test.js +376 -0
- package/app/utils/test-utils.js +257 -0
- package/app/utils/url.js +291 -0
- package/app/utils/url.test.js +421 -0
- package/app/utils/utils.js +201 -0
- package/app/utils/utils.test.js +182 -0
- package/babel.config.js +7 -0
- package/cache-hash-config.json +8 -0
- package/config/default.js +64 -0
- package/config/mocks/default.js +131 -0
- package/config/sites.js +78 -0
- package/jest-setup.js +191 -0
- package/jest.config.js +50 -0
- package/jsconfig.json +13 -0
- package/package.json +105 -0
- package/scripts/extract-default-messages.js +92 -0
- package/tests/lighthouserc.js +37 -0
- package/translations/README.md +127 -0
- package/translations/compiled/de-DE.json +3212 -0
- package/translations/compiled/en-GB.json +3212 -0
- package/translations/compiled/en-US.json +3212 -0
- package/translations/compiled/en-XA.json +6948 -0
- package/translations/compiled/es-MX.json +3216 -0
- package/translations/compiled/fr-FR.json +3216 -0
- package/translations/compiled/it-IT.json +3188 -0
- package/translations/compiled/ja-JP.json +3200 -0
- package/translations/compiled/ko-KR.json +3180 -0
- package/translations/compiled/pt-BR.json +3220 -0
- package/translations/compiled/zh-CN.json +3212 -0
- package/translations/compiled/zh-TW.json +3208 -0
- package/translations/de-DE.json +1417 -0
- package/translations/en-GB.json +1417 -0
- package/translations/en-US.json +1417 -0
- package/translations/es-MX.json +1417 -0
- package/translations/fr-FR.json +1417 -0
- package/translations/it-IT.json +1417 -0
- package/translations/ja-JP.json +1417 -0
- package/translations/ko-KR.json +1417 -0
- package/translations/pt-BR.json +1417 -0
- package/translations/zh-CN.json +1417 -0
- package/translations/zh-TW.json +1417 -0
- package/worker/main.js +36 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import PropTypes from 'prop-types'
|
|
9
|
+
import {FormattedMessage} from 'react-intl'
|
|
10
|
+
|
|
11
|
+
// Chakra Components
|
|
12
|
+
import {Box, Fade, Flex, Stack, Text} from '@chakra-ui/react'
|
|
13
|
+
|
|
14
|
+
// Project Components
|
|
15
|
+
import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive'
|
|
16
|
+
import ItemVariantProvider from '@salesforce/retail-react-app/app/components/item-variant'
|
|
17
|
+
import CartItemVariantImage from '@salesforce/retail-react-app/app/components/item-variant/item-image'
|
|
18
|
+
import CartItemVariantName from '@salesforce/retail-react-app/app/components/item-variant/item-name'
|
|
19
|
+
import CartItemVariantAttributes from '@salesforce/retail-react-app/app/components/item-variant/item-attributes'
|
|
20
|
+
import CartItemVariantPrice from '@salesforce/retail-react-app/app/components/item-variant/item-price'
|
|
21
|
+
import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
|
|
22
|
+
import QuantityPicker from '@salesforce/retail-react-app/app/components/quantity-picker'
|
|
23
|
+
|
|
24
|
+
// Utilities
|
|
25
|
+
import {noop} from '@salesforce/retail-react-app/app/utils/utils'
|
|
26
|
+
|
|
27
|
+
// Hooks
|
|
28
|
+
import {useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Component representing a product item usually in a list with details about the product - name, variant, pricing, etc.
|
|
32
|
+
* @param {Object} product Product to be represented in the list item.
|
|
33
|
+
* @param {node} primaryAction Child component representing the most prominent action to be performed by the user.
|
|
34
|
+
* @param {node} secondaryActions Child component representing the other actions relevant to the product to be performed by the user.
|
|
35
|
+
* @param {func} onItemQuantityChange callback function to be invoked whenever item quantity changes.
|
|
36
|
+
* @param {boolean} showLoading Renders a loading spinner with overlay if set to true.
|
|
37
|
+
* @returns A JSX element representing product item in a list (eg: wishlist, cart, etc).
|
|
38
|
+
*/
|
|
39
|
+
const ProductItem = ({
|
|
40
|
+
product,
|
|
41
|
+
primaryAction,
|
|
42
|
+
secondaryActions,
|
|
43
|
+
onItemQuantityChange = noop,
|
|
44
|
+
showLoading = false
|
|
45
|
+
}) => {
|
|
46
|
+
const {stepQuantity, showInventoryMessage, inventoryMessage, quantity, setQuantity} =
|
|
47
|
+
useDerivedProduct(product)
|
|
48
|
+
return (
|
|
49
|
+
<Box position="relative" data-testid={`sf-cart-item-${product.productId}`}>
|
|
50
|
+
<ItemVariantProvider variant={product}>
|
|
51
|
+
{showLoading && <LoadingSpinner />}
|
|
52
|
+
<Stack layerStyle="cardBordered" align="flex-start">
|
|
53
|
+
<Flex width="full" alignItems="flex-start" backgroundColor="white">
|
|
54
|
+
<CartItemVariantImage width={['88px', '136px']} mr={4} />
|
|
55
|
+
<Stack spacing={3} flex={1}>
|
|
56
|
+
<Stack spacing={1}>
|
|
57
|
+
<CartItemVariantName />
|
|
58
|
+
<CartItemVariantAttributes />
|
|
59
|
+
<HideOnDesktop>
|
|
60
|
+
<Box marginTop={2}>
|
|
61
|
+
<CartItemVariantPrice align="left" />
|
|
62
|
+
</Box>
|
|
63
|
+
</HideOnDesktop>
|
|
64
|
+
</Stack>
|
|
65
|
+
|
|
66
|
+
<Flex align="flex-end" justify="space-between">
|
|
67
|
+
<Stack spacing={1}>
|
|
68
|
+
<Text fontSize="sm" color="gray.700">
|
|
69
|
+
<FormattedMessage
|
|
70
|
+
defaultMessage="Quantity:"
|
|
71
|
+
id="product_item.label.quantity"
|
|
72
|
+
/>
|
|
73
|
+
</Text>
|
|
74
|
+
<QuantityPicker
|
|
75
|
+
step={stepQuantity}
|
|
76
|
+
value={quantity}
|
|
77
|
+
min={0}
|
|
78
|
+
clampValueOnBlur={false}
|
|
79
|
+
onBlur={(e) => {
|
|
80
|
+
// Default to last known quantity if a user leaves the box with an invalid value
|
|
81
|
+
const {value} = e.target
|
|
82
|
+
|
|
83
|
+
if (!value) {
|
|
84
|
+
setQuantity(product.quantity)
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
onChange={(stringValue, numberValue) => {
|
|
88
|
+
// Set the Quantity of product to value of input if value number
|
|
89
|
+
if (numberValue >= 0) {
|
|
90
|
+
// Call handler
|
|
91
|
+
onItemQuantityChange(numberValue).then(
|
|
92
|
+
(isValidChange) =>
|
|
93
|
+
isValidChange && setQuantity(numberValue)
|
|
94
|
+
)
|
|
95
|
+
} else if (stringValue === '') {
|
|
96
|
+
// We want to allow the use to clear the input to start a new input so here we set the quantity to '' so NAN is not displayed
|
|
97
|
+
// User will not be able to add '' qauntity to the cart due to the add to cart button enablement rules
|
|
98
|
+
setQuantity(stringValue)
|
|
99
|
+
}
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</Stack>
|
|
103
|
+
<Stack>
|
|
104
|
+
<HideOnMobile>
|
|
105
|
+
<CartItemVariantPrice />
|
|
106
|
+
</HideOnMobile>
|
|
107
|
+
<Box display={['none', 'block', 'block', 'block']}>
|
|
108
|
+
{primaryAction}
|
|
109
|
+
</Box>
|
|
110
|
+
</Stack>
|
|
111
|
+
</Flex>
|
|
112
|
+
|
|
113
|
+
<Box>
|
|
114
|
+
{product && showInventoryMessage && (
|
|
115
|
+
<Fade in={true}>
|
|
116
|
+
<Text color="orange.600" fontWeight={600}>
|
|
117
|
+
{inventoryMessage}
|
|
118
|
+
</Text>
|
|
119
|
+
</Fade>
|
|
120
|
+
)}
|
|
121
|
+
</Box>
|
|
122
|
+
|
|
123
|
+
{secondaryActions}
|
|
124
|
+
</Stack>
|
|
125
|
+
</Flex>
|
|
126
|
+
|
|
127
|
+
<Box display={['block', 'none', 'none', 'none']} w={'full'}>
|
|
128
|
+
{primaryAction}
|
|
129
|
+
</Box>
|
|
130
|
+
</Stack>
|
|
131
|
+
</ItemVariantProvider>
|
|
132
|
+
</Box>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ProductItem.propTypes = {
|
|
137
|
+
product: PropTypes.object,
|
|
138
|
+
onItemQuantityChange: PropTypes.func,
|
|
139
|
+
onAddItemToCart: PropTypes.func,
|
|
140
|
+
showLoading: PropTypes.bool,
|
|
141
|
+
isWishlistItem: PropTypes.bool,
|
|
142
|
+
primaryAction: PropTypes.node,
|
|
143
|
+
secondaryActions: PropTypes.node
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default ProductItem
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import ProductItem from '@salesforce/retail-react-app/app/components/product-item/index'
|
|
9
|
+
import {mockedCustomerProductListsDetails} from '@salesforce/retail-react-app/app/mocks/mock-data'
|
|
10
|
+
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
|
|
11
|
+
import {screen} from '@testing-library/react'
|
|
12
|
+
|
|
13
|
+
jest.mock('@salesforce/commerce-sdk-react', () => {
|
|
14
|
+
const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
|
|
15
|
+
return {
|
|
16
|
+
...originalModule,
|
|
17
|
+
useCustomerBaskets: jest.fn().mockReturnValue({data: {baskets: [{currency: 'GBP'}]}})
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Set up and clean up
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.resetModules()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
jest.setTimeout(60000)
|
|
27
|
+
const MockedComponent = () => {
|
|
28
|
+
const product = mockedCustomerProductListsDetails.data[0]
|
|
29
|
+
return <ProductItem product={{...product, productName: product.name}} />
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('renders product item name, attributes and price', async () => {
|
|
33
|
+
renderWithProviders(<MockedComponent />)
|
|
34
|
+
|
|
35
|
+
expect(await screen.getByText(/apple ipod nano/i)).toBeInTheDocument()
|
|
36
|
+
expect(await screen.getByText(/color: green/i)).toBeInTheDocument()
|
|
37
|
+
expect(await screen.getByText(/memory size: 16 GB/i)).toBeInTheDocument()
|
|
38
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
import React, {forwardRef, useRef} from 'react'
|
|
8
|
+
import PropTypes from 'prop-types'
|
|
9
|
+
import {AspectRatio, Box, Heading, IconButton, Skeleton, Stack} from '@chakra-ui/react'
|
|
10
|
+
import ProductTile from '@salesforce/retail-react-app/app/components/product-tile'
|
|
11
|
+
import {ChevronLeftIcon, ChevronRightIcon} from '@salesforce/retail-react-app/app/components/icons'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders a scrollable, horizontal container of products with native scroll
|
|
15
|
+
* snapping and manual button controls.
|
|
16
|
+
*/
|
|
17
|
+
const ProductScroller = forwardRef(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
header,
|
|
21
|
+
title,
|
|
22
|
+
products,
|
|
23
|
+
isLoading,
|
|
24
|
+
scrollProps,
|
|
25
|
+
itemWidth = {base: '70%', md: '40%', lg: 'calc(33.33% - 10px)'},
|
|
26
|
+
productTileProps,
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref
|
|
30
|
+
) => {
|
|
31
|
+
const scrollRef = useRef()
|
|
32
|
+
|
|
33
|
+
// Renders nothing if we aren't loading and have no products.
|
|
34
|
+
if ((!products || products.length < 1) && !isLoading) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Scroll the container left or right by 100%. Passing no args or `1`
|
|
39
|
+
// scrolls to the right, and passing `-1` scrolls left.
|
|
40
|
+
const scroll = (direction = 1) => {
|
|
41
|
+
scrollRef.current?.scrollBy({
|
|
42
|
+
top: 0,
|
|
43
|
+
left: direction * window.innerWidth,
|
|
44
|
+
behavior: 'smooth'
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Box position="relative" data-testid="product-scroller" ref={ref}>
|
|
50
|
+
<Stack spacing={6} {...props}>
|
|
51
|
+
{isLoading && <Skeleton height={6} width="150px" m="auto" />}
|
|
52
|
+
|
|
53
|
+
{title && !header && !isLoading && (
|
|
54
|
+
<Heading as="h2" fontSize="xl" textAlign="center">
|
|
55
|
+
{title}
|
|
56
|
+
</Heading>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{!title && !isLoading && header}
|
|
60
|
+
|
|
61
|
+
<Stack
|
|
62
|
+
ref={scrollRef}
|
|
63
|
+
direction="row"
|
|
64
|
+
spacing={4}
|
|
65
|
+
wrap="nowrap"
|
|
66
|
+
overflowX="scroll"
|
|
67
|
+
px={{base: 4, md: 8, lg: 0}}
|
|
68
|
+
{...scrollProps}
|
|
69
|
+
sx={{
|
|
70
|
+
scrollPadding: {base: 16, md: 32, lg: 0},
|
|
71
|
+
scrollSnapType: 'x mandatory',
|
|
72
|
+
WebkitOverflowScrolling: 'touch', // Safari touch scrolling needed for scroll snap
|
|
73
|
+
...scrollProps?.sx
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{(isLoading ? [0, 1, 2, 4] : products).map((product, idx) => {
|
|
77
|
+
return (
|
|
78
|
+
<Box
|
|
79
|
+
key={product?.id || idx}
|
|
80
|
+
flex="0 0 auto"
|
|
81
|
+
width={itemWidth}
|
|
82
|
+
style={{scrollSnapAlign: 'start'}}
|
|
83
|
+
>
|
|
84
|
+
{isLoading ? (
|
|
85
|
+
<Stack data-testid="product-scroller-item-skeleton">
|
|
86
|
+
<AspectRatio ratio={1}>
|
|
87
|
+
<Skeleton />
|
|
88
|
+
</AspectRatio>
|
|
89
|
+
<Stack spacing={2}>
|
|
90
|
+
<Skeleton width="150px" height={5} />
|
|
91
|
+
<Skeleton width="75px" height={5} />
|
|
92
|
+
</Stack>
|
|
93
|
+
</Stack>
|
|
94
|
+
) : (
|
|
95
|
+
<ProductTile
|
|
96
|
+
data-testid="product-scroller-item"
|
|
97
|
+
product={product}
|
|
98
|
+
{...(typeof productTileProps === 'function'
|
|
99
|
+
? {...productTileProps(product)}
|
|
100
|
+
: {...productTileProps})}
|
|
101
|
+
dynamicImageProps={{
|
|
102
|
+
widths: ['70vw', '70vw', '40vw', '30vw']
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
</Box>
|
|
107
|
+
)
|
|
108
|
+
})}
|
|
109
|
+
</Stack>
|
|
110
|
+
</Stack>
|
|
111
|
+
|
|
112
|
+
{products?.length > 3 && (
|
|
113
|
+
<>
|
|
114
|
+
<Box
|
|
115
|
+
display={{
|
|
116
|
+
base: 'none',
|
|
117
|
+
lg: 'block'
|
|
118
|
+
}}
|
|
119
|
+
position="absolute"
|
|
120
|
+
top="50%"
|
|
121
|
+
left={{base: 0, lg: 4}}
|
|
122
|
+
transform="translateY(-50%)"
|
|
123
|
+
>
|
|
124
|
+
<IconButton
|
|
125
|
+
data-testid="product-scroller-nav-left"
|
|
126
|
+
aria-label="Scroll products left"
|
|
127
|
+
icon={<ChevronLeftIcon color="black" />}
|
|
128
|
+
borderRadius="full"
|
|
129
|
+
colorScheme="whiteAlpha"
|
|
130
|
+
onClick={() => scroll(-1)}
|
|
131
|
+
/>
|
|
132
|
+
</Box>
|
|
133
|
+
|
|
134
|
+
<Box
|
|
135
|
+
display={{
|
|
136
|
+
base: 'none',
|
|
137
|
+
lg: 'block'
|
|
138
|
+
}}
|
|
139
|
+
position="absolute"
|
|
140
|
+
top="50%"
|
|
141
|
+
right={{base: 0, lg: 4}}
|
|
142
|
+
transform="translateY(-50%)"
|
|
143
|
+
>
|
|
144
|
+
<IconButton
|
|
145
|
+
data-testid="product-scroller-nav-right"
|
|
146
|
+
aria-label="Scroll products right"
|
|
147
|
+
icon={<ChevronRightIcon color="black" />}
|
|
148
|
+
borderRadius="full"
|
|
149
|
+
colorScheme="whiteAlpha"
|
|
150
|
+
onClick={() => scroll(1)}
|
|
151
|
+
/>
|
|
152
|
+
</Box>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</Box>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
ProductScroller.displayName = 'ProductScroller'
|
|
161
|
+
|
|
162
|
+
ProductScroller.propTypes = {
|
|
163
|
+
header: PropTypes.any,
|
|
164
|
+
title: PropTypes.any,
|
|
165
|
+
products: PropTypes.array,
|
|
166
|
+
isLoading: PropTypes.bool,
|
|
167
|
+
scrollProps: PropTypes.object,
|
|
168
|
+
itemWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
|
|
169
|
+
productTileProps: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export default ProductScroller
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2021, salesforce.com, inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react'
|
|
8
|
+
import {screen} from '@testing-library/react'
|
|
9
|
+
import userEvent from '@testing-library/user-event'
|
|
10
|
+
import ProductScroller from '@salesforce/retail-react-app/app/components/product-scroller/index'
|
|
11
|
+
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
|
|
12
|
+
|
|
13
|
+
// Our component uses `scrollBy` on an html element, which we need
|
|
14
|
+
// to create ourselves as its not in jsdom by default. Here we make
|
|
15
|
+
// it a spy so we can assert it was called when we expect.
|
|
16
|
+
window.HTMLElement.prototype.scrollBy = jest.fn()
|
|
17
|
+
|
|
18
|
+
const testProducts = [1, 2, 3, 4].map((i) => ({
|
|
19
|
+
id: i,
|
|
20
|
+
productId: `${i}`,
|
|
21
|
+
productName: `Product ${i}`,
|
|
22
|
+
image: {disBaseLink: '/testimage'},
|
|
23
|
+
price: 9.99,
|
|
24
|
+
currency: 'USD'
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
describe('Product Scroller', () => {
|
|
28
|
+
test('renders loading skeletons', () => {
|
|
29
|
+
renderWithProviders(<ProductScroller isLoading />)
|
|
30
|
+
expect(screen.getAllByTestId('product-scroller-item-skeleton')).toHaveLength(4)
|
|
31
|
+
})
|
|
32
|
+
test('renders nothing when no products and not loading', () => {
|
|
33
|
+
renderWithProviders(<ProductScroller products={undefined} />)
|
|
34
|
+
expect(screen.queryByTestId('product-scroller')).not.toBeInTheDocument()
|
|
35
|
+
})
|
|
36
|
+
test('Renders product items', () => {
|
|
37
|
+
renderWithProviders(<ProductScroller title="Scroller Title" products={testProducts} />)
|
|
38
|
+
expect(screen.getByText('Scroller Title')).toBeInTheDocument()
|
|
39
|
+
expect(screen.getAllByTestId('product-scroller-item')).toHaveLength(4)
|
|
40
|
+
})
|
|
41
|
+
test('Renders scrollable product tiles with custom header component', () => {
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<ProductScroller
|
|
44
|
+
header={<h1 data-testid="custom-header">Scroller Header</h1>}
|
|
45
|
+
products={testProducts}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
expect(screen.getByTestId('custom-header')).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
test('Renders left/right scroll buttons', async () => {
|
|
51
|
+
const user = userEvent.setup()
|
|
52
|
+
renderWithProviders(<ProductScroller title="Scroller Title" products={testProducts} />)
|
|
53
|
+
await user.click(screen.getByTestId('product-scroller-nav-right'))
|
|
54
|
+
expect(window.HTMLElement.prototype.scrollBy).toHaveBeenCalledWith({
|
|
55
|
+
top: 0,
|
|
56
|
+
left: 1024,
|
|
57
|
+
behavior: 'smooth'
|
|
58
|
+
})
|
|
59
|
+
await user.click(screen.getByTestId('product-scroller-nav-left'))
|
|
60
|
+
expect(window.HTMLElement.prototype.scrollBy).toHaveBeenCalledWith({
|
|
61
|
+
top: 0,
|
|
62
|
+
left: -1024,
|
|
63
|
+
behavior: 'smooth'
|
|
64
|
+
})
|
|
65
|
+
expect(screen.getByTestId('product-scroller-nav-left')).toBeInTheDocument()
|
|
66
|
+
expect(screen.getByTestId('product-scroller-nav-right')).toBeInTheDocument()
|
|
67
|
+
})
|
|
68
|
+
test('Does not render left/right scroll buttons when less than 4 products', () => {
|
|
69
|
+
renderWithProviders(
|
|
70
|
+
<ProductScroller title="Scroller Title" products={testProducts.slice(0, 2)} />
|
|
71
|
+
)
|
|
72
|
+
expect(screen.queryByTestId('product-scroller-nav-left')).not.toBeInTheDocument()
|
|
73
|
+
expect(screen.queryByTestId('product-scroller-nav-right')).not.toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
test('productTileProps as object', async () => {
|
|
76
|
+
const user = userEvent.setup()
|
|
77
|
+
|
|
78
|
+
const onClickMock = jest.fn()
|
|
79
|
+
renderWithProviders(
|
|
80
|
+
<ProductScroller products={testProducts} productTileProps={{onClick: onClickMock}} />
|
|
81
|
+
)
|
|
82
|
+
await user.click(screen.getByText(testProducts[0].productName))
|
|
83
|
+
expect(onClickMock).toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
test('productTileProps as function', async () => {
|
|
86
|
+
const user = userEvent.setup()
|
|
87
|
+
|
|
88
|
+
const onClickMock = jest.fn()
|
|
89
|
+
renderWithProviders(
|
|
90
|
+
<ProductScroller
|
|
91
|
+
products={testProducts}
|
|
92
|
+
productTileProps={() => ({onClick: onClickMock})}
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
await user.click(screen.getByText(testProducts[0].productName))
|
|
96
|
+
expect(onClickMock).toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2022, Salesforce, Inc.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {useState} from 'react'
|
|
9
|
+
import PropTypes from 'prop-types'
|
|
10
|
+
import {HeartIcon, HeartSolidIcon} from '@salesforce/retail-react-app/app/components/icons'
|
|
11
|
+
|
|
12
|
+
// Components
|
|
13
|
+
import {
|
|
14
|
+
AspectRatio,
|
|
15
|
+
Box,
|
|
16
|
+
Skeleton as ChakraSkeleton,
|
|
17
|
+
Text,
|
|
18
|
+
Stack,
|
|
19
|
+
useMultiStyleConfig,
|
|
20
|
+
IconButton
|
|
21
|
+
} from '@chakra-ui/react'
|
|
22
|
+
import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-image'
|
|
23
|
+
|
|
24
|
+
// Hooks
|
|
25
|
+
import {useIntl} from 'react-intl'
|
|
26
|
+
|
|
27
|
+
// Other
|
|
28
|
+
import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url'
|
|
29
|
+
import Link from '@salesforce/retail-react-app/app/components/link'
|
|
30
|
+
import withRegistration from '@salesforce/retail-react-app/app/components/with-registration'
|
|
31
|
+
import {useCurrency} from '@salesforce/retail-react-app/app/hooks'
|
|
32
|
+
|
|
33
|
+
const IconButtonWithRegistration = withRegistration(IconButton)
|
|
34
|
+
|
|
35
|
+
// Component Skeleton
|
|
36
|
+
export const Skeleton = () => {
|
|
37
|
+
const styles = useMultiStyleConfig('ProductTile')
|
|
38
|
+
return (
|
|
39
|
+
<Box data-testid="sf-product-tile-skeleton">
|
|
40
|
+
<Stack spacing={2}>
|
|
41
|
+
<Box {...styles.imageWrapper}>
|
|
42
|
+
<AspectRatio ratio={1} {...styles.image}>
|
|
43
|
+
<ChakraSkeleton />
|
|
44
|
+
</AspectRatio>
|
|
45
|
+
</Box>
|
|
46
|
+
<ChakraSkeleton width="80px" height="20px" />
|
|
47
|
+
<ChakraSkeleton width={{base: '120px', md: '220px'}} height="12px" />
|
|
48
|
+
</Stack>
|
|
49
|
+
</Box>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The ProductTile is a simple visual representation of a
|
|
55
|
+
* product object. It will show it's default image, name and price.
|
|
56
|
+
* It also supports favourite products, controlled by a heart icon.
|
|
57
|
+
*/
|
|
58
|
+
const ProductTile = (props) => {
|
|
59
|
+
const intl = useIntl()
|
|
60
|
+
const {
|
|
61
|
+
product,
|
|
62
|
+
enableFavourite = false,
|
|
63
|
+
isFavourite,
|
|
64
|
+
onFavouriteToggle,
|
|
65
|
+
dynamicImageProps,
|
|
66
|
+
...rest
|
|
67
|
+
} = props
|
|
68
|
+
|
|
69
|
+
const {currency, image, price, productId, hitType} = product
|
|
70
|
+
|
|
71
|
+
// ProductTile is used by two components, RecommendedProducts and ProductList.
|
|
72
|
+
// RecommendedProducts provides a localized product name as `name` and non-localized product
|
|
73
|
+
// name as `productName`. ProductList provides a localized name as `productName` and does not
|
|
74
|
+
// use the `name` property.
|
|
75
|
+
const localizedProductName = product.name ?? product.productName
|
|
76
|
+
|
|
77
|
+
const {currency: activeCurrency} = useCurrency()
|
|
78
|
+
const [isFavouriteLoading, setFavouriteLoading] = useState(false)
|
|
79
|
+
const styles = useMultiStyleConfig('ProductTile')
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Link
|
|
83
|
+
data-testid="product-tile"
|
|
84
|
+
{...styles.container}
|
|
85
|
+
to={productUrlBuilder({id: productId}, intl.local)}
|
|
86
|
+
{...rest}
|
|
87
|
+
>
|
|
88
|
+
<Box {...styles.imageWrapper}>
|
|
89
|
+
{image && (
|
|
90
|
+
<AspectRatio {...styles.image}>
|
|
91
|
+
<DynamicImage
|
|
92
|
+
src={`${image.disBaseLink || image.link}[?sw={width}&q=60]`}
|
|
93
|
+
widths={dynamicImageProps?.widths}
|
|
94
|
+
imageProps={{
|
|
95
|
+
alt: image.alt,
|
|
96
|
+
...dynamicImageProps?.imageProps
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</AspectRatio>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{enableFavourite && (
|
|
103
|
+
<Box
|
|
104
|
+
onClick={(e) => {
|
|
105
|
+
// stop click event from bubbling
|
|
106
|
+
// to avoid user from clicking the underlying
|
|
107
|
+
// product while the favourite icon is disabled
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<IconButtonWithRegistration
|
|
112
|
+
aria-label={intl.formatMessage({
|
|
113
|
+
id: 'product_tile.assistive_msg.wishlist',
|
|
114
|
+
defaultMessage: 'Wishlist'
|
|
115
|
+
})}
|
|
116
|
+
icon={isFavourite ? <HeartSolidIcon /> : <HeartIcon />}
|
|
117
|
+
{...styles.favIcon}
|
|
118
|
+
disabled={isFavouriteLoading}
|
|
119
|
+
onClick={async () => {
|
|
120
|
+
setFavouriteLoading(true)
|
|
121
|
+
await onFavouriteToggle(!isFavourite)
|
|
122
|
+
setFavouriteLoading(false)
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</Box>
|
|
126
|
+
)}
|
|
127
|
+
</Box>
|
|
128
|
+
|
|
129
|
+
{/* Title */}
|
|
130
|
+
<Text {...styles.title}>{localizedProductName}</Text>
|
|
131
|
+
|
|
132
|
+
{/* Price */}
|
|
133
|
+
<Text {...styles.price} data-testid="product-tile-price">
|
|
134
|
+
{hitType === 'set' &&
|
|
135
|
+
intl.formatMessage({
|
|
136
|
+
id: 'product_tile.label.starting_at_price',
|
|
137
|
+
defaultMessage: 'Starting at'
|
|
138
|
+
})}{' '}
|
|
139
|
+
{intl.formatNumber(price, {
|
|
140
|
+
style: 'currency',
|
|
141
|
+
currency: currency || activeCurrency
|
|
142
|
+
})}
|
|
143
|
+
</Text>
|
|
144
|
+
</Link>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ProductTile.displayName = 'ProductTile'
|
|
149
|
+
|
|
150
|
+
ProductTile.propTypes = {
|
|
151
|
+
/**
|
|
152
|
+
* The product search hit that will be represented in this
|
|
153
|
+
* component.
|
|
154
|
+
*/
|
|
155
|
+
product: PropTypes.shape({
|
|
156
|
+
currency: PropTypes.string,
|
|
157
|
+
image: PropTypes.shape({
|
|
158
|
+
alt: PropTypes.string,
|
|
159
|
+
disBaseLink: PropTypes.string,
|
|
160
|
+
link: PropTypes.string
|
|
161
|
+
}),
|
|
162
|
+
price: PropTypes.number,
|
|
163
|
+
// `name` is present and localized when `product` is provided by a RecommendedProducts component
|
|
164
|
+
// (from Shopper Products `getProducts` endpoint), but is not present when `product` is
|
|
165
|
+
// provided by a ProductList component.
|
|
166
|
+
// See: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProducts
|
|
167
|
+
name: PropTypes.string,
|
|
168
|
+
// `productName` is localized when provided by a ProductList component (from Shopper Search
|
|
169
|
+
// `productSearch` endpoint), but is NOT localized when provided by a RecommendedProducts
|
|
170
|
+
// component (from Einstein Recommendations `getRecommendations` endpoint).
|
|
171
|
+
// See: https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-search?meta=productSearch
|
|
172
|
+
// See: https://developer.salesforce.com/docs/commerce/einstein-api/references/einstein-api-quick-start-guide?meta=getRecommendations
|
|
173
|
+
// Note: useEinstein() transforms snake_case property names from the API response to camelCase
|
|
174
|
+
productName: PropTypes.string,
|
|
175
|
+
productId: PropTypes.string,
|
|
176
|
+
hitType: PropTypes.string
|
|
177
|
+
}),
|
|
178
|
+
/**
|
|
179
|
+
* Enable adding/removing product as a favourite.
|
|
180
|
+
* Use case: wishlist.
|
|
181
|
+
*/
|
|
182
|
+
enableFavourite: PropTypes.bool,
|
|
183
|
+
/**
|
|
184
|
+
* Display the product as a faviourite.
|
|
185
|
+
*/
|
|
186
|
+
isFavourite: PropTypes.bool,
|
|
187
|
+
/**
|
|
188
|
+
* Callback function to be invoked when the user
|
|
189
|
+
* interacts with favourite icon/button.
|
|
190
|
+
*/
|
|
191
|
+
onFavouriteToggle: PropTypes.func,
|
|
192
|
+
dynamicImageProps: PropTypes.object
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default ProductTile
|