@akinon/projectzero 1.102.0 → 1.103.0-rc.81

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 (60) hide show
  1. package/CHANGELOG.md +239 -4
  2. package/app-template/.env.example +1 -0
  3. package/app-template/.github/instructions/routing.instructions.md +603 -0
  4. package/app-template/CHANGELOG.md +5003 -310
  5. package/app-template/README.md +25 -1
  6. package/app-template/package.json +21 -19
  7. package/app-template/public/locales/en/checkout.json +6 -0
  8. package/app-template/public/locales/en/common.json +48 -1
  9. package/app-template/public/locales/tr/checkout.json +6 -0
  10. package/app-template/public/locales/tr/common.json +48 -1
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +9 -82
  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/api/form/[...id]/route.ts +1 -7
  15. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  16. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  17. package/app-template/src/app/api/similar-products/route.ts +1 -0
  18. package/app-template/src/assets/fonts/pz-icon.css +3 -0
  19. package/app-template/src/components/__tests__/link.test.tsx +2 -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/generate-form-fields.tsx +43 -4
  24. package/app-template/src/components/input.tsx +9 -2
  25. package/app-template/src/components/modal.tsx +32 -16
  26. package/app-template/src/components/pagination.tsx +1 -0
  27. package/app-template/src/components/price.tsx +1 -1
  28. package/app-template/src/components/select.tsx +38 -26
  29. package/app-template/src/components/types/index.ts +25 -1
  30. package/app-template/src/hooks/index.ts +2 -0
  31. package/app-template/src/hooks/use-product-cart.ts +77 -0
  32. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  33. package/app-template/src/plugins.js +3 -1
  34. package/app-template/src/settings.js +8 -2
  35. package/app-template/src/types/index.ts +17 -0
  36. package/app-template/src/utils/variant-validation.ts +41 -0
  37. package/app-template/src/views/account/address-form.tsx +8 -4
  38. package/app-template/src/views/account/contact-form.tsx +1 -1
  39. package/app-template/src/views/account/content-header.tsx +2 -2
  40. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  41. package/app-template/src/views/basket/basket-content.tsx +106 -0
  42. package/app-template/src/views/basket/basket-item.tsx +22 -14
  43. package/app-template/src/views/basket/summary.tsx +10 -7
  44. package/app-template/src/views/breadcrumb.tsx +2 -2
  45. package/app-template/src/views/category/category-info.tsx +1 -0
  46. package/app-template/src/views/category/filters/index.tsx +1 -1
  47. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
  48. package/app-template/src/views/checkout/summary.tsx +10 -0
  49. package/app-template/src/views/guest-login/index.tsx +6 -1
  50. package/app-template/src/views/header/action-menu.tsx +1 -1
  51. package/app-template/src/views/header/search/index.tsx +17 -5
  52. package/app-template/src/views/product/product-actions.tsx +165 -0
  53. package/app-template/src/views/product/product-info.tsx +62 -263
  54. package/app-template/src/views/product/product-share.tsx +56 -0
  55. package/app-template/src/views/product/product-variants.tsx +26 -0
  56. package/app-template/src/views/product/slider.tsx +86 -73
  57. package/app-template/src/widgets/footer-menu.tsx +6 -2
  58. package/commands/plugins.ts +63 -16
  59. package/dist/commands/plugins.js +57 -16
  60. 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,73 @@
1
1
  'use client';
2
2
 
3
3
  import clsx from 'clsx';
4
- import { Button, Icon, Modal } from '@theme/components';
5
- import { useAddProductToBasket } from '../../hooks';
6
4
  import React, { useEffect, useState } from 'react';
7
- import { useAddStockAlertMutation } from '@akinon/next/data/client/wishlist';
8
- import { pushAddToCart, pushProductViewed } from '@theme/utils/gtm';
9
- import { PriceWrapper, Variant } from '@theme/views/product';
10
- import Share from '@theme/views/share';
5
+ import { PriceWrapper } from '@theme/views/product';
11
6
  import { ProductPageProps } from './layout';
12
7
  import MiscButtons from './misc-buttons';
13
- import { useLocalization } from '@akinon/next/hooks';
14
- import PluginModule, { Component } from '@akinon/next/components/plugin-module';
15
- import { Trans } from '@akinon/next/components/trans';
8
+ import { pushProductViewed } from '@theme/utils/gtm';
16
9
  import { useSession } from 'next-auth/react';
10
+ import { isVariantSelectionComplete } from '../../utils/variant-validation';
11
+ import { useProductCart } from '../../hooks/use-product-cart';
12
+ import { useStockAlert } from '../../hooks/use-stock-alert';
13
+ import { ProductVariants } from './product-variants';
14
+ import { ProductActions } from './product-actions';
15
+ import { ProductShare } from './product-share';
17
16
 
18
17
  export default function ProductInfo({ data }: ProductPageProps) {
19
- const { t } = useLocalization();
20
18
  const { data: session } = useSession();
21
- const [currentUrl, setCurrentUrl] = useState(null);
22
- const [productError, setProductError] = useState(null);
23
- const [isModalOpen, setIsModalOpen] = useState(false);
24
- const [stockAlertResponseMessage, setStockAlertResponseMessage] =
25
- useState(null);
26
19
  const [isVariantLoading, setIsVariantLoading] = useState(false);
27
20
 
28
- const [addProduct, { isLoading: isAddToCartLoading }] =
29
- useAddProductToBasket();
30
- const [addStockAlert, { isLoading: isAddToStockAlertLoading }] =
31
- useAddStockAlertMutation();
32
21
  const inStock = data.selected_variant !== null || data.product.in_stock;
33
22
 
34
- useEffect(() => {
35
- isVariantSelectionComplete() && setIsVariantLoading(false);
23
+ const {
24
+ addProductToCart,
25
+ productError: cartError,
26
+ clearProductError: clearCartError,
27
+ isAddToCartLoading
28
+ } = useProductCart({
29
+ product: data.product,
30
+ variants: data.variants
31
+ });
32
+
33
+ const {
34
+ addProductToStockAlertList,
35
+ isModalOpen,
36
+ stockAlertResponseMessage,
37
+ productError: stockError,
38
+ isAddToStockAlertLoading,
39
+ closeModal,
40
+ clearError: clearStockError
41
+ } = useStockAlert({
42
+ productPk: data.product.pk,
43
+ userEmail: session?.user?.email
44
+ });
45
+
46
+ const productError = cartError || stockError;
47
+ const clearProductError = () => {
48
+ clearCartError();
49
+ clearStockError();
50
+ };
36
51
 
52
+ useEffect(() => {
53
+ isVariantSelectionComplete(data.variants) && setIsVariantLoading(false);
37
54
  !inStock && setIsVariantLoading(false);
38
- }, [data]); // eslint-disable-line react-hooks/exhaustive-deps
55
+ }, [data, inStock]);
39
56
 
40
57
  useEffect(() => {
41
58
  if (isVariantLoading) {
42
- setProductError(null);
59
+ clearProductError();
43
60
  }
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
62
  }, [isVariantLoading]);
45
63
 
46
- useEffect(() => {
47
- setCurrentUrl(window.location.href);
48
- }, [currentUrl]);
49
-
50
64
  useEffect(() => {
51
65
  pushProductViewed(data?.product);
52
66
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
53
67
 
54
- const addProductToCart = async () => {
55
- if (!variantsSelectionCheck()) {
56
- return;
57
- }
58
-
59
- try {
60
- await addProduct({
61
- product: data.product.pk,
62
- quantity: 1,
63
- attributes: {}
64
- });
65
-
66
- pushAddToCart(data?.product);
67
- } catch (error) {
68
- setProductError(
69
- error?.data?.non_field_errors ||
70
- Object.keys(error?.data).map(
71
- (key) => `${key}: ${error?.data[key].join(', ')}`
72
- )
73
- );
74
- }
75
- };
76
-
77
- const variantsSelectionCheck = () => {
78
- const unselectedVariant = data.variants.find((variant) =>
79
- variant.options.every((opt) => !opt.is_selected)
80
- );
81
-
82
- if (unselectedVariant) {
83
- setProductError(() => (
84
- <Trans
85
- i18nKey="product.please_select_variant"
86
- components={{
87
- VariantName: <span>{unselectedVariant.attribute_name}</span>
88
- }}
89
- />
90
- ));
91
-
92
- return false;
93
- }
94
-
95
- return true;
96
- };
97
-
98
- const isVariantSelectionComplete = () => {
99
- return data?.variants.every((variant) =>
100
- variant?.options.some((opt) => opt.is_selected)
101
- );
102
- };
103
-
104
- const addProductToStockAlertList = async () => {
105
- try {
106
- await addStockAlert({
107
- productPk: data.product.pk,
108
- email: session?.user?.email
109
- })
110
- .unwrap()
111
- .then(handleSuccess)
112
- .catch((err) => handleError(err));
113
- } catch (error) {
114
- setProductError(error?.data?.non_field_errors || null);
115
- }
116
- };
117
-
118
- const handleModalClick = () => {
119
- setIsModalOpen(false);
120
- };
121
-
122
- const handleSuccess = () => {
123
- setStockAlertResponseMessage(() => (
124
- <Trans
125
- i18nKey="product.stock_alert.success_description"
126
- components={{
127
- Email: <span>{session?.user?.email}</span>
128
- }}
129
- />
130
- ));
131
- setIsModalOpen(true);
132
- };
133
-
134
- const handleError = (err) => {
135
- if (err.status !== 401) {
136
- setStockAlertResponseMessage(
137
- t('product.stock_alert.error_description').toString()
138
- );
139
- setIsModalOpen(true);
140
- }
141
- };
142
-
143
- const checkoutProviderProps = {
144
- product: data.product,
145
- clearBasket: true,
146
- addBeforeClick: variantsSelectionCheck,
147
- openMiniBasket: false,
148
- className: clsx([
149
- 'py-2.5',
150
- 'bg-black',
151
- 'relative',
152
- 'hover:bg-black',
153
- 'before:content-[""]',
154
- 'before:w-6',
155
- 'before:h-6',
156
- 'before:bg-white',
157
- 'before:absolute',
158
- 'before:rounded-r-[18px]',
159
- 'before:left-0',
160
- 'after:content-[""]',
161
- 'after:absolute',
162
- 'after:w-3',
163
- 'after:h-3',
164
- 'after:bg-[#d02c2f]',
165
- 'after:rounded-xl',
166
- 'after:left-1'
167
- ]),
168
- onError: (error) =>
169
- setProductError(
170
- error?.data?.non_field_errors ||
171
- Object.keys(error?.data).map(
172
- (key) => `${key}: ${error?.data[key].join(', ')}`
173
- )
174
- )
68
+ const handleVariantChange = () => {
69
+ clearProductError();
70
+ setIsVariantLoading(true);
175
71
  };
176
72
 
177
73
  return (
@@ -187,72 +83,26 @@ export default function ProductInfo({ data }: ProductPageProps) {
187
83
  retailPrice={data.product.retail_price}
188
84
  />
189
85
  </div>
190
- <div className="flex flex-col">
191
- {data.variants.map((variant) => (
192
- <Variant
193
- key={variant.attribute_key}
194
- {...variant}
195
- className="items-center mt-8"
196
- onChange={() => {
197
- setProductError(null);
198
- setIsVariantLoading(true);
199
- }}
200
- />
201
- ))}
202
- </div>
203
-
204
- {productError && (
205
- <div className="mt-4 text-xs text-center text-error">
206
- {productError}
207
- </div>
208
- )}
209
86
 
210
- <Button
211
- disabled={
212
- isAddToCartLoading || isAddToStockAlertLoading || isVariantLoading
213
- }
214
- className={clsx(
215
- 'fixed bottom-0 right-0 w-1/2 h-14 z-[20] flex items-center justify-center fill-primary-foreground',
216
- 'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
217
- )}
218
- onClick={() => {
219
- setProductError(null);
220
-
221
- if (inStock) {
222
- addProductToCart();
223
- } else {
224
- addProductToStockAlertList();
225
- }
226
- }}
227
- data-testid="product-add-to-cart"
228
- >
229
- {isVariantLoading ? (
230
- <Icon
231
- name="spinner"
232
- size={20}
233
- className="animate-spin mr-4 fill-primary"
234
- />
235
- ) : inStock ? (
236
- <span>{t('product.add_to_cart')}</span>
237
- ) : (
238
- <>
239
- <Icon name="bell" size={20} className="mr-4" />
240
- <span>{t('product.add_stock_alert')}</span>
241
- </>
242
- )}
243
- </Button>
244
-
245
- <PluginModule
246
- component={Component.AkifastCheckoutButton}
247
- props={{
248
- ...checkoutProviderProps,
249
- isPdp: true
250
- }}
87
+ <ProductVariants
88
+ variants={data.variants}
89
+ onVariantChange={handleVariantChange}
251
90
  />
252
91
 
253
- <PluginModule
254
- component={Component.OneClickCheckoutButtons}
255
- props={checkoutProviderProps}
92
+ <ProductActions
93
+ product={data.product}
94
+ variants={data.variants}
95
+ inStock={inStock}
96
+ isVariantLoading={isVariantLoading}
97
+ onAddToCart={addProductToCart}
98
+ onAddToStockAlert={addProductToStockAlertList}
99
+ onClearError={clearProductError}
100
+ isAddToCartLoading={isAddToCartLoading}
101
+ isAddToStockAlertLoading={isAddToStockAlertLoading}
102
+ productError={productError}
103
+ isModalOpen={isModalOpen}
104
+ stockAlertResponseMessage={stockAlertResponseMessage}
105
+ onCloseModal={closeModal}
256
106
  />
257
107
 
258
108
  <MiscButtons
@@ -261,58 +111,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
261
111
  variants={data.variants}
262
112
  />
263
113
 
264
- <Share
265
- className="my-2 sm:mb-4"
266
- buttonText={t('product.share')}
267
- items={[
268
- {
269
- href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
270
- currentUrl
271
- )}`,
272
- iconName: 'facebook',
273
- iconSize: 22
274
- },
275
- {
276
- href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
277
- currentUrl
278
- )}`,
279
- iconName: 'twitter',
280
- iconSize: 22
281
- },
282
- {
283
- href: `https://api.whatsapp.com/send?text=${
284
- data.product.name
285
- }%20${encodeURIComponent(currentUrl)}`,
286
- iconName: 'whatsapp',
287
- iconSize: 22
288
- }
289
- ]}
290
- />
291
-
292
- <Modal
293
- portalId="stock-alert-modal"
294
- open={isModalOpen}
295
- setOpen={setIsModalOpen}
296
- showCloseButton={false}
297
- className="w-5/6 md:max-w-md"
298
- >
299
- <div className="flex flex-col items-center justify-center gap-4 px-6 py-9">
300
- <Icon name="bell" size={48} />
301
- <h2 className="text-xl font-semibold">
302
- {t('product.stock_alert.title')}
303
- </h2>
304
- <div className="max-w-40 text-xs text-center leading-4">
305
- <p>{stockAlertResponseMessage}</p>
306
- </div>
307
- <Button
308
- onClick={handleModalClick}
309
- appearance="outlined"
310
- className="font-semibold px-10 h-12"
311
- >
312
- {t('product.stock_alert.close_button')}
313
- </Button>
314
- </div>
315
- </Modal>
114
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
316
115
  </>
317
116
  );
318
117
  }
@@ -0,0 +1,56 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import Share from '@theme/views/share';
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+
5
+ interface ProductShareProps {
6
+ productName: string;
7
+ className?: string;
8
+ }
9
+
10
+ export const ProductShare: React.FC<ProductShareProps> = ({
11
+ productName,
12
+ className
13
+ }) => {
14
+ const { t } = useLocalization();
15
+ const [currentUrl, setCurrentUrl] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ setCurrentUrl(window.location.href);
19
+ }, []);
20
+
21
+ if (!currentUrl) {
22
+ return null;
23
+ }
24
+
25
+ const shareItems = [
26
+ {
27
+ href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
28
+ currentUrl
29
+ )}`,
30
+ iconName: 'facebook' as const,
31
+ iconSize: 22
32
+ },
33
+ {
34
+ href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
35
+ currentUrl
36
+ )}`,
37
+ iconName: 'twitter' as const,
38
+ iconSize: 22
39
+ },
40
+ {
41
+ href: `https://api.whatsapp.com/send?text=${productName}%20${encodeURIComponent(
42
+ currentUrl
43
+ )}`,
44
+ iconName: 'whatsapp' as const,
45
+ iconSize: 22
46
+ }
47
+ ];
48
+
49
+ return (
50
+ <Share
51
+ className={className}
52
+ buttonText={t('product.share')}
53
+ items={shareItems}
54
+ />
55
+ );
56
+ };
@@ -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
+ };