@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.
- package/package.json +3 -2
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +1 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +0 -19
- package/src/api-manager/datasources/e4/e4-cart.datasource.ts +2 -2
- package/src/api-manager/datasources/e4/e4-product.datasource.ts +19 -4
- package/src/api-manager/datasources/e4/e4.datasource.ts +1 -2
- package/src/api-manager/datasources/e4/e4.remaps.ts +24 -0
- package/src/api-manager/datasources/e4/e4.translator.ts +17 -0
- package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
- package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
- package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
- package/src/api-manager/index.ts +21 -6
- package/src/api-manager/schemas/product.schema.ts +1 -1
- package/src/api-manager/services/address/queries/get-states.ts +1 -1
- package/src/api-manager/services/create-query.ts +10 -3
- package/src/api-manager/types/Datasource.ts +9 -9
- package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
- package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
- package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
- package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
- package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
- package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
- package/src/components/_components/section-header/section-header.tsx +1 -1
- package/src/components/_components/settings/setting.tsx +1 -1
- package/src/components/_components/wishlist-modal/TODO.md +10 -0
- package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
- package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
- package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
- package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
- package/src/components/countries-input.tsx +1 -0
- package/src/hooks/use-cart.tsx +82 -0
- package/src/hooks/use-variants.tsx +85 -0
- package/src/pages/account/addresses/address-form.tsx +289 -0
- package/src/pages/account/addresses/address-item.tsx +104 -0
- package/src/pages/account/dashboard-header.tsx +123 -0
- package/src/pages/account/navigation.tsx +84 -0
- package/src/pages/account/no-records.tsx +20 -0
- package/src/pages/account/orders/icons/delivery.tsx +14 -0
- package/src/pages/account/orders/icons/package-box.tsx +13 -0
- package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
- package/src/pages/account/orders/order-progress.tsx +111 -0
- package/src/pages/account/orders/order-row.tsx +61 -0
- package/src/pages/account/orders/order-summary.tsx +83 -0
- package/src/pages/account/orders/ordered-products.tsx +114 -0
- package/src/pages/account/profile/profile-button.test.tsx +59 -0
- package/src/pages/account/profile/profile-button.tsx +50 -0
- package/src/pages/account/profile/user-info.tsx +59 -0
- package/src/pages/account/styles.ts +32 -0
- package/src/pages/account/table-row.tsx +19 -0
- package/src/pages/account/wishlist/create-new-list.tsx +167 -0
- package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
- package/src/pages/blog/pagination.tsx +38 -0
- package/src/pages/cart/cart-item.tsx +312 -0
- package/src/pages/cart/checkout-form.tsx +122 -0
- package/src/pages/cart/coupon-entry.tsx +90 -0
- package/src/pages/cart/estimate-shipping.tsx +183 -0
- package/src/pages/cart/wrapper.tsx +30 -0
- package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
- package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
- package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
- package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
- package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
- package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
- package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
- package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
- package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
- package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
- package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
- package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
- package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
- package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
- package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
- package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
- package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
- package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
- package/src/pages/confirmation/address.tsx +22 -0
- package/src/pages/confirmation/confirmation-summary.tsx +52 -0
- package/src/pages/confirmation/ordered-products.tsx +108 -0
- package/src/pages/product-details/available-shops.tsx +48 -0
- package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
- package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
- package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
- package/src/pages/product-details/bopis/search-header.tsx +69 -0
- package/src/pages/product-details/checkbox-label.tsx +20 -0
- package/src/pages/product-details/currency.ts +64 -0
- package/src/pages/product-details/frequently-bought.tsx +90 -0
- package/src/pages/product-details/frequently-product-card.tsx +60 -0
- package/src/pages/product-details/product-comment.tsx +44 -0
- package/src/pages/product-details/product-description.tsx +22 -0
- package/src/pages/product-details/product-filter-card.tsx +257 -0
- package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
- package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
- package/src/pages/product-details/product-review.tsx +131 -0
- package/src/pages/product-details/product-tabs.tsx +51 -0
- package/src/pages/product-details/related-products.tsx +42 -0
- package/src/pages/product-details/types.ts +11 -0
- package/src/pages/product-list/breadcrumbs.tsx +39 -0
- package/src/pages/product-list/checkbox-label.tsx +20 -0
- package/src/pages/product-list/facet-group.tsx +125 -0
- package/src/pages/product-list/facet.tsx +36 -0
- package/src/pages/product-list/list-filter.tsx +40 -0
- package/src/pages/product-list/pagination.tsx +80 -0
- package/src/pages/product-list/product-list-context.tsx +302 -0
- package/src/pages/product-list/product-list-state.ts +187 -0
- package/src/pages/product-list/product-rating.tsx +16 -0
- package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
- package/src/pages/product-list/quick-view-dialog.tsx +54 -0
- package/src/pages/product-list/range-filter.tsx +125 -0
- package/src/pages/product-list/scrollbar.tsx +38 -0
- package/src/pages/product-list/search-bar.tsx +162 -0
- package/src/pages/product-list/selected-facets.tsx +80 -0
- package/src/pages/product-list/side-nav.tsx +49 -0
- package/src/pages/product-list/swatch.tsx +80 -0
- package/src/pages/product-list/types.ts +18 -0
- package/src/pages/product-list/use-product-list.ts +12 -0
- package/src/pages/product-list/useCategoryFilter.tsx +26 -0
- package/src/pages/product-list/utils/facet-helpers.ts +5 -0
- package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
- package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
- package/src/pages/product-list/utils/product-list-helper.ts +111 -0
- package/src/pages/product-list/utils/product-list-types.ts +41 -0
- package/src/pages/product-list/utils/search-for-category.ts +76 -0
- package/src/pages/product-list/utils/sort-options.ts +44 -0
- package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
- package/src/pages/quickorder/order-upload.tsx +150 -0
- package/src/pages/quickorder/quick-order-form.tsx +343 -0
- package/src/pages/quickorder/quick-order-row.tsx +144 -0
- package/tsconfig.json +1 -0
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +0 -10464
- /package/src/{components/_components/hooks/useOverflowDetect.ts → hooks/use-overflow-detect.ts} +0 -0
- /package/src/{components/_components/hooks/useSettings.ts → hooks/use-settings.ts} +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import Grid from "@mui/material/Grid2";
|
|
5
|
+
import Rating from "@mui/material/Rating";
|
|
6
|
+
import { CircularProgress, Typography } from "@mui/material";
|
|
7
|
+
|
|
8
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
9
|
+
import useCurrencyFormatter from "@/lib/currency-formatter";
|
|
10
|
+
import { H1, H2, H6 } from "@/components/_components/Typography";
|
|
11
|
+
import { FlexBox } from "@/components/_components/flex-box";
|
|
12
|
+
import QuantityButtons from "@/components/_components/product-quantity-buttons/product-quantity-buttons";
|
|
13
|
+
import AddToWishlistButton from "@/components/_components/wishlist-modal/add-to-wishlist-button";
|
|
14
|
+
// D365
|
|
15
|
+
import { ProductDimensionValueInventoryAvailability } from "@msdyn365-commerce/retail-proxy";
|
|
16
|
+
|
|
17
|
+
// API MANAGER
|
|
18
|
+
import {
|
|
19
|
+
InventorySearchCriteriaInput,
|
|
20
|
+
ProductDetails,
|
|
21
|
+
} from "@/api-manager/schemas/product.schema";
|
|
22
|
+
import { Cart } from "@/api-manager/schemas/cart.schema";
|
|
23
|
+
import getProductPrices from "@/api-manager/services/product/queries/get-product-prices";
|
|
24
|
+
import getProductAvailability from "@/api-manager/services/product/queries/get-product-availability";
|
|
25
|
+
|
|
26
|
+
// LOCAL HOOKS
|
|
27
|
+
import useVariants from "../../../hooks/use-variants";
|
|
28
|
+
import useCart, { AddToCartBopisInput } from "@/hooks/use-cart";
|
|
29
|
+
|
|
30
|
+
// LOCAL COMPONENTS
|
|
31
|
+
import ProductDimensions from "@/_components/product-dimensions/product-dimensions";
|
|
32
|
+
import ProductImages from "./product-intro-images";
|
|
33
|
+
import FindInStoreButton from "../bopis/find-in-store-button";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Master product data comes in as prop, selected variant details are called
|
|
37
|
+
* every render, if there is a valid variant id selected.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// ================================================================
|
|
41
|
+
type Props = { product: ProductDetails; cart: Cart };
|
|
42
|
+
// ================================================================
|
|
43
|
+
|
|
44
|
+
export default function ProductIntro({ product, cart }: Props) {
|
|
45
|
+
const [selectedImageIndex, setSelectedImage] = useState<number>(0);
|
|
46
|
+
const [showDimensionError, setShowDimensionError] = useState(false);
|
|
47
|
+
const [productRedId, setProductRedId] = useState<number>(product.RecordId);
|
|
48
|
+
|
|
49
|
+
const { Name, DimensionValues, Images, ItemId, RecordId } = product;
|
|
50
|
+
|
|
51
|
+
const { data: productPrices } = getProductPrices.useData({
|
|
52
|
+
productIds: [Number(productRedId)],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// HOOKS
|
|
56
|
+
const {
|
|
57
|
+
productVariant,
|
|
58
|
+
selectVariantId,
|
|
59
|
+
selectDimensions,
|
|
60
|
+
handleChangeVariant,
|
|
61
|
+
allDimensionsSelected,
|
|
62
|
+
} = useVariants({ product });
|
|
63
|
+
|
|
64
|
+
const { handleCartAmountChange, handleAddToCart, loading } = useCart({
|
|
65
|
+
cart,
|
|
66
|
+
selectVariantId,
|
|
67
|
+
recordId: RecordId,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { formatCurrency } = useCurrencyFormatter();
|
|
71
|
+
|
|
72
|
+
const searchCriteria = useMemo<InventorySearchCriteriaInput>(
|
|
73
|
+
() => ({
|
|
74
|
+
allDimensionsSelected: allDimensionsSelected(selectDimensions),
|
|
75
|
+
searchCriteria: {
|
|
76
|
+
defaultWarehouseOnly: true,
|
|
77
|
+
productIds: [selectVariantId || RecordId],
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
[allDimensionsSelected, selectDimensions, selectVariantId, RecordId],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const { data: shippingInventoryData, isLoading: inventoryLoading } =
|
|
84
|
+
getProductAvailability.useData(searchCriteria);
|
|
85
|
+
// HELPERS
|
|
86
|
+
const currentCartLine = cart?.CartLines?.find((item) => {
|
|
87
|
+
return item?.ProductId === (Number(selectVariantId) || RecordId);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const getInventoryLabel = (): string | null => {
|
|
91
|
+
const aggregate =
|
|
92
|
+
shippingInventoryData?.AggregatedProductInventoryAvailabilities &&
|
|
93
|
+
shippingInventoryData?.AggregatedProductInventoryAvailabilities[0];
|
|
94
|
+
if (aggregate) {
|
|
95
|
+
const { TotalAvailableInventoryLevelLabel } = aggregate;
|
|
96
|
+
return TotalAvailableInventoryLevelLabel || null;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const shouldDisableAddToCart = () => {
|
|
102
|
+
//TODO: Incorporate global inventory enabled flag.
|
|
103
|
+
if (!allDimensionsSelected(selectDimensions)) {
|
|
104
|
+
return false; // Don't disable add to cart if dimensions are not selected, so clicking the button will provide an error message showing the user what to do.
|
|
105
|
+
}
|
|
106
|
+
const OOSCode = "OOS"; // Always means out of stock
|
|
107
|
+
const aggregate =
|
|
108
|
+
shippingInventoryData?.AggregatedProductInventoryAvailabilities &&
|
|
109
|
+
shippingInventoryData?.AggregatedProductInventoryAvailabilities[0];
|
|
110
|
+
if (aggregate) {
|
|
111
|
+
const { TotalAvailableInventoryLevelCode } = aggregate;
|
|
112
|
+
return TotalAvailableInventoryLevelCode === OOSCode;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// HANDLE SELECT IMAGE
|
|
118
|
+
const handleImageClick = (ind: number) => () => {
|
|
119
|
+
setSelectedImage(ind);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const images =
|
|
123
|
+
Array.isArray(productVariant?.Images) && productVariant.Images.length > 0
|
|
124
|
+
? productVariant.Images
|
|
125
|
+
: Array.isArray(Images) && Images.length > 0
|
|
126
|
+
? Images
|
|
127
|
+
: [];
|
|
128
|
+
|
|
129
|
+
const handleChangeVariantWrapper = (
|
|
130
|
+
dimension: ProductDimensionValueInventoryAvailability,
|
|
131
|
+
) => {
|
|
132
|
+
handleChangeVariant(dimension);
|
|
133
|
+
|
|
134
|
+
const newRecID = dimension?.ProductIds?.[0];
|
|
135
|
+
if (newRecID) setProductRedId(newRecID);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const onAddToCart = (bopisInput?: AddToCartBopisInput) => {
|
|
139
|
+
if (!allDimensionsSelected(selectDimensions)) {
|
|
140
|
+
setShowDimensionError(true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setShowDimensionError(false);
|
|
144
|
+
handleAddToCart(bopisInput);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const shouldOpenHelperModal = () => {
|
|
148
|
+
if (!allDimensionsSelected(selectDimensions)) {
|
|
149
|
+
setShowDimensionError(true);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
setShowDimensionError(false);
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Box width="100%">
|
|
158
|
+
<Grid container spacing={3} justifyContent="space-around">
|
|
159
|
+
<ProductImages
|
|
160
|
+
name={Name}
|
|
161
|
+
selectedImageIndex={selectedImageIndex}
|
|
162
|
+
primaryImageUrl={
|
|
163
|
+
Array.isArray(productVariant?.Images) &&
|
|
164
|
+
productVariant.Images.length > 0
|
|
165
|
+
? productVariant.Images[0]
|
|
166
|
+
: Array.isArray(Images) && Images.length > 0
|
|
167
|
+
? Images[0]
|
|
168
|
+
: ""
|
|
169
|
+
}
|
|
170
|
+
images={images}
|
|
171
|
+
handleImageClick={handleImageClick}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
{/* PRODUCT INFO AREA */}
|
|
175
|
+
<Grid size={{ md: 6, xs: 12 }} alignItems="center">
|
|
176
|
+
{/* PRODUCT NAME */}
|
|
177
|
+
<H1 mb={1}>{Name}</H1>
|
|
178
|
+
|
|
179
|
+
{/* PRODUCT BRAND */}
|
|
180
|
+
<FlexBox alignItems="center" mb={1}>
|
|
181
|
+
<H6>SKU: {productVariant?.ItemId || ItemId}</H6>
|
|
182
|
+
</FlexBox>
|
|
183
|
+
|
|
184
|
+
{/* PRODUCT RATING */}
|
|
185
|
+
<FlexBox alignItems="center" gap={1} mb={2}>
|
|
186
|
+
<Box lineHeight="1">Rated:</Box>
|
|
187
|
+
<Rating color="warn" value={0} readOnly />
|
|
188
|
+
<H6 lineHeight="1">(0)</H6>
|
|
189
|
+
</FlexBox>
|
|
190
|
+
|
|
191
|
+
{/* PRODUCT VARIANTS */}
|
|
192
|
+
<ProductDimensions
|
|
193
|
+
dimensionGroups={DimensionValues}
|
|
194
|
+
selectDimensions={selectDimensions}
|
|
195
|
+
handleChangeVariant={handleChangeVariantWrapper}
|
|
196
|
+
showDimensionError={showDimensionError}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
{/* PRODUCT PRICE & STOCK */}
|
|
200
|
+
<Box pt={1} mb={3}>
|
|
201
|
+
<H2 color="primary.main" mb={0.5} lineHeight="1">
|
|
202
|
+
{productPrices && productPrices[0].CustomerContextualPrice ? (
|
|
203
|
+
formatCurrency(productPrices[0].CustomerContextualPrice)
|
|
204
|
+
) : (
|
|
205
|
+
<CircularProgress size={20} />
|
|
206
|
+
)}
|
|
207
|
+
</H2>
|
|
208
|
+
<Box color="inherit">{}</Box>
|
|
209
|
+
</Box>
|
|
210
|
+
<FlexBox alignItems="center" gap={1} mb={2}>
|
|
211
|
+
{/* INVENTORY */}
|
|
212
|
+
<Box sx={{ my: 3 }}>
|
|
213
|
+
{inventoryLoading ? (
|
|
214
|
+
<CircularProgress size={20} />
|
|
215
|
+
) : (
|
|
216
|
+
<Typography
|
|
217
|
+
variant="body2"
|
|
218
|
+
color="text.secondary"
|
|
219
|
+
fontWeight={600}
|
|
220
|
+
>
|
|
221
|
+
{getInventoryLabel()}
|
|
222
|
+
</Typography>
|
|
223
|
+
)}
|
|
224
|
+
</Box>
|
|
225
|
+
|
|
226
|
+
<FlexBox gap={2}>
|
|
227
|
+
<QuantityButtons
|
|
228
|
+
loading={loading}
|
|
229
|
+
handleAddToCart={onAddToCart}
|
|
230
|
+
productCartLine={currentCartLine}
|
|
231
|
+
handleCartAmountChange={handleCartAmountChange}
|
|
232
|
+
isOOS={shouldDisableAddToCart()}
|
|
233
|
+
/>
|
|
234
|
+
<FindInStoreButton
|
|
235
|
+
productId={selectVariantId || RecordId}
|
|
236
|
+
canOpenModal={shouldOpenHelperModal}
|
|
237
|
+
handleAddToCart={handleAddToCart}
|
|
238
|
+
allDimensionsSelected={allDimensionsSelected(selectDimensions)}
|
|
239
|
+
/>
|
|
240
|
+
<AddToWishlistButton
|
|
241
|
+
path="Account.Wishlists.btnAddToWishlist"
|
|
242
|
+
product={product}
|
|
243
|
+
/>
|
|
244
|
+
</FlexBox>
|
|
245
|
+
</FlexBox>
|
|
246
|
+
</Grid>
|
|
247
|
+
</Grid>
|
|
248
|
+
</Box>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import Button from "@mui/material/Button";
|
|
5
|
+
import Rating from "@mui/material/Rating";
|
|
6
|
+
import TextField from "@mui/material/TextField";
|
|
7
|
+
import * as yup from "yup";
|
|
8
|
+
import { useFormik } from "formik";
|
|
9
|
+
// Local CUSTOM COMPONENT
|
|
10
|
+
import ProductComment from "./product-comment";
|
|
11
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
12
|
+
import { FlexBox } from "@/components/_components/flex-box";
|
|
13
|
+
import { H2, H5 } from "@/components/_components/Typography";
|
|
14
|
+
|
|
15
|
+
export default function ProductReview() {
|
|
16
|
+
const initialValues = {
|
|
17
|
+
rating: 0,
|
|
18
|
+
comment: "",
|
|
19
|
+
date: new Date().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const validationSchema = yup.object().shape({
|
|
23
|
+
rating: yup.number().required("required"),
|
|
24
|
+
comment: yup.string().required("required"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
dirty,
|
|
29
|
+
values,
|
|
30
|
+
errors,
|
|
31
|
+
touched,
|
|
32
|
+
isValid,
|
|
33
|
+
handleBlur,
|
|
34
|
+
handleChange,
|
|
35
|
+
handleSubmit,
|
|
36
|
+
setFieldValue,
|
|
37
|
+
} = useFormik({
|
|
38
|
+
initialValues,
|
|
39
|
+
validationSchema,
|
|
40
|
+
onSubmit: async (_values, { resetForm }) => {
|
|
41
|
+
resetForm();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
{commentList.map((item, ind) => (
|
|
48
|
+
<ProductComment {...item} key={ind} />
|
|
49
|
+
))}
|
|
50
|
+
|
|
51
|
+
<H2 fontWeight="600" mt={7} mb={2.5}>
|
|
52
|
+
Write a Review for this product
|
|
53
|
+
</H2>
|
|
54
|
+
|
|
55
|
+
<form onSubmit={handleSubmit}>
|
|
56
|
+
<Box mb={2.5}>
|
|
57
|
+
<FlexBox mb={1.5} gap={0.5}>
|
|
58
|
+
<H5 color="grey.700">Your Rating</H5>
|
|
59
|
+
<H5 color="error.main">*</H5>
|
|
60
|
+
</FlexBox>
|
|
61
|
+
|
|
62
|
+
<Rating
|
|
63
|
+
color="warn"
|
|
64
|
+
size="medium"
|
|
65
|
+
value={values.rating}
|
|
66
|
+
onChange={(_, value: number | null) =>
|
|
67
|
+
setFieldValue("rating", value)
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
</Box>
|
|
71
|
+
|
|
72
|
+
<Box mb={3}>
|
|
73
|
+
<FlexBox mb={1.5} gap={0.5}>
|
|
74
|
+
<H5 color="grey.700">Your Review</H5>
|
|
75
|
+
<H5 color="error.main">*</H5>
|
|
76
|
+
</FlexBox>
|
|
77
|
+
|
|
78
|
+
<TextField
|
|
79
|
+
rows={8}
|
|
80
|
+
multiline
|
|
81
|
+
fullWidth
|
|
82
|
+
name="comment"
|
|
83
|
+
variant="outlined"
|
|
84
|
+
onBlur={handleBlur}
|
|
85
|
+
value={values.comment}
|
|
86
|
+
onChange={handleChange}
|
|
87
|
+
placeholder="Write a review here..."
|
|
88
|
+
error={!!touched.comment && !!errors.comment}
|
|
89
|
+
helperText={(touched.comment && errors.comment) as string}
|
|
90
|
+
/>
|
|
91
|
+
</Box>
|
|
92
|
+
|
|
93
|
+
<Button
|
|
94
|
+
variant="contained"
|
|
95
|
+
color="primary"
|
|
96
|
+
type="submit"
|
|
97
|
+
disabled={!(dirty && isValid)}
|
|
98
|
+
>
|
|
99
|
+
Submit
|
|
100
|
+
</Button>
|
|
101
|
+
</form>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const commentList = [
|
|
107
|
+
{
|
|
108
|
+
name: "Jannie Schumm",
|
|
109
|
+
imgUrl: "/assets/images/faces/7.png",
|
|
110
|
+
rating: 4.7,
|
|
111
|
+
date: "2021-02-14",
|
|
112
|
+
comment:
|
|
113
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Varius massa id ut mattis. Facilisis vitae gravida egestas ac account.",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "Joe Kenan",
|
|
117
|
+
imgUrl: "/assets/images/faces/6.png",
|
|
118
|
+
rating: 4.7,
|
|
119
|
+
date: "2019-08-10",
|
|
120
|
+
comment:
|
|
121
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Varius massa id ut mattis. Facilisis vitae gravida egestas ac account.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "Jenifer Tulio",
|
|
125
|
+
imgUrl: "/assets/images/faces/8.png",
|
|
126
|
+
rating: 4.7,
|
|
127
|
+
date: "2021-02-05",
|
|
128
|
+
comment:
|
|
129
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Varius massa id ut mattis. Facilisis vitae gravida egestas ac account.",
|
|
130
|
+
},
|
|
131
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import Box from "@mui/material/Box";
|
|
5
|
+
import Tab from "@mui/material/Tab";
|
|
6
|
+
import Tabs from "@mui/material/Tabs";
|
|
7
|
+
import styled from "@mui/material/styles/styled";
|
|
8
|
+
// LOCAL CUSTOM COMPONENTS
|
|
9
|
+
import ProductDescription from "./product-description";
|
|
10
|
+
import { AttributeValue } from "@msdyn365-commerce/retail-proxy";
|
|
11
|
+
// STYLED COMPONENT
|
|
12
|
+
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|
13
|
+
minHeight: 0,
|
|
14
|
+
marginTop: 80,
|
|
15
|
+
marginBottom: 24,
|
|
16
|
+
borderBottom: `1px solid ${theme.palette.text.disabled}`,
|
|
17
|
+
"& .inner-tab": {
|
|
18
|
+
minHeight: 40,
|
|
19
|
+
fontWeight: 600,
|
|
20
|
+
textTransform: "capitalize",
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
type Props = { productAttributes: AttributeValue[] };
|
|
25
|
+
|
|
26
|
+
export default function ProductTabs({ productAttributes }: Props) {
|
|
27
|
+
const [selectedOption, setSelectedOption] = useState(0);
|
|
28
|
+
const handleOptionClick = (_: React.SyntheticEvent, value: number) =>
|
|
29
|
+
setSelectedOption(value);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<StyledTabs
|
|
34
|
+
textColor="primary"
|
|
35
|
+
value={selectedOption}
|
|
36
|
+
indicatorColor="primary"
|
|
37
|
+
onChange={handleOptionClick}
|
|
38
|
+
>
|
|
39
|
+
<Tab className="inner-tab" label="Description" />
|
|
40
|
+
<Tab className="inner-tab" label="Review (3)" />
|
|
41
|
+
</StyledTabs>
|
|
42
|
+
|
|
43
|
+
<Box mb={6}>
|
|
44
|
+
{selectedOption === 0 && (
|
|
45
|
+
<ProductDescription attributes={productAttributes} />
|
|
46
|
+
)}
|
|
47
|
+
{/*selectedOption === 1 && <ProductReview />*/}
|
|
48
|
+
</Box>
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import Grid from "@mui/material/Grid2";
|
|
5
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
6
|
+
import { H3 } from "@/components/_components/Typography";
|
|
7
|
+
import ProductCard1Wrapper from "@/components/_components/product-cards/product-card-1/product-card-1-wrapper";
|
|
8
|
+
// CUSTOM DATA MODEL
|
|
9
|
+
import GetRelatedProductsDataFetcher from "@/api-manager/services/product/queries/get-related-products";
|
|
10
|
+
|
|
11
|
+
// ==============================================================
|
|
12
|
+
type Props = { RecordId: number };
|
|
13
|
+
// ==============================================================
|
|
14
|
+
|
|
15
|
+
export default function RelatedProducts({ RecordId }: Props) {
|
|
16
|
+
const { data: products } = GetRelatedProductsDataFetcher.useData({
|
|
17
|
+
id: RecordId,
|
|
18
|
+
});
|
|
19
|
+
return (
|
|
20
|
+
<Box mb={7.5}>
|
|
21
|
+
<H3 mb={3}>Related Products</H3>
|
|
22
|
+
|
|
23
|
+
<Grid container spacing={3}>
|
|
24
|
+
{products?.map((item, ind) => (
|
|
25
|
+
<Grid size={{ lg: 3, md: 4, sm: 6, xs: 12 }} key={ind}>
|
|
26
|
+
<ProductCard1Wrapper
|
|
27
|
+
hoverEffect
|
|
28
|
+
id={item?.ItemId || ""}
|
|
29
|
+
slug={String(item?.RecordId) || ""}
|
|
30
|
+
title={item?.Name || ""}
|
|
31
|
+
price={item.Price}
|
|
32
|
+
rating={4.5}
|
|
33
|
+
imgUrl={item.PrimaryImageUrl || ""}
|
|
34
|
+
discount={10}
|
|
35
|
+
mode="PLP"
|
|
36
|
+
/>
|
|
37
|
+
</Grid>
|
|
38
|
+
))}
|
|
39
|
+
</Grid>
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ProductFilters {
|
|
2
|
+
brand: string[];
|
|
3
|
+
color: string[];
|
|
4
|
+
sales: string[];
|
|
5
|
+
price: number[];
|
|
6
|
+
rating: number;
|
|
7
|
+
// category: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ProductFilterKeys = keyof ProductFilters;
|
|
11
|
+
export type ProductFilterValues = ProductFilters[ProductFilterKeys];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Breadcrumbs, Typography } from "@mui/material";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import {
|
|
5
|
+
getCategoryTranslatedName,
|
|
6
|
+
getCategoryUrl,
|
|
7
|
+
} from "./utils/search-for-category";
|
|
8
|
+
import { CategoryHierarchy } from "@/lib/category-helpers";
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
breadcrumbs: CategoryHierarchy[];
|
|
12
|
+
locale: string;
|
|
13
|
+
productName?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function CategoryBreadcrumbs({
|
|
17
|
+
breadcrumbs,
|
|
18
|
+
locale,
|
|
19
|
+
productName,
|
|
20
|
+
}: Props) {
|
|
21
|
+
return (
|
|
22
|
+
<Breadcrumbs>
|
|
23
|
+
{breadcrumbs.map((bc, index) =>
|
|
24
|
+
index < breadcrumbs.length - 1 || productName ? (
|
|
25
|
+
<Link href={getCategoryUrl(bc)} key={bc.Name}>
|
|
26
|
+
{getCategoryTranslatedName(locale, bc.NameTranslations)}
|
|
27
|
+
</Link>
|
|
28
|
+
) : (
|
|
29
|
+
<Typography variant={"body1"} key={bc.Name}>
|
|
30
|
+
{getCategoryTranslatedName(locale, bc.NameTranslations)}
|
|
31
|
+
</Typography>
|
|
32
|
+
),
|
|
33
|
+
)}
|
|
34
|
+
{productName ? (
|
|
35
|
+
<Typography variant={"body1"}>{productName}</Typography>
|
|
36
|
+
) : undefined}
|
|
37
|
+
</Breadcrumbs>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
// MUI
|
|
3
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
4
|
+
import Checkbox, { CheckboxProps } from "@mui/material/Checkbox";
|
|
5
|
+
|
|
6
|
+
// ==============================================================
|
|
7
|
+
interface Props extends CheckboxProps {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
// ==============================================================
|
|
11
|
+
|
|
12
|
+
export default function CheckboxLabel({ label, ...props }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<FormControlLabel
|
|
15
|
+
label={label}
|
|
16
|
+
control={<Checkbox size="small" color="primary" {...props} />}
|
|
17
|
+
slotProps={{ typography: { fontSize: 14, lineHeight: 1 } }}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { H6 } from "@/components/_components/Typography";
|
|
2
|
+
import {
|
|
3
|
+
AttributeDataType,
|
|
4
|
+
DisplayTemplate,
|
|
5
|
+
ProductRefiner,
|
|
6
|
+
ProductRefinerSource,
|
|
7
|
+
ProductRefinerValue,
|
|
8
|
+
} from "@msdyn365-commerce/retail-proxy";
|
|
9
|
+
import { Box, ButtonBase, Collapse, FormGroup } from "@mui/material";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
12
|
+
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
|
13
|
+
import Swatch from "./swatch";
|
|
14
|
+
import RangeFilter from "./range-filter";
|
|
15
|
+
import ListFilter from "./list-filter";
|
|
16
|
+
import { areRefinersEqual } from "@/lib/refiner-utils";
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
facet: ProductRefiner;
|
|
20
|
+
selectedRefiners: ProductRefinerValue[];
|
|
21
|
+
handleRefinerSelect(refiner: ProductRefinerValue): void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RenderedFacetProps = {
|
|
25
|
+
facet: ProductRefiner;
|
|
26
|
+
handleRefinerSelect(refiner: ProductRefinerValue): void;
|
|
27
|
+
isRefinerSelected(r1: ProductRefinerValue): boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default function FacetGroup({
|
|
31
|
+
facet,
|
|
32
|
+
selectedRefiners,
|
|
33
|
+
handleRefinerSelect,
|
|
34
|
+
}: Props) {
|
|
35
|
+
const isAnyRefinerFromFacetSelected = () =>
|
|
36
|
+
facet.Values?.some((v) => isRefinerSelected(v));
|
|
37
|
+
const isRefinerSelected = (r1: ProductRefinerValue) =>
|
|
38
|
+
selectedRefiners.some((r2) => areRefinersEqual(r1, r2));
|
|
39
|
+
|
|
40
|
+
const [expanded, setExpanded] = React.useState(
|
|
41
|
+
isAnyRefinerFromFacetSelected(),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const renderRefinerType = () => {
|
|
45
|
+
let component: React.ReactNode | null = null;
|
|
46
|
+
//Swatch
|
|
47
|
+
if (facet.KeyName?.toLowerCase() === "color") {
|
|
48
|
+
component = (
|
|
49
|
+
<Box mt={1}>
|
|
50
|
+
<Swatch
|
|
51
|
+
facet={facet}
|
|
52
|
+
handleRefinerSelect={handleRefinerSelect}
|
|
53
|
+
isRefinerSelected={isRefinerSelected}
|
|
54
|
+
/>
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
} else if (
|
|
58
|
+
facet.DataTypeValue === AttributeDataType.Currency &&
|
|
59
|
+
facet.DisplayTemplateValue === DisplayTemplate.Slider
|
|
60
|
+
) {
|
|
61
|
+
component = (
|
|
62
|
+
<RangeFilter
|
|
63
|
+
facet={facet}
|
|
64
|
+
handleRefinerSelect={handleRefinerSelect}
|
|
65
|
+
isRefinerSelected={isRefinerSelected}
|
|
66
|
+
selectedValues={getPriceRefinerValue()}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
} else if (facet.SourceValue === 4) {
|
|
70
|
+
const updatedFacet = {
|
|
71
|
+
...facet,
|
|
72
|
+
Values: facet?.Values?.map((item) => ({
|
|
73
|
+
...item,
|
|
74
|
+
RightValueBoundString: item.LeftValueBoundString,
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
component = (
|
|
78
|
+
<ListFilter
|
|
79
|
+
facet={updatedFacet}
|
|
80
|
+
handleRefinerSelect={handleRefinerSelect}
|
|
81
|
+
isRefinerSelected={isRefinerSelected}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
component = (
|
|
86
|
+
<ListFilter
|
|
87
|
+
facet={facet}
|
|
88
|
+
handleRefinerSelect={handleRefinerSelect}
|
|
89
|
+
isRefinerSelected={isRefinerSelected}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return component;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function getPriceRefinerValue(): ProductRefinerValue | undefined {
|
|
97
|
+
return selectedRefiners.find(
|
|
98
|
+
(refiner) => refiner.RefinerSourceValue === ProductRefinerSource.Price,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Box pb={2} pt={2}>
|
|
104
|
+
<ButtonBase
|
|
105
|
+
sx={{ display: "block", width: "100%", borderRadius: 1 }}
|
|
106
|
+
onClick={() => setExpanded(!expanded)}
|
|
107
|
+
>
|
|
108
|
+
<Box display="flex" alignItems="center" pl={1}>
|
|
109
|
+
<H6 component="p" fontWeight="bold">
|
|
110
|
+
{facet.KeyName}
|
|
111
|
+
</H6>
|
|
112
|
+
<Box flexGrow={1} />
|
|
113
|
+
{expanded ? (
|
|
114
|
+
<ExpandLessIcon color="action" />
|
|
115
|
+
) : (
|
|
116
|
+
<ExpandMoreIcon color="action" />
|
|
117
|
+
)}
|
|
118
|
+
</Box>
|
|
119
|
+
</ButtonBase>
|
|
120
|
+
<FormGroup>
|
|
121
|
+
<Collapse in={expanded}>{renderRefinerType()}</Collapse>
|
|
122
|
+
</FormGroup>
|
|
123
|
+
</Box>
|
|
124
|
+
);
|
|
125
|
+
}
|