@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.
Files changed (425) hide show
  1. package/.eslintignore +7 -0
  2. package/.eslintrc.js +25 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc.yaml +7 -0
  5. package/CHANGELOG.md +173 -0
  6. package/LICENSE +14 -0
  7. package/README.md +48 -0
  8. package/app/assets/svg/account.svg +3 -0
  9. package/app/assets/svg/alert.svg +3 -0
  10. package/app/assets/svg/basket.svg +3 -0
  11. package/app/assets/svg/brand-logo.svg +10 -0
  12. package/app/assets/svg/cc-amex.svg +7 -0
  13. package/app/assets/svg/cc-cvv.svg +8 -0
  14. package/app/assets/svg/cc-discover.svg +14 -0
  15. package/app/assets/svg/cc-mastercard.svg +8 -0
  16. package/app/assets/svg/cc-visa.svg +11 -0
  17. package/app/assets/svg/check-circle.svg +3 -0
  18. package/app/assets/svg/check.svg +3 -0
  19. package/app/assets/svg/chevron-down.svg +3 -0
  20. package/app/assets/svg/chevron-left.svg +3 -0
  21. package/app/assets/svg/chevron-right.svg +3 -0
  22. package/app/assets/svg/chevron-up.svg +3 -0
  23. package/app/assets/svg/close.svg +3 -0
  24. package/app/assets/svg/dashboard.svg +4 -0
  25. package/app/assets/svg/figma-logo.svg +14 -0
  26. package/app/assets/svg/file.svg +3 -0
  27. package/app/assets/svg/filter.svg +3 -0
  28. package/app/assets/svg/flag-ca.svg +5 -0
  29. package/app/assets/svg/flag-cn.svg +19 -0
  30. package/app/assets/svg/flag-fr.svg +19 -0
  31. package/app/assets/svg/flag-gb.svg +16 -0
  32. package/app/assets/svg/flag-it.svg +29 -0
  33. package/app/assets/svg/flag-jp.svg +10 -0
  34. package/app/assets/svg/flag-us.svg +7 -0
  35. package/app/assets/svg/github-logo.svg +40 -0
  36. package/app/assets/svg/hamburger.svg +8 -0
  37. package/app/assets/svg/heart-solid.svg +7 -0
  38. package/app/assets/svg/heart.svg +3 -0
  39. package/app/assets/svg/info.svg +3 -0
  40. package/app/assets/svg/like.svg +4 -0
  41. package/app/assets/svg/location.svg +3 -0
  42. package/app/assets/svg/lock.svg +3 -0
  43. package/app/assets/svg/paypal.svg +19 -0
  44. package/app/assets/svg/plug.svg +3 -0
  45. package/app/assets/svg/plus.svg +3 -0
  46. package/app/assets/svg/receipt.svg +3 -0
  47. package/app/assets/svg/search.svg +8 -0
  48. package/app/assets/svg/signout.svg +3 -0
  49. package/app/assets/svg/social-facebook.svg +3 -0
  50. package/app/assets/svg/social-instagram.svg +3 -0
  51. package/app/assets/svg/social-pinterest.svg +4 -0
  52. package/app/assets/svg/social-twitter.svg +3 -0
  53. package/app/assets/svg/social-youtube.svg +3 -0
  54. package/app/assets/svg/user.svg +3 -0
  55. package/app/assets/svg/visibility-off.svg +5 -0
  56. package/app/assets/svg/visibility.svg +3 -0
  57. package/app/components/_app/index.jsx +401 -0
  58. package/app/components/_app/index.test.js +85 -0
  59. package/app/components/_app/partials/above-header.jsx +10 -0
  60. package/app/components/_app-config/index.jsx +125 -0
  61. package/app/components/_app-config/index.test.js +77 -0
  62. package/app/components/_error/index.jsx +142 -0
  63. package/app/components/_error/index.test.js +25 -0
  64. package/app/components/action-card/index.jsx +75 -0
  65. package/app/components/address-display/index.jsx +30 -0
  66. package/app/components/basic-tile/index.jsx +65 -0
  67. package/app/components/basic-tile/index.test.js +23 -0
  68. package/app/components/breadcrumb/index.jsx +67 -0
  69. package/app/components/breadcrumb/index.test.js +30 -0
  70. package/app/components/confirmation-modal/index.jsx +111 -0
  71. package/app/components/confirmation-modal/index.test.js +98 -0
  72. package/app/components/drawer-menu/index.jsx +405 -0
  73. package/app/components/drawer-menu/index.test.js +33 -0
  74. package/app/components/dynamic-image/index.jsx +56 -0
  75. package/app/components/field/index.jsx +161 -0
  76. package/app/components/footer/index.jsx +269 -0
  77. package/app/components/footer/index.test.js +22 -0
  78. package/app/components/forms/address-fields.jsx +49 -0
  79. package/app/components/forms/credit-card-fields.jsx +149 -0
  80. package/app/components/forms/form-action-buttons.jsx +55 -0
  81. package/app/components/forms/login-fields.jsx +31 -0
  82. package/app/components/forms/password-requirements.jsx +99 -0
  83. package/app/components/forms/post-checkout-registration-fields.jsx +43 -0
  84. package/app/components/forms/profile-fields.jsx +36 -0
  85. package/app/components/forms/promo-code-fields.jsx +43 -0
  86. package/app/components/forms/registration-fields.jsx +42 -0
  87. package/app/components/forms/reset-password-fields.jsx +31 -0
  88. package/app/components/forms/state-province-options.jsx +75 -0
  89. package/app/components/forms/update-password-fields.jsx +49 -0
  90. package/app/components/forms/useAddressFields.jsx +196 -0
  91. package/app/components/forms/useCreditCardFields.jsx +146 -0
  92. package/app/components/forms/useLoginFields.jsx +52 -0
  93. package/app/components/forms/useProfileFields.jsx +95 -0
  94. package/app/components/forms/usePromoCodeFields.jsx +39 -0
  95. package/app/components/forms/useRegistrationFields.jsx +136 -0
  96. package/app/components/forms/useResetPasswordFields.jsx +40 -0
  97. package/app/components/forms/useUpdatePasswordFields.jsx +89 -0
  98. package/app/components/header/index.jsx +290 -0
  99. package/app/components/header/index.test.js +217 -0
  100. package/app/components/hero/index.jsx +84 -0
  101. package/app/components/hero/index.test.js +40 -0
  102. package/app/components/icons/index.jsx +158 -0
  103. package/app/components/icons/index.test.js +20 -0
  104. package/app/components/image-gallery/index.jsx +176 -0
  105. package/app/components/image-gallery/index.test.js +485 -0
  106. package/app/components/item-variant/index.jsx +33 -0
  107. package/app/components/item-variant/item-attributes.jsx +107 -0
  108. package/app/components/item-variant/item-image.jsx +73 -0
  109. package/app/components/item-variant/item-name.jsx +28 -0
  110. package/app/components/item-variant/item-price.jsx +117 -0
  111. package/app/components/link/index.jsx +32 -0
  112. package/app/components/link/index.test.js +72 -0
  113. package/app/components/links-list/index.jsx +89 -0
  114. package/app/components/links-list/index.test.js +62 -0
  115. package/app/components/list-menu/index.jsx +280 -0
  116. package/app/components/list-menu/index.test.js +44 -0
  117. package/app/components/loading-spinner/index.jsx +46 -0
  118. package/app/components/locale-selector/index.jsx +124 -0
  119. package/app/components/locale-selector/index.test.js +37 -0
  120. package/app/components/locale-text/index.jsx +97 -0
  121. package/app/components/locale-text/index.test.js +36 -0
  122. package/app/components/login/index.jsx +96 -0
  123. package/app/components/nested-accordion/index.jsx +185 -0
  124. package/app/components/nested-accordion/index.test.js +98 -0
  125. package/app/components/offline-banner/index.jsx +40 -0
  126. package/app/components/offline-banner/index.test.js +15 -0
  127. package/app/components/offline-boundary/index.jsx +104 -0
  128. package/app/components/offline-boundary/index.test.js +123 -0
  129. package/app/components/order-summary/index.jsx +331 -0
  130. package/app/components/page-action-placeholder/index.jsx +50 -0
  131. package/app/components/pagination/index.jsx +134 -0
  132. package/app/components/pagination/index.test.js +25 -0
  133. package/app/components/product-item/index.jsx +146 -0
  134. package/app/components/product-item/index.test.js +38 -0
  135. package/app/components/product-scroller/index.jsx +172 -0
  136. package/app/components/product-scroller/index.test.js +98 -0
  137. package/app/components/product-tile/index.jsx +195 -0
  138. package/app/components/product-tile/index.test.js +96 -0
  139. package/app/components/product-view/index.jsx +538 -0
  140. package/app/components/product-view/index.test.js +224 -0
  141. package/app/components/product-view-modal/index.jsx +48 -0
  142. package/app/components/product-view-modal/index.test.js +72 -0
  143. package/app/components/promo-code/index.jsx +162 -0
  144. package/app/components/promo-popover/index.jsx +83 -0
  145. package/app/components/quantity-picker/index.jsx +58 -0
  146. package/app/components/radio-card/index.jsx +75 -0
  147. package/app/components/recommended-products/index.jsx +227 -0
  148. package/app/components/register/index.jsx +114 -0
  149. package/app/components/reset-password/index.jsx +87 -0
  150. package/app/components/responsive/index.jsx +29 -0
  151. package/app/components/scroll-to-top/index.jsx +24 -0
  152. package/app/components/scroll-to-top/index.test.js +46 -0
  153. package/app/components/search/index.jsx +279 -0
  154. package/app/components/search/index.test.js +127 -0
  155. package/app/components/search/partials/recent-searches.jsx +76 -0
  156. package/app/components/search/partials/search-suggestions.jsx +45 -0
  157. package/app/components/search/partials/suggestions.jsx +43 -0
  158. package/app/components/section/index.jsx +68 -0
  159. package/app/components/seo/index.jsx +33 -0
  160. package/app/components/social-icons/index.jsx +101 -0
  161. package/app/components/social-icons/index.test.js +30 -0
  162. package/app/components/swatch-group/index.jsx +77 -0
  163. package/app/components/swatch-group/index.test.js +136 -0
  164. package/app/components/swatch-group/swatch.jsx +94 -0
  165. package/app/components/toggle-card/index.jsx +97 -0
  166. package/app/components/with-registration/index.jsx +58 -0
  167. package/app/components/with-registration/index.test.js +85 -0
  168. package/app/constants.js +109 -0
  169. package/app/contexts/index.js +92 -0
  170. package/app/hooks/einstein-mock-data.js +916 -0
  171. package/app/hooks/index.js +17 -0
  172. package/app/hooks/use-add-to-cart-modal.js +344 -0
  173. package/app/hooks/use-add-to-cart-modal.test.js +625 -0
  174. package/app/hooks/use-auth-modal.js +337 -0
  175. package/app/hooks/use-auth-modal.test.js +365 -0
  176. package/app/hooks/use-currency.js +20 -0
  177. package/app/hooks/use-currency.test.js +41 -0
  178. package/app/hooks/use-current-basket.js +39 -0
  179. package/app/hooks/use-current-customer.js +29 -0
  180. package/app/hooks/use-derived-product.js +77 -0
  181. package/app/hooks/use-derived-product.test.js +69 -0
  182. package/app/hooks/use-einstein.js +512 -0
  183. package/app/hooks/use-einstein.test.js +224 -0
  184. package/app/hooks/use-intersection-observer.js +64 -0
  185. package/app/hooks/use-limit-urls.js +31 -0
  186. package/app/hooks/use-limit-urls.test.js +40 -0
  187. package/app/hooks/use-multi-site.js +36 -0
  188. package/app/hooks/use-multi-site.test.js +53 -0
  189. package/app/hooks/use-navigation.js +37 -0
  190. package/app/hooks/use-navigation.test.js +109 -0
  191. package/app/hooks/use-page-urls.js +35 -0
  192. package/app/hooks/use-page-urls.test.js +39 -0
  193. package/app/hooks/use-pdp-search-params.js +16 -0
  194. package/app/hooks/use-pdp-search-params.test.js +52 -0
  195. package/app/hooks/use-previous.js +17 -0
  196. package/app/hooks/use-product-view-modal.js +93 -0
  197. package/app/hooks/use-product-view-modal.test.js +172 -0
  198. package/app/hooks/use-search-params.js +96 -0
  199. package/app/hooks/use-search-params.test.js +91 -0
  200. package/app/hooks/use-sort-urls.js +33 -0
  201. package/app/hooks/use-sort-urls.test.js +42 -0
  202. package/app/hooks/use-toast.js +68 -0
  203. package/app/hooks/use-toast.test.js +58 -0
  204. package/app/hooks/use-variant.js +32 -0
  205. package/app/hooks/use-variant.test.js +81 -0
  206. package/app/hooks/use-variation-attributes.js +138 -0
  207. package/app/hooks/use-variation-attributes.test.js +119 -0
  208. package/app/hooks/use-variation-params.js +31 -0
  209. package/app/hooks/use-variation-params.test.js +73 -0
  210. package/app/hooks/use-wish-list.js +42 -0
  211. package/app/main.jsx +14 -0
  212. package/app/mocks/basket-with-suit.js +146 -0
  213. package/app/mocks/empty-basket.js +39 -0
  214. package/app/mocks/mock-data.js +5632 -0
  215. package/app/mocks/product-set-winter-lookM.js +1224 -0
  216. package/app/mocks/searchResults.js +144 -0
  217. package/app/mocks/variant-750518699578M.js +434 -0
  218. package/app/page-designer/README.md +102 -0
  219. package/app/page-designer/assets/image-tile/index.jsx +51 -0
  220. package/app/page-designer/assets/image-tile/index.test.js +30 -0
  221. package/app/page-designer/assets/image-with-text/index.jsx +140 -0
  222. package/app/page-designer/assets/image-with-text/index.test.js +38 -0
  223. package/app/page-designer/assets/index.js +9 -0
  224. package/app/page-designer/index.js +10 -0
  225. package/app/page-designer/layouts/carousel/index.jsx +222 -0
  226. package/app/page-designer/layouts/carousel/index.test.js +43 -0
  227. package/app/page-designer/layouts/index.js +14 -0
  228. package/app/page-designer/layouts/mobileGrid1r1c/index.jsx +36 -0
  229. package/app/page-designer/layouts/mobileGrid1r1c/index.test.js +35 -0
  230. package/app/page-designer/layouts/mobileGrid2r1c/index.jsx +37 -0
  231. package/app/page-designer/layouts/mobileGrid2r1c/index.test.js +47 -0
  232. package/app/page-designer/layouts/mobileGrid2r2c/index.jsx +37 -0
  233. package/app/page-designer/layouts/mobileGrid2r2c/index.test.js +71 -0
  234. package/app/page-designer/layouts/mobileGrid2r3c/index.jsx +37 -0
  235. package/app/page-designer/layouts/mobileGrid2r3c/index.test.js +95 -0
  236. package/app/page-designer/layouts/mobileGrid3r1c/index.jsx +37 -0
  237. package/app/page-designer/layouts/mobileGrid3r1c/index.test.js +59 -0
  238. package/app/page-designer/layouts/mobileGrid3r2c/index.jsx +37 -0
  239. package/app/page-designer/layouts/mobileGrid3r2c/index.test.js +95 -0
  240. package/app/page-designer/utils.js +14 -0
  241. package/app/pages/account/addresses.jsx +382 -0
  242. package/app/pages/account/addresses.test.js +120 -0
  243. package/app/pages/account/constant.js +57 -0
  244. package/app/pages/account/index.jsx +237 -0
  245. package/app/pages/account/index.test.js +188 -0
  246. package/app/pages/account/order-detail.jsx +397 -0
  247. package/app/pages/account/order-history.jsx +264 -0
  248. package/app/pages/account/orders.jsx +30 -0
  249. package/app/pages/account/orders.test.js +95 -0
  250. package/app/pages/account/profile.jsx +357 -0
  251. package/app/pages/account/wishlist/index.jsx +195 -0
  252. package/app/pages/account/wishlist/index.mock.js +1481 -0
  253. package/app/pages/account/wishlist/index.test.js +56 -0
  254. package/app/pages/account/wishlist/partials/wishlist-primary-action.jsx +170 -0
  255. package/app/pages/account/wishlist/partials/wishlist-primary-action.mock.js +1623 -0
  256. package/app/pages/account/wishlist/partials/wishlist-primary-action.test.js +99 -0
  257. package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.jsx +120 -0
  258. package/app/pages/account/wishlist/partials/wishlist-secondary-button-group.test.js +391 -0
  259. package/app/pages/cart/index.jsx +476 -0
  260. package/app/pages/cart/index.test.js +481 -0
  261. package/app/pages/cart/partials/cart-cta.jsx +46 -0
  262. package/app/pages/cart/partials/cart-secondary-button-group.jsx +135 -0
  263. package/app/pages/cart/partials/cart-secondary-button-group.test.js +103 -0
  264. package/app/pages/cart/partials/cart-skeleton.jsx +93 -0
  265. package/app/pages/cart/partials/cart-title.jsx +27 -0
  266. package/app/pages/cart/partials/empty-cart.jsx +86 -0
  267. package/app/pages/checkout/confirmation.jsx +541 -0
  268. package/app/pages/checkout/confirmation.mock.js +450 -0
  269. package/app/pages/checkout/confirmation.test.js +114 -0
  270. package/app/pages/checkout/index.jsx +169 -0
  271. package/app/pages/checkout/index.test.js +582 -0
  272. package/app/pages/checkout/partials/cc-radio-group.jsx +122 -0
  273. package/app/pages/checkout/partials/checkout-footer.jsx +140 -0
  274. package/app/pages/checkout/partials/checkout-footer.test.js +16 -0
  275. package/app/pages/checkout/partials/checkout-header.jsx +54 -0
  276. package/app/pages/checkout/partials/checkout-header.test.js +16 -0
  277. package/app/pages/checkout/partials/checkout-skeleton.jsx +52 -0
  278. package/app/pages/checkout/partials/contact-info.jsx +251 -0
  279. package/app/pages/checkout/partials/contact-info.test.js +43 -0
  280. package/app/pages/checkout/partials/payment-form.jsx +97 -0
  281. package/app/pages/checkout/partials/payment.jsx +276 -0
  282. package/app/pages/checkout/partials/shipping-address-selection.jsx +377 -0
  283. package/app/pages/checkout/partials/shipping-address.jsx +132 -0
  284. package/app/pages/checkout/partials/shipping-options.jsx +232 -0
  285. package/app/pages/checkout/util/checkout-context.js +94 -0
  286. package/app/pages/home/data.js +134 -0
  287. package/app/pages/home/index.jsx +301 -0
  288. package/app/pages/home/index.test.js +23 -0
  289. package/app/pages/login/index.jsx +123 -0
  290. package/app/pages/login/index.test.js +229 -0
  291. package/app/pages/login-redirect/index.jsx +23 -0
  292. package/app/pages/login-redirect/index.test.js +16 -0
  293. package/app/pages/page-not-found/index.jsx +90 -0
  294. package/app/pages/page-not-found/index.test.js +31 -0
  295. package/app/pages/product-detail/index.jsx +394 -0
  296. package/app/pages/product-detail/index.mock.js +197 -0
  297. package/app/pages/product-detail/index.test.js +162 -0
  298. package/app/pages/product-detail/partials/information-accordion.jsx +121 -0
  299. package/app/pages/product-list/index.jsx +735 -0
  300. package/app/pages/product-list/index.test.js +180 -0
  301. package/app/pages/product-list/partials/above-page-header.jsx +10 -0
  302. package/app/pages/product-list/partials/checkbox-refinements.jsx +41 -0
  303. package/app/pages/product-list/partials/checkbox-refinements.test.js +53 -0
  304. package/app/pages/product-list/partials/color-refinements.jsx +88 -0
  305. package/app/pages/product-list/partials/empty-results.jsx +118 -0
  306. package/app/pages/product-list/partials/link-refinements.jsx +38 -0
  307. package/app/pages/product-list/partials/page-header.jsx +42 -0
  308. package/app/pages/product-list/partials/radio-refinements.jsx +60 -0
  309. package/app/pages/product-list/partials/refinements.jsx +144 -0
  310. package/app/pages/product-list/partials/selected-refinements.jsx +100 -0
  311. package/app/pages/product-list/partials/size-refinements.jsx +55 -0
  312. package/app/pages/registration/index.jsx +87 -0
  313. package/app/pages/registration/index.test.jsx +132 -0
  314. package/app/pages/reset-password/index.jsx +112 -0
  315. package/app/pages/reset-password/index.test.jsx +141 -0
  316. package/app/request-processor.js +118 -0
  317. package/app/request-processor.test.js +23 -0
  318. package/app/routes.jsx +111 -0
  319. package/app/routes.test.js +13 -0
  320. package/app/ssr.js +70 -0
  321. package/app/static/ico/favicon.ico +0 -0
  322. package/app/static/img/global/app-icon-192.png +0 -0
  323. package/app/static/img/global/app-icon-512.png +0 -0
  324. package/app/static/img/global/apple-touch-icon.png +0 -0
  325. package/app/static/img/hero.png +0 -0
  326. package/app/static/manifest.json +19 -0
  327. package/app/static/robots.txt +2 -0
  328. package/app/theme/components/base/accordion.js +21 -0
  329. package/app/theme/components/base/alert.js +17 -0
  330. package/app/theme/components/base/badge.js +25 -0
  331. package/app/theme/components/base/button.js +77 -0
  332. package/app/theme/components/base/checkbox.js +30 -0
  333. package/app/theme/components/base/container.js +17 -0
  334. package/app/theme/components/base/drawer.js +26 -0
  335. package/app/theme/components/base/formLabel.js +13 -0
  336. package/app/theme/components/base/icon.js +13 -0
  337. package/app/theme/components/base/input.js +44 -0
  338. package/app/theme/components/base/modal.js +11 -0
  339. package/app/theme/components/base/popover.js +61 -0
  340. package/app/theme/components/base/radio.js +33 -0
  341. package/app/theme/components/base/select.js +15 -0
  342. package/app/theme/components/base/skeleton.js +12 -0
  343. package/app/theme/components/base/tooltip.js +19 -0
  344. package/app/theme/components/project/_app.js +25 -0
  345. package/app/theme/components/project/breadcrumb.js +25 -0
  346. package/app/theme/components/project/checkout-footer.js +35 -0
  347. package/app/theme/components/project/drawer-menu.js +66 -0
  348. package/app/theme/components/project/footer.js +84 -0
  349. package/app/theme/components/project/header.js +84 -0
  350. package/app/theme/components/project/image-gallery.js +59 -0
  351. package/app/theme/components/project/links-list.js +43 -0
  352. package/app/theme/components/project/list-menu.js +91 -0
  353. package/app/theme/components/project/locale-selector.js +42 -0
  354. package/app/theme/components/project/nested-accordion.js +26 -0
  355. package/app/theme/components/project/offline-banner.js +25 -0
  356. package/app/theme/components/project/pagination.js +22 -0
  357. package/app/theme/components/project/product-tile.js +32 -0
  358. package/app/theme/components/project/social-icons.js +52 -0
  359. package/app/theme/components/project/swatch-group.js +115 -0
  360. package/app/theme/foundations/colors.js +170 -0
  361. package/app/theme/foundations/gradients.js +9 -0
  362. package/app/theme/foundations/layerStyles.js +41 -0
  363. package/app/theme/foundations/shadows.js +9 -0
  364. package/app/theme/foundations/sizes.js +18 -0
  365. package/app/theme/foundations/space.js +9 -0
  366. package/app/theme/foundations/styles.js +21 -0
  367. package/app/theme/index.js +104 -0
  368. package/app/utils/cc-utils.js +112 -0
  369. package/app/utils/cc-utils.test.js +41 -0
  370. package/app/utils/image-groups-utils.js +62 -0
  371. package/app/utils/image-groups-utils.test.js +65 -0
  372. package/app/utils/locale.js +78 -0
  373. package/app/utils/locale.test.js +112 -0
  374. package/app/utils/password-utils.js +21 -0
  375. package/app/utils/phone-utils.js +22 -0
  376. package/app/utils/phone-utils.test.js +15 -0
  377. package/app/utils/product-utils.js +35 -0
  378. package/app/utils/product-utils.test.js +51 -0
  379. package/app/utils/responsive-image.js +198 -0
  380. package/app/utils/responsive-image.test.js +170 -0
  381. package/app/utils/routes-utils.js +111 -0
  382. package/app/utils/routes-utils.test.js +291 -0
  383. package/app/utils/site-utils.js +222 -0
  384. package/app/utils/site-utils.test.js +376 -0
  385. package/app/utils/test-utils.js +257 -0
  386. package/app/utils/url.js +291 -0
  387. package/app/utils/url.test.js +421 -0
  388. package/app/utils/utils.js +201 -0
  389. package/app/utils/utils.test.js +182 -0
  390. package/babel.config.js +7 -0
  391. package/cache-hash-config.json +8 -0
  392. package/config/default.js +64 -0
  393. package/config/mocks/default.js +131 -0
  394. package/config/sites.js +78 -0
  395. package/jest-setup.js +191 -0
  396. package/jest.config.js +50 -0
  397. package/jsconfig.json +13 -0
  398. package/package.json +105 -0
  399. package/scripts/extract-default-messages.js +92 -0
  400. package/tests/lighthouserc.js +37 -0
  401. package/translations/README.md +127 -0
  402. package/translations/compiled/de-DE.json +3212 -0
  403. package/translations/compiled/en-GB.json +3212 -0
  404. package/translations/compiled/en-US.json +3212 -0
  405. package/translations/compiled/en-XA.json +6948 -0
  406. package/translations/compiled/es-MX.json +3216 -0
  407. package/translations/compiled/fr-FR.json +3216 -0
  408. package/translations/compiled/it-IT.json +3188 -0
  409. package/translations/compiled/ja-JP.json +3200 -0
  410. package/translations/compiled/ko-KR.json +3180 -0
  411. package/translations/compiled/pt-BR.json +3220 -0
  412. package/translations/compiled/zh-CN.json +3212 -0
  413. package/translations/compiled/zh-TW.json +3208 -0
  414. package/translations/de-DE.json +1417 -0
  415. package/translations/en-GB.json +1417 -0
  416. package/translations/en-US.json +1417 -0
  417. package/translations/es-MX.json +1417 -0
  418. package/translations/fr-FR.json +1417 -0
  419. package/translations/it-IT.json +1417 -0
  420. package/translations/ja-JP.json +1417 -0
  421. package/translations/ko-KR.json +1417 -0
  422. package/translations/pt-BR.json +1417 -0
  423. package/translations/zh-CN.json +1417 -0
  424. package/translations/zh-TW.json +1417 -0
  425. package/worker/main.js +36 -0
@@ -0,0 +1,735 @@
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, {useEffect, useState} from 'react'
9
+ import PropTypes from 'prop-types'
10
+ import {useHistory, useLocation, useParams} from 'react-router-dom'
11
+ import {FormattedMessage, useIntl} from 'react-intl'
12
+ import {Helmet} from 'react-helmet'
13
+ import {
14
+ useCategory,
15
+ useCustomerId,
16
+ useProductSearch,
17
+ useShopperCustomersMutation
18
+ } from '@salesforce/commerce-sdk-react'
19
+ import {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
20
+
21
+ // Components
22
+ import {
23
+ Box,
24
+ Flex,
25
+ SimpleGrid,
26
+ Grid,
27
+ Select,
28
+ Text,
29
+ FormControl,
30
+ Stack,
31
+ useDisclosure,
32
+ Button,
33
+ Modal,
34
+ ModalHeader,
35
+ ModalBody,
36
+ ModalFooter,
37
+ ModalContent,
38
+ ModalCloseButton,
39
+ ModalOverlay,
40
+ Drawer,
41
+ DrawerBody,
42
+ DrawerHeader,
43
+ DrawerOverlay,
44
+ DrawerContent,
45
+ DrawerCloseButton
46
+ } from '@chakra-ui/react'
47
+
48
+ // Project Components
49
+ import Pagination from '@salesforce/retail-react-app/app/components/pagination'
50
+ import ProductTile, {
51
+ Skeleton as ProductTileSkeleton
52
+ } from '@salesforce/retail-react-app/app/components/product-tile'
53
+ import {HideOnDesktop} from '@salesforce/retail-react-app/app/components/responsive'
54
+ import Refinements from '@salesforce/retail-react-app/app/pages/product-list/partials/refinements'
55
+ import SelectedRefinements from '@salesforce/retail-react-app/app/pages/product-list/partials/selected-refinements'
56
+ import EmptySearchResults from '@salesforce/retail-react-app/app/pages/product-list/partials/empty-results'
57
+ import PageHeader from '@salesforce/retail-react-app/app/pages/product-list/partials/page-header'
58
+ import AbovePageHeader from '@salesforce/retail-react-app/app/pages/product-list/partials/above-page-header'
59
+
60
+ // Icons
61
+ import {FilterIcon, ChevronDownIcon} from '@salesforce/retail-react-app/app/components/icons'
62
+
63
+ // Hooks
64
+ import {
65
+ useLimitUrls,
66
+ usePageUrls,
67
+ useSortUrls,
68
+ useSearchParams
69
+ } from '@salesforce/retail-react-app/app/hooks'
70
+ import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
71
+ // import {parse as parseSearchParams} from '../../hooks/use-search-params'
72
+ import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
73
+
74
+ // Others
75
+ import {HTTPNotFound, HTTPError} from '@salesforce/pwa-kit-react-sdk/ssr/universal/errors'
76
+
77
+ // Constants
78
+ import {
79
+ DEFAULT_LIMIT_VALUES,
80
+ API_ERROR_MESSAGE,
81
+ MAX_CACHE_AGE,
82
+ TOAST_ACTION_VIEW_WISHLIST,
83
+ TOAST_MESSAGE_ADDED_TO_WISHLIST,
84
+ TOAST_MESSAGE_REMOVED_FROM_WISHLIST
85
+ } from '@salesforce/retail-react-app/app/constants'
86
+ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
87
+ import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
88
+ import {useWishList} from '@salesforce/retail-react-app/app/hooks/use-wish-list'
89
+ import {isHydrated} from '@salesforce/retail-react-app/app/utils/utils'
90
+
91
+ // NOTE: You can ignore certain refinements on a template level by updating the below
92
+ // list of ignored refinements.
93
+ const REFINEMENT_DISALLOW_LIST = ['c_isNew']
94
+
95
+ /*
96
+ * This is a simple product listing page. It displays a paginated list
97
+ * of product hit objects. Allowing for sorting and filtering based on the
98
+ * allowable filters and sort refinements.
99
+ */
100
+ const ProductList = (props) => {
101
+ // Using destructuring to omit properties; we must rename `isLoading` because we use a different
102
+ // `isLoading` later in this function.
103
+ // eslint-disable-next-line react/prop-types, @typescript-eslint/no-unused-vars
104
+ const {isLoading: _unusedIsLoading, staticContext, ...rest} = props
105
+ const {isOpen, onOpen, onClose} = useDisclosure()
106
+ const {formatMessage} = useIntl()
107
+ const navigate = useNavigation()
108
+ const history = useHistory()
109
+ const params = useParams()
110
+ const location = useLocation()
111
+ const toast = useToast()
112
+ const einstein = useEinstein()
113
+ const {res} = useServerContext()
114
+ const customerId = useCustomerId()
115
+ const [searchParams, {stringify: stringifySearchParams}] = useSearchParams()
116
+
117
+ /**************** Page State ****************/
118
+ const [filtersLoading, setFiltersLoading] = useState(false)
119
+ const [wishlistLoading, setWishlistLoading] = useState([])
120
+ const [sortOpen, setSortOpen] = useState(false)
121
+
122
+ const urlParams = new URLSearchParams(location.search)
123
+ let searchQuery = urlParams.get('q')
124
+ const isSearch = !!searchQuery
125
+
126
+ if (params.categoryId) {
127
+ searchParams._refine.push(`cgid=${params.categoryId}`)
128
+ }
129
+
130
+ /**************** Mutation Actions ****************/
131
+ const {mutateAsync: createCustomerProductListItem} = useShopperCustomersMutation(
132
+ 'createCustomerProductListItem'
133
+ )
134
+ const {mutateAsync: deleteCustomerProductListItem} = useShopperCustomersMutation(
135
+ 'deleteCustomerProductListItem'
136
+ )
137
+
138
+ /**************** Query Actions ****************/
139
+ const {
140
+ isLoading,
141
+ isRefetching,
142
+ data: productSearchResult
143
+ } = useProductSearch(
144
+ {
145
+ parameters: {
146
+ ...searchParams,
147
+ refine: searchParams._refine
148
+ }
149
+ },
150
+ {
151
+ keepPreviousData: true
152
+ }
153
+ )
154
+
155
+ const {error, data: category} = useCategory(
156
+ {
157
+ parameters: {
158
+ id: params.categoryId
159
+ }
160
+ },
161
+ {
162
+ enabled: !isSearch && !!params.categoryId
163
+ }
164
+ )
165
+
166
+ // Apply disallow list to refinements.
167
+ if (productSearchResult?.refinements) {
168
+ productSearchResult.refinements = productSearchResult.refinements.filter(
169
+ ({attributeId}) => !REFINEMENT_DISALLOW_LIST.includes(attributeId)
170
+ )
171
+ }
172
+
173
+ /**************** Error Handling ****************/
174
+ const errorStatus = error?.response?.status
175
+ switch (errorStatus) {
176
+ case undefined:
177
+ // No Error.
178
+ break
179
+ case 404:
180
+ throw new HTTPNotFound('Category Not Found.')
181
+ default:
182
+ throw new HTTPError(`HTTP Error ${errorStatus} occurred.`)
183
+ }
184
+
185
+ /**************** Response Handling ****************/
186
+ if (res) {
187
+ res.set('Cache-Control', `max-age=${MAX_CACHE_AGE}`)
188
+ }
189
+
190
+ // Reset scroll position when `isRefetching` becomes `true`.
191
+ useEffect(() => {
192
+ isRefetching && window.scrollTo(0, 0)
193
+ setFiltersLoading(isRefetching)
194
+ }, [isRefetching])
195
+
196
+ /**************** Render Variables ****************/
197
+ const basePath = `${location.pathname}${location.search}`
198
+ const showNoResults = !isLoading && productSearchResult && !productSearchResult?.hits
199
+ const {total, sortingOptions} = productSearchResult || {}
200
+ const selectedSortingOptionLabel =
201
+ sortingOptions?.find(
202
+ (option) => option.id === productSearchResult?.selectedSortingOption
203
+ ) ?? sortingOptions?.[0]
204
+
205
+ // Get urls to be used for pagination, page size changes, and sorting.
206
+ const pageUrls = usePageUrls({total})
207
+ const sortUrls = useSortUrls({options: sortingOptions})
208
+ const limitUrls = useLimitUrls()
209
+
210
+ /**************** Action Handlers ****************/
211
+ const {data: wishlist} = useWishList()
212
+ const addItemToWishlist = async (product) => {
213
+ setWishlistLoading([...wishlistLoading, product.productId])
214
+
215
+ // TODO: This wishlist object is from an old API, we need to replace it with the new one.
216
+ const listId = wishlist.id
217
+ await createCustomerProductListItem(
218
+ {
219
+ parameters: {customerId, listId},
220
+ body: {
221
+ quantity: 1,
222
+ public: false,
223
+ priority: 1,
224
+ type: 'product',
225
+ productId: product.productId
226
+ }
227
+ },
228
+ {
229
+ onError: () => {
230
+ toast({
231
+ title: formatMessage(API_ERROR_MESSAGE),
232
+ status: 'error'
233
+ })
234
+ },
235
+ onSuccess: () => {
236
+ toast({
237
+ title: formatMessage(TOAST_MESSAGE_ADDED_TO_WISHLIST, {quantity: 1}),
238
+ status: 'success',
239
+ action: (
240
+ // it would be better if we could use <Button as={Link}>
241
+ // but unfortunately the Link component is not compatible
242
+ // with Chakra Toast, since the ToastManager is rendered via portal
243
+ // and the toast doesn't have access to intl provider, which is a
244
+ // requirement of the Link component.
245
+ <Button variant="link" onClick={() => navigate('/account/wishlist')}>
246
+ {formatMessage(TOAST_ACTION_VIEW_WISHLIST)}
247
+ </Button>
248
+ )
249
+ })
250
+ },
251
+ onSettled: () => {
252
+ setWishlistLoading(wishlistLoading.filter((id) => id !== product.productId))
253
+ }
254
+ }
255
+ )
256
+ }
257
+
258
+ const removeItemFromWishlist = async (product) => {
259
+ setWishlistLoading([...wishlistLoading, product.productId])
260
+
261
+ const listId = wishlist.id
262
+ const itemId = wishlist.customerProductListItems.find(
263
+ (i) => i.productId === product.productId
264
+ ).id
265
+
266
+ await deleteCustomerProductListItem(
267
+ {
268
+ body: {},
269
+ parameters: {customerId, listId, itemId}
270
+ },
271
+ {
272
+ onError: () => {
273
+ toast({
274
+ title: formatMessage(API_ERROR_MESSAGE),
275
+ status: 'error'
276
+ })
277
+ },
278
+ onSuccess: () => {
279
+ toast({
280
+ title: formatMessage(TOAST_MESSAGE_REMOVED_FROM_WISHLIST),
281
+ status: 'success'
282
+ })
283
+ },
284
+ onSettled: () => {
285
+ setWishlistLoading(wishlistLoading.filter((id) => id !== product.productId))
286
+ }
287
+ }
288
+ )
289
+ }
290
+
291
+ // Toggles filter on and off
292
+ const toggleFilter = (value, attributeId, selected, allowMultiple = true) => {
293
+ const searchParamsCopy = {...searchParams}
294
+
295
+ // Remove the `offset` search param if present.
296
+ delete searchParamsCopy.offset
297
+
298
+ // If we aren't allowing for multiple selections, simply clear any value set for the
299
+ // attribute, and apply a new one if required.
300
+ if (!allowMultiple) {
301
+ const previousValue = searchParamsCopy.refine[attributeId]
302
+ delete searchParamsCopy.refine[attributeId]
303
+
304
+ // Note the loose comparison, for "string != number" checks.
305
+ if (!selected && value.value != previousValue) {
306
+ searchParamsCopy.refine[attributeId] = value.value
307
+ }
308
+ } else {
309
+ // Get the attibute value as an array.
310
+ let attributeValue = searchParamsCopy.refine[attributeId] || []
311
+
312
+ // Ensure that the value is still converted into an array if it's a `string` or `number`.
313
+ if (typeof attributeValue === 'string') {
314
+ attributeValue = attributeValue.split('|')
315
+ } else if (typeof attributeValue === 'number') {
316
+ attributeValue = [attributeValue]
317
+ }
318
+
319
+ // Either set the value, or filter the value out.
320
+ if (!selected) {
321
+ attributeValue.push(value.value)
322
+ } else {
323
+ // Note the loose comparison, for "string != number" checks.
324
+ attributeValue = attributeValue?.filter((v) => v != value.value)
325
+ }
326
+
327
+ // Update the attribute value in the new search params.
328
+ searchParamsCopy.refine[attributeId] = attributeValue
329
+
330
+ // If the update value is an empty array, remove the current attribute key.
331
+ if (searchParamsCopy.refine[attributeId].length === 0) {
332
+ delete searchParamsCopy.refine[attributeId]
333
+ }
334
+ }
335
+
336
+ if (isSearch) {
337
+ navigate(`/search?${stringifySearchParams(searchParamsCopy)}`)
338
+ } else {
339
+ navigate(`/category/${params.categoryId}?${stringifySearchParams(searchParamsCopy)}`)
340
+ }
341
+ }
342
+
343
+ // Clears all filters
344
+ const resetFilters = () => {
345
+ const newSearchParams = {
346
+ ...searchParams,
347
+ refine: []
348
+ }
349
+ const newPath = isSearch
350
+ ? `/search?${stringifySearchParams(newSearchParams)}`
351
+ : `/category/${params.categoryId}?${stringifySearchParams(newSearchParams)}`
352
+
353
+ navigate(newPath)
354
+ }
355
+
356
+ /**************** Einstein ****************/
357
+ useEffect(() => {
358
+ if (productSearchResult) {
359
+ isSearch
360
+ ? einstein.sendViewSearch(searchQuery, productSearchResult)
361
+ : einstein.sendViewCategory(category, productSearchResult)
362
+ }
363
+ }, [productSearchResult])
364
+
365
+ return (
366
+ <Box
367
+ className="sf-product-list-page"
368
+ data-testid="sf-product-list-page"
369
+ layerStyle="page"
370
+ paddingTop={{base: 6, lg: 8}}
371
+ {...rest}
372
+ >
373
+ <Helmet>
374
+ <title>{category?.pageTitle}</title>
375
+ <meta name="description" content={category?.pageDescription} />
376
+ <meta name="keywords" content={category?.pageKeywords} />
377
+ </Helmet>
378
+ {showNoResults ? (
379
+ <EmptySearchResults searchQuery={searchQuery} category={category} />
380
+ ) : (
381
+ <>
382
+ <AbovePageHeader />
383
+ {/* Header */}
384
+ <Stack
385
+ display={{base: 'none', lg: 'flex'}}
386
+ direction="row"
387
+ justify="flex-start"
388
+ align="flex-start"
389
+ spacing={4}
390
+ marginBottom={6}
391
+ >
392
+ <Flex align="left" width="287px">
393
+ <PageHeader
394
+ searchQuery={searchQuery}
395
+ category={category}
396
+ productSearchResult={productSearchResult}
397
+ isLoading={isLoading}
398
+ />
399
+ </Flex>
400
+
401
+ <Box flex={1} paddingTop={'45px'}>
402
+ <SelectedRefinements
403
+ filters={productSearchResult?.refinements}
404
+ toggleFilter={toggleFilter}
405
+ handleReset={() => resetFilters()}
406
+ selectedFilterValues={productSearchResult?.selectedRefinements}
407
+ />
408
+ </Box>
409
+ <Box paddingTop={'45px'}>
410
+ <Sort
411
+ sortUrls={sortUrls}
412
+ productSearchResult={productSearchResult}
413
+ basePath={basePath}
414
+ />
415
+ </Box>
416
+ </Stack>
417
+
418
+ <HideOnDesktop>
419
+ <Stack spacing={6}>
420
+ <PageHeader
421
+ searchQuery={searchQuery}
422
+ category={category}
423
+ productSearchResult={productSearchResult}
424
+ isLoading={isLoading}
425
+ />
426
+ <Stack
427
+ display={{base: 'flex', md: 'none'}}
428
+ direction="row"
429
+ justify="flex-start"
430
+ align="center"
431
+ spacing={1}
432
+ height={12}
433
+ borderColor="gray.100"
434
+ >
435
+ <Flex align="center">
436
+ <Button
437
+ fontSize="sm"
438
+ colorScheme="black"
439
+ variant="outline"
440
+ marginRight={2}
441
+ display="inline-flex"
442
+ leftIcon={<FilterIcon boxSize={5} />}
443
+ onClick={onOpen}
444
+ >
445
+ <FormattedMessage
446
+ defaultMessage="Filter"
447
+ id="product_list.button.filter"
448
+ />
449
+ </Button>
450
+ </Flex>
451
+ <Flex align="center">
452
+ <Button
453
+ maxWidth="245px"
454
+ fontSize="sm"
455
+ marginRight={2}
456
+ colorScheme="black"
457
+ variant="outline"
458
+ display="inline-flex"
459
+ rightIcon={<ChevronDownIcon boxSize={5} />}
460
+ onClick={() => setSortOpen(true)}
461
+ >
462
+ {formatMessage(
463
+ {
464
+ id: 'product_list.button.sort_by',
465
+ defaultMessage: 'Sort By: {sortOption}'
466
+ },
467
+ {
468
+ sortOption: selectedSortingOptionLabel?.label
469
+ }
470
+ )}
471
+ </Button>
472
+ </Flex>
473
+ </Stack>
474
+ </Stack>
475
+ <Box marginBottom={4}>
476
+ <SelectedRefinements
477
+ filters={productSearchResult?.refinements}
478
+ toggleFilter={toggleFilter}
479
+ selectedFilterValues={productSearchResult?.selectedRefinements}
480
+ />
481
+ </Box>
482
+ </HideOnDesktop>
483
+
484
+ {/* Body */}
485
+ <Grid templateColumns={{base: '1fr', md: '280px 1fr'}} columnGap={6}>
486
+ <Stack display={{base: 'none', md: 'flex'}}>
487
+ <Refinements
488
+ isLoading={filtersLoading}
489
+ toggleFilter={toggleFilter}
490
+ filters={productSearchResult?.refinements}
491
+ selectedFilters={searchParams.refine}
492
+ />
493
+ </Stack>
494
+ <Box>
495
+ <SimpleGrid
496
+ columns={[2, 2, 3, 3]}
497
+ spacingX={4}
498
+ spacingY={{base: 12, lg: 16}}
499
+ >
500
+ {isHydrated() && (isRefetching || !productSearchResult)
501
+ ? new Array(searchParams.limit)
502
+ .fill(0)
503
+ .map((value, index) => (
504
+ <ProductTileSkeleton key={index} />
505
+ ))
506
+ : productSearchResult.hits.map((productSearchItem) => {
507
+ const productId = productSearchItem.productId
508
+ const isInWishlist =
509
+ !!wishlist?.customerProductListItems?.find(
510
+ (item) => item.productId === productId
511
+ )
512
+
513
+ return (
514
+ <ProductTile
515
+ data-testid={`sf-product-tile-${productSearchItem.productId}`}
516
+ key={productSearchItem.productId}
517
+ product={productSearchItem}
518
+ enableFavourite={true}
519
+ isFavourite={isInWishlist}
520
+ onClick={() => {
521
+ if (searchQuery) {
522
+ einstein.sendClickSearch(
523
+ searchQuery,
524
+ productSearchItem
525
+ )
526
+ } else if (category) {
527
+ einstein.sendClickCategory(
528
+ category,
529
+ productSearchItem
530
+ )
531
+ }
532
+ }}
533
+ onFavouriteToggle={(isFavourite) => {
534
+ const action = isFavourite
535
+ ? addItemToWishlist
536
+ : removeItemFromWishlist
537
+ return action(productSearchItem)
538
+ }}
539
+ dynamicImageProps={{
540
+ widths: [
541
+ '50vw',
542
+ '50vw',
543
+ '20vw',
544
+ '20vw',
545
+ '25vw'
546
+ ]
547
+ }}
548
+ />
549
+ )
550
+ })}
551
+ </SimpleGrid>
552
+ {/* Footer */}
553
+ <Flex
554
+ justifyContent={['center', 'center', 'flex-start']}
555
+ paddingTop={8}
556
+ >
557
+ <Pagination currentURL={basePath} urls={pageUrls} />
558
+
559
+ {/*
560
+ Our design doesn't call for a page size select. Show this element if you want
561
+ to add one to your design.
562
+ */}
563
+ <Select
564
+ display="none"
565
+ value={basePath}
566
+ onChange={({target}) => {
567
+ history.push(target.value)
568
+ }}
569
+ >
570
+ {limitUrls.map((href, index) => (
571
+ <option key={href} value={href}>
572
+ {DEFAULT_LIMIT_VALUES[index]}
573
+ </option>
574
+ ))}
575
+ </Select>
576
+ </Flex>
577
+ </Box>
578
+ </Grid>
579
+ </>
580
+ )}
581
+ <Modal
582
+ isOpen={isOpen}
583
+ onClose={onClose}
584
+ size="full"
585
+ motionPreset="slideInBottom"
586
+ scrollBehavior="inside"
587
+ >
588
+ <ModalOverlay />
589
+ <ModalContent top={0} marginTop={0}>
590
+ <ModalHeader>
591
+ <Text fontWeight="bold" fontSize="2xl">
592
+ <FormattedMessage
593
+ defaultMessage="Filter"
594
+ id="product_list.modal.title.filter"
595
+ />
596
+ </Text>
597
+ </ModalHeader>
598
+ <ModalCloseButton />
599
+ <ModalBody py={4}>
600
+ {filtersLoading && <LoadingSpinner />}
601
+ <Refinements
602
+ toggleFilter={toggleFilter}
603
+ filters={productSearchResult?.refinements}
604
+ selectedFilters={productSearchResult?.selectedRefinements}
605
+ />
606
+ </ModalBody>
607
+
608
+ <ModalFooter
609
+ // justify="space-between"
610
+ display="block"
611
+ width="full"
612
+ borderTop="1px solid"
613
+ borderColor="gray.100"
614
+ paddingBottom={10}
615
+ >
616
+ <Stack>
617
+ <Button width="full" onClick={onClose}>
618
+ {formatMessage(
619
+ {
620
+ id: 'product_list.modal.button.view_items',
621
+ defaultMessage: 'View {prroductCount} items'
622
+ },
623
+ {
624
+ prroductCount: productSearchResult?.total
625
+ }
626
+ )}
627
+ </Button>
628
+ <Button width="full" variant="outline" onClick={resetFilters}>
629
+ <FormattedMessage
630
+ defaultMessage="Clear Filters"
631
+ id="product_list.modal.button.clear_filters"
632
+ />
633
+ </Button>
634
+ </Stack>
635
+ </ModalFooter>
636
+ </ModalContent>
637
+ </Modal>
638
+ <Drawer
639
+ placement="bottom"
640
+ isOpen={sortOpen}
641
+ onClose={() => setSortOpen(false)}
642
+ size="sm"
643
+ motionPreset="slideInBottom"
644
+ scrollBehavior="inside"
645
+ isFullHeight={false}
646
+ height="50%"
647
+ >
648
+ <DrawerOverlay />
649
+ <DrawerContent marginTop={0}>
650
+ <DrawerHeader boxShadow="none">
651
+ <Text fontWeight="bold" fontSize="2xl">
652
+ <FormattedMessage
653
+ defaultMessage="Sort By"
654
+ id="product_list.drawer.title.sort_by"
655
+ />
656
+ </Text>
657
+ </DrawerHeader>
658
+ <DrawerCloseButton />
659
+ <DrawerBody>
660
+ {sortUrls.map((href, idx) => (
661
+ <Button
662
+ width="full"
663
+ onClick={() => {
664
+ setSortOpen(false)
665
+ history.push(href)
666
+ }}
667
+ fontSize={'md'}
668
+ key={idx}
669
+ marginTop={0}
670
+ variant="menu-link"
671
+ >
672
+ <Text
673
+ as={
674
+ selectedSortingOptionLabel?.label ===
675
+ productSearchResult?.sortingOptions[idx]?.label && 'u'
676
+ }
677
+ >
678
+ {productSearchResult?.sortingOptions[idx]?.label}
679
+ </Text>
680
+ </Button>
681
+ ))}
682
+ </DrawerBody>
683
+ </DrawerContent>
684
+ </Drawer>
685
+ </Box>
686
+ )
687
+ }
688
+
689
+ ProductList.getTemplateName = () => 'product-list'
690
+
691
+ ProductList.propTypes = {
692
+ onAddToWishlistClick: PropTypes.func,
693
+ onRemoveWishlistClick: PropTypes.func,
694
+ category: PropTypes.object
695
+ }
696
+
697
+ export default ProductList
698
+
699
+ const Sort = ({sortUrls, productSearchResult, basePath, ...otherProps}) => {
700
+ const intl = useIntl()
701
+ const history = useHistory()
702
+
703
+ return (
704
+ <FormControl data-testid="sf-product-list-sort" id="page_sort" width="auto" {...otherProps}>
705
+ <Select
706
+ value={basePath.replace(/(offset)=(\d+)/i, '$1=0')}
707
+ onChange={({target}) => {
708
+ history.push(target.value)
709
+ }}
710
+ height={11}
711
+ width="240px"
712
+ >
713
+ {sortUrls.map((href, index) => (
714
+ <option key={href} value={href}>
715
+ {intl.formatMessage(
716
+ {
717
+ id: 'product_list.select.sort_by',
718
+ defaultMessage: 'Sort By: {sortOption}'
719
+ },
720
+ {
721
+ sortOption: productSearchResult?.sortingOptions[index]?.label
722
+ }
723
+ )}
724
+ </option>
725
+ ))}
726
+ </Select>
727
+ </FormControl>
728
+ )
729
+ }
730
+
731
+ Sort.propTypes = {
732
+ sortUrls: PropTypes.array,
733
+ productSearchResult: PropTypes.object,
734
+ basePath: PropTypes.string
735
+ }