@akinon/projectzero 1.106.0 → 1.107.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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @akinon/projectzero
2
2
 
3
+ ## 1.107.0
4
+
3
5
  ## 1.106.0
4
6
 
5
7
  ### Minor Changes
@@ -1,5 +1,38 @@
1
1
  # projectzeronext
2
2
 
3
+ ## 1.107.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1606f335: ZERO-3527: Refactor ProductInfo component to streamline state management and improve variant handling.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [4ca44c78]
12
+ - Updated dependencies [28c7ea79]
13
+ - Updated dependencies [72bfcbf2]
14
+ - Updated dependencies [b6e5b624]
15
+ - Updated dependencies [6bfbdc27]
16
+ - Updated dependencies [5b500797]
17
+ - Updated dependencies [9442cf01]
18
+ - @akinon/pz-saved-card@1.107.0
19
+ - @akinon/next@1.107.0
20
+ - @akinon/pz-basket-gift-pack@1.107.0
21
+ - @akinon/pz-akifast@1.107.0
22
+ - @akinon/pz-b2b@1.107.0
23
+ - @akinon/pz-bkm@1.107.0
24
+ - @akinon/pz-checkout-gift-pack@1.107.0
25
+ - @akinon/pz-click-collect@1.107.0
26
+ - @akinon/pz-credit-payment@1.107.0
27
+ - @akinon/pz-gpay@1.107.0
28
+ - @akinon/pz-hepsipay@1.107.0
29
+ - @akinon/pz-masterpass@1.107.0
30
+ - @akinon/pz-one-click-checkout@1.107.0
31
+ - @akinon/pz-otp@1.107.0
32
+ - @akinon/pz-pay-on-delivery@1.107.0
33
+ - @akinon/pz-tabby-extension@1.107.0
34
+ - @akinon/pz-tamara-extension@1.107.0
35
+
3
36
  ## 1.106.0
4
37
 
5
38
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "1.106.0",
3
+ "version": "1.107.0",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -24,23 +24,23 @@
24
24
  "test:middleware": "jest middleware-matcher.test.ts --bail"
25
25
  },
26
26
  "dependencies": {
27
- "@akinon/next": "1.106.0",
28
- "@akinon/pz-akifast": "1.106.0",
29
- "@akinon/pz-b2b": "1.106.0",
30
- "@akinon/pz-basket-gift-pack": "1.106.0",
31
- "@akinon/pz-bkm": "1.106.0",
32
- "@akinon/pz-checkout-gift-pack": "1.106.0",
33
- "@akinon/pz-click-collect": "1.106.0",
34
- "@akinon/pz-credit-payment": "1.106.0",
35
- "@akinon/pz-gpay": "1.106.0",
36
- "@akinon/pz-hepsipay": "1.106.0",
37
- "@akinon/pz-masterpass": "1.106.0",
38
- "@akinon/pz-one-click-checkout": "1.106.0",
39
- "@akinon/pz-otp": "1.106.0",
40
- "@akinon/pz-pay-on-delivery": "1.106.0",
41
- "@akinon/pz-saved-card": "1.106.0",
42
- "@akinon/pz-tabby-extension": "1.106.0",
43
- "@akinon/pz-tamara-extension": "1.106.0",
27
+ "@akinon/next": "1.107.0",
28
+ "@akinon/pz-akifast": "1.107.0",
29
+ "@akinon/pz-b2b": "1.107.0",
30
+ "@akinon/pz-basket-gift-pack": "1.107.0",
31
+ "@akinon/pz-bkm": "1.107.0",
32
+ "@akinon/pz-checkout-gift-pack": "1.107.0",
33
+ "@akinon/pz-click-collect": "1.107.0",
34
+ "@akinon/pz-credit-payment": "1.107.0",
35
+ "@akinon/pz-gpay": "1.107.0",
36
+ "@akinon/pz-hepsipay": "1.107.0",
37
+ "@akinon/pz-masterpass": "1.107.0",
38
+ "@akinon/pz-one-click-checkout": "1.107.0",
39
+ "@akinon/pz-otp": "1.107.0",
40
+ "@akinon/pz-pay-on-delivery": "1.107.0",
41
+ "@akinon/pz-saved-card": "1.107.0",
42
+ "@akinon/pz-tabby-extension": "1.107.0",
43
+ "@akinon/pz-tamara-extension": "1.107.0",
44
44
  "@hookform/resolvers": "2.9.0",
45
45
  "@next/third-parties": "14.1.0",
46
46
  "@react-google-maps/api": "2.17.1",
@@ -63,7 +63,7 @@
63
63
  "yup": "0.32.11"
64
64
  },
65
65
  "devDependencies": {
66
- "@akinon/eslint-plugin-projectzero": "1.106.0",
66
+ "@akinon/eslint-plugin-projectzero": "1.107.0",
67
67
  "@semantic-release/changelog": "6.0.2",
68
68
  "@semantic-release/exec": "6.0.3",
69
69
  "@semantic-release/git": "10.0.1",
@@ -0,0 +1,77 @@
1
+ import { useState } from 'react';
2
+ import { useAddProductToBasket } from './index';
3
+ import { pushAddToCart } from '@theme/utils/gtm';
4
+ import { validateVariantSelection } from '../utils/variant-validation';
5
+ import { VariantType } from '@akinon/next/types';
6
+
7
+ interface Product {
8
+ pk: number;
9
+ [key: string]: any;
10
+ }
11
+
12
+ interface UseProductCartProps {
13
+ product: Product;
14
+ variants: VariantType[];
15
+ }
16
+
17
+ interface AddToCartError {
18
+ data?: {
19
+ non_field_errors?: string[];
20
+ [key: string]: string[];
21
+ };
22
+ }
23
+
24
+ export const useProductCart = ({ product, variants }: UseProductCartProps) => {
25
+ const [productError, setProductError] = useState<React.ReactNode | null>(null);
26
+ const [addProduct, { isLoading: isAddToCartLoading }] = useAddProductToBasket();
27
+
28
+ const formatError = (error: AddToCartError) => {
29
+ if (error?.data?.non_field_errors) {
30
+ return error.data.non_field_errors;
31
+ }
32
+
33
+ if (error?.data) {
34
+ return Object.keys(error.data).map(
35
+ (key) => `${key}: ${error.data[key].join(', ')}`
36
+ );
37
+ }
38
+
39
+ return 'An error occurred';
40
+ };
41
+
42
+ const addProductToCart = async () => {
43
+ const validation = validateVariantSelection(variants);
44
+
45
+ if (!validation.isValid) {
46
+ setProductError(validation.errorMessage);
47
+ return false;
48
+ }
49
+
50
+ try {
51
+ await addProduct({
52
+ product: product.pk,
53
+ quantity: 1,
54
+ attributes: {}
55
+ });
56
+
57
+ pushAddToCart(product);
58
+ setProductError(null);
59
+ return true;
60
+ } catch (error) {
61
+ const formattedError = formatError(error as AddToCartError);
62
+ setProductError(formattedError);
63
+ return false;
64
+ }
65
+ };
66
+
67
+ const clearProductError = () => {
68
+ setProductError(null);
69
+ };
70
+
71
+ return {
72
+ addProductToCart,
73
+ productError,
74
+ clearProductError,
75
+ isAddToCartLoading
76
+ };
77
+ };
@@ -0,0 +1,74 @@
1
+ import React, { useState } from 'react';
2
+ import { useAddStockAlertMutation } from '@akinon/next/data/client/wishlist';
3
+ import { Trans } from '@akinon/next/components/trans';
4
+ import { useLocalization } from '@akinon/next/hooks';
5
+
6
+ interface UseStockAlertProps {
7
+ productPk: number;
8
+ userEmail?: string;
9
+ }
10
+
11
+ export const useStockAlert = ({ productPk, userEmail }: UseStockAlertProps) => {
12
+ const { t } = useLocalization();
13
+ const [isModalOpen, setIsModalOpen] = useState(false);
14
+ const [stockAlertResponseMessage, setStockAlertResponseMessage] = useState<React.ReactNode | null>(null);
15
+ const [productError, setProductError] = useState<React.ReactNode | null>(null);
16
+
17
+ const [addStockAlert, { isLoading: isAddToStockAlertLoading }] = useAddStockAlertMutation();
18
+
19
+ const handleSuccess = () => {
20
+ setStockAlertResponseMessage(React.createElement(
21
+ Trans,
22
+ {
23
+ i18nKey: "product.stock_alert.success_description",
24
+ components: {
25
+ Email: React.createElement('span', {}, userEmail)
26
+ }
27
+ }
28
+ ));
29
+ setIsModalOpen(true);
30
+ setProductError(null);
31
+ };
32
+
33
+ const handleError = (err: any) => {
34
+ if (err.status !== 401) {
35
+ setStockAlertResponseMessage(
36
+ t('product.stock_alert.error_description').toString()
37
+ );
38
+ setIsModalOpen(true);
39
+ }
40
+ };
41
+
42
+ const addProductToStockAlertList = async () => {
43
+ try {
44
+ await addStockAlert({
45
+ productPk,
46
+ email: userEmail
47
+ })
48
+ .unwrap()
49
+ .then(handleSuccess)
50
+ .catch(handleError);
51
+ } catch (error: any) {
52
+ setProductError(error?.data?.non_field_errors || null);
53
+ }
54
+ };
55
+
56
+ const closeModal = () => {
57
+ setIsModalOpen(false);
58
+ };
59
+
60
+ const clearError = () => {
61
+ setProductError(null);
62
+ };
63
+
64
+ return {
65
+ addProductToStockAlertList,
66
+ isModalOpen,
67
+ setIsModalOpen,
68
+ stockAlertResponseMessage,
69
+ productError,
70
+ isAddToStockAlertLoading,
71
+ closeModal,
72
+ clearError
73
+ };
74
+ };
@@ -15,5 +15,6 @@ module.exports = [
15
15
  'pz-saved-card',
16
16
  'pz-tabby-extension',
17
17
  'pz-tamara-extension',
18
+ 'pz-cybersource-uc',
18
19
  'pz-hepsipay'
19
20
  ];
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { Trans } from '@akinon/next/components/trans';
3
+ import { VariantType } from '@akinon/next/types';
4
+
5
+ export const isVariantSelectionComplete = (variants: VariantType[]): boolean => {
6
+ return variants?.every((variant) =>
7
+ variant?.options.some((opt) => opt.is_selected)
8
+ );
9
+ };
10
+
11
+ export const getUnselectedVariant = (variants: VariantType[]): VariantType | undefined => {
12
+ return variants.find((variant) =>
13
+ variant.options.every((opt) => !opt.is_selected)
14
+ );
15
+ };
16
+
17
+ export const createVariantErrorMessage = (unselectedVariant: VariantType) => {
18
+ const TransComponent = Trans as any;
19
+ return React.createElement(
20
+ TransComponent,
21
+ {
22
+ i18nKey: "product.please_select_variant",
23
+ components: {
24
+ VariantName: React.createElement('span', {}, unselectedVariant.attribute_name)
25
+ }
26
+ }
27
+ );
28
+ };
29
+
30
+ export const validateVariantSelection = (variants: VariantType[]) => {
31
+ const unselectedVariant = getUnselectedVariant(variants);
32
+
33
+ if (unselectedVariant) {
34
+ return {
35
+ isValid: false,
36
+ errorMessage: createVariantErrorMessage(unselectedVariant)
37
+ };
38
+ }
39
+
40
+ return { isValid: true, errorMessage: null };
41
+ };
@@ -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,178 +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
  }
44
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
45
62
  }, [isVariantLoading]);
46
63
 
47
- useEffect(() => {
48
- setCurrentUrl(window.location.href);
49
- }, [currentUrl]);
50
-
51
64
  useEffect(() => {
52
65
  pushProductViewed(data?.product);
53
66
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
54
67
 
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
- )
68
+ const handleVariantChange = () => {
69
+ clearProductError();
70
+ setIsVariantLoading(true);
176
71
  };
177
72
 
178
73
  return (
@@ -188,72 +83,26 @@ export default function ProductInfo({ data }: ProductPageProps) {
188
83
  retailPrice={data.product.retail_price}
189
84
  />
190
85
  </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>
204
-
205
- {productError && (
206
- <div className="mt-4 text-xs text-center text-error">
207
- {productError}
208
- </div>
209
- )}
210
86
 
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
- }}
87
+ <ProductVariants
88
+ variants={data.variants}
89
+ onVariantChange={handleVariantChange}
252
90
  />
253
91
 
254
- <PluginModule
255
- component={Component.OneClickCheckoutButtons}
256
- 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}
257
106
  />
258
107
 
259
108
  <MiscButtons
@@ -262,58 +111,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
262
111
  variants={data.variants}
263
112
  />
264
113
 
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>
114
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
317
115
  </>
318
116
  );
319
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
+ };
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { Variant } from '@theme/views/product';
3
+ import { VariantType } from '@akinon/next/types';
4
+
5
+ interface ProductVariantsProps {
6
+ variants: VariantType[];
7
+ onVariantChange: () => void;
8
+ }
9
+
10
+ export const ProductVariants: React.FC<ProductVariantsProps> = ({
11
+ variants,
12
+ onVariantChange
13
+ }) => {
14
+ return (
15
+ <div className="flex flex-col">
16
+ {variants.map((variant) => (
17
+ <Variant
18
+ key={variant.attribute_key}
19
+ {...variant}
20
+ className="items-center mt-8"
21
+ onChange={onVariantChange}
22
+ />
23
+ ))}
24
+ </div>
25
+ );
26
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.106.0",
3
+ "version": "1.107.0",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {