@akinon/projectzero 1.98.0 → 1.99.0-rc.66

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 (52) hide show
  1. package/CHANGELOG.md +233 -4
  2. package/app-template/.env.example +1 -0
  3. package/app-template/CHANGELOG.md +4978 -320
  4. package/app-template/README.md +25 -1
  5. package/app-template/package.json +19 -19
  6. package/app-template/public/locales/en/common.json +42 -1
  7. package/app-template/public/locales/tr/common.json +42 -1
  8. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +9 -82
  9. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +17 -4
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +12 -1
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +29 -11
  12. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +12 -1
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +67 -0
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +28 -10
  15. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +12 -1
  16. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  17. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  18. package/app-template/src/app/api/similar-products/route.ts +1 -0
  19. package/app-template/src/assets/fonts/pz-icon.css +3 -0
  20. package/app-template/src/components/accordion.tsx +22 -19
  21. package/app-template/src/components/currency-select.tsx +1 -0
  22. package/app-template/src/components/file-input.tsx +27 -7
  23. package/app-template/src/components/input.tsx +2 -1
  24. package/app-template/src/components/modal.tsx +32 -16
  25. package/app-template/src/components/select.tsx +38 -26
  26. package/app-template/src/components/types/index.ts +25 -1
  27. package/app-template/src/hooks/index.ts +2 -0
  28. package/app-template/src/hooks/use-product-cart.ts +77 -0
  29. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  30. package/app-template/src/plugins.js +3 -1
  31. package/app-template/src/settings.js +8 -2
  32. package/app-template/src/types/index.ts +74 -3
  33. package/app-template/src/utils/variant-validation.ts +41 -0
  34. package/app-template/src/views/account/address-form.tsx +8 -4
  35. package/app-template/src/views/account/content-header.tsx +2 -2
  36. package/app-template/src/views/basket/basket-content.tsx +106 -0
  37. package/app-template/src/views/basket/basket-item.tsx +16 -13
  38. package/app-template/src/views/basket/summary.tsx +10 -7
  39. package/app-template/src/views/guest-login/index.tsx +6 -1
  40. package/app-template/src/views/header/action-menu.tsx +1 -1
  41. package/app-template/src/views/header/search/index.tsx +17 -5
  42. package/app-template/src/views/login/index.tsx +11 -10
  43. package/app-template/src/views/otp-login/index.tsx +11 -6
  44. package/app-template/src/views/product/product-actions.tsx +165 -0
  45. package/app-template/src/views/product/product-info.tsx +61 -263
  46. package/app-template/src/views/product/product-share.tsx +56 -0
  47. package/app-template/src/views/product/product-variants.tsx +26 -0
  48. package/app-template/src/views/product/slider.tsx +86 -73
  49. package/app-template/src/views/register/index.tsx +15 -11
  50. package/commands/plugins.ts +63 -16
  51. package/dist/commands/plugins.js +57 -16
  52. package/package.json +1 -1
@@ -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,72 @@
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
  }, [isVariantLoading]);
45
62
 
46
- useEffect(() => {
47
- setCurrentUrl(window.location.href);
48
- }, [currentUrl]);
49
-
50
63
  useEffect(() => {
51
64
  pushProductViewed(data?.product);
52
65
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
53
66
 
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
- )
67
+ const handleVariantChange = () => {
68
+ clearProductError();
69
+ setIsVariantLoading(true);
175
70
  };
176
71
 
177
72
  return (
@@ -187,72 +82,26 @@ export default function ProductInfo({ data }: ProductPageProps) {
187
82
  retailPrice={data.product.retail_price}
188
83
  />
189
84
  </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
85
 
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
- }}
86
+ <ProductVariants
87
+ variants={data.variants}
88
+ onVariantChange={handleVariantChange}
251
89
  />
252
90
 
253
- <PluginModule
254
- component={Component.OneClickCheckoutButtons}
255
- props={checkoutProviderProps}
91
+ <ProductActions
92
+ product={data.product}
93
+ variants={data.variants}
94
+ inStock={inStock}
95
+ isVariantLoading={isVariantLoading}
96
+ onAddToCart={addProductToCart}
97
+ onAddToStockAlert={addProductToStockAlertList}
98
+ onClearError={clearProductError}
99
+ isAddToCartLoading={isAddToCartLoading}
100
+ isAddToStockAlertLoading={isAddToStockAlertLoading}
101
+ productError={productError}
102
+ isModalOpen={isModalOpen}
103
+ stockAlertResponseMessage={stockAlertResponseMessage}
104
+ onCloseModal={closeModal}
256
105
  />
257
106
 
258
107
  <MiscButtons
@@ -261,58 +110,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
261
110
  variants={data.variants}
262
111
  />
263
112
 
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>
113
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
316
114
  </>
317
115
  );
318
116
  }
@@ -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
+ };