@akinon/projectzero 2.0.0-beta.12 → 2.0.0-beta.14

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 (179) hide show
  1. package/CHANGELOG.md +102 -23
  2. package/app-template/.env.example +1 -0
  3. package/app-template/.github/instructions/account.instructions.md +749 -0
  4. package/app-template/.github/instructions/checkout.instructions.md +678 -0
  5. package/app-template/.github/instructions/default.instructions.md +279 -0
  6. package/app-template/.github/instructions/edge-cases.instructions.md +73 -0
  7. package/app-template/.github/instructions/routing.instructions.md +603 -0
  8. package/app-template/.github/instructions/settings.instructions.md +338 -0
  9. package/app-template/.gitignore +3 -0
  10. package/app-template/AGENTS.md +7 -0
  11. package/app-template/CHANGELOG.md +1387 -310
  12. package/app-template/Procfile +1 -1
  13. package/app-template/akinon.json +0 -3
  14. package/app-template/build.sh +10 -0
  15. package/app-template/docs/advanced-usage.md +101 -0
  16. package/app-template/docs/sentry-usage.md +35 -0
  17. package/app-template/next-env.d.ts +1 -0
  18. package/app-template/{next.config.ts → next.config.mjs} +6 -6
  19. package/app-template/package.json +58 -51
  20. package/app-template/postcss.config.mjs +1 -4
  21. package/app-template/public/locales/en/checkout.json +11 -0
  22. package/app-template/public/locales/en/common.json +50 -1
  23. package/app-template/public/locales/en/product.json +62 -1
  24. package/app-template/public/locales/tr/checkout.json +11 -0
  25. package/app-template/public/locales/tr/common.json +50 -1
  26. package/app-template/public/locales/tr/product.json +63 -0
  27. package/app-template/public/masterpass-javascript-sdk-web.min.js +1 -0
  28. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/[...prettyurl]/page.tsx +9 -9
  29. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/layout.tsx +2 -2
  30. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/cancellation/page.tsx +6 -6
  31. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/page.tsx +6 -6
  32. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/page.tsx +1 -1
  33. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/profile/page.tsx +2 -2
  34. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/address/stores/page.tsx +2 -2
  35. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/page.tsx +1 -1
  36. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket/page.tsx +2 -2
  37. package/app-template/src/app/[pz]/category/[pk]/page.tsx +27 -0
  38. package/app-template/src/app/[pz]/flat-page/[pk]/page.tsx +23 -0
  39. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/forms/[pk]/generate/page.tsx +2 -3
  40. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +93 -0
  41. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/page.tsx +2 -4
  42. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/layout.tsx +3 -10
  43. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/list/page.tsx +2 -4
  44. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/not-found.tsx +5 -7
  45. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/page.tsx +6 -4
  46. package/app-template/src/app/[pz]/product/[pk]/page.tsx +102 -0
  47. package/app-template/src/app/[pz]/special-page/[pk]/page.tsx +35 -0
  48. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/email-set-primary/[[...id]]/page.tsx +3 -4
  49. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/registration/account-confirm-email/[[...id]]/page.tsx +3 -3
  50. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/reset/[[...id]]/page.tsx +6 -12
  51. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/[node]/route.ts +2 -2
  52. package/app-template/src/app/api/auth/[...nextauth]/route.ts +3 -0
  53. package/app-template/src/app/api/form/[...id]/route.ts +1 -7
  54. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  55. package/app-template/src/app/api/product-categories/route.ts +1 -0
  56. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  57. package/app-template/src/app/api/similar-products/route.ts +1 -0
  58. package/app-template/src/app/api/virtual-try-on/limited-categories/route.ts +1 -0
  59. package/app-template/src/app/api/virtual-try-on/route.ts +1 -0
  60. package/app-template/src/assets/globals.scss +4 -133
  61. package/app-template/src/auth.ts +3 -0
  62. package/app-template/src/components/__tests__/badge.test.tsx +2 -2
  63. package/app-template/src/components/__tests__/link.test.tsx +2 -0
  64. package/app-template/src/components/accordion.tsx +23 -20
  65. package/app-template/src/components/button.tsx +1 -1
  66. package/app-template/src/components/carousel-core.tsx +4 -11
  67. package/app-template/src/components/checkbox.tsx +1 -1
  68. package/app-template/src/components/currency-select.tsx +1 -0
  69. package/app-template/src/components/file-input.tsx +27 -7
  70. package/app-template/src/components/generate-form-fields.tsx +49 -10
  71. package/app-template/src/components/input.tsx +11 -5
  72. package/app-template/src/components/modal.tsx +32 -16
  73. package/app-template/src/components/pagination.tsx +1 -0
  74. package/app-template/src/components/price.tsx +1 -1
  75. package/app-template/src/components/pwa-tags.tsx +1 -0
  76. package/app-template/src/components/select.tsx +39 -27
  77. package/app-template/src/components/shimmer.tsx +1 -1
  78. package/app-template/src/components/types/index.ts +25 -1
  79. package/app-template/src/hooks/use-fav-button.tsx +4 -8
  80. package/app-template/src/hooks/use-product-cart.ts +77 -0
  81. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  82. package/app-template/src/plugins.js +12 -2
  83. package/app-template/src/redux/middlewares/category.ts +5 -4
  84. package/app-template/src/redux/store.ts +21 -1
  85. package/app-template/src/routes/index.ts +2 -1
  86. package/app-template/src/settings.js +3 -1
  87. package/app-template/src/types/index.ts +74 -3
  88. package/app-template/src/types/next-auth.d.ts +2 -2
  89. package/app-template/src/utils/variant-validation.ts +41 -0
  90. package/app-template/src/views/account/address-form.tsx +8 -4
  91. package/app-template/src/views/account/contact-form.tsx +2 -2
  92. package/app-template/src/views/account/content-header.tsx +4 -3
  93. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  94. package/app-template/src/views/account/order.tsx +1 -1
  95. package/app-template/src/views/account/orders/order-cancellation-item.tsx +1 -1
  96. package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +1 -1
  97. package/app-template/src/views/basket/basket-item.tsx +6 -1
  98. package/app-template/src/views/basket/summary.tsx +16 -0
  99. package/app-template/src/views/breadcrumb.tsx +2 -2
  100. package/app-template/src/views/category/category-info.tsx +2 -1
  101. package/app-template/src/views/category/filters/index.tsx +1 -1
  102. package/app-template/src/views/checkout/auth.tsx +1 -1
  103. package/app-template/src/views/checkout/layout/header.tsx +1 -1
  104. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +22 -6
  105. package/app-template/src/views/checkout/steps/payment/options/funds-transfer.tsx +25 -5
  106. package/app-template/src/views/checkout/steps/payment/options/loyalty.tsx +21 -2
  107. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +22 -4
  108. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
  109. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
  110. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +3 -3
  111. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +1 -1
  112. package/app-template/src/views/checkout/summary.tsx +12 -2
  113. package/app-template/src/views/find-in-store/index.tsx +2 -2
  114. package/app-template/src/views/header/action-menu.tsx +2 -6
  115. package/app-template/src/views/header/band.tsx +2 -2
  116. package/app-template/src/views/header/index.tsx +1 -1
  117. package/app-template/src/views/header/mini-basket.tsx +2 -2
  118. package/app-template/src/views/header/mobile-menu.tsx +6 -6
  119. package/app-template/src/views/header/navbar.tsx +1 -1
  120. package/app-template/src/views/header/pwa-back-button.tsx +1 -1
  121. package/app-template/src/views/header/search/index.tsx +13 -3
  122. package/app-template/src/views/header/search/results.tsx +1 -1
  123. package/app-template/src/views/header/user-menu.tsx +1 -3
  124. package/app-template/src/views/login/index.tsx +14 -13
  125. package/app-template/src/views/otp-login/index.tsx +11 -6
  126. package/app-template/src/views/product/layout.tsx +15 -1
  127. package/app-template/src/views/product/product-actions.tsx +165 -0
  128. package/app-template/src/views/product/product-info.tsx +69 -261
  129. package/app-template/src/views/product/product-share.tsx +56 -0
  130. package/app-template/src/views/product/product-variants.tsx +26 -0
  131. package/app-template/src/views/product/slider.tsx +22 -1
  132. package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
  133. package/app-template/src/views/register/index.tsx +17 -21
  134. package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
  135. package/app-template/src/widgets/footer-info.tsx +1 -1
  136. package/app-template/src/widgets/footer-menu.tsx +7 -3
  137. package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
  138. package/app-template/src/widgets/home-stories-eng.tsx +43 -35
  139. package/app-template/tailwind.config.js +129 -1
  140. package/app-template/tsconfig.json +29 -11
  141. package/codemods/migrate-segments/index.js +591 -0
  142. package/commands/plugins.ts +62 -14
  143. package/dist/commands/plugins.js +62 -14
  144. package/package.json +1 -1
  145. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +0 -22
  146. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +0 -20
  147. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +0 -74
  148. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +0 -84
  149. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +0 -27
  150. package/app-template/src/pages/api/auth/[...nextauth].ts +0 -3
  151. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/address/page.tsx +0 -0
  152. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-email/page.tsx +0 -0
  153. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/change-password/page.tsx +0 -0
  154. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/contact/page.tsx +0 -0
  155. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/coupons/page.tsx +0 -0
  156. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/email-verification/page.tsx +0 -0
  157. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/faq/page.tsx +0 -0
  158. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/favourite-products/page.tsx +0 -0
  159. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/my-quotations/page.tsx +0 -0
  160. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/[id]/layout.tsx +0 -0
  161. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/account/orders/page.tsx +0 -0
  162. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/anonymous-tracking/page.tsx +0 -0
  163. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/auth/oauth-login/page.tsx +0 -0
  164. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/basket-b2b/page.tsx +0 -0
  165. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/category/[pk]/loading.tsx +0 -0
  166. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/client-root.tsx +0 -0
  167. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/contact-us/page.tsx +0 -0
  168. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/error.tsx +0 -0
  169. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/flat-page/[pk]/loading.tsx +0 -0
  170. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/group-product/[pk]/loading.tsx +0 -0
  171. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/landing-page/[pk]/loading.tsx +0 -0
  172. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/list/loading.tsx +0 -0
  173. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/checkout/page.tsx +0 -0
  174. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/orders/completed/[token]/layout.tsx +0 -0
  175. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/page.tsx +0 -0
  176. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/special-page/[pk]/loading.tsx +0 -0
  177. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/template.tsx +0 -0
  178. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/users/password/reset/page.tsx +0 -0
  179. /package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/xml-sitemap/route.ts +0 -0
@@ -1,184 +1,81 @@
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';
16
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
17
17
 
18
18
  export default function ProductInfo({ data }: ProductPageProps) {
19
- const { t } = useLocalization();
20
19
  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
20
  const [isVariantLoading, setIsVariantLoading] = useState(false);
27
21
 
28
- const [addProduct, { isLoading: isAddToCartLoading }] =
29
- useAddProductToBasket();
30
- const [addStockAlert, { isLoading: isAddToStockAlertLoading }] =
31
- useAddStockAlertMutation();
32
22
  const inStock = data.selected_variant !== null || data.product.in_stock;
33
23
 
34
- useEffect(() => {
35
- isVariantSelectionComplete() && setIsVariantLoading(false);
24
+ const {
25
+ addProductToCart,
26
+ productError: cartError,
27
+ clearProductError: clearCartError,
28
+ isAddToCartLoading
29
+ } = useProductCart({
30
+ product: data.product,
31
+ variants: data.variants
32
+ });
33
+
34
+ const {
35
+ addProductToStockAlertList,
36
+ isModalOpen,
37
+ stockAlertResponseMessage,
38
+ productError: stockError,
39
+ isAddToStockAlertLoading,
40
+ closeModal,
41
+ clearError: clearStockError
42
+ } = useStockAlert({
43
+ productPk: data.product.pk,
44
+ userEmail: session?.user?.email
45
+ });
46
+
47
+ const productError = cartError || stockError;
48
+ const clearProductError = () => {
49
+ clearCartError();
50
+ clearStockError();
51
+ };
36
52
 
53
+ useEffect(() => {
54
+ isVariantSelectionComplete(data.variants) && setIsVariantLoading(false);
37
55
  !inStock && setIsVariantLoading(false);
38
- }, [data]); // eslint-disable-line react-hooks/exhaustive-deps
56
+ }, [data, inStock]);
39
57
 
40
58
  useEffect(() => {
41
59
  if (isVariantLoading) {
42
- setProductError(null);
60
+ clearProductError();
43
61
  }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
63
  }, [isVariantLoading]);
45
64
 
46
- useEffect(() => {
47
- setCurrentUrl(window.location.href);
48
- }, [currentUrl]);
49
-
50
65
  useEffect(() => {
51
66
  pushProductViewed(data?.product);
52
67
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
53
68
 
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
- )
69
+ const handleVariantChange = () => {
70
+ clearProductError();
71
+ setIsVariantLoading(true);
175
72
  };
176
73
 
177
74
  return (
178
75
  <>
179
76
  <div
180
77
  className={clsx(
181
- 'fixed bottom-0 left-0 w-1/2 h-14 z-20 bg-white mt-0 border-t border-gray-500 items-center justify-center',
78
+ 'fixed bottom-0 left-0 w-1/2 h-14 z-[20] bg-white mt-0 border-t border-gray-500 items-center justify-center',
182
79
  'sm:relative sm:flex sm:items-center sm:mt-5 sm:border-none'
183
80
  )}
184
81
  >
@@ -187,132 +84,43 @@ export default function ProductInfo({ data }: ProductPageProps) {
187
84
  retailPrice={data.product.retail_price}
188
85
  />
189
86
  </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
87
 
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);
88
+ <ProductVariants
89
+ variants={data.variants}
90
+ onVariantChange={handleVariantChange}
91
+ />
220
92
 
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>
93
+ <ProductActions
94
+ product={data.product}
95
+ variants={data.variants}
96
+ inStock={inStock}
97
+ isVariantLoading={isVariantLoading}
98
+ onAddToCart={addProductToCart}
99
+ onAddToStockAlert={addProductToStockAlertList}
100
+ onClearError={clearProductError}
101
+ isAddToCartLoading={isAddToCartLoading}
102
+ isAddToStockAlertLoading={isAddToStockAlertLoading}
103
+ productError={productError}
104
+ isModalOpen={isModalOpen}
105
+ stockAlertResponseMessage={stockAlertResponseMessage}
106
+ onCloseModal={closeModal}
107
+ />
244
108
 
245
109
  <PluginModule
246
- component={Component.AkifastCheckoutButton}
110
+ component={Component.VirtualTryOnPlugin}
247
111
  props={{
248
- ...checkoutProviderProps,
249
- isPdp: true
112
+ product: data.product,
113
+ className: 'hidden sm:flex'
250
114
  }}
251
115
  />
252
116
 
253
- <PluginModule
254
- component={Component.OneClickCheckoutButtons}
255
- props={checkoutProviderProps}
256
- />
257
-
258
117
  <MiscButtons
259
118
  productName={data.product.name}
260
119
  productPk={data.product.pk}
261
120
  variants={data.variants}
262
121
  />
263
122
 
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>
123
+ <ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
316
124
  </>
317
125
  );
318
126
  }
@@ -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
+ };
@@ -7,6 +7,7 @@ import { Product } from '@akinon/next/types';
7
7
  import { Image } from '@akinon/next/components/image';
8
8
  import useFavButton from '../../hooks/use-fav-button';
9
9
  import { twMerge } from 'tailwind-merge';
10
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
10
11
 
11
12
  type ProductSliderItem = {
12
13
  product: Product;
@@ -86,7 +87,27 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
86
87
  </div>
87
88
 
88
89
  <div className="relative lg:col-span-5">
89
- <FavButton className="absolute right-8 top-6 z-20 sm:hidden" />
90
+ <FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
91
+
92
+ <PluginModule
93
+ component={Component.ProductImageSearchFeature}
94
+ props={{
95
+ product,
96
+ activeIndex,
97
+ showResetButton: true,
98
+ enableTextSearch: true,
99
+ isEnabled: true
100
+ }}
101
+ />
102
+
103
+ <PluginModule
104
+ component={Component.VirtualTryOnPlugin}
105
+ props={{
106
+ product,
107
+ className:
108
+ 'sm:hidden absolute bottom-[70px] right-[5px] z-[30] w-auto px-4 text-xs mt-0'
109
+ }}
110
+ />
90
111
 
91
112
  <CarouselCore
92
113
  responsive={{
@@ -109,7 +109,7 @@ const ProductPointerWidget = (props: ProductPointerWidgetProps) => {
109
109
  hidden: buttonStatus
110
110
  })}
111
111
  >
112
- <div className="w-full h-full flex items-center gap-2 shrink-0">
112
+ <div className="w-full h-full flex items-center gap-2 flex-shrink-0">
113
113
  <Image
114
114
  src={productItem?.kwargs?.value?.card_image?.url}
115
115
  alt={productItem?.value?.alt}
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { yupResolver } from '@hookform/resolvers/yup';
4
4
  import clsx from 'clsx';
5
- import { signIn, SignInOptions } from 'next-auth/react';
5
+ import { signIn } from 'next-auth/react';
6
6
  import { useState } from 'react';
7
7
  import { SubmitHandler, useForm } from 'react-hook-form';
8
8
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
9
- import { RegisterFormType } from '@theme/types';
9
+ import { RegisterFormType, FormType, PzSignInOptions } from '@theme/types';
10
10
  import { Button, Checkbox, Icon, Input, Modal } from '@theme/components';
11
11
  import * as yup from 'yup';
12
12
  import { useCaptcha, useLocalization, useRouter } from '@akinon/next/hooks';
@@ -120,29 +120,21 @@ export const Register = () => {
120
120
  const [showPassword, setShowPassword] = useState(false);
121
121
  const { user_phone_format } = useAppSelector((state) => state.config);
122
122
 
123
- const registerHandler: SubmitHandler<RegisterFormType> = async (data) => {
123
+ const registerHandler = async (data: RegisterFormType) => {
124
124
  return await signIn('default', {
125
125
  redirect: false,
126
126
  callbackUrl: '/',
127
127
  captchaValidated,
128
- ...data
129
- } as SignInOptions);
128
+ ...data,
129
+ formType: FormType.register
130
+ } as PzSignInOptions & { redirect: false });
130
131
  };
131
132
 
132
133
  const onSubmit: SubmitHandler<RegisterFormType> = async (data) => {
133
134
  const registerResponse = await registerHandler(data);
134
135
 
135
- if (registerResponse.error === 'Captcha') {
136
- if (await validateCaptcha()) {
137
- onSubmit(data);
138
- }
139
-
140
- return;
141
- }
142
-
143
136
  if (registerResponse.error) {
144
- const errors: AuthError[] = JSON.parse(registerResponse.error);
145
-
137
+ const errors: AuthError[] = JSON.parse(registerResponse.code);
146
138
 
147
139
  if (errors.find((error) => error.type === 'captcha')) {
148
140
  if (await validateCaptcha()) {
@@ -171,25 +163,25 @@ export const Register = () => {
171
163
  try {
172
164
  parsedValue = JSON.parse(item.value);
173
165
  } catch {
174
- parsedValue = [item.value];
166
+ parsedValue = [item.value];
175
167
  }
176
168
  } else {
177
- parsedValue = item.value;
169
+ parsedValue = item.value;
178
170
  }
179
171
 
180
172
  if (Array.isArray(parsedValue)) {
181
173
  setError(item.name as keyof RegisterFormType, {
182
174
  type: 'custom',
183
- message: parsedValue.join(', '),
175
+ message: parsedValue.join(', ')
184
176
  });
185
177
  } else {
186
178
  Object.keys(parsedValue).forEach((key) => {
187
179
  const fieldName = key as keyof RegisterFormType;
188
180
  const errorMessages = parsedValue[key] as string[];
189
-
181
+
190
182
  setError(fieldName, {
191
183
  type: 'custom',
192
- message: errorMessages.join(', '),
184
+ message: errorMessages.join(', ')
193
185
  });
194
186
  });
195
187
  }
@@ -247,7 +239,11 @@ export const Register = () => {
247
239
  </p>
248
240
 
249
241
  <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
250
- <input type="hidden" value="register" {...register('formType')} />
242
+ <input
243
+ type="hidden"
244
+ value={FormType.register}
245
+ {...register('formType')}
246
+ />
251
247
  <input type="hidden" value={locale} {...register('locale')} />
252
248
 
253
249
  <div className={clsx({ 'mb-4': errors.email })}>