@evenicanpm/storefront-core 1.0.0-alpha → 1.0.0
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 +1 -1
- package/src/api-manager/api-manager.config.json +1 -1
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +4 -4
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +100 -7
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +39 -17
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +220 -49
- package/src/api-manager/datasources/d365/utils/decode-jwt.ts +2 -2
- package/src/api-manager/datasources/e4/e4-cart.datasource.ts +6 -0
- package/src/api-manager/datasources/e4/e4-product.datasource.ts +5 -0
- package/src/api-manager/datasources/e4/e4-user.datasource.ts +39 -15
- package/src/api-manager/datasources/e4/e4.datasource.ts +1 -0
- package/src/api-manager/datasources/e4/e4.translator.ts +2 -1
- package/src/api-manager/datasources/e4/graphqlRequestSdk.ts +5231 -38282
- package/src/api-manager/index.ts +58 -11
- package/src/api-manager/lib/get-graphql-client.ts +7 -7
- package/src/api-manager/schemas/address.schema.ts +1 -19
- package/src/api-manager/schemas/cart.schema.ts +18 -0
- package/src/api-manager/schemas/product.schema.ts +7 -0
- package/src/api-manager/schemas/user.schema.ts +52 -1
- package/src/api-manager/services/address/queries/get-states.ts +1 -1
- package/src/api-manager/services/cart/mutations/checkout.ts +16 -1
- package/src/api-manager/services/cart/queries/get-cart-lines-inventory.ts +15 -0
- package/src/api-manager/services/create-extension-query.ts +26 -0
- package/src/api-manager/services/create-mutation.ts +5 -1
- package/src/api-manager/services/create-query.ts +0 -1
- package/src/api-manager/services/organization/queries/get-channel-configuration.ts +1 -1
- package/src/api-manager/services/product/queries/get-product-prices.ts +16 -0
- package/src/api-manager/services/product/queries/search-products.ts +1 -0
- package/src/api-manager/services/user/mutations/add-to-wishlist.ts +24 -0
- package/src/api-manager/services/user/mutations/copy-cart-to-wishlist.ts +23 -0
- package/src/api-manager/services/user/mutations/create-wishlist.ts +4 -4
- package/src/api-manager/services/user/mutations/delete-wishlist.ts +4 -3
- package/src/api-manager/services/user/mutations/remove-from-wishlist.ts +21 -0
- package/src/api-manager/services/user/mutations/update-wishlist.ts +23 -0
- package/src/api-manager/services/user/queries/get-wishlist-details.ts +11 -4
- package/src/api-manager/services/user/queries/get-wishlists.ts +4 -3
- package/src/api-manager/types/Datasource.ts +33 -5
- package/src/auth/auth-options.ts +2 -2
- package/src/auth/msal.ts +9 -9
- package/src/auth/next-auth-cookie-manager.ts +5 -3
- package/src/auth/providers/aadb2c-provider.ts +3 -4
- package/src/cms/endpoints.ts +2 -3
- package/src/components/_components/LazyImage.tsx +5 -2
- package/src/components/_components/header/components/user.tsx +1 -2
- package/src/components/_components/product-cards/product-card-1/components/quantity-buttons.tsx +17 -8
- package/src/components/_components/product-cards/product-card-1/product-card-1-wrapper.tsx +304 -0
- package/src/components/_components/product-cards/product-card-1/product-card.tsx +16 -55
- package/src/components/_components/product-cards/product-card-8/product-card.tsx +80 -60
- package/src/components/_components/product-cards/product-card-8/styles/index.ts +10 -0
- package/src/components/_components/product-cards/product-card-wishlist/components/hover-actions.tsx +37 -0
- package/src/components/_components/product-cards/product-card-wishlist/index.ts +1 -0
- package/src/components/_components/product-cards/product-card-wishlist/product-card.tsx +118 -0
- package/src/components/_components/product-cards/product-card-wishlist/styles/index.ts +51 -0
- package/src/components/_components/products-view/product-view-dialog.tsx +48 -10
- package/src/components/_components/products-view/products-grid-view.tsx +31 -17
- package/src/components/_components/products-view/products-list-view.tsx +30 -16
- package/src/components/_components/search-box/__test__/search-input-category.test.tsx +110 -0
- package/src/components/_components/search-box/__test__/search-input.test.tsx +137 -0
- package/src/components/_components/search-box/__test__/search-results.json +165 -0
- package/src/components/_components/search-box/components/search-result.tsx +66 -12
- package/src/components/_components/search-box/search-input-category.tsx +135 -32
- package/src/components/_components/search-box/styles/index.ts +2 -0
- package/src/components/_components/section-header/section-creator.tsx +1 -0
- package/src/components/_components/wishlist/add-to-wishlist-dialog.tsx +161 -0
- package/src/components/_components/wishlist/components/create-new-list.tsx +167 -0
- package/src/components/_components/wishlist/components/create-wishlist-button.tsx +40 -0
- package/src/components/_components/wishlist/components/dashboard-header.tsx +123 -0
- package/src/components/_components/wishlist/components/navigation.tsx +87 -0
- package/src/components/_components/wishlist/components/no-records.tsx +20 -0
- package/src/components/_components/wishlist/components/wishlist-modal-item.tsx +66 -0
- package/src/components/_components/wishlist/remove-from-wishlist-dialog.tsx +174 -0
- package/src/components/_components/wishlist/styles.ts +61 -0
- package/src/components/countries-input.tsx +6 -12
- package/src/lib/cart-cookie-handler-constants.ts +8 -0
- package/src/lib/cart-cookie-handler.ts +27 -5
- package/src/lib/create-graphql-client.ts +1 -2
- package/src/lib/product-list-types.ts +41 -0
- package/src/config.ts +0 -47
|
@@ -5,30 +5,44 @@ import { FlexBetween } from "@/components/_components/flex-box/index";
|
|
|
5
5
|
import ProductCard16 from "@/components/_components/product-cards/product-card-16/index";
|
|
6
6
|
// DATA MODEL
|
|
7
7
|
import { ProductSearchResult as Product } from "@msdyn365-commerce/retail-proxy";
|
|
8
|
+
import { ProductPrice } from "@/api-manager/schemas/product.schema";
|
|
8
9
|
|
|
9
10
|
// ========================================================
|
|
10
|
-
type Props = {
|
|
11
|
+
type Props = {
|
|
12
|
+
products: Product[];
|
|
13
|
+
pagination?: ReactNode;
|
|
14
|
+
productPrices?: ProductPrice[];
|
|
15
|
+
};
|
|
11
16
|
// ========================================================
|
|
12
17
|
|
|
13
|
-
export default function ProductsGridView({
|
|
18
|
+
export default function ProductsGridView({
|
|
19
|
+
products,
|
|
20
|
+
pagination,
|
|
21
|
+
productPrices,
|
|
22
|
+
}: Props) {
|
|
14
23
|
return (
|
|
15
24
|
<Fragment>
|
|
16
25
|
<Grid container spacing={3}>
|
|
17
|
-
{products.map((item: Product) =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
{products.map((item: Product) => {
|
|
27
|
+
const matchingPrice = productPrices?.find(
|
|
28
|
+
(price) => price.ProductId === item.RecordId,
|
|
29
|
+
);
|
|
30
|
+
return (
|
|
31
|
+
<Grid size={{ lg: 4, sm: 6, xs: 12 }} key={item.ItemId}>
|
|
32
|
+
<ProductCard16
|
|
33
|
+
id={item.RecordId}
|
|
34
|
+
key={item.RecordId}
|
|
35
|
+
slug={item.ItemId ?? ""}
|
|
36
|
+
title={item.Name ?? ""}
|
|
37
|
+
price={matchingPrice?.CustomerContextualPrice ?? null}
|
|
38
|
+
discount={0} // Need to get discount info for product.
|
|
39
|
+
rating={item.TotalRatings ?? 0}
|
|
40
|
+
imgUrl={item.PrimaryImageUrl ?? ""}
|
|
41
|
+
showAddToCart={false}
|
|
42
|
+
/>
|
|
43
|
+
</Grid>
|
|
44
|
+
);
|
|
45
|
+
})}
|
|
32
46
|
</Grid>
|
|
33
47
|
|
|
34
48
|
<FlexBetween flexWrap="wrap" mt={6}>
|
|
@@ -4,28 +4,42 @@ import FlexBetween from "@/components/_components/flex-box/flex-between";
|
|
|
4
4
|
import { ProductCard9 } from "@/components/_components/product-cards/product-card-9/index";
|
|
5
5
|
// DATA MODEL
|
|
6
6
|
import { ProductSearchResult as Product } from "@msdyn365-commerce/retail-proxy";
|
|
7
|
+
import { ProductPrice } from "@/api-manager/schemas/product.schema";
|
|
7
8
|
|
|
8
9
|
// ==========================================================
|
|
9
|
-
type Props = {
|
|
10
|
+
type Props = {
|
|
11
|
+
products: Product[];
|
|
12
|
+
pagination?: ReactNode;
|
|
13
|
+
productPrices?: ProductPrice[];
|
|
14
|
+
};
|
|
10
15
|
// ==========================================================
|
|
11
16
|
|
|
12
|
-
export default function ProductsListView({
|
|
17
|
+
export default function ProductsListView({
|
|
18
|
+
products,
|
|
19
|
+
pagination,
|
|
20
|
+
productPrices,
|
|
21
|
+
}: Props) {
|
|
13
22
|
return (
|
|
14
23
|
<div>
|
|
15
|
-
{products.map((item) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
{products.map((item) => {
|
|
25
|
+
const matchingPrice = productPrices?.find(
|
|
26
|
+
(price) => price.ProductId === item.RecordId,
|
|
27
|
+
);
|
|
28
|
+
return (
|
|
29
|
+
<ProductCard9
|
|
30
|
+
id={item.RecordId}
|
|
31
|
+
key={item.RecordId}
|
|
32
|
+
slug={item.ItemId ?? ""}
|
|
33
|
+
title={item.Name ?? ""}
|
|
34
|
+
price={matchingPrice?.CustomerContextualPrice ?? null}
|
|
35
|
+
off={0} // Need to get discount info for product.
|
|
36
|
+
rating={item.TotalRatings ?? 0}
|
|
37
|
+
imgUrl={item.PrimaryImageUrl ?? ""}
|
|
38
|
+
showAddToCart={false}
|
|
39
|
+
showFavorite={false}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
29
43
|
|
|
30
44
|
<FlexBetween flexWrap="wrap" mt={4}>
|
|
31
45
|
{pagination}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import SearchInputWithCategories from "../../../../../../../../../storefront/src/app/_components/search-box/search-input-category";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { expect, describe, it, vi, beforeAll } from "vitest";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { categories } from "@app/_components/navbar/utils/transform-nav.test";
|
|
6
|
+
import NavProvider from "@components/NavProvider";
|
|
7
|
+
import { CategoryHierarchy } from "@lib/category-helpers";
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
vi.mock("next/navigation", () => ({
|
|
11
|
+
usePathname: vi.fn().mockReturnValue("/"),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("@services/product", () => {
|
|
14
|
+
return {
|
|
15
|
+
searchProducts: vi.fn().mockReturnValue([
|
|
16
|
+
{
|
|
17
|
+
Name: "Shorts",
|
|
18
|
+
ItemCode: 1,
|
|
19
|
+
Category: { RecordId: 10, ParentCategory: 11 },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
Name: "Ladies Shorts",
|
|
23
|
+
ItemCode: 2,
|
|
24
|
+
Category: { RecordId: 2, ParentCategory: 1 },
|
|
25
|
+
},
|
|
26
|
+
]),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
vi.mock("next/headers", () => ({
|
|
30
|
+
cookies: vi.fn().mockReturnValue(new Map([["storefront", "token"]])),
|
|
31
|
+
}));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("SearchInputWithCategories", async () => {
|
|
35
|
+
it.skip("should render the search input and the category button", () => {
|
|
36
|
+
render(<SearchInputWithCategories />);
|
|
37
|
+
const input = screen.getByRole("textbox");
|
|
38
|
+
const categories = screen.getByText(/all categories/i);
|
|
39
|
+
|
|
40
|
+
expect(input).toBeInTheDocument();
|
|
41
|
+
expect(categories).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it.skip("Handles category change and changes the button text to selected.", async () => {
|
|
45
|
+
const user = userEvent.setup();
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<NavProvider
|
|
49
|
+
categories={categories as unknown as CategoryHierarchy[]}
|
|
50
|
+
cmsNav={[]}
|
|
51
|
+
>
|
|
52
|
+
<SearchInputWithCategories />
|
|
53
|
+
</NavProvider>,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const categoriesButton = screen.getByText(/all categories/i);
|
|
57
|
+
await user.click(categoriesButton);
|
|
58
|
+
const dropdownMenuItem = await screen.findByText("Women");
|
|
59
|
+
|
|
60
|
+
screen.debug(undefined, 10000);
|
|
61
|
+
await user.click(dropdownMenuItem);
|
|
62
|
+
const buttonWithNewText = screen.getByRole("button", {
|
|
63
|
+
name: /Women/i,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(buttonWithNewText).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.skip("should render search results after user types", async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
|
|
72
|
+
render(<SearchInputWithCategories />);
|
|
73
|
+
|
|
74
|
+
const input = screen.getByRole("textbox");
|
|
75
|
+
expect(screen.queryByText("Shorts")).not.toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
await user.click(input);
|
|
78
|
+
await user.keyboard("Shorts"); // search term doesn't matter here, mock will return data regardless
|
|
79
|
+
|
|
80
|
+
expect(await screen.findByText("Shorts")).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it.skip("should filter search results by category", async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
render(
|
|
86
|
+
<NavProvider
|
|
87
|
+
categories={categories as unknown as CategoryHierarchy[]}
|
|
88
|
+
cmsNav={[]}
|
|
89
|
+
>
|
|
90
|
+
<SearchInputWithCategories />
|
|
91
|
+
</NavProvider>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const categoriesButton = screen.getByText(/all categories/i);
|
|
95
|
+
const input = screen.getByRole("textbox");
|
|
96
|
+
|
|
97
|
+
await user.click(input);
|
|
98
|
+
await user.keyboard("Shorts");
|
|
99
|
+
expect(await screen.findByText("Shorts")).toBeInTheDocument();
|
|
100
|
+
|
|
101
|
+
await user.click(categoriesButton);
|
|
102
|
+
const dropdownMenuItem = await screen.findByText("Women");
|
|
103
|
+
await user.click(dropdownMenuItem);
|
|
104
|
+
await user.click(input);
|
|
105
|
+
await user.keyboard("Shorts");
|
|
106
|
+
|
|
107
|
+
waitFor(() => expect(screen.getByText("Shorts")).not.toBeInTheDocument());
|
|
108
|
+
expect(screen.queryByText("Ladies Shorts")).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { expect, describe, it, vi, beforeAll } from "vitest";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
5
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
6
|
+
import ENUS from "../../../../../../../../../storefront/messages/en-US.json";
|
|
7
|
+
import { act } from "react";
|
|
8
|
+
import searchResultsData from "./search-results.json";
|
|
9
|
+
import SearchInput from "../search-input";
|
|
10
|
+
import { NotificationProvider } from "@/components/Notifications/notification-context";
|
|
11
|
+
|
|
12
|
+
const queryClient = new QueryClient();
|
|
13
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
14
|
+
<NextIntlClientProvider locale="en-US" messages={ENUS}>
|
|
15
|
+
<NotificationProvider>
|
|
16
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
17
|
+
</NotificationProvider>
|
|
18
|
+
</NextIntlClientProvider>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const searchResults = searchResultsData;
|
|
22
|
+
const mockSearchProducts = vi.fn().mockResolvedValue(searchResults);
|
|
23
|
+
|
|
24
|
+
function sleep(ms: number) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
vi.mock("next/navigation", () => ({
|
|
30
|
+
usePathname: vi.fn().mockReturnValue("/"),
|
|
31
|
+
}));
|
|
32
|
+
// vi.mock("@/api-manager/services/product/mutations/search-products", () => ({
|
|
33
|
+
// default: () => ({
|
|
34
|
+
// mutateAsync: vi.fn().mockReturnValue([
|
|
35
|
+
// {
|
|
36
|
+
// Name: "Amber Sports Sunglasses",
|
|
37
|
+
// ItemId: "91007",
|
|
38
|
+
// Price: 250,
|
|
39
|
+
// },
|
|
40
|
+
// ]),
|
|
41
|
+
// }),
|
|
42
|
+
// }));
|
|
43
|
+
vi.mock("@/api-manager/services/product/mutations/search-products", () => ({
|
|
44
|
+
default: () => ({
|
|
45
|
+
mutateAsync: mockSearchProducts,
|
|
46
|
+
}),
|
|
47
|
+
}));
|
|
48
|
+
vi.mock("next/headers", () => ({
|
|
49
|
+
cookies: vi.fn().mockReturnValue(new Map([["storefront", "token"]])),
|
|
50
|
+
}));
|
|
51
|
+
window.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
52
|
+
matches: true, // to mock mobile screen
|
|
53
|
+
media: query,
|
|
54
|
+
onchange: null,
|
|
55
|
+
addListener: vi.fn(),
|
|
56
|
+
removeListener: vi.fn(),
|
|
57
|
+
addEventListener: vi.fn(),
|
|
58
|
+
removeEventListener: vi.fn(),
|
|
59
|
+
dispatchEvent: vi.fn(),
|
|
60
|
+
}));
|
|
61
|
+
// vi.mock("@mui/material/useMediaQuery", () => ({
|
|
62
|
+
// default: vi.fn().mockReturnValue(true), // to mock mobile screen
|
|
63
|
+
// }));
|
|
64
|
+
});
|
|
65
|
+
const handleCloseDrawer = vi.fn();
|
|
66
|
+
describe("SearchInput", () => {
|
|
67
|
+
it("should render the search input", () => {
|
|
68
|
+
render(
|
|
69
|
+
<Wrapper>
|
|
70
|
+
<SearchInput handleCloseDrawer={handleCloseDrawer} />
|
|
71
|
+
</Wrapper>,
|
|
72
|
+
);
|
|
73
|
+
const input = screen.getByRole("textbox");
|
|
74
|
+
expect(input).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should render search results with item names and prices after user types", async () => {
|
|
78
|
+
const user = userEvent.setup();
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<Wrapper>
|
|
82
|
+
<SearchInput handleCloseDrawer={handleCloseDrawer} />
|
|
83
|
+
</Wrapper>,
|
|
84
|
+
);
|
|
85
|
+
const input = screen.getAllByRole("textbox")[0];
|
|
86
|
+
expect(
|
|
87
|
+
screen.queryByText("Three Stone Diamond White Gold Ring"),
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
// user.click(input);
|
|
91
|
+
// user.keyboard("shorts"); // search term doesn't matter here, mock will return data regardless
|
|
92
|
+
|
|
93
|
+
await act(async () => {
|
|
94
|
+
await user.type(input, "ring");
|
|
95
|
+
});
|
|
96
|
+
expect(input).toHaveValue("ring");
|
|
97
|
+
await act(async () => await sleep(1000)); // TODO: replace with debounce
|
|
98
|
+
expect(mockSearchProducts).toHaveBeenCalledTimes(1);
|
|
99
|
+
// make sure its mobile version of search results
|
|
100
|
+
expect(
|
|
101
|
+
await screen.findByText("Three Stone Diamond White Gold Ring"),
|
|
102
|
+
).toBeInTheDocument();
|
|
103
|
+
expect(await screen.findByText("$410.00")).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should have the correct link", async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<Wrapper>
|
|
111
|
+
<SearchInput handleCloseDrawer={handleCloseDrawer} />
|
|
112
|
+
</Wrapper>,
|
|
113
|
+
);
|
|
114
|
+
const input = screen.getAllByRole("textbox")[0];
|
|
115
|
+
expect(
|
|
116
|
+
screen.queryByText("Three Stone Diamond White Gold Ring"),
|
|
117
|
+
).not.toBeInTheDocument();
|
|
118
|
+
|
|
119
|
+
// user.click(input);
|
|
120
|
+
// user.keyboard("shorts"); // search term doesn't matter here, mock will return data regardless
|
|
121
|
+
|
|
122
|
+
await act(async () => {
|
|
123
|
+
await user.type(input, "ring");
|
|
124
|
+
});
|
|
125
|
+
expect(input).toHaveValue("ring");
|
|
126
|
+
await act(async () => await sleep(1000)); // TODO: replace with debounce
|
|
127
|
+
// make sure its mobile version of search results
|
|
128
|
+
const link = screen.getByRole("link", {
|
|
129
|
+
name: "product Three Stone Diamond White Gold Ring $410.00",
|
|
130
|
+
});
|
|
131
|
+
expect(link).toBeInTheDocument();
|
|
132
|
+
expect(link).toHaveAttribute(
|
|
133
|
+
"href",
|
|
134
|
+
"/product/Three Stone Diamond White Gold Ring/68719492176",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"ItemId": "91078",
|
|
4
|
+
"Name": "Three Stone Diamond White Gold Ring",
|
|
5
|
+
"Price": 410,
|
|
6
|
+
"PrimaryImageUrl": "Products/91078_000_001.png",
|
|
7
|
+
"RecordId": 68719492176,
|
|
8
|
+
"ProductNumber": "91078",
|
|
9
|
+
"AverageRating": 4.071428571428571,
|
|
10
|
+
"TotalRatings": 14,
|
|
11
|
+
"Description": "Three diamonds on a white gold band.",
|
|
12
|
+
"BasePrice": 410,
|
|
13
|
+
"AttributeValues": [],
|
|
14
|
+
"IsMasterProduct": true,
|
|
15
|
+
"MasterProductId": 68719492176,
|
|
16
|
+
"DefaultUnitOfMeasure": "ea",
|
|
17
|
+
"ExtensionProperties": []
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"ItemId": "91079",
|
|
21
|
+
"Name": "Three Stone Diamond Trellis Ring",
|
|
22
|
+
"Price": 440,
|
|
23
|
+
"PrimaryImageUrl": "Products/91079_000_001.png",
|
|
24
|
+
"RecordId": 68719492185,
|
|
25
|
+
"ProductNumber": "91079",
|
|
26
|
+
"TotalRatings": 0,
|
|
27
|
+
"Description": "A trellis band complete with three stone diamonds.",
|
|
28
|
+
"BasePrice": 440,
|
|
29
|
+
"AttributeValues": [],
|
|
30
|
+
"IsMasterProduct": true,
|
|
31
|
+
"MasterProductId": 68719492185,
|
|
32
|
+
"DefaultUnitOfMeasure": "ea",
|
|
33
|
+
"ExtensionProperties": []
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"ItemId": "91077",
|
|
37
|
+
"Name": "Four Stone Diamond White Gold Ring",
|
|
38
|
+
"Price": 360,
|
|
39
|
+
"PrimaryImageUrl": "Products/91077_000_001.png",
|
|
40
|
+
"RecordId": 68719492166,
|
|
41
|
+
"ProductNumber": "91077",
|
|
42
|
+
"TotalRatings": 0,
|
|
43
|
+
"Description": "A white gold band complete with four stone diamonds.",
|
|
44
|
+
"BasePrice": 360,
|
|
45
|
+
"AttributeValues": [],
|
|
46
|
+
"IsMasterProduct": true,
|
|
47
|
+
"MasterProductId": 68719492166,
|
|
48
|
+
"DefaultUnitOfMeasure": "ea",
|
|
49
|
+
"ExtensionProperties": []
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"ItemId": "93040",
|
|
53
|
+
"Name": "Ombre Fringe Skirt",
|
|
54
|
+
"Price": 95,
|
|
55
|
+
"PrimaryImageUrl": "Products/93040_000_001.png",
|
|
56
|
+
"RecordId": 68719494571,
|
|
57
|
+
"ProductNumber": "93040",
|
|
58
|
+
"AverageRating": 4.235294117647059,
|
|
59
|
+
"TotalRatings": 17,
|
|
60
|
+
"Description": "This ombre red-to-white fringe skirt is both elegant and fun.",
|
|
61
|
+
"BasePrice": 95,
|
|
62
|
+
"AttributeValues": [],
|
|
63
|
+
"IsMasterProduct": true,
|
|
64
|
+
"MasterProductId": 68719494571,
|
|
65
|
+
"DefaultUnitOfMeasure": "ea",
|
|
66
|
+
"ExtensionProperties": []
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"ItemId": "91085",
|
|
70
|
+
"Name": "Gold Wrapped Pearl Earrings",
|
|
71
|
+
"Price": 95,
|
|
72
|
+
"PrimaryImageUrl": "Products/91085_000_001.png",
|
|
73
|
+
"RecordId": 68719492200,
|
|
74
|
+
"ProductNumber": "91085",
|
|
75
|
+
"AverageRating": 3.9166666666666665,
|
|
76
|
+
"TotalRatings": 12,
|
|
77
|
+
"Description": "Pearls wrapped in gold held by golden clasps.",
|
|
78
|
+
"BasePrice": 95,
|
|
79
|
+
"AttributeValues": [],
|
|
80
|
+
"IsMasterProduct": false,
|
|
81
|
+
"MasterProductId": 0,
|
|
82
|
+
"DefaultUnitOfMeasure": "ea",
|
|
83
|
+
"ExtensionProperties": []
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"ItemId": "91081",
|
|
87
|
+
"Name": "3\" Feather Earrings",
|
|
88
|
+
"Price": 55,
|
|
89
|
+
"PrimaryImageUrl": "Products/91081_000_001.png",
|
|
90
|
+
"RecordId": 68719492196,
|
|
91
|
+
"ProductNumber": "91081",
|
|
92
|
+
"TotalRatings": 0,
|
|
93
|
+
"Description": "Black 3\" feathers held by gold clips.",
|
|
94
|
+
"BasePrice": 55,
|
|
95
|
+
"AttributeValues": [],
|
|
96
|
+
"IsMasterProduct": false,
|
|
97
|
+
"MasterProductId": 0,
|
|
98
|
+
"DefaultUnitOfMeasure": "ea",
|
|
99
|
+
"ExtensionProperties": []
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"ItemId": "91080",
|
|
103
|
+
"Name": "4\" Feather Earrings",
|
|
104
|
+
"Price": 60,
|
|
105
|
+
"PrimaryImageUrl": "Products/91080_000_001.png",
|
|
106
|
+
"RecordId": 68719492195,
|
|
107
|
+
"ProductNumber": "91080",
|
|
108
|
+
"TotalRatings": 0,
|
|
109
|
+
"Description": "Dark grey 4\" feathers at the end of gold clips.",
|
|
110
|
+
"BasePrice": 60,
|
|
111
|
+
"AttributeValues": [],
|
|
112
|
+
"IsMasterProduct": false,
|
|
113
|
+
"MasterProductId": 0,
|
|
114
|
+
"DefaultUnitOfMeasure": "ea",
|
|
115
|
+
"ExtensionProperties": []
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"ItemId": "91045",
|
|
119
|
+
"Name": "Herringbone Fedora",
|
|
120
|
+
"Price": 180,
|
|
121
|
+
"PrimaryImageUrl": "Products/91045_000_001.png",
|
|
122
|
+
"RecordId": 68719495883,
|
|
123
|
+
"ProductNumber": "91045",
|
|
124
|
+
"TotalRatings": 0,
|
|
125
|
+
"Description": "A grey band circling a fedora complete with a herringbone pattern. \n",
|
|
126
|
+
"BasePrice": 180,
|
|
127
|
+
"AttributeValues": [],
|
|
128
|
+
"IsMasterProduct": true,
|
|
129
|
+
"MasterProductId": 68719495883,
|
|
130
|
+
"DefaultUnitOfMeasure": "ea",
|
|
131
|
+
"ExtensionProperties": []
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"ItemId": "91086",
|
|
135
|
+
"Name": "Embedded Pearl Earrings",
|
|
136
|
+
"Price": 85,
|
|
137
|
+
"PrimaryImageUrl": "Products/91086_000_001.png",
|
|
138
|
+
"RecordId": 68719492201,
|
|
139
|
+
"ProductNumber": "91086",
|
|
140
|
+
"TotalRatings": 0,
|
|
141
|
+
"Description": "Pearls embedded in sterling silver earrings.",
|
|
142
|
+
"BasePrice": 85,
|
|
143
|
+
"AttributeValues": [],
|
|
144
|
+
"IsMasterProduct": false,
|
|
145
|
+
"MasterProductId": 0,
|
|
146
|
+
"DefaultUnitOfMeasure": "ea",
|
|
147
|
+
"ExtensionProperties": []
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"ItemId": "91084",
|
|
151
|
+
"Name": "Golden Flower Earrings",
|
|
152
|
+
"Price": 120,
|
|
153
|
+
"PrimaryImageUrl": "Products/91084_000_001.png",
|
|
154
|
+
"RecordId": 68719492199,
|
|
155
|
+
"ProductNumber": "91084",
|
|
156
|
+
"TotalRatings": 0,
|
|
157
|
+
"Description": "Golden 1/4\" flowers held by golden clasps.",
|
|
158
|
+
"BasePrice": 120,
|
|
159
|
+
"AttributeValues": [],
|
|
160
|
+
"IsMasterProduct": false,
|
|
161
|
+
"MasterProductId": 0,
|
|
162
|
+
"DefaultUnitOfMeasure": "ea",
|
|
163
|
+
"ExtensionProperties": []
|
|
164
|
+
}
|
|
165
|
+
]
|
|
@@ -4,26 +4,80 @@ import MenuItem from "@mui/material/MenuItem";
|
|
|
4
4
|
import { SearchResultCard } from "../styles/index";
|
|
5
5
|
import { ProductSearchResult } from "@msdyn365-commerce/retail-proxy";
|
|
6
6
|
import { getProductDetailUrl } from "@/lib/build-product-detail-url";
|
|
7
|
+
import { Box, Grid, useMediaQuery, useTheme } from "@mui/material";
|
|
8
|
+
import LazyImage from "@/_components/LazyImage";
|
|
9
|
+
import { H4, Span } from "@/_components/Typography";
|
|
10
|
+
import { currency } from "@/lib/lib";
|
|
11
|
+
import ProductCard8 from "@/_components/product-cards/product-card-8/product-card";
|
|
12
|
+
import { DialogType } from "../search-input-category";
|
|
7
13
|
|
|
8
14
|
interface Props {
|
|
9
15
|
results: ProductSearchResult[];
|
|
10
16
|
handleCloseDrawer?: () => void; // Close search drawer in mobile screen
|
|
17
|
+
openDialog: (type: DialogType, productId: number) => void;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
export default function SearchResult({
|
|
20
|
+
export default function SearchResult({
|
|
21
|
+
results,
|
|
22
|
+
handleCloseDrawer,
|
|
23
|
+
openDialog,
|
|
24
|
+
}: Props) {
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const downMd = useMediaQuery(theme.breakpoints.down(1150));
|
|
14
27
|
return (
|
|
15
28
|
<SearchResultCard>
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
{downMd ? (
|
|
30
|
+
<>
|
|
31
|
+
{results.map((item, i) => (
|
|
32
|
+
<MenuItem key={item?.Name || "" + i} onClick={handleCloseDrawer}>
|
|
33
|
+
<NavLink href={getProductDetailUrl(item)} key={item.RecordId}>
|
|
34
|
+
<Box display="flex" alignItems="center">
|
|
35
|
+
<Box position="relative" height={130} width={130}>
|
|
36
|
+
<LazyImage
|
|
37
|
+
height={300}
|
|
38
|
+
width={300}
|
|
39
|
+
alt="product"
|
|
40
|
+
className="product-img"
|
|
41
|
+
src={item.PrimaryImageUrl}
|
|
42
|
+
/>
|
|
43
|
+
</Box>
|
|
44
|
+
<Box flex="1" ml={2} whiteSpace="normal">
|
|
45
|
+
<H4>{item.Name}</H4>
|
|
46
|
+
<Span>{currency(item.Price)}</Span>
|
|
47
|
+
</Box>
|
|
48
|
+
</Box>
|
|
49
|
+
</NavLink>
|
|
50
|
+
</MenuItem>
|
|
51
|
+
))}
|
|
52
|
+
</>
|
|
53
|
+
) : (
|
|
54
|
+
<Grid container>
|
|
55
|
+
{results.map((item) => (
|
|
56
|
+
<Grid
|
|
57
|
+
key={item.ItemId}
|
|
58
|
+
item
|
|
59
|
+
xs={12}
|
|
60
|
+
sm={6}
|
|
61
|
+
md={4}
|
|
62
|
+
onClick={handleCloseDrawer}
|
|
63
|
+
>
|
|
64
|
+
<ProductCard8
|
|
65
|
+
slug={item.ItemId ?? ""}
|
|
66
|
+
price={item.Price}
|
|
67
|
+
title={item.Name ?? ""}
|
|
68
|
+
thumbnail={item.PrimaryImageUrl}
|
|
69
|
+
id={item.RecordId ?? ""}
|
|
70
|
+
rating={item.AverageRating ?? 0}
|
|
71
|
+
reviews={item.TotalRatings ?? 0}
|
|
72
|
+
categories={[]} // TODO: Category info not available in item
|
|
73
|
+
openDialog={openDialog}
|
|
74
|
+
enableWishlistDialog={false}
|
|
75
|
+
enableQuickViewDialog={false} // Enable quick view dialog
|
|
76
|
+
/>
|
|
77
|
+
</Grid>
|
|
78
|
+
))}
|
|
79
|
+
</Grid>
|
|
80
|
+
)}
|
|
27
81
|
</SearchResultCard>
|
|
28
82
|
);
|
|
29
83
|
}
|