@akinon/projectzero 1.99.0 → 1.100.0-rc.72

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +235 -4
  2. package/app-template/.env.example +1 -0
  3. package/app-template/.github/instructions/routing.instructions.md +603 -0
  4. package/app-template/CHANGELOG.md +5001 -312
  5. package/app-template/README.md +25 -1
  6. package/app-template/package.json +21 -19
  7. package/app-template/public/locales/en/common.json +48 -1
  8. package/app-template/public/locales/tr/common.json +48 -1
  9. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +9 -82
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +17 -4
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +12 -1
  12. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +29 -11
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +12 -1
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +67 -0
  15. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +28 -10
  16. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +12 -1
  17. package/app-template/src/app/api/form/[...id]/route.ts +1 -7
  18. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  19. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  20. package/app-template/src/app/api/similar-products/route.ts +1 -0
  21. package/app-template/src/assets/fonts/pz-icon.css +3 -0
  22. package/app-template/src/components/__tests__/link.test.tsx +2 -0
  23. package/app-template/src/components/accordion.tsx +22 -19
  24. package/app-template/src/components/currency-select.tsx +1 -0
  25. package/app-template/src/components/file-input.tsx +27 -7
  26. package/app-template/src/components/generate-form-fields.tsx +43 -4
  27. package/app-template/src/components/input.tsx +9 -2
  28. package/app-template/src/components/modal.tsx +32 -16
  29. package/app-template/src/components/pagination.tsx +1 -0
  30. package/app-template/src/components/select.tsx +38 -26
  31. package/app-template/src/components/types/index.ts +25 -1
  32. package/app-template/src/hooks/index.ts +2 -0
  33. package/app-template/src/hooks/use-product-cart.ts +77 -0
  34. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  35. package/app-template/src/plugins.js +3 -1
  36. package/app-template/src/settings.js +8 -2
  37. package/app-template/src/types/index.ts +74 -3
  38. package/app-template/src/utils/variant-validation.ts +41 -0
  39. package/app-template/src/views/account/address-form.tsx +8 -4
  40. package/app-template/src/views/account/contact-form.tsx +1 -1
  41. package/app-template/src/views/account/content-header.tsx +2 -2
  42. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  43. package/app-template/src/views/basket/basket-content.tsx +106 -0
  44. package/app-template/src/views/basket/basket-item.tsx +22 -14
  45. package/app-template/src/views/basket/summary.tsx +10 -7
  46. package/app-template/src/views/breadcrumb.tsx +2 -2
  47. package/app-template/src/views/category/category-info.tsx +1 -0
  48. package/app-template/src/views/category/filters/index.tsx +1 -1
  49. package/app-template/src/views/guest-login/index.tsx +6 -1
  50. package/app-template/src/views/header/action-menu.tsx +1 -1
  51. package/app-template/src/views/header/search/index.tsx +17 -5
  52. package/app-template/src/views/login/index.tsx +11 -10
  53. package/app-template/src/views/otp-login/index.tsx +11 -6
  54. package/app-template/src/views/product/product-actions.tsx +165 -0
  55. package/app-template/src/views/product/product-info.tsx +62 -263
  56. package/app-template/src/views/product/product-share.tsx +56 -0
  57. package/app-template/src/views/product/product-variants.tsx +26 -0
  58. package/app-template/src/views/product/slider.tsx +86 -73
  59. package/app-template/src/views/register/index.tsx +15 -11
  60. package/app-template/src/widgets/footer-menu.tsx +6 -2
  61. package/commands/plugins.ts +63 -16
  62. package/dist/commands/plugins.js +57 -16
  63. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { signIn, SignInOptions } from 'next-auth/react';
3
+ import { signIn } 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 } from '@theme/types';
7
+ import { LoginFormType, FormType, PzSignInOptions } 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,8 +79,9 @@ export const Login = () => {
79
79
  redirect: false,
80
80
  callbackUrl: searchParams.get('callbackUrl') ?? '/',
81
81
  captchaValidated,
82
- ...data
83
- } as SignInOptions);
82
+ ...data,
83
+ formType: FormType.login
84
+ } as PzSignInOptions);
84
85
 
85
86
  if (loginResponse.error) {
86
87
  const errors: AuthError[] = JSON.parse(loginResponse.error);
@@ -109,25 +110,25 @@ export const Login = () => {
109
110
  try {
110
111
  parsedValue = JSON.parse(item.value);
111
112
  } catch {
112
- parsedValue = [item.value];
113
+ parsedValue = [item.value];
113
114
  }
114
115
  } else {
115
- parsedValue = item.value;
116
+ parsedValue = item.value;
116
117
  }
117
118
 
118
119
  if (Array.isArray(parsedValue)) {
119
120
  setError(item.name as keyof LoginFormType, {
120
121
  type: 'custom',
121
- message: parsedValue.join(', '),
122
+ message: parsedValue.join(', ')
122
123
  });
123
124
  } else {
124
125
  Object.keys(parsedValue).forEach((key) => {
125
126
  const fieldName = key as keyof LoginFormType;
126
127
  const errorMessages = parsedValue[key] as string[];
127
-
128
+
128
129
  setError(fieldName, {
129
130
  type: 'custom',
130
- message: errorMessages.join(', '),
131
+ message: errorMessages.join(', ')
131
132
  });
132
133
  });
133
134
  }
@@ -161,7 +162,7 @@ export const Login = () => {
161
162
  method="post"
162
163
  onSubmit={handleSubmit(onSubmit)}
163
164
  >
164
- <input type="hidden" value="login" {...register('formType')} />
165
+ <input type="hidden" value={FormType.login} {...register('formType')} />
165
166
  <input type="hidden" value={locale} {...register('locale')} />
166
167
 
167
168
  <div className={clsx(errors.email ? 'mb-8' : 'mb-4')}>
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { signIn, SignInOptions } from 'next-auth/react';
3
+ import { signIn } from 'next-auth/react';
4
4
  import { useState } from 'react';
5
- import { OtpLoginFormType } from '@theme/types';
5
+ import { OtpLoginFormType, FormType, PzSignInOptions } 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,8 +47,9 @@ export const OtpLogin = () => {
47
47
  const loginResponse = await signIn('default', {
48
48
  redirect: false,
49
49
  callbackUrl: '/',
50
- ...data
51
- } as SignInOptions);
50
+ ...data,
51
+ formType: FormType.otpLogin
52
+ } as PzSignInOptions);
52
53
 
53
54
  if (loginResponse.error) {
54
55
  const errors: AuthError[] = JSON.parse(loginResponse.error);
@@ -100,7 +101,11 @@ export const OtpLogin = () => {
100
101
  {t('auth.login.title_gsm')}
101
102
  </h2>
102
103
  <form onSubmit={handleSubmit(onSubmit)}>
103
- <input type="hidden" value="otpLogin" {...register('formType')} />
104
+ <input
105
+ type="hidden"
106
+ value={FormType.otpLogin}
107
+ {...register('formType')}
108
+ />
104
109
  <input type="hidden" value={locale} {...register('locale')} />
105
110
 
106
111
  <div className="mb-4">
@@ -109,7 +114,7 @@ export const OtpLogin = () => {
109
114
  className="h-14"
110
115
  label={t('auth.login.form.phone.placeholder')}
111
116
  type="tel"
112
- format={user_phone_format.replace(/\9/g, '#')}
117
+ format={user_phone_format.replace(/9/g, '#')}
113
118
  mask="_"
114
119
  allowEmptyFormatting={true}
115
120
  control={control}
@@ -0,0 +1,165 @@
1
+ import React from 'react';
2
+ import clsx from 'clsx';
3
+ import { Button, Icon, Modal } from '@theme/components';
4
+ import { useLocalization } from '@akinon/next/hooks';
5
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
6
+ import { validateVariantSelection } from '../../utils/variant-validation';
7
+ import { VariantType } from '@akinon/next/types';
8
+
9
+ interface Product {
10
+ pk: number;
11
+ name: string;
12
+ [key: string]: any;
13
+ }
14
+
15
+ interface ProductActionsProps {
16
+ product: Product;
17
+ variants: VariantType[];
18
+ inStock: boolean;
19
+ isVariantLoading: boolean;
20
+ onAddToCart: () => void;
21
+ onAddToStockAlert: () => void;
22
+ onClearError: () => void;
23
+ isAddToCartLoading: boolean;
24
+ isAddToStockAlertLoading: boolean;
25
+ productError: React.ReactNode | null;
26
+ isModalOpen: boolean;
27
+ stockAlertResponseMessage: React.ReactNode | null;
28
+ onCloseModal: () => void;
29
+ }
30
+
31
+ export const ProductActions: React.FC<ProductActionsProps> = ({
32
+ product,
33
+ variants,
34
+ inStock,
35
+ isVariantLoading,
36
+ onAddToCart,
37
+ onAddToStockAlert,
38
+ onClearError,
39
+ isAddToCartLoading,
40
+ isAddToStockAlertLoading,
41
+ productError,
42
+ isModalOpen,
43
+ stockAlertResponseMessage,
44
+ onCloseModal
45
+ }) => {
46
+ const { t } = useLocalization();
47
+
48
+ const checkoutProviderProps = {
49
+ product,
50
+ clearBasket: true,
51
+ addBeforeClick: () => validateVariantSelection(variants).isValid,
52
+ openMiniBasket: false,
53
+ className: clsx([
54
+ 'py-2.5',
55
+ 'bg-black',
56
+ 'relative',
57
+ 'hover:bg-black',
58
+ 'before:content-[""]',
59
+ 'before:w-6',
60
+ 'before:h-6',
61
+ 'before:bg-white',
62
+ 'before:absolute',
63
+ 'before:rounded-r-[18px]',
64
+ 'before:left-0',
65
+ 'after:content-[""]',
66
+ 'after:absolute',
67
+ 'after:w-3',
68
+ 'after:h-3',
69
+ 'after:bg-[#d02c2f]',
70
+ 'after:rounded-xl',
71
+ 'after:left-1'
72
+ ]),
73
+ onError: (error: any) => {
74
+ const formattedError = error?.data?.non_field_errors ||
75
+ Object.keys(error?.data || {}).map(
76
+ (key) => `${key}: ${error?.data[key].join(', ')}`
77
+ );
78
+ // This would need to be handled by parent component
79
+ console.error('Checkout error:', formattedError);
80
+ }
81
+ };
82
+
83
+ const handleMainActionClick = () => {
84
+ onClearError();
85
+
86
+ if (inStock) {
87
+ onAddToCart();
88
+ } else {
89
+ onAddToStockAlert();
90
+ }
91
+ };
92
+
93
+ return (
94
+ <>
95
+ {productError && (
96
+ <div className="mt-4 text-xs text-center text-error">
97
+ {productError}
98
+ </div>
99
+ )}
100
+
101
+ <Button
102
+ disabled={isAddToCartLoading || isAddToStockAlertLoading || isVariantLoading}
103
+ className={clsx(
104
+ 'fixed bottom-0 right-0 w-1/2 h-14 z-[20] flex items-center justify-center fill-primary-foreground',
105
+ 'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
106
+ )}
107
+ onClick={handleMainActionClick}
108
+ data-testid="product-add-to-cart"
109
+ >
110
+ {isVariantLoading ? (
111
+ <Icon
112
+ name="spinner"
113
+ size={20}
114
+ className="animate-spin mr-4 fill-primary"
115
+ />
116
+ ) : inStock ? (
117
+ <span>{t('product.add_to_cart')}</span>
118
+ ) : (
119
+ <>
120
+ <Icon name="bell" size={20} className="mr-4" />
121
+ <span>{t('product.add_stock_alert')}</span>
122
+ </>
123
+ )}
124
+ </Button>
125
+
126
+ <PluginModule
127
+ component={Component.AkifastCheckoutButton}
128
+ props={{
129
+ ...checkoutProviderProps,
130
+ isPdp: true
131
+ }}
132
+ />
133
+
134
+ <PluginModule
135
+ component={Component.OneClickCheckoutButtons}
136
+ props={checkoutProviderProps}
137
+ />
138
+
139
+ <Modal
140
+ portalId="stock-alert-modal"
141
+ open={isModalOpen}
142
+ setOpen={onCloseModal}
143
+ showCloseButton={false}
144
+ className="w-5/6 md:max-w-md"
145
+ >
146
+ <div className="flex flex-col items-center justify-center gap-4 px-6 py-9">
147
+ <Icon name="bell" size={48} />
148
+ <h2 className="text-xl font-semibold">
149
+ {t('product.stock_alert.title')}
150
+ </h2>
151
+ <div className="max-w-40 text-xs text-center leading-4">
152
+ <p>{stockAlertResponseMessage}</p>
153
+ </div>
154
+ <Button
155
+ onClick={onCloseModal}
156
+ appearance="outlined"
157
+ className="font-semibold px-10 h-12"
158
+ >
159
+ {t('product.stock_alert.close_button')}
160
+ </Button>
161
+ </div>
162
+ </Modal>
163
+ </>
164
+ );
165
+ };
@@ -1,177 +1,73 @@
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';
6
4
  import React, { useEffect, useState } from 'react';
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';
5
+ import { PriceWrapper } from '@theme/views/product';
11
6
  import { ProductPageProps } from './layout';
12
7
  import MiscButtons from './misc-buttons';
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';
8
+ import { pushProductViewed } from '@theme/utils/gtm';
16
9
  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';
17
16
 
18
17
  export default function ProductInfo({ data }: ProductPageProps) {
19
- const { t } = useLocalization();
20
18
  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);
26
19
  const [isVariantLoading, setIsVariantLoading] = useState(false);
27
20
 
28
- const [addProduct, { isLoading: isAddToCartLoading }] =
29
- useAddProductToBasket();
30
- const [addStockAlert, { isLoading: isAddToStockAlertLoading }] =
31
- useAddStockAlertMutation();
32
21
  const inStock = data.selected_variant !== null || data.product.in_stock;
33
22
 
34
- useEffect(() => {
35
- isVariantSelectionComplete() && setIsVariantLoading(false);
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
+ };
36
51
 
52
+ useEffect(() => {
53
+ isVariantSelectionComplete(data.variants) && setIsVariantLoading(false);
37
54
  !inStock && setIsVariantLoading(false);
38
- }, [data]); // eslint-disable-line react-hooks/exhaustive-deps
55
+ }, [data, inStock]);
39
56
 
40
57
  useEffect(() => {
41
58
  if (isVariantLoading) {
42
- setProductError(null);
59
+ clearProductError();
43
60
  }
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
62
  }, [isVariantLoading]);
45
63
 
46
- useEffect(() => {
47
- setCurrentUrl(window.location.href);
48
- }, [currentUrl]);
49
-
50
64
  useEffect(() => {
51
65
  pushProductViewed(data?.product);
52
66
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
53
67
 
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
- )
68
+ const handleVariantChange = () => {
69
+ clearProductError();
70
+ setIsVariantLoading(true);
175
71
  };
176
72
 
177
73
  return (
@@ -187,72 +83,26 @@ export default function ProductInfo({ data }: ProductPageProps) {
187
83
  retailPrice={data.product.retail_price}
188
84
  />
189
85
  </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>
203
-
204
- {productError && (
205
- <div className="mt-4 text-xs text-center text-error">
206
- {productError}
207
- </div>
208
- )}
209
86
 
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
- }}
87
+ <ProductVariants
88
+ variants={data.variants}
89
+ onVariantChange={handleVariantChange}
251
90
  />
252
91
 
253
- <PluginModule
254
- component={Component.OneClickCheckoutButtons}
255
- props={checkoutProviderProps}
92
+ <ProductActions
93
+ product={data.product}
94
+ variants={data.variants}
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}
256
106
  />
257
107
 
258
108
  <MiscButtons
@@ -261,58 +111,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
261
111
  variants={data.variants}
262
112
  />
263
113
 
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>
114
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
316
115
  </>
317
116
  );
318
117
  }
@@ -0,0 +1,56 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import Share from '@theme/views/share';
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+
5
+ interface ProductShareProps {
6
+ productName: string;
7
+ className?: string;
8
+ }
9
+
10
+ export const ProductShare: React.FC<ProductShareProps> = ({
11
+ productName,
12
+ className
13
+ }) => {
14
+ const { t } = useLocalization();
15
+ const [currentUrl, setCurrentUrl] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ setCurrentUrl(window.location.href);
19
+ }, []);
20
+
21
+ if (!currentUrl) {
22
+ return null;
23
+ }
24
+
25
+ const shareItems = [
26
+ {
27
+ href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
28
+ currentUrl
29
+ )}`,
30
+ iconName: 'facebook' as const,
31
+ iconSize: 22
32
+ },
33
+ {
34
+ href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
35
+ currentUrl
36
+ )}`,
37
+ iconName: 'twitter' as const,
38
+ iconSize: 22
39
+ },
40
+ {
41
+ href: `https://api.whatsapp.com/send?text=${productName}%20${encodeURIComponent(
42
+ currentUrl
43
+ )}`,
44
+ iconName: 'whatsapp' as const,
45
+ iconSize: 22
46
+ }
47
+ ];
48
+
49
+ return (
50
+ <Share
51
+ className={className}
52
+ buttonText={t('product.share')}
53
+ items={shareItems}
54
+ />
55
+ );
56
+ };