@akinon/projectzero 1.105.0-rc.84 → 1.105.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 +5 -233
- package/app-template/.env.example +0 -1
- package/app-template/CHANGELOG.md +323 -4947
- package/app-template/README.md +1 -25
- package/app-template/package.json +19 -19
- package/app-template/public/locales/en/checkout.json +0 -6
- package/app-template/public/locales/en/common.json +1 -42
- package/app-template/public/locales/tr/checkout.json +0 -6
- 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]/landing-page/[pk]/page.tsx +1 -12
- package/app-template/src/assets/fonts/pz-icon.css +0 -3
- package/app-template/src/components/accordion.tsx +19 -22
- package/app-template/src/components/file-input.tsx +7 -27
- package/app-template/src/components/input.tsx +1 -2
- package/app-template/src/components/price.tsx +1 -1
- package/app-template/src/components/types/index.ts +1 -24
- package/app-template/src/hooks/index.ts +0 -2
- package/app-template/src/plugins.js +1 -2
- package/app-template/src/settings.js +1 -6
- package/app-template/src/views/basket/basket-item.tsx +13 -16
- package/app-template/src/views/basket/summary.tsx +7 -10
- package/app-template/src/views/checkout/summary.tsx +0 -10
- package/app-template/src/views/guest-login/index.tsx +1 -6
- package/app-template/src/views/header/search/index.tsx +5 -17
- package/app-template/src/views/product/product-info.tsx +263 -61
- package/app-template/src/views/product/slider.tsx +73 -86
- package/commands/plugins.ts +16 -63
- package/dist/commands/plugins.js +16 -57
- package/package.json +1 -1
- 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/checkout/steps/payment/options/store-credit.tsx +0 -121
- 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
|
@@ -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 {
|
|
6
|
+
import { 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,12 +19,11 @@ import { pushRemoveFromCart } from '@theme/utils/gtm';
|
|
|
19
19
|
interface Props {
|
|
20
20
|
basketItem?: BasketItemType;
|
|
21
21
|
namespace?: string;
|
|
22
|
-
onBasketUpdate?: (basket: Basket) => void;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export const BasketItem = (props: Props) => {
|
|
26
25
|
const { t } = useLocalization();
|
|
27
|
-
const { basketItem, namespace
|
|
26
|
+
const { basketItem, namespace } = props;
|
|
28
27
|
const [updateQuantityMutation] = useUpdateQuantityMutation();
|
|
29
28
|
const dispatch = useAppDispatch();
|
|
30
29
|
const [isRemoveBasketModalOpen, setRemoveBasketModalOpen] = useState(false);
|
|
@@ -55,21 +54,19 @@ export const BasketItem = (props: Props) => {
|
|
|
55
54
|
requestParams.namespace = namespace;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
await updateQuantityMutation(requestParams)
|
|
58
|
+
.unwrap()
|
|
59
|
+
.then((data) =>
|
|
60
|
+
dispatch(
|
|
61
|
+
basketApi.util.updateQueryData(
|
|
62
|
+
'getBasket',
|
|
63
|
+
undefined,
|
|
64
|
+
(draftBasket) => {
|
|
65
|
+
Object.assign(draftBasket, data.basket);
|
|
66
|
+
}
|
|
67
|
+
)
|
|
67
68
|
)
|
|
68
69
|
);
|
|
69
|
-
onBasketUpdate?.(response.basket);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('Error updating quantity:', error);
|
|
72
|
-
}
|
|
73
70
|
};
|
|
74
71
|
|
|
75
72
|
const deleteProduct = async (productPk?: number) => {
|
|
@@ -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
|
});
|
|
@@ -8,7 +8,6 @@ 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';
|
|
12
11
|
|
|
13
12
|
export const Summary = () => {
|
|
14
13
|
const { t } = useLocalization();
|
|
@@ -39,7 +38,6 @@ export const Summary = () => {
|
|
|
39
38
|
'flex flex-col w-full mb-4 border border-solid border-gray-400'
|
|
40
39
|
}}
|
|
41
40
|
/>
|
|
42
|
-
<StoreCredits />
|
|
43
41
|
<div className="flex flex-col w-full border border-solid border-gray-400">
|
|
44
42
|
<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">
|
|
45
43
|
<span className="text-black-800 text-xl font-light sm:text-2xl">
|
|
@@ -120,14 +118,6 @@ export const Summary = () => {
|
|
|
120
118
|
<Price value={preOrder?.shipping_amount} />
|
|
121
119
|
</span>
|
|
122
120
|
</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
|
-
)}
|
|
131
121
|
<div className="flex items-center justify-between w-full text-xs text-black-800 py-1 px-4 sm:px-5">
|
|
132
122
|
<span>{t('checkout.summary.discounts_total')}</span>
|
|
133
123
|
<span>
|
|
@@ -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,73 +1,178 @@
|
|
|
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
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
45
|
}, [isVariantLoading]);
|
|
63
46
|
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setCurrentUrl(window.location.href);
|
|
49
|
+
}, [currentUrl]);
|
|
50
|
+
|
|
64
51
|
useEffect(() => {
|
|
65
52
|
pushProductViewed(data?.product);
|
|
66
53
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
54
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
const addProductToCart = async () => {
|
|
56
|
+
if (!variantsSelectionCheck()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await addProduct({
|
|
62
|
+
product: data.product.pk,
|
|
63
|
+
quantity: 1,
|
|
64
|
+
attributes: {}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pushAddToCart(data?.product);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
setProductError(
|
|
70
|
+
error?.data?.non_field_errors ||
|
|
71
|
+
Object.keys(error?.data).map(
|
|
72
|
+
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const variantsSelectionCheck = () => {
|
|
79
|
+
const unselectedVariant = data.variants.find((variant) =>
|
|
80
|
+
variant.options.every((opt) => !opt.is_selected)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (unselectedVariant) {
|
|
84
|
+
setProductError(() => (
|
|
85
|
+
<Trans
|
|
86
|
+
i18nKey="product.please_select_variant"
|
|
87
|
+
components={{
|
|
88
|
+
VariantName: <span>{unselectedVariant.attribute_name}</span>
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
));
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isVariantSelectionComplete = () => {
|
|
100
|
+
return data?.variants.every((variant) =>
|
|
101
|
+
variant?.options.some((opt) => opt.is_selected)
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const addProductToStockAlertList = async () => {
|
|
106
|
+
try {
|
|
107
|
+
await addStockAlert({
|
|
108
|
+
productPk: data.product.pk,
|
|
109
|
+
email: session?.user?.email
|
|
110
|
+
})
|
|
111
|
+
.unwrap()
|
|
112
|
+
.then(handleSuccess)
|
|
113
|
+
.catch((err) => handleError(err));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
setProductError(error?.data?.non_field_errors || null);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleModalClick = () => {
|
|
120
|
+
setIsModalOpen(false);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleSuccess = () => {
|
|
124
|
+
setStockAlertResponseMessage(() => (
|
|
125
|
+
<Trans
|
|
126
|
+
i18nKey="product.stock_alert.success_description"
|
|
127
|
+
components={{
|
|
128
|
+
Email: <span>{session?.user?.email}</span>
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
));
|
|
132
|
+
setIsModalOpen(true);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleError = (err) => {
|
|
136
|
+
if (err.status !== 401) {
|
|
137
|
+
setStockAlertResponseMessage(
|
|
138
|
+
t('product.stock_alert.error_description').toString()
|
|
139
|
+
);
|
|
140
|
+
setIsModalOpen(true);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const checkoutProviderProps = {
|
|
145
|
+
product: data.product,
|
|
146
|
+
clearBasket: true,
|
|
147
|
+
addBeforeClick: variantsSelectionCheck,
|
|
148
|
+
openMiniBasket: false,
|
|
149
|
+
className: clsx([
|
|
150
|
+
'py-2.5',
|
|
151
|
+
'bg-black',
|
|
152
|
+
'relative',
|
|
153
|
+
'hover:bg-black',
|
|
154
|
+
'before:content-[""]',
|
|
155
|
+
'before:w-6',
|
|
156
|
+
'before:h-6',
|
|
157
|
+
'before:bg-white',
|
|
158
|
+
'before:absolute',
|
|
159
|
+
'before:rounded-r-[18px]',
|
|
160
|
+
'before:left-0',
|
|
161
|
+
'after:content-[""]',
|
|
162
|
+
'after:absolute',
|
|
163
|
+
'after:w-3',
|
|
164
|
+
'after:h-3',
|
|
165
|
+
'after:bg-[#d02c2f]',
|
|
166
|
+
'after:rounded-xl',
|
|
167
|
+
'after:left-1'
|
|
168
|
+
]),
|
|
169
|
+
onError: (error) =>
|
|
170
|
+
setProductError(
|
|
171
|
+
error?.data?.non_field_errors ||
|
|
172
|
+
Object.keys(error?.data).map(
|
|
173
|
+
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
174
|
+
)
|
|
175
|
+
)
|
|
71
176
|
};
|
|
72
177
|
|
|
73
178
|
return (
|
|
@@ -83,26 +188,72 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
83
188
|
retailPrice={data.product.retail_price}
|
|
84
189
|
/>
|
|
85
190
|
</div>
|
|
191
|
+
<div className="flex flex-col">
|
|
192
|
+
{data.variants.map((variant) => (
|
|
193
|
+
<Variant
|
|
194
|
+
key={variant.attribute_key}
|
|
195
|
+
{...variant}
|
|
196
|
+
className="items-center mt-8"
|
|
197
|
+
onChange={() => {
|
|
198
|
+
setProductError(null);
|
|
199
|
+
setIsVariantLoading(true);
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
86
204
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
205
|
+
{productError && (
|
|
206
|
+
<div className="mt-4 text-xs text-center text-error">
|
|
207
|
+
{productError}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
<Button
|
|
212
|
+
disabled={
|
|
213
|
+
isAddToCartLoading || isAddToStockAlertLoading || isVariantLoading
|
|
214
|
+
}
|
|
215
|
+
className={clsx(
|
|
216
|
+
'fixed bottom-0 right-0 w-1/2 h-14 z-[20] flex items-center justify-center fill-primary-foreground',
|
|
217
|
+
'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
|
|
218
|
+
)}
|
|
219
|
+
onClick={() => {
|
|
220
|
+
setProductError(null);
|
|
221
|
+
|
|
222
|
+
if (inStock) {
|
|
223
|
+
addProductToCart();
|
|
224
|
+
} else {
|
|
225
|
+
addProductToStockAlertList();
|
|
226
|
+
}
|
|
227
|
+
}}
|
|
228
|
+
data-testid="product-add-to-cart"
|
|
229
|
+
>
|
|
230
|
+
{isVariantLoading ? (
|
|
231
|
+
<Icon
|
|
232
|
+
name="spinner"
|
|
233
|
+
size={20}
|
|
234
|
+
className="animate-spin mr-4 fill-primary"
|
|
235
|
+
/>
|
|
236
|
+
) : inStock ? (
|
|
237
|
+
<span>{t('product.add_to_cart')}</span>
|
|
238
|
+
) : (
|
|
239
|
+
<>
|
|
240
|
+
<Icon name="bell" size={20} className="mr-4" />
|
|
241
|
+
<span>{t('product.add_stock_alert')}</span>
|
|
242
|
+
</>
|
|
243
|
+
)}
|
|
244
|
+
</Button>
|
|
245
|
+
|
|
246
|
+
<PluginModule
|
|
247
|
+
component={Component.AkifastCheckoutButton}
|
|
248
|
+
props={{
|
|
249
|
+
...checkoutProviderProps,
|
|
250
|
+
isPdp: true
|
|
251
|
+
}}
|
|
90
252
|
/>
|
|
91
253
|
|
|
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}
|
|
254
|
+
<PluginModule
|
|
255
|
+
component={Component.OneClickCheckoutButtons}
|
|
256
|
+
props={checkoutProviderProps}
|
|
106
257
|
/>
|
|
107
258
|
|
|
108
259
|
<MiscButtons
|
|
@@ -111,7 +262,58 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
111
262
|
variants={data.variants}
|
|
112
263
|
/>
|
|
113
264
|
|
|
114
|
-
<
|
|
265
|
+
<Share
|
|
266
|
+
className="my-2 sm:mb-4"
|
|
267
|
+
buttonText={t('product.share')}
|
|
268
|
+
items={[
|
|
269
|
+
{
|
|
270
|
+
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
|
271
|
+
currentUrl
|
|
272
|
+
)}`,
|
|
273
|
+
iconName: 'facebook',
|
|
274
|
+
iconSize: 22
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
|
278
|
+
currentUrl
|
|
279
|
+
)}`,
|
|
280
|
+
iconName: 'twitter',
|
|
281
|
+
iconSize: 22
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
href: `https://api.whatsapp.com/send?text=${
|
|
285
|
+
data.product.name
|
|
286
|
+
}%20${encodeURIComponent(currentUrl)}`,
|
|
287
|
+
iconName: 'whatsapp',
|
|
288
|
+
iconSize: 22
|
|
289
|
+
}
|
|
290
|
+
]}
|
|
291
|
+
/>
|
|
292
|
+
|
|
293
|
+
<Modal
|
|
294
|
+
portalId="stock-alert-modal"
|
|
295
|
+
open={isModalOpen}
|
|
296
|
+
setOpen={setIsModalOpen}
|
|
297
|
+
showCloseButton={false}
|
|
298
|
+
className="w-5/6 md:max-w-md"
|
|
299
|
+
>
|
|
300
|
+
<div className="flex flex-col items-center justify-center gap-4 px-6 py-9">
|
|
301
|
+
<Icon name="bell" size={48} />
|
|
302
|
+
<h2 className="text-xl font-semibold">
|
|
303
|
+
{t('product.stock_alert.title')}
|
|
304
|
+
</h2>
|
|
305
|
+
<div className="max-w-40 text-xs text-center leading-4">
|
|
306
|
+
<p>{stockAlertResponseMessage}</p>
|
|
307
|
+
</div>
|
|
308
|
+
<Button
|
|
309
|
+
onClick={handleModalClick}
|
|
310
|
+
appearance="outlined"
|
|
311
|
+
className="font-semibold px-10 h-12"
|
|
312
|
+
>
|
|
313
|
+
{t('product.stock_alert.close_button')}
|
|
314
|
+
</Button>
|
|
315
|
+
</div>
|
|
316
|
+
</Modal>
|
|
115
317
|
</>
|
|
116
318
|
);
|
|
117
319
|
}
|