@evenicanpm/storefront-core 1.0.0 → 1.0.1

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 (133) hide show
  1. package/package.json +3 -2
  2. package/src/api-manager/datasources/d365/d365-address.datasource.ts +1 -1
  3. package/src/api-manager/datasources/d365/d365-user.datasource.ts +0 -19
  4. package/src/api-manager/datasources/e4/e4-cart.datasource.ts +2 -2
  5. package/src/api-manager/datasources/e4/e4-product.datasource.ts +19 -4
  6. package/src/api-manager/datasources/e4/e4.datasource.ts +1 -2
  7. package/src/api-manager/datasources/e4/e4.remaps.ts +24 -0
  8. package/src/api-manager/datasources/e4/e4.translator.ts +17 -0
  9. package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
  10. package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
  11. package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
  12. package/src/api-manager/index.ts +21 -6
  13. package/src/api-manager/schemas/product.schema.ts +1 -1
  14. package/src/api-manager/services/address/queries/get-states.ts +1 -1
  15. package/src/api-manager/services/create-query.ts +10 -3
  16. package/src/api-manager/types/Datasource.ts +9 -9
  17. package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
  18. package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
  19. package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
  20. package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
  21. package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
  22. package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
  23. package/src/components/_components/section-header/section-header.tsx +1 -1
  24. package/src/components/_components/settings/setting.tsx +1 -1
  25. package/src/components/_components/wishlist-modal/TODO.md +10 -0
  26. package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
  27. package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
  28. package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
  29. package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
  30. package/src/components/countries-input.tsx +1 -0
  31. package/src/hooks/use-cart.tsx +82 -0
  32. package/src/hooks/use-variants.tsx +85 -0
  33. package/src/pages/account/addresses/address-form.tsx +289 -0
  34. package/src/pages/account/addresses/address-item.tsx +104 -0
  35. package/src/pages/account/dashboard-header.tsx +123 -0
  36. package/src/pages/account/navigation.tsx +84 -0
  37. package/src/pages/account/no-records.tsx +20 -0
  38. package/src/pages/account/orders/icons/delivery.tsx +14 -0
  39. package/src/pages/account/orders/icons/package-box.tsx +13 -0
  40. package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
  41. package/src/pages/account/orders/order-progress.tsx +111 -0
  42. package/src/pages/account/orders/order-row.tsx +61 -0
  43. package/src/pages/account/orders/order-summary.tsx +83 -0
  44. package/src/pages/account/orders/ordered-products.tsx +114 -0
  45. package/src/pages/account/profile/profile-button.test.tsx +59 -0
  46. package/src/pages/account/profile/profile-button.tsx +50 -0
  47. package/src/pages/account/profile/user-info.tsx +59 -0
  48. package/src/pages/account/styles.ts +32 -0
  49. package/src/pages/account/table-row.tsx +19 -0
  50. package/src/pages/account/wishlist/create-new-list.tsx +167 -0
  51. package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
  52. package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
  53. package/src/pages/blog/pagination.tsx +38 -0
  54. package/src/pages/cart/cart-item.tsx +312 -0
  55. package/src/pages/cart/checkout-form.tsx +122 -0
  56. package/src/pages/cart/coupon-entry.tsx +90 -0
  57. package/src/pages/cart/estimate-shipping.tsx +183 -0
  58. package/src/pages/cart/wrapper.tsx +30 -0
  59. package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
  60. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
  61. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
  62. package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
  63. package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
  64. package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
  65. package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
  66. package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
  67. package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
  68. package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
  69. package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
  70. package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
  71. package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
  72. package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
  73. package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
  74. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
  75. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
  76. package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
  77. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
  78. package/src/pages/confirmation/address.tsx +22 -0
  79. package/src/pages/confirmation/confirmation-summary.tsx +52 -0
  80. package/src/pages/confirmation/ordered-products.tsx +108 -0
  81. package/src/pages/product-details/available-shops.tsx +48 -0
  82. package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
  83. package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
  84. package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
  85. package/src/pages/product-details/bopis/search-header.tsx +69 -0
  86. package/src/pages/product-details/checkbox-label.tsx +20 -0
  87. package/src/pages/product-details/currency.ts +64 -0
  88. package/src/pages/product-details/frequently-bought.tsx +90 -0
  89. package/src/pages/product-details/frequently-product-card.tsx +60 -0
  90. package/src/pages/product-details/product-comment.tsx +44 -0
  91. package/src/pages/product-details/product-description.tsx +22 -0
  92. package/src/pages/product-details/product-filter-card.tsx +257 -0
  93. package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
  94. package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
  95. package/src/pages/product-details/product-review.tsx +131 -0
  96. package/src/pages/product-details/product-tabs.tsx +51 -0
  97. package/src/pages/product-details/related-products.tsx +42 -0
  98. package/src/pages/product-details/types.ts +11 -0
  99. package/src/pages/product-list/breadcrumbs.tsx +39 -0
  100. package/src/pages/product-list/checkbox-label.tsx +20 -0
  101. package/src/pages/product-list/facet-group.tsx +125 -0
  102. package/src/pages/product-list/facet.tsx +36 -0
  103. package/src/pages/product-list/list-filter.tsx +40 -0
  104. package/src/pages/product-list/pagination.tsx +80 -0
  105. package/src/pages/product-list/product-list-context.tsx +302 -0
  106. package/src/pages/product-list/product-list-state.ts +187 -0
  107. package/src/pages/product-list/product-rating.tsx +16 -0
  108. package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
  109. package/src/pages/product-list/quick-view-dialog.tsx +54 -0
  110. package/src/pages/product-list/range-filter.tsx +125 -0
  111. package/src/pages/product-list/scrollbar.tsx +38 -0
  112. package/src/pages/product-list/search-bar.tsx +162 -0
  113. package/src/pages/product-list/selected-facets.tsx +80 -0
  114. package/src/pages/product-list/side-nav.tsx +49 -0
  115. package/src/pages/product-list/swatch.tsx +80 -0
  116. package/src/pages/product-list/types.ts +18 -0
  117. package/src/pages/product-list/use-product-list.ts +12 -0
  118. package/src/pages/product-list/useCategoryFilter.tsx +26 -0
  119. package/src/pages/product-list/utils/facet-helpers.ts +5 -0
  120. package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
  121. package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
  122. package/src/pages/product-list/utils/product-list-helper.ts +111 -0
  123. package/src/pages/product-list/utils/product-list-types.ts +41 -0
  124. package/src/pages/product-list/utils/search-for-category.ts +76 -0
  125. package/src/pages/product-list/utils/sort-options.ts +44 -0
  126. package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
  127. package/src/pages/quickorder/order-upload.tsx +150 -0
  128. package/src/pages/quickorder/quick-order-form.tsx +343 -0
  129. package/src/pages/quickorder/quick-order-row.tsx +144 -0
  130. package/tsconfig.json +1 -0
  131. package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +0 -10464
  132. /package/src/{components/_components/hooks/useOverflowDetect.ts → hooks/use-overflow-detect.ts} +0 -0
  133. /package/src/{components/_components/hooks/useSettings.ts → hooks/use-settings.ts} +0 -0
@@ -0,0 +1,205 @@
1
+ import { FlexBox } from "@evenicanpm/storefront-core/src/components/_components/flex-box";
2
+ import {
3
+ H1,
4
+ H2,
5
+ H6,
6
+ } from "@evenicanpm/storefront-core/src/components/_components/Typography";
7
+ // import ImageWithFallback from "@app/_shared/ImageWithFallback";
8
+ import ImageWithFallback from "@evenicanpm/storefront-core/src/components/ImageWithFallback";
9
+ import { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
10
+ import {
11
+ Button,
12
+ CircularProgress,
13
+ DialogContent,
14
+ Typography,
15
+ } from "@mui/material";
16
+ import Grid from "@mui/material/Grid";
17
+ import Box from "@mui/material/Box";
18
+ import ProductRating from "./product-rating";
19
+ import useCart, { AddToCartBopisInput } from "@/hooks/use-cart";
20
+ import useCurrencyFormatter from "@/lib/currency-formatter";
21
+ import ProductDimensions from "@/components/_components/product-dimensions/product-dimensions";
22
+ import QuantityButtons from "@/components/_components/product-quantity-buttons/product-quantity-buttons";
23
+ import useVariants from "@/hooks/use-variants";
24
+ import {
25
+ InventorySearchCriteriaInput,
26
+ ProductDetails,
27
+ } from "@evenicanpm/storefront-core/src/api-manager/schemas/product.schema";
28
+ import { Cart } from "@evenicanpm/storefront-core/src/api-manager/schemas/cart.schema";
29
+ import { useMemo, useState } from "react";
30
+ import getProductAvailability from "@/api-manager/services/product/queries/get-product-availability";
31
+ import Link from "next/link";
32
+ import buildDetailUrl from "@evenicanpm/storefront-core/src/lib/build-detail-url";
33
+ import { useTranslations } from "next-intl";
34
+
35
+ interface Props {
36
+ product: ProductSearchResult;
37
+ cart: Cart;
38
+ productDetails: ProductDetails;
39
+ handleClose: () => void;
40
+ }
41
+
42
+ export default function QuickViewContent({
43
+ product,
44
+ cart,
45
+ productDetails,
46
+ handleClose,
47
+ }: Props) {
48
+ const [showDimensionError, setShowDimensionError] = useState(false);
49
+ const {
50
+ selectVariantId,
51
+ selectDimensions,
52
+ handleChangeVariant,
53
+ allDimensionsSelected,
54
+ } = useVariants({ product: productDetails });
55
+ const t = useTranslations("ProductList.QuickView");
56
+ const { formatCurrency } = useCurrencyFormatter();
57
+ const { handleCartAmountChange, handleAddToCart, loading } = useCart(
58
+ cart
59
+ ? {
60
+ cart,
61
+ recordId: product.RecordId,
62
+ selectVariantId: selectVariantId,
63
+ }
64
+ : {
65
+ cart: { Id: "", CartLines: [] },
66
+ recordId: product.RecordId,
67
+ selectVariantId: selectVariantId,
68
+ },
69
+ );
70
+
71
+ const searchCriteria = useMemo<InventorySearchCriteriaInput>(
72
+ () => ({
73
+ allDimensionsSelected: allDimensionsSelected(selectDimensions),
74
+ searchCriteria: {
75
+ defaultWarehouseOnly: true,
76
+ productIds: [selectVariantId || product.RecordId],
77
+ },
78
+ }),
79
+ [allDimensionsSelected, selectDimensions, product, selectVariantId],
80
+ );
81
+
82
+ const { data: shippingInventoryData, isLoading: inventoryLoading } =
83
+ getProductAvailability.useData(searchCriteria);
84
+
85
+ const currentCartLine = cart?.CartLines?.find((item) => {
86
+ return item?.ProductId === (Number(selectVariantId) || product.RecordId);
87
+ });
88
+
89
+ const onAddToCart = (bopisInput?: AddToCartBopisInput) => {
90
+ if (!allDimensionsSelected(selectDimensions)) {
91
+ setShowDimensionError(true);
92
+ return;
93
+ }
94
+ setShowDimensionError(false);
95
+ handleAddToCart(bopisInput);
96
+ };
97
+
98
+ const getInventoryLabel = (): string | null => {
99
+ const aggregate =
100
+ shippingInventoryData?.AggregatedProductInventoryAvailabilities &&
101
+ shippingInventoryData?.AggregatedProductInventoryAvailabilities[0];
102
+ if (aggregate) {
103
+ const { TotalAvailableInventoryLevelLabel } = aggregate;
104
+ return TotalAvailableInventoryLevelLabel || null;
105
+ }
106
+ return null;
107
+ };
108
+
109
+ return (
110
+ <DialogContent
111
+ sx={{
112
+ padding: 2,
113
+ }}
114
+ >
115
+ <Grid container spacing={3} justifyContent="space-around" mt={0}>
116
+ <Box height={"100%"} sx={{ marginTop: "auto", marginBottom: "auto" }}>
117
+ <ImageWithFallback
118
+ src={product.PrimaryImageUrl ?? ""}
119
+ alt={product.Name || "Product Image"}
120
+ width="300"
121
+ height="300"
122
+ />
123
+ </Box>
124
+ <Grid item md={6} xs={12} alignItems="center" p={4}>
125
+ <H1 mb={1}>{product.Name}</H1>
126
+ <FlexBox alignItems="center" mb={1}>
127
+ <H6>SKU: {product.ItemId}</H6>
128
+ </FlexBox>
129
+
130
+ {/* PRODUCT RATING */}
131
+ <FlexBox alignItems="center" gap={1} mb={2}>
132
+ <ProductRating rating={product.TotalRatings || 0} mb={1} />
133
+ </FlexBox>
134
+
135
+ {/* PRODUCT VARIANTS */}
136
+ {productDetails ? (
137
+ <ProductDimensions
138
+ dimensionGroups={productDetails.DimensionValues}
139
+ selectDimensions={selectDimensions}
140
+ handleChangeVariant={handleChangeVariant}
141
+ showDimensionError={showDimensionError}
142
+ />
143
+ ) : null}
144
+
145
+ {/* PRODUCT PRICE & STOCK */}
146
+ <Box pt={1} mb={3}>
147
+ <H2 color="primary.main" mb={0.5} lineHeight="1">
148
+ {formatCurrency(product.Price)}
149
+ </H2>
150
+ <Box color="inherit">{}</Box>
151
+ </Box>
152
+
153
+ <Box sx={{ my: 3 }}>
154
+ {inventoryLoading ? (
155
+ <CircularProgress size={20} />
156
+ ) : (
157
+ <Typography
158
+ variant="body2"
159
+ color="text.secondary"
160
+ fontWeight={600}
161
+ >
162
+ {getInventoryLabel()}
163
+ </Typography>
164
+ )}
165
+ </Box>
166
+
167
+ <Link
168
+ href={buildDetailUrl(product.Name ?? "", product.RecordId || 0)}
169
+ >
170
+ <Typography
171
+ sx={{
172
+ color: "primary.main",
173
+ "&:hover": {
174
+ textDecoration: "underline",
175
+ },
176
+ }}
177
+ >
178
+ {t("seeFullDetails")}
179
+ </Typography>
180
+ </Link>
181
+
182
+ {/* ACTION BUTTONS */}
183
+ <Box sx={{ mt: 2 }}>
184
+ <QuantityButtons
185
+ loading={loading}
186
+ handleAddToCart={onAddToCart}
187
+ productCartLine={currentCartLine}
188
+ handleCartAmountChange={handleCartAmountChange}
189
+ isOOS={false}
190
+ />
191
+ <Button
192
+ color="primary"
193
+ variant="outlined"
194
+ size="small"
195
+ onClick={handleClose}
196
+ sx={{ height: 36.5, ml: 2, px: 4 }}
197
+ >
198
+ {t("close")}
199
+ </Button>
200
+ </Box>
201
+ </Grid>
202
+ </Grid>
203
+ </DialogContent>
204
+ );
205
+ }
@@ -0,0 +1,54 @@
1
+ import { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
2
+ import { CircularProgress, Dialog, DialogContent } from "@mui/material";
3
+ import getCart from "@/api-manager/services/cart/queries/get-cart";
4
+ import getProductById from "@/api-manager/services/product/queries/get-product-by-id";
5
+ import QuickViewContent from "./quick-view-dialog-content";
6
+
7
+ interface Props {
8
+ open: boolean;
9
+ handleClose: () => void;
10
+ product: ProductSearchResult;
11
+ }
12
+
13
+ export default function QuickView({ open, handleClose, product }: Props) {
14
+ if (!open) {
15
+ return;
16
+ }
17
+
18
+ const { data: cart } = getCart.useData();
19
+ const { data: productDetails } = getProductById.useData({
20
+ id: product.RecordId,
21
+ });
22
+
23
+ return (
24
+ <Dialog
25
+ fullWidth={true}
26
+ maxWidth="md"
27
+ open={open}
28
+ onClose={handleClose}
29
+ aria-labelledby="quick-view-dialog-title"
30
+ >
31
+ {cart && productDetails ? (
32
+ <QuickViewContent
33
+ product={product}
34
+ cart={cart}
35
+ productDetails={productDetails}
36
+ handleClose={handleClose}
37
+ />
38
+ ) : (
39
+ <DialogContent
40
+ sx={{
41
+ padding: 2,
42
+ width: "100%",
43
+ height: "200px",
44
+ display: "flex",
45
+ justifyContent: "center",
46
+ alignItems: "center",
47
+ }}
48
+ >
49
+ <CircularProgress size={20} sx={{ width: "200px" }} />
50
+ </DialogContent>
51
+ )}
52
+ </Dialog>
53
+ );
54
+ }
@@ -0,0 +1,125 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Box, Typography, Slider, TextField } from "@mui/material";
5
+ import { RenderedFacetProps } from "./facet-group";
6
+ import {
7
+ ProductRefinerSource,
8
+ ProductRefinerValue,
9
+ } from "@msdyn365-commerce/retail-proxy";
10
+ import React from "react";
11
+
12
+ type Props = RenderedFacetProps & {
13
+ selectedValues?: ProductRefinerValue;
14
+ };
15
+ export default function RangeFilter({
16
+ facet,
17
+ handleRefinerSelect,
18
+ selectedValues,
19
+ }: Props) {
20
+ const getMinValForSlider = () => {
21
+ const item = facet.Values?.[0];
22
+ if (item) {
23
+ return Number(item.LeftValueBoundString);
24
+ }
25
+ return 0;
26
+ };
27
+
28
+ const getMaxValForSlider = () => {
29
+ const item = facet.Values?.[0];
30
+ if (item) {
31
+ return Number(item.RightValueBoundString);
32
+ }
33
+ return 0;
34
+ };
35
+ const [minVal, setMinVal] = useState<number>(getMinValForSlider());
36
+ const [maxVal, setMaxVal] = useState<number>(getMaxValForSlider());
37
+ const [textMin, setTextMin] = useState<number>(getMinValForSlider());
38
+ const [textMax, setTextMax] = useState<number>(getMaxValForSlider());
39
+ const [selected, setSelected] = useState<boolean>(false);
40
+
41
+ useEffect(() => {
42
+ if (selectedValues) {
43
+ setMinVal(Number(selectedValues.LeftValueBoundString));
44
+ setMaxVal(Number(selectedValues.RightValueBoundString));
45
+ setTextMin(Number(selectedValues.LeftValueBoundString));
46
+ setTextMax(Number(selectedValues.RightValueBoundString));
47
+ } else {
48
+ }
49
+ }, [selectedValues]);
50
+
51
+ useEffect(() => {
52
+ // debouncing
53
+ const request = setTimeout(pushPriceFilter, 600);
54
+ return () => clearTimeout(request);
55
+ });
56
+
57
+ const handleSliderChange = (_: Event, newValue: number | number[]) => {
58
+ if (Array.isArray(newValue)) {
59
+ setMinVal(Number(newValue[0]));
60
+ setTextMin(Number(newValue[0]));
61
+ setMaxVal(Number(newValue[1]));
62
+ setTextMax(Number(newValue[1]));
63
+ setSelected(true);
64
+ }
65
+ };
66
+
67
+ const handleTextMin = (evt: React.ChangeEvent<HTMLInputElement>) => {
68
+ const val = Number(evt.target.value);
69
+ setTextMin(val);
70
+ setMinVal(val);
71
+ setSelected(true);
72
+ };
73
+
74
+ const handleTextMax = (evt: React.ChangeEvent<HTMLInputElement>) => {
75
+ const val = Number(evt.target.value);
76
+ setTextMax(val);
77
+ setMaxVal(val);
78
+ setSelected(true);
79
+ };
80
+
81
+ const pushPriceFilter = () => {
82
+ if (selected) {
83
+ const refiner: ProductRefinerValue = {
84
+ RefinerRecordId: 0,
85
+ RefinerSourceValue: ProductRefinerSource.Price,
86
+ LeftValueBoundString: minVal.toString(),
87
+ RightValueBoundString: maxVal.toString(),
88
+ DataTypeValue: 1,
89
+ };
90
+ handleRefinerSelect(refiner);
91
+ }
92
+ };
93
+
94
+ return (
95
+ <Box mb={3}>
96
+ <Slider
97
+ min={getMinValForSlider()}
98
+ max={getMaxValForSlider()}
99
+ size="small"
100
+ value={[minVal, maxVal]}
101
+ valueLabelDisplay="auto"
102
+ onChange={handleSliderChange}
103
+ />
104
+ <Box display="flex" gap={1} alignItems="center">
105
+ <TextField
106
+ fullWidth
107
+ size="small"
108
+ type="number"
109
+ label="Min"
110
+ value={textMin}
111
+ onChange={handleTextMin}
112
+ />
113
+ <Typography>-</Typography>
114
+ <TextField
115
+ fullWidth
116
+ size="small"
117
+ type="number"
118
+ label="Max"
119
+ value={textMax}
120
+ onChange={handleTextMax}
121
+ />
122
+ </Box>
123
+ </Box>
124
+ );
125
+ }
@@ -0,0 +1,38 @@
1
+ import { ReactNode } from "react";
2
+ import { SxProps, Theme } from "@mui/material/styles";
3
+ import { Props } from "simplebar-react";
4
+ import "simplebar-react/dist/simplebar.min.css";
5
+ import { alpha, styled } from "@mui/material/styles";
6
+ import SimpleBar from "simplebar-react";
7
+
8
+ // =============================================================
9
+ interface ScrollbarProps extends Props {
10
+ autoHide?: boolean;
11
+ sx?: SxProps<Theme>;
12
+ children: ReactNode;
13
+ }
14
+ // =============================================================
15
+
16
+ const StyledScrollBar = styled(SimpleBar)(({ theme }) => ({
17
+ maxHeight: "100%",
18
+ "& .simplebar-scrollbar": {
19
+ "&.simplebar-visible:before": { opacity: 1 },
20
+ "&:before": { backgroundColor: alpha(theme.palette.grey[400], 0.6) },
21
+ },
22
+ "& .simplebar-track.simplebar-vertical": { width: 9 },
23
+ "& .simplebar-track.simplebar-horizontal .simplebar-scrollbar": { height: 6 },
24
+ "& .simplebar-mask": { zIndex: "inherit" },
25
+ }));
26
+
27
+ export default function Scrollbar({
28
+ children,
29
+ autoHide = true,
30
+ sx,
31
+ ...props
32
+ }: ScrollbarProps) {
33
+ return (
34
+ <StyledScrollBar sx={sx} autoHide={autoHide} {...props}>
35
+ {children}
36
+ </StyledScrollBar>
37
+ );
38
+ }
@@ -0,0 +1,162 @@
1
+ "use client";
2
+ import {
3
+ Box,
4
+ MenuItem,
5
+ Typography,
6
+ ToggleButtonGroup,
7
+ ToggleButton,
8
+ TextField,
9
+ } from "@mui/material";
10
+ import Apps from "@mui/icons-material/Apps";
11
+ import ViewList from "@mui/icons-material/ViewList";
12
+ import FilterList from "@mui/icons-material/FilterList";
13
+ import IconButton from "@mui/material/IconButton";
14
+ import { Theme } from "@mui/material/styles";
15
+ import { ViewMode } from "./utils/product-list-types";
16
+ import useProductList from "./use-product-list";
17
+ import { useTranslations } from "next-intl";
18
+ import useMediaQuery from "@mui/material/useMediaQuery";
19
+ import Sidenav from "./side-nav";
20
+ import React from "react";
21
+
22
+ const sxView = {
23
+ display: "flex",
24
+ border: "none",
25
+ "& button": {
26
+ border: "none",
27
+ borderTopRightRadius: "50% !important",
28
+ borderBottomRightRadius: "50% !important",
29
+ borderTopLeftRadius: "50% !important",
30
+ borderBottomLeftRadius: "50% !important",
31
+ },
32
+ };
33
+
34
+ interface SearchBarProps {
35
+ children?: React.ReactNode;
36
+ }
37
+
38
+ const SearchBar = ({ children }: SearchBarProps) => {
39
+ const {
40
+ itemsPerPage,
41
+ sortOptions,
42
+ selectedSort,
43
+ view,
44
+ paramHandlers: { handleSortChange, handleViewChange, handlePerPageChange },
45
+ } = useProductList();
46
+ const t = useTranslations("ProductList.SearchBar");
47
+ const downMd = useMediaQuery((theme: Theme) => theme.breakpoints.down("md"));
48
+
49
+ const handleSelectedOption = (e: React.ChangeEvent<HTMLInputElement>) => {
50
+ const codeSelection = e.target.value;
51
+ const optionSelection = sortOptions.find(
52
+ (option) => option.code === codeSelection,
53
+ );
54
+ if (optionSelection !== undefined) {
55
+ handleSortChange(optionSelection);
56
+ }
57
+ };
58
+ return (
59
+ <Box
60
+ display="flex"
61
+ alignItems="center"
62
+ columnGap={4}
63
+ flexWrap="wrap"
64
+ justifyContent={downMd ? "flex-start" : "flex-end"}
65
+ >
66
+ <Box display="flex" alignItems="center" gap={1} flex="1 1 0" pb={1}>
67
+ <Typography color="grey.600" whiteSpace="pre">
68
+ Per page:
69
+ </Typography>
70
+
71
+ <TextField
72
+ select
73
+ fullWidth
74
+ size="small"
75
+ value={itemsPerPage}
76
+ variant="outlined"
77
+ placeholder="Sort by"
78
+ onChange={(e) => handlePerPageChange(Number(e.target.value))}
79
+ sx={{ flex: "1 1 0", minWidth: "150px" }}
80
+ >
81
+ <MenuItem value={20}>20</MenuItem>
82
+ <MenuItem value={50}>50</MenuItem>
83
+ <MenuItem value={100}>100</MenuItem>
84
+ </TextField>
85
+ </Box>
86
+
87
+ <Box display="flex" alignItems="center" gap={1} flex="1 1 0" pb={1}>
88
+ <Typography color="grey.600" whiteSpace="pre">
89
+ Sort by:
90
+ </Typography>
91
+
92
+ <TextField
93
+ select
94
+ fullWidth
95
+ size="small"
96
+ value={selectedSort}
97
+ variant="outlined"
98
+ placeholder="Sort by"
99
+ onChange={handleSelectedOption}
100
+ sx={{ flex: "1 1 0", minWidth: "150px" }}
101
+ >
102
+ {sortOptions.map((option) => (
103
+ <MenuItem key={option.code} value={option.code}>
104
+ {option.displayName}
105
+ </MenuItem>
106
+ ))}
107
+ </TextField>
108
+ </Box>
109
+
110
+ <Box display="flex" alignItems="center" my="0.25rem" pb={1}>
111
+ <Typography color="grey.600" mr={1}>
112
+ View:
113
+ </Typography>
114
+
115
+ <ToggleButtonGroup
116
+ exclusive
117
+ value={view}
118
+ onChange={(_e, value: ViewMode) => handleViewChange(value)}
119
+ sx={sxView}
120
+ >
121
+ <ToggleButton
122
+ value="grid"
123
+ aria-label={t("gridView")}
124
+ title={t("gridView")}
125
+ >
126
+ <Apps
127
+ fontSize="small"
128
+ color={view === "grid" ? "primary" : "inherit"}
129
+ />
130
+ </ToggleButton>
131
+ <ToggleButton
132
+ value="list"
133
+ aria-label={t("listView")}
134
+ title={t("listView")}
135
+ >
136
+ <ViewList
137
+ fontSize="small"
138
+ color={view === "list" ? "primary" : "inherit"}
139
+ />
140
+ </ToggleButton>
141
+ </ToggleButtonGroup>
142
+
143
+ {/* SHOW IN THE SMALL DEVICE */}
144
+ {downMd && (
145
+ <Sidenav
146
+ handler={(close) => (
147
+ <IconButton onClick={close}>
148
+ <FilterList fontSize="small" />
149
+ </IconButton>
150
+ )}
151
+ >
152
+ <Box px={3} py={2}>
153
+ {children}
154
+ </Box>
155
+ </Sidenav>
156
+ )}
157
+ </Box>
158
+ </Box>
159
+ );
160
+ };
161
+
162
+ export default SearchBar;
@@ -0,0 +1,80 @@
1
+ import {
2
+ ProductRefinerSource,
3
+ ProductRefinerValue,
4
+ } from "@msdyn365-commerce/retail-proxy";
5
+ import { Button, Chip, Stack } from "@mui/material";
6
+ import { Paragraph } from "@/components/_components/Typography";
7
+ import React, { useState, useEffect } from "react";
8
+ import useProductList from "./use-product-list";
9
+ import { useTranslations } from "next-intl";
10
+
11
+ export default function SelectedFacets() {
12
+ const t = useTranslations("ProductList.Facets");
13
+ const {
14
+ selectedRefiners,
15
+ productData,
16
+ paramHandlers: { handleRefinerChange, handleRefinersClear },
17
+ } = useProductList();
18
+
19
+ const refiners = productData?.refiners;
20
+ const [lastRefiners, setLastRefiners] = useState<typeof refiners>([]);
21
+
22
+ useEffect(() => {
23
+ if (refiners && refiners.length > 0) {
24
+ setLastRefiners(refiners);
25
+ }
26
+ }, [refiners]);
27
+
28
+ const getRefinerLabel = (refiner: ProductRefinerValue) => {
29
+ const refinerRecord = lastRefiners?.find(
30
+ (r) => r.RecordId === refiner.RefinerRecordId,
31
+ );
32
+
33
+ // RefinerRecordId for some filters is sent as 0 instead of valid value.
34
+ // category refiner RecordId in lastRefiners is 0 but here (RefinerRecordId) its a valid value.
35
+ if (!refinerRecord || refiner.RefinerRecordId === 0) {
36
+ const sourceValue = refiner.RefinerSourceValue;
37
+
38
+ const sourceLabel = ProductRefinerSource[sourceValue ?? 0];
39
+
40
+ if (
41
+ sourceLabel !== undefined &&
42
+ sourceValue !== ProductRefinerSource.Attribute
43
+ ) {
44
+ return sourceLabel;
45
+ }
46
+ }
47
+
48
+ return refinerRecord?.KeyName ?? "Unknown"; // use this to avoid undefined.
49
+ };
50
+
51
+ const getRefinerValue = (refiner: ProductRefinerValue) => {
52
+ if (refiner.RefinerSourceValue === ProductRefinerSource.Price) {
53
+ return `${refiner.LeftValueBoundString} - ${refiner.RightValueBoundString}`;
54
+ }
55
+ if (refiner.RefinerSourceValue === ProductRefinerSource.Rating) {
56
+ return `${refiner.LeftValueBoundString} stars and up`;
57
+ }
58
+ return (
59
+ refiner.LeftValueBoundLocalizedString || refiner.LeftValueBoundString
60
+ );
61
+ };
62
+
63
+ return (
64
+ <Stack direction={"row"} spacing={2}>
65
+ {selectedRefiners.map((refiner) => (
66
+ <Chip
67
+ key={`${refiner.RefinerRecordId}-${refiner.LeftValueBoundString}`}
68
+ title={refiner.LeftValueBoundString}
69
+ label={`${getRefinerLabel(refiner)}: ${getRefinerValue(refiner)}`}
70
+ onDelete={() => handleRefinerChange(refiner)}
71
+ />
72
+ ))}
73
+ {selectedRefiners.length > 0 ? (
74
+ <Button variant={"text"} sx={{ ml: 2 }} onClick={handleRefinersClear}>
75
+ <Paragraph>{t("clearAll")}</Paragraph>
76
+ </Button>
77
+ ) : undefined}
78
+ </Stack>
79
+ );
80
+ }