@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,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
// CUSTOM UTILS LIBRARY FUNCTION
|
|
5
|
+
import { ProductDetails } from "@/api-manager/schemas/product.schema";
|
|
6
|
+
import { ProductDimensionValueInventoryAvailability } from "@msdyn365-commerce/retail-proxy";
|
|
7
|
+
import getProductById from "@/api-manager/services/product/queries/get-product-by-id";
|
|
8
|
+
|
|
9
|
+
//=========================================
|
|
10
|
+
type Props = {
|
|
11
|
+
product: ProductDetails;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SelectedDimension = Record<
|
|
15
|
+
number,
|
|
16
|
+
ProductDimensionValueInventoryAvailability
|
|
17
|
+
>;
|
|
18
|
+
//========================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This Hook handles the product variant selection and updates the selected variant id
|
|
22
|
+
* based on the selected dimensions.
|
|
23
|
+
*/
|
|
24
|
+
export default function useVariants({ product }: Props) {
|
|
25
|
+
const { DimensionValues } = product;
|
|
26
|
+
|
|
27
|
+
const [selectVariantId, setSelectVariantId] = useState<number>();
|
|
28
|
+
const [selectDimensions, setSelectDimensions] = useState<SelectedDimension>(
|
|
29
|
+
{},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const { data: productVariant, isPending: variantPending } =
|
|
33
|
+
getProductById.useData({ id: selectVariantId });
|
|
34
|
+
|
|
35
|
+
// HELPERS
|
|
36
|
+
const allDimensionsSelected = (currentDimensions: SelectedDimension) =>
|
|
37
|
+
Object.keys(currentDimensions).length === DimensionValues.length;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Adds selected dimension to the current selectDimensions state
|
|
41
|
+
* Updates the selectVariantId if all dimensions are selected
|
|
42
|
+
* @param dimensionValue dimension that was last change to trigger this function
|
|
43
|
+
*/
|
|
44
|
+
const handleChangeVariant = async (
|
|
45
|
+
dimensionValue: ProductDimensionValueInventoryAvailability,
|
|
46
|
+
) => {
|
|
47
|
+
// Wrap the selection logic inside the callback
|
|
48
|
+
// to ensure we have the correct current value of state
|
|
49
|
+
setSelectDimensions((state) => {
|
|
50
|
+
if (!dimensionValue?.DimensionTypeValue) {
|
|
51
|
+
console.error("DimensionTypeValue is missing");
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const newDimensionsState = {
|
|
56
|
+
...state,
|
|
57
|
+
[dimensionValue?.DimensionTypeValue]: dimensionValue,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!allDimensionsSelected(newDimensionsState)) return newDimensionsState;
|
|
61
|
+
|
|
62
|
+
const dimensionsArray = Object.values(newDimensionsState);
|
|
63
|
+
|
|
64
|
+
// DEDUCE PRODUCT ID FROM SELECTED DIMENSIONS
|
|
65
|
+
const selectedVariantId = dimensionsArray.reduce(
|
|
66
|
+
(intersectedIds: number[], { ProductIds }) => {
|
|
67
|
+
return intersectedIds.filter((id) => ProductIds?.includes(id));
|
|
68
|
+
},
|
|
69
|
+
dimensionsArray[0]?.ProductIds || [],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
setSelectVariantId(selectedVariantId[0]);
|
|
73
|
+
return newDimensionsState;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
allDimensionsSelected,
|
|
79
|
+
handleChangeVariant,
|
|
80
|
+
productVariant,
|
|
81
|
+
selectVariantId,
|
|
82
|
+
variantPending,
|
|
83
|
+
selectDimensions,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
FormControl,
|
|
6
|
+
InputLabel,
|
|
7
|
+
TextField,
|
|
8
|
+
MenuItem,
|
|
9
|
+
Select,
|
|
10
|
+
OutlinedInput,
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
import Grid from "@mui/material/Grid2";
|
|
13
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
14
|
+
import { useNotification } from "@/components/Notifications/use-notification";
|
|
15
|
+
import { useRouter } from "next/navigation";
|
|
16
|
+
// Forms
|
|
17
|
+
import { useFormik } from "formik";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { withZodSchema } from "formik-validator-zod";
|
|
20
|
+
// API and Interfaces
|
|
21
|
+
import { Address } from "@/api-manager/schemas/address.schema";
|
|
22
|
+
import { CountryRegionInfo } from "@msdyn365-commerce/retail-proxy";
|
|
23
|
+
import getCountries from "@/api-manager/services/address/queries/get-countries";
|
|
24
|
+
import getStates from "@/api-manager/services/address/queries/get-states";
|
|
25
|
+
import useCreateAddress from "@/api-manager/services/address/mutations/create-address";
|
|
26
|
+
import useUpdateAddress from "@/api-manager/services/address/mutations/update-address";
|
|
27
|
+
|
|
28
|
+
// =============================================================
|
|
29
|
+
type Props = { address: Address };
|
|
30
|
+
// =============================================================
|
|
31
|
+
export default function AddressForm({ address }: Props) {
|
|
32
|
+
const [tn, t] = ["Account.Addresses", "Address"].map(useTranslations);
|
|
33
|
+
const { setNotification } = useNotification();
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
const locale = useLocale();
|
|
36
|
+
|
|
37
|
+
const { data: countries } = getCountries.useData({ locale });
|
|
38
|
+
const defaultCountry = countries?.find(
|
|
39
|
+
(c) => c.CountryRegionId === address?.ThreeLetterISORegionName,
|
|
40
|
+
);
|
|
41
|
+
const [selectedCountry, setSelectedCountry] =
|
|
42
|
+
React.useState<CountryRegionInfo | null>(defaultCountry ?? null);
|
|
43
|
+
const { data: states } = getStates.useData({
|
|
44
|
+
countryId: selectedCountry?.CountryRegionId,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { mutateAsync: createAddress } = useCreateAddress();
|
|
48
|
+
const { mutateAsync: updateAddress } = useUpdateAddress();
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!selectedCountry && defaultCountry) {
|
|
52
|
+
setSelectedCountry(defaultCountry);
|
|
53
|
+
}
|
|
54
|
+
}, [defaultCountry, selectedCountry]);
|
|
55
|
+
|
|
56
|
+
const FormSchema = z.object({
|
|
57
|
+
Name: z.string().nonempty(t("Validation.required", { field: t("name") })),
|
|
58
|
+
Street: z
|
|
59
|
+
.string()
|
|
60
|
+
.nonempty(t("Validation.required", { field: t("street") })),
|
|
61
|
+
City: z.string().nonempty(t("Validation.required", { field: t("city") })),
|
|
62
|
+
State: z.string().nonempty(t("Validation.required", { field: t("state") })),
|
|
63
|
+
ZipCode: z
|
|
64
|
+
.string()
|
|
65
|
+
.nonempty(t("Validation.required", { field: t("zipCode") })),
|
|
66
|
+
Country: z
|
|
67
|
+
.string()
|
|
68
|
+
.nonempty(t("Validation.required", { field: t("country") })),
|
|
69
|
+
Phone: z.string().optional().default(""),
|
|
70
|
+
});
|
|
71
|
+
type FormSchemaType = z.infer<typeof FormSchema>;
|
|
72
|
+
|
|
73
|
+
const handleSubmit = async (values: FormSchemaType) => {
|
|
74
|
+
let newAddress: Address = {
|
|
75
|
+
Name: values.Name,
|
|
76
|
+
Street: values.Street,
|
|
77
|
+
City: values.City,
|
|
78
|
+
State: values.State,
|
|
79
|
+
ZipCode: values.ZipCode,
|
|
80
|
+
TwoLetterISORegionName: selectedCountry?.ISOCode,
|
|
81
|
+
ThreeLetterISORegionName: selectedCountry?.CountryRegionId,
|
|
82
|
+
Phone: values.Phone || undefined,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (address?.Id && address?.Id !== "new") {
|
|
87
|
+
newAddress = { ...newAddress, Id: address?.Id };
|
|
88
|
+
await updateAddress(newAddress);
|
|
89
|
+
} else {
|
|
90
|
+
await createAddress(newAddress);
|
|
91
|
+
}
|
|
92
|
+
setNotification({
|
|
93
|
+
message: tn("addressSuccess"),
|
|
94
|
+
severity: "success",
|
|
95
|
+
});
|
|
96
|
+
router.push("/account/addresses");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
setNotification({
|
|
99
|
+
message: tn("addressFailed"),
|
|
100
|
+
severity: "error",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const setValues = (address: Address) => {
|
|
106
|
+
return {
|
|
107
|
+
Name: address?.Name || "",
|
|
108
|
+
Street: address?.Street || "",
|
|
109
|
+
City: address?.City || "",
|
|
110
|
+
State: address?.State || "",
|
|
111
|
+
ZipCode: address?.ZipCode || "",
|
|
112
|
+
Country: address?.ThreeLetterISORegionName || "",
|
|
113
|
+
Phone: address?.Phone || "",
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const intialValues = setValues(address);
|
|
118
|
+
|
|
119
|
+
const formik = useFormik<FormSchemaType>({
|
|
120
|
+
initialValues: intialValues,
|
|
121
|
+
onSubmit: handleSubmit,
|
|
122
|
+
validate: withZodSchema(FormSchema),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<form onSubmit={formik.handleSubmit}>
|
|
127
|
+
<Grid container spacing={3}>
|
|
128
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
129
|
+
<TextField
|
|
130
|
+
name="Name"
|
|
131
|
+
fullWidth
|
|
132
|
+
autoComplete="off"
|
|
133
|
+
label={t("name")}
|
|
134
|
+
value={formik.values.Name}
|
|
135
|
+
onChange={formik.handleChange}
|
|
136
|
+
onBlur={formik.handleBlur}
|
|
137
|
+
error={formik.touched.Name && Boolean(formik.errors.Name)}
|
|
138
|
+
helperText={formik.touched.Name && formik.errors.Name}
|
|
139
|
+
required
|
|
140
|
+
/>
|
|
141
|
+
</Grid>
|
|
142
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
143
|
+
<TextField
|
|
144
|
+
name="Street"
|
|
145
|
+
fullWidth
|
|
146
|
+
autoComplete="off"
|
|
147
|
+
label={t("street")}
|
|
148
|
+
value={formik.values.Street}
|
|
149
|
+
onChange={formik.handleChange}
|
|
150
|
+
onBlur={formik.handleBlur}
|
|
151
|
+
error={formik.touched.Street && Boolean(formik.errors.Street)}
|
|
152
|
+
helperText={formik.touched.Street && formik.errors.Street}
|
|
153
|
+
required
|
|
154
|
+
/>
|
|
155
|
+
</Grid>
|
|
156
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
157
|
+
<TextField
|
|
158
|
+
name="City"
|
|
159
|
+
fullWidth
|
|
160
|
+
autoComplete="off"
|
|
161
|
+
label={t("city")}
|
|
162
|
+
value={formik.values.City}
|
|
163
|
+
onChange={formik.handleChange}
|
|
164
|
+
onBlur={formik.handleBlur}
|
|
165
|
+
error={formik.touched.City && Boolean(formik.errors.City)}
|
|
166
|
+
helperText={formik.touched.City && formik.errors.City}
|
|
167
|
+
required
|
|
168
|
+
/>
|
|
169
|
+
</Grid>
|
|
170
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
171
|
+
<FormControl
|
|
172
|
+
fullWidth
|
|
173
|
+
size="small"
|
|
174
|
+
error={formik.touched.Country && Boolean(formik.errors.Country)}
|
|
175
|
+
>
|
|
176
|
+
<InputLabel required id="Country">
|
|
177
|
+
{t("country")}
|
|
178
|
+
</InputLabel>
|
|
179
|
+
<Select
|
|
180
|
+
labelId="Country"
|
|
181
|
+
name="Country"
|
|
182
|
+
input={<OutlinedInput label={t("country")} />}
|
|
183
|
+
value={formik.values.Country}
|
|
184
|
+
onChange={(event) => {
|
|
185
|
+
const countryId = event.target.value;
|
|
186
|
+
|
|
187
|
+
// Update selected country
|
|
188
|
+
const selected =
|
|
189
|
+
countries?.find((c) => c.CountryRegionId === countryId) ||
|
|
190
|
+
null;
|
|
191
|
+
setSelectedCountry(selected);
|
|
192
|
+
|
|
193
|
+
formik.setFieldValue("Country", countryId);
|
|
194
|
+
// Reset state field when country changes
|
|
195
|
+
formik.setFieldValue("State", "");
|
|
196
|
+
}}
|
|
197
|
+
onBlur={formik.handleBlur}
|
|
198
|
+
required
|
|
199
|
+
>
|
|
200
|
+
<MenuItem value="">Select Country</MenuItem>
|
|
201
|
+
{countries?.map(({ ShortName, CountryRegionId }, index) => (
|
|
202
|
+
<MenuItem value={CountryRegionId} key={index}>
|
|
203
|
+
{ShortName}
|
|
204
|
+
</MenuItem>
|
|
205
|
+
))}
|
|
206
|
+
</Select>
|
|
207
|
+
{formik.touched.Country && formik.errors.Country && (
|
|
208
|
+
<Box
|
|
209
|
+
component="span"
|
|
210
|
+
sx={{ color: "error.main", fontSize: "0.75rem", mt: 0.5 }}
|
|
211
|
+
>
|
|
212
|
+
{formik.errors.Country}
|
|
213
|
+
</Box>
|
|
214
|
+
)}
|
|
215
|
+
</FormControl>
|
|
216
|
+
</Grid>
|
|
217
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
218
|
+
<FormControl
|
|
219
|
+
fullWidth
|
|
220
|
+
size="small"
|
|
221
|
+
error={formik.touched.State && Boolean(formik.errors.State)}
|
|
222
|
+
>
|
|
223
|
+
<InputLabel required id="State">
|
|
224
|
+
{t("state")}
|
|
225
|
+
</InputLabel>
|
|
226
|
+
<Select
|
|
227
|
+
labelId="State"
|
|
228
|
+
name="State"
|
|
229
|
+
input={<OutlinedInput label={t("state")} />}
|
|
230
|
+
value={formik.values.State}
|
|
231
|
+
onChange={formik.handleChange}
|
|
232
|
+
onBlur={formik.handleBlur}
|
|
233
|
+
required
|
|
234
|
+
>
|
|
235
|
+
<MenuItem value="">Select a state</MenuItem>
|
|
236
|
+
{states?.map(({ StateName, StateId }, index) => (
|
|
237
|
+
<MenuItem value={StateId} key={index}>
|
|
238
|
+
{StateName}
|
|
239
|
+
</MenuItem>
|
|
240
|
+
))}
|
|
241
|
+
</Select>
|
|
242
|
+
{formik.touched.State && formik.errors.State && (
|
|
243
|
+
<Box
|
|
244
|
+
component="span"
|
|
245
|
+
sx={{ color: "error.main", fontSize: "0.75rem", mt: 0.5 }}
|
|
246
|
+
>
|
|
247
|
+
{formik.errors.State}
|
|
248
|
+
</Box>
|
|
249
|
+
)}
|
|
250
|
+
</FormControl>
|
|
251
|
+
</Grid>
|
|
252
|
+
|
|
253
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
254
|
+
<TextField
|
|
255
|
+
name="ZipCode"
|
|
256
|
+
fullWidth
|
|
257
|
+
autoComplete="off"
|
|
258
|
+
label={t("zipCode")}
|
|
259
|
+
value={formik.values.ZipCode}
|
|
260
|
+
onChange={formik.handleChange}
|
|
261
|
+
onBlur={formik.handleBlur}
|
|
262
|
+
error={formik.touched.ZipCode && Boolean(formik.errors.ZipCode)}
|
|
263
|
+
helperText={formik.touched.ZipCode && formik.errors.ZipCode}
|
|
264
|
+
required
|
|
265
|
+
/>
|
|
266
|
+
</Grid>
|
|
267
|
+
<Grid size={{ sm: 6, xs: 12 }}>
|
|
268
|
+
<TextField
|
|
269
|
+
name="Phone"
|
|
270
|
+
fullWidth
|
|
271
|
+
autoComplete="off"
|
|
272
|
+
label={t("phone")}
|
|
273
|
+
value={formik.values.Phone}
|
|
274
|
+
onChange={formik.handleChange}
|
|
275
|
+
onBlur={formik.handleBlur}
|
|
276
|
+
error={formik.touched.Phone && Boolean(formik.errors.Phone)}
|
|
277
|
+
helperText={formik.touched.Phone && formik.errors.Phone}
|
|
278
|
+
required
|
|
279
|
+
/>
|
|
280
|
+
</Grid>
|
|
281
|
+
</Grid>
|
|
282
|
+
<Grid size={{ sm: 6, xs: 12 }} sx={{ mt: 3 }}>
|
|
283
|
+
<Button type="submit" variant="contained" color="primary">
|
|
284
|
+
{t("save")}
|
|
285
|
+
</Button>
|
|
286
|
+
</Grid>
|
|
287
|
+
</form>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import IconButton from "@mui/material/IconButton";
|
|
2
|
+
import { Button, Box, Chip } from "@mui/material";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useNotification } from "@/components/Notifications/use-notification";
|
|
6
|
+
// MUI ICON COMPONENTS
|
|
7
|
+
import { MdEdit as Edit } from "react-icons/md";
|
|
8
|
+
import { MdDelete as Delete } from "react-icons/md";
|
|
9
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
10
|
+
import { Paragraph } from "@/components/_components/Typography";
|
|
11
|
+
// LOCAL CUSTOM COMPONENT
|
|
12
|
+
import TableRow from "../table-row";
|
|
13
|
+
// API
|
|
14
|
+
import useDeleteAddress from "@/api-manager/services/address/mutations/delete-address";
|
|
15
|
+
import useUpdateUser from "@/api-manager/services/user/mutations/update-user";
|
|
16
|
+
import { Address } from "@/api-manager/schemas/address.schema";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
address: Address;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function AddressListItem({ address }: Props) {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const t = useTranslations("Account.Addresses");
|
|
25
|
+
const { setNotification } = useNotification();
|
|
26
|
+
|
|
27
|
+
const { mutateAsync: updateUser } = useUpdateUser();
|
|
28
|
+
const { mutateAsync: deleteAddress } = useDeleteAddress();
|
|
29
|
+
|
|
30
|
+
const setPrimaryAddress = (id: string) => {
|
|
31
|
+
updateUser({ AccountNumber: "", DefaultShippingAddressId: id });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const editAddress = (id: string) => {
|
|
35
|
+
router.push(`/account/addresses/${id}`);
|
|
36
|
+
};
|
|
37
|
+
const handleDelete = async (id: string) => {
|
|
38
|
+
try {
|
|
39
|
+
await deleteAddress(id);
|
|
40
|
+
setNotification({
|
|
41
|
+
message: t("deleteAddressSuccess"),
|
|
42
|
+
severity: "success",
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
setNotification({
|
|
46
|
+
message: t("deleteAddressFailed"),
|
|
47
|
+
severity: "error",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<TableRow
|
|
54
|
+
sx={{
|
|
55
|
+
display: "grid",
|
|
56
|
+
gridTemplateColumns: "1fr 2fr 1fr 1.5fr", // Define equal column structure
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
gap: 2,
|
|
59
|
+
padding: "10px 15px",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Paragraph ellipsis>{address?.Name}</Paragraph>
|
|
63
|
+
<Paragraph ellipsis>{address?.FullAddress}</Paragraph>
|
|
64
|
+
<Paragraph ellipsis>{address?.Phone}</Paragraph>
|
|
65
|
+
|
|
66
|
+
{/* Actions Column */}
|
|
67
|
+
<Box display="flex" alignItems="center" gap={1} justifySelf="end">
|
|
68
|
+
{address?.IsPrimary ? (
|
|
69
|
+
<Chip size="small" label={t("primaryFlag")} color="primary" />
|
|
70
|
+
) : (
|
|
71
|
+
<Button
|
|
72
|
+
color="secondary"
|
|
73
|
+
variant="outlined"
|
|
74
|
+
size="small"
|
|
75
|
+
onClick={() => {
|
|
76
|
+
const rid = address.RecordId?.toString();
|
|
77
|
+
setPrimaryAddress(address.Id ?? rid ?? "");
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{t("btnPrimaryAddress")}
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<IconButton
|
|
85
|
+
onClick={() => {
|
|
86
|
+
const rid = address.RecordId?.toString();
|
|
87
|
+
editAddress(address.Id ?? rid ?? "");
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Edit fontSize="large" color="inherit" />
|
|
91
|
+
</IconButton>
|
|
92
|
+
|
|
93
|
+
<IconButton
|
|
94
|
+
onClick={() => {
|
|
95
|
+
const rid = address.RecordId?.toString();
|
|
96
|
+
handleDelete(address.Id ?? rid ?? "");
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Delete fontSize="large" color="inherit" />
|
|
100
|
+
</IconButton>
|
|
101
|
+
</Box>
|
|
102
|
+
</TableRow>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { Box, Button, IconButton, Theme, styled } from "@mui/material";
|
|
4
|
+
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
// ICON COMPONENTS
|
|
7
|
+
import { MdMenu as Menu } from "react-icons/md";
|
|
8
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
9
|
+
import SideNav from "@/components/_components/side-nav/index";
|
|
10
|
+
import { H2 } from "@/components/_components/Typography";
|
|
11
|
+
import FlexBox from "@/components/_components/flex-box/flex-box";
|
|
12
|
+
// LOCAL CUSTOM COMPONENTS
|
|
13
|
+
import Navigation from "./navigation";
|
|
14
|
+
import { IconType } from "react-icons";
|
|
15
|
+
|
|
16
|
+
// STYLED COMPONENT
|
|
17
|
+
const StyledBox = styled("div")(({ theme }) => ({
|
|
18
|
+
display: "flex",
|
|
19
|
+
marginTop: theme.spacing(-2),
|
|
20
|
+
marginBottom: theme.spacing(3),
|
|
21
|
+
"& .headerHold": {
|
|
22
|
+
flexGrow: 1,
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
justifyContent: "space-between",
|
|
25
|
+
},
|
|
26
|
+
[theme.breakpoints.up("md")]: {
|
|
27
|
+
"& .sidenav": { display: "none" },
|
|
28
|
+
},
|
|
29
|
+
[theme.breakpoints.down("md")]: {
|
|
30
|
+
flexDirection: "column",
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// ==============================================================
|
|
35
|
+
|
|
36
|
+
type WithButtons = {
|
|
37
|
+
title: string;
|
|
38
|
+
titleMessage?: string;
|
|
39
|
+
buttons: ReactNode;
|
|
40
|
+
Icon: IconType;
|
|
41
|
+
href?: never;
|
|
42
|
+
buttonText?: never;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type WithGeneratedButton = {
|
|
46
|
+
title: string;
|
|
47
|
+
titleMessage?: string;
|
|
48
|
+
href?: string;
|
|
49
|
+
buttonText?: string;
|
|
50
|
+
Icon: IconType;
|
|
51
|
+
buttons?: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type Props = WithButtons | WithGeneratedButton;
|
|
55
|
+
|
|
56
|
+
// ==============================================================
|
|
57
|
+
|
|
58
|
+
export default function DashboardHeader({
|
|
59
|
+
title,
|
|
60
|
+
titleMessage,
|
|
61
|
+
buttons,
|
|
62
|
+
href,
|
|
63
|
+
buttonText,
|
|
64
|
+
Icon,
|
|
65
|
+
}: Props) {
|
|
66
|
+
const t = useTranslations();
|
|
67
|
+
const isTablet = useMediaQuery((theme: Theme) =>
|
|
68
|
+
theme.breakpoints.down(1025),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const GENERATED_BUTTON = href ? (
|
|
72
|
+
<Button
|
|
73
|
+
href={href}
|
|
74
|
+
color="primary"
|
|
75
|
+
LinkComponent={Link}
|
|
76
|
+
sx={{ bgcolor: "primary.light", px: 4 }}
|
|
77
|
+
>
|
|
78
|
+
{t(buttonText)}
|
|
79
|
+
</Button>
|
|
80
|
+
) : null;
|
|
81
|
+
|
|
82
|
+
const HEADER_LINKS = (
|
|
83
|
+
<FlexBox gap={1} justifyContent="flex-end">
|
|
84
|
+
{buttons || GENERATED_BUTTON}
|
|
85
|
+
</FlexBox>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<StyledBox>
|
|
90
|
+
<FlexBox mt={2} className="headerHold">
|
|
91
|
+
<FlexBox alignItems="center" gap={0.5}>
|
|
92
|
+
{Icon && (
|
|
93
|
+
<IconButton color="primary">
|
|
94
|
+
<Icon />
|
|
95
|
+
</IconButton>
|
|
96
|
+
)}
|
|
97
|
+
<H2 my={0} lineHeight={1} ellipsis>
|
|
98
|
+
{titleMessage ? titleMessage : t(title)}
|
|
99
|
+
</H2>
|
|
100
|
+
</FlexBox>
|
|
101
|
+
|
|
102
|
+
<FlexBox className="button-container" justifyContent="flex-end">
|
|
103
|
+
{!isTablet && HEADER_LINKS}
|
|
104
|
+
</FlexBox>
|
|
105
|
+
|
|
106
|
+
<div className="sidenav">
|
|
107
|
+
<SideNav
|
|
108
|
+
position="left"
|
|
109
|
+
handler={(close) => (
|
|
110
|
+
<IconButton onClick={close}>
|
|
111
|
+
<Menu fontSize="small" />
|
|
112
|
+
</IconButton>
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
<Navigation />
|
|
116
|
+
</SideNav>
|
|
117
|
+
</div>
|
|
118
|
+
</FlexBox>
|
|
119
|
+
|
|
120
|
+
{isTablet ? <Box mt={2}>{HEADER_LINKS}</Box> : null}
|
|
121
|
+
</StyledBox>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import { Fragment } from "react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
|
|
7
|
+
// ICON COMPONENTS
|
|
8
|
+
import {
|
|
9
|
+
MdOutlineShoppingBag as ShoppingBagOutlined,
|
|
10
|
+
MdFavoriteBorder as FavoriteBorder,
|
|
11
|
+
MdPerson as Person,
|
|
12
|
+
MdPlace as Place,
|
|
13
|
+
} from "react-icons/md";
|
|
14
|
+
|
|
15
|
+
// GLOBAL CUSTOM COMPONENTS
|
|
16
|
+
import FlexBox from "@/components/_components/flex-box/flex-box";
|
|
17
|
+
import { Paragraph, Span } from "@/components/_components/Typography";
|
|
18
|
+
|
|
19
|
+
// API
|
|
20
|
+
import getUser from "@/api-manager/services/user/queries/get-user";
|
|
21
|
+
import getUserCounts from "@/api-manager/services/user/queries/get-user-counts";
|
|
22
|
+
|
|
23
|
+
// STYLED COMPONENTS
|
|
24
|
+
import { MainContainer, StyledNavLink } from "./styles";
|
|
25
|
+
|
|
26
|
+
const MENU_ITEMS = [
|
|
27
|
+
{
|
|
28
|
+
title: "DASHBOARD",
|
|
29
|
+
list: [
|
|
30
|
+
{
|
|
31
|
+
href: "/account/orders",
|
|
32
|
+
title: "orders",
|
|
33
|
+
Icon: ShoppingBagOutlined,
|
|
34
|
+
count: 0,
|
|
35
|
+
},
|
|
36
|
+
{ href: "/account/wish-lists", title: "wishlists", Icon: FavoriteBorder },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: "ACCOUNT SETTINGS",
|
|
41
|
+
list: [
|
|
42
|
+
{ href: "/account/profile", title: "profile", Icon: Person },
|
|
43
|
+
{ href: "/account/addresses", title: "addresses", Icon: Place, count: 0 },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export default function Navigation() {
|
|
49
|
+
const pathname = usePathname();
|
|
50
|
+
const t = useTranslations("Account.Menu");
|
|
51
|
+
|
|
52
|
+
const { data: user } = getUser.useData();
|
|
53
|
+
const { data: counts } = getUserCounts.useData(String(user?.RecordId) || "");
|
|
54
|
+
|
|
55
|
+
MENU_ITEMS[0].list[0].count = counts?.orderCount;
|
|
56
|
+
MENU_ITEMS[1].list[1].count = counts?.addressCount;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<MainContainer>
|
|
60
|
+
{MENU_ITEMS.map((item) => (
|
|
61
|
+
<Fragment key={item.title}>
|
|
62
|
+
<Paragraph p="26px 30px 1rem" color="grey.600" fontSize={12}>
|
|
63
|
+
{item.title}
|
|
64
|
+
</Paragraph>
|
|
65
|
+
|
|
66
|
+
{item.list.map(({ Icon, count, href, title }) => (
|
|
67
|
+
<StyledNavLink
|
|
68
|
+
href={href}
|
|
69
|
+
key={title}
|
|
70
|
+
isCurrentPath={pathname.includes(href)}
|
|
71
|
+
>
|
|
72
|
+
<FlexBox alignItems="center" gap={1}>
|
|
73
|
+
<Icon color="inherit" fontSize="large" className="nav-icon" />
|
|
74
|
+
<Span>{t(title)}</Span>
|
|
75
|
+
</FlexBox>
|
|
76
|
+
|
|
77
|
+
<Span>{count}</Span>
|
|
78
|
+
</StyledNavLink>
|
|
79
|
+
))}
|
|
80
|
+
</Fragment>
|
|
81
|
+
))}
|
|
82
|
+
</MainContainer>
|
|
83
|
+
);
|
|
84
|
+
}
|