@akinon/projectzero 1.55.0-rc.0 → 1.55.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 -34
- package/app-template/.gitignore +0 -2
- package/app-template/CHANGELOG.md +80 -1871
- package/app-template/package.json +18 -17
- 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]/account/coupons/page.tsx +4 -4
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/profile/page.tsx +0 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +2 -5
- package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx +8 -12
- package/app-template/src/components/checkbox.tsx +2 -2
- package/app-template/src/components/input.tsx +7 -19
- package/app-template/src/components/pagination.tsx +18 -13
- package/app-template/src/components/price.tsx +4 -9
- package/app-template/src/redux/reducers/category.ts +1 -7
- package/app-template/src/settings.js +1 -6
- package/app-template/src/views/account/address-form.tsx +2 -12
- package/app-template/src/views/account/contact-form.tsx +6 -23
- package/app-template/src/views/account/favourite-products/favourite-products-list.tsx +1 -5
- package/app-template/src/views/breadcrumb.tsx +1 -4
- package/app-template/src/views/category/category-active-filters.tsx +6 -16
- package/app-template/src/views/category/category-info.tsx +17 -31
- package/app-template/src/views/category/filters/index.tsx +108 -16
- package/app-template/src/views/category/layout.tsx +3 -5
- package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +37 -43
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +3 -19
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +34 -230
- package/app-template/src/views/header/mobile-menu.tsx +8 -25
- package/app-template/tsconfig.json +4 -14
- package/package.json +2 -2
- package/app-template/src/app/[commerce]/[locale]/[currency]/[...prettyurl]/page.tsx +0 -8
- package/app-template/src/views/category/filters/filter-item.tsx +0 -163
|
@@ -1,174 +1,38 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
1
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
|
-
import {
|
|
4
|
-
setCurrentStep,
|
|
5
|
-
setSelectedShippingOptions
|
|
6
|
-
} from '@akinon/next/redux/reducers/checkout';
|
|
2
|
+
import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
|
|
7
3
|
import { RootState } from '@theme/redux/store';
|
|
8
|
-
import {
|
|
9
|
-
useSetShippingOptionMutation,
|
|
10
|
-
useSetAttributeBasedShippingOptionsMutation,
|
|
11
|
-
useSetDataSourceShippingOptionsMutation
|
|
12
|
-
} from '@akinon/next/data/client/checkout';
|
|
4
|
+
import { useSetShippingOptionMutation } from '@akinon/next/data/client/checkout';
|
|
13
5
|
import { Price, Button, Radio } from '@theme/components';
|
|
14
6
|
import { CheckoutStep } from '@akinon/next/types';
|
|
15
7
|
import { useLocalization } from '@akinon/next/hooks';
|
|
16
8
|
|
|
17
|
-
const ShippingOptions
|
|
9
|
+
const ShippingOptions = () => {
|
|
18
10
|
const { t } = useLocalization();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
steps,
|
|
22
|
-
shippingOptions,
|
|
23
|
-
attributeBasedShippingOptions,
|
|
24
|
-
dataSourceShippingOptions,
|
|
25
|
-
preOrder,
|
|
26
|
-
addressList,
|
|
27
|
-
selectedShippingOptions
|
|
28
|
-
} = useAppSelector((state: RootState) => state.checkout);
|
|
29
|
-
const {
|
|
30
|
-
shipping_option,
|
|
31
|
-
shipping_address,
|
|
32
|
-
attribute_based_shipping_options,
|
|
33
|
-
data_source_shipping_options
|
|
34
|
-
} = preOrder ?? {};
|
|
35
|
-
|
|
36
|
-
const [setShippingOption] = useSetShippingOptionMutation();
|
|
37
|
-
const [setAttributeBasedShippingOptions] =
|
|
38
|
-
useSetAttributeBasedShippingOptionsMutation();
|
|
39
|
-
const [setDataSourceShippingOption] =
|
|
40
|
-
useSetDataSourceShippingOptionsMutation();
|
|
41
|
-
|
|
42
|
-
const prevAttributeBasedOptionsRef = useRef(attributeBasedShippingOptions);
|
|
43
|
-
|
|
44
|
-
const [selectedPks, setSelectedPks] = useState<
|
|
45
|
-
{ dataSourcePk: number; optionPk: number }[] | null
|
|
46
|
-
>(null);
|
|
47
|
-
|
|
48
|
-
const initializeSelectedOptions = useCallback(() => {
|
|
49
|
-
if (attribute_based_shipping_options) {
|
|
50
|
-
const newSelected = { ...selectedShippingOptions };
|
|
51
|
-
let hasChanges = false;
|
|
52
|
-
|
|
53
|
-
Object.entries(attributeBasedShippingOptions).forEach(
|
|
54
|
-
([color, options]) => {
|
|
55
|
-
if (
|
|
56
|
-
!newSelected[color] ||
|
|
57
|
-
!options.some((opt) => opt.pk === newSelected[color])
|
|
58
|
-
) {
|
|
59
|
-
newSelected[color] = options[0].pk;
|
|
60
|
-
hasChanges = true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (hasChanges) {
|
|
66
|
-
dispatch(setSelectedShippingOptions(newSelected));
|
|
67
|
-
setAttributeBasedShippingOptions(newSelected);
|
|
68
|
-
}
|
|
69
|
-
} else if (shippingOptions.length > 0 && !shipping_option) {
|
|
70
|
-
setShippingOption(shippingOptions[0].pk);
|
|
71
|
-
}
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, [
|
|
74
|
-
attributeBasedShippingOptions,
|
|
75
|
-
selectedShippingOptions,
|
|
76
|
-
setAttributeBasedShippingOptions,
|
|
77
|
-
attribute_based_shipping_options,
|
|
78
|
-
shippingOptions,
|
|
79
|
-
shipping_option,
|
|
80
|
-
setShippingOption
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!data_source_shipping_options) return;
|
|
85
|
-
|
|
86
|
-
const initialSelectedPks = data_source_shipping_options.map((option) => ({
|
|
87
|
-
dataSourcePk: option.data_source.pk,
|
|
88
|
-
optionPk: option.pk
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
setSelectedPks(initialSelectedPks);
|
|
92
|
-
}, [data_source_shipping_options]);
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (
|
|
96
|
-
JSON.stringify(prevAttributeBasedOptionsRef.current) !==
|
|
97
|
-
JSON.stringify(attributeBasedShippingOptions) ||
|
|
98
|
-
Object.keys(selectedShippingOptions).length === 0 ||
|
|
99
|
-
(!shipping_option && shippingOptions.length > 0)
|
|
100
|
-
) {
|
|
101
|
-
initializeSelectedOptions();
|
|
102
|
-
prevAttributeBasedOptionsRef.current = attributeBasedShippingOptions;
|
|
103
|
-
}
|
|
104
|
-
}, [
|
|
105
|
-
attributeBasedShippingOptions,
|
|
106
|
-
selectedShippingOptions,
|
|
107
|
-
initializeSelectedOptions,
|
|
108
|
-
shipping_option,
|
|
109
|
-
shippingOptions
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
const handleAttributeBasedOptionChange = useCallback(
|
|
113
|
-
(color: string, newPk: number) => {
|
|
114
|
-
const updatedOptions = { ...selectedShippingOptions, [color]: newPk };
|
|
115
|
-
dispatch(setSelectedShippingOptions(updatedOptions));
|
|
116
|
-
setAttributeBasedShippingOptions(updatedOptions);
|
|
117
|
-
},
|
|
118
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
-
[selectedShippingOptions, setAttributeBasedShippingOptions]
|
|
11
|
+
const { steps, shippingOptions, preOrder, addressList } = useAppSelector(
|
|
12
|
+
(state: RootState) => state.checkout
|
|
120
13
|
);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<div className="w-full lg:w-2/5">
|
|
125
|
-
<div className="border-b border-gray-400 px-8 py-4">
|
|
126
|
-
<h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
|
|
127
|
-
</div>
|
|
128
|
-
<div className="py-4 px-8">
|
|
129
|
-
<p className="text-xs">
|
|
130
|
-
{t('checkout.address.shipping.select_address_to_continue')}
|
|
131
|
-
</p>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const updateData = (dataSourcePk: number, newPk: number) => {
|
|
138
|
-
const updatedSelectedPks = selectedPks?.map((item) =>
|
|
139
|
-
item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if (!updatedSelectedPks) return;
|
|
143
|
-
|
|
144
|
-
setSelectedPks(updatedSelectedPks);
|
|
145
|
-
|
|
146
|
-
const pks = updatedSelectedPks.map((item) => item.optionPk);
|
|
147
|
-
setDataSourceShippingOption(pks);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const handleRadioChange = (
|
|
151
|
-
e: React.ChangeEvent<HTMLInputElement>,
|
|
152
|
-
dataSourcePk: number
|
|
153
|
-
) => {
|
|
154
|
-
const newPk = parseInt(e.currentTarget.value);
|
|
155
|
-
updateData(dataSourcePk, newPk);
|
|
156
|
-
};
|
|
14
|
+
const { shipping_option, shipping_address } = preOrder ?? {};
|
|
15
|
+
const [setShippingOption] = useSetShippingOptionMutation();
|
|
16
|
+
const dispatch = useAppDispatch();
|
|
157
17
|
|
|
158
18
|
return (
|
|
159
19
|
<div className="w-full lg:w-2/5">
|
|
160
20
|
<div className="border-b border-gray-400 px-8 py-4">
|
|
161
21
|
<h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
|
|
162
22
|
</div>
|
|
163
|
-
<
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
23
|
+
{addressList.length < 1 ? (
|
|
24
|
+
<div className="py-4 px-8">
|
|
25
|
+
<p className="text-xs">
|
|
26
|
+
{t('checkout.address.shipping.select_address_to_continue')}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
) : (
|
|
30
|
+
<div className="py-4 px-6">
|
|
31
|
+
<p className="text-xs border-gray-400 pb-4">
|
|
32
|
+
{t('checkout.address.shipping.chosen_address')}:{' '}
|
|
33
|
+
{shipping_address?.city.name}
|
|
34
|
+
</p>
|
|
35
|
+
{shippingOptions.map((option) => (
|
|
172
36
|
<div
|
|
173
37
|
key={option.pk}
|
|
174
38
|
className="py-4 border-t border-gray-400 flex justify-between"
|
|
@@ -176,7 +40,9 @@ const ShippingOptions: React.FC = () => {
|
|
|
176
40
|
<Radio
|
|
177
41
|
name="shipping"
|
|
178
42
|
checked={option.pk === shipping_option?.pk}
|
|
179
|
-
onChange={() =>
|
|
43
|
+
onChange={() => {
|
|
44
|
+
setShippingOption(option.pk);
|
|
45
|
+
}}
|
|
180
46
|
data-testid={`checkout-shipping-option-${option.pk}`}
|
|
181
47
|
>
|
|
182
48
|
{option.name}
|
|
@@ -186,78 +52,16 @@ const ShippingOptions: React.FC = () => {
|
|
|
186
52
|
</span>
|
|
187
53
|
</div>
|
|
188
54
|
))}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
className="py-4 border-t border-gray-400 flex justify-between"
|
|
200
|
-
>
|
|
201
|
-
<Radio
|
|
202
|
-
name={`attribute-based-shipping-${color}`}
|
|
203
|
-
checked={selectedShippingOptions[color] === option.pk}
|
|
204
|
-
onChange={() =>
|
|
205
|
-
handleAttributeBasedOptionChange(color, option.pk)
|
|
206
|
-
}
|
|
207
|
-
data-testid={`checkout-attribute-based-shipping-${option.pk}`}
|
|
208
|
-
>
|
|
209
|
-
{`${option.shipping_option_name}`}
|
|
210
|
-
</Radio>
|
|
211
|
-
<span className="text-xs">
|
|
212
|
-
<Price value={option.shipping_amount} />
|
|
213
|
-
</span>
|
|
214
|
-
</div>
|
|
215
|
-
))}
|
|
216
|
-
</div>
|
|
217
|
-
)
|
|
218
|
-
)}
|
|
219
|
-
|
|
220
|
-
{dataSourceShippingOptions &&
|
|
221
|
-
dataSourceShippingOptions.length > 0 &&
|
|
222
|
-
dataSourceShippingOptions.map((option) => (
|
|
223
|
-
<div key={option.pk}>
|
|
224
|
-
<h3 className="text-lg font-bold">{option?.name}</h3>
|
|
225
|
-
{option.data_source_shipping_options.map((opt) => (
|
|
226
|
-
<div
|
|
227
|
-
key={opt.pk}
|
|
228
|
-
className="py-4 border-t border-gray-400 flex justify-between"
|
|
229
|
-
>
|
|
230
|
-
<Radio
|
|
231
|
-
name={`data-source-shipping-${option.pk}`}
|
|
232
|
-
checked={
|
|
233
|
-
selectedPks?.some((item) => item.optionPk === opt.pk) ||
|
|
234
|
-
false
|
|
235
|
-
}
|
|
236
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
237
|
-
handleRadioChange(e, option.pk)
|
|
238
|
-
}
|
|
239
|
-
value={opt.pk}
|
|
240
|
-
data-testid={`checkout-data-source-shipping-${opt.pk}`}
|
|
241
|
-
>
|
|
242
|
-
{opt?.shipping_option_name}
|
|
243
|
-
</Radio>
|
|
244
|
-
<span className="text-xs">
|
|
245
|
-
<Price value={opt?.shipping_amount} />
|
|
246
|
-
</span>
|
|
247
|
-
</div>
|
|
248
|
-
))}
|
|
249
|
-
</div>
|
|
250
|
-
))}
|
|
251
|
-
|
|
252
|
-
<Button
|
|
253
|
-
className="mt-2 w-full"
|
|
254
|
-
disabled={!steps.shipping.completed}
|
|
255
|
-
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
256
|
-
data-testid="checkout-shipping-save"
|
|
257
|
-
>
|
|
258
|
-
{t('checkout.address.shipping.button')}
|
|
259
|
-
</Button>
|
|
260
|
-
</div>
|
|
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>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
261
65
|
</div>
|
|
262
66
|
);
|
|
263
67
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState
|
|
3
|
+
import { useState } 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,30 +32,9 @@ 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
|
-
|
|
57
35
|
return (
|
|
58
36
|
<>
|
|
37
|
+
{/* MENU OVERLAY */}
|
|
59
38
|
<div
|
|
60
39
|
className={clsx(
|
|
61
40
|
'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black bg-opacity-80 transition duration-500',
|
|
@@ -63,10 +42,14 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
63
42
|
'!visible !opacity-100 scroll-lock': isMobileMenuOpen
|
|
64
43
|
}
|
|
65
44
|
)}
|
|
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
|
+
}}
|
|
66
50
|
/>
|
|
67
|
-
|
|
51
|
+
{/* TODO: Add a way to close the menu when clicking outside of it */}
|
|
68
52
|
<div
|
|
69
|
-
ref={menuRef}
|
|
70
53
|
className={clsx(
|
|
71
54
|
'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',
|
|
72
55
|
{
|
|
@@ -3,19 +3,7 @@
|
|
|
3
3
|
"display": "Default",
|
|
4
4
|
"compilerOptions": {
|
|
5
5
|
"baseUrl": "./src",
|
|
6
|
-
"paths": {
|
|
7
|
-
"@theme/*": ["./*"],
|
|
8
|
-
"@root/*": ["./app/[commerce]/[locale]/[currency]/*"],
|
|
9
|
-
"@product/*": ["./app/[commerce]/[locale]/[currency]/product/*"],
|
|
10
|
-
"@group-product/*": [
|
|
11
|
-
"./app/[commerce]/[locale]/[currency]/group-product/*"
|
|
12
|
-
],
|
|
13
|
-
"@category/*": ["./app/[commerce]/[locale]/[currency]/category/*"],
|
|
14
|
-
"@special-page/*": [
|
|
15
|
-
"./app/[commerce]/[locale]/[currency]/special-page/*"
|
|
16
|
-
],
|
|
17
|
-
"@flat-page/*": ["./app/[commerce]/[locale]/[currency]/flat-page/*"]
|
|
18
|
-
},
|
|
6
|
+
"paths": { "@theme/*": ["./*"] },
|
|
19
7
|
"allowSyntheticDefaultImports": true,
|
|
20
8
|
"composite": false,
|
|
21
9
|
"declaration": true,
|
|
@@ -52,5 +40,7 @@
|
|
|
52
40
|
".next/types/**/*.ts",
|
|
53
41
|
"../../packages/**/*"
|
|
54
42
|
],
|
|
55
|
-
"exclude": ["node_modules",
|
|
43
|
+
"exclude": ["node_modules",
|
|
44
|
+
"../../packages/projectzero/app-template"
|
|
45
|
+
]
|
|
56
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/projectzero",
|
|
3
|
-
"version": "1.55.0
|
|
3
|
+
"version": "1.55.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI tool to manage your Project Zero Next project",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@types/temp": "0.9.4"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"loading-spinner": "
|
|
22
|
+
"loading-spinner": "1.2.1",
|
|
23
23
|
"prompt-checkbox": "2.2.0",
|
|
24
24
|
"semver": "7.6.2",
|
|
25
25
|
"temp": "0.9.4",
|
|
@@ -1,163 +0,0 @@
|
|
|
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
|
-
};
|