@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +5 -233
  2. package/app-template/.env.example +0 -1
  3. package/app-template/CHANGELOG.md +323 -4947
  4. package/app-template/README.md +1 -25
  5. package/app-template/package.json +19 -19
  6. package/app-template/public/locales/en/checkout.json +0 -6
  7. package/app-template/public/locales/en/common.json +1 -42
  8. package/app-template/public/locales/tr/checkout.json +0 -6
  9. package/app-template/public/locales/tr/common.json +1 -42
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +82 -9
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +1 -12
  12. package/app-template/src/assets/fonts/pz-icon.css +0 -3
  13. package/app-template/src/components/accordion.tsx +19 -22
  14. package/app-template/src/components/file-input.tsx +7 -27
  15. package/app-template/src/components/input.tsx +1 -2
  16. package/app-template/src/components/price.tsx +1 -1
  17. package/app-template/src/components/types/index.ts +1 -24
  18. package/app-template/src/hooks/index.ts +0 -2
  19. package/app-template/src/plugins.js +1 -2
  20. package/app-template/src/settings.js +1 -6
  21. package/app-template/src/views/basket/basket-item.tsx +13 -16
  22. package/app-template/src/views/basket/summary.tsx +7 -10
  23. package/app-template/src/views/checkout/summary.tsx +0 -10
  24. package/app-template/src/views/guest-login/index.tsx +1 -6
  25. package/app-template/src/views/header/search/index.tsx +5 -17
  26. package/app-template/src/views/product/product-info.tsx +263 -61
  27. package/app-template/src/views/product/slider.tsx +73 -86
  28. package/commands/plugins.ts +16 -63
  29. package/dist/commands/plugins.js +16 -57
  30. package/package.json +1 -1
  31. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
  32. package/app-template/src/app/api/image-proxy/route.ts +0 -1
  33. package/app-template/src/app/api/similar-product-list/route.ts +0 -1
  34. package/app-template/src/app/api/similar-products/route.ts +0 -1
  35. package/app-template/src/hooks/use-product-cart.ts +0 -77
  36. package/app-template/src/hooks/use-stock-alert.ts +0 -74
  37. package/app-template/src/utils/variant-validation.ts +0 -41
  38. package/app-template/src/views/basket/basket-content.tsx +0 -106
  39. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +0 -121
  40. package/app-template/src/views/product/product-actions.tsx +0 -165
  41. package/app-template/src/views/product/product-share.tsx +0 -56
  42. 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 { Basket, BasketItem as BasketItemType } from '@akinon/next/types';
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, onBasketUpdate } = props;
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
- 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
- }
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, onBasketUpdate } = props;
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
- onBasketUpdate?.(basket);
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
- onBasketUpdate?.(basket);
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
- import { Icon, Input } from '@theme/components';
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
- <Input
69
+ <input
78
70
  value={searchText}
79
- onChange={handleSearchTextChange}
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={handleCloseSearch}
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 { PriceWrapper } from '@theme/views/product';
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 { pushProductViewed } from '@theme/utils/gtm';
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(data.variants) && setIsVariantLoading(false);
35
+ isVariantSelectionComplete() && setIsVariantLoading(false);
36
+
54
37
  !inStock && setIsVariantLoading(false);
55
- }, [data, inStock]);
38
+ }, [data]); // eslint-disable-line react-hooks/exhaustive-deps
56
39
 
57
40
  useEffect(() => {
58
41
  if (isVariantLoading) {
59
- clearProductError();
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 handleVariantChange = () => {
69
- clearProductError();
70
- setIsVariantLoading(true);
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
- <ProductVariants
88
- variants={data.variants}
89
- onVariantChange={handleVariantChange}
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
- <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}
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
- <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
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
  }