@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,421 @@
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
+
8
+ import {
9
+ buildUrlSet,
10
+ categoryUrlBuilder,
11
+ productUrlBuilder,
12
+ searchUrlBuilder,
13
+ getPathWithLocale,
14
+ rebuildPathWithParams,
15
+ removeQueryParamsFromPath,
16
+ absoluteUrl,
17
+ createUrlTemplate,
18
+ removeSiteLocaleFromPath
19
+ } from '@salesforce/retail-react-app/app/utils/url'
20
+ import {getUrlConfig} from '@salesforce/retail-react-app/app/utils/site-utils'
21
+ import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
22
+
23
+ afterEach(() => {
24
+ jest.clearAllMocks()
25
+ })
26
+
27
+ jest.mock('@salesforce/pwa-kit-react-sdk/utils/url', () => {
28
+ const original = jest.requireActual('@salesforce/pwa-kit-react-sdk/utils/url')
29
+ return {
30
+ ...original,
31
+ getAppOrigin: jest.fn(() => 'https://www.example.com')
32
+ }
33
+ })
34
+ jest.mock('./utils', () => {
35
+ const original = jest.requireActual('./utils')
36
+ return {
37
+ ...original,
38
+ getConfig: jest.fn(() => mockConfig)
39
+ }
40
+ })
41
+
42
+ jest.mock('./site-utils', () => {
43
+ const original = jest.requireActual('./site-utils')
44
+ return {
45
+ ...original,
46
+ getUrlConfig: jest.fn()
47
+ }
48
+ })
49
+
50
+ describe('buildUrlSet returns the expected set of urls', () => {
51
+ test('when no values are passed in', () => {
52
+ const set = buildUrlSet()
53
+
54
+ expect(set).toEqual([])
55
+ })
56
+
57
+ test('when the values array is not empty', () => {
58
+ const set = buildUrlSet('/mens/clothing', 'offset', [0, 5, 10])
59
+
60
+ expect(set).toEqual([
61
+ '/mens/clothing?offset=0',
62
+ '/mens/clothing?offset=5',
63
+ '/mens/clothing?offset=10'
64
+ ])
65
+ })
66
+
67
+ test('when the values array is empty', () => {
68
+ const set = buildUrlSet('/mens/clothing', 'offset', [])
69
+
70
+ expect(set).toEqual([])
71
+ })
72
+
73
+ test('when extra parameters are provided', () => {
74
+ const set = buildUrlSet('/mens/clothing', 'offset', [0, 5, 10], {sort: 'high-to-low'})
75
+
76
+ expect(set).toEqual([
77
+ '/mens/clothing?offset=0&sort=high-to-low',
78
+ '/mens/clothing?offset=5&sort=high-to-low',
79
+ '/mens/clothing?offset=10&sort=high-to-low'
80
+ ])
81
+ })
82
+
83
+ test('when url has existing params', () => {
84
+ const set = buildUrlSet('/mens/clothing?sort=high-to-low', 'offset', [0, 5, 10])
85
+
86
+ expect(set).toEqual([
87
+ '/mens/clothing?sort=high-to-low&offset=0',
88
+ '/mens/clothing?sort=high-to-low&offset=5',
89
+ '/mens/clothing?sort=high-to-low&offset=10'
90
+ ])
91
+ })
92
+
93
+ test('when valueless params are present', () => {
94
+ const set = buildUrlSet('/mens/clothing?server_only', 'offset', [0, 5, 10])
95
+
96
+ expect(set).toEqual([
97
+ '/mens/clothing?server_only&offset=0',
98
+ '/mens/clothing?server_only&offset=5',
99
+ '/mens/clothing?server_only&offset=10'
100
+ ])
101
+ })
102
+ })
103
+
104
+ describe('url builder test', () => {
105
+ // Save the original `window.location` object to not affect other test
106
+ const originalLocation = window.location
107
+
108
+ beforeEach(() => {
109
+ delete window.location
110
+ window.location = {...originalLocation, assign: jest.fn()}
111
+ })
112
+ afterEach(() => {
113
+ // Restore `window.location` to the `jsdom` `Location` object
114
+ window.location = originalLocation
115
+ })
116
+ test('searchUrlBuilder returns expect', () => {
117
+ const url = searchUrlBuilder('term')
118
+
119
+ expect(url).toBe('/search?q=term')
120
+ })
121
+
122
+ test('searchUrlBuilder returns expect with & symbol', () => {
123
+ const url = searchUrlBuilder('term&term')
124
+
125
+ expect(url).toBe('/search?q=term%26term')
126
+ })
127
+
128
+ test('productUrlBuilder returns expect', () => {
129
+ const url = productUrlBuilder({id: 'productId'})
130
+
131
+ expect(url).toBe('/product/productId')
132
+ })
133
+
134
+ test('categoryUrlBuilder returns expect', () => {
135
+ const url = categoryUrlBuilder({id: 'men'})
136
+ expect(url).toBe(`/category/men`)
137
+ })
138
+ })
139
+
140
+ describe('getPathWithLocale', () => {
141
+ getUrlConfig.mockImplementation(() => mockConfig.app.url)
142
+
143
+ test('getPathWithLocale returns expected for PLP', () => {
144
+ const location = new URL('http://localhost:3000/uk/it-IT/category/newarrivals-womens')
145
+ const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'it-IT')
146
+
147
+ const relativeUrl = getPathWithLocale('fr-FR', buildUrl, {location})
148
+ expect(relativeUrl).toBe(`/uk/fr/category/newarrivals-womens`)
149
+ })
150
+
151
+ test('getPathWithLocale uses default site for siteRef when it is no defined in the url', () => {
152
+ const location = new URL('http://localhost:3000/category/newarrivals-womens')
153
+ const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'it-IT')
154
+
155
+ const relativeUrl = getPathWithLocale('fr-FR', buildUrl, {location})
156
+ expect(relativeUrl).toBe(`/uk/fr/category/newarrivals-womens`)
157
+ })
158
+
159
+ test('getPathWithLocale returns expected for PLP without refine param', () => {
160
+ const location = new URL(
161
+ 'http://localhost:3000/uk/it-IT/category/newarrivals-womens?limit=25&refine=c_refinementColor%3DBianco&sort=best-matches&offset=25'
162
+ )
163
+ const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'it-IT')
164
+
165
+ const relativeUrl = getPathWithLocale('fr-FR', buildUrl, {
166
+ disallowParams: ['refine'],
167
+ location
168
+ })
169
+ expect(relativeUrl).toBe(
170
+ `/uk/fr/category/newarrivals-womens?limit=25&sort=best-matches&offset=25`
171
+ )
172
+ })
173
+
174
+ test('getPathWithLocale returns expected for Homepage', () => {
175
+ const location = new URL('http://localhost:3000/uk/it-IT/')
176
+ const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'it-IT')
177
+
178
+ const relativeUrl = getPathWithLocale('fr-FR', buildUrl, {location})
179
+ expect(relativeUrl).toBe(`/uk/fr/`)
180
+ })
181
+
182
+ test('getPathWithLocale returns / when both site and locale are default', () => {
183
+ const location = new URL('http://localhost:3000/')
184
+ const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'en-GB')
185
+
186
+ const relativeUrl = getPathWithLocale('en-GB', buildUrl, {location})
187
+ expect(relativeUrl).toBe(`/`)
188
+ })
189
+ })
190
+
191
+ describe('createUrlTemplate tests', () => {
192
+ const defaultSite = mockConfig.app.sites[0]
193
+ const defaultAlias = mockConfig.app.siteAliases[defaultSite.id]
194
+ const defaultSiteMock = {...defaultSite, alias: defaultAlias}
195
+
196
+ const nonDefaultSite = mockConfig.app.sites[1]
197
+ const nonDefaultAlias = mockConfig.app.siteAliases[nonDefaultSite.id]
198
+ const nonDefaultSiteMock = {...nonDefaultSite, alias: nonDefaultAlias}
199
+
200
+ const configValues = ['path', 'query_param', 'none']
201
+
202
+ let cases = []
203
+ for (let i = 0; i < configValues.length; i++) {
204
+ for (let j = 0; j < configValues.length; j++) {
205
+ for (let showDefaultsValues = 0; showDefaultsValues < 2; showDefaultsValues++) {
206
+ if (showDefaultsValues === 0) {
207
+ cases.push({
208
+ urlConfig: {
209
+ locale: configValues[i],
210
+ site: configValues[j],
211
+ showDefaults: true
212
+ },
213
+ site: defaultSiteMock,
214
+ locale: {id: 'en-GB'}
215
+ })
216
+ } else {
217
+ for (let isDefaultSite = 0; isDefaultSite < 2; isDefaultSite++) {
218
+ for (let isDefaultLocale = 0; isDefaultLocale < 2; isDefaultLocale++) {
219
+ if (isDefaultSite === 0) {
220
+ cases.push({
221
+ urlConfig: {
222
+ locale: configValues[i],
223
+ site: configValues[j],
224
+ showDefaults: false
225
+ },
226
+ site: defaultSiteMock,
227
+ locale:
228
+ isDefaultLocale === 0
229
+ ? {id: 'en-GB'}
230
+ : {id: 'fr-FR', alias: 'fr'}
231
+ })
232
+ } else {
233
+ cases.push({
234
+ urlConfig: {
235
+ locale: configValues[i],
236
+ site: configValues[j],
237
+ showDefaults: false
238
+ },
239
+ site: nonDefaultSiteMock,
240
+ locale:
241
+ isDefaultLocale === 0
242
+ ? {id: 'en-US'}
243
+ : {id: 'fr-FR', alias: 'fr'}
244
+ })
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ const paths = ['/testpath', '/']
254
+ const expectedResults = (path) => {
255
+ return path !== '/'
256
+ ? [
257
+ `/uk/en-GB${path}`,
258
+ `${path}`,
259
+ `/fr${path}`,
260
+ `/us${path}`,
261
+ `/us/fr${path}`,
262
+ `/en-GB${path}?site=uk`,
263
+ `${path}`,
264
+ `/fr${path}`,
265
+ `${path}?site=us`,
266
+ `/fr${path}?site=us`,
267
+ `/en-GB${path}`,
268
+ `${path}`,
269
+ `/fr${path}`,
270
+ `${path}`,
271
+ `/fr${path}`,
272
+ `/uk${path}?locale=en-GB`,
273
+ `${path}`,
274
+ `${path}?locale=fr`,
275
+ `/us${path}`,
276
+ `/us${path}?locale=fr`,
277
+ `${path}?site=uk&locale=en-GB`,
278
+ `${path}`,
279
+ `${path}?locale=fr`,
280
+ `${path}?site=us`,
281
+ `${path}?site=us&locale=fr`,
282
+ `${path}?locale=en-GB`,
283
+ `${path}`,
284
+ `${path}?locale=fr`,
285
+ `${path}`,
286
+ `${path}?locale=fr`,
287
+ `/uk${path}`,
288
+ `${path}`,
289
+ `${path}`,
290
+ `/us${path}`,
291
+ `/us${path}`,
292
+ `${path}?site=uk`,
293
+ `${path}`,
294
+ `${path}`,
295
+ `${path}?site=us`,
296
+ `${path}?site=us`,
297
+ `${path}`,
298
+ `${path}`,
299
+ `${path}`,
300
+ `${path}`,
301
+ `${path}`
302
+ ]
303
+ : [
304
+ `${path}`,
305
+ `${path}`,
306
+ `/fr${path}`,
307
+ `/us${path}`,
308
+ `/us/fr${path}`,
309
+ `${path}`,
310
+ `${path}`,
311
+ `/fr${path}`,
312
+ `${path}?site=us`,
313
+ `/fr${path}?site=us`,
314
+ `${path}`,
315
+ `${path}`,
316
+ `/fr${path}`,
317
+ `${path}`,
318
+ `/fr${path}`,
319
+ `${path}`,
320
+ `${path}`,
321
+ `${path}?locale=fr`,
322
+ `/us${path}`,
323
+ `/us${path}?locale=fr`,
324
+ `${path}`,
325
+ `${path}`,
326
+ `${path}?locale=fr`,
327
+ `${path}?site=us`,
328
+ `${path}?site=us&locale=fr`,
329
+ `${path}`,
330
+ `${path}`,
331
+ `${path}?locale=fr`,
332
+ `${path}`,
333
+ `${path}?locale=fr`,
334
+ `${path}`,
335
+ `${path}`,
336
+ `${path}`,
337
+ `/us${path}`,
338
+ `/us${path}`,
339
+ `${path}`,
340
+ `${path}`,
341
+ `${path}`,
342
+ `${path}?site=us`,
343
+ `${path}?site=us`,
344
+ `${path}`,
345
+ `${path}`,
346
+ `${path}`,
347
+ `${path}`,
348
+ `${path}`
349
+ ]
350
+ }
351
+ paths.forEach((path) => {
352
+ cases.forEach(({urlConfig, site, locale}, index) => {
353
+ test(`URL template path:${path}, site:${site.alias}, locale.id:${locale.id}${
354
+ locale?.alias ? `, locale.alias:${locale.alias}` : ''
355
+ } and urlConfig:${JSON.stringify(urlConfig)}`, () => {
356
+ const buildUrl = createUrlTemplate(
357
+ {url: urlConfig},
358
+ site.id,
359
+ locale?.alias || locale?.id
360
+ )
361
+ const resultUrl = buildUrl(
362
+ path,
363
+ mockConfig.app.siteAliases[site.id],
364
+ locale?.alias || locale?.id
365
+ )
366
+
367
+ expect(resultUrl).toEqual(expectedResults(path)[index])
368
+ })
369
+ })
370
+ })
371
+ })
372
+
373
+ describe('rebuildPathWithParams test', () => {
374
+ test('returns updated url', () => {
375
+ const url = '/en/product/25501032M?color=black&size=M'
376
+ const updatedUrl = rebuildPathWithParams(url, {pid: undefined})
377
+ expect(updatedUrl).toBe('/en/product/25501032M?color=black&size=M')
378
+ })
379
+ })
380
+
381
+ describe('removeQueryParamsFromPath test', () => {
382
+ test('returns updated url', () => {
383
+ const url = '/en/product/25501032M?color=black&size=M&something=123'
384
+ const updatedUrl = removeQueryParamsFromPath(url, ['color', 'size'])
385
+ expect(updatedUrl).toBe('/en/product/25501032M?something=123')
386
+ })
387
+ })
388
+
389
+ describe('absoluteUrl', function () {
390
+ test('return expected when path is a relative url', () => {
391
+ const url = absoluteUrl('/uk/en/women/dresses')
392
+ expect(url).toBe('https://www.example.com/uk/en/women/dresses')
393
+ })
394
+
395
+ test('return expected when path is an absolute url', () => {
396
+ const url = absoluteUrl('https://www.example.com/uk/en/women/dresses')
397
+ expect(url).toBe('https://www.example.com/uk/en/women/dresses')
398
+ })
399
+ })
400
+
401
+ describe('removeSiteLocaleFromPath', function () {
402
+ test('return path without site alias and locale', () => {
403
+ const pathName = removeSiteLocaleFromPath('/uk/en-GB/account/wishlist')
404
+ expect(pathName).toBe('/account/wishlist')
405
+ })
406
+
407
+ test('return path without site alias if they appear multiple times', () => {
408
+ const pathName = removeSiteLocaleFromPath('/uk/en-GB/uk/en-GB/account/wishlist')
409
+ expect(pathName).toBe('/account/wishlist')
410
+ })
411
+
412
+ test('return expected path name when no locale or site alias appear', () => {
413
+ const pathName = removeSiteLocaleFromPath('/account/wishlist')
414
+ expect(pathName).toBe('/account/wishlist')
415
+ })
416
+
417
+ test('return empty string when no path name is passed', () => {
418
+ const pathName = removeSiteLocaleFromPath()
419
+ expect(pathName).toBe('')
420
+ })
421
+ })
@@ -0,0 +1,201 @@
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
+
8
+ /**
9
+ * Call requestIdleCallback in supported browsers.
10
+ *
11
+ * https://developers.google.com/web/updates/2015/08/using-requestidlecallback
12
+ * http://caniuse.com/#feat=requestidlecallback
13
+ */
14
+ export const requestIdleCallback = (fn) => {
15
+ if ('requestIdleCallback' in window) {
16
+ return window.requestIdleCallback(fn)
17
+ } else {
18
+ return setTimeout(() => fn(), 1)
19
+ }
20
+ }
21
+
22
+ export const watchOnlineStatus = (callback, win = window) => {
23
+ const off = () => callback(false)
24
+ const on = () => callback(true)
25
+ win.addEventListener('offline', off)
26
+ win.addEventListener('online', on)
27
+ const unsubscribe = () => {
28
+ win.removeEventListener('offline', off)
29
+ win.removeEventListener('online', on)
30
+ }
31
+ return unsubscribe
32
+ }
33
+
34
+ /**
35
+ * Performs a shallow comparison on two objects
36
+ * @param {Object} a
37
+ * @param {Object} b
38
+ * @returns {boolean}
39
+ */
40
+ export const shallowEquals = (a, b) => {
41
+ for (let key in a) {
42
+ if (!(key in b) || a[key] !== b[key]) {
43
+ return false
44
+ }
45
+ }
46
+ for (let key in b) {
47
+ if (!(key in a) || a[key] !== b[key]) {
48
+ return false
49
+ }
50
+ }
51
+ return true
52
+ }
53
+
54
+ /**
55
+ * No operation function. You can use this
56
+ * empty function when you wish to pass
57
+ * around a function that will do nothing.
58
+ * Usually used as default for event handlers.
59
+ */
60
+ export const noop = () => {}
61
+
62
+ /**
63
+ * Flattens a tree data structure into an array.
64
+ * @param {*} node
65
+ * @returns
66
+ */
67
+ export const flatten = (node, key = 'children') => {
68
+ const children = (node[key] || []).reduce((a, b) => {
69
+ return Array.isArray(b[key]) && !!b[key].length
70
+ ? {...a, ...flatten(b, key)}
71
+ : {...a, [b.id]: b}
72
+ }, {})
73
+
74
+ return {
75
+ [node.id]: node,
76
+ ...children
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Check the current execution environment
82
+ * is client side or server side
83
+ * @returns Boolean
84
+ */
85
+ export const isServer = typeof window === 'undefined'
86
+
87
+ /**
88
+ * retrieves an item from session storage
89
+ * @param {string} key
90
+ * @returns JSON | undefined
91
+ */
92
+ export const getSessionJSONItem = (key) => {
93
+ if (isServer) {
94
+ return undefined
95
+ }
96
+ const item = window.sessionStorage.getItem(key)
97
+ if (item) {
98
+ return JSON.parse(item)
99
+ } else {
100
+ return undefined
101
+ }
102
+ }
103
+ /**
104
+ * sets an item in session storage
105
+ * @param {string} key
106
+ * @param {string} value
107
+ */
108
+ export const setSessionJSONItem = (key, value) => {
109
+ window.sessionStorage.setItem(key, JSON.stringify(value))
110
+ }
111
+
112
+ /**
113
+ * clears an item in session storage
114
+ * @param {string} key
115
+ */
116
+ export const clearSessionJSONItem = (key) => {
117
+ window.sessionStorage.removeItem(key)
118
+ }
119
+
120
+ /**
121
+ * escapes special regex characters
122
+ * @param {string} str
123
+ * @returns escaped string
124
+ */
125
+ export const escapeRegexChars = (str) => str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
126
+
127
+ /**
128
+ * bolds a substring of a string by adding <b> tags
129
+ * @param {string} str
130
+ * @param {string} substr
131
+ * @returns stringified HTML Node
132
+ */
133
+ export const boldString = (str, substr) => {
134
+ return str.replace(RegExp(escapeRegexChars(substr.trim()), 'gi'), '<b>$&</b>')
135
+ }
136
+
137
+ /**
138
+ * Capitalizes the words in a string
139
+ * @param {string} text
140
+ * @returns capitalized text
141
+ */
142
+ export const capitalize = (text) => {
143
+ return text
144
+ .toLowerCase()
145
+ .split(' ')
146
+ .map((s) => s.charAt(0).toUpperCase() + s.substring(1))
147
+ .join(' ')
148
+ }
149
+
150
+ const safeToCamel = (str) => {
151
+ if (str.startsWith('_') || str.startsWith('c_')) {
152
+ return str
153
+ }
154
+ return str.replace(/([-_][a-z])/gi, ($1) => {
155
+ return $1[1].toUpperCase()
156
+ })
157
+ }
158
+
159
+ const isPlainObject = (obj) => {
160
+ return obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function'
161
+ }
162
+
163
+ export const keysToCamel = (obj) => {
164
+ if (isPlainObject(obj)) {
165
+ const n = {}
166
+
167
+ Object.keys(obj).forEach((k) => {
168
+ n[safeToCamel(k)] = keysToCamel(obj[k])
169
+ })
170
+
171
+ return n
172
+ } else if (Array.isArray(obj)) {
173
+ return obj.map(keysToCamel)
174
+ }
175
+
176
+ return obj
177
+ }
178
+
179
+ /**
180
+ * Merge two arrays (arr1 and arr2) into one array
181
+ * and merge the matched items (objects with the same id)
182
+ * into one object. There is an assumption where arr2 is
183
+ * always a subset of arr1.
184
+ *
185
+ * @param arr1 - array of objects
186
+ * @param arr2
187
+ * @return {Array}
188
+ */
189
+ export const mergeMatchedItems = (arr1 = [], arr2 = []) => {
190
+ const merged = arr1.map((item) => {
191
+ const match = arr2.find((item2) => item2.id === item.id)
192
+ return match ? {...item, ...match} : item
193
+ })
194
+ return merged
195
+ }
196
+
197
+ /**
198
+ * This util func determines if the app is finished hydration
199
+ * @return {boolean}
200
+ */
201
+ export const isHydrated = () => typeof window !== 'undefined' && !window.__HYDRATING__