@evenicanpm/storefront-core 1.0.0 → 1.0.2

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/graphqlRequestSdk.ts +131 -2168
  10. package/src/api-manager/datasources/e4/{e4-user.datasource.ts → user/e4-user.datasource.ts} +5 -7
  11. package/src/api-manager/datasources/e4/user/e4-user.remaps.ts +4 -0
  12. package/src/api-manager/datasources/e4/user/e4-user.translator.ts +10 -0
  13. package/src/api-manager/index.ts +21 -6
  14. package/src/api-manager/schemas/product.schema.ts +1 -1
  15. package/src/api-manager/services/address/queries/get-states.ts +1 -1
  16. package/src/api-manager/services/create-query.ts +10 -3
  17. package/src/api-manager/types/Datasource.ts +9 -9
  18. package/src/components/Blocks/Components/ProductCarousel/index.tsx +6 -6
  19. package/src/components/_components/navbar/category-based-menu/components/categories.tsx +1 -1
  20. package/src/components/_components/navbar/mega-menu/mega-menu.tsx +1 -1
  21. package/src/components/_components/navbar/nav-list/nav-item-child.tsx +2 -2
  22. package/src/components/_components/product-dimensions/product-dimensions.tsx +114 -0
  23. package/src/components/_components/product-quantity-buttons/product-quantity-buttons.tsx +88 -0
  24. package/src/components/_components/section-header/section-header.tsx +1 -1
  25. package/src/components/_components/settings/setting.tsx +1 -1
  26. package/src/components/_components/wishlist-modal/TODO.md +10 -0
  27. package/src/components/_components/wishlist-modal/add-to-wishlist-button.tsx +68 -0
  28. package/src/components/_components/wishlist-modal/add-to-wishlist-dialog.tsx +161 -0
  29. package/src/components/_components/wishlist-modal/remove-from-wishlist-dialog.tsx +174 -0
  30. package/src/components/_components/wishlist-modal/wishlist-modal-item.tsx +65 -0
  31. package/src/components/countries-input.tsx +1 -0
  32. package/src/hooks/use-cart.tsx +82 -0
  33. package/src/hooks/use-variants.tsx +85 -0
  34. package/src/pages/account/addresses/address-form.tsx +289 -0
  35. package/src/pages/account/addresses/address-item.tsx +104 -0
  36. package/src/pages/account/dashboard-header.tsx +123 -0
  37. package/src/pages/account/navigation.tsx +84 -0
  38. package/src/pages/account/no-records.tsx +20 -0
  39. package/src/pages/account/orders/icons/delivery.tsx +14 -0
  40. package/src/pages/account/orders/icons/package-box.tsx +13 -0
  41. package/src/pages/account/orders/icons/truck-filled.tsx +14 -0
  42. package/src/pages/account/orders/order-progress.tsx +111 -0
  43. package/src/pages/account/orders/order-row.tsx +61 -0
  44. package/src/pages/account/orders/order-summary.tsx +83 -0
  45. package/src/pages/account/orders/ordered-products.tsx +114 -0
  46. package/src/pages/account/profile/profile-button.test.tsx +59 -0
  47. package/src/pages/account/profile/profile-button.tsx +50 -0
  48. package/src/pages/account/profile/user-info.tsx +59 -0
  49. package/src/pages/account/styles.ts +32 -0
  50. package/src/pages/account/table-row.tsx +19 -0
  51. package/src/pages/account/wishlist/create-new-list.tsx +167 -0
  52. package/src/pages/account/wishlist/create-wishlist-button.tsx +40 -0
  53. package/src/pages/account/wishlist/wishlist-item.tsx +82 -0
  54. package/src/pages/blog/pagination.tsx +38 -0
  55. package/src/pages/cart/cart-item.tsx +312 -0
  56. package/src/pages/cart/checkout-form.tsx +122 -0
  57. package/src/pages/cart/coupon-entry.tsx +90 -0
  58. package/src/pages/cart/estimate-shipping.tsx +183 -0
  59. package/src/pages/cart/wrapper.tsx +30 -0
  60. package/src/pages/checkout/checkout-alt-form/address-card.tsx +73 -0
  61. package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +133 -0
  62. package/src/pages/checkout/checkout-alt-form/checkout-step.tsx +120 -0
  63. package/src/pages/checkout/checkout-alt-form/customer-information.tsx +94 -0
  64. package/src/pages/checkout/checkout-alt-form/delivery-address.tsx +170 -0
  65. package/src/pages/checkout/checkout-alt-form/delivery-date.tsx +114 -0
  66. package/src/pages/checkout/checkout-alt-form/delivery-options.tsx +186 -0
  67. package/src/pages/checkout/checkout-alt-form/edit-address-form.tsx +130 -0
  68. package/src/pages/checkout/checkout-alt-form/heading.tsx +50 -0
  69. package/src/pages/checkout/checkout-alt-form/index.ts +1 -0
  70. package/src/pages/checkout/checkout-alt-form/new-address-form.tsx +294 -0
  71. package/src/pages/checkout/checkout-alt-form/node.tsx +9 -0
  72. package/src/pages/checkout/checkout-alt-form/payment-details.tsx +344 -0
  73. package/src/pages/checkout/checkout-alt-form/safe-card-preview-data.tsx +36 -0
  74. package/src/pages/checkout/checkout-alt-form/types.ts +20 -0
  75. package/src/pages/checkout/checkout-alt-summary/cart-item.tsx +39 -0
  76. package/src/pages/checkout/checkout-alt-summary/checkout-alt-summary.tsx +40 -0
  77. package/src/pages/checkout/checkout-alt-summary/index.ts +1 -0
  78. package/src/pages/checkout/checkout-alt-summary/list-item.tsx +31 -0
  79. package/src/pages/confirmation/address.tsx +22 -0
  80. package/src/pages/confirmation/confirmation-summary.tsx +52 -0
  81. package/src/pages/confirmation/ordered-products.tsx +108 -0
  82. package/src/pages/product-details/available-shops.tsx +48 -0
  83. package/src/pages/product-details/bopis/find-in-store-button.tsx +63 -0
  84. package/src/pages/product-details/bopis/find-in-store-modal.tsx +257 -0
  85. package/src/pages/product-details/bopis/pickup-option-select.tsx +127 -0
  86. package/src/pages/product-details/bopis/search-header.tsx +69 -0
  87. package/src/pages/product-details/checkbox-label.tsx +20 -0
  88. package/src/pages/product-details/currency.ts +64 -0
  89. package/src/pages/product-details/frequently-bought.tsx +90 -0
  90. package/src/pages/product-details/frequently-product-card.tsx +60 -0
  91. package/src/pages/product-details/product-comment.tsx +44 -0
  92. package/src/pages/product-details/product-description.tsx +22 -0
  93. package/src/pages/product-details/product-filter-card.tsx +257 -0
  94. package/src/pages/product-details/product-intro/product-intro-images.tsx +87 -0
  95. package/src/pages/product-details/product-intro/product-intro.tsx +250 -0
  96. package/src/pages/product-details/product-review.tsx +131 -0
  97. package/src/pages/product-details/product-tabs.tsx +51 -0
  98. package/src/pages/product-details/related-products.tsx +42 -0
  99. package/src/pages/product-details/types.ts +11 -0
  100. package/src/pages/product-list/breadcrumbs.tsx +39 -0
  101. package/src/pages/product-list/checkbox-label.tsx +20 -0
  102. package/src/pages/product-list/facet-group.tsx +125 -0
  103. package/src/pages/product-list/facet.tsx +36 -0
  104. package/src/pages/product-list/list-filter.tsx +40 -0
  105. package/src/pages/product-list/pagination.tsx +80 -0
  106. package/src/pages/product-list/product-list-context.tsx +302 -0
  107. package/src/pages/product-list/product-list-state.ts +187 -0
  108. package/src/pages/product-list/product-rating.tsx +16 -0
  109. package/src/pages/product-list/quick-view-dialog-content.tsx +205 -0
  110. package/src/pages/product-list/quick-view-dialog.tsx +54 -0
  111. package/src/pages/product-list/range-filter.tsx +125 -0
  112. package/src/pages/product-list/scrollbar.tsx +38 -0
  113. package/src/pages/product-list/search-bar.tsx +162 -0
  114. package/src/pages/product-list/selected-facets.tsx +80 -0
  115. package/src/pages/product-list/side-nav.tsx +49 -0
  116. package/src/pages/product-list/swatch.tsx +80 -0
  117. package/src/pages/product-list/types.ts +18 -0
  118. package/src/pages/product-list/use-product-list.ts +12 -0
  119. package/src/pages/product-list/useCategoryFilter.tsx +26 -0
  120. package/src/pages/product-list/utils/facet-helpers.ts +5 -0
  121. package/src/pages/product-list/utils/generate-breadcrumbs.ts +30 -0
  122. package/src/pages/product-list/utils/getCategoryFilterFromSlug.ts +153 -0
  123. package/src/pages/product-list/utils/product-list-helper.ts +111 -0
  124. package/src/pages/product-list/utils/product-list-types.ts +41 -0
  125. package/src/pages/product-list/utils/search-for-category.ts +76 -0
  126. package/src/pages/product-list/utils/sort-options.ts +44 -0
  127. package/src/pages/product-list/utils/use-previous-refiners.ts +14 -0
  128. package/src/pages/quickorder/order-upload.tsx +150 -0
  129. package/src/pages/quickorder/quick-order-form.tsx +343 -0
  130. package/src/pages/quickorder/quick-order-row.tsx +144 -0
  131. package/tsconfig.json +1 -0
  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,111 @@
1
+ import { Fragment } from "react";
2
+ import { useTranslations } from "next-intl";
3
+ // MUI
4
+ import Box from "@mui/material/Box";
5
+ import Card from "@mui/material/Card";
6
+ import Avatar from "@mui/material/Avatar";
7
+ import styled from "@mui/material/styles/styled";
8
+ import { MdDone as Done } from "react-icons/md";
9
+ // CUSTOM ICON COMPONENTS
10
+ import Delivery from "./icons/delivery";
11
+ import PackageBox from "./icons/package-box";
12
+ import TruckFilled from "./icons/truck-filled";
13
+ // GLOBAL CUSTOM COMPONENTS
14
+ import { Paragraph } from "@/components/_components/Typography";
15
+ import { FlexBetween, FlexBox } from "@/components/_components/flex-box/index";
16
+
17
+ // STYLED COMPONENTS
18
+ const StyledFlexbox = styled(FlexBetween)(({ theme }) => ({
19
+ flexWrap: "wrap",
20
+ marginTop: "2rem",
21
+ marginBottom: "2rem",
22
+ [theme.breakpoints.down("sm")]: { flexDirection: "column" },
23
+ "& .line": {
24
+ height: 4,
25
+ minWidth: 50,
26
+ flex: "1 1 0",
27
+ [theme.breakpoints.down("sm")]: { flex: "unset", height: 50, minWidth: 4 },
28
+ },
29
+ }));
30
+
31
+ const StyledAvatar = styled(Avatar)(({ theme }) => ({
32
+ top: -5,
33
+ right: -5,
34
+ width: 22,
35
+ height: 22,
36
+ position: "absolute",
37
+ bgcolor: theme.palette.grey[200],
38
+ color: theme.palette.success.main,
39
+ }));
40
+
41
+ type Props = {
42
+ status: number;
43
+ };
44
+ export default function OrderProgress({ status }: Props) {
45
+ const t = useTranslations("Account.OrderDetails");
46
+
47
+ let ORDER_STATUS = "";
48
+
49
+ // Map API statuses to ORDER_STATUS_LIST values
50
+ if ([1, 2, 4, 5].includes(status)) ORDER_STATUS = "Packaging";
51
+ if ([3].includes(status)) ORDER_STATUS = "Complete";
52
+
53
+ const hideDeliveryDate = !![4, 5].includes(status);
54
+
55
+ const STEP_ICONS = [PackageBox, TruckFilled, Delivery];
56
+ const ORDER_STATUS_LIST = ["Packaging", "Shipping", "Delivering", "Complete"];
57
+
58
+ const statusIndex = ORDER_STATUS_LIST.indexOf(ORDER_STATUS);
59
+
60
+ return (
61
+ <Card sx={{ p: "2rem 1.5rem", mb: 4 }}>
62
+ <StyledFlexbox>
63
+ {STEP_ICONS.map((Icon, index) => (
64
+ <Fragment key={index}>
65
+ <Box position="relative">
66
+ <Avatar
67
+ alt="shipping"
68
+ sx={{
69
+ width: 64,
70
+ height: 64,
71
+ color: index <= statusIndex ? "white" : "primary.main",
72
+ bgcolor: index <= statusIndex ? "primary.main" : "grey.300",
73
+ }}
74
+ >
75
+ <Icon color="inherit" fontSize="large" />
76
+ </Avatar>
77
+
78
+ {index < statusIndex ? (
79
+ <StyledAvatar alt="done">
80
+ <Done color="inherit" size={16} />
81
+ </StyledAvatar>
82
+ ) : null}
83
+ </Box>
84
+
85
+ {index < STEP_ICONS.length - 1 ? (
86
+ <Box
87
+ className="line"
88
+ bgcolor={index < statusIndex ? "primary.main" : "grey.300"}
89
+ />
90
+ ) : null}
91
+ </Fragment>
92
+ ))}
93
+ </StyledFlexbox>
94
+
95
+ {!hideDeliveryDate && (
96
+ <FlexBox justifyContent={{ xs: "center", sm: "flex-end" }}>
97
+ <Paragraph
98
+ p="0.5rem 1rem"
99
+ textAlign="center"
100
+ borderRadius="300px"
101
+ color="primary.main"
102
+ bgcolor="primary.light"
103
+ >
104
+ {t(statusIndex === 3 ? "deliveredDate" : "estimatedDeliveryDate")}
105
+ <b> 4th October</b>
106
+ </Paragraph>
107
+ </FlexBox>
108
+ )}
109
+ </Card>
110
+ );
111
+ }
@@ -0,0 +1,61 @@
1
+ import Link from "next/link";
2
+ import Box from "@mui/material/Box";
3
+ import Chip from "@mui/material/Chip";
4
+ import IconButton from "@mui/material/IconButton";
5
+ import { MdEast as East } from "react-icons/md";
6
+ import { ChipProps } from "@mui/material";
7
+ import { format } from "date-fns";
8
+ // GLOBAL CUSTOM COMPONENT
9
+ import { H6, Paragraph } from "@/components/_components/Typography";
10
+ // Local CUSTOM COMPONENT
11
+ import TableRow from "../table-row";
12
+ // CUSTOM UTILS LIBRARY FUNCTION
13
+ import useCurrencyFormatter from "@/lib/currency-formatter";
14
+ import { SalesOrder } from "@/api-manager/schemas/order.schema";
15
+
16
+ // =================================================
17
+ type Props = { order: SalesOrder };
18
+ // =================================================
19
+ const STATUS_MAP: Record<number, { label: string; color: ChipProps["color"] }> =
20
+ {
21
+ 1: { label: "Open", color: "secondary" },
22
+ 2: { label: "Processing", color: "secondary" },
23
+ 3: { label: "Complete", color: "success" },
24
+ 4: { label: "On Hold", color: "primary" },
25
+ 5: { label: "Cancelled", color: "primary" },
26
+ };
27
+
28
+ const getStatusInfo = (status: number) => {
29
+ return STATUS_MAP[status] ?? { label: "Unknown", color: "default" };
30
+ };
31
+ export default function OrderRow({ order }: Props) {
32
+ const { formatCurrency } = useCurrencyFormatter();
33
+
34
+ const { label, color } = getStatusInfo(order.StatusValue ?? 0);
35
+
36
+ return (
37
+ <Link href={`/account/orders/${order.Id}`}>
38
+ <TableRow sx={{ gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr" }}>
39
+ <H6 ellipsis>{order?.ChannelReferenceId}</H6>
40
+
41
+ <Box textAlign="center">
42
+ <Chip size="small" label={label} color={color} />
43
+ </Box>
44
+
45
+ <Paragraph textAlign={{ sm: "center", xs: "left" }}>
46
+ {format(new Date(order.TransactionDate ?? ""), "MMM dd, yyyy")}
47
+ </Paragraph>
48
+
49
+ <Paragraph textAlign="center">
50
+ {formatCurrency(order.TotalAmount ?? 0)}
51
+ </Paragraph>
52
+
53
+ <Box display={{ sm: "inline-flex", xs: "none" }} justifyContent="end">
54
+ <IconButton sx={{ color: "grey.500" }}>
55
+ <East fontSize="large" />
56
+ </IconButton>
57
+ </Box>
58
+ </TableRow>
59
+ </Link>
60
+ );
61
+ }
@@ -0,0 +1,83 @@
1
+ import Grid from "@mui/material/Grid2";
2
+ import Card from "@mui/material/Card";
3
+ import Divider from "@mui/material/Divider";
4
+ import { useTranslations } from "next-intl";
5
+ // GLOBAL CUSTOM COMPONENTS
6
+ import { FlexBetween } from "@/components/_components/flex-box/index";
7
+ import { H5, H6, Paragraph } from "@/components/_components/Typography";
8
+ // CUSTOM UTILS LIBRARY FUNCTION
9
+ import useCurrencyFormatter from "@/lib/currency-formatter";
10
+ import { SalesOrder } from "@/api-manager/schemas/order.schema";
11
+
12
+ // ==============================================================
13
+ type Props = { order: SalesOrder };
14
+ // ==============================================================
15
+
16
+ function ListItem({ title, value }: { title: string; value: string }) {
17
+ const t = useTranslations("Account.OrderDetails");
18
+ return (
19
+ <FlexBetween mb={1}>
20
+ <Paragraph color="grey.600">{t(title)}</Paragraph>
21
+ <H6>{value}</H6>
22
+ </FlexBetween>
23
+ );
24
+ }
25
+
26
+ export default function OrderSummary({ order }: Props) {
27
+ const t = useTranslations("Account.OrderDetails");
28
+ const { formatCurrency } = useCurrencyFormatter();
29
+ const shippingAddress = order.ShippingAddress;
30
+ return (
31
+ <Grid container spacing={3}>
32
+ {/* SHIPMENT ADDRESS SECTION */}
33
+ <Grid size={{ lg: 6, md: 6, xs: 12 }}>
34
+ <Card sx={{ p: 3 }}>
35
+ <H5 mt={0} mb={2}>
36
+ {t("addressHeading")}
37
+ </H5>
38
+
39
+ {shippingAddress && (
40
+ <Paragraph fontSize={14} my={0}>
41
+ {`${shippingAddress.Name}, ${shippingAddress.Street}, ${shippingAddress.City} ${shippingAddress.State} ${shippingAddress.ThreeLetterISORegionName} ${shippingAddress.ZipCode}`}
42
+ </Paragraph>
43
+ )}
44
+ </Card>
45
+ </Grid>
46
+
47
+ {/* TOTAL SUMMERY SECTION */}
48
+ <Grid size={{ lg: 6, md: 6, xs: 12 }}>
49
+ <Card sx={{ p: 3 }}>
50
+ <H5 mt={0} mb={2}>
51
+ {t("headingSummary")}
52
+ </H5>
53
+
54
+ <ListItem
55
+ title="subTotal"
56
+ value={formatCurrency(order.SubtotalAmount ?? 0)}
57
+ />
58
+ <ListItem
59
+ title="shippingTotal"
60
+ value={formatCurrency(order.ShippingChargeAmount ?? 0)}
61
+ />
62
+ <ListItem
63
+ title="taxTotal"
64
+ value={formatCurrency(order.TaxAmount ?? 0)}
65
+ />
66
+ <ListItem
67
+ title="discountTotal"
68
+ value={formatCurrency(order.DiscountAmount ?? 0)}
69
+ />
70
+
71
+ <Divider sx={{ mb: 1 }} />
72
+
73
+ <FlexBetween mb={2}>
74
+ <H6>{t("total")}</H6>
75
+ <H6>{formatCurrency(order.TotalAmount ?? 0)}</H6>
76
+ </FlexBetween>
77
+
78
+ <Paragraph>{t("paidBy")}</Paragraph>
79
+ </Card>
80
+ </Grid>
81
+ </Grid>
82
+ );
83
+ }
@@ -0,0 +1,114 @@
1
+ import Card from "@mui/material/Card";
2
+ import Button from "@mui/material/Button";
3
+ import Avatar from "@mui/material/Avatar";
4
+ import Box from "@mui/material/Box";
5
+ import Grid from "@mui/material/Grid2";
6
+ import { format } from "date-fns";
7
+ import Link from "next/link";
8
+ import { useTranslations } from "next-intl";
9
+ // GLOBAL CUSTOM COMPONENTS
10
+ import { H6, Paragraph, Span } from "@/components/_components/Typography";
11
+ import { FlexBetween, FlexBox } from "@/components/_components/flex-box/index";
12
+ // CUSTOM UTILS LIBRARY FUNCTION
13
+ import useCurrencyFormatter from "@/lib/currency-formatter";
14
+ import { SalesLine, SalesOrder } from "@/api-manager/schemas/order.schema";
15
+ import { ProductDimensionType } from "@msdyn365-commerce/retail-proxy";
16
+ import buildDetailUrl from "@/lib/build-detail-url";
17
+
18
+ // ==============================================================
19
+ type Props = { order: SalesOrder };
20
+ // ==============================================================
21
+
22
+ export default function OrderedProducts({ order }: Props) {
23
+ const t = useTranslations("Account.OrderDetails");
24
+ const { formatCurrency } = useCurrencyFormatter();
25
+
26
+ const deliveredDate = "";
27
+
28
+ return (
29
+ <Card sx={{ p: 0, mb: "30px" }}>
30
+ <FlexBetween px={3} py={2} flexWrap="wrap" bgcolor="grey.200">
31
+ <Item title="orderId" value={order?.ChannelReferenceId ?? ""} />
32
+ <Item
33
+ title="orderDate"
34
+ value={format(new Date(order?.TransactionDate ?? ""), "dd MMM, yyyy")}
35
+ />
36
+ <Item
37
+ title="deliveredOn"
38
+ value={
39
+ deliveredDate
40
+ ? format(new Date(deliveredDate), "dd MMM, yyyy")
41
+ : t("deliveredOnValue")
42
+ }
43
+ />
44
+ </FlexBetween>
45
+
46
+ <Grid
47
+ container
48
+ spacing={2}
49
+ padding={"20px 15px 10px"}
50
+ alignItems={"center"}
51
+ >
52
+ {order?.SalesLines?.map((item: SalesLine, index: number) => (
53
+ <Link
54
+ href={buildDetailUrl(
55
+ item?.Product?.Name ?? "",
56
+ item?.Product?.RecordId ?? 0,
57
+ )}
58
+ key={index}
59
+ style={{ display: "contents" }}
60
+ >
61
+ <Grid size={{ xs: 12, sm: 5 }}>
62
+ <FlexBox gap={2.5} alignItems="center" padding={"12px 15px"}>
63
+ <Avatar
64
+ alt={item?.Product?.Name}
65
+ src={item?.Product?.PrimaryImageUrl}
66
+ sx={{ height: 64, width: 64 }}
67
+ />
68
+ <div>
69
+ <H6>{item?.Product?.Name}</H6>
70
+ <Paragraph color="grey.600">
71
+ {formatCurrency(item?.Price ?? 0)} x {item?.Quantity || 1}
72
+ </Paragraph>
73
+ </div>
74
+ </FlexBox>
75
+ </Grid>
76
+
77
+ <Grid size={{ xs: 8, sm: 4 }}>
78
+ {item.Product?.Dimensions?.length ? (
79
+ item.Product.Dimensions.map((dim, dimInd) => (
80
+ <Paragraph key={dimInd} paddingLeft={"15px"}>
81
+ <Span>
82
+ {ProductDimensionType[dim.DimensionTypeValue || 0]}:
83
+ </Span>
84
+ <Span sx={{ ml: 1 }}>{dim.DimensionValue?.Value}</Span>
85
+ </Paragraph>
86
+ ))
87
+ ) : (
88
+ <Box />
89
+ )}
90
+ </Grid>
91
+
92
+ <Grid size={{ xs: 4, sm: 3 }}>
93
+ <Box justifySelf={"end"}>
94
+ <Button variant="text" color="primary">
95
+ {t("writeReview")}
96
+ </Button>
97
+ </Box>
98
+ </Grid>
99
+ </Link>
100
+ ))}
101
+ </Grid>
102
+ </Card>
103
+ );
104
+ }
105
+
106
+ function Item({ title, value }: { title: string; value: string }) {
107
+ const t = useTranslations("Account.OrderDetails");
108
+ return (
109
+ <FlexBox gap={1} alignItems="center">
110
+ <Paragraph color="grey.600">{t(title)}</Paragraph>
111
+ <Paragraph>{value}</Paragraph>
112
+ </FlexBox>
113
+ );
114
+ }
@@ -0,0 +1,59 @@
1
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
2
+ import { vi } from "vitest";
3
+ import ProfileButton from "./profile-button";
4
+ import { getMsalInstance, initializeMsal, getRequestConfig } from "@/auth/msal";
5
+
6
+ vi.mock("@/auth/msal", () => ({
7
+ initializeMsal: vi.fn(),
8
+ getMsalInstance: vi.fn(),
9
+ getRequestConfig: vi.fn(),
10
+ }));
11
+
12
+ vi.mock("@/components/T/index", () => ({
13
+ default: ({ path }: { path: string }) => <span>{path}</span>,
14
+ }));
15
+
16
+ describe("ProfileButton Component", () => {
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ it("renders disabled button initially (MSAL not ready)", () => {
22
+ render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
23
+ const button = screen.getByRole("button");
24
+ expect(button).toBeDisabled();
25
+ });
26
+
27
+ it("initializes MSAL on mount", async () => {
28
+ render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
29
+ await waitFor(() => {
30
+ expect(initializeMsal).toHaveBeenCalled();
31
+ });
32
+ });
33
+
34
+ it("calls MSAL loginRedirect when clicked", async () => {
35
+ (initializeMsal as ReturnType<typeof vi.fn>).mockImplementation(() =>
36
+ Promise.resolve(),
37
+ );
38
+ (getRequestConfig as ReturnType<typeof vi.fn>).mockReturnValue({
39
+ scopes: ["user.read"],
40
+ });
41
+ (getMsalInstance as ReturnType<typeof vi.fn>).mockReturnValue({
42
+ loginRedirect: vi.fn(),
43
+ });
44
+ render(<ProfileButton type="edit" path="Account.Profile.btnEditProfile" />);
45
+
46
+ await waitFor(() => {
47
+ expect(initializeMsal).toHaveBeenCalled();
48
+ });
49
+
50
+ const button = screen.getByRole("button");
51
+ fireEvent.click(button);
52
+
53
+ await waitFor(() => {
54
+ expect(getMsalInstance().loginRedirect).toHaveBeenCalledWith({
55
+ scopes: ["user.read"],
56
+ });
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { getMsalInstance, initializeMsal, getRequestConfig } from "@/auth/msal";
4
+ import { useEffect, useState } from "react";
5
+ import { Button } from "@mui/material";
6
+ import T from "@/components/T/index";
7
+ import { RedirectRequest } from "@azure/msal-browser";
8
+ interface Props {
9
+ type: string;
10
+ path: string;
11
+ }
12
+
13
+ const ProfileButton = ({ type, path }: Props) => {
14
+ const [isMsalReady, setIsMsalReady] = useState(false);
15
+
16
+ useEffect(() => {
17
+ const initMsal = async () => {
18
+ await initializeMsal();
19
+ setIsMsalReady(true);
20
+ };
21
+ initMsal();
22
+ }, []);
23
+
24
+ const handleProfile = async () => {
25
+ if (!isMsalReady) {
26
+ console.error("MSAL is not initialized yet.");
27
+ return;
28
+ }
29
+
30
+ try {
31
+ const req = getRequestConfig(type);
32
+ await getMsalInstance().loginRedirect(req as RedirectRequest); // "handleRedirectPromise" is in user component.
33
+ } catch (error) {
34
+ console.error("Profile update through MSAL failed", error);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Button
40
+ onClick={() => handleProfile()}
41
+ disabled={!isMsalReady}
42
+ color="primary"
43
+ sx={{ bgcolor: "primary.light", px: 4 }}
44
+ >
45
+ <T path={path} />
46
+ </Button>
47
+ );
48
+ };
49
+
50
+ export default ProfileButton;
@@ -0,0 +1,59 @@
1
+ import _ from "lodash";
2
+ import useMediaQuery from "@mui/material/useMediaQuery";
3
+ import { Theme } from "@mui/material/styles";
4
+ import Card from "@mui/material/Card";
5
+ import { useTranslations } from "next-intl";
6
+ // GLOBAL CUSTOM COMPONENTS
7
+ import FlexBox from "@/components/_components/flex-box/flex-box";
8
+ import { Small, Span } from "@/components/_components/Typography";
9
+ // API
10
+ import { User } from "@/api-manager/schemas/user.schema";
11
+ type Props = {
12
+ user: User;
13
+ };
14
+ export default function UserInfo({ user }: Props) {
15
+ const downMd = useMediaQuery((theme: Theme) => theme.breakpoints.down("sm"));
16
+
17
+ return (
18
+ <Card
19
+ sx={{
20
+ mt: 3,
21
+ display: "flex",
22
+ flexWrap: "wrap",
23
+ gap: "5rem",
24
+ p: "0.75rem 1.5rem",
25
+ alignItems: "center",
26
+ justifyContent: "flex-start",
27
+ ...(downMd && {
28
+ gap: "1rem",
29
+ alignItems: "start",
30
+ flexDirection: "column",
31
+ justifyContent: "flex-start",
32
+ }),
33
+ }}
34
+ >
35
+ <TableRowItem
36
+ title="labelFirstName"
37
+ value={_.capitalize(user?.FirstName)}
38
+ />
39
+ <TableRowItem
40
+ title="labelLastName"
41
+ value={_.capitalize(user?.LastName)}
42
+ />
43
+ <TableRowItem title="labelEmail" value={user?.Email ?? ""} />
44
+ </Card>
45
+ );
46
+ }
47
+
48
+ function TableRowItem({ title, value = "" }: { title: string; value: string }) {
49
+ const t = useTranslations("Account.Profile");
50
+ return (
51
+ <FlexBox flexDirection="column" p={1}>
52
+ <Small color="grey.600" mb={0.5}>
53
+ {t(title)}
54
+ </Small>
55
+
56
+ <Span>{value}</Span>
57
+ </FlexBox>
58
+ );
59
+ }
@@ -0,0 +1,32 @@
1
+ import Card from "@mui/material/Card";
2
+ import styled from "@mui/material/styles/styled";
3
+ import NavLink from "@/components/_components/nav-link/nav-link";
4
+
5
+ export const MainContainer = styled(Card)(({ theme }) => ({
6
+ paddingBottom: "1.5rem",
7
+ [theme.breakpoints.down("md")]: {
8
+ boxShadow: "none",
9
+ overflowY: "auto",
10
+ height: "calc(100vh - 64px)",
11
+ },
12
+ }));
13
+
14
+ export const StyledNavLink = styled(NavLink, {
15
+ shouldForwardProp: (prop) => prop !== "isCurrentPath",
16
+ })<{ isCurrentPath: boolean }>(({ theme, isCurrentPath }) => ({
17
+ display: "flex",
18
+ alignItems: "center",
19
+ borderLeft: "4px solid",
20
+ paddingLeft: "1.5rem",
21
+ paddingRight: "1.5rem",
22
+ marginBottom: "1.25rem",
23
+ justifyContent: "space-between",
24
+ borderColor: isCurrentPath ? theme.palette.primary.main : "transparent",
25
+ "& .nav-icon": {
26
+ color: isCurrentPath ? theme.palette.primary.main : theme.palette.grey[600],
27
+ },
28
+ "&:hover": {
29
+ borderColor: theme.palette.primary.main,
30
+ "& .nav-icon": { color: theme.palette.primary.main },
31
+ },
32
+ }));
@@ -0,0 +1,19 @@
1
+ import Card from "@mui/material/Card";
2
+ import styled from "@mui/material/styles/styled";
3
+
4
+ const TableRow = styled(Card)(({ theme }) => ({
5
+ gap: 16,
6
+ marginBlock: 16,
7
+ display: "grid",
8
+ borderRadius: 10,
9
+ cursor: "pointer",
10
+ alignItems: "center",
11
+ padding: ".6rem 1.2rem",
12
+ gridTemplateColumns: "1.5fr 2fr 1.5fr auto",
13
+ [theme.breakpoints.down("sm")]: {
14
+ gap: 8,
15
+ gridTemplateColumns: "repeat(2, 1fr)",
16
+ },
17
+ }));
18
+
19
+ export default TableRow;