@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,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
+ }