@akinon/projectzero 1.96.0-rc.57 → 1.96.0-snapshot-ZERO-35861-20250908151109

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 (54) hide show
  1. package/CHANGELOG.md +5 -236
  2. package/app-template/.env.example +0 -1
  3. package/app-template/CHANGELOG.md +312 -4844
  4. package/app-template/README.md +1 -25
  5. package/app-template/package.json +19 -21
  6. package/app-template/public/locales/en/common.json +1 -42
  7. package/app-template/public/locales/tr/common.json +1 -42
  8. package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +82 -9
  9. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +4 -17
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +1 -12
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +11 -29
  12. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +1 -12
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +10 -28
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +1 -12
  15. package/app-template/src/assets/fonts/pz-icon.css +0 -3
  16. package/app-template/src/components/accordion.tsx +19 -22
  17. package/app-template/src/components/currency-select.tsx +0 -1
  18. package/app-template/src/components/file-input.tsx +7 -27
  19. package/app-template/src/components/input.tsx +1 -2
  20. package/app-template/src/components/modal.tsx +16 -32
  21. package/app-template/src/components/select.tsx +26 -38
  22. package/app-template/src/components/types/index.ts +1 -25
  23. package/app-template/src/hooks/index.ts +0 -2
  24. package/app-template/src/plugins.js +1 -3
  25. package/app-template/src/settings.js +2 -8
  26. package/app-template/src/types/index.ts +3 -74
  27. package/app-template/src/views/account/address-form.tsx +4 -8
  28. package/app-template/src/views/account/content-header.tsx +2 -2
  29. package/app-template/src/views/basket/basket-item.tsx +13 -16
  30. package/app-template/src/views/basket/summary.tsx +7 -10
  31. package/app-template/src/views/guest-login/index.tsx +1 -6
  32. package/app-template/src/views/header/action-menu.tsx +1 -1
  33. package/app-template/src/views/header/search/index.tsx +5 -17
  34. package/app-template/src/views/login/index.tsx +10 -11
  35. package/app-template/src/views/otp-login/index.tsx +6 -11
  36. package/app-template/src/views/product/product-info.tsx +263 -61
  37. package/app-template/src/views/product/slider.tsx +73 -86
  38. package/app-template/src/views/register/index.tsx +11 -15
  39. package/commands/plugins.ts +16 -63
  40. package/dist/commands/plugins.js +16 -57
  41. package/package.json +1 -1
  42. package/app-template/.github/instructions/checkout.instructions.md +0 -678
  43. package/app-template/AGENTS.md +0 -7
  44. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
  45. package/app-template/src/app/api/image-proxy/route.ts +0 -1
  46. package/app-template/src/app/api/similar-product-list/route.ts +0 -1
  47. package/app-template/src/app/api/similar-products/route.ts +0 -1
  48. package/app-template/src/hooks/use-product-cart.ts +0 -77
  49. package/app-template/src/hooks/use-stock-alert.ts +0 -74
  50. package/app-template/src/utils/variant-validation.ts +0 -41
  51. package/app-template/src/views/basket/basket-content.tsx +0 -106
  52. package/app-template/src/views/product/product-actions.tsx +0 -165
  53. package/app-template/src/views/product/product-share.tsx +0 -56
  54. package/app-template/src/views/product/product-variants.tsx +0 -26
@@ -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,10 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { signIn } from 'next-auth/react';
3
+ import { signIn, SignInOptions } from 'next-auth/react';
4
4
  import { useSearchParams } from 'next/navigation';
5
5
  import { useState } from 'react';
6
6
  import { ROUTES } from '@theme/routes';
7
- import { LoginFormType, FormType, PzSignInOptions } from '@theme/types';
7
+ import { LoginFormType } from '@theme/types';
8
8
  import { Button, Input, Link } from '@theme/components';
9
9
  import { SubmitHandler, useForm } from 'react-hook-form';
10
10
  import * as yup from 'yup';
@@ -79,9 +79,8 @@ export const Login = () => {
79
79
  redirect: false,
80
80
  callbackUrl: searchParams.get('callbackUrl') ?? '/',
81
81
  captchaValidated,
82
- ...data,
83
- formType: FormType.login
84
- } as PzSignInOptions);
82
+ ...data
83
+ } as SignInOptions);
85
84
 
86
85
  if (loginResponse.error) {
87
86
  const errors: AuthError[] = JSON.parse(loginResponse.error);
@@ -110,25 +109,25 @@ export const Login = () => {
110
109
  try {
111
110
  parsedValue = JSON.parse(item.value);
112
111
  } catch {
113
- parsedValue = [item.value];
112
+ parsedValue = [item.value];
114
113
  }
115
114
  } else {
116
- parsedValue = item.value;
115
+ parsedValue = item.value;
117
116
  }
118
117
 
119
118
  if (Array.isArray(parsedValue)) {
120
119
  setError(item.name as keyof LoginFormType, {
121
120
  type: 'custom',
122
- message: parsedValue.join(', ')
121
+ message: parsedValue.join(', '),
123
122
  });
124
123
  } else {
125
124
  Object.keys(parsedValue).forEach((key) => {
126
125
  const fieldName = key as keyof LoginFormType;
127
126
  const errorMessages = parsedValue[key] as string[];
128
-
127
+
129
128
  setError(fieldName, {
130
129
  type: 'custom',
131
- message: errorMessages.join(', ')
130
+ message: errorMessages.join(', '),
132
131
  });
133
132
  });
134
133
  }
@@ -162,7 +161,7 @@ export const Login = () => {
162
161
  method="post"
163
162
  onSubmit={handleSubmit(onSubmit)}
164
163
  >
165
- <input type="hidden" value={FormType.login} {...register('formType')} />
164
+ <input type="hidden" value="login" {...register('formType')} />
166
165
  <input type="hidden" value={locale} {...register('locale')} />
167
166
 
168
167
  <div className={clsx(errors.email ? 'mb-8' : 'mb-4')}>
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { signIn } from 'next-auth/react';
3
+ import { signIn, SignInOptions } from 'next-auth/react';
4
4
  import { useState } from 'react';
5
- import { OtpLoginFormType, FormType, PzSignInOptions } from '@theme/types';
5
+ import { OtpLoginFormType } from '@theme/types';
6
6
  import { Button, Input } from '@theme/components';
7
7
  import { SubmitHandler, useForm } from 'react-hook-form';
8
8
  import * as yup from 'yup';
@@ -47,9 +47,8 @@ export const OtpLogin = () => {
47
47
  const loginResponse = await signIn('default', {
48
48
  redirect: false,
49
49
  callbackUrl: '/',
50
- ...data,
51
- formType: FormType.otpLogin
52
- } as PzSignInOptions);
50
+ ...data
51
+ } as SignInOptions);
53
52
 
54
53
  if (loginResponse.error) {
55
54
  const errors: AuthError[] = JSON.parse(loginResponse.error);
@@ -101,11 +100,7 @@ export const OtpLogin = () => {
101
100
  {t('auth.login.title_gsm')}
102
101
  </h2>
103
102
  <form onSubmit={handleSubmit(onSubmit)}>
104
- <input
105
- type="hidden"
106
- value={FormType.otpLogin}
107
- {...register('formType')}
108
- />
103
+ <input type="hidden" value="otpLogin" {...register('formType')} />
109
104
  <input type="hidden" value={locale} {...register('locale')} />
110
105
 
111
106
  <div className="mb-4">
@@ -114,7 +109,7 @@ export const OtpLogin = () => {
114
109
  className="h-14"
115
110
  label={t('auth.login.form.phone.placeholder')}
116
111
  type="tel"
117
- format={user_phone_format.replace(/9/g, '#')}
112
+ format={user_phone_format.replace(/\9/g, '#')}
118
113
  mask="_"
119
114
  allowEmptyFormatting={true}
120
115
  control={control}
@@ -1,72 +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
44
  }, [isVariantLoading]);
62
45
 
46
+ useEffect(() => {
47
+ setCurrentUrl(window.location.href);
48
+ }, [currentUrl]);
49
+
63
50
  useEffect(() => {
64
51
  pushProductViewed(data?.product);
65
52
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
66
53
 
67
- const handleVariantChange = () => {
68
- clearProductError();
69
- 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
+ )
70
175
  };
71
176
 
72
177
  return (
@@ -82,26 +187,72 @@ export default function ProductInfo({ data }: ProductPageProps) {
82
187
  retailPrice={data.product.retail_price}
83
188
  />
84
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>
85
203
 
86
- <ProductVariants
87
- variants={data.variants}
88
- 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
+ }}
89
251
  />
90
252
 
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}
253
+ <PluginModule
254
+ component={Component.OneClickCheckoutButtons}
255
+ props={checkoutProviderProps}
105
256
  />
106
257
 
107
258
  <MiscButtons
@@ -110,7 +261,58 @@ export default function ProductInfo({ data }: ProductPageProps) {
110
261
  variants={data.variants}
111
262
  />
112
263
 
113
- <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>
114
316
  </>
115
317
  );
116
318
  }