@akinon/projectzero 1.99.0-rc.69 → 1.99.0-snapshot-ZERO-3640-20250919140314
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 +5 -238
- package/app-template/.env.example +0 -1
- package/app-template/CHANGELOG.md +304 -5041
- package/app-template/README.md +1 -25
- package/app-template/package.json +19 -21
- package/app-template/public/locales/en/common.json +1 -42
- package/app-template/public/locales/tr/common.json +1 -42
- package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +82 -9
- package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +4 -17
- package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +1 -12
- package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +11 -29
- package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +1 -12
- package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +10 -28
- package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +1 -12
- package/app-template/src/assets/fonts/pz-icon.css +0 -3
- package/app-template/src/components/__tests__/link.test.tsx +0 -2
- package/app-template/src/components/accordion.tsx +19 -22
- package/app-template/src/components/currency-select.tsx +0 -1
- package/app-template/src/components/file-input.tsx +7 -27
- package/app-template/src/components/input.tsx +2 -9
- package/app-template/src/components/modal.tsx +16 -32
- package/app-template/src/components/pagination.tsx +0 -1
- package/app-template/src/components/select.tsx +26 -38
- package/app-template/src/components/types/index.ts +1 -25
- package/app-template/src/hooks/index.ts +0 -2
- package/app-template/src/plugins.js +1 -3
- package/app-template/src/settings.js +2 -8
- package/app-template/src/types/index.ts +3 -74
- package/app-template/src/views/account/address-form.tsx +4 -8
- 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 +2 -8
- package/app-template/src/views/basket/basket-item.tsx +14 -22
- package/app-template/src/views/basket/summary.tsx +7 -10
- package/app-template/src/views/breadcrumb.tsx +2 -2
- package/app-template/src/views/category/category-info.tsx +0 -1
- package/app-template/src/views/category/filters/index.tsx +1 -1
- package/app-template/src/views/guest-login/index.tsx +1 -6
- package/app-template/src/views/header/action-menu.tsx +1 -1
- package/app-template/src/views/header/search/index.tsx +5 -17
- package/app-template/src/views/login/index.tsx +10 -11
- package/app-template/src/views/otp-login/index.tsx +6 -11
- package/app-template/src/views/product/product-info.tsx +263 -62
- package/app-template/src/views/product/slider.tsx +73 -86
- package/app-template/src/views/register/index.tsx +11 -15
- package/app-template/src/widgets/footer-menu.tsx +2 -6
- package/commands/plugins.ts +16 -63
- package/dist/commands/plugins.js +16 -57
- package/package.json +1 -1
- package/app-template/.github/instructions/account.instructions.md +0 -749
- package/app-template/.github/instructions/edge-cases.instructions.md +0 -73
- package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
- package/app-template/src/app/api/image-proxy/route.ts +0 -1
- package/app-template/src/app/api/similar-product-list/route.ts +0 -1
- package/app-template/src/app/api/similar-products/route.ts +0 -1
- package/app-template/src/hooks/use-product-cart.ts +0 -77
- package/app-template/src/hooks/use-stock-alert.ts +0 -74
- package/app-template/src/utils/variant-validation.ts +0 -41
- package/app-template/src/views/basket/basket-content.tsx +0 -106
- package/app-template/src/views/product/product-actions.tsx +0 -165
- package/app-template/src/views/product/product-share.tsx +0 -56
- package/app-template/src/views/product/product-variants.tsx +0 -26
|
@@ -18,7 +18,6 @@ import clsx from 'clsx';
|
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
basket: Basket;
|
|
21
|
-
onBasketUpdate?: (basket: Basket) => void;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
const voucherCodeFormSchema = (t) =>
|
|
@@ -28,7 +27,7 @@ const voucherCodeFormSchema = (t) =>
|
|
|
28
27
|
|
|
29
28
|
export const Summary = (props: Props) => {
|
|
30
29
|
const { t } = useLocalization();
|
|
31
|
-
const { basket
|
|
30
|
+
const { basket } = props;
|
|
32
31
|
const router = useRouter();
|
|
33
32
|
const {
|
|
34
33
|
register,
|
|
@@ -54,7 +53,7 @@ export const Summary = (props: Props) => {
|
|
|
54
53
|
const removeVoucherCode = () => {
|
|
55
54
|
removeVoucherCodeMutation()
|
|
56
55
|
.unwrap()
|
|
57
|
-
.then((basket) =>
|
|
56
|
+
.then((basket) =>
|
|
58
57
|
dispatch(
|
|
59
58
|
basketApi.util.updateQueryData(
|
|
60
59
|
'getBasket',
|
|
@@ -63,9 +62,8 @@ export const Summary = (props: Props) => {
|
|
|
63
62
|
Object.assign(draftBasket, basket);
|
|
64
63
|
}
|
|
65
64
|
)
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
})
|
|
65
|
+
)
|
|
66
|
+
)
|
|
69
67
|
.catch((error: Error) => {
|
|
70
68
|
setError('voucherCode', { message: error.data.non_field_errors });
|
|
71
69
|
});
|
|
@@ -76,7 +74,7 @@ export const Summary = (props: Props) => {
|
|
|
76
74
|
voucher_code: data.voucherCode
|
|
77
75
|
})
|
|
78
76
|
.unwrap()
|
|
79
|
-
.then((basket) =>
|
|
77
|
+
.then((basket) =>
|
|
80
78
|
dispatch(
|
|
81
79
|
basketApi.util.updateQueryData(
|
|
82
80
|
'getBasket',
|
|
@@ -85,9 +83,8 @@ export const Summary = (props: Props) => {
|
|
|
85
83
|
Object.assign(draftBasket, basket);
|
|
86
84
|
}
|
|
87
85
|
)
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
})
|
|
86
|
+
)
|
|
87
|
+
)
|
|
91
88
|
.catch((error: Error) => {
|
|
92
89
|
setError('voucherCode', { message: error.data.non_field_errors });
|
|
93
90
|
});
|
|
@@ -12,7 +12,7 @@ export interface BreadcrumbProps {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export default function Breadcrumb(props: BreadcrumbProps) {
|
|
15
|
-
const { t
|
|
15
|
+
const { t } = 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())}
|
|
32
32
|
</Link>
|
|
33
33
|
{index !== list.length - 1 && <Icon name="chevron-end" size={8} />}
|
|
34
34
|
</Fragment>
|
|
@@ -57,7 +57,6 @@ 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
|
|
61
60
|
}, [searchParams, data.products, page]);
|
|
62
61
|
|
|
63
62
|
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, useTransition } from 'react';
|
|
9
|
+
import { useMemo, useState, useTransition } from 'react';
|
|
10
10
|
import { FilterItem } from './filter-item';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
@@ -51,14 +51,9 @@ 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
|
-
|
|
59
54
|
setError(key as keyof GuestLoginFormParams, {
|
|
60
55
|
type: 'custom',
|
|
61
|
-
message
|
|
56
|
+
message: response?.errors[key]?.join(', ')
|
|
62
57
|
});
|
|
63
58
|
});
|
|
64
59
|
} 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
|
-
|
|
7
|
+
|
|
8
|
+
import { Icon } from '@theme/components';
|
|
8
9
|
import Results from './results';
|
|
9
10
|
import { ROUTES } from '@theme/routes';
|
|
10
11
|
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,14 +41,6 @@ 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
|
-
|
|
52
44
|
return (
|
|
53
45
|
<>
|
|
54
46
|
<div
|
|
@@ -74,9 +66,9 @@ export default function Search() {
|
|
|
74
66
|
{t('common.search.results_for')}
|
|
75
67
|
</span>
|
|
76
68
|
<div className="flex items-center">
|
|
77
|
-
<
|
|
69
|
+
<input
|
|
78
70
|
value={searchText}
|
|
79
|
-
onChange={
|
|
71
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
80
72
|
onKeyDown={(e) => {
|
|
81
73
|
if (e.key === 'Enter' && searchText.trim() !== '') {
|
|
82
74
|
router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
|
|
@@ -86,18 +78,14 @@ export default function Search() {
|
|
|
86
78
|
placeholder={t('common.search.placeholder')}
|
|
87
79
|
ref={inputRef}
|
|
88
80
|
/>
|
|
89
|
-
|
|
90
|
-
<PluginModule component={Component.HeaderImageSearchFeature} />
|
|
91
|
-
|
|
92
81
|
<Icon
|
|
93
82
|
name="close"
|
|
94
83
|
size={14}
|
|
95
|
-
onClick={
|
|
84
|
+
onClick={() => dispatch(closeSearch())}
|
|
96
85
|
className="cursor-pointer"
|
|
97
86
|
/>
|
|
98
87
|
</div>
|
|
99
88
|
</div>
|
|
100
|
-
|
|
101
89
|
<Results searchText={searchText} />
|
|
102
90
|
</div>
|
|
103
91
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { signIn } from 'next-auth/react';
|
|
3
|
+
import { signIn, SignInOptions } from 'next-auth/react';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { useState } from 'react';
|
|
6
6
|
import { ROUTES } from '@theme/routes';
|
|
7
|
-
import { LoginFormType
|
|
7
|
+
import { LoginFormType } from '@theme/types';
|
|
8
8
|
import { Button, Input, Link } from '@theme/components';
|
|
9
9
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
|
10
10
|
import * as yup from 'yup';
|
|
@@ -79,9 +79,8 @@ export const Login = () => {
|
|
|
79
79
|
redirect: false,
|
|
80
80
|
callbackUrl: searchParams.get('callbackUrl') ?? '/',
|
|
81
81
|
captchaValidated,
|
|
82
|
-
...data
|
|
83
|
-
|
|
84
|
-
} as PzSignInOptions);
|
|
82
|
+
...data
|
|
83
|
+
} as SignInOptions);
|
|
85
84
|
|
|
86
85
|
if (loginResponse.error) {
|
|
87
86
|
const errors: AuthError[] = JSON.parse(loginResponse.error);
|
|
@@ -110,25 +109,25 @@ export const Login = () => {
|
|
|
110
109
|
try {
|
|
111
110
|
parsedValue = JSON.parse(item.value);
|
|
112
111
|
} catch {
|
|
113
|
-
parsedValue = [item.value];
|
|
112
|
+
parsedValue = [item.value];
|
|
114
113
|
}
|
|
115
114
|
} else {
|
|
116
|
-
parsedValue = item.value;
|
|
115
|
+
parsedValue = item.value;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
if (Array.isArray(parsedValue)) {
|
|
120
119
|
setError(item.name as keyof LoginFormType, {
|
|
121
120
|
type: 'custom',
|
|
122
|
-
message: parsedValue.join(', ')
|
|
121
|
+
message: parsedValue.join(', '),
|
|
123
122
|
});
|
|
124
123
|
} else {
|
|
125
124
|
Object.keys(parsedValue).forEach((key) => {
|
|
126
125
|
const fieldName = key as keyof LoginFormType;
|
|
127
126
|
const errorMessages = parsedValue[key] as string[];
|
|
128
|
-
|
|
127
|
+
|
|
129
128
|
setError(fieldName, {
|
|
130
129
|
type: 'custom',
|
|
131
|
-
message: errorMessages.join(', ')
|
|
130
|
+
message: errorMessages.join(', '),
|
|
132
131
|
});
|
|
133
132
|
});
|
|
134
133
|
}
|
|
@@ -162,7 +161,7 @@ export const Login = () => {
|
|
|
162
161
|
method="post"
|
|
163
162
|
onSubmit={handleSubmit(onSubmit)}
|
|
164
163
|
>
|
|
165
|
-
<input type="hidden" value=
|
|
164
|
+
<input type="hidden" value="login" {...register('formType')} />
|
|
166
165
|
<input type="hidden" value={locale} {...register('locale')} />
|
|
167
166
|
|
|
168
167
|
<div className={clsx(errors.email ? 'mb-8' : 'mb-4')}>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { signIn } from 'next-auth/react';
|
|
3
|
+
import { signIn, SignInOptions } from 'next-auth/react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
|
-
import { OtpLoginFormType
|
|
5
|
+
import { OtpLoginFormType } from '@theme/types';
|
|
6
6
|
import { Button, Input } from '@theme/components';
|
|
7
7
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
|
8
8
|
import * as yup from 'yup';
|
|
@@ -47,9 +47,8 @@ export const OtpLogin = () => {
|
|
|
47
47
|
const loginResponse = await signIn('default', {
|
|
48
48
|
redirect: false,
|
|
49
49
|
callbackUrl: '/',
|
|
50
|
-
...data
|
|
51
|
-
|
|
52
|
-
} as PzSignInOptions);
|
|
50
|
+
...data
|
|
51
|
+
} as SignInOptions);
|
|
53
52
|
|
|
54
53
|
if (loginResponse.error) {
|
|
55
54
|
const errors: AuthError[] = JSON.parse(loginResponse.error);
|
|
@@ -101,11 +100,7 @@ export const OtpLogin = () => {
|
|
|
101
100
|
{t('auth.login.title_gsm')}
|
|
102
101
|
</h2>
|
|
103
102
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
104
|
-
<input
|
|
105
|
-
type="hidden"
|
|
106
|
-
value={FormType.otpLogin}
|
|
107
|
-
{...register('formType')}
|
|
108
|
-
/>
|
|
103
|
+
<input type="hidden" value="otpLogin" {...register('formType')} />
|
|
109
104
|
<input type="hidden" value={locale} {...register('locale')} />
|
|
110
105
|
|
|
111
106
|
<div className="mb-4">
|
|
@@ -114,7 +109,7 @@ export const OtpLogin = () => {
|
|
|
114
109
|
className="h-14"
|
|
115
110
|
label={t('auth.login.form.phone.placeholder')}
|
|
116
111
|
type="tel"
|
|
117
|
-
format={user_phone_format.replace(
|
|
112
|
+
format={user_phone_format.replace(/\9/g, '#')}
|
|
118
113
|
mask="_"
|
|
119
114
|
allowEmptyFormatting={true}
|
|
120
115
|
control={control}
|
|
@@ -1,73 +1,177 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
+
import { Button, Icon, Modal } from '@theme/components';
|
|
5
|
+
import { useAddProductToBasket } from '../../hooks';
|
|
4
6
|
import React, { useEffect, useState } from 'react';
|
|
5
|
-
import {
|
|
7
|
+
import { useAddStockAlertMutation } from '@akinon/next/data/client/wishlist';
|
|
8
|
+
import { pushAddToCart, pushProductViewed } from '@theme/utils/gtm';
|
|
9
|
+
import { PriceWrapper, Variant } from '@theme/views/product';
|
|
10
|
+
import Share from '@theme/views/share';
|
|
6
11
|
import { ProductPageProps } from './layout';
|
|
7
12
|
import MiscButtons from './misc-buttons';
|
|
8
|
-
import {
|
|
13
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
14
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
15
|
+
import { Trans } from '@akinon/next/components/trans';
|
|
9
16
|
import { useSession } from 'next-auth/react';
|
|
10
|
-
import { isVariantSelectionComplete } from '../../utils/variant-validation';
|
|
11
|
-
import { useProductCart } from '../../hooks/use-product-cart';
|
|
12
|
-
import { useStockAlert } from '../../hooks/use-stock-alert';
|
|
13
|
-
import { ProductVariants } from './product-variants';
|
|
14
|
-
import { ProductActions } from './product-actions';
|
|
15
|
-
import { ProductShare } from './product-share';
|
|
16
17
|
|
|
17
18
|
export default function ProductInfo({ data }: ProductPageProps) {
|
|
19
|
+
const { t } = useLocalization();
|
|
18
20
|
const { data: session } = useSession();
|
|
21
|
+
const [currentUrl, setCurrentUrl] = useState(null);
|
|
22
|
+
const [productError, setProductError] = useState(null);
|
|
23
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
24
|
+
const [stockAlertResponseMessage, setStockAlertResponseMessage] =
|
|
25
|
+
useState(null);
|
|
19
26
|
const [isVariantLoading, setIsVariantLoading] = useState(false);
|
|
20
27
|
|
|
28
|
+
const [addProduct, { isLoading: isAddToCartLoading }] =
|
|
29
|
+
useAddProductToBasket();
|
|
30
|
+
const [addStockAlert, { isLoading: isAddToStockAlertLoading }] =
|
|
31
|
+
useAddStockAlertMutation();
|
|
21
32
|
const inStock = data.selected_variant !== null || data.product.in_stock;
|
|
22
33
|
|
|
23
|
-
const {
|
|
24
|
-
addProductToCart,
|
|
25
|
-
productError: cartError,
|
|
26
|
-
clearProductError: clearCartError,
|
|
27
|
-
isAddToCartLoading
|
|
28
|
-
} = useProductCart({
|
|
29
|
-
product: data.product,
|
|
30
|
-
variants: data.variants
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const {
|
|
34
|
-
addProductToStockAlertList,
|
|
35
|
-
isModalOpen,
|
|
36
|
-
stockAlertResponseMessage,
|
|
37
|
-
productError: stockError,
|
|
38
|
-
isAddToStockAlertLoading,
|
|
39
|
-
closeModal,
|
|
40
|
-
clearError: clearStockError
|
|
41
|
-
} = useStockAlert({
|
|
42
|
-
productPk: data.product.pk,
|
|
43
|
-
userEmail: session?.user?.email
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const productError = cartError || stockError;
|
|
47
|
-
const clearProductError = () => {
|
|
48
|
-
clearCartError();
|
|
49
|
-
clearStockError();
|
|
50
|
-
};
|
|
51
|
-
|
|
52
34
|
useEffect(() => {
|
|
53
|
-
isVariantSelectionComplete(
|
|
35
|
+
isVariantSelectionComplete() && setIsVariantLoading(false);
|
|
36
|
+
|
|
54
37
|
!inStock && setIsVariantLoading(false);
|
|
55
|
-
}, [data
|
|
38
|
+
}, [data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
39
|
|
|
57
40
|
useEffect(() => {
|
|
58
41
|
if (isVariantLoading) {
|
|
59
|
-
|
|
42
|
+
setProductError(null);
|
|
60
43
|
}
|
|
61
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
44
|
}, [isVariantLoading]);
|
|
63
45
|
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setCurrentUrl(window.location.href);
|
|
48
|
+
}, [currentUrl]);
|
|
49
|
+
|
|
64
50
|
useEffect(() => {
|
|
65
51
|
pushProductViewed(data?.product);
|
|
66
52
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
53
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
const addProductToCart = async () => {
|
|
55
|
+
if (!variantsSelectionCheck()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await addProduct({
|
|
61
|
+
product: data.product.pk,
|
|
62
|
+
quantity: 1,
|
|
63
|
+
attributes: {}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
pushAddToCart(data?.product);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
setProductError(
|
|
69
|
+
error?.data?.non_field_errors ||
|
|
70
|
+
Object.keys(error?.data).map(
|
|
71
|
+
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const variantsSelectionCheck = () => {
|
|
78
|
+
const unselectedVariant = data.variants.find((variant) =>
|
|
79
|
+
variant.options.every((opt) => !opt.is_selected)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (unselectedVariant) {
|
|
83
|
+
setProductError(() => (
|
|
84
|
+
<Trans
|
|
85
|
+
i18nKey="product.please_select_variant"
|
|
86
|
+
components={{
|
|
87
|
+
VariantName: <span>{unselectedVariant.attribute_name}</span>
|
|
88
|
+
}}
|
|
89
|
+
/>
|
|
90
|
+
));
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isVariantSelectionComplete = () => {
|
|
99
|
+
return data?.variants.every((variant) =>
|
|
100
|
+
variant?.options.some((opt) => opt.is_selected)
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const addProductToStockAlertList = async () => {
|
|
105
|
+
try {
|
|
106
|
+
await addStockAlert({
|
|
107
|
+
productPk: data.product.pk,
|
|
108
|
+
email: session?.user?.email
|
|
109
|
+
})
|
|
110
|
+
.unwrap()
|
|
111
|
+
.then(handleSuccess)
|
|
112
|
+
.catch((err) => handleError(err));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
setProductError(error?.data?.non_field_errors || null);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleModalClick = () => {
|
|
119
|
+
setIsModalOpen(false);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleSuccess = () => {
|
|
123
|
+
setStockAlertResponseMessage(() => (
|
|
124
|
+
<Trans
|
|
125
|
+
i18nKey="product.stock_alert.success_description"
|
|
126
|
+
components={{
|
|
127
|
+
Email: <span>{session?.user?.email}</span>
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
));
|
|
131
|
+
setIsModalOpen(true);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleError = (err) => {
|
|
135
|
+
if (err.status !== 401) {
|
|
136
|
+
setStockAlertResponseMessage(
|
|
137
|
+
t('product.stock_alert.error_description').toString()
|
|
138
|
+
);
|
|
139
|
+
setIsModalOpen(true);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const checkoutProviderProps = {
|
|
144
|
+
product: data.product,
|
|
145
|
+
clearBasket: true,
|
|
146
|
+
addBeforeClick: variantsSelectionCheck,
|
|
147
|
+
openMiniBasket: false,
|
|
148
|
+
className: clsx([
|
|
149
|
+
'py-2.5',
|
|
150
|
+
'bg-black',
|
|
151
|
+
'relative',
|
|
152
|
+
'hover:bg-black',
|
|
153
|
+
'before:content-[""]',
|
|
154
|
+
'before:w-6',
|
|
155
|
+
'before:h-6',
|
|
156
|
+
'before:bg-white',
|
|
157
|
+
'before:absolute',
|
|
158
|
+
'before:rounded-r-[18px]',
|
|
159
|
+
'before:left-0',
|
|
160
|
+
'after:content-[""]',
|
|
161
|
+
'after:absolute',
|
|
162
|
+
'after:w-3',
|
|
163
|
+
'after:h-3',
|
|
164
|
+
'after:bg-[#d02c2f]',
|
|
165
|
+
'after:rounded-xl',
|
|
166
|
+
'after:left-1'
|
|
167
|
+
]),
|
|
168
|
+
onError: (error) =>
|
|
169
|
+
setProductError(
|
|
170
|
+
error?.data?.non_field_errors ||
|
|
171
|
+
Object.keys(error?.data).map(
|
|
172
|
+
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
173
|
+
)
|
|
174
|
+
)
|
|
71
175
|
};
|
|
72
176
|
|
|
73
177
|
return (
|
|
@@ -83,26 +187,72 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
83
187
|
retailPrice={data.product.retail_price}
|
|
84
188
|
/>
|
|
85
189
|
</div>
|
|
190
|
+
<div className="flex flex-col">
|
|
191
|
+
{data.variants.map((variant) => (
|
|
192
|
+
<Variant
|
|
193
|
+
key={variant.attribute_key}
|
|
194
|
+
{...variant}
|
|
195
|
+
className="items-center mt-8"
|
|
196
|
+
onChange={() => {
|
|
197
|
+
setProductError(null);
|
|
198
|
+
setIsVariantLoading(true);
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
86
203
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
204
|
+
{productError && (
|
|
205
|
+
<div className="mt-4 text-xs text-center text-error">
|
|
206
|
+
{productError}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
<Button
|
|
211
|
+
disabled={
|
|
212
|
+
isAddToCartLoading || isAddToStockAlertLoading || isVariantLoading
|
|
213
|
+
}
|
|
214
|
+
className={clsx(
|
|
215
|
+
'fixed bottom-0 right-0 w-1/2 h-14 z-[20] flex items-center justify-center fill-primary-foreground',
|
|
216
|
+
'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
|
|
217
|
+
)}
|
|
218
|
+
onClick={() => {
|
|
219
|
+
setProductError(null);
|
|
220
|
+
|
|
221
|
+
if (inStock) {
|
|
222
|
+
addProductToCart();
|
|
223
|
+
} else {
|
|
224
|
+
addProductToStockAlertList();
|
|
225
|
+
}
|
|
226
|
+
}}
|
|
227
|
+
data-testid="product-add-to-cart"
|
|
228
|
+
>
|
|
229
|
+
{isVariantLoading ? (
|
|
230
|
+
<Icon
|
|
231
|
+
name="spinner"
|
|
232
|
+
size={20}
|
|
233
|
+
className="animate-spin mr-4 fill-primary"
|
|
234
|
+
/>
|
|
235
|
+
) : inStock ? (
|
|
236
|
+
<span>{t('product.add_to_cart')}</span>
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
<Icon name="bell" size={20} className="mr-4" />
|
|
240
|
+
<span>{t('product.add_stock_alert')}</span>
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
243
|
+
</Button>
|
|
244
|
+
|
|
245
|
+
<PluginModule
|
|
246
|
+
component={Component.AkifastCheckoutButton}
|
|
247
|
+
props={{
|
|
248
|
+
...checkoutProviderProps,
|
|
249
|
+
isPdp: true
|
|
250
|
+
}}
|
|
90
251
|
/>
|
|
91
252
|
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
inStock={inStock}
|
|
96
|
-
isVariantLoading={isVariantLoading}
|
|
97
|
-
onAddToCart={addProductToCart}
|
|
98
|
-
onAddToStockAlert={addProductToStockAlertList}
|
|
99
|
-
onClearError={clearProductError}
|
|
100
|
-
isAddToCartLoading={isAddToCartLoading}
|
|
101
|
-
isAddToStockAlertLoading={isAddToStockAlertLoading}
|
|
102
|
-
productError={productError}
|
|
103
|
-
isModalOpen={isModalOpen}
|
|
104
|
-
stockAlertResponseMessage={stockAlertResponseMessage}
|
|
105
|
-
onCloseModal={closeModal}
|
|
253
|
+
<PluginModule
|
|
254
|
+
component={Component.OneClickCheckoutButtons}
|
|
255
|
+
props={checkoutProviderProps}
|
|
106
256
|
/>
|
|
107
257
|
|
|
108
258
|
<MiscButtons
|
|
@@ -111,7 +261,58 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
111
261
|
variants={data.variants}
|
|
112
262
|
/>
|
|
113
263
|
|
|
114
|
-
<
|
|
264
|
+
<Share
|
|
265
|
+
className="my-2 sm:mb-4"
|
|
266
|
+
buttonText={t('product.share')}
|
|
267
|
+
items={[
|
|
268
|
+
{
|
|
269
|
+
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
|
270
|
+
currentUrl
|
|
271
|
+
)}`,
|
|
272
|
+
iconName: 'facebook',
|
|
273
|
+
iconSize: 22
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
|
277
|
+
currentUrl
|
|
278
|
+
)}`,
|
|
279
|
+
iconName: 'twitter',
|
|
280
|
+
iconSize: 22
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
href: `https://api.whatsapp.com/send?text=${
|
|
284
|
+
data.product.name
|
|
285
|
+
}%20${encodeURIComponent(currentUrl)}`,
|
|
286
|
+
iconName: 'whatsapp',
|
|
287
|
+
iconSize: 22
|
|
288
|
+
}
|
|
289
|
+
]}
|
|
290
|
+
/>
|
|
291
|
+
|
|
292
|
+
<Modal
|
|
293
|
+
portalId="stock-alert-modal"
|
|
294
|
+
open={isModalOpen}
|
|
295
|
+
setOpen={setIsModalOpen}
|
|
296
|
+
showCloseButton={false}
|
|
297
|
+
className="w-5/6 md:max-w-md"
|
|
298
|
+
>
|
|
299
|
+
<div className="flex flex-col items-center justify-center gap-4 px-6 py-9">
|
|
300
|
+
<Icon name="bell" size={48} />
|
|
301
|
+
<h2 className="text-xl font-semibold">
|
|
302
|
+
{t('product.stock_alert.title')}
|
|
303
|
+
</h2>
|
|
304
|
+
<div className="max-w-40 text-xs text-center leading-4">
|
|
305
|
+
<p>{stockAlertResponseMessage}</p>
|
|
306
|
+
</div>
|
|
307
|
+
<Button
|
|
308
|
+
onClick={handleModalClick}
|
|
309
|
+
appearance="outlined"
|
|
310
|
+
className="font-semibold px-10 h-12"
|
|
311
|
+
>
|
|
312
|
+
{t('product.stock_alert.close_button')}
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</Modal>
|
|
115
316
|
</>
|
|
116
317
|
);
|
|
117
318
|
}
|