@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,120 @@
1
+ import Card from "@/components/_components/BazaarCard";
2
+ import Heading from "./heading";
3
+ import { Button, useTheme } from "@mui/material";
4
+ import Grid from "@mui/material/Grid2";
5
+ import React from "react";
6
+ import { Span } from "@/components/_components/Typography";
7
+ import { FlexBox } from "@/components/_components/flex-box";
8
+ import { Cart } from "@/api-manager/schemas/cart.schema";
9
+ import { useTranslations } from "next-intl";
10
+
11
+ interface Props {
12
+ title: string;
13
+ stepNumber: number;
14
+ activeStep: number;
15
+ setActiveStep: (newStep: number) => void;
16
+ isLastStep: boolean;
17
+ cart: Cart;
18
+ component: React.FC<CheckoutStepComponentProps>;
19
+ }
20
+
21
+ export type StepViewState = "active" | "preview" | "pending";
22
+ export type StepSubmissionState = "idle" | "submitting" | "error";
23
+ export interface CheckoutStepComponentProps {
24
+ stepSubmissionState: StepSubmissionState;
25
+ setStepSubmissionState: (newState: StepSubmissionState) => void;
26
+ stepViewState: StepViewState;
27
+ handleStepCompletion: () => void;
28
+ cart: Cart;
29
+ }
30
+
31
+ function CheckoutStep({
32
+ component: Component,
33
+ title,
34
+ stepNumber,
35
+ activeStep,
36
+ setActiveStep,
37
+ cart,
38
+ }: Props) {
39
+ const t = useTranslations("Checkout.Steps");
40
+ const [hasCompleted, setHasCompleted] = React.useState<boolean>(false);
41
+ const [stepSubmissionState, setStepSubmissionState] =
42
+ React.useState<StepSubmissionState>("idle");
43
+ const theme = useTheme();
44
+ const handleStepCompletion = () => {
45
+ setHasCompleted(true);
46
+ setActiveStep(stepNumber + 1);
47
+ };
48
+
49
+ const handleActivateStep = () => {
50
+ setActiveStep(stepNumber);
51
+ };
52
+ const getStepViewState = (): StepViewState => {
53
+ if (activeStep === stepNumber) {
54
+ return "active";
55
+ } else if (hasCompleted) {
56
+ return "preview";
57
+ } else if (activeStep < stepNumber) {
58
+ return "pending";
59
+ }
60
+ return "pending";
61
+ };
62
+ const componentProps: CheckoutStepComponentProps = {
63
+ stepSubmissionState,
64
+ stepViewState: getStepViewState(),
65
+ handleStepCompletion: handleStepCompletion,
66
+ setStepSubmissionState,
67
+ cart,
68
+ };
69
+
70
+ const handleSubmitChildComponent = () => setStepSubmissionState("submitting");
71
+
72
+ return (
73
+ <Card
74
+ sx={{ p: 3, mb: 3 }}
75
+ style={
76
+ stepSubmissionState === "error"
77
+ ? { border: `1px solid ${theme.palette.error.main}` }
78
+ : {}
79
+ }
80
+ >
81
+ <FlexBox justifyContent={"space-between"}>
82
+ <Heading
83
+ number={stepNumber}
84
+ title={title}
85
+ stepViewState={getStepViewState()}
86
+ />
87
+ {getStepViewState() !== "active" && hasCompleted && (
88
+ <Span
89
+ sx={{ textDecoration: "underline", cursor: "pointer" }}
90
+ role="button"
91
+ component="a"
92
+ onClick={handleActivateStep}
93
+ >
94
+ {t("editButton")}
95
+ </Span>
96
+ )}
97
+ </FlexBox>
98
+
99
+ <Grid spacing={1}>
100
+ <Component {...componentProps} />
101
+ </Grid>
102
+ <Grid
103
+ container
104
+ justifyContent={"flex-end"}
105
+ mt={3}
106
+ sx={{ display: getStepViewState() === "active" ? "flex" : "none" }}
107
+ >
108
+ <Button
109
+ color={"primary"}
110
+ variant={"contained"}
111
+ onClick={handleSubmitChildComponent}
112
+ >
113
+ {t("saveButton")}
114
+ </Button>
115
+ </Grid>
116
+ </Card>
117
+ );
118
+ }
119
+
120
+ export default React.memo(CheckoutStep);
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import Box from "@mui/material/Box";
3
+ import Grid from "@mui/material/Grid2";
4
+ import TextField from "@mui/material/TextField";
5
+ import useUpdateCart from "@/api-manager/services/cart/mutations/update-cart";
6
+ import { useTranslations } from "next-intl";
7
+ import { CheckoutStepComponentProps } from "./checkout-step";
8
+ import { Paragraph } from "@/components/_components/Typography";
9
+ import * as yup from "yup";
10
+ import { useForm } from "react-hook-form";
11
+ import { yupResolver } from "@hookform/resolvers/yup";
12
+
13
+ export default function CustomerInformation(props: CheckoutStepComponentProps) {
14
+ const {
15
+ stepViewState,
16
+ cart: { Id: cartId, ReceiptEmail: email },
17
+ stepSubmissionState,
18
+ setStepSubmissionState,
19
+ } = props;
20
+ const { mutateAsync: updateCart } = useUpdateCart();
21
+ const formRef = React.useRef<HTMLFormElement>(null);
22
+ const t = useTranslations("Checkout.CustomerInformation");
23
+
24
+ const validationSchema = yup.object({
25
+ email: yup.string().email(t("emailInvalid")).required(t("emailRequired")),
26
+ });
27
+ const {
28
+ register,
29
+ handleSubmit,
30
+ formState: { errors, isSubmitted },
31
+ setValue,
32
+ } = useForm({
33
+ defaultValues: { email: email || "" },
34
+ resolver: yupResolver(validationSchema),
35
+ });
36
+
37
+ React.useEffect(() => {
38
+ setValue("email", email || "");
39
+ }, [email, setValue]);
40
+
41
+ const handleSave = React.useCallback(
42
+ (formData: { email: string }) => {
43
+ updateCart({
44
+ cart: { ReceiptEmail: formData.email, Id: cartId },
45
+ })
46
+ .then(() => {
47
+ props.handleStepCompletion && props.handleStepCompletion();
48
+ setStepSubmissionState("idle");
49
+ })
50
+ .catch(() => {
51
+ setStepSubmissionState("error");
52
+ });
53
+ },
54
+ [cartId, props, setStepSubmissionState, updateCart],
55
+ );
56
+
57
+ React.useEffect(() => {
58
+ if (stepSubmissionState === "submitting") {
59
+ handleSubmit(handleSave)();
60
+ }
61
+ }, [handleSave, handleSubmit, stepSubmissionState]);
62
+
63
+ React.useEffect(() => {
64
+ if (errors && isSubmitted) {
65
+ setStepSubmissionState("error");
66
+ }
67
+ }, [errors, isSubmitted, setStepSubmissionState]);
68
+ if (stepViewState === "preview") {
69
+ return (
70
+ <Box ml={2}>
71
+ <Paragraph>
72
+ {t("emailLabel")}: {email}
73
+ </Paragraph>
74
+ </Box>
75
+ );
76
+ }
77
+ return (
78
+ <form onSubmit={handleSubmit(handleSave)} ref={formRef}>
79
+ <Grid size={{ sm: 5, xs: 5 }}>
80
+ <TextField
81
+ {...register("email")}
82
+ fullWidth
83
+ sx={{ flexGrow: 1 }}
84
+ type="email"
85
+ name={"email"}
86
+ label={t("emailLabel")}
87
+ variant="outlined"
88
+ error={Boolean(!!errors.email && errors.email.message)}
89
+ helperText={errors.email?.message}
90
+ />
91
+ </Grid>
92
+ </form>
93
+ );
94
+ }
@@ -0,0 +1,170 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useLocale, useTranslations } from "next-intl";
3
+ import React from "react";
4
+ import NewAddressForm from "./new-address-form";
5
+ import { Address } from "@/api-manager/schemas/address.schema";
6
+ import AddressCard from "./address-card";
7
+
8
+ import getUser from "@/api-manager/services/user/queries/get-user";
9
+ import useCreateAddress from "@/api-manager/services/address/mutations/create-address";
10
+ import useUpdateAddress from "@/api-manager/services/address/mutations/update-address";
11
+ import useDeleteAddress from "@/api-manager/services/address/mutations/delete-address";
12
+ import getCountries from "@/api-manager/services/address/queries/get-countries";
13
+ import useUpdateCart from "@/api-manager/services/cart/mutations/update-cart";
14
+ import { CheckoutStepComponentProps } from "./checkout-step";
15
+ import Grid from "@mui/material/Grid2";
16
+ import useUpdateCartLines from "@/api-manager/services/cart/mutations/update-cart-lines";
17
+ import { cloneDeep } from "lodash";
18
+
19
+ export default function DeliveryAddress({
20
+ cart,
21
+ handleStepCompletion,
22
+ stepSubmissionState,
23
+ setStepSubmissionState,
24
+ stepViewState,
25
+ }: CheckoutStepComponentProps) {
26
+ const t = useTranslations("Checkout.Address");
27
+ const locale = useLocale();
28
+
29
+ const { data: user, isLoading: userLoading } = getUser.useData();
30
+ const isGuest = !userLoading && !user?.RecordId;
31
+
32
+ const [addressList, setAddressList] = useState<Address[]>([]);
33
+ useEffect(() => {
34
+ if (!isGuest && user?.Addresses?.length) {
35
+ setAddressList(user.Addresses);
36
+ } else if (cart.ShippingAddress) {
37
+ setAddressList([cart.ShippingAddress]);
38
+ } else {
39
+ setAddressList([]);
40
+ }
41
+ }, [isGuest, user, cart.ShippingAddress]);
42
+
43
+ const { data: countries } = getCountries.useData({ locale });
44
+
45
+ const { mutateAsync: updateCart } = useUpdateCart();
46
+ const { mutateAsync: updateCartLines } = useUpdateCartLines();
47
+
48
+ const { mutateAsync: createAddress } = useCreateAddress();
49
+ const { mutateAsync: updateAddress } = useUpdateAddress();
50
+ const { mutateAsync: deleteAddress } = useDeleteAddress();
51
+
52
+ const [modalOpen, setModalOpen] = useState(false);
53
+ const [editAddress, setEditAddress] = useState<Address | undefined>(
54
+ undefined,
55
+ );
56
+ const openNew = () => {
57
+ setEditAddress(undefined);
58
+ setModalOpen(true);
59
+ };
60
+ const openEdit = (addr: Address) => {
61
+ setEditAddress(addr);
62
+ setModalOpen(true);
63
+ };
64
+ const closeModal = () => setModalOpen(false);
65
+
66
+ const saveToCart = async (address: Address) => {
67
+ const lines = cloneDeep(cart.CartLines ?? []).map((l) => ({
68
+ ...l,
69
+ DeliveryMode: "",
70
+ }));
71
+ await updateCartLines({
72
+ cartId: cart.Id,
73
+ cartLine: lines,
74
+ version: cart.Version,
75
+ });
76
+ await updateCart({ cart: { Id: cart.Id, ShippingAddress: address } });
77
+ };
78
+
79
+ const handleGuestFlow = async (address: Address) => {
80
+ await saveToCart(address);
81
+ setStepSubmissionState("submitting");
82
+ closeModal();
83
+ };
84
+
85
+ const handleAuthFlow = async (address: Address) => {
86
+ const isNew = !address.Id;
87
+ const saved = isNew
88
+ ? await createAddress(address)
89
+ : await updateAddress(address);
90
+ // Update local list
91
+ if (isNew) {
92
+ setAddressList((prev) => [...prev, saved]);
93
+ } else {
94
+ setAddressList((prev) =>
95
+ prev.map((a) => (a.Id === saved.Id ? saved : a)),
96
+ );
97
+ }
98
+ await saveToCart(saved);
99
+ if (isNew) setStepSubmissionState("submitting");
100
+ closeModal();
101
+ };
102
+
103
+ const handleSaveAddress = (address: Address) => {
104
+ return isGuest ? handleGuestFlow(address) : handleAuthFlow(address);
105
+ };
106
+
107
+ const handleDelete = async (address: Address) => {
108
+ if (!isGuest && address.Id) {
109
+ await deleteAddress(address.Id);
110
+ }
111
+ setAddressList((prev) => prev.filter((a) => a.Id !== address.Id));
112
+ };
113
+
114
+ useEffect(() => {
115
+ if (stepSubmissionState === "submitting") {
116
+ handleStepCompletion();
117
+ setStepSubmissionState("idle");
118
+ }
119
+ }, [stepSubmissionState, handleStepCompletion, setStepSubmissionState]);
120
+
121
+ if (stepViewState === "pending" || !countries) return null;
122
+ if (stepViewState === "preview") {
123
+ const sel = cart.ShippingAddress;
124
+ if (!sel) return null;
125
+ return <AddressCard address={sel} hideControls isCartAddress />;
126
+ }
127
+
128
+ return (
129
+ <Grid container spacing={2}>
130
+ <NewAddressForm
131
+ countries={countries}
132
+ open={modalOpen}
133
+ isGuest={isGuest}
134
+ address={editAddress}
135
+ cartId={cart.Id}
136
+ handleClose={closeModal}
137
+ handleCreateOrUpdateAddress={handleSaveAddress}
138
+ />
139
+
140
+ {addressList.map((addr) => (
141
+ <Grid key={addr.Id || addr.RecordId} size={{ md: 4, sm: 6, xs: 12 }}>
142
+ <AddressCard
143
+ address={addr}
144
+ isCartAddress={cart.ShippingAddress?.Id === addr.Id}
145
+ onEdit={() => openEdit(addr)}
146
+ onSelect={() => handleSaveAddress(addr)}
147
+ onDelete={() => handleDelete(addr)}
148
+ />
149
+ </Grid>
150
+ ))}
151
+
152
+ <Grid size={{ md: 4, sm: 6, xs: 12 }} onClick={openNew} data-testid="addr-new">
153
+ <AddressCard
154
+ address={{
155
+ Id: "new",
156
+ Name: t("createNewAddress"),
157
+ Street: "",
158
+ City: "",
159
+ State: "",
160
+ ZipCode: "",
161
+ Phone: "",
162
+ }}
163
+ hideControls
164
+ isCartAddress={false}
165
+ onSelect={openNew}
166
+ />
167
+ </Grid>
168
+ </Grid>
169
+ );
170
+ }
@@ -0,0 +1,114 @@
1
+ import { ChangeEvent, useEffect, useState } from "react";
2
+ import Box from "@mui/material/Box";
3
+ import Card from "@mui/material/Card";
4
+ import Grid from "@mui/material/Grid2";
5
+ import MenuItem from "@mui/material/MenuItem";
6
+ import TextField from "@mui/material/TextField";
7
+ import { FormikErrors, FormikTouched } from "formik";
8
+ import { format } from "date-fns";
9
+ import Heading from "./heading";
10
+ import { InitialValues } from "./types";
11
+
12
+ // ==============================================================
13
+ interface Props {
14
+ values: InitialValues;
15
+ errors: FormikErrors<InitialValues>;
16
+ touched: FormikTouched<InitialValues>;
17
+ handleChange: (
18
+ event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
19
+ ) => void;
20
+ }
21
+ // ==============================================================
22
+
23
+ export default function DeliveryDate({
24
+ errors,
25
+ handleChange,
26
+ touched,
27
+ values,
28
+ }: Props) {
29
+ const [dateList, setDateList] = useState<{ label: string; value: string }[]>(
30
+ [],
31
+ );
32
+
33
+ useEffect(() => {
34
+ const list = [];
35
+ const today = new Date();
36
+ const dateCount = today.getDate();
37
+
38
+ list.push({
39
+ label: format(today, "dd MMMM"),
40
+ value: today.toISOString(),
41
+ });
42
+
43
+ for (let i = 1; i < 10; i++) {
44
+ today.setDate(dateCount + i);
45
+ list.push({
46
+ label: format(today, "dd MMMM"),
47
+ value: today.toISOString(),
48
+ });
49
+ }
50
+
51
+ setDateList(list);
52
+ }, []);
53
+
54
+ const timeList = [
55
+ { label: "9AM - 11AM", value: "9AM - 11AM" },
56
+ { label: "11AM - 1PM", value: "11AM - 1PM" },
57
+ { label: "3PM - 5PM", value: "3PM - 5PM" },
58
+ { label: "5PM - 7PM", value: "5PM - 7PM" },
59
+ ];
60
+
61
+ return (
62
+ <Card sx={{ p: 3, mb: 3 }}>
63
+ <Heading
64
+ number={1}
65
+ title="Delivery Date and Time"
66
+ stepViewState={"active"}
67
+ />
68
+
69
+ <Box mb={3.5}>
70
+ <Grid container spacing={3}>
71
+ <Grid size={{ sm: 6, xs: 12 }}>
72
+ <TextField
73
+ select
74
+ fullWidth
75
+ type="text"
76
+ name="date"
77
+ label="Delivery Date"
78
+ value={values.date}
79
+ onChange={(e) => handleChange(e)}
80
+ helperText={touched.date && errors.date}
81
+ error={Boolean(touched.date && errors.date)}
82
+ >
83
+ {dateList.map((item) => (
84
+ <MenuItem value={item.value} key={item.label}>
85
+ {item.label}
86
+ </MenuItem>
87
+ ))}
88
+ </TextField>
89
+ </Grid>
90
+
91
+ <Grid size={{ sm: 6, xs: 12 }}>
92
+ <TextField
93
+ select
94
+ fullWidth
95
+ type="text"
96
+ name="time"
97
+ value={values.time}
98
+ label="Delivery Time"
99
+ onChange={handleChange}
100
+ helperText={touched.time && errors.time}
101
+ error={Boolean(touched.time && errors.time)}
102
+ >
103
+ {timeList.map((item) => (
104
+ <MenuItem value={item.value} key={item.value}>
105
+ {item.value}
106
+ </MenuItem>
107
+ ))}
108
+ </TextField>
109
+ </Grid>
110
+ </Grid>
111
+ </Box>
112
+ </Card>
113
+ );
114
+ }
@@ -0,0 +1,186 @@
1
+ import {
2
+ Box,
3
+ FormControl,
4
+ FormControlLabel,
5
+ Radio,
6
+ RadioGroup,
7
+ } from "@mui/material";
8
+ import Grid from "@mui/material/Grid2";
9
+ import { useTranslations } from "next-intl";
10
+ import { Address } from "@/api-manager/schemas/address.schema";
11
+ import { FlexBox } from "@/components/_components/flex-box";
12
+ import {
13
+ DeliveryOption,
14
+ DeliveryPreferenceType,
15
+ LineDeliverySpecification,
16
+ } from "@msdyn365-commerce/retail-proxy";
17
+ import { H4, Paragraph, Span } from "@/components/_components/Typography";
18
+ import useCurrencyFormatter from "@/lib/currency-formatter";
19
+ import { CheckoutStepComponentProps } from "./checkout-step";
20
+ import useGetDeliveryOptions from "@/api-manager/services/address/mutations/get-delivery-options";
21
+ import React from "react";
22
+ import useUpdateCartLineDeliveryMode from "@/api-manager/services/cart/mutations/update-cart-line-delivery-mode";
23
+ import { cloneDeep } from "lodash";
24
+ import useRemoveCartLineDeliveryMode from "@/api-manager/services/cart/mutations/remove-cart-line-delivery-mode";
25
+
26
+ export default function DeliveryOptions(props: CheckoutStepComponentProps) {
27
+ const { cart, stepViewState, stepSubmissionState, setStepSubmissionState } =
28
+ props;
29
+ const [deliveryOptions, setDeliveryOptions] = React.useState<
30
+ DeliveryOption[]
31
+ >([]);
32
+ const [deliveryUnavailable, setDeliveryUnavailable] =
33
+ React.useState<boolean>(false);
34
+ const [selectedOption, setSelectedOption] = React.useState<string | null>(
35
+ null,
36
+ );
37
+ const [currentShippingAddress, setCurrentShippingAddress] =
38
+ React.useState<Address | null>(null);
39
+ const t = useTranslations("Checkout.DeliveryOptions");
40
+ const { formatCurrency } = useCurrencyFormatter();
41
+ const { mutateAsync: getDeliveryOptions } = useGetDeliveryOptions();
42
+ const { mutateAsync: updateDeliveryOption } = useUpdateCartLineDeliveryMode();
43
+ const { mutate: removeDeliveryOption } = useRemoveCartLineDeliveryMode();
44
+
45
+ const handleUpdateDeliveryOption = React.useCallback(() => {
46
+ const cartLineDeliverySpecification: LineDeliverySpecification[] = [];
47
+ if (cart.CartLines && selectedOption) {
48
+ for (const cl of cart.CartLines) {
49
+ const lineSpec: LineDeliverySpecification = {
50
+ LineId: cl.LineId,
51
+ DeliverySpecification: {
52
+ DeliveryModeId: selectedOption,
53
+ DeliveryPreferenceTypeValue: DeliveryPreferenceType.ShipToAddress,
54
+ DeliveryAddress: cart.ShippingAddress,
55
+ },
56
+ };
57
+ cartLineDeliverySpecification.push(lineSpec);
58
+ }
59
+ updateDeliveryOption({ cartId: cart.Id, cartLineDeliverySpecification })
60
+ .then(() => {
61
+ props.handleStepCompletion();
62
+ setStepSubmissionState("idle");
63
+ })
64
+ .catch(() => {
65
+ setStepSubmissionState("error");
66
+ });
67
+ }
68
+ }, [
69
+ cart.CartLines,
70
+ cart.Id,
71
+ cart.ShippingAddress,
72
+ selectedOption,
73
+ updateDeliveryOption,
74
+ props,
75
+ setStepSubmissionState,
76
+ ]);
77
+ React.useEffect(() => {
78
+ if (stepSubmissionState === "submitting") {
79
+ handleUpdateDeliveryOption();
80
+ }
81
+ }, [handleUpdateDeliveryOption, stepSubmissionState]);
82
+
83
+ React.useEffect(() => {
84
+ if (stepViewState === "active") {
85
+ const cartLines = cloneDeep(cart.CartLines);
86
+ if (cartLines.map((cl) => cl.DeliveryMode).some((mode) => mode !== "")) {
87
+ removeDeliveryOption({ cart: cart });
88
+ }
89
+ }
90
+ }, [cart, removeDeliveryOption, stepViewState]);
91
+
92
+ React.useEffect(() => {
93
+ if (currentShippingAddress) {
94
+ const { Id, CartLines } = cart;
95
+ const cartLineIds = CartLines.map((cl) => cl.LineId).filter(
96
+ (line) => line !== undefined,
97
+ );
98
+ getDeliveryOptions({
99
+ cartId: Id,
100
+ cartLineIds,
101
+ shippingAddress: currentShippingAddress,
102
+ }).then((response) => {
103
+ setDeliveryUnavailable(response.length === 0);
104
+ setDeliveryOptions(response || []);
105
+ });
106
+ }
107
+ }, [cart, currentShippingAddress, getDeliveryOptions]);
108
+
109
+ React.useEffect(() => {
110
+ if (cart.ShippingAddress) {
111
+ if (currentShippingAddress) {
112
+ //Deep equals the address
113
+ if (
114
+ JSON.stringify(currentShippingAddress) ===
115
+ JSON.stringify(cart.ShippingAddress)
116
+ ) {
117
+ return;
118
+ }
119
+ }
120
+ // If the address has changed, change the local state which will trigger the effect to get the delivery options
121
+ setCurrentShippingAddress(cart.ShippingAddress);
122
+ }
123
+ }, [cart.ShippingAddress, currentShippingAddress]);
124
+
125
+ const handleOptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
126
+ setSelectedOption(event.target.value);
127
+ };
128
+ const renderChargeLine = (dlvOption: DeliveryOption) => (
129
+ <Grid container justifyContent={"space-between"}>
130
+ <Paragraph>{dlvOption.Description}</Paragraph>
131
+ <Paragraph>{formatCurrency(dlvOption.TotalChargeAmount || 0)}</Paragraph>
132
+ </Grid>
133
+ );
134
+
135
+ if (stepViewState === "pending") {
136
+ return;
137
+ }
138
+
139
+ if (stepViewState === "preview") {
140
+ const deliveryOption = deliveryOptions.find(
141
+ (dlv) => dlv.Code === selectedOption,
142
+ );
143
+ if (deliveryOption) {
144
+ return (
145
+ <Box mt={2}>
146
+ <H4>{deliveryOption.Description}</H4>
147
+ <Span>{formatCurrency(deliveryOption.TotalChargeAmount || 0)}</Span>
148
+ </Box>
149
+ );
150
+ }
151
+ }
152
+ return (
153
+ <Box mb={3.5}>
154
+ <Grid container spacing={1}>
155
+ <Grid size={{ sm: 6, xs: 12 }}>
156
+ <FlexBox flexDirection={"column"} alignItems={"flex-start"} mt={4}>
157
+ <FormControl fullWidth>
158
+ {!deliveryOptions.length && deliveryUnavailable && (
159
+ <Paragraph color={"error"} textAlign={"center"} fontSize={14}>
160
+ {t("noDeliveryOptionsFound")}
161
+ </Paragraph>
162
+ )}
163
+ {/* <FormLabel id={"deliveryOptions"}>{t("deliveryOptionsLabel")}</FormLabel> */}
164
+ <RadioGroup
165
+ aria-labelledby={"deliveryOptions"}
166
+ name={"Delivery-Options"}
167
+ onChange={handleOptionChange}
168
+ value={selectedOption}
169
+ >
170
+ {deliveryOptions?.map((dlv) => (
171
+ <FormControlLabel
172
+ sx={{ ".MuiFormControlLabel-label": { width: "100%" } }}
173
+ key={dlv.Code}
174
+ value={dlv.Code}
175
+ control={<Radio />}
176
+ label={renderChargeLine(dlv)}
177
+ />
178
+ ))}
179
+ </RadioGroup>
180
+ </FormControl>
181
+ </FlexBox>
182
+ </Grid>
183
+ </Grid>
184
+ </Box>
185
+ );
186
+ }