@akinon/projectzero 1.63.0 → 1.65.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 +62 -0
- package/app-template/package.json +18 -19
- 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/price.tsx +24 -8
- 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/account/favourite-products/favourite-products-list.tsx +5 -1
- 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,67 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 1.65.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5a4c607: ZERO-2764: Add case-warning rule to eslint-plugin-projectzero
|
|
8
|
+
- 74a1269: ZERO-2658:Edit product name clickability on completed order page
|
|
9
|
+
- d13fd36: ZERO-2614: Refactor Price component to remove unnecessary code and improve readability also fix the decimal scale
|
|
10
|
+
- 902d828: ZERO-2621:edit position of loaderSpinner in favorites page
|
|
11
|
+
- 4614eee: ZERO-2602: The script that checks the build for standalone projects
|
|
12
|
+
- 86d2531: ZERO-2693: resolve dependency collision warning for eslint-config-next
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [d13fd36]
|
|
17
|
+
- Updated dependencies [86d2531]
|
|
18
|
+
- @akinon/next@1.65.0
|
|
19
|
+
- @akinon/pz-akifast@1.65.0
|
|
20
|
+
- @akinon/pz-b2b@1.65.0
|
|
21
|
+
- @akinon/pz-basket-gift-pack@1.65.0
|
|
22
|
+
- @akinon/pz-bkm@1.65.0
|
|
23
|
+
- @akinon/pz-checkout-gift-pack@1.65.0
|
|
24
|
+
- @akinon/pz-click-collect@1.65.0
|
|
25
|
+
- @akinon/pz-credit-payment@1.65.0
|
|
26
|
+
- @akinon/pz-gpay@1.65.0
|
|
27
|
+
- @akinon/pz-masterpass@1.65.0
|
|
28
|
+
- @akinon/pz-one-click-checkout@1.65.0
|
|
29
|
+
- @akinon/pz-otp@1.65.0
|
|
30
|
+
- @akinon/pz-pay-on-delivery@1.65.0
|
|
31
|
+
- @akinon/pz-saved-card@1.65.0
|
|
32
|
+
- @akinon/pz-tabby-extension@1.65.0
|
|
33
|
+
|
|
34
|
+
## 1.64.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- c53ea3e: ZERO-2609: Reset additional form fields when selectedFormType is not company
|
|
39
|
+
- 27a5296: ZERO-2631:Fix Checkbox Click
|
|
40
|
+
- e665a0a: ZERO-2625: Add click outside functionality to close mobile menu
|
|
41
|
+
- d3474c6: ZERO-2655: Add data source shipping option
|
|
42
|
+
- 7a4bb76: ZERO-2610:Refactor FilterItem component for better readability and efficiency
|
|
43
|
+
- fac2e5b: ZERO-2622: Add isMenuOpen state to Category reducer and update CategoryInfo component
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [c53ea3e]
|
|
48
|
+
- Updated dependencies [d3474c6]
|
|
49
|
+
- @akinon/next@1.64.0
|
|
50
|
+
- @akinon/pz-akifast@1.64.0
|
|
51
|
+
- @akinon/pz-b2b@1.64.0
|
|
52
|
+
- @akinon/pz-basket-gift-pack@1.64.0
|
|
53
|
+
- @akinon/pz-bkm@1.64.0
|
|
54
|
+
- @akinon/pz-checkout-gift-pack@1.64.0
|
|
55
|
+
- @akinon/pz-click-collect@1.64.0
|
|
56
|
+
- @akinon/pz-credit-payment@1.64.0
|
|
57
|
+
- @akinon/pz-gpay@1.64.0
|
|
58
|
+
- @akinon/pz-masterpass@1.64.0
|
|
59
|
+
- @akinon/pz-one-click-checkout@1.64.0
|
|
60
|
+
- @akinon/pz-otp@1.64.0
|
|
61
|
+
- @akinon/pz-pay-on-delivery@1.64.0
|
|
62
|
+
- @akinon/pz-saved-card@1.64.0
|
|
63
|
+
- @akinon/pz-tabby-extension@1.64.0
|
|
64
|
+
|
|
3
65
|
## 1.63.0
|
|
4
66
|
|
|
5
67
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -22,27 +22,26 @@
|
|
|
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.65.0",
|
|
26
|
+
"@akinon/pz-akifast": "1.65.0",
|
|
27
|
+
"@akinon/pz-b2b": "1.65.0",
|
|
28
|
+
"@akinon/pz-basket-gift-pack": "1.65.0",
|
|
29
|
+
"@akinon/pz-bkm": "1.65.0",
|
|
30
|
+
"@akinon/pz-checkout-gift-pack": "1.65.0",
|
|
31
|
+
"@akinon/pz-click-collect": "1.65.0",
|
|
32
|
+
"@akinon/pz-credit-payment": "1.65.0",
|
|
33
|
+
"@akinon/pz-gpay": "1.65.0",
|
|
34
|
+
"@akinon/pz-masterpass": "1.65.0",
|
|
35
|
+
"@akinon/pz-one-click-checkout": "1.65.0",
|
|
36
|
+
"@akinon/pz-otp": "1.65.0",
|
|
37
|
+
"@akinon/pz-pay-on-delivery": "1.65.0",
|
|
38
|
+
"@akinon/pz-saved-card": "1.65.0",
|
|
39
|
+
"@akinon/pz-tabby-extension": "1.65.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",
|
|
43
43
|
"@sentry/nextjs": "7.116.0",
|
|
44
44
|
"dayjs": "1.11.5",
|
|
45
|
-
"eslint-config-next": "14.2.2",
|
|
46
45
|
"lossless-json": "2.0.5",
|
|
47
46
|
"next": "14.2.5",
|
|
48
47
|
"next-auth": "4.24.5",
|
|
@@ -61,7 +60,7 @@
|
|
|
61
60
|
"yup": "0.32.11"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@akinon/eslint-plugin-projectzero": "1.
|
|
63
|
+
"@akinon/eslint-plugin-projectzero": "1.65.0",
|
|
65
64
|
"@semantic-release/changelog": "6.0.2",
|
|
66
65
|
"@semantic-release/exec": "6.0.3",
|
|
67
66
|
"@semantic-release/git": "10.0.1",
|
|
@@ -81,7 +80,7 @@
|
|
|
81
80
|
"clsx": "1.1.1",
|
|
82
81
|
"currency-symbol-map": "5.1.0",
|
|
83
82
|
"eslint": "8.14.0",
|
|
84
|
-
"eslint-config-next": "14.
|
|
83
|
+
"eslint-config-next": "14.2.3",
|
|
85
84
|
"eslint-config-prettier": "8.5.0",
|
|
86
85
|
"husky": "8.0.0",
|
|
87
86
|
"jest": "29.7.0",
|
package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx
CHANGED
|
@@ -182,21 +182,25 @@ const CheckoutCompleted = ({
|
|
|
182
182
|
}}
|
|
183
183
|
>
|
|
184
184
|
{data.order.orderitem_set.map((item) => (
|
|
185
|
-
<div
|
|
186
|
-
|
|
185
|
+
<div
|
|
186
|
+
key={`order-item-${item.id}`}
|
|
187
|
+
className="flex justify-between gap-x-4 w-full"
|
|
188
|
+
>
|
|
189
|
+
<Link
|
|
190
|
+
className="flex justify-between gap-x-4 flex-1 items-center transition-all text-xs text-black-800 hover:text-secondary"
|
|
191
|
+
href={item.product.absolute_url}
|
|
192
|
+
passHref
|
|
193
|
+
>
|
|
187
194
|
<Image
|
|
188
195
|
src={item.product.image}
|
|
189
196
|
alt={item.product.name}
|
|
190
197
|
width={64}
|
|
191
198
|
height={96}
|
|
192
199
|
/>
|
|
200
|
+
|
|
201
|
+
<span>{item.product.name}</span>
|
|
193
202
|
</Link>
|
|
194
|
-
<div className="flex justify-
|
|
195
|
-
<>
|
|
196
|
-
<div className="text-xs text-black-800 transition-all w-full hover:text-secondary">
|
|
197
|
-
{item.product.name}
|
|
198
|
-
</div>
|
|
199
|
-
</>
|
|
203
|
+
<div className="flex justify-end items-center">
|
|
200
204
|
<div>
|
|
201
205
|
{item.retail_price !== item.price && (
|
|
202
206
|
<div className="text-black-800 line-through text-xs min-w-max sm:text-sm">
|
|
@@ -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>}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import NumberFormat, { NumberFormatProps } from 'react-number-format';
|
|
3
3
|
import { getCurrency } from '@akinon/next/utils';
|
|
4
|
-
import { PriceProps } from '@theme/types';
|
|
5
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { PriceProps } from '../types';
|
|
6
6
|
import Settings from '@theme/settings';
|
|
7
7
|
|
|
8
8
|
export const Price = (props: NumberFormatProps & PriceProps) => {
|
|
@@ -21,12 +21,10 @@ export const Price = (props: NumberFormatProps & PriceProps) => {
|
|
|
21
21
|
fixedDecimalScale = true,
|
|
22
22
|
...rest
|
|
23
23
|
} = props;
|
|
24
|
+
|
|
24
25
|
const { currency: selectedCurrencyCode } = useLocalization();
|
|
25
26
|
const currencyCode_ = currencyCode || selectedCurrencyCode;
|
|
26
27
|
|
|
27
|
-
// TODO: This is very bad practice. It broke decimalScale.
|
|
28
|
-
const _value = value?.toString().replace('.', ',');
|
|
29
|
-
|
|
30
28
|
const currency = useMemo(
|
|
31
29
|
() =>
|
|
32
30
|
getCurrency({
|
|
@@ -38,21 +36,39 @@ export const Price = (props: NumberFormatProps & PriceProps) => {
|
|
|
38
36
|
[currencyCode_, useCurrencySymbol, useCurrencyAfterPrice, useCurrencySpace]
|
|
39
37
|
);
|
|
40
38
|
|
|
39
|
+
const numericValue =
|
|
40
|
+
typeof value === 'string'
|
|
41
|
+
? parseFloat(value)
|
|
42
|
+
: typeof value === 'number'
|
|
43
|
+
? value
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
const formattedValue = Number.isFinite(numericValue)
|
|
47
|
+
? numericValue.toFixed(decimalScale)
|
|
48
|
+
: '0';
|
|
49
|
+
|
|
50
|
+
const displayValue =
|
|
51
|
+
useNegative && numericValue < 0
|
|
52
|
+
? `-${useNegativeSpace ? ' ' : ''}${Math.abs(numericValue).toFixed(
|
|
53
|
+
decimalScale
|
|
54
|
+
)}`
|
|
55
|
+
: formattedValue;
|
|
56
|
+
|
|
41
57
|
const currentCurrencyDecimalScale = Settings.localization.currencies.find(
|
|
42
58
|
(currency) => currency.code === currencyCode_
|
|
43
59
|
).decimalScale;
|
|
44
60
|
|
|
45
61
|
return (
|
|
46
62
|
<NumberFormat
|
|
47
|
-
value={
|
|
48
|
-
{...{
|
|
49
|
-
[useCurrencyAfterPrice ? 'suffix' : 'prefix']: currency
|
|
50
|
-
}}
|
|
63
|
+
value={displayValue}
|
|
51
64
|
displayType={displayType}
|
|
52
65
|
thousandSeparator={thousandSeparator}
|
|
53
66
|
decimalScale={currentCurrencyDecimalScale ?? decimalScale}
|
|
54
67
|
decimalSeparator={decimalSeparator}
|
|
55
68
|
fixedDecimalScale={fixedDecimalScale}
|
|
69
|
+
prefix={!useCurrencyAfterPrice ? currency : undefined}
|
|
70
|
+
suffix={useCurrencyAfterPrice ? currency : undefined}
|
|
71
|
+
isNumericString={true}
|
|
56
72
|
{...rest}
|
|
57
73
|
/>
|
|
58
74
|
);
|
|
@@ -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)}
|
|
@@ -30,7 +30,11 @@ const FavoriteProductsList = () => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (isLoading || isFetching) {
|
|
33
|
-
return
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-center justify-center h-80">
|
|
35
|
+
<LoaderSpinner />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
return (
|
|
@@ -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
|
{
|