@akinon/projectzero 1.63.0 → 1.64.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 +2 -0
- package/app-template/CHANGELOG.md +31 -0
- package/app-template/package.json +17 -17
- package/app-template/src/components/checkbox.tsx +2 -2
- package/app-template/src/redux/reducers/category.ts +7 -1
- package/app-template/src/views/account/address-form.tsx +12 -2
- package/app-template/src/views/category/category-info.tsx +31 -17
- package/app-template/src/views/category/filters/filter-item.tsx +131 -0
- package/app-template/src/views/category/filters/index.tsx +5 -105
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +130 -32
- package/app-template/src/views/header/mobile-menu.tsx +25 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 1.64.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c53ea3e: ZERO-2609: Reset additional form fields when selectedFormType is not company
|
|
8
|
+
- 27a5296: ZERO-2631:Fix Checkbox Click
|
|
9
|
+
- e665a0a: ZERO-2625: Add click outside functionality to close mobile menu
|
|
10
|
+
- d3474c6: ZERO-2655: Add data source shipping option
|
|
11
|
+
- 7a4bb76: ZERO-2610:Refactor FilterItem component for better readability and efficiency
|
|
12
|
+
- fac2e5b: ZERO-2622: Add isMenuOpen state to Category reducer and update CategoryInfo component
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [c53ea3e]
|
|
17
|
+
- Updated dependencies [d3474c6]
|
|
18
|
+
- @akinon/next@1.64.0
|
|
19
|
+
- @akinon/pz-akifast@1.64.0
|
|
20
|
+
- @akinon/pz-b2b@1.64.0
|
|
21
|
+
- @akinon/pz-basket-gift-pack@1.64.0
|
|
22
|
+
- @akinon/pz-bkm@1.64.0
|
|
23
|
+
- @akinon/pz-checkout-gift-pack@1.64.0
|
|
24
|
+
- @akinon/pz-click-collect@1.64.0
|
|
25
|
+
- @akinon/pz-credit-payment@1.64.0
|
|
26
|
+
- @akinon/pz-gpay@1.64.0
|
|
27
|
+
- @akinon/pz-masterpass@1.64.0
|
|
28
|
+
- @akinon/pz-one-click-checkout@1.64.0
|
|
29
|
+
- @akinon/pz-otp@1.64.0
|
|
30
|
+
- @akinon/pz-pay-on-delivery@1.64.0
|
|
31
|
+
- @akinon/pz-saved-card@1.64.0
|
|
32
|
+
- @akinon/pz-tabby-extension@1.64.0
|
|
33
|
+
|
|
3
34
|
## 1.63.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.64.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.64.0",
|
|
26
|
+
"@akinon/pz-akifast": "1.64.0",
|
|
27
|
+
"@akinon/pz-b2b": "1.64.0",
|
|
28
|
+
"@akinon/pz-basket-gift-pack": "1.64.0",
|
|
29
|
+
"@akinon/pz-bkm": "1.64.0",
|
|
30
|
+
"@akinon/pz-checkout-gift-pack": "1.64.0",
|
|
31
|
+
"@akinon/pz-click-collect": "1.64.0",
|
|
32
|
+
"@akinon/pz-credit-payment": "1.64.0",
|
|
33
|
+
"@akinon/pz-gpay": "1.64.0",
|
|
34
|
+
"@akinon/pz-masterpass": "1.64.0",
|
|
35
|
+
"@akinon/pz-one-click-checkout": "1.64.0",
|
|
36
|
+
"@akinon/pz-otp": "1.64.0",
|
|
37
|
+
"@akinon/pz-pay-on-delivery": "1.64.0",
|
|
38
|
+
"@akinon/pz-saved-card": "1.64.0",
|
|
39
|
+
"@akinon/pz-tabby-extension": "1.64.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",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"yup": "0.32.11"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@akinon/eslint-plugin-projectzero": "1.
|
|
64
|
+
"@akinon/eslint-plugin-projectzero": "1.64.0",
|
|
65
65
|
"@semantic-release/changelog": "6.0.2",
|
|
66
66
|
"@semantic-release/exec": "6.0.3",
|
|
67
67
|
"@semantic-release/git": "10.0.1",
|
|
@@ -3,7 +3,7 @@ import { CheckboxProps } from '@theme/components/types';
|
|
|
3
3
|
import { twMerge } from 'tailwind-merge';
|
|
4
4
|
|
|
5
5
|
const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
|
|
6
|
-
const { children, checked, error, ...rest } = props;
|
|
6
|
+
const { children, checked = false, error, ...rest } = props;
|
|
7
7
|
|
|
8
8
|
return (
|
|
9
9
|
<label className={twMerge('flex flex-col text-xs', props.className)}>
|
|
@@ -12,7 +12,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
|
|
|
12
12
|
type="checkbox"
|
|
13
13
|
{...rest}
|
|
14
14
|
ref={ref}
|
|
15
|
-
|
|
15
|
+
defaultChecked={checked}
|
|
16
16
|
className="w-4 h-4 shrink-0"
|
|
17
17
|
/>
|
|
18
18
|
{children && <span className="ml-2">{children}</span>}
|
|
@@ -7,11 +7,13 @@ import { WIDGET_TYPE } from '@theme/types';
|
|
|
7
7
|
export interface CategoryState {
|
|
8
8
|
facets: Facet[];
|
|
9
9
|
selectedFacets: Facet[];
|
|
10
|
+
isMenuOpen: boolean;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const initialState: CategoryState = {
|
|
13
14
|
facets: [],
|
|
14
|
-
selectedFacets: []
|
|
15
|
+
selectedFacets: [],
|
|
16
|
+
isMenuOpen: false
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
const categorySlice = createSlice({
|
|
@@ -24,6 +26,9 @@ const categorySlice = createSlice({
|
|
|
24
26
|
setSelectedFacets(state, action) {
|
|
25
27
|
state.selectedFacets = action.payload;
|
|
26
28
|
},
|
|
29
|
+
setMenuOpen(state, action) {
|
|
30
|
+
state.isMenuOpen = action.payload;
|
|
31
|
+
},
|
|
27
32
|
toggleFacet(state, action) {
|
|
28
33
|
const facets = JSON.parse(JSON.stringify(state.facets));
|
|
29
34
|
|
|
@@ -81,6 +86,7 @@ const categorySlice = createSlice({
|
|
|
81
86
|
export const {
|
|
82
87
|
setFacets,
|
|
83
88
|
setSelectedFacets,
|
|
89
|
+
setMenuOpen,
|
|
84
90
|
toggleFacet,
|
|
85
91
|
removeCategoryFacet,
|
|
86
92
|
resetSelectedFacets
|
|
@@ -183,12 +183,22 @@ export const AddressForm = (props: Props) => {
|
|
|
183
183
|
if (data && country) {
|
|
184
184
|
reset({
|
|
185
185
|
...data,
|
|
186
|
-
is_corporate:
|
|
187
|
-
String(data.is_corporate) === AddressType.company ? 'true' : 'false' // TODO: Fix this! This hack for radio buttons can't be set to boolean value
|
|
186
|
+
is_corporate: String(data.is_corporate)
|
|
188
187
|
});
|
|
189
188
|
}
|
|
190
189
|
}, [data, country, reset]);
|
|
191
190
|
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (selectedFormType !== AddressType.company) {
|
|
193
|
+
reset({
|
|
194
|
+
...watch(),
|
|
195
|
+
company_name: '',
|
|
196
|
+
tax_office: '',
|
|
197
|
+
tax_no: ''
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}, [selectedFormType, reset, watch]);
|
|
201
|
+
|
|
192
202
|
return (
|
|
193
203
|
<form
|
|
194
204
|
onSubmit={handleSubmit(onSubmit)}
|
|
@@ -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,131 @@
|
|
|
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 } 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
|
+
|
|
11
|
+
const COMPONENT_TYPES = {
|
|
12
|
+
[WIDGET_TYPE.category]: Radio,
|
|
13
|
+
[WIDGET_TYPE.multiselect]: Checkbox
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const sizeKey = commonProductAttributes.find(
|
|
17
|
+
(item) => item.translationKey === 'size'
|
|
18
|
+
).key;
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
facet: Facet;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sortByPredefinedOrder = (
|
|
25
|
+
aLabel: string,
|
|
26
|
+
bLabel: string,
|
|
27
|
+
order: string[]
|
|
28
|
+
) => {
|
|
29
|
+
const aIndex = order.indexOf(aLabel);
|
|
30
|
+
const bIndex = order.indexOf(bLabel);
|
|
31
|
+
|
|
32
|
+
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
33
|
+
if (aIndex !== -1) return -1;
|
|
34
|
+
if (bIndex !== -1) return 1;
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sortByNumericValue = (aLabel: string, bLabel: string) => {
|
|
40
|
+
const aNum = parseInt(aLabel, 10);
|
|
41
|
+
const bNum = parseInt(bLabel, 10);
|
|
42
|
+
|
|
43
|
+
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
|
|
44
|
+
if (!isNaN(aNum)) return -1;
|
|
45
|
+
if (!isNaN(bNum)) return 1;
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const sortChoices = (
|
|
51
|
+
facetKey: string,
|
|
52
|
+
choices: FacetChoice[]
|
|
53
|
+
): FacetChoice[] => {
|
|
54
|
+
if (facetKey === sizeKey) {
|
|
55
|
+
const order = ['xs', 's', 'm', 'l', 'xl'];
|
|
56
|
+
|
|
57
|
+
return choices.sort((a, b) => {
|
|
58
|
+
const aLabel = a.label.toLowerCase();
|
|
59
|
+
const bLabel = b.label.toLowerCase();
|
|
60
|
+
|
|
61
|
+
const orderComparison = sortByPredefinedOrder(aLabel, bLabel, order);
|
|
62
|
+
if (orderComparison !== null) return orderComparison;
|
|
63
|
+
|
|
64
|
+
const numericComparison = sortByNumericValue(aLabel, bLabel);
|
|
65
|
+
if (numericComparison !== null) return numericComparison;
|
|
66
|
+
|
|
67
|
+
return aLabel.localeCompare(bLabel);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return choices;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
|
|
75
|
+
if (facetKey === sizeKey) {
|
|
76
|
+
return SizeFilter;
|
|
77
|
+
}
|
|
78
|
+
return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const FilterItem = ({ facet }: Props) => {
|
|
82
|
+
const dispatch = useAppDispatch();
|
|
83
|
+
const router = useRouter();
|
|
84
|
+
|
|
85
|
+
const handleSelectFilter = (choice: FacetChoice) => {
|
|
86
|
+
if (facet.key === 'category_ids') {
|
|
87
|
+
router.push(choice.url);
|
|
88
|
+
} else {
|
|
89
|
+
dispatch(toggleFacet({ facet, choice }));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const Component = getComponentByWidgetType(facet.widget_type, facet.key);
|
|
94
|
+
const choices = sortChoices(facet.key, [...facet.data.choices]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Accordion
|
|
98
|
+
key={facet.key}
|
|
99
|
+
title={facet.name}
|
|
100
|
+
isCollapse={choices.some((choice) => choice.is_selected)}
|
|
101
|
+
dataTestId={`filter-${facet.name}`}
|
|
102
|
+
>
|
|
103
|
+
<div
|
|
104
|
+
className={clsx('flex gap-4', {
|
|
105
|
+
'flex-wrap flex-row': facet.key === sizeKey,
|
|
106
|
+
'flex-col': facet.key !== sizeKey
|
|
107
|
+
})}
|
|
108
|
+
>
|
|
109
|
+
{choices.map((choice, index) => (
|
|
110
|
+
<Component
|
|
111
|
+
key={choice.label}
|
|
112
|
+
data={choice}
|
|
113
|
+
name={facet.key}
|
|
114
|
+
onChange={() => facet.key !== sizeKey && handleSelectFilter(choice)}
|
|
115
|
+
onClick={() => facet.key === sizeKey && handleSelectFilter(choice)}
|
|
116
|
+
checked={choice.is_selected}
|
|
117
|
+
data-testid={`${choice.label.trim()}`}
|
|
118
|
+
>
|
|
119
|
+
{choice.label} (
|
|
120
|
+
<span
|
|
121
|
+
data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
|
|
122
|
+
>
|
|
123
|
+
{choice.quantity}
|
|
124
|
+
</span>
|
|
125
|
+
)
|
|
126
|
+
</Component>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</Accordion>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { WIDGET_TYPE } from '@theme/types';
|
|
4
3
|
import clsx from 'clsx';
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import { useLocalization, useRouter } from '@akinon/next/hooks';
|
|
10
|
-
import { Facet, FacetChoice } from '@akinon/next/types';
|
|
5
|
+
import { Button, Icon } from '@theme/components';
|
|
6
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
11
7
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
12
|
-
import {
|
|
13
|
-
resetSelectedFacets,
|
|
14
|
-
toggleFacet
|
|
15
|
-
} from '@theme/redux/reducers/category';
|
|
8
|
+
import { resetSelectedFacets } from '@theme/redux/reducers/category';
|
|
16
9
|
import CategoryActiveFilters from '@theme/views/category/category-active-filters';
|
|
17
10
|
import { useMemo } from 'react';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
const COMPONENT_TYPES = {
|
|
21
|
-
[WIDGET_TYPE.category]: Radio,
|
|
22
|
-
[WIDGET_TYPE.multiselect]: Checkbox
|
|
23
|
-
};
|
|
11
|
+
import { FilterItem } from './filter-item';
|
|
24
12
|
|
|
25
13
|
interface Props {
|
|
26
14
|
isMenuOpen: boolean;
|
|
@@ -28,31 +16,11 @@ interface Props {
|
|
|
28
16
|
}
|
|
29
17
|
|
|
30
18
|
export const Filters = (props: Props) => {
|
|
31
|
-
const router = useRouter();
|
|
32
19
|
const facets = useAppSelector((state) => state.category.facets);
|
|
33
20
|
const dispatch = useAppDispatch();
|
|
34
21
|
const { t } = useLocalization();
|
|
35
22
|
const { isMenuOpen, setIsMenuOpen } = props;
|
|
36
23
|
|
|
37
|
-
const handleSelectFilter = ({
|
|
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
|
-
};
|
|
55
|
-
|
|
56
24
|
const haveFilter = useMemo(() => {
|
|
57
25
|
return (
|
|
58
26
|
facets.filter(
|
|
@@ -66,10 +34,6 @@ export const Filters = (props: Props) => {
|
|
|
66
34
|
dispatch(resetSelectedFacets());
|
|
67
35
|
};
|
|
68
36
|
|
|
69
|
-
const sizeKey = commonProductAttributes.find(
|
|
70
|
-
(item) => item.translationKey === 'size'
|
|
71
|
-
).key;
|
|
72
|
-
|
|
73
37
|
return (
|
|
74
38
|
<div
|
|
75
39
|
className={clsx(
|
|
@@ -88,71 +52,7 @@ export const Filters = (props: Props) => {
|
|
|
88
52
|
<span>{t('category.filters.ready_to_wear')}</span>
|
|
89
53
|
</div>
|
|
90
54
|
{facets.map((facet) => {
|
|
91
|
-
|
|
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
|
-
|
|
111
|
-
return (
|
|
112
|
-
<Accordion
|
|
113
|
-
key={facet.key}
|
|
114
|
-
title={facet.name}
|
|
115
|
-
isCollapse={choices.some((choice) => choice.is_selected)}
|
|
116
|
-
dataTestId={`filter-${facet.name}`}
|
|
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>
|
|
155
|
-
);
|
|
55
|
+
return <FilterItem key={facet.key} facet={facet} />;
|
|
156
56
|
})}
|
|
157
57
|
<div className="lg:hidden">
|
|
158
58
|
<CategoryActiveFilters />
|
|
@@ -1,20 +1,69 @@
|
|
|
1
1
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
2
2
|
import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
3
3
|
import { RootState } from '@theme/redux/store';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useSetShippingOptionMutation,
|
|
6
|
+
useSetDataSourceShippingOptionsMutation
|
|
7
|
+
} from '@akinon/next/data/client/checkout';
|
|
5
8
|
import { Price, Button, Radio } from '@theme/components';
|
|
6
9
|
import { CheckoutStep } from '@akinon/next/types';
|
|
7
10
|
import { useLocalization } from '@akinon/next/hooks';
|
|
11
|
+
import { useEffect, useState } from 'react';
|
|
8
12
|
|
|
9
13
|
const ShippingOptions = () => {
|
|
10
14
|
const { t } = useLocalization();
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const {
|
|
16
|
+
steps,
|
|
17
|
+
shippingOptions,
|
|
18
|
+
dataSourceShippingOptions,
|
|
19
|
+
preOrder,
|
|
20
|
+
addressList
|
|
21
|
+
} = useAppSelector((state: RootState) => state.checkout);
|
|
22
|
+
const { shipping_option, shipping_address, data_source_shipping_options } =
|
|
23
|
+
preOrder ?? {};
|
|
24
|
+
|
|
25
|
+
const [selectedPks, setSelectedPks] = useState<
|
|
26
|
+
{ dataSourcePk: number; optionPk: number }[] | null
|
|
27
|
+
>(null);
|
|
28
|
+
|
|
15
29
|
const [setShippingOption] = useSetShippingOptionMutation();
|
|
30
|
+
const [setDataSourceShippingOption] =
|
|
31
|
+
useSetDataSourceShippingOptionsMutation();
|
|
32
|
+
|
|
16
33
|
const dispatch = useAppDispatch();
|
|
17
34
|
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!data_source_shipping_options) return;
|
|
37
|
+
|
|
38
|
+
const initialSelectedPks = data_source_shipping_options.map((option) => ({
|
|
39
|
+
dataSourcePk: option.data_source.pk,
|
|
40
|
+
optionPk: option.pk
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
setSelectedPks(initialSelectedPks);
|
|
44
|
+
}, [data_source_shipping_options]);
|
|
45
|
+
|
|
46
|
+
const updateData = (dataSourcePk: number, newPk: number) => {
|
|
47
|
+
const updatedSelectedPks = selectedPks?.map((item) =>
|
|
48
|
+
item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!updatedSelectedPks) return;
|
|
52
|
+
|
|
53
|
+
setSelectedPks(updatedSelectedPks);
|
|
54
|
+
|
|
55
|
+
const pks = updatedSelectedPks.map((item) => item.optionPk);
|
|
56
|
+
setDataSourceShippingOption(pks);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleRadioChange = (
|
|
60
|
+
e: React.ChangeEvent<HTMLInputElement>,
|
|
61
|
+
dataSourcePk: number
|
|
62
|
+
) => {
|
|
63
|
+
const newPk = parseInt(e.currentTarget.value);
|
|
64
|
+
updateData(dataSourcePk, newPk);
|
|
65
|
+
};
|
|
66
|
+
|
|
18
67
|
return (
|
|
19
68
|
<div className="w-full lg:w-2/5">
|
|
20
69
|
<div className="border-b border-gray-400 px-8 py-4">
|
|
@@ -32,34 +81,83 @@ const ShippingOptions = () => {
|
|
|
32
81
|
{t('checkout.address.shipping.chosen_address')}:{' '}
|
|
33
82
|
{shipping_address?.city.name}
|
|
34
83
|
</p>
|
|
35
|
-
{shippingOptions.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
{shippingOptions && shippingOptions.length > 0 && (
|
|
85
|
+
<>
|
|
86
|
+
{shippingOptions.map((option) => (
|
|
87
|
+
<div
|
|
88
|
+
key={option.pk}
|
|
89
|
+
className="py-4 border-t border-gray-400 flex justify-between"
|
|
90
|
+
>
|
|
91
|
+
<Radio
|
|
92
|
+
name="shipping"
|
|
93
|
+
checked={option.pk === shipping_option?.pk}
|
|
94
|
+
onChange={() => {
|
|
95
|
+
setShippingOption(option.pk);
|
|
96
|
+
}}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setShippingOption(option?.pk);
|
|
99
|
+
}}
|
|
100
|
+
data-testid={`checkout-shipping-option-${option.pk}`}
|
|
101
|
+
>
|
|
102
|
+
{option.name}
|
|
103
|
+
</Radio>
|
|
104
|
+
<span className="text-xs">
|
|
105
|
+
<Price value={option.shipping_amount} />
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
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
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
{dataSourceShippingOptions && dataSourceShippingOptions.length > 0 && (
|
|
120
|
+
<>
|
|
121
|
+
{dataSourceShippingOptions.map((option) => (
|
|
122
|
+
<div key={option.pk}>
|
|
123
|
+
<h3 className="text-lg font-bold">{option?.name}</h3>
|
|
124
|
+
{option.data_source_shipping_options.map((opt) => (
|
|
125
|
+
<div
|
|
126
|
+
key={opt.pk}
|
|
127
|
+
className="py-4 border-t border-gray-400 flex justify-between"
|
|
128
|
+
>
|
|
129
|
+
<Radio
|
|
130
|
+
name={`data-source-shipping-${option.pk}`}
|
|
131
|
+
checked={
|
|
132
|
+
selectedPks?.some(
|
|
133
|
+
(item) => item.optionPk === opt.pk
|
|
134
|
+
) || false
|
|
135
|
+
}
|
|
136
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
137
|
+
handleRadioChange(e, option.pk)
|
|
138
|
+
}
|
|
139
|
+
value={opt.pk}
|
|
140
|
+
data-testid={`checkout-data-source-shipping-${opt.pk}`}
|
|
141
|
+
>
|
|
142
|
+
{opt?.shipping_option_name}
|
|
143
|
+
</Radio>
|
|
144
|
+
<span className="text-xs">
|
|
145
|
+
<Price value={opt?.shipping_amount} />
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
<Button
|
|
152
|
+
className="mt-2 w-full"
|
|
153
|
+
disabled={!steps.shipping.completed}
|
|
154
|
+
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
155
|
+
data-testid="checkout-shipping-save"
|
|
47
156
|
>
|
|
48
|
-
{
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</span>
|
|
53
|
-
</div>
|
|
54
|
-
))}
|
|
55
|
-
<Button
|
|
56
|
-
className="mt-2 w-full"
|
|
57
|
-
disabled={!steps.shipping.completed}
|
|
58
|
-
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
59
|
-
data-testid="checkout-shipping-save"
|
|
60
|
-
>
|
|
61
|
-
{t('checkout.address.shipping.button')}
|
|
62
|
-
</Button>
|
|
157
|
+
{t('checkout.address.shipping.button')}
|
|
158
|
+
</Button>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
63
161
|
</div>
|
|
64
162
|
)}
|
|
65
163
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { MenuItemType } from '@akinon/next/types';
|
|
5
5
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
6
6
|
import { closeMobileMenu } from '@akinon/next/redux/reducers/header';
|
|
@@ -32,9 +32,30 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
32
32
|
(state) => state.header.isMobileMenuOpen
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
function handleClickOutside(event: MouseEvent) {
|
|
39
|
+
if (
|
|
40
|
+
isMobileMenuOpen &&
|
|
41
|
+
menuRef.current &&
|
|
42
|
+
!menuRef.current.contains(event.target as Node)
|
|
43
|
+
) {
|
|
44
|
+
dispatch(closeMobileMenu());
|
|
45
|
+
setSelectedSubMenu(null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
53
|
+
};
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
}, [isMobileMenuOpen]);
|
|
56
|
+
|
|
35
57
|
return (
|
|
36
58
|
<>
|
|
37
|
-
{/* MENU OVERLAY */}
|
|
38
59
|
<div
|
|
39
60
|
className={clsx(
|
|
40
61
|
'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black bg-opacity-80 transition duration-500',
|
|
@@ -42,14 +63,10 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
42
63
|
'!visible !opacity-100 scroll-lock': isMobileMenuOpen
|
|
43
64
|
}
|
|
44
65
|
)}
|
|
45
|
-
// TODO: Remove this after we have a better solution for clicking outside of the menu
|
|
46
|
-
onClick={() => {
|
|
47
|
-
dispatch(closeMobileMenu());
|
|
48
|
-
setSelectedSubMenu(null);
|
|
49
|
-
}}
|
|
50
66
|
/>
|
|
51
|
-
|
|
67
|
+
|
|
52
68
|
<div
|
|
69
|
+
ref={menuRef}
|
|
53
70
|
className={clsx(
|
|
54
71
|
'fixed top-0 left-0 z-50 flex flex-col bg-white w-72 pt-4 h-screen invisible opacity-0 transition duration-500 transform -translate-x-72',
|
|
55
72
|
{
|