@magento/venia-pwa-live-search 1.0.0-alpha6

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 (202) hide show
  1. package/package.json +31 -0
  2. package/postcss.config.js +3 -0
  3. package/src/api/fragments.js +193 -0
  4. package/src/api/graphql.js +26 -0
  5. package/src/api/mutations.js +94 -0
  6. package/src/api/queries.js +225 -0
  7. package/src/api/search.js +222 -0
  8. package/src/components/AddToCartButton/AddToCartButton.jsx +32 -0
  9. package/src/components/AddToCartButton/AddToCartButton.stories.mdx +14 -0
  10. package/src/components/AddToCartButton/index.js +10 -0
  11. package/src/components/Alert/Alert.jsx +155 -0
  12. package/src/components/Alert/index.js +11 -0
  13. package/src/components/Breadcrumbs/Breadcrumbs.jsx +34 -0
  14. package/src/components/Breadcrumbs/MockPages.js +14 -0
  15. package/src/components/Breadcrumbs/index.js +11 -0
  16. package/src/components/ButtonShimmer/ButtonShimmer.css +32 -0
  17. package/src/components/ButtonShimmer/ButtonShimmer.jsx +23 -0
  18. package/src/components/ButtonShimmer/index.js +11 -0
  19. package/src/components/CategoryFilters/CategoryFilters.jsx +59 -0
  20. package/src/components/CategoryFilters/index.js +10 -0
  21. package/src/components/Facets/Facets.jsx +50 -0
  22. package/src/components/Facets/Range/RangeFacet.js +20 -0
  23. package/src/components/Facets/Scalar/ScalarFacet.js +29 -0
  24. package/src/components/Facets/SelectedFilters.js +80 -0
  25. package/src/components/Facets/format.js +52 -0
  26. package/src/components/Facets/index.js +14 -0
  27. package/src/components/Facets/mocks.js +119 -0
  28. package/src/components/FacetsShimmer/FacetsShimmer.css +49 -0
  29. package/src/components/FacetsShimmer/FacetsShimmer.jsx +25 -0
  30. package/src/components/FacetsShimmer/index.js +11 -0
  31. package/src/components/FilterButton/FilterButton.jsx +40 -0
  32. package/src/components/FilterButton/index.js +11 -0
  33. package/src/components/ImageCarousel/Image.jsx +34 -0
  34. package/src/components/ImageCarousel/ImageCarousel.jsx +103 -0
  35. package/src/components/ImageCarousel/index.js +11 -0
  36. package/src/components/InputButtonGroup/InputButtonGroup.jsx +120 -0
  37. package/src/components/InputButtonGroup/index.js +11 -0
  38. package/src/components/LabelledInput/LabelledInput.jsx +51 -0
  39. package/src/components/LabelledInput/index.js +11 -0
  40. package/src/components/Loading/Loading.jsx +32 -0
  41. package/src/components/Loading/index.js +11 -0
  42. package/src/components/NoResults/NoResults.jsx +55 -0
  43. package/src/components/NoResults/index.js +11 -0
  44. package/src/components/Pagination/Pagination.jsx +105 -0
  45. package/src/components/Pagination/index.js +10 -0
  46. package/src/components/PerPagePicker/PerPagePicker.jsx +114 -0
  47. package/src/components/PerPagePicker/index.js +11 -0
  48. package/src/components/Pill/Pill.jsx +34 -0
  49. package/src/components/Pill/index.js +11 -0
  50. package/src/components/Pill/mock.js +23 -0
  51. package/src/components/ProductCardShimmer/ProductCardShimmer.css +72 -0
  52. package/src/components/ProductCardShimmer/ProductCardShimmer.jsx +28 -0
  53. package/src/components/ProductCardShimmer/index.js +11 -0
  54. package/src/components/ProductItem/MockData.js +508 -0
  55. package/src/components/ProductItem/ProductItem.css +84 -0
  56. package/src/components/ProductItem/ProductItem.jsx +347 -0
  57. package/src/components/ProductItem/ProductPrice.jsx +181 -0
  58. package/src/components/ProductItem/index.js +11 -0
  59. package/src/components/ProductList/MockData.js +190 -0
  60. package/src/components/ProductList/ProductList.jsx +127 -0
  61. package/src/components/ProductList/index.js +11 -0
  62. package/src/components/ProductList/product-list.css +18 -0
  63. package/src/components/SearchBar/SearchBar.jsx +33 -0
  64. package/src/components/SearchBar/index.js +11 -0
  65. package/src/components/Shimmer/Shimmer.css +82 -0
  66. package/src/components/Shimmer/Shimmer.jsx +66 -0
  67. package/src/components/Shimmer/index.js +11 -0
  68. package/src/components/Slider/Slider.css +61 -0
  69. package/src/components/Slider/Slider.jsx +103 -0
  70. package/src/components/Slider/index.jsx +11 -0
  71. package/src/components/SliderDoubleControl/SliderDoubleControl.css +83 -0
  72. package/src/components/SliderDoubleControl/SliderDoubleControl.jsx +220 -0
  73. package/src/components/SliderDoubleControl/index.js +11 -0
  74. package/src/components/SortDropdown/SortDropdown.jsx +126 -0
  75. package/src/components/SortDropdown/index.js +11 -0
  76. package/src/components/SwatchButton/SwatchButton.jsx +72 -0
  77. package/src/components/SwatchButton/index.js +11 -0
  78. package/src/components/SwatchButtonGroup/SwatchButtonGroup.jsx +86 -0
  79. package/src/components/SwatchButtonGroup/index.js +11 -0
  80. package/src/components/ViewSwitcher/ViewSwitcher.jsx +46 -0
  81. package/src/components/ViewSwitcher/index.js +11 -0
  82. package/src/components/WishlistButton/WishlistButton.jsx +67 -0
  83. package/src/components/WishlistButton/index.js +11 -0
  84. package/src/containers/App.jsx +145 -0
  85. package/src/containers/LiveSearchPLPLoader.jsx +24 -0
  86. package/src/containers/LiveSearchPopoverLoader.jsx +190 -0
  87. package/src/containers/LiveSearchSRLPLoader.jsx +24 -0
  88. package/src/containers/ProductListingPage.jsx +66 -0
  89. package/src/containers/ProductsContainer.jsx +145 -0
  90. package/src/containers/ProductsHeader.jsx +123 -0
  91. package/src/context/attributeMetadata.js +63 -0
  92. package/src/context/cart.js +97 -0
  93. package/src/context/displayChange.js +90 -0
  94. package/src/context/events.js +160 -0
  95. package/src/context/index.js +19 -0
  96. package/src/context/products.jsx +336 -0
  97. package/src/context/resultsModifierContext.js +35 -0
  98. package/src/context/search.jsx +127 -0
  99. package/src/context/store.jsx +93 -0
  100. package/src/context/translation.jsx +125 -0
  101. package/src/context/widgetConfig.jsx +120 -0
  102. package/src/context/wishlist.jsx +97 -0
  103. package/src/hooks/eventing/useEventListener.js +13 -0
  104. package/src/hooks/eventing/useLocation.js +21 -0
  105. package/src/hooks/eventing/useMagentoExtensionContext.js +28 -0
  106. package/src/hooks/eventing/usePageView.js +36 -0
  107. package/src/hooks/eventing/useShopperContext.js +33 -0
  108. package/src/hooks/eventing/useStorefrontInstanceContext.js +46 -0
  109. package/src/hooks/eventing/useViewedOffsets.js +74 -0
  110. package/src/hooks/useAccessibleDropdown.js +148 -0
  111. package/src/hooks/useLiveSearchPLPConfig.js +112 -0
  112. package/src/hooks/useLiveSearchPopoverConfig.js +83 -0
  113. package/src/hooks/useLiveSearchSRLPConfig.js +97 -0
  114. package/src/hooks/usePagination.js +83 -0
  115. package/src/hooks/useRangeFacet.js +62 -0
  116. package/src/hooks/useScalarFacet.js +61 -0
  117. package/src/hooks/useSliderFacet.js +43 -0
  118. package/src/i18n/Sorani.js +60 -0
  119. package/src/i18n/ar_AE.js +60 -0
  120. package/src/i18n/bg_BG.js +60 -0
  121. package/src/i18n/bn_IN.js +60 -0
  122. package/src/i18n/ca_ES.js +60 -0
  123. package/src/i18n/cs_CZ.js +60 -0
  124. package/src/i18n/da_DK.js +60 -0
  125. package/src/i18n/de_DE.js +60 -0
  126. package/src/i18n/el_GR.js +60 -0
  127. package/src/i18n/en_GA.js +60 -0
  128. package/src/i18n/en_GB.js +60 -0
  129. package/src/i18n/en_US.js +70 -0
  130. package/src/i18n/es_ES.js +60 -0
  131. package/src/i18n/et_EE.js +60 -0
  132. package/src/i18n/eu_ES.js +60 -0
  133. package/src/i18n/fa_IR.js +60 -0
  134. package/src/i18n/fi_FI.js +60 -0
  135. package/src/i18n/fr_FR.js +60 -0
  136. package/src/i18n/gl_ES.js +60 -0
  137. package/src/i18n/hi_IN.js +60 -0
  138. package/src/i18n/hu_HU.js +60 -0
  139. package/src/i18n/hy_AM.js +60 -0
  140. package/src/i18n/id_ID.js +60 -0
  141. package/src/i18n/index.js +89 -0
  142. package/src/i18n/it_IT.js +60 -0
  143. package/src/i18n/ja_JP.js +60 -0
  144. package/src/i18n/ko_KR.js +60 -0
  145. package/src/i18n/lt_LT.js +60 -0
  146. package/src/i18n/lv_LV.js +60 -0
  147. package/src/i18n/nb_NO.js +60 -0
  148. package/src/i18n/nl_NL.js +60 -0
  149. package/src/i18n/pt_BR.js +60 -0
  150. package/src/i18n/pt_PT.js +60 -0
  151. package/src/i18n/ro_RO.js +60 -0
  152. package/src/i18n/ru_RU.js +60 -0
  153. package/src/i18n/sv_SE.js +60 -0
  154. package/src/i18n/th_TH.js +60 -0
  155. package/src/i18n/tr_TR.js +60 -0
  156. package/src/i18n/zh_Hans_CN.js +60 -0
  157. package/src/i18n/zh_Hant_TW.js +60 -0
  158. package/src/icons/NoImage.svg +1 -0
  159. package/src/icons/adjustments.svg +3 -0
  160. package/src/icons/cart.svg +3 -0
  161. package/src/icons/checkmark.svg +3 -0
  162. package/src/icons/chevron.svg +3 -0
  163. package/src/icons/emptyHeart.svg +3 -0
  164. package/src/icons/error.svg +3 -0
  165. package/src/icons/filledHeart.svg +3 -0
  166. package/src/icons/filter.svg +29 -0
  167. package/src/icons/gridView.svg +11 -0
  168. package/src/icons/info.svg +3 -0
  169. package/src/icons/listView.svg +5 -0
  170. package/src/icons/loading.svg +6 -0
  171. package/src/icons/plus.svg +4 -0
  172. package/src/icons/sort.svg +18 -0
  173. package/src/icons/warning.svg +3 -0
  174. package/src/icons/x.svg +3 -0
  175. package/src/index.jsx +65 -0
  176. package/src/queries/customerGroupCode.gql.js +10 -0
  177. package/src/queries/eventing/getMagentoExtensionContext.gql.js +13 -0
  178. package/src/queries/eventing/getPageType.gql.js +14 -0
  179. package/src/queries/eventing/getStorefrontContext.gql.js +27 -0
  180. package/src/queries/index.js +3 -0
  181. package/src/queries/liveSearchPlpConfigs.gql.js +30 -0
  182. package/src/queries/liveSearchPopoverConfigs.gql.js +28 -0
  183. package/src/styles/autocomplete.module.css +56 -0
  184. package/src/styles/index.css +1638 -0
  185. package/src/styles/searchBar.module.css +119 -0
  186. package/src/styles/tokens.css +99 -0
  187. package/src/targets/intercept.js +21 -0
  188. package/src/utils/constants.js +26 -0
  189. package/src/utils/decodeHtmlString.js +13 -0
  190. package/src/utils/dom.js +14 -0
  191. package/src/utils/eventing/getCookie.js +9 -0
  192. package/src/utils/eventing/usePageTypeFromUrl.js +26 -0
  193. package/src/utils/getProductImage.js +94 -0
  194. package/src/utils/getProductPrice.js +83 -0
  195. package/src/utils/getUserViewHistory.js +27 -0
  196. package/src/utils/handleUrlFilters.js +164 -0
  197. package/src/utils/htmlStringDecode.js +13 -0
  198. package/src/utils/modifyResults.js +164 -0
  199. package/src/utils/sort.js +95 -0
  200. package/src/utils/useIntersectionObserver.js +27 -0
  201. package/src/utils/validateStoreDetails.js +39 -0
  202. package/src/wrappers/wrapUseApp.js +28 -0
@@ -0,0 +1,86 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { SwatchButton } from '../SwatchButton';
12
+
13
+ const MAX_SWATCHES = 5;
14
+
15
+ export const SwatchButtonGroup = ({
16
+ isSelected,
17
+ swatches,
18
+ showMore,
19
+ productUrl,
20
+ onClick,
21
+ sku
22
+ }) => {
23
+ const moreSwatches = swatches.length > MAX_SWATCHES;
24
+ const numberOfOptions = moreSwatches ? MAX_SWATCHES - 1 : swatches.length;
25
+
26
+ return (
27
+ <div className="ds-sdk-product-item__product-swatch-group flex column items-center space-x-2">
28
+ {moreSwatches ? (
29
+ <div className="flex">
30
+ {swatches.slice(0, numberOfOptions).map((swatch) => {
31
+ const checked = isSelected(swatch.id);
32
+ return (
33
+ swatch &&
34
+ swatch.type === 'COLOR_HEX' && (
35
+ <div
36
+ key={swatch.id}
37
+ className="ds-sdk-product-item__product-swatch-item mr-2 text-sm text-primary"
38
+ >
39
+ <SwatchButton
40
+ id={swatch.id}
41
+ value={swatch.value}
42
+ type={swatch.type}
43
+ checked={!!checked}
44
+ onClick={() => onClick([swatch.id], sku)}
45
+ />
46
+ </div>
47
+ )
48
+ );
49
+ })}
50
+ <a href={productUrl} className="hover:no-underline">
51
+ <div className="ds-sdk-product-item__product-swatch-item text-sm text-primary">
52
+ <SwatchButton
53
+ id="show-more"
54
+ value={`+${swatches.length - numberOfOptions}`}
55
+ type="TEXT"
56
+ checked={false}
57
+ onClick={showMore}
58
+ />
59
+ </div>
60
+ </a>
61
+ </div>
62
+ ) : (
63
+ swatches.slice(0, numberOfOptions).map((swatch) => {
64
+ const checked = isSelected(swatch.id);
65
+ return (
66
+ swatch &&
67
+ swatch.type === 'COLOR_HEX' && (
68
+ <div
69
+ key={swatch.id}
70
+ className="ds-sdk-product-item__product-swatch-item text-sm text-primary"
71
+ >
72
+ <SwatchButton
73
+ id={swatch.id}
74
+ value={swatch.value}
75
+ type={swatch.type}
76
+ checked={!!checked}
77
+ onClick={() => onClick([swatch.id], sku)}
78
+ />
79
+ </div>
80
+ )
81
+ );
82
+ })
83
+ )}
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,11 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ export * from './SwatchButtonGroup';
11
+ export { SwatchButtonGroup as default } from './SwatchButtonGroup';
@@ -0,0 +1,46 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
5
+ accordance with the terms of the Adobe license agreement accompanying
6
+ it.
7
+ */
8
+
9
+ import React from 'react';
10
+ import { useProducts } from '../../context';
11
+ import { handleViewType } from '../../utils/handleUrlFilters';
12
+
13
+ import GridView from '../../icons/gridView.svg';
14
+ import ListView from '../../icons/listView.svg';
15
+
16
+ const ViewSwitcher = () => {
17
+ const { viewType, setViewType } = useProducts();
18
+
19
+ const handleClick = (newViewType) => {
20
+ handleViewType(newViewType);
21
+ setViewType(newViewType);
22
+ };
23
+
24
+ return (
25
+ <div className="flex justify-between">
26
+ <button
27
+ className={`flex items-center ${
28
+ viewType === 'gridview' ? 'bg-gray-100' : ''
29
+ } ring-black ring-opacity-5 p-sm text-sm h-[32px] border border-gray-300`}
30
+ onClick={() => handleClick('gridview')}
31
+ >
32
+ <GridView className="h-[20px] w-[20px]" />
33
+ </button>
34
+ <button
35
+ className={`flex items-center ${
36
+ viewType === 'listview' ? 'bg-gray-100' : ''
37
+ } ring-black ring-opacity-5 p-sm text-sm h-[32px] border border-gray-300`}
38
+ onClick={() => handleClick('listview')}
39
+ >
40
+ <ListView className="h-[20px] w-[20px]" />
41
+ </button>
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default ViewSwitcher;
@@ -0,0 +1,11 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ export * from './ViewSwitcher';
11
+ export { default } from './ViewSwitcher';
@@ -0,0 +1,67 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ import React from 'react';
11
+
12
+ import { useWishlist } from '../../context';
13
+ import EmptyHeart from '../../icons/emptyHeart.svg';
14
+ import FilledHeart from '../../icons/filledHeart.svg';
15
+ import { classNames } from '../../utils/dom';
16
+
17
+ export const WishlistButton = ({ type, productSku }) => {
18
+ const { isAuthorized, wishlist, addItemToWishlist, removeItemFromWishlist } =
19
+ useWishlist();
20
+
21
+ const wishlistItemStatus = wishlist?.items_v2?.items.find(
22
+ (ws) => ws.product.sku === productSku
23
+ );
24
+ const isWishlistItem = !!wishlistItemStatus;
25
+
26
+ const heart = isWishlistItem ? <FilledHeart /> : <EmptyHeart />;
27
+
28
+ const preventBubbleUp = (e) => {
29
+ e.stopPropagation();
30
+ e.preventDefault();
31
+ };
32
+
33
+ const handleAddWishlist = (e) => {
34
+ preventBubbleUp(e);
35
+ const selectedWishlistId = wishlist?.id;
36
+ if (isAuthorized) {
37
+ addItemToWishlist(selectedWishlistId, {
38
+ sku: productSku,
39
+ quantity: 1,
40
+ });
41
+ } else {
42
+ // FIXME: Update this for AEM/CIF compatibility if needed
43
+ window.location.href = `${window.origin}/customer/account/login/`;
44
+ }
45
+ };
46
+
47
+ const handleRemoveWishlist = (e) => {
48
+ preventBubbleUp(e);
49
+ if (!wishlistItemStatus) return;
50
+ removeItemFromWishlist(wishlist?.id, wishlistItemStatus.id);
51
+ };
52
+
53
+ return (
54
+ <div
55
+ className={classNames(
56
+ `ds-sdk-wishlist-${type}-button mt-[-2px]`,
57
+ type !== 'inLineWithName'
58
+ ? 'w-[30px] absolute top-0 right-0'
59
+ : 'w-[24px]'
60
+ )}
61
+ >
62
+ <div onClick={isWishlistItem ? handleRemoveWishlist : handleAddWishlist}>
63
+ {heart}
64
+ </div>
65
+ </div>
66
+ );
67
+ };
@@ -0,0 +1,11 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ export * from './WishlistButton';
11
+ export { default } from './WishlistButton';
@@ -0,0 +1,145 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ import React, { useState } from 'react';
11
+ import { FilterButton } from '../components/FilterButton';
12
+ import Loading from '../components/Loading';
13
+ import Shimmer from '../components/Shimmer';
14
+
15
+ import { CategoryFilters } from '../components/CategoryFilters';
16
+ import { SelectedFilters } from '../components/Facets';
17
+ import {
18
+ useProducts,
19
+ useSearch,
20
+ useSensor,
21
+ useStore,
22
+ useTranslation
23
+ } from '../context';
24
+ import ProductsContainer from './ProductsContainer';
25
+ import { ProductsHeader } from './ProductsHeader';
26
+
27
+ const App = () => {
28
+ const searchCtx = useSearch();
29
+ const productsCtx = useProducts();
30
+ const { screenSize } = useSensor();
31
+ const translation = useTranslation();
32
+ const { displayMode } = useStore().config;
33
+ const [showFilters, setShowFilters] = useState(true);
34
+
35
+ const loadingLabel = translation.Loading.title;
36
+
37
+ let title = productsCtx.categoryName || '';
38
+ if (productsCtx.variables.phrase) {
39
+ const text = translation.CategoryFilters.results;
40
+ title = text.replace('{phrase}', `"${productsCtx.variables.phrase !== null && productsCtx.variables.phrase !== undefined ? productsCtx.variables.phrase : ''}"`);
41
+
42
+ }
43
+
44
+ const getResults = (totalCount) => {
45
+ const resultsTranslation = translation.CategoryFilters.products;
46
+ return resultsTranslation.replace('{totalCount}', `${totalCount}`);
47
+ };
48
+
49
+ return (
50
+ <>
51
+ {!(displayMode === 'PAGE') &&
52
+ (!screenSize.mobile && showFilters && productsCtx.facets.length > 0 ? (
53
+ <div className="ds-widgets bg-body py-2 px-2">
54
+ <div className="flex">
55
+ <CategoryFilters
56
+ loading={productsCtx.loading}
57
+ pageLoading={productsCtx.pageLoading}
58
+ facets={productsCtx.facets}
59
+ totalCount={productsCtx.totalCount}
60
+ categoryName={productsCtx.categoryName !== null && productsCtx.categoryName !== undefined ? productsCtx.categoryName : ''}
61
+ phrase={productsCtx.variables.phrase !== null && productsCtx.variables.phrase !== undefined ? productsCtx.variables.phrase : ''}
62
+ showFilters={showFilters}
63
+ setShowFilters={setShowFilters}
64
+ filterCount={searchCtx.filterCount}
65
+ />
66
+ <div
67
+ className={`ds-widgets_results flex flex-col items-center ${
68
+ productsCtx.categoryName ? 'pt-16' : 'pt-28'
69
+ } w-full h-full`}
70
+ >
71
+ <ProductsHeader
72
+ facets={productsCtx.facets}
73
+ totalCount={productsCtx.totalCount}
74
+ screenSize={screenSize}
75
+ />
76
+ <SelectedFilters />
77
+ <ProductsContainer showFilters={showFilters} />
78
+ </div>
79
+ </div>
80
+ </div>
81
+ ) : (
82
+ <div className="ds-widgets bg-body py-2 px-2">
83
+ <div className="flex flex-col">
84
+ <div className="flex flex-col items-center w-full h-full">
85
+ <div className="justify-start w-full h-full">
86
+ <div className="hidden sm:flex ds-widgets-_actions relative max-w-[21rem] w-full h-full px-2 flex-col overflow-y-auto">
87
+ <div className="ds-widgets_actions_header flex justify-between items-center mb-md">
88
+ {title && <span>{title}</span>}
89
+ {!productsCtx.loading && (
90
+ <span className="text-primary text-sm">
91
+ {getResults(productsCtx.totalCount)}
92
+ </span>
93
+ )}
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <div className="ds-widgets_results flex flex-col items-center w-full h-full">
99
+ <div className="flex w-full h-full">
100
+ {!screenSize.mobile &&
101
+ !productsCtx.loading &&
102
+ productsCtx.facets.length > 0 && (
103
+ <div className="flex w-full h-full">
104
+ <FilterButton
105
+ displayFilter={() => setShowFilters(true)}
106
+ type="desktop"
107
+ title={`${translation.Filter.showTitle}${
108
+ searchCtx.filterCount > 0
109
+ ? ` (${searchCtx.filterCount})`
110
+ : ''
111
+ }`}
112
+ />
113
+ </div>
114
+ )}
115
+ </div>
116
+ {productsCtx.loading ? (
117
+ screenSize.mobile ? (
118
+ <Loading label={loadingLabel} />
119
+ ) : (
120
+ <Shimmer />
121
+ )
122
+ ) : (
123
+ <>
124
+ <div className="flex w-full h-full">
125
+ <ProductsHeader
126
+ facets={productsCtx.facets}
127
+ totalCount={productsCtx.totalCount}
128
+ screenSize={screenSize}
129
+ />
130
+ </div>
131
+ <SelectedFilters />
132
+ <ProductsContainer
133
+ showFilters={showFilters && productsCtx.facets.length > 0}
134
+ />
135
+ </>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ ))}
141
+ </>
142
+ );
143
+ };
144
+
145
+ export default App;
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import LiveSearchPLP from '../index';
3
+ import { useLiveSearchPLPConfig } from '../hooks/useLiveSearchPLPConfig';
4
+ import { ResultsModifierProvider } from '../context/resultsModifierContext';
5
+
6
+ export const LiveSearchPLPLoader = ({categoryId}) => {
7
+ const { config, loading, error } = useLiveSearchPLPConfig({categoryId});
8
+
9
+ if (loading) {
10
+ return <div></div>;
11
+ }
12
+
13
+ if (error || !config) {
14
+ return <div>Error loading Live Search configuration</div>;
15
+ }
16
+
17
+ return (
18
+ <ResultsModifierProvider baseUrl={config?.baseUrl} baseUrlWithoutProtocol={config?.baseUrlwithoutProtocol}>
19
+ <LiveSearchPLP storeDetails={config} />
20
+ </ResultsModifierProvider>
21
+ );
22
+ };
23
+
24
+ export default LiveSearchPLPLoader;
@@ -0,0 +1,190 @@
1
+ import React, { useEffect, useState, useCallback, useMemo } from 'react';
2
+ import { useHistory } from 'react-router-dom';
3
+ import { useAutocomplete, Popover, LiveSearch } from '@magento/storefront-search-as-you-type';
4
+ import { Form } from 'informed';
5
+ import TextInput from '@magento/venia-ui/lib/components/TextInput';
6
+ import { useStyle } from '@magento/venia-ui/lib/classify';
7
+ import defaultClasses from '../styles/searchBar.module.css';
8
+ import autoCompleteClasses from '../styles/autocomplete.module.css';
9
+ import { useLiveSearchPopoverConfig } from '../hooks/useLiveSearchPopoverConfig';
10
+ import { Search as SearchIcon } from 'react-feather';
11
+
12
+ const LiveSearchPopoverLoader = () => {
13
+ const classes = useStyle(defaultClasses);
14
+ const history = useHistory();
15
+ const [isPopoverVisible, setPopoverVisible] = useState(false);
16
+
17
+ const {
18
+ storeDetails,
19
+ configReady,
20
+ storeLoading,
21
+ customerLoading,
22
+ storeError
23
+ } = useLiveSearchPopoverConfig();
24
+
25
+ const liveSearch = useMemo(() => {
26
+ if (!storeDetails || Object.keys(storeDetails).length === 0) return null;
27
+ return new LiveSearch(storeDetails);
28
+ }, [JSON.stringify(storeDetails)]);
29
+
30
+ const {
31
+ performSearch,
32
+ minQueryLength,
33
+ currencySymbol
34
+ } = liveSearch ? liveSearch : {
35
+ performSearch: () => Promise.resolve({}),
36
+ minQueryLength: 3,
37
+ currencySymbol: '$'
38
+ };
39
+
40
+
41
+ const {
42
+ formProps,
43
+ formRef,
44
+ inputProps,
45
+ inputRef,
46
+ results,
47
+ resultsRef,
48
+ loading: searchLoading,
49
+ searchTerm
50
+ } = useAutocomplete(performSearch, minQueryLength);
51
+
52
+ const transformResults = originalResults => {
53
+ if (!originalResults?.data?.productSearch?.items) return originalResults;
54
+
55
+ const cleanUrl = url =>
56
+ url?.replace(storeDetails.baseUrlwithoutProtocol, '');
57
+
58
+ const transformedItems = originalResults.data.productSearch.items.map(item => {
59
+ const product = item.product;
60
+ if (!product) return item;
61
+
62
+ return {
63
+ ...item,
64
+ product: {
65
+ ...product,
66
+ canonical_url: cleanUrl(product.canonical_url),
67
+ image: { ...product.image, url: cleanUrl(product.image?.url) },
68
+ small_image: { ...product.small_image, url: cleanUrl(product.small_image?.url) },
69
+ thumbnail: { ...product.thumbnail, url: cleanUrl(product.thumbnail?.url) }
70
+ }
71
+ };
72
+ });
73
+
74
+ return {
75
+ ...originalResults,
76
+ data: {
77
+ ...originalResults.data,
78
+ productSearch: {
79
+ ...originalResults.data.productSearch,
80
+ items: transformedItems
81
+ }
82
+ }
83
+ };
84
+ };
85
+
86
+ const modifiedResults = transformResults(results);
87
+ inputRef.current = document.getElementById('search_query');
88
+ formRef.current = document.getElementById('search-autocomplete-form');
89
+
90
+ const getSearchStatusMessage = () => {
91
+ if (!searchTerm) return 'Search for a product';
92
+
93
+ if (searchTerm.length < minQueryLength) {
94
+ return `Search term must be at least ${minQueryLength} characters`;
95
+ }
96
+
97
+ if (searchLoading) {
98
+ return 'Fetching results...';
99
+ }
100
+
101
+ if (
102
+ searchTerm.length >= minQueryLength &&
103
+ !searchLoading &&
104
+ modifiedResults?.data?.productSearch?.items?.length === 0
105
+ ) {
106
+ return 'No results were found.';
107
+ }
108
+
109
+ return null;
110
+ };
111
+
112
+ const searchStatusMessage = getSearchStatusMessage();
113
+
114
+ useEffect(() => {
115
+ if (searchTerm.length >= minQueryLength) {
116
+ setPopoverVisible(true);
117
+ }
118
+ }, [searchTerm, minQueryLength]);
119
+
120
+ const handleSubmit = useCallback(
121
+ event => {
122
+ const query = inputRef.current?.value;
123
+ if (query) {
124
+ setPopoverVisible(false);
125
+ history.push(`/search.html?query=${query}`);
126
+ }
127
+ },
128
+ [history, inputRef]
129
+ );
130
+
131
+ if (!configReady) return null; // Or a loading spinner
132
+
133
+ return (
134
+ <Form
135
+ autoComplete="off"
136
+ className={classes.form}
137
+ id="search-autocomplete-form"
138
+ {...formProps}
139
+ action="/search.html"
140
+ onSubmit={handleSubmit}
141
+ >
142
+ <div className={classes.search}>
143
+ <TextInput
144
+ id="search_query"
145
+ before={<SearchIcon />}
146
+ field="query"
147
+ data-cy="SearchField-textInput"
148
+ {...inputProps}
149
+ />
150
+ <div data-cy="Autocomplete-root" className={autoCompleteClasses.root_visible}>
151
+ {searchStatusMessage && (
152
+ <label
153
+ id="search_query_label"
154
+ data-cy="Autocomplete-message"
155
+ className={classes.message}
156
+ >
157
+ {searchStatusMessage}
158
+ </label>
159
+ )}
160
+ <div
161
+ id="search_autocomplete"
162
+ className={`${classes.suggestions} ${classes.popover}`}
163
+ >
164
+ {searchTerm &&
165
+ !searchLoading &&
166
+ results &&
167
+ isPopoverVisible && (
168
+ <Popover
169
+ active={searchTerm.length >= minQueryLength}
170
+ response={modifiedResults}
171
+ formRef={formRef}
172
+ resultsRef={resultsRef}
173
+ inputRef={inputRef}
174
+ pageSize={storeDetails.config.pageSize}
175
+ currencySymbol={currencySymbol}
176
+ currencyRate={storeDetails.config.currencyRate}
177
+ minQueryLengthHit={
178
+ searchTerm.length >= minQueryLength
179
+ }
180
+ searchRoute={storeDetails.searchRoute}
181
+ />
182
+ )}
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </Form>
187
+ );
188
+ };
189
+
190
+ export default LiveSearchPopoverLoader;
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import LiveSearchPLP from '../index';
3
+ import { useLiveSearchSRLPConfig } from '../hooks/useLiveSearchSRLPConfig';
4
+ import { ResultsModifierProvider } from '../context/resultsModifierContext';
5
+
6
+ export const LiveSearchSRLPLoader = () => {
7
+ const { config, loading, error } = useLiveSearchSRLPConfig();
8
+
9
+ if (loading) {
10
+ return <div></div>;
11
+ }
12
+
13
+ if (error || !config) {
14
+ return <div>Error loading Live Search configuration</div>;
15
+ }
16
+
17
+ return (
18
+ <ResultsModifierProvider baseUrl={config?.baseUrl} baseUrlWithoutProtocol={config?.baseUrlwithoutProtocol}>
19
+ <LiveSearchPLP storeDetails={config} />
20
+ </ResultsModifierProvider>
21
+ );
22
+ };
23
+
24
+ export default LiveSearchSRLPLoader;
@@ -0,0 +1,66 @@
1
+ /*
2
+ Copyright 2024 Adobe
3
+ All Rights Reserved.
4
+
5
+ NOTICE: Adobe permits you to use, modify, and distribute this file in
6
+ accordance with the terms of the Adobe license agreement accompanying
7
+ it.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { validateStoreDetailsKeys } from '../utils/validateStoreDetails';
12
+
13
+ import '../styles/global.css';
14
+
15
+ import {
16
+ AttributeMetadataProvider,
17
+ CartProvider,
18
+ ProductsContextProvider,
19
+ SearchProvider,
20
+ StoreContextProvider,
21
+ } from '../context';
22
+ import Resize from '../context/displayChange';
23
+ import Translation from '../context/translation';
24
+ import { getUserViewHistory } from '../utils/getUserViewHistory';
25
+ import App from './App';
26
+
27
+ /**
28
+ * A plug-and-play React component that provides the full Live Search PLP context.
29
+ * @param {object} props
30
+ * @param {object} props.storeDetails - Store configuration data (must include context).
31
+ */
32
+ const ProductListingPage = ({ storeDetails }) => {
33
+ if (!storeDetails) {
34
+ throw new Error("LiveSearchPLP's storeDetails prop was not provided");
35
+ }
36
+
37
+ const userViewHistory = getUserViewHistory();
38
+
39
+ const updatedStoreDetails = {
40
+ ...storeDetails,
41
+ context: {
42
+ ...storeDetails.context,
43
+ userViewHistory,
44
+ },
45
+ };
46
+
47
+ return (
48
+ <StoreContextProvider {...validateStoreDetailsKeys(updatedStoreDetails)}>
49
+ <AttributeMetadataProvider>
50
+ <SearchProvider>
51
+ <Resize>
52
+ <Translation>
53
+ <ProductsContextProvider>
54
+ <CartProvider>
55
+ <App />
56
+ </CartProvider>
57
+ </ProductsContextProvider>
58
+ </Translation>
59
+ </Resize>
60
+ </SearchProvider>
61
+ </AttributeMetadataProvider>
62
+ </StoreContextProvider>
63
+ );
64
+ };
65
+
66
+ export default ProductListingPage;