@evenicanpm/storefront-core 2.1.0 → 2.3.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 +9 -4
- package/src/api-manager/README.md +52 -27
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +166 -17
- package/src/api-manager/datasources/d365/d365-invoice.datasource.ts +100 -0
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +75 -18
- package/src/api-manager/datasources/d365/d365-product.datasource.ts +19 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +96 -22
- package/src/api-manager/datasources/d365/d365.datasource.ts +3 -0
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +14 -5
- package/src/api-manager/datasources/e4/e4.datasource.ts +9 -0
- package/src/api-manager/datasources/e4/middleware.ts +35 -0
- package/src/api-manager/datasources/e4/order/e4-order.datasource.ts +7 -4
- package/src/api-manager/datasources/e4/user/e4-user.datasource.ts +4 -0
- package/src/api-manager/index.ts +54 -3
- package/src/api-manager/lib/get-graphql-client.ts +13 -6
- package/src/api-manager/schemas/cart.schema.ts +10 -1
- package/src/api-manager/schemas/invoice.schema.ts +55 -0
- package/src/api-manager/schemas/order.schema.ts +13 -1
- package/src/api-manager/schemas/user.schema.ts +8 -0
- package/src/api-manager/services/create-query.ts +5 -56
- package/src/api-manager/services/invoice/queries/get-invoice-details.ts +18 -0
- package/src/api-manager/services/invoice/queries/get-invoices.ts +18 -0
- package/src/api-manager/services/order/queries/get-order-details.ts +1 -1
- package/src/api-manager/services/order/queries/get-orders.ts +6 -3
- package/src/api-manager/services/user/queries/get-customer-balance.ts +20 -0
- package/src/api-manager/types/Datasource.ts +21 -2
- package/src/auth/better-auth.ts +89 -0
- package/src/auth/signout.ts +5 -34
- package/src/cms/blocks/block-manager.tsx +18 -3
- package/src/cms/blocks/components/cta-banner.tsx +65 -0
- package/src/cms/blocks/components/hero-image.tsx +83 -0
- package/src/cms/blocks/index.tsx +2 -0
- package/src/cms/blocks/interfaces.ts +20 -0
- package/src/cms/draft-mode-badge.tsx +26 -0
- package/src/cms/queries.ts +81 -0
- package/src/components/flex-box/flex-box.tsx +2 -0
- package/src/components/header/__tests__/header.test.tsx +9 -2
- package/src/components/header/__tests__/user.test.tsx +34 -19
- package/src/components/header/components/user.tsx +70 -29
- package/src/components/mini-cart/mini-cart.tsx +3 -3
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/add-to-wishlist-dialog.tsx +3 -1
- package/src/components/wishlist-dialogs/add-to-wishlist/compound/wishlist-dialog-header.tsx +4 -1
- package/src/hooks/use-nextauth-session.ts +10 -14
- package/src/pages/account/account-navigation.tsx +88 -60
- package/src/pages/account/account-routes.ts +52 -0
- package/src/pages/account/orders/order-history-filter-types.tsx +26 -0
- package/src/pages/account/orders/order-history-filters-panel.tsx +375 -0
- package/src/pages/account/orders/order-history-header.tsx +54 -0
- package/src/pages/account/orders/order-history-pagination.tsx +55 -0
- package/src/pages/account/orders/order-history-root.tsx +125 -0
- package/src/pages/account/orders/order-history-row.tsx +110 -0
- package/src/pages/account/orders/order-history-search-and-filter.tsx +449 -0
- package/src/pages/account/orders/order-history-search-bar.tsx +84 -0
- package/src/pages/account/orders/order-history-search-dropdown.tsx +53 -0
- package/src/pages/account/orders/order-history-sort.tsx +90 -0
- package/src/pages/account/orders/order-history-table.tsx +54 -0
- package/src/pages/account/orders/order-row.tsx +2 -2
- package/src/pages/account/orders/orders-drop-down-handler.tsx +19 -0
- package/src/pages/account/table-header.tsx +20 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +1 -1
- package/src/pages/blog/blog-card.tsx +9 -3
- package/src/pages/blog/blog-detail-view.tsx +150 -0
- package/src/pages/blog/blog-list-view.tsx +59 -0
- package/src/pages/cart/__tests__/checkout-form.test.tsx +4 -2
- package/src/pages/cart/estimate-shipping.tsx +6 -5
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +43 -21
- package/src/pages/checkout/checkout-alt-form/steps/payment/payment-details.tsx +144 -19
- package/src/pages/cms-page-view.tsx +53 -0
- package/src/pages/quickorder/order-upload.tsx +12 -6
- package/src/pages/quickorder/quick-order.tsx +25 -20
- package/src/api-manager/datasources/d365/utils/decode-jwt.ts +0 -10
- package/src/auth/auth-options.test.ts +0 -70
- package/src/auth/auth-options.ts +0 -101
- package/src/auth/next-auth-cookie-manager.ts +0 -87
- package/src/auth/providers/aadb2c-provider.ts +0 -32
- package/src/auth/providers/authentik-provider.ts +0 -24
- package/src/auth/providers/entra-external-id-provider.ts +0 -50
- package/src/auth/providers/index.ts +0 -16
- package/src/auth/providers/keycloak-provider.ts +0 -22
- package/src/auth/refresh-token.ts +0 -132
- package/src/auth/types/next-auth.d.ts +0 -38
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GetInvoicesDetailsInput } from "@evenicanpm/storefront-core/src/api-manager/schemas/invoice.schema";
|
|
2
|
+
import {
|
|
3
|
+
createApiPath,
|
|
4
|
+
createQuery,
|
|
5
|
+
} from "@evenicanpm/storefront-core/src/api-manager/services/create-query";
|
|
6
|
+
import type { SalesInvoice } from "@msdyn365-commerce/retail-proxy/dist";
|
|
7
|
+
|
|
8
|
+
const apiPath = createApiPath("invoice", "getInvoiceDetails");
|
|
9
|
+
const config = {
|
|
10
|
+
queryKey: "invoices",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getInvoices = createQuery<GetInvoicesDetailsInput, SalesInvoice>(
|
|
14
|
+
apiPath,
|
|
15
|
+
config,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export default getInvoices;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApiPath,
|
|
3
|
+
createQuery,
|
|
4
|
+
} from "@evenicanpm/storefront-core/src/api-manager/services/create-query";
|
|
5
|
+
import type { OrderInvoice } from "@msdyn365-commerce/retail-proxy/dist";
|
|
6
|
+
import type { GetInvoicesInput } from "../../../schemas/invoice.schema";
|
|
7
|
+
|
|
8
|
+
const apiPath = createApiPath("invoice", "getInvoices");
|
|
9
|
+
const config = {
|
|
10
|
+
queryKey: "invoices",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getInvoices = createQuery<GetInvoicesInput, OrderInvoice[]>(
|
|
14
|
+
apiPath,
|
|
15
|
+
config,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export default getInvoices;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
GetSalesOrdersInput,
|
|
2
3
|
SalesOrder,
|
|
3
|
-
SalesOrderInput,
|
|
4
4
|
} from "@evenicanpm/storefront-core/src/api-manager/schemas/order.schema";
|
|
5
5
|
import {
|
|
6
6
|
createApiPath,
|
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
|
|
10
10
|
const apiPath = createApiPath("order", "getSalesOrders");
|
|
11
11
|
const config = {
|
|
12
|
-
queryKey: "
|
|
12
|
+
queryKey: "SalesOrders",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
const getOrders = createQuery<
|
|
15
|
+
const getOrders = createQuery<GetSalesOrdersInput, SalesOrder[]>(
|
|
16
|
+
apiPath,
|
|
17
|
+
config,
|
|
18
|
+
);
|
|
16
19
|
|
|
17
20
|
export default getOrders;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CustomerBalances,
|
|
3
|
+
GetCustomerBalanceInput,
|
|
4
|
+
} from "@evenicanpm/storefront-core/src/api-manager/schemas/user.schema";
|
|
5
|
+
import {
|
|
6
|
+
createApiPath,
|
|
7
|
+
createQuery,
|
|
8
|
+
} from "@evenicanpm/storefront-core/src/api-manager/services/create-query";
|
|
9
|
+
|
|
10
|
+
const apiPath = createApiPath("user", "getCustomerBalance");
|
|
11
|
+
|
|
12
|
+
const getCustomerBalance = createQuery<
|
|
13
|
+
GetCustomerBalanceInput,
|
|
14
|
+
CustomerBalances
|
|
15
|
+
>(apiPath, {
|
|
16
|
+
queryKey: "customerBalance",
|
|
17
|
+
disableProperties: ["accountNumber"],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default getCustomerBalance;
|
|
@@ -7,10 +7,16 @@ import type {
|
|
|
7
7
|
GetStatesInput,
|
|
8
8
|
} from "@evenicanpm/storefront-core/src/api-manager/schemas/address.schema";
|
|
9
9
|
import type * as cartSchemas from "@evenicanpm/storefront-core/src/api-manager/schemas/cart.schema";
|
|
10
|
+
import type {
|
|
11
|
+
GetInvoicesDetailsInput,
|
|
12
|
+
GetInvoicesInput,
|
|
13
|
+
Invoice,
|
|
14
|
+
InvoiceDetails,
|
|
15
|
+
} from "@evenicanpm/storefront-core/src/api-manager/schemas/invoice.schema";
|
|
10
16
|
import type {
|
|
11
17
|
GetSalesOrderDetailsInput,
|
|
18
|
+
GetSalesOrdersInput,
|
|
12
19
|
SalesOrder,
|
|
13
|
-
SalesOrderInput,
|
|
14
20
|
} from "@evenicanpm/storefront-core/src/api-manager/schemas/order.schema";
|
|
15
21
|
import type {
|
|
16
22
|
ChannelConfiguration,
|
|
@@ -47,6 +53,7 @@ export interface DatasourceApis {
|
|
|
47
53
|
order: OrderApi;
|
|
48
54
|
address: AddressApi;
|
|
49
55
|
organization: OrganizationApi;
|
|
56
|
+
invoice: InvoiceApi;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
export abstract class Datasource {
|
|
@@ -59,6 +66,7 @@ export abstract class Datasource {
|
|
|
59
66
|
abstract address: Partial<AddressApi>;
|
|
60
67
|
abstract organization: Partial<OrganizationApi>;
|
|
61
68
|
abstract session: Partial<SessionApi>;
|
|
69
|
+
abstract invoice: Partial<InvoiceApi>;
|
|
62
70
|
|
|
63
71
|
constructor(name: string) {
|
|
64
72
|
this.name = name;
|
|
@@ -89,6 +97,9 @@ export interface UserApi {
|
|
|
89
97
|
updateUser(user: userSchemas.User): Promise<userSchemas.User>;
|
|
90
98
|
getUser(): Promise<userSchemas.User | null>;
|
|
91
99
|
getUserCounts(userId: string): Promise<userSchemas.UserCounts>;
|
|
100
|
+
getCustomerBalance(
|
|
101
|
+
input: userSchemas.GetCustomerBalanceInput,
|
|
102
|
+
): Promise<userSchemas.CustomerBalances>;
|
|
92
103
|
// wishlists
|
|
93
104
|
getWishlists(): Promise<userSchemas.Wishlist[]>;
|
|
94
105
|
getWishlistDetails(
|
|
@@ -199,7 +210,7 @@ export interface CategoryApi {
|
|
|
199
210
|
* Order Data Actions
|
|
200
211
|
*/
|
|
201
212
|
export interface OrderApi {
|
|
202
|
-
getSalesOrders(input:
|
|
213
|
+
getSalesOrders(input: GetSalesOrdersInput): Promise<SalesOrder[]>;
|
|
203
214
|
getSalesOrderDetails(input: GetSalesOrderDetailsInput): Promise<SalesOrder>;
|
|
204
215
|
}
|
|
205
216
|
|
|
@@ -214,3 +225,11 @@ export interface AddressApi {
|
|
|
214
225
|
updateAddress(address: Address): Promise<Address>;
|
|
215
226
|
deleteAddress(addressId: string): Promise<string>;
|
|
216
227
|
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Invoice Data Actions
|
|
231
|
+
*/
|
|
232
|
+
export interface InvoiceApi {
|
|
233
|
+
getInvoices(input: GetInvoicesInput): Promise<Invoice[]>;
|
|
234
|
+
getInvoiceDetails(input: GetInvoicesDetailsInput): Promise<InvoiceDetails>;
|
|
235
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { nextCookies } from "better-auth/next-js";
|
|
3
|
+
import { customSession, genericOAuth } from "better-auth/plugins";
|
|
4
|
+
|
|
5
|
+
const tenantName = process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_TENANT_NAME || "";
|
|
6
|
+
const loginAuthority = `https://${tenantName}.ciamlogin.com/${tenantName}.onmicrosoft.com`;
|
|
7
|
+
|
|
8
|
+
export const auth = betterAuth({
|
|
9
|
+
secret: process.env.BETTER_AUTH_SECRET,
|
|
10
|
+
baseURL: process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL,
|
|
11
|
+
session: {
|
|
12
|
+
cookieCache: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
maxAge: 60 * 60, // 1 hour
|
|
15
|
+
strategy: "jwe",
|
|
16
|
+
refreshCache: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
account: {
|
|
20
|
+
storeStateStrategy: "cookie",
|
|
21
|
+
storeAccountCookie: true,
|
|
22
|
+
},
|
|
23
|
+
plugins: [
|
|
24
|
+
genericOAuth({
|
|
25
|
+
config: [
|
|
26
|
+
{
|
|
27
|
+
providerId: "entra-external-id",
|
|
28
|
+
clientId: process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_ID || "",
|
|
29
|
+
clientSecret:
|
|
30
|
+
process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CLIENT_SECRET || "",
|
|
31
|
+
discoveryUrl: `${loginAuthority}/v2.0/.well-known/openid-configuration`,
|
|
32
|
+
scopes: (
|
|
33
|
+
process.env.NEXT_PUBLIC_ENTRA_EXTERNAL_ID_CUSTOM_SCOPES ||
|
|
34
|
+
"offline_access openid email profile"
|
|
35
|
+
).split(" "),
|
|
36
|
+
pkce: true,
|
|
37
|
+
prompt: "login",
|
|
38
|
+
getUserInfo: async (tokens) => {
|
|
39
|
+
// Decode the id_token to get user claims
|
|
40
|
+
const idToken = tokens.idToken;
|
|
41
|
+
if (idToken) {
|
|
42
|
+
const parts = idToken.split(".");
|
|
43
|
+
if (parts.length === 3) {
|
|
44
|
+
const payload = JSON.parse(
|
|
45
|
+
Buffer.from(parts[1], "base64url").toString(),
|
|
46
|
+
);
|
|
47
|
+
return {
|
|
48
|
+
id: payload.oid || payload.sub,
|
|
49
|
+
name: payload.name,
|
|
50
|
+
email:
|
|
51
|
+
payload.email ||
|
|
52
|
+
payload.preferred_username ||
|
|
53
|
+
payload.emails?.[0],
|
|
54
|
+
emailVerified: !!payload.email_verified,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
customSession(async ({ user, session }, ctx) => {
|
|
64
|
+
let accessToken = "";
|
|
65
|
+
try {
|
|
66
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
67
|
+
user.id,
|
|
68
|
+
);
|
|
69
|
+
const account = accounts?.find(
|
|
70
|
+
(a) => a.providerId === "entra-external-id",
|
|
71
|
+
);
|
|
72
|
+
// For Entra External ID, use id_token as the access token
|
|
73
|
+
accessToken = account?.idToken || account?.accessToken || "";
|
|
74
|
+
} catch {
|
|
75
|
+
// Account data may not be accessible in all modes
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
user,
|
|
79
|
+
session: {
|
|
80
|
+
...session,
|
|
81
|
+
accessToken,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
nextCookies(), // must be last plugin
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type Session = typeof auth.$Infer.Session;
|
package/src/auth/signout.ts
CHANGED
|
@@ -1,42 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
if (!response.ok) {
|
|
5
|
-
throw new Error(`Failed to fetch CSRF token: ${response.statusText}`);
|
|
6
|
-
}
|
|
7
|
-
const data: { csrfToken: string } = await response.json();
|
|
8
|
-
return data.csrfToken;
|
|
9
|
-
} catch (error) {
|
|
10
|
-
console.error("Error fetching CSRF token:", error);
|
|
11
|
-
throw error;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
|
|
3
|
+
const authClient = createAuthClient();
|
|
14
4
|
|
|
15
5
|
/**
|
|
16
|
-
*
|
|
17
|
-
* Solution to known next-auth issue - https://github.com/nextauthjs/next-auth/issues/4612
|
|
6
|
+
* Signs the user out and redirects to the home page.
|
|
18
7
|
*/
|
|
19
8
|
const signOut = async (): Promise<void> => {
|
|
20
9
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const formData = new URLSearchParams({
|
|
24
|
-
csrfToken,
|
|
25
|
-
json: "true",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const response = await fetch("/api/auth/signout", {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: {
|
|
31
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
32
|
-
},
|
|
33
|
-
body: formData,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
if (!response.ok) {
|
|
37
|
-
throw new Error(`Failed to sign out: ${response.statusText}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
10
|
+
await authClient.signOut();
|
|
40
11
|
globalThis.location.href = "/";
|
|
41
12
|
} catch (error) {
|
|
42
13
|
console.error(error);
|
|
@@ -3,12 +3,14 @@ import type { Block } from "@/cms/blocks/interfaces";
|
|
|
3
3
|
import {
|
|
4
4
|
BrandLogosScroller,
|
|
5
5
|
CategoryCarousel,
|
|
6
|
+
CtaBanner,
|
|
6
7
|
FeaturedCategories,
|
|
7
8
|
FeaturedProducts,
|
|
8
9
|
FeaturePill,
|
|
9
10
|
Footer,
|
|
10
11
|
Hero,
|
|
11
12
|
HeroCarousel,
|
|
13
|
+
HeroImage,
|
|
12
14
|
Image,
|
|
13
15
|
ProductCarousel,
|
|
14
16
|
ProductSectionFullWidth,
|
|
@@ -18,6 +20,8 @@ import {
|
|
|
18
20
|
|
|
19
21
|
const componentMap = {
|
|
20
22
|
"blocks.hero-section": Hero,
|
|
23
|
+
"blocks.hero-image": HeroImage,
|
|
24
|
+
"blocks.cta-banner": CtaBanner,
|
|
21
25
|
"blocks.features-pill": FeaturePill, // Should replace this with bazaar-react-ts/src/pages-sections/market-2/section-2/section-2.tsx
|
|
22
26
|
"blocks.featured-products": FeaturedProducts,
|
|
23
27
|
"blocks.featured-categories": FeaturedCategories,
|
|
@@ -40,8 +44,16 @@ const getBlockComponent: <B extends Block>(
|
|
|
40
44
|
) => JSX.Element | null = (block: Block) => {
|
|
41
45
|
const Component = componentMap[
|
|
42
46
|
block.__component as keyof typeof componentMap
|
|
43
|
-
] as React.ComponentType<Block
|
|
44
|
-
|
|
47
|
+
] as React.ComponentType<Block> | undefined;
|
|
48
|
+
if (!Component) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`BlockManager: no component mapped for "${block.__component}"`,
|
|
51
|
+
);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<Component key={`block-${block.__component}-${block.id}`} {...block} />
|
|
56
|
+
);
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
interface BlockManagerProps {
|
|
@@ -55,7 +67,10 @@ const BlockManager: React.FC<BlockManagerProps> = ({ blocks }) => {
|
|
|
55
67
|
{blocks.map((block, idx) => {
|
|
56
68
|
const isLast = idx === blocks.length - 1;
|
|
57
69
|
return (
|
|
58
|
-
<Box
|
|
70
|
+
<Box
|
|
71
|
+
key={`block-wrapper-${block.__component}-${block.id}`}
|
|
72
|
+
mb={isLast ? 0 : 6}
|
|
73
|
+
>
|
|
59
74
|
{getBlockComponent(block, idx)}
|
|
60
75
|
</Box>
|
|
61
76
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Box, Button, Container, Typography } from "@mui/material";
|
|
2
|
+
import NextLink from "next/link";
|
|
3
|
+
import type { CtaBannerBlock } from "@/cms/blocks/interfaces";
|
|
4
|
+
|
|
5
|
+
const CtaBanner = ({
|
|
6
|
+
heading,
|
|
7
|
+
subtext,
|
|
8
|
+
buttonText,
|
|
9
|
+
buttonLink,
|
|
10
|
+
backgroundColor,
|
|
11
|
+
}: CtaBannerBlock) => {
|
|
12
|
+
const bgColor = backgroundColor || "#1a237e";
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box
|
|
16
|
+
sx={{
|
|
17
|
+
backgroundColor: bgColor,
|
|
18
|
+
color: "#fff",
|
|
19
|
+
py: { xs: 6, md: 8 },
|
|
20
|
+
px: 2,
|
|
21
|
+
textAlign: "center",
|
|
22
|
+
width: "100%",
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<Container maxWidth="md">
|
|
26
|
+
<Typography
|
|
27
|
+
variant="h3"
|
|
28
|
+
component="h2"
|
|
29
|
+
sx={{ fontWeight: 700, mb: subtext ? 2 : 3, color: "#fff" }}
|
|
30
|
+
>
|
|
31
|
+
{heading}
|
|
32
|
+
</Typography>
|
|
33
|
+
{subtext && (
|
|
34
|
+
<Typography
|
|
35
|
+
variant="h6"
|
|
36
|
+
sx={{ color: "rgba(255,255,255,0.85)", mb: 3, fontWeight: 400 }}
|
|
37
|
+
>
|
|
38
|
+
{subtext}
|
|
39
|
+
</Typography>
|
|
40
|
+
)}
|
|
41
|
+
{buttonText && buttonLink && (
|
|
42
|
+
<Button
|
|
43
|
+
component={NextLink}
|
|
44
|
+
href={buttonLink}
|
|
45
|
+
variant="contained"
|
|
46
|
+
size="large"
|
|
47
|
+
sx={{
|
|
48
|
+
backgroundColor: "#fff",
|
|
49
|
+
color: bgColor,
|
|
50
|
+
fontWeight: 700,
|
|
51
|
+
px: 4,
|
|
52
|
+
"&:hover": {
|
|
53
|
+
backgroundColor: "rgba(255,255,255,0.9)",
|
|
54
|
+
},
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{buttonText}
|
|
58
|
+
</Button>
|
|
59
|
+
)}
|
|
60
|
+
</Container>
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default CtaBanner;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Box, Button, Container, Typography } from "@mui/material";
|
|
2
|
+
import NextImage from "next/image";
|
|
3
|
+
import NextLink from "next/link";
|
|
4
|
+
import type { HeroImageBlock } from "@/cms/blocks/interfaces";
|
|
5
|
+
|
|
6
|
+
const sxBanner = {
|
|
7
|
+
position: "relative",
|
|
8
|
+
backgroundColor: "grey.900",
|
|
9
|
+
color: "white",
|
|
10
|
+
height: 500,
|
|
11
|
+
width: "100%",
|
|
12
|
+
display: "flex",
|
|
13
|
+
alignItems: "center",
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
overflow: "hidden",
|
|
16
|
+
marginBottom: "50px",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const contentContainer = {
|
|
20
|
+
position: "absolute",
|
|
21
|
+
top: 0,
|
|
22
|
+
left: 0,
|
|
23
|
+
right: 0,
|
|
24
|
+
bottom: 0,
|
|
25
|
+
display: "flex",
|
|
26
|
+
flexDirection: "column",
|
|
27
|
+
justifyContent: "center",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
zIndex: 2,
|
|
30
|
+
textAlign: "center",
|
|
31
|
+
padding: "0 20px",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const overlayStyle = {
|
|
35
|
+
position: "absolute",
|
|
36
|
+
inset: 0,
|
|
37
|
+
backgroundColor: "rgba(10,25,60,0.55)",
|
|
38
|
+
zIndex: 1,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const HeroImage = ({
|
|
42
|
+
heading,
|
|
43
|
+
subheading,
|
|
44
|
+
buttonText,
|
|
45
|
+
buttonLink,
|
|
46
|
+
image,
|
|
47
|
+
}: HeroImageBlock) => {
|
|
48
|
+
const imageUrl = image?.url;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box sx={sxBanner}>
|
|
52
|
+
{imageUrl && (
|
|
53
|
+
<NextImage
|
|
54
|
+
src={imageUrl}
|
|
55
|
+
alt={heading}
|
|
56
|
+
fill
|
|
57
|
+
style={{ objectFit: "cover", objectPosition: "center" }}
|
|
58
|
+
priority
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
<Box sx={overlayStyle} />
|
|
62
|
+
<Container sx={contentContainer}>
|
|
63
|
+
<Typography variant="h1" gutterBottom>
|
|
64
|
+
{heading}
|
|
65
|
+
</Typography>
|
|
66
|
+
{subheading && (
|
|
67
|
+
<Typography variant="h5" gutterBottom sx={{ marginBottom: "30px" }}>
|
|
68
|
+
{subheading}
|
|
69
|
+
</Typography>
|
|
70
|
+
)}
|
|
71
|
+
{buttonText && buttonLink && (
|
|
72
|
+
<NextLink href={buttonLink} passHref>
|
|
73
|
+
<Button color="primary" size="large" variant="contained">
|
|
74
|
+
{buttonText}
|
|
75
|
+
</Button>
|
|
76
|
+
</NextLink>
|
|
77
|
+
)}
|
|
78
|
+
</Container>
|
|
79
|
+
</Box>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default HeroImage;
|
package/src/cms/blocks/index.tsx
CHANGED
|
@@ -14,3 +14,5 @@ export { default as ProductCarousel } from "@/cms/blocks/components/product-caro
|
|
|
14
14
|
export { default as ProductSectionFullWidth } from "@/cms/blocks/components/product-section-fullwidth/index";
|
|
15
15
|
export { default as RichText } from "@/cms/blocks/components/rich-text";
|
|
16
16
|
export { default as Services } from "@/cms/blocks/components/services/index";
|
|
17
|
+
export { default as CtaBanner } from "./components/cta-banner";
|
|
18
|
+
export { default as HeroImage } from "./components/hero-image";
|
|
@@ -141,6 +141,26 @@ export interface RichTextBlock {
|
|
|
141
141
|
content: string;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
export interface HeroImageBlock {
|
|
145
|
+
id: number;
|
|
146
|
+
__component: "blocks.hero-image";
|
|
147
|
+
heading: string;
|
|
148
|
+
subheading?: string | null;
|
|
149
|
+
buttonText?: string | null;
|
|
150
|
+
buttonLink?: string | null;
|
|
151
|
+
image?: Media | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CtaBannerBlock {
|
|
155
|
+
id: number;
|
|
156
|
+
__component: "blocks.cta-banner";
|
|
157
|
+
heading: string;
|
|
158
|
+
subtext?: string | null;
|
|
159
|
+
buttonText?: string | null;
|
|
160
|
+
buttonLink?: string | null;
|
|
161
|
+
backgroundColor?: string | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
export interface ImageFormat {
|
|
145
165
|
ext: string;
|
|
146
166
|
url: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Box, Chip } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
export function DraftModeBadge() {
|
|
4
|
+
return (
|
|
5
|
+
<Box
|
|
6
|
+
sx={{
|
|
7
|
+
position: "fixed",
|
|
8
|
+
bottom: 16,
|
|
9
|
+
right: 16,
|
|
10
|
+
zIndex: 9999,
|
|
11
|
+
}}
|
|
12
|
+
>
|
|
13
|
+
<Chip
|
|
14
|
+
label="Draft Mode"
|
|
15
|
+
size="small"
|
|
16
|
+
sx={{
|
|
17
|
+
backgroundColor: "#f59e0b",
|
|
18
|
+
color: "#000",
|
|
19
|
+
fontWeight: 700,
|
|
20
|
+
fontSize: "0.75rem",
|
|
21
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
</Box>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THIS IS A REACT-QUERY WRAPPER LAYER AROUND THE CMS SDK.
|
|
3
|
+
* IT PROVIDES TYPING AND STRUCTURE TO THE QUERIES USED THROUGHOUT THE APP, AND
|
|
4
|
+
* IT ALSO PROVIDES A SINGLE PLACE TO CONFIGURE QUERY OPTIONS FOR CMS DATA.
|
|
5
|
+
*/
|
|
6
|
+
import { cms } from "@evenicanpm/cms";
|
|
7
|
+
import type { Blog } from "@evenicanpm/cms/src/content/blogs/types";
|
|
8
|
+
import type { Page } from "@evenicanpm/cms/src/content/pages/types";
|
|
9
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
10
|
+
import type { StrapiResponse } from "strapi-sdk-js";
|
|
11
|
+
|
|
12
|
+
// strapi-sdk-js's StrapiResponseMetaPagination is missing pageCount, which Strapi
|
|
13
|
+
// includes when queries use withCount:true. This extends the meta to include it.
|
|
14
|
+
type StrapiPagedResponse<T> = StrapiResponse<T> & {
|
|
15
|
+
meta: {
|
|
16
|
+
pagination: {
|
|
17
|
+
page: number;
|
|
18
|
+
pageSize: number;
|
|
19
|
+
pageCount: number;
|
|
20
|
+
total: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const cmsQueryKeys = {
|
|
26
|
+
navigation: () => ["cms", "navigation"] as const,
|
|
27
|
+
page: (
|
|
28
|
+
slug: string,
|
|
29
|
+
status: "draft" | "published" = "published",
|
|
30
|
+
locale?: string,
|
|
31
|
+
) => ["cms", "pages", slug, status, locale] as const,
|
|
32
|
+
blogs: (page: number, pageSize: number, locale?: string) =>
|
|
33
|
+
["cms", "blogs", page, pageSize, locale] as const,
|
|
34
|
+
blog: (id: string, locale?: string) => ["cms", "blog", id, locale] as const,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const cmsQueryOptions = {
|
|
38
|
+
navigation: () =>
|
|
39
|
+
queryOptions({
|
|
40
|
+
queryKey: cmsQueryKeys.navigation(),
|
|
41
|
+
queryFn: (): Promise<unknown> => cms.navigation.getMainNavigation(),
|
|
42
|
+
}),
|
|
43
|
+
page: (
|
|
44
|
+
slug: string,
|
|
45
|
+
status: "draft" | "published" = "published",
|
|
46
|
+
locale?: string,
|
|
47
|
+
) =>
|
|
48
|
+
queryOptions({
|
|
49
|
+
queryKey: cmsQueryKeys.page(slug, status, locale),
|
|
50
|
+
queryFn: (): Promise<StrapiResponse<Page[]>> =>
|
|
51
|
+
(
|
|
52
|
+
cms.pages.getPage as (opts: {
|
|
53
|
+
slug: string;
|
|
54
|
+
status?: "draft" | "published";
|
|
55
|
+
locale?: string;
|
|
56
|
+
}) => Promise<StrapiResponse<Page[]>>
|
|
57
|
+
)({ slug, status, locale }),
|
|
58
|
+
}),
|
|
59
|
+
blogs: (page: number, pageSize: number, locale?: string) =>
|
|
60
|
+
queryOptions({
|
|
61
|
+
queryKey: cmsQueryKeys.blogs(page, pageSize, locale),
|
|
62
|
+
queryFn: (): Promise<StrapiPagedResponse<Blog[]>> =>
|
|
63
|
+
(
|
|
64
|
+
cms.blogs.getBlogs as (opts: {
|
|
65
|
+
pagination: { page: number; pageSize: number };
|
|
66
|
+
locale?: string;
|
|
67
|
+
}) => Promise<StrapiPagedResponse<Blog[]>>
|
|
68
|
+
)({ pagination: { page, pageSize }, locale }),
|
|
69
|
+
}),
|
|
70
|
+
blog: (id: string, locale?: string) =>
|
|
71
|
+
queryOptions({
|
|
72
|
+
queryKey: cmsQueryKeys.blog(id, locale),
|
|
73
|
+
queryFn: (): Promise<StrapiResponse<Blog>> =>
|
|
74
|
+
(
|
|
75
|
+
cms.blogs.getBlog as (opts: {
|
|
76
|
+
id: string;
|
|
77
|
+
locale?: string;
|
|
78
|
+
}) => Promise<StrapiResponse<Blog>>
|
|
79
|
+
)({ id, locale }),
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
@@ -7,8 +7,15 @@ vi.mock("@evenicanpm/storefront-core/src/hooks/use-nextauth-session", () => ({
|
|
|
7
7
|
useAppSession: vi.fn(() => ({ session: null })),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
|
-
vi.mock("
|
|
11
|
-
|
|
10
|
+
vi.mock("better-auth/react", () => ({
|
|
11
|
+
createAuthClient: () => ({
|
|
12
|
+
useSession: () => ({ data: null, isPending: false }),
|
|
13
|
+
signIn: { oauth2: vi.fn() },
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("better-auth/client/plugins", () => ({
|
|
18
|
+
genericOAuthClient: () => ({}),
|
|
12
19
|
}));
|
|
13
20
|
|
|
14
21
|
vi.mock(
|