@akinon/projectzero 1.103.0-snapshot-ZERO-3648-20251002191321 → 1.104.0-rc.83
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 +235 -5
- package/app-template/.env.example +1 -0
- package/app-template/.github/instructions/routing.instructions.md +603 -0
- package/app-template/CHANGELOG.md +5002 -310
- package/app-template/README.md +25 -1
- package/app-template/docs/sentry-usage.md +35 -0
- package/app-template/package.json +19 -19
- package/app-template/public/locales/en/checkout.json +6 -0
- package/app-template/public/locales/en/common.json +48 -1
- package/app-template/public/locales/tr/checkout.json +6 -0
- package/app-template/public/locales/tr/common.json +48 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +9 -82
- package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +12 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +67 -0
- package/app-template/src/app/api/form/[...id]/route.ts +1 -7
- package/app-template/src/app/api/image-proxy/route.ts +1 -0
- package/app-template/src/app/api/similar-product-list/route.ts +1 -0
- package/app-template/src/app/api/similar-products/route.ts +1 -0
- package/app-template/src/assets/fonts/pz-icon.css +3 -0
- package/app-template/src/components/__tests__/link.test.tsx +2 -0
- package/app-template/src/components/accordion.tsx +22 -19
- package/app-template/src/components/currency-select.tsx +1 -0
- package/app-template/src/components/file-input.tsx +27 -7
- package/app-template/src/components/generate-form-fields.tsx +43 -4
- package/app-template/src/components/input.tsx +9 -2
- package/app-template/src/components/modal.tsx +32 -16
- package/app-template/src/components/pagination.tsx +1 -0
- package/app-template/src/components/price.tsx +1 -1
- package/app-template/src/components/select.tsx +38 -26
- package/app-template/src/components/types/index.ts +25 -1
- package/app-template/src/hooks/index.ts +2 -0
- package/app-template/src/hooks/use-product-cart.ts +77 -0
- package/app-template/src/hooks/use-stock-alert.ts +74 -0
- package/app-template/src/plugins.js +3 -1
- package/app-template/src/settings.js +8 -2
- package/app-template/src/types/index.ts +17 -0
- package/app-template/src/utils/variant-validation.ts +41 -0
- package/app-template/src/views/account/address-form.tsx +8 -4
- package/app-template/src/views/account/contact-form.tsx +1 -1
- package/app-template/src/views/account/content-header.tsx +2 -2
- package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
- package/app-template/src/views/basket/basket-content.tsx +106 -0
- package/app-template/src/views/basket/basket-item.tsx +22 -14
- package/app-template/src/views/basket/summary.tsx +10 -7
- package/app-template/src/views/breadcrumb.tsx +2 -2
- package/app-template/src/views/category/category-info.tsx +1 -0
- package/app-template/src/views/category/filters/index.tsx +1 -1
- package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
- package/app-template/src/views/checkout/summary.tsx +10 -0
- package/app-template/src/views/guest-login/index.tsx +6 -1
- package/app-template/src/views/header/action-menu.tsx +1 -1
- package/app-template/src/views/header/search/index.tsx +17 -5
- package/app-template/src/views/product/product-actions.tsx +165 -0
- package/app-template/src/views/product/product-info.tsx +62 -263
- package/app-template/src/views/product/product-share.tsx +56 -0
- package/app-template/src/views/product/product-variants.tsx +26 -0
- package/app-template/src/views/product/slider.tsx +86 -73
- package/app-template/src/widgets/footer-menu.tsx +6 -2
- package/commands/plugins.ts +63 -20
- package/dist/commands/plugins.js +57 -20
- package/package.json +1 -1
|
@@ -259,23 +259,25 @@ export const AddressForm = (props: Props) => {
|
|
|
259
259
|
{/* TODO: Fix select and textarea components */}
|
|
260
260
|
|
|
261
261
|
<Select
|
|
262
|
-
className="w-full border-gray-500 text-sm
|
|
262
|
+
className="w-full border-gray-500 text-sm"
|
|
263
263
|
options={countryOptions}
|
|
264
264
|
{...register('country')}
|
|
265
265
|
error={errors.country}
|
|
266
266
|
data-testid="address-form-country"
|
|
267
267
|
label={t('account.address_book.form.country.title')}
|
|
268
|
+
labelClassName="mb-3"
|
|
268
269
|
required
|
|
269
270
|
/>
|
|
270
271
|
|
|
271
272
|
{city && (
|
|
272
273
|
<Select
|
|
273
|
-
className="w-full border-gray-500 text-sm
|
|
274
|
+
className="w-full border-gray-500 text-sm"
|
|
274
275
|
options={cityOptions}
|
|
275
276
|
{...register('city')}
|
|
276
277
|
error={errors.city}
|
|
277
278
|
data-testid="address-form-city"
|
|
278
279
|
label={t('account.address_book.form.province.title')}
|
|
280
|
+
labelClassName="mb-3"
|
|
279
281
|
required
|
|
280
282
|
/>
|
|
281
283
|
)}
|
|
@@ -283,24 +285,26 @@ export const AddressForm = (props: Props) => {
|
|
|
283
285
|
<div className="flex gap-4">
|
|
284
286
|
<div className="flex-1">
|
|
285
287
|
<Select
|
|
286
|
-
className="w-full border-gray-500 text-sm
|
|
288
|
+
className="w-full border-gray-500 text-sm"
|
|
287
289
|
options={townshipOptions}
|
|
288
290
|
{...register('township')}
|
|
289
291
|
error={errors.township}
|
|
290
292
|
data-testid="address-form-township"
|
|
291
293
|
label={t('account.address_book.form.township.title')}
|
|
294
|
+
labelClassName="mb-3"
|
|
292
295
|
required
|
|
293
296
|
/>
|
|
294
297
|
</div>
|
|
295
298
|
{district && (
|
|
296
299
|
<div className="flex-1">
|
|
297
300
|
<Select
|
|
298
|
-
className="w-full border-gray-500 text-sm
|
|
301
|
+
className="w-full border-gray-500 text-sm"
|
|
299
302
|
options={districtOptions}
|
|
300
303
|
{...register('district')}
|
|
301
304
|
error={errors.district}
|
|
302
305
|
data-testid="address-form-district"
|
|
303
306
|
label={t('account.address_book.form.district.title')}
|
|
307
|
+
labelClassName="mb-3"
|
|
304
308
|
required
|
|
305
309
|
/>
|
|
306
310
|
</div>
|
|
@@ -111,7 +111,7 @@ const ContactForm = () => {
|
|
|
111
111
|
resolver: yupResolver(contactFormSchema(t))
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
const onSubmit: SubmitHandler<ContactFormType> = (data
|
|
114
|
+
const onSubmit: SubmitHandler<ContactFormType> = (data) => {
|
|
115
115
|
const formData = new FormData();
|
|
116
116
|
|
|
117
117
|
Object.keys(data ?? {}).forEach((key) => {
|
|
@@ -31,13 +31,13 @@ export const ContentHeader = (props: Props) => {
|
|
|
31
31
|
</h3>
|
|
32
32
|
<Select
|
|
33
33
|
onChange={handleChange}
|
|
34
|
-
className="w-full mb-4 md:mb-0 md:w-56
|
|
34
|
+
className="w-full mb-4 md:mb-0 md:w-56 text-xs"
|
|
35
35
|
options={orders}
|
|
36
36
|
data-testid="account-orders-header-select"
|
|
37
37
|
></Select>
|
|
38
38
|
<Button
|
|
39
39
|
className={clsx(
|
|
40
|
-
'w-full md:w-56',
|
|
40
|
+
'w-full md:w-56 md:ms-4',
|
|
41
41
|
isButtonDisabled &&
|
|
42
42
|
'hover:bg-black hover:text-white disabled:opacity-75'
|
|
43
43
|
)}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Accordion, LoaderSpinner, TabPanel, Tabs } from '@theme/components';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
|
|
6
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
searchKey?: string;
|
|
@@ -11,6 +12,7 @@ interface Props {
|
|
|
11
12
|
export function FaqTabs(props: Props) {
|
|
12
13
|
const { searchKey } = props;
|
|
13
14
|
const { data, isLoading } = useGetWidgetQuery('faq');
|
|
15
|
+
const { locale } = useLocalization();
|
|
14
16
|
|
|
15
17
|
if (isLoading) {
|
|
16
18
|
return <LoaderSpinner className="mt-4" />;
|
|
@@ -29,8 +31,12 @@ export function FaqTabs(props: Props) {
|
|
|
29
31
|
{data?.attributes?.faq_contents
|
|
30
32
|
?.filter(
|
|
31
33
|
(faq) =>
|
|
32
|
-
faq.value.content
|
|
33
|
-
|
|
34
|
+
faq.value.content
|
|
35
|
+
.toLocaleLowerCase(locale)
|
|
36
|
+
.includes(searchKey) ||
|
|
37
|
+
faq.value.title
|
|
38
|
+
.toLocaleLowerCase(locale)
|
|
39
|
+
.includes(searchKey)
|
|
34
40
|
)
|
|
35
41
|
.map((faq, index) => {
|
|
36
42
|
if (faq.value.category == item.value.category_id) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
4
|
+
import { Basket } from '@akinon/next/types';
|
|
5
|
+
import { Button, LoaderSpinner, Link } from '@theme/components';
|
|
6
|
+
import { BasketItem, Summary } from '@theme/views/basket';
|
|
7
|
+
import { ROUTES } from '@theme/routes';
|
|
8
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
import { pushCartView } from '@theme/utils/gtm';
|
|
11
|
+
|
|
12
|
+
interface BasketContentProps {
|
|
13
|
+
initialBasket: Basket;
|
|
14
|
+
multiBasket: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function BasketContent({
|
|
18
|
+
initialBasket,
|
|
19
|
+
multiBasket
|
|
20
|
+
}: BasketContentProps) {
|
|
21
|
+
const { t } = useLocalization();
|
|
22
|
+
const [basket, setBasket] = useState<Basket>(initialBasket);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (basket) {
|
|
26
|
+
const products = basket.basketitem_set.map((basketItem) => ({
|
|
27
|
+
...basketItem.product
|
|
28
|
+
}));
|
|
29
|
+
pushCartView(products);
|
|
30
|
+
}
|
|
31
|
+
}, [basket]);
|
|
32
|
+
|
|
33
|
+
const handleBasketUpdate = (updatedBasket: Basket) => {
|
|
34
|
+
setBasket(updatedBasket);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!basket) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex justify-center w-full">
|
|
40
|
+
<LoaderSpinner />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="max-w-screen-xl p-4 flex flex-col text-primary-800 lg:p-8 xl:flex-row xl:mx-auto">
|
|
47
|
+
{basket.basketitem_set && basket.basketitem_set.length > 0 ? (
|
|
48
|
+
<>
|
|
49
|
+
<div className="flex-1 xl:mr-16">
|
|
50
|
+
<div className="flex items-center justify-between py-2 border-b border-gray-200 lg:py-3">
|
|
51
|
+
<h2 className="text-xl lg:text-2xl font-light">
|
|
52
|
+
{t('basket.my_cart')}
|
|
53
|
+
</h2>
|
|
54
|
+
<Link
|
|
55
|
+
href={ROUTES.HOME}
|
|
56
|
+
className="text-xs hover:text-secondary-500"
|
|
57
|
+
>
|
|
58
|
+
{t('basket.back_to_shopping')}
|
|
59
|
+
</Link>
|
|
60
|
+
</div>
|
|
61
|
+
<ul>
|
|
62
|
+
{multiBasket ? (
|
|
63
|
+
<PluginModule
|
|
64
|
+
component={Component.MultiBasket}
|
|
65
|
+
props={{
|
|
66
|
+
BasketItem,
|
|
67
|
+
onBasketUpdate: handleBasketUpdate
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
) : (
|
|
71
|
+
basket.basketitem_set.map((basketItem, index) => (
|
|
72
|
+
<BasketItem
|
|
73
|
+
key={index}
|
|
74
|
+
basketItem={basketItem}
|
|
75
|
+
onBasketUpdate={handleBasketUpdate}
|
|
76
|
+
/>
|
|
77
|
+
))
|
|
78
|
+
)}
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
<Summary basket={basket} onBasketUpdate={handleBasketUpdate} />
|
|
82
|
+
</>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="flex flex-col items-center container max-w-screen-sm py-4 px-4 xs:py-6 xs:px-6 sm:py-8 sm:px-8 lg:max-w-screen-xl">
|
|
85
|
+
<h1
|
|
86
|
+
className="w-full text-xl font-light text-secondary text-center sm:text-2xl"
|
|
87
|
+
data-testid="basket-empty"
|
|
88
|
+
>
|
|
89
|
+
{t('basket.empty.title')}
|
|
90
|
+
</h1>
|
|
91
|
+
|
|
92
|
+
<div className="w-full text-sm text-black-800 text-center my-4 mb-2 sm:text-base">
|
|
93
|
+
<p>{t('basket.empty.content_first')}</p>
|
|
94
|
+
<p>{t('basket.empty.content_second')}.</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<Link href={ROUTES.HOME} passHref>
|
|
98
|
+
<Button className="px-10 mt-2" appearance="filled">
|
|
99
|
+
{t('basket.empty.button')}
|
|
100
|
+
</Button>
|
|
101
|
+
</Link>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
useUpdateQuantityMutation
|
|
4
4
|
} from '@akinon/next/data/client/basket';
|
|
5
5
|
import { useAppDispatch } from '@akinon/next/redux/hooks';
|
|
6
|
-
import { BasketItem as BasketItemType } from '@akinon/next/types';
|
|
6
|
+
import { Basket, BasketItem as BasketItemType } from '@akinon/next/types';
|
|
7
7
|
import { Price, Button, Icon, Modal, Select, Link } from '@theme/components';
|
|
8
8
|
import { useState } from 'react';
|
|
9
9
|
import { useAddFavoriteMutation } from '@akinon/next/data/client/wishlist';
|
|
@@ -19,11 +19,12 @@ import { pushRemoveFromCart } from '@theme/utils/gtm';
|
|
|
19
19
|
interface Props {
|
|
20
20
|
basketItem?: BasketItemType;
|
|
21
21
|
namespace?: string;
|
|
22
|
+
onBasketUpdate?: (basket: Basket) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const BasketItem = (props: Props) => {
|
|
25
26
|
const { t } = useLocalization();
|
|
26
|
-
const { basketItem, namespace } = props;
|
|
27
|
+
const { basketItem, namespace, onBasketUpdate } = props;
|
|
27
28
|
const [updateQuantityMutation] = useUpdateQuantityMutation();
|
|
28
29
|
const dispatch = useAppDispatch();
|
|
29
30
|
const [isRemoveBasketModalOpen, setRemoveBasketModalOpen] = useState(false);
|
|
@@ -39,7 +40,12 @@ export const BasketItem = (props: Props) => {
|
|
|
39
40
|
quantity: number,
|
|
40
41
|
attributes: object = {}
|
|
41
42
|
) => {
|
|
42
|
-
const requestParams:
|
|
43
|
+
const requestParams: {
|
|
44
|
+
product: number;
|
|
45
|
+
quantity: number;
|
|
46
|
+
attributes: object;
|
|
47
|
+
namespace?: string;
|
|
48
|
+
} = {
|
|
43
49
|
product: productPk,
|
|
44
50
|
quantity,
|
|
45
51
|
attributes
|
|
@@ -49,19 +55,21 @@ export const BasketItem = (props: Props) => {
|
|
|
49
55
|
requestParams.namespace = namespace;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
.unwrap()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
(draftBasket)
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
)
|
|
58
|
+
try {
|
|
59
|
+
const response = await updateQuantityMutation(requestParams).unwrap();
|
|
60
|
+
dispatch(
|
|
61
|
+
basketApi.util.updateQueryData(
|
|
62
|
+
'getBasket',
|
|
63
|
+
undefined,
|
|
64
|
+
(draftBasket) => {
|
|
65
|
+
Object.assign(draftBasket, response.basket);
|
|
66
|
+
}
|
|
63
67
|
)
|
|
64
68
|
);
|
|
69
|
+
onBasketUpdate?.(response.basket);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error updating quantity:', error);
|
|
72
|
+
}
|
|
65
73
|
};
|
|
66
74
|
|
|
67
75
|
const deleteProduct = async (productPk?: number) => {
|
|
@@ -18,6 +18,7 @@ import clsx from 'clsx';
|
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
basket: Basket;
|
|
21
|
+
onBasketUpdate?: (basket: Basket) => void;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const voucherCodeFormSchema = (t) =>
|
|
@@ -27,7 +28,7 @@ const voucherCodeFormSchema = (t) =>
|
|
|
27
28
|
|
|
28
29
|
export const Summary = (props: Props) => {
|
|
29
30
|
const { t } = useLocalization();
|
|
30
|
-
const { basket } = props;
|
|
31
|
+
const { basket, onBasketUpdate } = props;
|
|
31
32
|
const router = useRouter();
|
|
32
33
|
const {
|
|
33
34
|
register,
|
|
@@ -53,7 +54,7 @@ export const Summary = (props: Props) => {
|
|
|
53
54
|
const removeVoucherCode = () => {
|
|
54
55
|
removeVoucherCodeMutation()
|
|
55
56
|
.unwrap()
|
|
56
|
-
.then((basket) =>
|
|
57
|
+
.then((basket) => {
|
|
57
58
|
dispatch(
|
|
58
59
|
basketApi.util.updateQueryData(
|
|
59
60
|
'getBasket',
|
|
@@ -62,8 +63,9 @@ export const Summary = (props: Props) => {
|
|
|
62
63
|
Object.assign(draftBasket, basket);
|
|
63
64
|
}
|
|
64
65
|
)
|
|
65
|
-
)
|
|
66
|
-
|
|
66
|
+
);
|
|
67
|
+
onBasketUpdate?.(basket);
|
|
68
|
+
})
|
|
67
69
|
.catch((error: Error) => {
|
|
68
70
|
setError('voucherCode', { message: error.data.non_field_errors });
|
|
69
71
|
});
|
|
@@ -74,7 +76,7 @@ export const Summary = (props: Props) => {
|
|
|
74
76
|
voucher_code: data.voucherCode
|
|
75
77
|
})
|
|
76
78
|
.unwrap()
|
|
77
|
-
.then((basket) =>
|
|
79
|
+
.then((basket) => {
|
|
78
80
|
dispatch(
|
|
79
81
|
basketApi.util.updateQueryData(
|
|
80
82
|
'getBasket',
|
|
@@ -83,8 +85,9 @@ export const Summary = (props: Props) => {
|
|
|
83
85
|
Object.assign(draftBasket, basket);
|
|
84
86
|
}
|
|
85
87
|
)
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
+
);
|
|
89
|
+
onBasketUpdate?.(basket);
|
|
90
|
+
})
|
|
88
91
|
.catch((error: Error) => {
|
|
89
92
|
setError('voucherCode', { message: error.data.non_field_errors });
|
|
90
93
|
});
|
|
@@ -12,7 +12,7 @@ export interface BreadcrumbProps {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export default function Breadcrumb(props: BreadcrumbProps) {
|
|
15
|
-
const { t } = useLocalization();
|
|
15
|
+
const { t, locale } = useLocalization();
|
|
16
16
|
const { breadcrumbList = [] } = props;
|
|
17
17
|
|
|
18
18
|
const list = [
|
|
@@ -28,7 +28,7 @@ export default function Breadcrumb(props: BreadcrumbProps) {
|
|
|
28
28
|
{list.map((item, index) => (
|
|
29
29
|
<Fragment key={index}>
|
|
30
30
|
<Link href={item.url}>
|
|
31
|
-
{capitalize(item.text.toLocaleLowerCase())}
|
|
31
|
+
{capitalize(item.text.toLocaleLowerCase(locale))}
|
|
32
32
|
</Link>
|
|
33
33
|
{index !== list.length - 1 && <Icon name="chevron-end" size={8} />}
|
|
34
34
|
</Fragment>
|
|
@@ -57,6 +57,7 @@ export default function ListPage(props: ListPageProps) {
|
|
|
57
57
|
newUrl.searchParams.delete('page');
|
|
58
58
|
router.push(newUrl.pathname + newUrl.search, undefined);
|
|
59
59
|
}
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
61
|
}, [searchParams, data.products, page]);
|
|
61
62
|
|
|
62
63
|
const { t } = useLocalization();
|
|
@@ -6,7 +6,7 @@ import { useLocalization } from '@akinon/next/hooks';
|
|
|
6
6
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
7
7
|
import { resetSelectedFacets } from '@theme/redux/reducers/category';
|
|
8
8
|
import CategoryActiveFilters from '@theme/views/category/category-active-filters';
|
|
9
|
-
import { useMemo,
|
|
9
|
+
import { useMemo, useTransition } from 'react';
|
|
10
10
|
import { FilterItem } from './filter-item';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import {
|
|
3
|
+
useGetCheckoutLoyaltyBalanceQuery,
|
|
4
|
+
usePayWithLoyaltyBalanceMutation
|
|
5
|
+
} from '@akinon/next/data/client/checkout';
|
|
6
|
+
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
9
|
+
import { twMerge } from 'tailwind-merge';
|
|
10
|
+
import { Icon, Price } from '@theme/components';
|
|
11
|
+
import { Trans } from '@akinon/next/components';
|
|
12
|
+
import { LoaderSpinner } from '@theme/components';
|
|
13
|
+
|
|
14
|
+
export const StoreCredits = () => {
|
|
15
|
+
const { t } = useLocalization();
|
|
16
|
+
|
|
17
|
+
const [payWithLoyaltyBalance, { isLoading: isPayWithLoyaltyBalanceLoading }] =
|
|
18
|
+
usePayWithLoyaltyBalanceMutation();
|
|
19
|
+
|
|
20
|
+
const { loyaltyBalance, preOrder } = useAppSelector(
|
|
21
|
+
(state) => state.checkout
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const { isLoading: isLoyaltyBalanceLoading } =
|
|
25
|
+
useGetCheckoutLoyaltyBalanceQuery(undefined, {
|
|
26
|
+
refetchOnMountOrArgChange: true,
|
|
27
|
+
skip: !preOrder?.payment_option
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const isLoyaltyBalanceUsed = useMemo(() => {
|
|
31
|
+
return parseFloat(preOrder?.loyalty_money ?? '0') > 0;
|
|
32
|
+
}, [preOrder?.loyalty_money]);
|
|
33
|
+
|
|
34
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
|
|
36
|
+
const handleClick = async () => {
|
|
37
|
+
if (isLoading) return;
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await payWithLoyaltyBalance(isLoyaltyBalanceUsed ? '0' : loyaltyBalance);
|
|
42
|
+
} finally {
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (preOrder?.is_guest) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isLoyaltyBalanceLoading) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="mb-3 px-4 py-3 xs:px-0">
|
|
54
|
+
<LoaderSpinner />
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parseFloat(loyaltyBalance) <= 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className={twMerge(
|
|
65
|
+
'hidden flex-col w-full mb-4 border border-solid border-gray-400 px-0 md:p-4',
|
|
66
|
+
isPayWithLoyaltyBalanceLoading && 'pointer-events-none opacity-30',
|
|
67
|
+
parseFloat(loyaltyBalance) > 0 && 'block'
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<div className="flex w-full items-center">
|
|
71
|
+
<button onClick={handleClick}>
|
|
72
|
+
<span
|
|
73
|
+
className={clsx(
|
|
74
|
+
'flex h-5 w-5 items-center justify-center rounded border border-solid border-primary',
|
|
75
|
+
isLoyaltyBalanceUsed ? 'bg-primary' : 'bg-white'
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<Icon
|
|
79
|
+
name={isLoyaltyBalanceUsed ? 'check' : ''}
|
|
80
|
+
size={10}
|
|
81
|
+
className={clsx({ 'text-white': isLoyaltyBalanceUsed })}
|
|
82
|
+
/>
|
|
83
|
+
</span>
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<div className="w-full pl-4">
|
|
87
|
+
<p className="cursor-pointer text-sm" onClick={handleClick}>
|
|
88
|
+
{t('checkout.payment.store_credit.use_my_store_credits')}
|
|
89
|
+
</p>
|
|
90
|
+
<p className="flex text-sm text-[#606060]">
|
|
91
|
+
{t('checkout.payment.store_credit.available_balance')}:
|
|
92
|
+
<Price
|
|
93
|
+
value={loyaltyBalance}
|
|
94
|
+
currencyCode={preOrder?.currency_type_label}
|
|
95
|
+
className="pe-1 font-bold"
|
|
96
|
+
/>
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{isLoyaltyBalanceUsed && parseFloat(preOrder?.unpaid_amount) > 0 && (
|
|
102
|
+
<p className="my-4 text-[15px] font-light italic text-[#707070] max-xs:text-xs">
|
|
103
|
+
<Trans
|
|
104
|
+
i18nKey="checkout.payment.store_credit.insufficient_balance"
|
|
105
|
+
components={{
|
|
106
|
+
Amount: (
|
|
107
|
+
<div className="inline-flex">
|
|
108
|
+
<Price
|
|
109
|
+
value={preOrder?.unpaid_amount}
|
|
110
|
+
currencyCode={preOrder?.currency_type_label}
|
|
111
|
+
className="text-primary"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -8,6 +8,7 @@ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
|
8
8
|
import { twMerge } from 'tailwind-merge';
|
|
9
9
|
import { Image } from '@akinon/next/components/image';
|
|
10
10
|
import { Trans } from '@akinon/next/components/trans';
|
|
11
|
+
import { StoreCredits } from './steps/payment/options/store-credit';
|
|
11
12
|
|
|
12
13
|
export const Summary = () => {
|
|
13
14
|
const { t } = useLocalization();
|
|
@@ -38,6 +39,7 @@ export const Summary = () => {
|
|
|
38
39
|
'flex flex-col w-full mb-4 border border-solid border-gray-400'
|
|
39
40
|
}}
|
|
40
41
|
/>
|
|
42
|
+
<StoreCredits />
|
|
41
43
|
<div className="flex flex-col w-full border border-solid border-gray-400">
|
|
42
44
|
<div className="flex justify-between items-center flex-row border-b border-solid border-gray-400 px-4 py-2 sm:px-5 sm:py-4 sm:min-h-15">
|
|
43
45
|
<span className="text-black-800 text-xl font-light sm:text-2xl">
|
|
@@ -118,6 +120,14 @@ export const Summary = () => {
|
|
|
118
120
|
<Price value={preOrder?.shipping_amount} />
|
|
119
121
|
</span>
|
|
120
122
|
</div>
|
|
123
|
+
{parseFloat(preOrder?.loyalty_money) > 0 && (
|
|
124
|
+
<div className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5">
|
|
125
|
+
<span>{t('checkout.summary.loyalty_money_total')}</span>
|
|
126
|
+
<span>
|
|
127
|
+
<Price value={preOrder?.loyalty_money} />
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
121
131
|
<div className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5">
|
|
122
132
|
<span>{t('checkout.summary.discounts_total')}</span>
|
|
123
133
|
<span>
|
|
@@ -51,9 +51,14 @@ const GuestLogin = () => {
|
|
|
51
51
|
).unwrap();
|
|
52
52
|
|
|
53
53
|
Object.keys(response?.errors || {}).forEach((key) => {
|
|
54
|
+
const errorValue = response?.errors[key];
|
|
55
|
+
const message = Array.isArray(errorValue)
|
|
56
|
+
? errorValue.join(', ')
|
|
57
|
+
: errorValue || '';
|
|
58
|
+
|
|
54
59
|
setError(key as keyof GuestLoginFormParams, {
|
|
55
60
|
type: 'custom',
|
|
56
|
-
message
|
|
61
|
+
message
|
|
57
62
|
});
|
|
58
63
|
});
|
|
59
64
|
} catch (error) {
|
|
@@ -4,11 +4,11 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
5
5
|
import { closeSearch } from '@akinon/next/redux/reducers/header';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
|
-
|
|
8
|
-
import { Icon } from '@theme/components';
|
|
7
|
+
import { Icon, Input } from '@theme/components';
|
|
9
8
|
import Results from './results';
|
|
10
9
|
import { ROUTES } from '@theme/routes';
|
|
11
10
|
import { useLocalization, useRouter } from '@akinon/next/hooks';
|
|
11
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
12
12
|
|
|
13
13
|
export default function Search() {
|
|
14
14
|
const { t } = useLocalization();
|
|
@@ -41,6 +41,14 @@ export default function Search() {
|
|
|
41
41
|
};
|
|
42
42
|
}, [isSearchOpen, dispatch]);
|
|
43
43
|
|
|
44
|
+
const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
setSearchText(e.target.value);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleCloseSearch = () => {
|
|
49
|
+
dispatch(closeSearch());
|
|
50
|
+
};
|
|
51
|
+
|
|
44
52
|
return (
|
|
45
53
|
<>
|
|
46
54
|
<div
|
|
@@ -66,9 +74,9 @@ export default function Search() {
|
|
|
66
74
|
{t('common.search.results_for')}
|
|
67
75
|
</span>
|
|
68
76
|
<div className="flex items-center">
|
|
69
|
-
<
|
|
77
|
+
<Input
|
|
70
78
|
value={searchText}
|
|
71
|
-
onChange={
|
|
79
|
+
onChange={handleSearchTextChange}
|
|
72
80
|
onKeyDown={(e) => {
|
|
73
81
|
if (e.key === 'Enter' && searchText.trim() !== '') {
|
|
74
82
|
router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
|
|
@@ -78,14 +86,18 @@ export default function Search() {
|
|
|
78
86
|
placeholder={t('common.search.placeholder')}
|
|
79
87
|
ref={inputRef}
|
|
80
88
|
/>
|
|
89
|
+
|
|
90
|
+
<PluginModule component={Component.HeaderImageSearchFeature} />
|
|
91
|
+
|
|
81
92
|
<Icon
|
|
82
93
|
name="close"
|
|
83
94
|
size={14}
|
|
84
|
-
onClick={
|
|
95
|
+
onClick={handleCloseSearch}
|
|
85
96
|
className="cursor-pointer"
|
|
86
97
|
/>
|
|
87
98
|
</div>
|
|
88
99
|
</div>
|
|
100
|
+
|
|
89
101
|
<Results searchText={searchText} />
|
|
90
102
|
</div>
|
|
91
103
|
</div>
|