@akinon/projectzero 1.65.0 → 1.67.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 +4 -0
- package/app-template/CHANGELOG.md +63 -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/components/pagination.tsx +13 -18
- 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/options/redirection.tsx +43 -37
- 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
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 1.67.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a1a99f0: ZERO-2887: Add LoaderSpinner to Filters component
|
|
8
|
+
- bc2b411: ZERO-2825: Add attribute-based shipping options to checkout page
|
|
9
|
+
- 3d2212a: ZERO-2745: Add multi basket support
|
|
10
|
+
- 38a634e: ZERO-2893: Refactor category filter handling and URL parameters
|
|
11
|
+
- 9e25a64: ZERO-2835: Update category page layout with breadcrumb
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [bc2b411]
|
|
16
|
+
- Updated dependencies [3d2212a]
|
|
17
|
+
- Updated dependencies [8f47cca]
|
|
18
|
+
- Updated dependencies [9e25a64]
|
|
19
|
+
- @akinon/next@1.67.0
|
|
20
|
+
- @akinon/pz-akifast@1.67.0
|
|
21
|
+
- @akinon/pz-b2b@1.67.0
|
|
22
|
+
- @akinon/pz-basket-gift-pack@1.67.0
|
|
23
|
+
- @akinon/pz-bkm@1.67.0
|
|
24
|
+
- @akinon/pz-checkout-gift-pack@1.67.0
|
|
25
|
+
- @akinon/pz-click-collect@1.67.0
|
|
26
|
+
- @akinon/pz-credit-payment@1.67.0
|
|
27
|
+
- @akinon/pz-gpay@1.67.0
|
|
28
|
+
- @akinon/pz-masterpass@1.67.0
|
|
29
|
+
- @akinon/pz-one-click-checkout@1.67.0
|
|
30
|
+
- @akinon/pz-otp@1.67.0
|
|
31
|
+
- @akinon/pz-pay-on-delivery@1.67.0
|
|
32
|
+
- @akinon/pz-saved-card@1.67.0
|
|
33
|
+
- @akinon/pz-tabby-extension@1.67.0
|
|
34
|
+
|
|
35
|
+
## 1.66.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- 572d2e8: ZERO-2667: Add iframe support for redirection payment methods
|
|
40
|
+
- 2e6104d: ZERO-2888:Edit the numbering in the pagination and the visibility of the prev and next buttons
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [572d2e8]
|
|
45
|
+
- Updated dependencies [2e6104d]
|
|
46
|
+
- Updated dependencies [7b05522]
|
|
47
|
+
- Updated dependencies [29ead87]
|
|
48
|
+
- Updated dependencies [12a873e]
|
|
49
|
+
- Updated dependencies [f2c325c]
|
|
50
|
+
- @akinon/next@1.66.0
|
|
51
|
+
- @akinon/pz-otp@1.66.0
|
|
52
|
+
- @akinon/pz-akifast@1.66.0
|
|
53
|
+
- @akinon/pz-b2b@1.66.0
|
|
54
|
+
- @akinon/pz-basket-gift-pack@1.66.0
|
|
55
|
+
- @akinon/pz-bkm@1.66.0
|
|
56
|
+
- @akinon/pz-checkout-gift-pack@1.66.0
|
|
57
|
+
- @akinon/pz-click-collect@1.66.0
|
|
58
|
+
- @akinon/pz-credit-payment@1.66.0
|
|
59
|
+
- @akinon/pz-gpay@1.66.0
|
|
60
|
+
- @akinon/pz-masterpass@1.66.0
|
|
61
|
+
- @akinon/pz-one-click-checkout@1.66.0
|
|
62
|
+
- @akinon/pz-pay-on-delivery@1.66.0
|
|
63
|
+
- @akinon/pz-saved-card@1.66.0
|
|
64
|
+
- @akinon/pz-tabby-extension@1.66.0
|
|
65
|
+
|
|
3
66
|
## 1.65.0
|
|
4
67
|
|
|
5
68
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.67.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.67.0",
|
|
26
|
+
"@akinon/pz-akifast": "1.67.0",
|
|
27
|
+
"@akinon/pz-b2b": "1.67.0",
|
|
28
|
+
"@akinon/pz-basket-gift-pack": "1.67.0",
|
|
29
|
+
"@akinon/pz-bkm": "1.67.0",
|
|
30
|
+
"@akinon/pz-checkout-gift-pack": "1.67.0",
|
|
31
|
+
"@akinon/pz-click-collect": "1.67.0",
|
|
32
|
+
"@akinon/pz-credit-payment": "1.67.0",
|
|
33
|
+
"@akinon/pz-gpay": "1.67.0",
|
|
34
|
+
"@akinon/pz-masterpass": "1.67.0",
|
|
35
|
+
"@akinon/pz-one-click-checkout": "1.67.0",
|
|
36
|
+
"@akinon/pz-otp": "1.67.0",
|
|
37
|
+
"@akinon/pz-pay-on-delivery": "1.67.0",
|
|
38
|
+
"@akinon/pz-saved-card": "1.67.0",
|
|
39
|
+
"@akinon/pz-tabby-extension": "1.67.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.67.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
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { PaginationProps } from '@theme/components/types';
|
|
3
3
|
import { twMerge } from 'tailwind-merge';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import {
|
|
5
|
+
import { Button } from '@theme/components';
|
|
6
6
|
import usePagination from '@akinon/next/hooks/use-pagination';
|
|
7
7
|
import { useLocalization } from '@akinon/next/hooks';
|
|
8
8
|
import { useRouter } from '@akinon/next/hooks';
|
|
@@ -92,9 +92,7 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
92
92
|
}
|
|
93
93
|
}, [numberOfPages, page, pageList, threshold]);
|
|
94
94
|
|
|
95
|
-
const handleClick = (
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
|
|
95
|
+
const handleClick = (url: string) => {
|
|
98
96
|
const newUrl = new URL(url, window.location.origin);
|
|
99
97
|
const page = newUrl.searchParams.get('page');
|
|
100
98
|
|
|
@@ -129,7 +127,7 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
129
127
|
if (type === 'list') {
|
|
130
128
|
createListItems();
|
|
131
129
|
}
|
|
132
|
-
}, []);
|
|
130
|
+
}, [createListItems, type]);
|
|
133
131
|
|
|
134
132
|
useEffect(() => {
|
|
135
133
|
if (total && total !== paginationTotal) {
|
|
@@ -200,9 +198,8 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
200
198
|
>
|
|
201
199
|
{prev && currentPage !== 1 && (
|
|
202
200
|
<li>
|
|
203
|
-
<
|
|
204
|
-
onClick={(
|
|
205
|
-
href={prev}
|
|
201
|
+
<button
|
|
202
|
+
onClick={() => handleClick(prev)}
|
|
206
203
|
className={twMerge(
|
|
207
204
|
'flex cursor-pointer text-sm px-2',
|
|
208
205
|
prevClassName
|
|
@@ -212,16 +209,15 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
212
209
|
<span className="hidden lg:inline-block ms-4">
|
|
213
210
|
{t('category.pagination.previous')}
|
|
214
211
|
</span>
|
|
215
|
-
</
|
|
212
|
+
</button>
|
|
216
213
|
</li>
|
|
217
214
|
)}
|
|
218
215
|
|
|
219
216
|
{paginationItems.map((item, i) => (
|
|
220
217
|
<li key={i}>
|
|
221
218
|
{item?.url != '#' ? (
|
|
222
|
-
<
|
|
223
|
-
onClick={(
|
|
224
|
-
href={item.url}
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => handleClick(item.url)}
|
|
225
221
|
className={twMerge(
|
|
226
222
|
clsx(
|
|
227
223
|
'text-xs px-2 cursor-pointer',
|
|
@@ -234,7 +230,7 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
234
230
|
)}
|
|
235
231
|
>
|
|
236
232
|
{item?.page}
|
|
237
|
-
</
|
|
233
|
+
</button>
|
|
238
234
|
) : (
|
|
239
235
|
<span className="cursor-default text-xs flex items-center justify-center">
|
|
240
236
|
{item?.page}
|
|
@@ -245,9 +241,8 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
245
241
|
|
|
246
242
|
{showNext && (
|
|
247
243
|
<li>
|
|
248
|
-
<
|
|
249
|
-
onClick={(
|
|
250
|
-
href={next}
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => handleClick(next)}
|
|
251
246
|
className={twMerge(
|
|
252
247
|
'flex cursor-pointer text-xs px-2',
|
|
253
248
|
nextClassName
|
|
@@ -257,7 +252,7 @@ export const Pagination = (props: PaginationProps) => {
|
|
|
257
252
|
{t('category.pagination.next')}
|
|
258
253
|
</span>
|
|
259
254
|
<span>></span>
|
|
260
|
-
</
|
|
255
|
+
</button>
|
|
261
256
|
</li>
|
|
262
257
|
)}
|
|
263
258
|
</ul>
|
|
@@ -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>
|
|
@@ -9,6 +9,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
9
9
|
import * as yup from 'yup';
|
|
10
10
|
import { useEffect, useState } from 'react';
|
|
11
11
|
import { getPosError } from '@akinon/next/utils';
|
|
12
|
+
import { useMessageListener } from '@akinon/next/hooks';
|
|
12
13
|
|
|
13
14
|
interface FormValues {
|
|
14
15
|
agreement: boolean;
|
|
@@ -25,7 +26,6 @@ const formSchema = () =>
|
|
|
25
26
|
export default function RedirectionPayment() {
|
|
26
27
|
const { payment_option } = useAppSelector((state) => state.checkout.preOrder);
|
|
27
28
|
const [formError, setFormError] = useState(null);
|
|
28
|
-
|
|
29
29
|
const {
|
|
30
30
|
register,
|
|
31
31
|
handleSubmit,
|
|
@@ -34,11 +34,12 @@ export default function RedirectionPayment() {
|
|
|
34
34
|
resolver: yupResolver(formSchema())
|
|
35
35
|
});
|
|
36
36
|
const [completeRedirectionPayment] = useCompleteRedirectionPaymentMutation();
|
|
37
|
-
|
|
38
37
|
const onSubmit = async () => {
|
|
39
38
|
completeRedirectionPayment();
|
|
40
39
|
};
|
|
41
40
|
|
|
41
|
+
useMessageListener();
|
|
42
|
+
|
|
42
43
|
useEffect(() => {
|
|
43
44
|
const posErrors = getPosError();
|
|
44
45
|
|
|
@@ -48,44 +49,49 @@ export default function RedirectionPayment() {
|
|
|
48
49
|
}, []);
|
|
49
50
|
|
|
50
51
|
return (
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<p className="px-4 md:px-0">
|
|
57
|
-
You can quickly and easily pay and complete your order with{' '}
|
|
58
|
-
{payment_option.name}.
|
|
59
|
-
</p>
|
|
60
|
-
|
|
61
|
-
<Checkbox
|
|
62
|
-
className="px-4 md:px-0"
|
|
63
|
-
{...register('agreement')}
|
|
64
|
-
error={errors.agreement}
|
|
52
|
+
<div className="checkout-redirection-payment-wrapper">
|
|
53
|
+
<form
|
|
54
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
55
|
+
className="lg-5 space-y-5 lg:p-10"
|
|
65
56
|
>
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
<h1 className="text-2xl font-bold px-4 md:px-0">
|
|
58
|
+
Pay With {payment_option.name}
|
|
59
|
+
</h1>
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<div
|
|
79
|
-
className="w-full text-xs text-start px-1 mt-3 text-error"
|
|
80
|
-
data-testid="checkout-form-error"
|
|
61
|
+
<p className="px-4 md:px-0">
|
|
62
|
+
You can quickly and easily pay and complete your order with{' '}
|
|
63
|
+
{payment_option.name}.
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<Checkbox
|
|
67
|
+
className="px-4 md:px-0"
|
|
68
|
+
{...register('agreement')}
|
|
69
|
+
error={errors.agreement}
|
|
81
70
|
>
|
|
82
|
-
|
|
83
|
-
</
|
|
84
|
-
|
|
71
|
+
Check here to indicate that you have read and agree to the all terms.
|
|
72
|
+
</Checkbox>
|
|
73
|
+
|
|
74
|
+
{formError?.non_field_errors && (
|
|
75
|
+
<div
|
|
76
|
+
className="w-full text-xs text-start px-1 mt-3 text-error"
|
|
77
|
+
data-testid="checkout-form-error"
|
|
78
|
+
>
|
|
79
|
+
{formError.non_field_errors}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{formError?.status && (
|
|
83
|
+
<div
|
|
84
|
+
className="w-full text-xs text-start px-1 mt-3 text-error"
|
|
85
|
+
data-testid="checkout-form-error"
|
|
86
|
+
>
|
|
87
|
+
{formError.status}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
<Button className={twMerge('w-full md:w-36 px-4 md:px-0')}>
|
|
92
|
+
{payment_option.name}
|
|
93
|
+
</Button>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
90
96
|
);
|
|
91
97
|
}
|
|
@@ -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>
|