@akinon/projectzero 1.55.0 → 1.56.0-rc.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 +37 -0
- package/app-template/.gitignore +2 -0
- package/app-template/CHANGELOG.md +1871 -59
- package/app-template/package.json +17 -18
- package/app-template/public/locales/en/account.json +4 -4
- package/app-template/public/locales/tr/account.json +1 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/[...prettyurl]/page.tsx +8 -0
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/coupons/page.tsx +4 -4
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/profile/page.tsx +1 -0
- package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +5 -2
- package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx +12 -8
- package/app-template/src/components/checkbox.tsx +2 -2
- package/app-template/src/components/input.tsx +19 -7
- package/app-template/src/components/pagination.tsx +13 -18
- package/app-template/src/components/price.tsx +9 -4
- package/app-template/src/redux/reducers/category.ts +7 -1
- package/app-template/src/settings.js +6 -1
- package/app-template/src/views/account/address-form.tsx +12 -2
- package/app-template/src/views/account/contact-form.tsx +23 -6
- package/app-template/src/views/account/favourite-products/favourite-products-list.tsx +5 -1
- 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/category-info.tsx +31 -17
- package/app-template/src/views/category/filters/filter-item.tsx +163 -0
- package/app-template/src/views/category/filters/index.tsx +16 -108
- 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 +230 -34
- package/app-template/src/views/header/mobile-menu.tsx +25 -8
- package/app-template/tsconfig.json +14 -4
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
import { useSearchParams } from 'next/navigation';
|
|
6
6
|
import { CategoryHeader } from './category-header';
|
|
@@ -8,13 +8,14 @@ import { Filters } from './filters';
|
|
|
8
8
|
import { Pagination } from '@theme/components';
|
|
9
9
|
import { ProductItem } from '@theme/views/product-item';
|
|
10
10
|
import { GetCategoryResponse } from '@akinon/next/types';
|
|
11
|
-
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
12
|
-
import { setFacets } from '@theme/redux/reducers/category';
|
|
11
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
12
|
+
import { setFacets, setMenuOpen } from '@theme/redux/reducers/category';
|
|
13
13
|
import CategoryActiveFilters from '@theme/views/category/category-active-filters';
|
|
14
14
|
import { useLocalization } from '@akinon/next/hooks';
|
|
15
15
|
import { Link, LoaderSpinner } from '@akinon/next/components';
|
|
16
16
|
import { ROUTES } from '@theme/routes';
|
|
17
17
|
import { useRouter } from '@akinon/next/hooks';
|
|
18
|
+
import { RootState } from '@theme/redux/store';
|
|
18
19
|
|
|
19
20
|
interface ListPageProps {
|
|
20
21
|
data: GetCategoryResponse;
|
|
@@ -22,13 +23,16 @@ interface ListPageProps {
|
|
|
22
23
|
|
|
23
24
|
export default function ListPage(props: ListPageProps) {
|
|
24
25
|
const { data } = props;
|
|
25
|
-
const
|
|
26
|
+
const dispatch = useAppDispatch();
|
|
27
|
+
const isMenuOpen = useAppSelector(
|
|
28
|
+
(state: RootState) => state.category.isMenuOpen
|
|
29
|
+
);
|
|
26
30
|
|
|
27
31
|
const searchParams = useSearchParams();
|
|
28
32
|
const router = useRouter();
|
|
29
33
|
|
|
30
34
|
const layoutSize = useMemo(
|
|
31
|
-
() => searchParams.get('layout') ?? 3,
|
|
35
|
+
() => Number(searchParams.get('layout') ?? 3),
|
|
32
36
|
[searchParams]
|
|
33
37
|
);
|
|
34
38
|
|
|
@@ -37,16 +41,24 @@ export default function ListPage(props: ListPageProps) {
|
|
|
37
41
|
[searchParams]
|
|
38
42
|
);
|
|
39
43
|
|
|
44
|
+
const itemDimensions = useMemo(() => {
|
|
45
|
+
switch (layoutSize) {
|
|
46
|
+
case 2:
|
|
47
|
+
return { width: 510, height: 765 };
|
|
48
|
+
case 3:
|
|
49
|
+
default:
|
|
50
|
+
return { width: 340, height: 510 };
|
|
51
|
+
}
|
|
52
|
+
}, [layoutSize]);
|
|
53
|
+
|
|
40
54
|
useEffect(() => {
|
|
41
55
|
if (page > 1 && data.products?.length === 0) {
|
|
42
56
|
const newUrl = new URL(window.location.href);
|
|
43
|
-
|
|
44
57
|
newUrl.searchParams.delete('page');
|
|
45
58
|
router.push(newUrl.pathname + newUrl.search, undefined);
|
|
46
59
|
}
|
|
47
|
-
}, [searchParams, data.products, page]);
|
|
60
|
+
}, [searchParams, data.products, page]);
|
|
48
61
|
|
|
49
|
-
const dispatch = useAppDispatch();
|
|
50
62
|
const { t } = useLocalization();
|
|
51
63
|
|
|
52
64
|
useEffect(() => {
|
|
@@ -58,9 +70,12 @@ export default function ListPage(props: ListPageProps) {
|
|
|
58
70
|
<>
|
|
59
71
|
<div className="container px-4 mx-auto lg:px-0 lg:my-4">
|
|
60
72
|
<div className="grid grid-cols-[19rem_1fr]">
|
|
61
|
-
<Filters
|
|
73
|
+
<Filters
|
|
74
|
+
isMenuOpen={isMenuOpen}
|
|
75
|
+
setIsMenuOpen={(open) => dispatch(setMenuOpen(open))}
|
|
76
|
+
/>
|
|
62
77
|
<div
|
|
63
|
-
onClick={() =>
|
|
78
|
+
onClick={() => dispatch(setMenuOpen(false))}
|
|
64
79
|
className={clsx(
|
|
65
80
|
'transition-opacity duration-300 ease-linear lg:hidden',
|
|
66
81
|
isMenuOpen
|
|
@@ -71,7 +86,7 @@ export default function ListPage(props: ListPageProps) {
|
|
|
71
86
|
<div className="flex flex-col items-center lg:items-stretch col-span-2 lg:col-span-1">
|
|
72
87
|
<CategoryHeader
|
|
73
88
|
totalCount={data.pagination?.total_count}
|
|
74
|
-
setMenuStatus={() =>
|
|
89
|
+
setMenuStatus={() => dispatch(setMenuOpen(true))}
|
|
75
90
|
sortOptions={data.sorters}
|
|
76
91
|
/>
|
|
77
92
|
<div className="hidden lg:block">
|
|
@@ -91,18 +106,17 @@ export default function ListPage(props: ListPageProps) {
|
|
|
91
106
|
|
|
92
107
|
<div
|
|
93
108
|
className={clsx('grid gap-x-4 gap-y-12 grid-cols-2', {
|
|
94
|
-
'md:grid-cols-3':
|
|
95
|
-
'lg:grid-cols-2':
|
|
96
|
-
'lg:grid-cols-3':
|
|
109
|
+
'md:grid-cols-3': layoutSize === 3,
|
|
110
|
+
'lg:grid-cols-2': layoutSize === 2,
|
|
111
|
+
'lg:grid-cols-3': layoutSize === 3
|
|
97
112
|
})}
|
|
98
113
|
>
|
|
99
114
|
{data.products.map((product, index) => (
|
|
100
115
|
<ProductItem
|
|
101
116
|
key={product.pk}
|
|
102
117
|
product={product}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
height={510}
|
|
118
|
+
width={itemDimensions.width}
|
|
119
|
+
height={itemDimensions.height}
|
|
106
120
|
index={index}
|
|
107
121
|
/>
|
|
108
122
|
))}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
3
|
+
import { Facet, FacetChoice } from '@akinon/next/types';
|
|
4
|
+
import { Accordion, Radio, Checkbox, LoaderSpinner } from '../../../components';
|
|
5
|
+
import { WIDGET_TYPE } from '../../../types';
|
|
6
|
+
import { SizeFilter } from './size-filter';
|
|
7
|
+
import { toggleFacet } from '@theme/redux/reducers/category';
|
|
8
|
+
import { commonProductAttributes } from '@theme/settings';
|
|
9
|
+
import { useRouter } from '@akinon/next/hooks';
|
|
10
|
+
import { usePathname } from 'next/navigation';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
|
|
13
|
+
const COMPONENT_TYPES = {
|
|
14
|
+
[WIDGET_TYPE.category]: Radio,
|
|
15
|
+
[WIDGET_TYPE.multiselect]: Checkbox
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const sizeKey = commonProductAttributes.find(
|
|
19
|
+
(item) => item.translationKey === 'size'
|
|
20
|
+
).key;
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
facet: Facet;
|
|
24
|
+
isPending: boolean;
|
|
25
|
+
startTransition: (callback: () => void) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sortByPredefinedOrder = (
|
|
29
|
+
aLabel: string,
|
|
30
|
+
bLabel: string,
|
|
31
|
+
order: string[]
|
|
32
|
+
) => {
|
|
33
|
+
const aIndex = order.indexOf(aLabel);
|
|
34
|
+
const bIndex = order.indexOf(bLabel);
|
|
35
|
+
|
|
36
|
+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
37
|
+
if (aIndex !== -1) return -1;
|
|
38
|
+
if (bIndex !== -1) return 1;
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const sortByNumericValue = (aLabel: string, bLabel: string) => {
|
|
44
|
+
const aNum = parseInt(aLabel, 10);
|
|
45
|
+
const bNum = parseInt(bLabel, 10);
|
|
46
|
+
|
|
47
|
+
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
|
|
48
|
+
if (!isNaN(aNum)) return -1;
|
|
49
|
+
if (!isNaN(bNum)) return 1;
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const sortChoices = (
|
|
55
|
+
facetKey: string,
|
|
56
|
+
choices: FacetChoice[]
|
|
57
|
+
): FacetChoice[] => {
|
|
58
|
+
if (facetKey === sizeKey) {
|
|
59
|
+
const order = ['xs', 's', 'm', 'l', 'xl'];
|
|
60
|
+
|
|
61
|
+
return choices.sort((a, b) => {
|
|
62
|
+
const aLabel = a.label.toLowerCase();
|
|
63
|
+
const bLabel = b.label.toLowerCase();
|
|
64
|
+
|
|
65
|
+
const orderComparison = sortByPredefinedOrder(aLabel, bLabel, order);
|
|
66
|
+
if (orderComparison !== null) return orderComparison;
|
|
67
|
+
|
|
68
|
+
const numericComparison = sortByNumericValue(aLabel, bLabel);
|
|
69
|
+
if (numericComparison !== null) return numericComparison;
|
|
70
|
+
|
|
71
|
+
return aLabel.localeCompare(bLabel);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return choices;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
|
|
79
|
+
if (facetKey === sizeKey) {
|
|
80
|
+
return SizeFilter;
|
|
81
|
+
}
|
|
82
|
+
return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const FilterItem = ({ facet, isPending, startTransition }: Props) => {
|
|
86
|
+
const dispatch = useAppDispatch();
|
|
87
|
+
const router = useRouter();
|
|
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
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const Component = getComponentByWidgetType(facet.widget_type, facet.key);
|
|
115
|
+
const choices = sortChoices(facet.key, [...facet.data.choices]);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Accordion
|
|
119
|
+
key={facet.key}
|
|
120
|
+
title={facet.name}
|
|
121
|
+
isCollapse={choices.some((choice) => choice.is_selected)}
|
|
122
|
+
dataTestId={`filter-${facet.name}`}
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
className={clsx('flex gap-4', {
|
|
126
|
+
'flex-wrap flex-row': facet.key === sizeKey,
|
|
127
|
+
'flex-col': facet.key !== sizeKey
|
|
128
|
+
})}
|
|
129
|
+
>
|
|
130
|
+
{choices.map((choice, index) => (
|
|
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}
|
|
143
|
+
>
|
|
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>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
</Accordion>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { WIDGET_TYPE } from '@theme/types';
|
|
4
3
|
import clsx from 'clsx';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import { SizeFilter } from './size-filter';
|
|
8
|
-
|
|
9
|
-
import { useLocalization, useRouter } from '@akinon/next/hooks';
|
|
10
|
-
import { Facet, FacetChoice } from '@akinon/next/types';
|
|
4
|
+
import { Button, Icon } from '@theme/components';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
11
6
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
12
|
-
import {
|
|
13
|
-
resetSelectedFacets,
|
|
14
|
-
toggleFacet
|
|
15
|
-
} from '@theme/redux/reducers/category';
|
|
7
|
+
import { resetSelectedFacets } from '@theme/redux/reducers/category';
|
|
16
8
|
import CategoryActiveFilters from '@theme/views/category/category-active-filters';
|
|
17
|
-
import { useMemo } from 'react';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
const COMPONENT_TYPES = {
|
|
21
|
-
[WIDGET_TYPE.category]: Radio,
|
|
22
|
-
[WIDGET_TYPE.multiselect]: Checkbox
|
|
23
|
-
};
|
|
9
|
+
import { useMemo, useState, useTransition } from 'react';
|
|
10
|
+
import { FilterItem } from './filter-item';
|
|
24
11
|
|
|
25
12
|
interface Props {
|
|
26
13
|
isMenuOpen: boolean;
|
|
@@ -28,37 +15,16 @@ interface Props {
|
|
|
28
15
|
}
|
|
29
16
|
|
|
30
17
|
export const Filters = (props: Props) => {
|
|
31
|
-
const router = useRouter();
|
|
32
18
|
const facets = useAppSelector((state) => state.category.facets);
|
|
33
19
|
const dispatch = useAppDispatch();
|
|
34
20
|
const { t } = useLocalization();
|
|
35
21
|
const { isMenuOpen, setIsMenuOpen } = props;
|
|
36
22
|
|
|
37
|
-
const
|
|
38
|
-
facet,
|
|
39
|
-
choice
|
|
40
|
-
}: {
|
|
41
|
-
facet: Facet;
|
|
42
|
-
choice: FacetChoice;
|
|
43
|
-
}) => {
|
|
44
|
-
if (facet.key === 'category_ids') {
|
|
45
|
-
router.push(choice.url);
|
|
46
|
-
} else {
|
|
47
|
-
dispatch(
|
|
48
|
-
toggleFacet({
|
|
49
|
-
facet,
|
|
50
|
-
choice
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
23
|
+
const [isPending, startTransition] = useTransition();
|
|
55
24
|
|
|
56
25
|
const haveFilter = useMemo(() => {
|
|
57
|
-
return (
|
|
58
|
-
|
|
59
|
-
(facet) =>
|
|
60
|
-
facet.data.choices.filter((choice) => choice.is_selected).length > 0
|
|
61
|
-
).length > 0
|
|
26
|
+
return facets.some((facet) =>
|
|
27
|
+
facet.data.choices.some((choice) => choice.is_selected)
|
|
62
28
|
);
|
|
63
29
|
}, [facets]);
|
|
64
30
|
|
|
@@ -66,10 +32,6 @@ export const Filters = (props: Props) => {
|
|
|
66
32
|
dispatch(resetSelectedFacets());
|
|
67
33
|
};
|
|
68
34
|
|
|
69
|
-
const sizeKey = commonProductAttributes.find(
|
|
70
|
-
(item) => item.translationKey === 'size'
|
|
71
|
-
).key;
|
|
72
|
-
|
|
73
35
|
return (
|
|
74
36
|
<div
|
|
75
37
|
className={clsx(
|
|
@@ -87,76 +49,22 @@ export const Filters = (props: Props) => {
|
|
|
87
49
|
<span className="text-sm">1 {t('category.filters.results')}</span>
|
|
88
50
|
<span>{t('category.filters.ready_to_wear')}</span>
|
|
89
51
|
</div>
|
|
90
|
-
{facets.map((facet) => {
|
|
91
|
-
let Component = null;
|
|
92
|
-
const choices = [...facet.data.choices];
|
|
93
|
-
|
|
94
|
-
if (facet.key === sizeKey) {
|
|
95
|
-
// If it's a size facet, use the custom size filter component
|
|
96
|
-
Component = SizeFilter;
|
|
97
|
-
|
|
98
|
-
const order = ['xs', 's', 'm', 'l', 'xl'];
|
|
99
|
-
choices.sort((a, b) => {
|
|
100
|
-
return (
|
|
101
|
-
order.indexOf(a.label.toLowerCase()) -
|
|
102
|
-
order.indexOf(b.label.toLowerCase())
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
} else {
|
|
106
|
-
Component =
|
|
107
|
-
COMPONENT_TYPES[facet.widget_type] ||
|
|
108
|
-
COMPONENT_TYPES[WIDGET_TYPE.category];
|
|
109
|
-
}
|
|
110
52
|
|
|
53
|
+
{facets.map((facet) => {
|
|
111
54
|
return (
|
|
112
|
-
<
|
|
55
|
+
<FilterItem
|
|
113
56
|
key={facet.key}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<div
|
|
119
|
-
className={clsx(
|
|
120
|
-
'flex gap-4 flex-wrap',
|
|
121
|
-
facet.key === sizeKey ? 'flex-row' : 'flex-col' // TODO: This condition must be refactor to a better way
|
|
122
|
-
)}
|
|
123
|
-
>
|
|
124
|
-
{choices.map((choice, index) => (
|
|
125
|
-
<Component // TODO: This dynamic component can be a hook or higher order component so it props can be standardized
|
|
126
|
-
key={choice.label}
|
|
127
|
-
data={choice}
|
|
128
|
-
name={facet.key}
|
|
129
|
-
onChange={() => {
|
|
130
|
-
if (facet.key !== sizeKey) {
|
|
131
|
-
// TODO: This condition must be refactor to a better way
|
|
132
|
-
handleSelectFilter({ facet, choice });
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
|
-
onClick={() => {
|
|
136
|
-
if (facet.key === sizeKey) {
|
|
137
|
-
// TODO: This condition must be refactor to a better way
|
|
138
|
-
handleSelectFilter({ facet, choice });
|
|
139
|
-
}
|
|
140
|
-
}}
|
|
141
|
-
checked={choice.is_selected}
|
|
142
|
-
data-testid={`${choice.label.trim()}`}
|
|
143
|
-
>
|
|
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
|
-
</div>
|
|
154
|
-
</Accordion>
|
|
57
|
+
facet={facet}
|
|
58
|
+
isPending={isPending}
|
|
59
|
+
startTransition={startTransition}
|
|
60
|
+
/>
|
|
155
61
|
);
|
|
156
62
|
})}
|
|
63
|
+
|
|
157
64
|
<div className="lg:hidden">
|
|
158
65
|
<CategoryActiveFilters />
|
|
159
66
|
</div>
|
|
67
|
+
|
|
160
68
|
{haveFilter && (
|
|
161
69
|
<div className="lg:hidden">
|
|
162
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)}
|