@akinon/projectzero 1.66.0 → 1.68.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/CHANGELOG.md +8 -0
- package/app-template/.gitignore +1 -0
- package/app-template/CHANGELOG.md +74 -0
- package/app-template/package.json +17 -17
- package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +5 -2
- package/app-template/src/types/index.ts +1 -0
- package/app-template/src/views/breadcrumb.tsx +4 -1
- package/app-template/src/views/category/category-active-filters.tsx +16 -6
- package/app-template/src/views/category/filters/filter-item.tsx +57 -25
- package/app-template/src/views/category/filters/index.tsx +16 -8
- package/app-template/src/views/category/layout.tsx +5 -3
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +19 -3
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +160 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/app-template/.gitignore
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 1.68.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 907813c: ZERO-2934: add payment-gateway/<gateway> redirect in middleware
|
|
8
|
+
- 714e0b4: ZERO-2759: update pz-click-collect peer dependencies
|
|
9
|
+
- c873740: ZERO-2903: add types
|
|
10
|
+
- 034b813: ZERO-2903: create saved card plugin
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [907813c]
|
|
15
|
+
- Updated dependencies [82cf1e5]
|
|
16
|
+
- Updated dependencies [b92001c]
|
|
17
|
+
- Updated dependencies [714e0b4]
|
|
18
|
+
- Updated dependencies [c873740]
|
|
19
|
+
- Updated dependencies [d899cc7]
|
|
20
|
+
- Updated dependencies [63597bc]
|
|
21
|
+
- Updated dependencies [fa4c716]
|
|
22
|
+
- Updated dependencies [57d1657]
|
|
23
|
+
- Updated dependencies [2eba2a8]
|
|
24
|
+
- Updated dependencies [3be7462]
|
|
25
|
+
- Updated dependencies [8fb37c4]
|
|
26
|
+
- Updated dependencies [034b813]
|
|
27
|
+
- Updated dependencies [ce25dac]
|
|
28
|
+
- Updated dependencies [48d508f]
|
|
29
|
+
- @akinon/pz-tabby-extension@1.68.0
|
|
30
|
+
- @akinon/next@1.68.0
|
|
31
|
+
- @akinon/pz-saved-card@1.68.0
|
|
32
|
+
- @akinon/pz-click-collect@1.68.0
|
|
33
|
+
- @akinon/pz-akifast@1.68.0
|
|
34
|
+
- @akinon/pz-b2b@1.68.0
|
|
35
|
+
- @akinon/pz-basket-gift-pack@1.68.0
|
|
36
|
+
- @akinon/pz-bkm@1.68.0
|
|
37
|
+
- @akinon/pz-checkout-gift-pack@1.68.0
|
|
38
|
+
- @akinon/pz-credit-payment@1.68.0
|
|
39
|
+
- @akinon/pz-gpay@1.68.0
|
|
40
|
+
- @akinon/pz-masterpass@1.68.0
|
|
41
|
+
- @akinon/pz-one-click-checkout@1.68.0
|
|
42
|
+
- @akinon/pz-otp@1.68.0
|
|
43
|
+
- @akinon/pz-pay-on-delivery@1.68.0
|
|
44
|
+
|
|
45
|
+
## 1.67.0
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- a1a99f0: ZERO-2887: Add LoaderSpinner to Filters component
|
|
50
|
+
- bc2b411: ZERO-2825: Add attribute-based shipping options to checkout page
|
|
51
|
+
- 3d2212a: ZERO-2745: Add multi basket support
|
|
52
|
+
- 38a634e: ZERO-2893: Refactor category filter handling and URL parameters
|
|
53
|
+
- 9e25a64: ZERO-2835: Update category page layout with breadcrumb
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [bc2b411]
|
|
58
|
+
- Updated dependencies [3d2212a]
|
|
59
|
+
- Updated dependencies [8f47cca]
|
|
60
|
+
- Updated dependencies [9e25a64]
|
|
61
|
+
- @akinon/next@1.67.0
|
|
62
|
+
- @akinon/pz-akifast@1.67.0
|
|
63
|
+
- @akinon/pz-b2b@1.67.0
|
|
64
|
+
- @akinon/pz-basket-gift-pack@1.67.0
|
|
65
|
+
- @akinon/pz-bkm@1.67.0
|
|
66
|
+
- @akinon/pz-checkout-gift-pack@1.67.0
|
|
67
|
+
- @akinon/pz-click-collect@1.67.0
|
|
68
|
+
- @akinon/pz-credit-payment@1.67.0
|
|
69
|
+
- @akinon/pz-gpay@1.67.0
|
|
70
|
+
- @akinon/pz-masterpass@1.67.0
|
|
71
|
+
- @akinon/pz-one-click-checkout@1.67.0
|
|
72
|
+
- @akinon/pz-otp@1.67.0
|
|
73
|
+
- @akinon/pz-pay-on-delivery@1.67.0
|
|
74
|
+
- @akinon/pz-saved-card@1.67.0
|
|
75
|
+
- @akinon/pz-tabby-extension@1.67.0
|
|
76
|
+
|
|
3
77
|
## 1.66.0
|
|
4
78
|
|
|
5
79
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.68.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -22,21 +22,21 @@
|
|
|
22
22
|
"prestart": "pz-prestart"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@akinon/next": "1.
|
|
26
|
-
"@akinon/pz-akifast": "1.
|
|
27
|
-
"@akinon/pz-b2b": "1.
|
|
28
|
-
"@akinon/pz-basket-gift-pack": "1.
|
|
29
|
-
"@akinon/pz-bkm": "1.
|
|
30
|
-
"@akinon/pz-checkout-gift-pack": "1.
|
|
31
|
-
"@akinon/pz-click-collect": "1.
|
|
32
|
-
"@akinon/pz-credit-payment": "1.
|
|
33
|
-
"@akinon/pz-gpay": "1.
|
|
34
|
-
"@akinon/pz-masterpass": "1.
|
|
35
|
-
"@akinon/pz-one-click-checkout": "1.
|
|
36
|
-
"@akinon/pz-otp": "1.
|
|
37
|
-
"@akinon/pz-pay-on-delivery": "1.
|
|
38
|
-
"@akinon/pz-saved-card": "1.
|
|
39
|
-
"@akinon/pz-tabby-extension": "1.
|
|
25
|
+
"@akinon/next": "1.68.0",
|
|
26
|
+
"@akinon/pz-akifast": "1.68.0",
|
|
27
|
+
"@akinon/pz-b2b": "1.68.0",
|
|
28
|
+
"@akinon/pz-basket-gift-pack": "1.68.0",
|
|
29
|
+
"@akinon/pz-bkm": "1.68.0",
|
|
30
|
+
"@akinon/pz-checkout-gift-pack": "1.68.0",
|
|
31
|
+
"@akinon/pz-click-collect": "1.68.0",
|
|
32
|
+
"@akinon/pz-credit-payment": "1.68.0",
|
|
33
|
+
"@akinon/pz-gpay": "1.68.0",
|
|
34
|
+
"@akinon/pz-masterpass": "1.68.0",
|
|
35
|
+
"@akinon/pz-one-click-checkout": "1.68.0",
|
|
36
|
+
"@akinon/pz-otp": "1.68.0",
|
|
37
|
+
"@akinon/pz-pay-on-delivery": "1.68.0",
|
|
38
|
+
"@akinon/pz-saved-card": "1.68.0",
|
|
39
|
+
"@akinon/pz-tabby-extension": "1.68.0",
|
|
40
40
|
"@hookform/resolvers": "2.9.0",
|
|
41
41
|
"@next/third-parties": "14.1.0",
|
|
42
42
|
"@react-google-maps/api": "2.17.1",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"yup": "0.32.11"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@akinon/eslint-plugin-projectzero": "1.
|
|
63
|
+
"@akinon/eslint-plugin-projectzero": "1.68.0",
|
|
64
64
|
"@semantic-release/changelog": "6.0.2",
|
|
65
65
|
"@semantic-release/exec": "6.0.3",
|
|
66
66
|
"@semantic-release/git": "10.0.1",
|
|
@@ -4,11 +4,14 @@ import { PageProps } from '@akinon/next/types';
|
|
|
4
4
|
import CategoryLayout from '@theme/views/category/layout';
|
|
5
5
|
|
|
6
6
|
async function Page({ params, searchParams }: PageProps<{ pk: number }>) {
|
|
7
|
-
const { data } = await getCategoryData({
|
|
7
|
+
const { data, breadcrumbData } = await getCategoryData({
|
|
8
|
+
pk: params.pk,
|
|
9
|
+
searchParams
|
|
10
|
+
});
|
|
8
11
|
|
|
9
12
|
return (
|
|
10
13
|
<>
|
|
11
|
-
<CategoryLayout data={data} />
|
|
14
|
+
<CategoryLayout data={data} breadcrumbData={breadcrumbData} />
|
|
12
15
|
</>
|
|
13
16
|
);
|
|
14
17
|
}
|
|
@@ -5,6 +5,7 @@ import { Icon, Link } from '@theme/components';
|
|
|
5
5
|
import { ROUTES } from '@theme/routes';
|
|
6
6
|
import { useLocalization } from '@akinon/next/hooks';
|
|
7
7
|
import { BreadcrumbResultType } from '@akinon/next/types';
|
|
8
|
+
import { capitalize } from '@akinon/next/utils';
|
|
8
9
|
|
|
9
10
|
export interface BreadcrumbProps {
|
|
10
11
|
breadcrumbList?: BreadcrumbResultType[];
|
|
@@ -26,7 +27,9 @@ export default function Breadcrumb(props: BreadcrumbProps) {
|
|
|
26
27
|
<div className="flex items-center gap-3 text-xs leading-4 text-gray-950">
|
|
27
28
|
{list.map((item, index) => (
|
|
28
29
|
<Fragment key={index}>
|
|
29
|
-
<Link href={item.url}>
|
|
30
|
+
<Link href={item.url}>
|
|
31
|
+
{capitalize(item.text.toLocaleLowerCase())}
|
|
32
|
+
</Link>
|
|
30
33
|
{index !== list.length - 1 && <Icon name="chevron-end" size={8} />}
|
|
31
34
|
</Fragment>
|
|
32
35
|
))}
|
|
@@ -22,17 +22,28 @@ const CategoryActiveFilters = () => {
|
|
|
22
22
|
const handleRemoveFilter = ({ facet, choice }) => {
|
|
23
23
|
if (facet.widget_type === WIDGET_TYPE.category) {
|
|
24
24
|
dispatch(removeCategoryFacet({ facet, choice }));
|
|
25
|
-
|
|
25
|
+
} else {
|
|
26
|
+
dispatch(toggleFacet({ facet, choice }));
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
30
|
+
urlSearchParams.delete(facet.search_key);
|
|
31
|
+
router.replace(pathname + '?' + urlSearchParams.toString());
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
const url = useMemo(() => {
|
|
32
|
-
const facetSearchParams =
|
|
33
|
-
|
|
35
|
+
const facetSearchParams = convertFacetSearchParams(selectedFacets);
|
|
36
|
+
const urlSearchParams = new URLSearchParams(searchParams.toString());
|
|
34
37
|
|
|
35
|
-
const
|
|
38
|
+
for (const key of Array.from(urlSearchParams.keys())) {
|
|
39
|
+
if (facetSearchParams.has(key)) {
|
|
40
|
+
urlSearchParams.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [key, value] of Array.from(facetSearchParams.entries())) {
|
|
45
|
+
urlSearchParams.append(key, value);
|
|
46
|
+
}
|
|
36
47
|
|
|
37
48
|
const searchText = searchParams.get('search_text');
|
|
38
49
|
const page = searchParams.get('page');
|
|
@@ -53,7 +64,6 @@ const CategoryActiveFilters = () => {
|
|
|
53
64
|
|
|
54
65
|
useEffect(() => {
|
|
55
66
|
router.push(url);
|
|
56
|
-
|
|
57
67
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
68
|
}, [url]);
|
|
59
69
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
3
3
|
import { Facet, FacetChoice } from '@akinon/next/types';
|
|
4
|
-
import { Accordion, Radio, Checkbox } from '../../../components';
|
|
4
|
+
import { Accordion, Radio, Checkbox, LoaderSpinner } from '../../../components';
|
|
5
5
|
import { WIDGET_TYPE } from '../../../types';
|
|
6
6
|
import { SizeFilter } from './size-filter';
|
|
7
7
|
import { toggleFacet } from '@theme/redux/reducers/category';
|
|
8
8
|
import { commonProductAttributes } from '@theme/settings';
|
|
9
9
|
import { useRouter } from '@akinon/next/hooks';
|
|
10
|
+
import { usePathname } from 'next/navigation';
|
|
11
|
+
import { useState } from 'react';
|
|
10
12
|
|
|
11
13
|
const COMPONENT_TYPES = {
|
|
12
14
|
[WIDGET_TYPE.category]: Radio,
|
|
@@ -19,6 +21,8 @@ const sizeKey = commonProductAttributes.find(
|
|
|
19
21
|
|
|
20
22
|
interface Props {
|
|
21
23
|
facet: Facet;
|
|
24
|
+
isPending: boolean;
|
|
25
|
+
startTransition: (callback: () => void) => void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const sortByPredefinedOrder = (
|
|
@@ -78,16 +82,33 @@ const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
|
|
|
78
82
|
return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
|
|
79
83
|
};
|
|
80
84
|
|
|
81
|
-
export const FilterItem = ({ facet }: Props) => {
|
|
85
|
+
export const FilterItem = ({ facet, isPending, startTransition }: Props) => {
|
|
82
86
|
const dispatch = useAppDispatch();
|
|
83
87
|
const router = useRouter();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const pathname = usePathname();
|
|
89
|
+
|
|
90
|
+
const [pendingChoice, setPendingChoice] = useState<string | null>(null);
|
|
91
|
+
|
|
92
|
+
const handleSelectFilter = ({
|
|
93
|
+
facet,
|
|
94
|
+
choice
|
|
95
|
+
}: {
|
|
96
|
+
facet: Facet;
|
|
97
|
+
choice: FacetChoice;
|
|
98
|
+
}) => {
|
|
99
|
+
setPendingChoice(choice.label);
|
|
100
|
+
startTransition(() => {
|
|
101
|
+
if (facet.key === 'category_ids') {
|
|
102
|
+
router.push(choice.url);
|
|
103
|
+
} else {
|
|
104
|
+
dispatch(toggleFacet({ facet, choice }));
|
|
105
|
+
}
|
|
106
|
+
setPendingChoice(null);
|
|
107
|
+
|
|
108
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
109
|
+
urlSearchParams.delete(facet.search_key);
|
|
110
|
+
router.replace(pathname + '?' + urlSearchParams.toString());
|
|
111
|
+
});
|
|
91
112
|
};
|
|
92
113
|
|
|
93
114
|
const Component = getComponentByWidgetType(facet.widget_type, facet.key);
|
|
@@ -107,23 +128,34 @@ export const FilterItem = ({ facet }: Props) => {
|
|
|
107
128
|
})}
|
|
108
129
|
>
|
|
109
130
|
{choices.map((choice, index) => (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
<div key={choice.label} className="relative">
|
|
132
|
+
<Component
|
|
133
|
+
key={choice.label}
|
|
134
|
+
data={choice}
|
|
135
|
+
name={facet.key}
|
|
136
|
+
onChange={() => handleSelectFilter({ facet, choice })}
|
|
137
|
+
onClick={() =>
|
|
138
|
+
facet.key === sizeKey && handleSelectFilter({ facet, choice })
|
|
139
|
+
}
|
|
140
|
+
checked={choice.is_selected}
|
|
141
|
+
data-testid={`${choice.label.trim()}`}
|
|
142
|
+
disabled={isPending}
|
|
122
143
|
>
|
|
123
|
-
{choice.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
{choice.label} (
|
|
145
|
+
<span
|
|
146
|
+
data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
|
|
147
|
+
>
|
|
148
|
+
{choice.quantity}
|
|
149
|
+
</span>
|
|
150
|
+
)
|
|
151
|
+
</Component>
|
|
152
|
+
|
|
153
|
+
{isPending && pendingChoice === choice.label && (
|
|
154
|
+
<div className="absolute inset-0 flex items-center justify-center z-50">
|
|
155
|
+
<LoaderSpinner />
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
127
159
|
))}
|
|
128
160
|
</div>
|
|
129
161
|
</Accordion>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
|
|
5
4
|
import { Button, Icon } from '@theme/components';
|
|
6
5
|
import { useLocalization } from '@akinon/next/hooks';
|
|
7
6
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
8
7
|
import { resetSelectedFacets } from '@theme/redux/reducers/category';
|
|
9
8
|
import CategoryActiveFilters from '@theme/views/category/category-active-filters';
|
|
10
|
-
import { useMemo } from 'react';
|
|
9
|
+
import { useMemo, useState, useTransition } from 'react';
|
|
11
10
|
import { FilterItem } from './filter-item';
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
@@ -21,12 +20,11 @@ export const Filters = (props: Props) => {
|
|
|
21
20
|
const { t } = useLocalization();
|
|
22
21
|
const { isMenuOpen, setIsMenuOpen } = props;
|
|
23
22
|
|
|
23
|
+
const [isPending, startTransition] = useTransition();
|
|
24
|
+
|
|
24
25
|
const haveFilter = useMemo(() => {
|
|
25
|
-
return (
|
|
26
|
-
|
|
27
|
-
(facet) =>
|
|
28
|
-
facet.data.choices.filter((choice) => choice.is_selected).length > 0
|
|
29
|
-
).length > 0
|
|
26
|
+
return facets.some((facet) =>
|
|
27
|
+
facet.data.choices.some((choice) => choice.is_selected)
|
|
30
28
|
);
|
|
31
29
|
}, [facets]);
|
|
32
30
|
|
|
@@ -51,12 +49,22 @@ export const Filters = (props: Props) => {
|
|
|
51
49
|
<span className="text-sm">1 {t('category.filters.results')}</span>
|
|
52
50
|
<span>{t('category.filters.ready_to_wear')}</span>
|
|
53
51
|
</div>
|
|
52
|
+
|
|
54
53
|
{facets.map((facet) => {
|
|
55
|
-
return
|
|
54
|
+
return (
|
|
55
|
+
<FilterItem
|
|
56
|
+
key={facet.key}
|
|
57
|
+
facet={facet}
|
|
58
|
+
isPending={isPending}
|
|
59
|
+
startTransition={startTransition}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
56
62
|
})}
|
|
63
|
+
|
|
57
64
|
<div className="lg:hidden">
|
|
58
65
|
<CategoryActiveFilters />
|
|
59
66
|
</div>
|
|
67
|
+
|
|
60
68
|
{haveFilter && (
|
|
61
69
|
<div className="lg:hidden">
|
|
62
70
|
<Button
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { GetCategoryResponse } from '@akinon/next/types';
|
|
2
|
+
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
6
|
|
|
7
7
|
export default async function Layout({
|
|
8
8
|
data,
|
|
9
|
-
children
|
|
9
|
+
children,
|
|
10
|
+
breadcrumbData
|
|
10
11
|
}: {
|
|
11
12
|
data: GetCategoryResponse;
|
|
12
13
|
children?: React.ReactNode;
|
|
14
|
+
breadcrumbData?: BreadcrumbResultType[];
|
|
13
15
|
}) {
|
|
14
16
|
return (
|
|
15
17
|
<>
|
|
@@ -28,7 +30,7 @@ export default async function Layout({
|
|
|
28
30
|
'lg:absolute lg:inset-x-0 z-10 container lg:my-4 mx-auto'
|
|
29
31
|
)}
|
|
30
32
|
>
|
|
31
|
-
<Breadcrumb />
|
|
33
|
+
<Breadcrumb breadcrumbList={breadcrumbData} />
|
|
32
34
|
</div>
|
|
33
35
|
<CategoryBanner {...data.category?.attributes?.category_banner} />
|
|
34
36
|
</div>
|
|
@@ -5,9 +5,12 @@ import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
|
|
|
5
5
|
import { CheckoutPaymentOption } from '@akinon/next/types';
|
|
6
6
|
import { Radio } from '@theme/components';
|
|
7
7
|
import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
|
|
8
|
+
import { useMemo } from 'react';
|
|
8
9
|
|
|
9
10
|
const PaymentOptionButtons = () => {
|
|
10
|
-
const { preOrder } = useAppSelector(
|
|
11
|
+
const { preOrder, attributeBasedShippingOptions } = useAppSelector(
|
|
12
|
+
(state: RootState) => state.checkout
|
|
13
|
+
);
|
|
11
14
|
const [setPaymentOption] = useSetPaymentOptionMutation();
|
|
12
15
|
const { filteredPaymentOptions } = usePaymentOptions();
|
|
13
16
|
|
|
@@ -22,10 +25,23 @@ const PaymentOptionButtons = () => {
|
|
|
22
25
|
});
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
const displayedPaymentOptions = useMemo(() => {
|
|
29
|
+
if (
|
|
30
|
+
attributeBasedShippingOptions &&
|
|
31
|
+
Object.keys(attributeBasedShippingOptions).length > 0
|
|
32
|
+
) {
|
|
33
|
+
return filteredPaymentOptions.filter(
|
|
34
|
+
(option) => option.slug.toLowerCase() !== 'pay-on-delivery'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return filteredPaymentOptions;
|
|
39
|
+
}, [filteredPaymentOptions, attributeBasedShippingOptions]);
|
|
40
|
+
|
|
25
41
|
return (
|
|
26
42
|
<>
|
|
27
43
|
<div className="w-full space-y-4 px-4 flex flex-col mb-8 md:hidden">
|
|
28
|
-
{
|
|
44
|
+
{displayedPaymentOptions.map((option) => (
|
|
29
45
|
<label
|
|
30
46
|
key={`payment-option-${option.pk}`}
|
|
31
47
|
className="border px-4 py-3 mt-3 flex h-12"
|
|
@@ -47,7 +63,7 @@ const PaymentOptionButtons = () => {
|
|
|
47
63
|
</div>
|
|
48
64
|
|
|
49
65
|
<div className="hidden md:flex">
|
|
50
|
-
{
|
|
66
|
+
{displayedPaymentOptions.map((option) => (
|
|
51
67
|
<button
|
|
52
68
|
key={`payment-option-${option.pk}`}
|
|
53
69
|
onClick={() => onClickHandler(option)}
|
|
@@ -1,36 +1,84 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
2
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
setCurrentStep,
|
|
5
|
+
setSelectedShippingOptions
|
|
6
|
+
} from '@akinon/next/redux/reducers/checkout';
|
|
3
7
|
import { RootState } from '@theme/redux/store';
|
|
4
8
|
import {
|
|
5
9
|
useSetShippingOptionMutation,
|
|
10
|
+
useSetAttributeBasedShippingOptionsMutation,
|
|
6
11
|
useSetDataSourceShippingOptionsMutation
|
|
7
12
|
} from '@akinon/next/data/client/checkout';
|
|
8
13
|
import { Price, Button, Radio } from '@theme/components';
|
|
9
14
|
import { CheckoutStep } from '@akinon/next/types';
|
|
10
15
|
import { useLocalization } from '@akinon/next/hooks';
|
|
11
|
-
import { useEffect, useState } from 'react';
|
|
12
16
|
|
|
13
|
-
const ShippingOptions = () => {
|
|
17
|
+
const ShippingOptions: React.FC = () => {
|
|
14
18
|
const { t } = useLocalization();
|
|
19
|
+
const dispatch = useAppDispatch();
|
|
15
20
|
const {
|
|
16
21
|
steps,
|
|
17
22
|
shippingOptions,
|
|
23
|
+
attributeBasedShippingOptions,
|
|
18
24
|
dataSourceShippingOptions,
|
|
19
25
|
preOrder,
|
|
20
|
-
addressList
|
|
26
|
+
addressList,
|
|
27
|
+
selectedShippingOptions
|
|
21
28
|
} = useAppSelector((state: RootState) => state.checkout);
|
|
22
|
-
const {
|
|
23
|
-
|
|
29
|
+
const {
|
|
30
|
+
shipping_option,
|
|
31
|
+
shipping_address,
|
|
32
|
+
attribute_based_shipping_options,
|
|
33
|
+
data_source_shipping_options
|
|
34
|
+
} = preOrder ?? {};
|
|
35
|
+
|
|
36
|
+
const [setShippingOption] = useSetShippingOptionMutation();
|
|
37
|
+
const [setAttributeBasedShippingOptions] =
|
|
38
|
+
useSetAttributeBasedShippingOptionsMutation();
|
|
39
|
+
const [setDataSourceShippingOption] =
|
|
40
|
+
useSetDataSourceShippingOptionsMutation();
|
|
41
|
+
|
|
42
|
+
const prevAttributeBasedOptionsRef = useRef(attributeBasedShippingOptions);
|
|
24
43
|
|
|
25
44
|
const [selectedPks, setSelectedPks] = useState<
|
|
26
45
|
{ dataSourcePk: number; optionPk: number }[] | null
|
|
27
46
|
>(null);
|
|
28
47
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
const initializeSelectedOptions = useCallback(() => {
|
|
49
|
+
if (attribute_based_shipping_options) {
|
|
50
|
+
const newSelected = { ...selectedShippingOptions };
|
|
51
|
+
let hasChanges = false;
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
Object.entries(attributeBasedShippingOptions).forEach(
|
|
54
|
+
([color, options]) => {
|
|
55
|
+
if (
|
|
56
|
+
!newSelected[color] ||
|
|
57
|
+
!options.some((opt) => opt.pk === newSelected[color])
|
|
58
|
+
) {
|
|
59
|
+
newSelected[color] = options[0].pk;
|
|
60
|
+
hasChanges = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (hasChanges) {
|
|
66
|
+
dispatch(setSelectedShippingOptions(newSelected));
|
|
67
|
+
setAttributeBasedShippingOptions(newSelected);
|
|
68
|
+
}
|
|
69
|
+
} else if (shippingOptions.length > 0 && !shipping_option) {
|
|
70
|
+
setShippingOption(shippingOptions[0].pk);
|
|
71
|
+
}
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [
|
|
74
|
+
attributeBasedShippingOptions,
|
|
75
|
+
selectedShippingOptions,
|
|
76
|
+
setAttributeBasedShippingOptions,
|
|
77
|
+
attribute_based_shipping_options,
|
|
78
|
+
shippingOptions,
|
|
79
|
+
shipping_option,
|
|
80
|
+
setShippingOption
|
|
81
|
+
]);
|
|
34
82
|
|
|
35
83
|
useEffect(() => {
|
|
36
84
|
if (!data_source_shipping_options) return;
|
|
@@ -43,6 +91,49 @@ const ShippingOptions = () => {
|
|
|
43
91
|
setSelectedPks(initialSelectedPks);
|
|
44
92
|
}, [data_source_shipping_options]);
|
|
45
93
|
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (
|
|
96
|
+
JSON.stringify(prevAttributeBasedOptionsRef.current) !==
|
|
97
|
+
JSON.stringify(attributeBasedShippingOptions) ||
|
|
98
|
+
Object.keys(selectedShippingOptions).length === 0 ||
|
|
99
|
+
(!shipping_option && shippingOptions.length > 0)
|
|
100
|
+
) {
|
|
101
|
+
initializeSelectedOptions();
|
|
102
|
+
prevAttributeBasedOptionsRef.current = attributeBasedShippingOptions;
|
|
103
|
+
}
|
|
104
|
+
}, [
|
|
105
|
+
attributeBasedShippingOptions,
|
|
106
|
+
selectedShippingOptions,
|
|
107
|
+
initializeSelectedOptions,
|
|
108
|
+
shipping_option,
|
|
109
|
+
shippingOptions
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const handleAttributeBasedOptionChange = useCallback(
|
|
113
|
+
(color: string, newPk: number) => {
|
|
114
|
+
const updatedOptions = { ...selectedShippingOptions, [color]: newPk };
|
|
115
|
+
dispatch(setSelectedShippingOptions(updatedOptions));
|
|
116
|
+
setAttributeBasedShippingOptions(updatedOptions);
|
|
117
|
+
},
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
[selectedShippingOptions, setAttributeBasedShippingOptions]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (addressList.length < 1) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="w-full lg:w-2/5">
|
|
125
|
+
<div className="border-b border-gray-400 px-8 py-4">
|
|
126
|
+
<h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="py-4 px-8">
|
|
129
|
+
<p className="text-xs">
|
|
130
|
+
{t('checkout.address.shipping.select_address_to_continue')}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
46
137
|
const updateData = (dataSourcePk: number, newPk: number) => {
|
|
47
138
|
const updatedSelectedPks = selectedPks?.map((item) =>
|
|
48
139
|
item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
|
|
@@ -106,58 +197,79 @@ const ShippingOptions = () => {
|
|
|
106
197
|
</span>
|
|
107
198
|
</div>
|
|
108
199
|
))}
|
|
109
|
-
<Button
|
|
110
|
-
className="mt-2 w-full"
|
|
111
|
-
disabled={!steps.shipping.completed}
|
|
112
|
-
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
113
|
-
data-testid="checkout-shipping-save"
|
|
114
|
-
>
|
|
115
|
-
{t('checkout.address.shipping.button')}
|
|
116
|
-
</Button>
|
|
117
200
|
</>
|
|
118
201
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
202
|
+
|
|
203
|
+
{attributeBasedShippingOptions &&
|
|
204
|
+
Object.keys(attributeBasedShippingOptions).length > 0 &&
|
|
205
|
+
Object.entries(attributeBasedShippingOptions).map(
|
|
206
|
+
([color, options]) => (
|
|
207
|
+
<div key={color}>
|
|
208
|
+
<h3 className="text-lg font-bold">{color}</h3>
|
|
209
|
+
{options.map((option) => (
|
|
125
210
|
<div
|
|
126
|
-
key={
|
|
211
|
+
key={option.pk}
|
|
127
212
|
className="py-4 border-t border-gray-400 flex justify-between"
|
|
128
213
|
>
|
|
129
214
|
<Radio
|
|
130
|
-
name={`
|
|
131
|
-
checked={
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
) || false
|
|
135
|
-
}
|
|
136
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
137
|
-
handleRadioChange(e, option.pk)
|
|
215
|
+
name={`attribute-based-shipping-${color}`}
|
|
216
|
+
checked={selectedShippingOptions[color] === option.pk}
|
|
217
|
+
onChange={() =>
|
|
218
|
+
handleAttributeBasedOptionChange(color, option.pk)
|
|
138
219
|
}
|
|
139
|
-
|
|
140
|
-
data-testid={`checkout-data-source-shipping-${opt.pk}`}
|
|
220
|
+
data-testid={`checkout-attribute-based-shipping-${option.pk}`}
|
|
141
221
|
>
|
|
142
|
-
{
|
|
222
|
+
{`${option.shipping_option_name}`}
|
|
143
223
|
</Radio>
|
|
144
224
|
<span className="text-xs">
|
|
145
|
-
<Price value={
|
|
225
|
+
<Price value={option.shipping_amount} />
|
|
146
226
|
</span>
|
|
147
227
|
</div>
|
|
148
228
|
))}
|
|
149
229
|
</div>
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
>
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
230
|
+
)
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{dataSourceShippingOptions &&
|
|
234
|
+
dataSourceShippingOptions.length > 0 &&
|
|
235
|
+
dataSourceShippingOptions.map((option) => (
|
|
236
|
+
<div key={option.pk}>
|
|
237
|
+
<h3 className="text-lg font-bold">{option?.name}</h3>
|
|
238
|
+
{option.data_source_shipping_options.map((opt) => (
|
|
239
|
+
<div
|
|
240
|
+
key={opt.pk}
|
|
241
|
+
className="py-4 border-t border-gray-400 flex justify-between"
|
|
242
|
+
>
|
|
243
|
+
<Radio
|
|
244
|
+
name={`data-source-shipping-${option.pk}`}
|
|
245
|
+
checked={
|
|
246
|
+
selectedPks?.some((item) => item.optionPk === opt.pk) ||
|
|
247
|
+
false
|
|
248
|
+
}
|
|
249
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
250
|
+
handleRadioChange(e, option.pk)
|
|
251
|
+
}
|
|
252
|
+
value={opt.pk}
|
|
253
|
+
data-testid={`checkout-data-source-shipping-${opt.pk}`}
|
|
254
|
+
>
|
|
255
|
+
{opt?.shipping_option_name}
|
|
256
|
+
</Radio>
|
|
257
|
+
<span className="text-xs">
|
|
258
|
+
<Price value={opt?.shipping_amount} />
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
|
|
265
|
+
<Button
|
|
266
|
+
className="mt-2 w-full"
|
|
267
|
+
disabled={!steps.shipping.completed}
|
|
268
|
+
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
269
|
+
data-testid="checkout-shipping-save"
|
|
270
|
+
>
|
|
271
|
+
{t('checkout.address.shipping.button')}
|
|
272
|
+
</Button>
|
|
161
273
|
</div>
|
|
162
274
|
)}
|
|
163
275
|
</div>
|