@akinon/projectzero 1.101.0-rc.76 → 1.101.0-snapshot-ZERO-3615-20250924121313

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 (65) hide show
  1. package/CHANGELOG.md +5 -238
  2. package/app-template/.env.example +0 -1
  3. package/app-template/CHANGELOG.md +304 -5040
  4. package/app-template/README.md +1 -25
  5. package/app-template/package.json +19 -21
  6. package/app-template/public/locales/en/checkout.json +0 -6
  7. package/app-template/public/locales/en/common.json +1 -48
  8. package/app-template/public/locales/tr/checkout.json +0 -6
  9. package/app-template/public/locales/tr/common.json +1 -48
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +82 -9
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +4 -17
  12. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +1 -12
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +11 -29
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +1 -12
  15. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +10 -28
  16. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +1 -12
  17. package/app-template/src/app/api/form/[...id]/route.ts +7 -1
  18. package/app-template/src/assets/fonts/pz-icon.css +0 -3
  19. package/app-template/src/components/__tests__/link.test.tsx +0 -2
  20. package/app-template/src/components/accordion.tsx +19 -22
  21. package/app-template/src/components/currency-select.tsx +0 -1
  22. package/app-template/src/components/file-input.tsx +7 -27
  23. package/app-template/src/components/generate-form-fields.tsx +4 -43
  24. package/app-template/src/components/input.tsx +2 -9
  25. package/app-template/src/components/modal.tsx +16 -32
  26. package/app-template/src/components/pagination.tsx +0 -1
  27. package/app-template/src/components/price.tsx +1 -1
  28. package/app-template/src/components/select.tsx +26 -38
  29. package/app-template/src/components/types/index.ts +1 -25
  30. package/app-template/src/hooks/index.ts +0 -2
  31. package/app-template/src/plugins.js +1 -3
  32. package/app-template/src/settings.js +2 -8
  33. package/app-template/src/types/index.ts +0 -17
  34. package/app-template/src/views/account/address-form.tsx +4 -8
  35. package/app-template/src/views/account/contact-form.tsx +1 -1
  36. package/app-template/src/views/account/content-header.tsx +2 -2
  37. package/app-template/src/views/account/faq/faq-tabs.tsx +2 -8
  38. package/app-template/src/views/basket/basket-item.tsx +14 -22
  39. package/app-template/src/views/basket/summary.tsx +7 -10
  40. package/app-template/src/views/breadcrumb.tsx +2 -2
  41. package/app-template/src/views/category/category-info.tsx +0 -1
  42. package/app-template/src/views/category/filters/index.tsx +1 -1
  43. package/app-template/src/views/checkout/summary.tsx +0 -10
  44. package/app-template/src/views/guest-login/index.tsx +1 -6
  45. package/app-template/src/views/header/action-menu.tsx +1 -1
  46. package/app-template/src/views/header/search/index.tsx +5 -17
  47. package/app-template/src/views/product/product-info.tsx +263 -62
  48. package/app-template/src/views/product/slider.tsx +73 -86
  49. package/app-template/src/widgets/footer-menu.tsx +2 -6
  50. package/commands/plugins.ts +16 -63
  51. package/dist/commands/plugins.js +16 -57
  52. package/package.json +1 -1
  53. package/app-template/.github/instructions/routing.instructions.md +0 -603
  54. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
  55. package/app-template/src/app/api/image-proxy/route.ts +0 -1
  56. package/app-template/src/app/api/similar-product-list/route.ts +0 -1
  57. package/app-template/src/app/api/similar-products/route.ts +0 -1
  58. package/app-template/src/hooks/use-product-cart.ts +0 -77
  59. package/app-template/src/hooks/use-stock-alert.ts +0 -74
  60. package/app-template/src/utils/variant-validation.ts +0 -41
  61. package/app-template/src/views/basket/basket-content.tsx +0 -106
  62. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +0 -121
  63. package/app-template/src/views/product/product-actions.tsx +0 -165
  64. package/app-template/src/views/product/product-share.tsx +0 -56
  65. package/app-template/src/views/product/product-variants.tsx +0 -26
@@ -3,7 +3,6 @@
3
3
  import { Accordion, LoaderSpinner, TabPanel, Tabs } from '@theme/components';
4
4
  import React from 'react';
5
5
  import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
6
- import { useLocalization } from '@akinon/next/hooks';
7
6
 
8
7
  interface Props {
9
8
  searchKey?: string;
@@ -12,7 +11,6 @@ interface Props {
12
11
  export function FaqTabs(props: Props) {
13
12
  const { searchKey } = props;
14
13
  const { data, isLoading } = useGetWidgetQuery('faq');
15
- const { locale } = useLocalization();
16
14
 
17
15
  if (isLoading) {
18
16
  return <LoaderSpinner className="mt-4" />;
@@ -31,12 +29,8 @@ export function FaqTabs(props: Props) {
31
29
  {data?.attributes?.faq_contents
32
30
  ?.filter(
33
31
  (faq) =>
34
- faq.value.content
35
- .toLocaleLowerCase(locale)
36
- .includes(searchKey) ||
37
- faq.value.title
38
- .toLocaleLowerCase(locale)
39
- .includes(searchKey)
32
+ faq.value.content.toLocaleLowerCase().includes(searchKey) ||
33
+ faq.value.title.toLocaleLowerCase().includes(searchKey)
40
34
  )
41
35
  .map((faq, index) => {
42
36
  if (faq.value.category == item.value.category_id) {
@@ -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);
@@ -40,12 +39,7 @@ export const BasketItem = (props: Props) => {
40
39
  quantity: number,
41
40
  attributes: object = {}
42
41
  ) => {
43
- const requestParams: {
44
- product: number;
45
- quantity: number;
46
- attributes: object;
47
- namespace?: string;
48
- } = {
42
+ const requestParams: any = {
49
43
  product: productPk,
50
44
  quantity,
51
45
  attributes
@@ -55,21 +49,19 @@ export const BasketItem = (props: Props) => {
55
49
  requestParams.namespace = namespace;
56
50
  }
57
51
 
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
- }
52
+ await updateQuantityMutation(requestParams)
53
+ .unwrap()
54
+ .then((data) =>
55
+ dispatch(
56
+ basketApi.util.updateQueryData(
57
+ 'getBasket',
58
+ undefined,
59
+ (draftBasket) => {
60
+ Object.assign(draftBasket, data.basket);
61
+ }
62
+ )
67
63
  )
68
64
  );
69
- onBasketUpdate?.(response.basket);
70
- } catch (error) {
71
- console.error('Error updating quantity:', error);
72
- }
73
65
  };
74
66
 
75
67
  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
  });
@@ -12,7 +12,7 @@ export interface BreadcrumbProps {
12
12
  }
13
13
 
14
14
  export default function Breadcrumb(props: BreadcrumbProps) {
15
- const { t, locale } = useLocalization();
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(locale))}
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 {
@@ -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) {
@@ -76,7 +76,7 @@ export default function ActionMenu() {
76
76
  : 'bg-secondary-500 text-white'
77
77
  )}
78
78
  >
79
- <span data-testid="header-basket-count">{totalQuantity}</span>
79
+ {totalQuantity}
80
80
  </Badge>
81
81
  ),
82
82
  miniBasket: <MiniBasket />
@@ -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,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 { 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
- // 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 handleVariantChange = () => {
69
- clearProductError();
70
- setIsVariantLoading(true);
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
- <ProductVariants
88
- variants={data.variants}
89
- onVariantChange={handleVariantChange}
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
- <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}
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
- <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
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
  }