@akinon/projectzero 2.0.39-beta.0 → 2.0.39
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/CHANGELOG.md +1 -1
- package/app-template/CHANGELOG.md +32 -32
- package/app-template/package.json +29 -29
- package/app-template/src/app/[pz]/account/layout.tsx +13 -2
- package/app-template/src/app/[pz]/account/orders/[id]/layout.tsx +26 -2
- package/app-template/src/app/[pz]/basket/layout.tsx +25 -0
- package/app-template/src/app/[pz]/category/[pk]/page.tsx +1 -1
- package/app-template/src/app/[pz]/client-root.tsx +2 -0
- package/app-template/src/app/[pz]/configure-page-context.ts +17 -0
- package/app-template/src/app/[pz]/flat-page/[pk]/page.tsx +14 -3
- package/app-template/src/app/[pz]/layout.tsx +1 -0
- package/app-template/src/app/[pz]/list/page.tsx +1 -1
- package/app-template/src/app/[pz]/orders/checkout/layout.tsx +23 -0
- package/app-template/src/app/[pz]/orders/completed/[token]/layout.tsx +24 -2
- package/app-template/src/app/[pz]/page.tsx +1 -1
- package/app-template/src/app/[pz]/pages/[slug]/page.tsx +1 -1
- package/app-template/src/app/[pz]/special-page/[pk]/page.tsx +10 -1
- package/app-template/src/views/category/layout.tsx +12 -2
- package/app-template/src/views/product/layout.tsx +8 -5
- package/package.json +1 -1
- package/app-template/src/utils/__tests__/theme-page-context.test.ts +0 -148
- package/app-template/src/utils/theme-page-context.ts +0 -309
package/CHANGELOG.md
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
-
## 2.0.39
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
- @akinon/
|
|
9
|
-
- @akinon/
|
|
10
|
-
- @akinon/pz-
|
|
11
|
-
- @akinon/pz-
|
|
12
|
-
- @akinon/pz-
|
|
13
|
-
- @akinon/pz-
|
|
14
|
-
- @akinon/pz-
|
|
15
|
-
- @akinon/pz-
|
|
16
|
-
- @akinon/pz-
|
|
17
|
-
- @akinon/pz-
|
|
18
|
-
- @akinon/pz-
|
|
19
|
-
- @akinon/pz-
|
|
20
|
-
- @akinon/pz-
|
|
21
|
-
- @akinon/pz-
|
|
22
|
-
- @akinon/pz-
|
|
23
|
-
- @akinon/pz-
|
|
24
|
-
- @akinon/pz-
|
|
25
|
-
- @akinon/pz-masterpass@2.0.39
|
|
26
|
-
- @akinon/pz-
|
|
27
|
-
- @akinon/pz-
|
|
28
|
-
- @akinon/pz-
|
|
29
|
-
- @akinon/pz-
|
|
30
|
-
- @akinon/pz-
|
|
31
|
-
- @akinon/pz-
|
|
32
|
-
- @akinon/pz-
|
|
33
|
-
- @akinon/pz-
|
|
34
|
-
- @akinon/pz-
|
|
3
|
+
## 2.0.39
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [115c5602]
|
|
8
|
+
- @akinon/pz-theme@2.0.39
|
|
9
|
+
- @akinon/next@2.0.39
|
|
10
|
+
- @akinon/pz-akifast@2.0.39
|
|
11
|
+
- @akinon/pz-apple-pay@2.0.39
|
|
12
|
+
- @akinon/pz-b2b@2.0.39
|
|
13
|
+
- @akinon/pz-basket-gift-pack@2.0.39
|
|
14
|
+
- @akinon/pz-bkm@2.0.39
|
|
15
|
+
- @akinon/pz-checkout-gift-pack@2.0.39
|
|
16
|
+
- @akinon/pz-click-collect@2.0.39
|
|
17
|
+
- @akinon/pz-credit-payment@2.0.39
|
|
18
|
+
- @akinon/pz-cybersource-uc@2.0.39
|
|
19
|
+
- @akinon/pz-flow-payment@2.0.39
|
|
20
|
+
- @akinon/pz-google-pay@2.0.39
|
|
21
|
+
- @akinon/pz-gpay@2.0.39
|
|
22
|
+
- @akinon/pz-haso@2.0.39
|
|
23
|
+
- @akinon/pz-hepsipay@2.0.39
|
|
24
|
+
- @akinon/pz-masterpass@2.0.39
|
|
25
|
+
- @akinon/pz-masterpass-rest@2.0.39
|
|
26
|
+
- @akinon/pz-multi-basket@2.0.39
|
|
27
|
+
- @akinon/pz-one-click-checkout@2.0.39
|
|
28
|
+
- @akinon/pz-otp@2.0.39
|
|
29
|
+
- @akinon/pz-pay-on-delivery@2.0.39
|
|
30
|
+
- @akinon/pz-saved-card@2.0.39
|
|
31
|
+
- @akinon/pz-similar-products@2.0.39
|
|
32
|
+
- @akinon/pz-tabby-extension@2.0.39
|
|
33
|
+
- @akinon/pz-tamara-extension@2.0.39
|
|
34
|
+
- @akinon/pz-virtual-try-on@2.0.39
|
|
35
35
|
|
|
36
36
|
## 2.0.38
|
|
37
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "2.0.39
|
|
3
|
+
"version": "2.0.39",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -24,33 +24,33 @@
|
|
|
24
24
|
"test:middleware": "jest middleware-matcher.test.ts --bail"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@akinon/next": "2.0.39
|
|
28
|
-
"@akinon/pz-akifast": "2.0.39
|
|
29
|
-
"@akinon/pz-apple-pay": "2.0.39
|
|
30
|
-
"@akinon/pz-b2b": "2.0.39
|
|
31
|
-
"@akinon/pz-basket-gift-pack": "2.0.39
|
|
32
|
-
"@akinon/pz-bkm": "2.0.39
|
|
33
|
-
"@akinon/pz-checkout-gift-pack": "2.0.39
|
|
34
|
-
"@akinon/pz-click-collect": "2.0.39
|
|
35
|
-
"@akinon/pz-credit-payment": "2.0.39
|
|
36
|
-
"@akinon/pz-cybersource-uc": "2.0.39
|
|
37
|
-
"@akinon/pz-flow-payment": "2.0.39
|
|
38
|
-
"@akinon/pz-google-pay": "2.0.39
|
|
39
|
-
"@akinon/pz-gpay": "2.0.39
|
|
40
|
-
"@akinon/pz-haso": "2.0.39
|
|
41
|
-
"@akinon/pz-hepsipay": "2.0.39
|
|
42
|
-
"@akinon/pz-masterpass": "2.0.39
|
|
43
|
-
"@akinon/pz-masterpass-rest": "2.0.39
|
|
44
|
-
"@akinon/pz-multi-basket": "2.0.39
|
|
45
|
-
"@akinon/pz-one-click-checkout": "2.0.39
|
|
46
|
-
"@akinon/pz-otp": "2.0.39
|
|
47
|
-
"@akinon/pz-pay-on-delivery": "2.0.39
|
|
48
|
-
"@akinon/pz-saved-card": "2.0.39
|
|
49
|
-
"@akinon/pz-similar-products": "2.0.39
|
|
50
|
-
"@akinon/pz-tabby-extension": "2.0.39
|
|
51
|
-
"@akinon/pz-tamara-extension": "2.0.39
|
|
52
|
-
"@akinon/pz-theme": "2.0.39
|
|
53
|
-
"@akinon/pz-virtual-try-on": "2.0.39
|
|
27
|
+
"@akinon/next": "2.0.39",
|
|
28
|
+
"@akinon/pz-akifast": "2.0.39",
|
|
29
|
+
"@akinon/pz-apple-pay": "2.0.39",
|
|
30
|
+
"@akinon/pz-b2b": "2.0.39",
|
|
31
|
+
"@akinon/pz-basket-gift-pack": "2.0.39",
|
|
32
|
+
"@akinon/pz-bkm": "2.0.39",
|
|
33
|
+
"@akinon/pz-checkout-gift-pack": "2.0.39",
|
|
34
|
+
"@akinon/pz-click-collect": "2.0.39",
|
|
35
|
+
"@akinon/pz-credit-payment": "2.0.39",
|
|
36
|
+
"@akinon/pz-cybersource-uc": "2.0.39",
|
|
37
|
+
"@akinon/pz-flow-payment": "2.0.39",
|
|
38
|
+
"@akinon/pz-google-pay": "2.0.39",
|
|
39
|
+
"@akinon/pz-gpay": "2.0.39",
|
|
40
|
+
"@akinon/pz-haso": "2.0.39",
|
|
41
|
+
"@akinon/pz-hepsipay": "2.0.39",
|
|
42
|
+
"@akinon/pz-masterpass": "2.0.39",
|
|
43
|
+
"@akinon/pz-masterpass-rest": "2.0.39",
|
|
44
|
+
"@akinon/pz-multi-basket": "2.0.39",
|
|
45
|
+
"@akinon/pz-one-click-checkout": "2.0.39",
|
|
46
|
+
"@akinon/pz-otp": "2.0.39",
|
|
47
|
+
"@akinon/pz-pay-on-delivery": "2.0.39",
|
|
48
|
+
"@akinon/pz-saved-card": "2.0.39",
|
|
49
|
+
"@akinon/pz-similar-products": "2.0.39",
|
|
50
|
+
"@akinon/pz-tabby-extension": "2.0.39",
|
|
51
|
+
"@akinon/pz-tamara-extension": "2.0.39",
|
|
52
|
+
"@akinon/pz-theme": "2.0.39",
|
|
53
|
+
"@akinon/pz-virtual-try-on": "2.0.39",
|
|
54
54
|
"@hookform/resolvers": "2.9.0",
|
|
55
55
|
"@next/third-parties": "16.2.6",
|
|
56
56
|
"@react-google-maps/api": "2.17.1",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"yup": "0.32.11"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@akinon/eslint-plugin-projectzero": "2.0.39
|
|
77
|
+
"@akinon/eslint-plugin-projectzero": "2.0.39",
|
|
78
78
|
"@semantic-release/changelog": "6.0.2",
|
|
79
79
|
"@semantic-release/exec": "6.0.3",
|
|
80
80
|
"@semantic-release/git": "10.0.1",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import AccountBackButton from '@theme/views/account/back-button';
|
|
2
2
|
import AccountMenu from '@theme/views/account/account-menu';
|
|
3
|
-
import
|
|
3
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
4
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
5
|
+
import { buildAccountPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
4
6
|
import { auth } from 'auth';
|
|
5
7
|
import { ROUTES } from 'routes';
|
|
6
8
|
import { redirect } from '@akinon/next/utils/redirect';
|
|
@@ -21,7 +23,16 @@ export default async function AccountLayout({
|
|
|
21
23
|
<AccountMenu />
|
|
22
24
|
<div className="w-full flex-1">
|
|
23
25
|
<AccountBackButton />
|
|
24
|
-
|
|
26
|
+
{/* The session is already awaited above, so the account context costs
|
|
27
|
+
no extra fetch. Profile-only fields (phone, gender) stay empty
|
|
28
|
+
until a live provider merges them in. */}
|
|
29
|
+
<ChromeSlot
|
|
30
|
+
slug={PLACEHOLDER_SLUGS.accountShell}
|
|
31
|
+
pageContext={buildAccountPageContext({
|
|
32
|
+
path: ROUTES.ACCOUNT,
|
|
33
|
+
sessionUser: session.user as Record<string, unknown>
|
|
34
|
+
})}
|
|
35
|
+
/>
|
|
25
36
|
{children}
|
|
26
37
|
</div>
|
|
27
38
|
</div>
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
3
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
4
|
+
import { LiveAccountOrderPageContext } from '@akinon/pz-theme/src/chrome/live-order-page-context';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Account order detail. Publishes the SAME `order` root as the success page,
|
|
8
|
+
* so a composition authored on one resolves on the other.
|
|
9
|
+
*/
|
|
10
|
+
export default async function Layout({
|
|
11
|
+
children,
|
|
12
|
+
params
|
|
13
|
+
}: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
params: Promise<{ id: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const { id } = await params;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<LiveAccountOrderPageContext orderId={id}>
|
|
22
|
+
<ChromeSlot
|
|
23
|
+
slug={PLACEHOLDER_SLUGS.accountOrderDetail}
|
|
24
|
+
fallback={children}
|
|
25
|
+
/>
|
|
26
|
+
</LiveAccountOrderPageContext>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
5
29
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
2
|
+
import { LiveBasketPageContext } from '@akinon/pz-theme/src/chrome/live-basket-page-context';
|
|
3
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
4
|
+
import { ROUTES } from '@theme/routes';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The basket body is handed to the theme editor here rather than in page.tsx,
|
|
8
|
+
* because the slot is an async server component and the page is a client one.
|
|
9
|
+
* With no composition published the slot renders the page untouched.
|
|
10
|
+
*
|
|
11
|
+
* The provider sits above the slot so the composition binds the shopper's live
|
|
12
|
+
* basket: the basket is session-scoped and has no server data module, so a
|
|
13
|
+
* server-built context would be a snapshot that never moves.
|
|
14
|
+
*/
|
|
15
|
+
export default function BasketLayout({
|
|
16
|
+
children
|
|
17
|
+
}: {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<LiveBasketPageContext path={ROUTES.BASKET}>
|
|
22
|
+
<ChromeSlot slug={PLACEHOLDER_SLUGS.basket} fallback={children} />
|
|
23
|
+
</LiveBasketPageContext>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -2,7 +2,7 @@ import { getCategoryData } from '@akinon/next/data/server';
|
|
|
2
2
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
3
|
import { ResolvedPageProps } from '@akinon/next/types';
|
|
4
4
|
import CategoryLayout from '@theme/views/category/layout';
|
|
5
|
-
import { buildListingPageContext } from '@theme/
|
|
5
|
+
import { buildListingPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
6
6
|
import { notFound } from 'next/navigation';
|
|
7
7
|
|
|
8
8
|
async function Page({ params, searchParams }: ResolvedPageProps<{ pk: number }>) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { configurePageContextRoutes } from '@akinon/pz-theme/src/chrome/page-context';
|
|
2
|
+
import { ROUTES, ORDER_ROUTES, ACCOUNT_ROUTES } from '@theme/routes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The packaged page-context builders default to `/basket`; this storefront
|
|
6
|
+
* routes it at `/baskets/basket`. Without this the published context would
|
|
7
|
+
* carry a basket path no link block could follow.
|
|
8
|
+
*
|
|
9
|
+
* Imported from BOTH the server layout and the client root: the routes live in
|
|
10
|
+
* module state, and the client bundle gets its own copy of that module.
|
|
11
|
+
*/
|
|
12
|
+
configurePageContextRoutes({
|
|
13
|
+
home: ROUTES.HOME,
|
|
14
|
+
basket: ROUTES.BASKET,
|
|
15
|
+
checkout: ORDER_ROUTES.CHECKOUT,
|
|
16
|
+
account: ACCOUNT_ROUTES.ACCOUNT
|
|
17
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
2
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
3
|
+
import { buildFlatPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
1
4
|
import { getFlatPageData, getSeoData } from '@akinon/next/data/server';
|
|
2
5
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
6
|
import {
|
|
@@ -65,9 +68,17 @@ async function Page({ params }: ResolvedPageProps<{ pk: number }>) {
|
|
|
65
68
|
|
|
66
69
|
return (
|
|
67
70
|
<div className="container mx-auto py-6">
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
<ChromeSlot
|
|
72
|
+
slug={PLACEHOLDER_SLUGS.flatPage}
|
|
73
|
+
pageContext={buildFlatPageContext(
|
|
74
|
+
data.flat_page as unknown as Record<string, unknown>
|
|
75
|
+
)}
|
|
76
|
+
fallback={
|
|
77
|
+
<div
|
|
78
|
+
className="mx-auto prose prose-headings:text-primary"
|
|
79
|
+
dangerouslySetInnerHTML={{ __html: data.flat_page.content }}
|
|
80
|
+
/>
|
|
81
|
+
}
|
|
71
82
|
/>
|
|
72
83
|
</div>
|
|
73
84
|
);
|
|
@@ -16,6 +16,7 @@ import { InstallPrompt } from '@theme/components/install-prompt';
|
|
|
16
16
|
import type { Viewport } from 'next';
|
|
17
17
|
import settings from '@theme/settings';
|
|
18
18
|
import RouteHandler from '@theme/components/route-handler';
|
|
19
|
+
import './configure-page-context';
|
|
19
20
|
|
|
20
21
|
export const viewport: Viewport = {
|
|
21
22
|
width: 'device-width',
|
|
@@ -3,7 +3,7 @@ import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
|
3
3
|
import { ResolvedPageProps } from '@akinon/next/types';
|
|
4
4
|
import CategoryLayout from '@theme/views/category/layout';
|
|
5
5
|
import { ROUTES } from '@theme/routes';
|
|
6
|
-
import { buildListingPageContext } from '@theme/
|
|
6
|
+
import { buildListingPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
7
7
|
|
|
8
8
|
async function Page({ searchParams }: ResolvedPageProps) {
|
|
9
9
|
const data = await getListData({ searchParams });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
3
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
4
|
+
import { LiveCheckoutPageContext } from '@akinon/pz-theme/src/chrome/live-checkout-page-context';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checkout is code-owned: only the zones above and below the flow are
|
|
8
|
+
* composable, so neither slot takes `fallback={children}` — a published
|
|
9
|
+
* composition must never be able to replace the payment steps.
|
|
10
|
+
*/
|
|
11
|
+
export default function CheckoutLayout({
|
|
12
|
+
children
|
|
13
|
+
}: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<LiveCheckoutPageContext>
|
|
18
|
+
<ChromeSlot slug={PLACEHOLDER_SLUGS.checkoutTop} />
|
|
19
|
+
{children}
|
|
20
|
+
<ChromeSlot slug={PLACEHOLDER_SLUGS.checkoutBottom} />
|
|
21
|
+
</LiveCheckoutPageContext>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
3
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
4
|
+
import { LiveOrderSuccessPageContext } from '@akinon/pz-theme/src/chrome/live-order-page-context';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The order-success body is composable and binds the shopper's own order.
|
|
8
|
+
* There is no server order fetcher, so the context comes from the client
|
|
9
|
+
* provider — which also keeps the order out of any cached shell.
|
|
10
|
+
*/
|
|
11
|
+
export default async function Layout({
|
|
12
|
+
children,
|
|
13
|
+
params
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
params: Promise<{ token: string }>;
|
|
17
|
+
}) {
|
|
18
|
+
const { token } = await params;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<LiveOrderSuccessPageContext token={token}>
|
|
23
|
+
<ChromeSlot slug={PLACEHOLDER_SLUGS.orderSuccess} fallback={children} />
|
|
24
|
+
</LiveOrderSuccessPageContext>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
5
27
|
}
|
|
@@ -2,7 +2,7 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
4
4
|
import ThemePlaceholder from '@akinon/pz-theme/src/theme-placeholder';
|
|
5
|
-
import { buildHomepagePageContext } from '@theme/
|
|
5
|
+
import { buildHomepagePageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
6
6
|
|
|
7
7
|
export const dynamic = 'force-static';
|
|
8
8
|
export const revalidate = 600;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ThemePlaceholder from '@akinon/pz-theme/src/theme-placeholder';
|
|
2
2
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
3
|
import { ResolvedPageProps } from '@akinon/next/types';
|
|
4
|
-
import { buildCustomPageContext } from '@theme/
|
|
4
|
+
import { buildCustomPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
5
5
|
|
|
6
6
|
export const dynamic = 'force-dynamic';
|
|
7
7
|
|
|
@@ -2,6 +2,7 @@ import { getSpecialPageData } from '@akinon/next/data/server';
|
|
|
2
2
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
3
|
import { ResolvedPageProps } from '@akinon/next/types';
|
|
4
4
|
import CategoryLayout from '@theme/views/category/layout';
|
|
5
|
+
import { buildListingPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
5
6
|
import SpecialPageBanner from '@theme/widgets/special-page-banner';
|
|
6
7
|
import SpecialPageCarousel from '@theme/widgets/special-page-carousel';
|
|
7
8
|
import { notFound } from 'next/navigation';
|
|
@@ -12,7 +13,15 @@ async function Page({ params, searchParams }: ResolvedPageProps<{ pk: number }>)
|
|
|
12
13
|
|
|
13
14
|
return (
|
|
14
15
|
<>
|
|
15
|
-
<CategoryLayout
|
|
16
|
+
<CategoryLayout
|
|
17
|
+
data={data}
|
|
18
|
+
pageContext={buildListingPageContext({
|
|
19
|
+
data,
|
|
20
|
+
pageType: 'special',
|
|
21
|
+
path: `/special-page/${params.pk}`,
|
|
22
|
+
title: (data.special_page as { name?: string } | undefined)?.name
|
|
23
|
+
})}
|
|
24
|
+
>
|
|
16
25
|
{data.special_page && (
|
|
17
26
|
<>
|
|
18
27
|
<SpecialPageBanner data={data.special_page} />
|
|
@@ -3,11 +3,14 @@ import { BreadcrumbResultType, GetCategoryResponse } from '@akinon/next/types';
|
|
|
3
3
|
import Breadcrumb from '@theme/views/breadcrumb';
|
|
4
4
|
import { CategoryBanner } from '@theme/views/category/category-banner';
|
|
5
5
|
import ListPage from '@theme/views/category/category-info';
|
|
6
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
7
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
6
8
|
|
|
7
9
|
export default async function Layout({
|
|
8
10
|
data,
|
|
9
11
|
children,
|
|
10
|
-
breadcrumbData
|
|
12
|
+
breadcrumbData,
|
|
13
|
+
pageContext
|
|
11
14
|
}: {
|
|
12
15
|
data: GetCategoryResponse;
|
|
13
16
|
children?: React.ReactNode;
|
|
@@ -35,7 +38,14 @@ export default async function Layout({
|
|
|
35
38
|
</div>
|
|
36
39
|
<CategoryBanner {...data?.category?.attributes?.category_banner} />
|
|
37
40
|
</div>
|
|
38
|
-
|
|
41
|
+
{/* The listing context was built on every request and thrown away: the
|
|
42
|
+
prop was declared and never read, so no listing binding has ever
|
|
43
|
+
resolved on any brand. */}
|
|
44
|
+
<ChromeSlot
|
|
45
|
+
slug={PLACEHOLDER_SLUGS.productListing}
|
|
46
|
+
fallback={<ListPage data={data} />}
|
|
47
|
+
pageContext={pageContext}
|
|
48
|
+
/>
|
|
39
49
|
</>
|
|
40
50
|
);
|
|
41
51
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import Breadcrumb from '@theme/views/breadcrumb';
|
|
2
2
|
import { ProductInfoSlider } from '@theme/views/product';
|
|
3
|
-
import
|
|
3
|
+
import ChromeSlot from '@akinon/pz-theme/src/chrome/chrome-slot';
|
|
4
|
+
import { PLACEHOLDER_SLUGS } from '@akinon/pz-theme/src/chrome/placeholder-slugs';
|
|
4
5
|
import { BreadcrumbResultType, ProductResult } from '@akinon/next/types';
|
|
5
|
-
import { buildProductPageContext } from '@theme/
|
|
6
|
+
import { buildProductPageContext } from '@akinon/pz-theme/src/chrome/page-context';
|
|
6
7
|
|
|
7
8
|
interface DeliveryReturnItem {
|
|
8
9
|
product_delivery_returns?: {
|
|
@@ -23,7 +24,9 @@ export default async function ProductLayout({
|
|
|
23
24
|
breadcrumbData,
|
|
24
25
|
children
|
|
25
26
|
}: ProductPageProps) {
|
|
26
|
-
|
|
27
|
+
// Breadcrumbs travel with the product context so the PDP is not the one
|
|
28
|
+
// page type whose composition cannot build a trail.
|
|
29
|
+
const productPageContext = buildProductPageContext(data, breadcrumbData);
|
|
27
30
|
const categoryIds = breadcrumbData
|
|
28
31
|
?.map((item) => item.extra_context?.attributes?.category_id)
|
|
29
32
|
.filter(Boolean)
|
|
@@ -51,8 +54,8 @@ export default async function ProductLayout({
|
|
|
51
54
|
<div className="w-full">{children}</div>
|
|
52
55
|
</div>
|
|
53
56
|
</div>
|
|
54
|
-
<
|
|
55
|
-
slug=
|
|
57
|
+
<ChromeSlot
|
|
58
|
+
slug={PLACEHOLDER_SLUGS.productDetail}
|
|
56
59
|
pageContext={productPageContext}
|
|
57
60
|
/>
|
|
58
61
|
</div>
|
package/package.json
CHANGED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildAccountPageContext,
|
|
3
|
-
buildBasketPageContext,
|
|
4
|
-
buildCustomPageContext,
|
|
5
|
-
buildHomepagePageContext,
|
|
6
|
-
buildListingPageContext,
|
|
7
|
-
buildProductPageContext
|
|
8
|
-
} from '../theme-page-context';
|
|
9
|
-
|
|
10
|
-
describe('theme-page-context utils', () => {
|
|
11
|
-
it('builds homepage and custom page contexts', () => {
|
|
12
|
-
const homepage = buildHomepagePageContext();
|
|
13
|
-
const customPage = buildCustomPageContext('summer-campaign');
|
|
14
|
-
|
|
15
|
-
expect(homepage.homepage.slug).toBe('homepage');
|
|
16
|
-
expect(customPage.customPage.slug).toBe('summer-campaign');
|
|
17
|
-
expect(customPage.customPage.title).toBe('Summer Campaign');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('builds product page context with normalized product aliases', () => {
|
|
21
|
-
const productPageContext = buildProductPageContext({
|
|
22
|
-
product: {
|
|
23
|
-
pk: 1,
|
|
24
|
-
name: 'Spring Sneaker',
|
|
25
|
-
absolute_url: '/products/spring-sneaker',
|
|
26
|
-
productimage_set: [{ image: 'https://cdn.example.com/image.jpg' }],
|
|
27
|
-
price: '199.90',
|
|
28
|
-
retail_price: '249.90',
|
|
29
|
-
currency_type: 'TRY',
|
|
30
|
-
attributes_kwargs: {}
|
|
31
|
-
},
|
|
32
|
-
selected_variant: null,
|
|
33
|
-
variants: []
|
|
34
|
-
} as never);
|
|
35
|
-
|
|
36
|
-
// The normalized aliases (title/url/images) are added dynamically and
|
|
37
|
-
// are not part of the declared product type.
|
|
38
|
-
const product = productPageContext.product as Record<string, any>;
|
|
39
|
-
expect(product.title).toBe('Spring Sneaker');
|
|
40
|
-
expect(product.url).toBe('/products/spring-sneaker');
|
|
41
|
-
expect(product.images[0].url).toBe('https://cdn.example.com/image.jpg');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('builds listing context aliases for category/search/plp style pages', () => {
|
|
45
|
-
const listingContext = buildListingPageContext({
|
|
46
|
-
data: {
|
|
47
|
-
category: {
|
|
48
|
-
name: 'Sneakers',
|
|
49
|
-
absolute_url: '/category/sneakers',
|
|
50
|
-
attributes: {
|
|
51
|
-
category_seo_title: {
|
|
52
|
-
value: {
|
|
53
|
-
text: 'Sneaker landing'
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
products: [
|
|
59
|
-
{
|
|
60
|
-
pk: 1,
|
|
61
|
-
name: 'Spring Sneaker',
|
|
62
|
-
absolute_url: '/products/spring-sneaker',
|
|
63
|
-
productimage_set: [{ image: 'https://cdn.example.com/image.jpg' }],
|
|
64
|
-
price: '199.90',
|
|
65
|
-
retail_price: '249.90',
|
|
66
|
-
currency_type: 'TRY',
|
|
67
|
-
attributes_kwargs: {}
|
|
68
|
-
}
|
|
69
|
-
],
|
|
70
|
-
facets: [{ key: 'color', name: 'Color' }],
|
|
71
|
-
pagination: { count: 12 },
|
|
72
|
-
sorters: [{ label: 'Newest', value: 'newest' }],
|
|
73
|
-
search_text: 'sneaker'
|
|
74
|
-
} as never,
|
|
75
|
-
breadcrumbData: [
|
|
76
|
-
{ label: 'Sneakers', path: '/category/sneakers' }
|
|
77
|
-
] as never,
|
|
78
|
-
pageType: 'search',
|
|
79
|
-
path: '/list'
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(listingContext.page.type).toBe('search');
|
|
83
|
-
expect(listingContext.category.name).toBe('Sneakers');
|
|
84
|
-
expect(listingContext.search.query).toBe('sneaker');
|
|
85
|
-
expect(listingContext.plp.products).toHaveLength(1);
|
|
86
|
-
expect(listingContext.listing.totalProducts).toBe(12);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('builds basket and account contexts from live data', () => {
|
|
90
|
-
const basketContext = buildBasketPageContext({
|
|
91
|
-
basket: {
|
|
92
|
-
pk: 10,
|
|
93
|
-
total_quantity: 3,
|
|
94
|
-
total_amount: '799.90',
|
|
95
|
-
total_product_amount: '899.90',
|
|
96
|
-
total_discount_amount: '100.00',
|
|
97
|
-
voucher_code: 'SPRING',
|
|
98
|
-
discounts: [],
|
|
99
|
-
basketitem_set: [
|
|
100
|
-
{
|
|
101
|
-
id: 1,
|
|
102
|
-
quantity: 2,
|
|
103
|
-
price: '399.95',
|
|
104
|
-
retail_price: '449.95',
|
|
105
|
-
total_amount: '799.90',
|
|
106
|
-
unit_price: '399.95',
|
|
107
|
-
currency_type: 'TRY',
|
|
108
|
-
image: 'https://cdn.example.com/image.jpg',
|
|
109
|
-
attributes_kwargs: {
|
|
110
|
-
color: { label: 'Color', value: 'Black' }
|
|
111
|
-
},
|
|
112
|
-
product: {
|
|
113
|
-
pk: 1,
|
|
114
|
-
name: 'Spring Sneaker',
|
|
115
|
-
absolute_url: '/products/spring-sneaker',
|
|
116
|
-
productimage_set: [
|
|
117
|
-
{ image: 'https://cdn.example.com/image.jpg' }
|
|
118
|
-
],
|
|
119
|
-
price: '399.95',
|
|
120
|
-
retail_price: '449.95',
|
|
121
|
-
currency_type: 'TRY',
|
|
122
|
-
attributes_kwargs: {}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
]
|
|
126
|
-
} as never,
|
|
127
|
-
path: '/baskets/basket'
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const accountContext = buildAccountPageContext({
|
|
131
|
-
path: '/account/profile',
|
|
132
|
-
profileInfo: {
|
|
133
|
-
first_name: 'Ada',
|
|
134
|
-
last_name: 'Lovelace',
|
|
135
|
-
email: 'ada@example.com'
|
|
136
|
-
},
|
|
137
|
-
sessionUser: {
|
|
138
|
-
name: 'Ada Lovelace',
|
|
139
|
-
email: 'ada@example.com'
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(basketContext.basket.totalQuantity).toBe(3);
|
|
144
|
-
expect(basketContext.basket.items[0].title).toBe('Spring Sneaker');
|
|
145
|
-
expect(accountContext.account.user.fullName).toBe('Ada Lovelace');
|
|
146
|
-
expect(accountContext.account.route).toBe('/account/profile');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { ROUTES } from '@theme/routes';
|
|
2
|
-
import type {
|
|
3
|
-
Basket,
|
|
4
|
-
BreadcrumbResultType,
|
|
5
|
-
GetCategoryResponse,
|
|
6
|
-
Product,
|
|
7
|
-
ProductResult
|
|
8
|
-
} from '@akinon/next/types';
|
|
9
|
-
|
|
10
|
-
type UnknownRecord = Record<string, unknown>;
|
|
11
|
-
|
|
12
|
-
const toRecord = (value: unknown): UnknownRecord =>
|
|
13
|
-
value && typeof value === 'object' ? (value as UnknownRecord) : {};
|
|
14
|
-
|
|
15
|
-
const toStringValue = (value: unknown, fallback = ''): string =>
|
|
16
|
-
typeof value === 'string' ? value : fallback;
|
|
17
|
-
|
|
18
|
-
const humanizeSlug = (value: string) =>
|
|
19
|
-
value
|
|
20
|
-
.split('-')
|
|
21
|
-
.filter(Boolean)
|
|
22
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
23
|
-
.join(' ');
|
|
24
|
-
|
|
25
|
-
const normalizeProductImages = (
|
|
26
|
-
product?: Product | null
|
|
27
|
-
): Array<Record<string, unknown>> =>
|
|
28
|
-
(product?.productimage_set || []).map((item, index) => ({
|
|
29
|
-
...item,
|
|
30
|
-
url: item.image,
|
|
31
|
-
alt: `${product?.name || 'Product'} ${index + 1}`
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
const normalizeAttributeEntries = (
|
|
35
|
-
attributes: unknown
|
|
36
|
-
): Array<Record<string, string>> => {
|
|
37
|
-
const record = toRecord(attributes);
|
|
38
|
-
|
|
39
|
-
return Object.values(record)
|
|
40
|
-
.map(entry => {
|
|
41
|
-
const typedEntry = toRecord(entry);
|
|
42
|
-
const label = toStringValue(typedEntry.label);
|
|
43
|
-
const value = toStringValue(typedEntry.value);
|
|
44
|
-
|
|
45
|
-
if (!label && !value) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return { label, value };
|
|
50
|
-
})
|
|
51
|
-
.filter((entry): entry is { label: string; value: string } => entry !== null);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const normalizeProductEntry = (
|
|
55
|
-
product?: Product | null
|
|
56
|
-
): Record<string, unknown> | null => {
|
|
57
|
-
if (!product) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const activePrice = toRecord((product as Product & UnknownRecord).active_price);
|
|
62
|
-
const images = normalizeProductImages(product);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
...product,
|
|
66
|
-
title: product.name,
|
|
67
|
-
name: product.name,
|
|
68
|
-
url: product.absolute_url,
|
|
69
|
-
image: product.image || images[0]?.url || '',
|
|
70
|
-
images,
|
|
71
|
-
price: activePrice.price ?? product.price,
|
|
72
|
-
retailPrice: activePrice.retail_price ?? product.retail_price,
|
|
73
|
-
currency: activePrice.currency_type ?? product.currency_type ?? '',
|
|
74
|
-
description: product.description || '',
|
|
75
|
-
attributes: normalizeAttributeEntries(product.attributes_kwargs)
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export const buildProductPageContext = (data: ProductResult) => {
|
|
80
|
-
const normalizedProduct = normalizeProductEntry(data.product);
|
|
81
|
-
const normalizedSelectedVariant = normalizeProductEntry(data.selected_variant);
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
page: {
|
|
85
|
-
type: 'product',
|
|
86
|
-
title: normalizedProduct?.title || 'Product',
|
|
87
|
-
path: normalizedProduct?.url || ''
|
|
88
|
-
},
|
|
89
|
-
product: {
|
|
90
|
-
...normalizedProduct,
|
|
91
|
-
selectedVariant: normalizedSelectedVariant,
|
|
92
|
-
selected_variant: normalizedSelectedVariant,
|
|
93
|
-
variants: data.variants || []
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export const buildHomepagePageContext = () => ({
|
|
99
|
-
page: {
|
|
100
|
-
type: 'homepage',
|
|
101
|
-
title: 'Homepage',
|
|
102
|
-
slug: 'homepage',
|
|
103
|
-
path: ROUTES.HOME
|
|
104
|
-
},
|
|
105
|
-
homepage: {
|
|
106
|
-
title: 'Homepage',
|
|
107
|
-
slug: 'homepage',
|
|
108
|
-
path: ROUTES.HOME
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
export const buildCustomPageContext = (slug: string) => {
|
|
113
|
-
const title = humanizeSlug(slug) || 'Custom Page';
|
|
114
|
-
const path = `/pages/${slug}`;
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
page: {
|
|
118
|
-
type: 'custom-page',
|
|
119
|
-
title,
|
|
120
|
-
slug,
|
|
121
|
-
path
|
|
122
|
-
},
|
|
123
|
-
customPage: {
|
|
124
|
-
title,
|
|
125
|
-
slug,
|
|
126
|
-
path
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export const buildListingPageContext = ({
|
|
132
|
-
data,
|
|
133
|
-
breadcrumbData,
|
|
134
|
-
pageType,
|
|
135
|
-
path
|
|
136
|
-
}: {
|
|
137
|
-
data: GetCategoryResponse;
|
|
138
|
-
breadcrumbData?: BreadcrumbResultType[];
|
|
139
|
-
pageType: 'category' | 'search' | 'plp';
|
|
140
|
-
path?: string;
|
|
141
|
-
}) => {
|
|
142
|
-
const normalizedProducts = (data.products || [])
|
|
143
|
-
.map(product => normalizeProductEntry(product))
|
|
144
|
-
.filter((product): product is Record<string, unknown> => product !== null);
|
|
145
|
-
const categorySeoTitle = toStringValue(
|
|
146
|
-
data.category?.attributes?.category_seo_title?.value?.text
|
|
147
|
-
);
|
|
148
|
-
const effectivePath = path || data.category?.absolute_url || ROUTES.HOME;
|
|
149
|
-
const totalProducts =
|
|
150
|
-
Number(toRecord(data.pagination).count) || normalizedProducts.length;
|
|
151
|
-
const title =
|
|
152
|
-
pageType === 'search'
|
|
153
|
-
? data.search_text
|
|
154
|
-
? `Search results for ${data.search_text}`
|
|
155
|
-
: 'Search Results'
|
|
156
|
-
: data.category?.name || 'Product Listing';
|
|
157
|
-
|
|
158
|
-
const category = {
|
|
159
|
-
...(data.category || {}),
|
|
160
|
-
title: data.category?.name || '',
|
|
161
|
-
name: data.category?.name || '',
|
|
162
|
-
url: data.category?.absolute_url || '',
|
|
163
|
-
description: categorySeoTitle,
|
|
164
|
-
seoTitle: categorySeoTitle,
|
|
165
|
-
facets: data.facets || [],
|
|
166
|
-
products: normalizedProducts
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const listingBase = {
|
|
170
|
-
title,
|
|
171
|
-
path: effectivePath,
|
|
172
|
-
query: data.search_text || '',
|
|
173
|
-
products: normalizedProducts,
|
|
174
|
-
totalProducts,
|
|
175
|
-
resultCount: totalProducts,
|
|
176
|
-
facets: data.facets || [],
|
|
177
|
-
sorters: data.sorters || [],
|
|
178
|
-
pagination: data.pagination || {},
|
|
179
|
-
category
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
page: {
|
|
184
|
-
type: pageType,
|
|
185
|
-
title,
|
|
186
|
-
path: effectivePath
|
|
187
|
-
},
|
|
188
|
-
category,
|
|
189
|
-
listing: {
|
|
190
|
-
...listingBase,
|
|
191
|
-
breadcrumbs: breadcrumbData || []
|
|
192
|
-
},
|
|
193
|
-
search: {
|
|
194
|
-
...listingBase,
|
|
195
|
-
title: data.search_text
|
|
196
|
-
? `Search results for ${data.search_text}`
|
|
197
|
-
: 'Search Results'
|
|
198
|
-
},
|
|
199
|
-
plp: {
|
|
200
|
-
...listingBase,
|
|
201
|
-
title: data.category?.name || 'Product Listing'
|
|
202
|
-
},
|
|
203
|
-
breadcrumbs: breadcrumbData || []
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export const buildBasketPageContext = ({
|
|
208
|
-
basket,
|
|
209
|
-
path
|
|
210
|
-
}: {
|
|
211
|
-
basket?: Basket | null;
|
|
212
|
-
path?: string;
|
|
213
|
-
}) => {
|
|
214
|
-
const normalizedItems = (basket?.basketitem_set || []).map(item => {
|
|
215
|
-
const normalizedProduct = normalizeProductEntry(item.product);
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
...item,
|
|
219
|
-
title: normalizedProduct?.title || item.product?.name || '',
|
|
220
|
-
name: normalizedProduct?.name || item.product?.name || '',
|
|
221
|
-
url: normalizedProduct?.url || item.product?.absolute_url || '',
|
|
222
|
-
image:
|
|
223
|
-
item.image ||
|
|
224
|
-
(typeof normalizedProduct?.image === 'string'
|
|
225
|
-
? normalizedProduct.image
|
|
226
|
-
: ''),
|
|
227
|
-
price: item.price || normalizedProduct?.price || '',
|
|
228
|
-
retailPrice: item.retail_price || normalizedProduct?.retailPrice || '',
|
|
229
|
-
totalAmount: item.total_amount || '',
|
|
230
|
-
unitPrice: item.unit_price || '',
|
|
231
|
-
currency: item.currency_type || normalizedProduct?.currency || '',
|
|
232
|
-
quantity: item.quantity,
|
|
233
|
-
attributes: normalizeAttributeEntries(
|
|
234
|
-
item.attributes_kwargs || item.product?.attributes_kwargs
|
|
235
|
-
),
|
|
236
|
-
product: normalizedProduct
|
|
237
|
-
};
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
page: {
|
|
242
|
-
type: 'basket',
|
|
243
|
-
title: 'Basket',
|
|
244
|
-
path: path || ROUTES.BASKET
|
|
245
|
-
},
|
|
246
|
-
basket: {
|
|
247
|
-
id: basket?.pk || null,
|
|
248
|
-
path: path || ROUTES.BASKET,
|
|
249
|
-
checkoutUrl: ROUTES.CHECKOUT,
|
|
250
|
-
continueShoppingUrl: ROUTES.HOME,
|
|
251
|
-
isEmpty: normalizedItems.length === 0,
|
|
252
|
-
itemCount: normalizedItems.length,
|
|
253
|
-
totalQuantity: basket?.total_quantity || 0,
|
|
254
|
-
totalAmount: basket?.total_amount || '',
|
|
255
|
-
totalProductAmount: basket?.total_product_amount || '',
|
|
256
|
-
totalDiscountAmount: basket?.total_discount_amount || '',
|
|
257
|
-
voucherCode: basket?.voucher_code || '',
|
|
258
|
-
discounts: basket?.discounts || [],
|
|
259
|
-
items: normalizedItems
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
export const buildAccountPageContext = ({
|
|
265
|
-
path,
|
|
266
|
-
profileInfo,
|
|
267
|
-
sessionUser
|
|
268
|
-
}: {
|
|
269
|
-
path?: string;
|
|
270
|
-
profileInfo?: UnknownRecord | null;
|
|
271
|
-
sessionUser?: UnknownRecord | null;
|
|
272
|
-
}) => {
|
|
273
|
-
const normalizedPath = path || ROUTES.ACCOUNT;
|
|
274
|
-
const firstName =
|
|
275
|
-
toStringValue(profileInfo?.first_name) ||
|
|
276
|
-
toStringValue(sessionUser?.firstName) ||
|
|
277
|
-
toStringValue(sessionUser?.name).split(' ')[0] ||
|
|
278
|
-
'';
|
|
279
|
-
const lastName =
|
|
280
|
-
toStringValue(profileInfo?.last_name) ||
|
|
281
|
-
toStringValue(sessionUser?.lastName) ||
|
|
282
|
-
toStringValue(sessionUser?.name).split(' ').slice(1).join(' ') ||
|
|
283
|
-
'';
|
|
284
|
-
const email =
|
|
285
|
-
toStringValue(profileInfo?.email) || toStringValue(sessionUser?.email);
|
|
286
|
-
const fullName =
|
|
287
|
-
[firstName, lastName].filter(Boolean).join(' ') ||
|
|
288
|
-
toStringValue(sessionUser?.name);
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
page: {
|
|
292
|
-
type: 'account',
|
|
293
|
-
title: 'Account',
|
|
294
|
-
path: normalizedPath
|
|
295
|
-
},
|
|
296
|
-
account: {
|
|
297
|
-
path: normalizedPath,
|
|
298
|
-
route: normalizedPath,
|
|
299
|
-
user: {
|
|
300
|
-
firstName,
|
|
301
|
-
lastName,
|
|
302
|
-
fullName,
|
|
303
|
-
email,
|
|
304
|
-
phone: toStringValue(profileInfo?.phone),
|
|
305
|
-
gender: toStringValue(profileInfo?.gender)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
};
|