@akinon/projectzero 1.42.0 → 1.43.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 +40 -0
- package/README.md +3 -2
- package/app-template/.lintstagedrc.js +5 -4
- package/app-template/CHANGELOG.md +380 -7
- package/app-template/docs/basic-setup.md +1 -1
- package/app-template/docs/plugins.md +7 -7
- package/app-template/package-lock.json +29303 -0
- package/app-template/package.json +23 -21
- 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]/basket/page.tsx +1 -1
- 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/price.tsx +9 -4
- package/app-template/src/redux/reducers/category.ts +7 -1
- package/app-template/src/views/account/address-form.tsx +22 -7
- package/app-template/src/views/account/contact-form.tsx +23 -6
- package/app-template/src/views/account/favorite-item.tsx +2 -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/payment/options/credit-card/index.tsx +33 -4
- package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +43 -37
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +128 -35
- package/app-template/src/views/find-in-store/index.tsx +2 -3
- package/app-template/src/views/header/mobile-menu.tsx +25 -8
- package/app-template/tsconfig.json +14 -4
- package/app-template/yarn.lock +1824 -1953
- package/commands/create.ts +29 -5
- package/dist/commands/create.js +25 -2
- package/package.json +2 -2
|
@@ -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 />
|
|
@@ -23,13 +23,29 @@ import { PaymentOption } from '@akinon/next/types';
|
|
|
23
23
|
const creditCardFormSchema = (
|
|
24
24
|
t,
|
|
25
25
|
payment_option: PaymentOption,
|
|
26
|
-
isMasterpassDirectPurchase?: boolean
|
|
26
|
+
isMasterpassDirectPurchase?: boolean,
|
|
27
|
+
isMasterpassCvcRequired?: boolean
|
|
27
28
|
) => {
|
|
28
29
|
if (
|
|
29
30
|
payment_option?.payment_type === 'masterpass' &&
|
|
30
31
|
isMasterpassDirectPurchase === false
|
|
31
32
|
) {
|
|
32
33
|
return yup.object().shape({
|
|
34
|
+
card_cvv: yup
|
|
35
|
+
.string()
|
|
36
|
+
.transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
|
|
37
|
+
.when('*', (_, schema) => {
|
|
38
|
+
if (isMasterpassCvcRequired) {
|
|
39
|
+
return schema
|
|
40
|
+
.length(
|
|
41
|
+
3,
|
|
42
|
+
t('checkout.payment.credit_card.form.error.cvv_length')
|
|
43
|
+
)
|
|
44
|
+
.required(t('checkout.payment.credit_card.form.error.required'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return schema;
|
|
48
|
+
}),
|
|
33
49
|
agreement: yup
|
|
34
50
|
.boolean()
|
|
35
51
|
.oneOf([true], t('checkout.payment.credit_card.form.error.required'))
|
|
@@ -87,10 +103,16 @@ const CheckoutCreditCard = () => {
|
|
|
87
103
|
control,
|
|
88
104
|
formState: { errors },
|
|
89
105
|
setError,
|
|
90
|
-
getValues
|
|
106
|
+
getValues,
|
|
107
|
+
clearErrors
|
|
91
108
|
} = useForm<CreditCardForm>({
|
|
92
109
|
resolver: yupResolver(
|
|
93
|
-
creditCardFormSchema(
|
|
110
|
+
creditCardFormSchema(
|
|
111
|
+
t,
|
|
112
|
+
payment_option,
|
|
113
|
+
masterpass?.isDirectPurchase,
|
|
114
|
+
masterpass?.cvcRequired
|
|
115
|
+
)
|
|
94
116
|
)
|
|
95
117
|
});
|
|
96
118
|
const [months, setMonths] = useState([]);
|
|
@@ -317,7 +339,14 @@ const CheckoutCreditCard = () => {
|
|
|
317
339
|
<PluginModule
|
|
318
340
|
component={Component.MasterpassCardList}
|
|
319
341
|
props={{
|
|
320
|
-
className: 'p-10'
|
|
342
|
+
className: 'p-10',
|
|
343
|
+
form: {
|
|
344
|
+
control,
|
|
345
|
+
register,
|
|
346
|
+
errors,
|
|
347
|
+
setFormValue,
|
|
348
|
+
clearErrors
|
|
349
|
+
}
|
|
321
350
|
}}
|
|
322
351
|
/>
|
|
323
352
|
|
|
@@ -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
|
}
|
|
@@ -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,37 +81,81 @@ const ShippingOptions = () => {
|
|
|
32
81
|
{t('checkout.address.shipping.chosen_address')}:{' '}
|
|
33
82
|
{shipping_address?.city.name}
|
|
34
83
|
</p>
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
{}
|
|
85
|
+
{shippingOptions && shippingOptions.length > 0 && (
|
|
86
|
+
<>
|
|
87
|
+
{shippingOptions.map((option) => (
|
|
88
|
+
<div
|
|
89
|
+
key={option.pk}
|
|
90
|
+
className="py-4 border-t border-gray-400 flex justify-between"
|
|
91
|
+
>
|
|
92
|
+
<Radio
|
|
93
|
+
name="shipping"
|
|
94
|
+
checked={option.pk === shipping_option?.pk}
|
|
95
|
+
onChange={() => {
|
|
96
|
+
setShippingOption(option.pk);
|
|
97
|
+
}}
|
|
98
|
+
data-testid={`checkout-shipping-option-${option.pk}`}
|
|
99
|
+
>
|
|
100
|
+
{option.name}
|
|
101
|
+
</Radio>
|
|
102
|
+
<span className="text-xs">
|
|
103
|
+
<Price value={option.shipping_amount} />
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
<Button
|
|
108
|
+
className="mt-2 w-full"
|
|
109
|
+
disabled={!steps.shipping.completed}
|
|
110
|
+
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
111
|
+
data-testid="checkout-shipping-save"
|
|
112
|
+
>
|
|
113
|
+
{t('checkout.address.shipping.button')}
|
|
114
|
+
</Button>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
{dataSourceShippingOptions && dataSourceShippingOptions.length > 0 && (
|
|
118
|
+
<>
|
|
119
|
+
{dataSourceShippingOptions.map((option) => (
|
|
120
|
+
<div key={option.pk}>
|
|
121
|
+
<h3 className="text-lg font-bold">{option?.name}</h3>
|
|
122
|
+
{option.data_source_shipping_options.map((opt) => (
|
|
123
|
+
<div
|
|
124
|
+
key={opt.pk}
|
|
125
|
+
className="py-4 border-t border-gray-400 flex justify-between"
|
|
126
|
+
>
|
|
127
|
+
<Radio
|
|
128
|
+
name={`data-source-shipping-${option.pk}`}
|
|
129
|
+
checked={
|
|
130
|
+
selectedPks?.some(
|
|
131
|
+
(item) => item.optionPk === opt.pk
|
|
132
|
+
) || false
|
|
133
|
+
}
|
|
134
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
135
|
+
handleRadioChange(e, option.pk)
|
|
136
|
+
}
|
|
137
|
+
value={opt.pk}
|
|
138
|
+
data-testid={`checkout-data-source-shipping-${opt.pk}`}
|
|
139
|
+
>
|
|
140
|
+
{opt?.shipping_option_name}
|
|
141
|
+
</Radio>
|
|
142
|
+
<span className="text-xs">
|
|
143
|
+
<Price value={opt?.shipping_amount} />
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
<Button
|
|
150
|
+
className="mt-2 w-full"
|
|
151
|
+
disabled={!steps.shipping.completed}
|
|
152
|
+
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
153
|
+
data-testid="checkout-shipping-save"
|
|
50
154
|
>
|
|
51
|
-
{
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</span>
|
|
56
|
-
</div>
|
|
57
|
-
))}
|
|
58
|
-
<Button
|
|
59
|
-
className="mt-2 w-full"
|
|
60
|
-
disabled={!steps.shipping.completed}
|
|
61
|
-
onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
|
|
62
|
-
data-testid="checkout-shipping-save"
|
|
63
|
-
>
|
|
64
|
-
{t('checkout.address.shipping.button')}
|
|
65
|
-
</Button>
|
|
155
|
+
{t('checkout.address.shipping.button')}
|
|
156
|
+
</Button>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
66
159
|
</div>
|
|
67
160
|
)}
|
|
68
161
|
</div>
|
|
@@ -162,10 +162,9 @@ export const FindInStore = ({ productPk, productName, variants }) => {
|
|
|
162
162
|
</div>
|
|
163
163
|
<Link
|
|
164
164
|
href={`https://maps.google.com/?q=${store.latitude},${store.longitude}`}
|
|
165
|
+
target="_blank"
|
|
165
166
|
>
|
|
166
|
-
<
|
|
167
|
-
<Button>{t('product.find_in_store.directions')}</Button>
|
|
168
|
-
</a>
|
|
167
|
+
<Button>{t('product.find_in_store.directions')}</Button>
|
|
169
168
|
</Link>
|
|
170
169
|
</div>
|
|
171
170
|
</Accordion>
|