@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.
- package/package.json +31 -0
- package/postcss.config.js +3 -0
- package/src/api/fragments.js +193 -0
- package/src/api/graphql.js +26 -0
- package/src/api/mutations.js +94 -0
- package/src/api/queries.js +225 -0
- package/src/api/search.js +222 -0
- package/src/components/AddToCartButton/AddToCartButton.jsx +32 -0
- package/src/components/AddToCartButton/AddToCartButton.stories.mdx +14 -0
- package/src/components/AddToCartButton/index.js +10 -0
- package/src/components/Alert/Alert.jsx +155 -0
- package/src/components/Alert/index.js +11 -0
- package/src/components/Breadcrumbs/Breadcrumbs.jsx +34 -0
- package/src/components/Breadcrumbs/MockPages.js +14 -0
- package/src/components/Breadcrumbs/index.js +11 -0
- package/src/components/ButtonShimmer/ButtonShimmer.css +32 -0
- package/src/components/ButtonShimmer/ButtonShimmer.jsx +23 -0
- package/src/components/ButtonShimmer/index.js +11 -0
- package/src/components/CategoryFilters/CategoryFilters.jsx +59 -0
- package/src/components/CategoryFilters/index.js +10 -0
- package/src/components/Facets/Facets.jsx +50 -0
- package/src/components/Facets/Range/RangeFacet.js +20 -0
- package/src/components/Facets/Scalar/ScalarFacet.js +29 -0
- package/src/components/Facets/SelectedFilters.js +80 -0
- package/src/components/Facets/format.js +52 -0
- package/src/components/Facets/index.js +14 -0
- package/src/components/Facets/mocks.js +119 -0
- package/src/components/FacetsShimmer/FacetsShimmer.css +49 -0
- package/src/components/FacetsShimmer/FacetsShimmer.jsx +25 -0
- package/src/components/FacetsShimmer/index.js +11 -0
- package/src/components/FilterButton/FilterButton.jsx +40 -0
- package/src/components/FilterButton/index.js +11 -0
- package/src/components/ImageCarousel/Image.jsx +34 -0
- package/src/components/ImageCarousel/ImageCarousel.jsx +103 -0
- package/src/components/ImageCarousel/index.js +11 -0
- package/src/components/InputButtonGroup/InputButtonGroup.jsx +120 -0
- package/src/components/InputButtonGroup/index.js +11 -0
- package/src/components/LabelledInput/LabelledInput.jsx +51 -0
- package/src/components/LabelledInput/index.js +11 -0
- package/src/components/Loading/Loading.jsx +32 -0
- package/src/components/Loading/index.js +11 -0
- package/src/components/NoResults/NoResults.jsx +55 -0
- package/src/components/NoResults/index.js +11 -0
- package/src/components/Pagination/Pagination.jsx +105 -0
- package/src/components/Pagination/index.js +10 -0
- package/src/components/PerPagePicker/PerPagePicker.jsx +114 -0
- package/src/components/PerPagePicker/index.js +11 -0
- package/src/components/Pill/Pill.jsx +34 -0
- package/src/components/Pill/index.js +11 -0
- package/src/components/Pill/mock.js +23 -0
- package/src/components/ProductCardShimmer/ProductCardShimmer.css +72 -0
- package/src/components/ProductCardShimmer/ProductCardShimmer.jsx +28 -0
- package/src/components/ProductCardShimmer/index.js +11 -0
- package/src/components/ProductItem/MockData.js +508 -0
- package/src/components/ProductItem/ProductItem.css +84 -0
- package/src/components/ProductItem/ProductItem.jsx +347 -0
- package/src/components/ProductItem/ProductPrice.jsx +181 -0
- package/src/components/ProductItem/index.js +11 -0
- package/src/components/ProductList/MockData.js +190 -0
- package/src/components/ProductList/ProductList.jsx +127 -0
- package/src/components/ProductList/index.js +11 -0
- package/src/components/ProductList/product-list.css +18 -0
- package/src/components/SearchBar/SearchBar.jsx +33 -0
- package/src/components/SearchBar/index.js +11 -0
- package/src/components/Shimmer/Shimmer.css +82 -0
- package/src/components/Shimmer/Shimmer.jsx +66 -0
- package/src/components/Shimmer/index.js +11 -0
- package/src/components/Slider/Slider.css +61 -0
- package/src/components/Slider/Slider.jsx +103 -0
- package/src/components/Slider/index.jsx +11 -0
- package/src/components/SliderDoubleControl/SliderDoubleControl.css +83 -0
- package/src/components/SliderDoubleControl/SliderDoubleControl.jsx +220 -0
- package/src/components/SliderDoubleControl/index.js +11 -0
- package/src/components/SortDropdown/SortDropdown.jsx +126 -0
- package/src/components/SortDropdown/index.js +11 -0
- package/src/components/SwatchButton/SwatchButton.jsx +72 -0
- package/src/components/SwatchButton/index.js +11 -0
- package/src/components/SwatchButtonGroup/SwatchButtonGroup.jsx +86 -0
- package/src/components/SwatchButtonGroup/index.js +11 -0
- package/src/components/ViewSwitcher/ViewSwitcher.jsx +46 -0
- package/src/components/ViewSwitcher/index.js +11 -0
- package/src/components/WishlistButton/WishlistButton.jsx +67 -0
- package/src/components/WishlistButton/index.js +11 -0
- package/src/containers/App.jsx +145 -0
- package/src/containers/LiveSearchPLPLoader.jsx +24 -0
- package/src/containers/LiveSearchPopoverLoader.jsx +190 -0
- package/src/containers/LiveSearchSRLPLoader.jsx +24 -0
- package/src/containers/ProductListingPage.jsx +66 -0
- package/src/containers/ProductsContainer.jsx +145 -0
- package/src/containers/ProductsHeader.jsx +123 -0
- package/src/context/attributeMetadata.js +63 -0
- package/src/context/cart.js +97 -0
- package/src/context/displayChange.js +90 -0
- package/src/context/events.js +160 -0
- package/src/context/index.js +19 -0
- package/src/context/products.jsx +336 -0
- package/src/context/resultsModifierContext.js +35 -0
- package/src/context/search.jsx +127 -0
- package/src/context/store.jsx +93 -0
- package/src/context/translation.jsx +125 -0
- package/src/context/widgetConfig.jsx +120 -0
- package/src/context/wishlist.jsx +97 -0
- package/src/hooks/eventing/useEventListener.js +13 -0
- package/src/hooks/eventing/useLocation.js +21 -0
- package/src/hooks/eventing/useMagentoExtensionContext.js +28 -0
- package/src/hooks/eventing/usePageView.js +36 -0
- package/src/hooks/eventing/useShopperContext.js +33 -0
- package/src/hooks/eventing/useStorefrontInstanceContext.js +46 -0
- package/src/hooks/eventing/useViewedOffsets.js +74 -0
- package/src/hooks/useAccessibleDropdown.js +148 -0
- package/src/hooks/useLiveSearchPLPConfig.js +112 -0
- package/src/hooks/useLiveSearchPopoverConfig.js +83 -0
- package/src/hooks/useLiveSearchSRLPConfig.js +97 -0
- package/src/hooks/usePagination.js +83 -0
- package/src/hooks/useRangeFacet.js +62 -0
- package/src/hooks/useScalarFacet.js +61 -0
- package/src/hooks/useSliderFacet.js +43 -0
- package/src/i18n/Sorani.js +60 -0
- package/src/i18n/ar_AE.js +60 -0
- package/src/i18n/bg_BG.js +60 -0
- package/src/i18n/bn_IN.js +60 -0
- package/src/i18n/ca_ES.js +60 -0
- package/src/i18n/cs_CZ.js +60 -0
- package/src/i18n/da_DK.js +60 -0
- package/src/i18n/de_DE.js +60 -0
- package/src/i18n/el_GR.js +60 -0
- package/src/i18n/en_GA.js +60 -0
- package/src/i18n/en_GB.js +60 -0
- package/src/i18n/en_US.js +70 -0
- package/src/i18n/es_ES.js +60 -0
- package/src/i18n/et_EE.js +60 -0
- package/src/i18n/eu_ES.js +60 -0
- package/src/i18n/fa_IR.js +60 -0
- package/src/i18n/fi_FI.js +60 -0
- package/src/i18n/fr_FR.js +60 -0
- package/src/i18n/gl_ES.js +60 -0
- package/src/i18n/hi_IN.js +60 -0
- package/src/i18n/hu_HU.js +60 -0
- package/src/i18n/hy_AM.js +60 -0
- package/src/i18n/id_ID.js +60 -0
- package/src/i18n/index.js +89 -0
- package/src/i18n/it_IT.js +60 -0
- package/src/i18n/ja_JP.js +60 -0
- package/src/i18n/ko_KR.js +60 -0
- package/src/i18n/lt_LT.js +60 -0
- package/src/i18n/lv_LV.js +60 -0
- package/src/i18n/nb_NO.js +60 -0
- package/src/i18n/nl_NL.js +60 -0
- package/src/i18n/pt_BR.js +60 -0
- package/src/i18n/pt_PT.js +60 -0
- package/src/i18n/ro_RO.js +60 -0
- package/src/i18n/ru_RU.js +60 -0
- package/src/i18n/sv_SE.js +60 -0
- package/src/i18n/th_TH.js +60 -0
- package/src/i18n/tr_TR.js +60 -0
- package/src/i18n/zh_Hans_CN.js +60 -0
- package/src/i18n/zh_Hant_TW.js +60 -0
- package/src/icons/NoImage.svg +1 -0
- package/src/icons/adjustments.svg +3 -0
- package/src/icons/cart.svg +3 -0
- package/src/icons/checkmark.svg +3 -0
- package/src/icons/chevron.svg +3 -0
- package/src/icons/emptyHeart.svg +3 -0
- package/src/icons/error.svg +3 -0
- package/src/icons/filledHeart.svg +3 -0
- package/src/icons/filter.svg +29 -0
- package/src/icons/gridView.svg +11 -0
- package/src/icons/info.svg +3 -0
- package/src/icons/listView.svg +5 -0
- package/src/icons/loading.svg +6 -0
- package/src/icons/plus.svg +4 -0
- package/src/icons/sort.svg +18 -0
- package/src/icons/warning.svg +3 -0
- package/src/icons/x.svg +3 -0
- package/src/index.jsx +65 -0
- package/src/queries/customerGroupCode.gql.js +10 -0
- package/src/queries/eventing/getMagentoExtensionContext.gql.js +13 -0
- package/src/queries/eventing/getPageType.gql.js +14 -0
- package/src/queries/eventing/getStorefrontContext.gql.js +27 -0
- package/src/queries/index.js +3 -0
- package/src/queries/liveSearchPlpConfigs.gql.js +30 -0
- package/src/queries/liveSearchPopoverConfigs.gql.js +28 -0
- package/src/styles/autocomplete.module.css +56 -0
- package/src/styles/index.css +1638 -0
- package/src/styles/searchBar.module.css +119 -0
- package/src/styles/tokens.css +99 -0
- package/src/targets/intercept.js +21 -0
- package/src/utils/constants.js +26 -0
- package/src/utils/decodeHtmlString.js +13 -0
- package/src/utils/dom.js +14 -0
- package/src/utils/eventing/getCookie.js +9 -0
- package/src/utils/eventing/usePageTypeFromUrl.js +26 -0
- package/src/utils/getProductImage.js +94 -0
- package/src/utils/getProductPrice.js +83 -0
- package/src/utils/getUserViewHistory.js +27 -0
- package/src/utils/handleUrlFilters.js +164 -0
- package/src/utils/htmlStringDecode.js +13 -0
- package/src/utils/modifyResults.js +164 -0
- package/src/utils/sort.js +95 -0
- package/src/utils/useIntersectionObserver.js +27 -0
- package/src/utils/validateStoreDetails.js +39 -0
- 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;
|