@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,11 +1,21 @@
1
1
  import { useState } from 'react';
2
2
  import { forwardRef } from 'react';
3
3
  import { FileInputProps } from '@theme/components/types';
4
- import clsx from 'clsx';
5
4
  import { useLocalization } from '@akinon/next/hooks';
5
+ import { twMerge } from 'tailwind-merge';
6
6
 
7
7
  export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
8
- function FileInput({ className, onChange, ...props }, ref) {
8
+ function FileInput(
9
+ {
10
+ buttonClassName,
11
+ onChange,
12
+ fileClassName,
13
+ fileNameWrapperClassName,
14
+ fileInputClassName,
15
+ ...props
16
+ },
17
+ ref
18
+ ) {
9
19
  const { t } = useLocalization();
10
20
  const [fileNames, setFileNames] = useState<string[]>([]);
11
21
 
@@ -24,24 +34,34 @@ export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
24
34
  type="file"
25
35
  {...props}
26
36
  ref={ref}
27
- className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
37
+ className={twMerge(
38
+ 'absolute inset-0 w-full h-full opacity-0 cursor-pointer',
39
+ fileInputClassName
40
+ )}
28
41
  onChange={handleFileChange}
29
42
  />
30
43
  <button
31
44
  type="button"
32
- className={clsx('bg-primary text-white py-2 px-4 text-sm', className)}
45
+ className={twMerge(
46
+ 'bg-primary text-white py-2 px-4 text-sm',
47
+ buttonClassName
48
+ )}
33
49
  >
34
50
  {t('common.file_input.select_file')}
35
51
  </button>
36
- <div className="mt-1 text-gray-500">
52
+ <div
53
+ className={twMerge('mt-1 text-gray-500', fileNameWrapperClassName)}
54
+ >
37
55
  {fileNames.length > 0 ? (
38
- <ul className="list-disc pl-4 text-xs">
56
+ <ul className={twMerge('list-disc pl-4 text-xs', fileClassName)}>
39
57
  {fileNames.map((name, index) => (
40
58
  <li key={index}>{name}</li>
41
59
  ))}
42
60
  </ul>
43
61
  ) : (
44
- <span className="text-xs">{t('common.file_input.no_file')}</span>
62
+ <span className={twMerge('text-xs', fileClassName)}>
63
+ {t('common.file_input.no_file')}
64
+ </span>
45
65
  )}
46
66
  </div>
47
67
  </div>
@@ -7,6 +7,7 @@ import { useForm } from 'react-hook-form';
7
7
  import { yupResolver } from '@hookform/resolvers/yup';
8
8
  import * as yup from 'yup';
9
9
  import DynamicForm from './dynamic-form';
10
+
10
11
  import {
11
12
  AllFieldClassesType,
12
13
  FieldPropertiesType,
@@ -14,6 +15,7 @@ import {
14
15
  FormPropertiesType,
15
16
  Schema
16
17
  } from '@akinon/next/types';
18
+ import { useLocalization } from '@akinon/next/hooks';
17
19
 
18
20
  export function GenerateFormFields({
19
21
  schema,
@@ -28,8 +30,14 @@ export function GenerateFormFields({
28
30
  formProperties: FormPropertiesType;
29
31
  submitButtonText: string;
30
32
  }) {
33
+ const { t } = useLocalization();
31
34
  const [fields, setFields] = useState([]);
32
35
  const [loading, setIsLoading] = useState(true);
36
+ const [isSubmitting, setIsSubmitting] = useState(false);
37
+ const [submitStatus, setSubmitStatus] = useState<
38
+ 'idle' | 'success' | 'error'
39
+ >('idle');
40
+ const [submitMessage, setSubmitMessage] = useState('');
33
41
 
34
42
  const generateValidationSchema = () => {
35
43
  const schemaObject = {};
@@ -80,6 +88,7 @@ export function GenerateFormFields({
80
88
  const {
81
89
  handleSubmit,
82
90
  register,
91
+ reset,
83
92
  formState: { errors }
84
93
  } = useForm<FormField>({
85
94
  resolver: yupResolver(generateValidationSchema())
@@ -108,6 +117,10 @@ export function GenerateFormFields({
108
117
  }, [schema, fieldProperties]);
109
118
 
110
119
  const onSubmit = async (data) => {
120
+ setIsSubmitting(true);
121
+ setSubmitStatus('idle');
122
+ setSubmitMessage('');
123
+
111
124
  try {
112
125
  const formData = new FormData();
113
126
 
@@ -115,12 +128,25 @@ export function GenerateFormFields({
115
128
  formData.append(key, data[key]);
116
129
  });
117
130
 
118
- fetch(formProperties.actionUrl, {
131
+ const response = await fetch(formProperties.actionUrl, {
119
132
  method: 'POST',
120
133
  body: formData
121
134
  });
135
+
136
+ if (response.ok) {
137
+ setSubmitStatus('success');
138
+ setSubmitMessage(t('common.forms.success'));
139
+ reset();
140
+ } else {
141
+ setSubmitStatus('error');
142
+ setSubmitMessage(t('common.forms.error'));
143
+ }
122
144
  } catch (error) {
123
- console.error('Form submit error:', error);
145
+ console.error(t('common.forms.submit_error'), error);
146
+ setSubmitStatus('error');
147
+ setSubmitMessage(t('common.forms.error'));
148
+ } finally {
149
+ setIsSubmitting(false);
124
150
  }
125
151
  };
126
152
 
@@ -157,7 +183,7 @@ export function GenerateFormFields({
157
183
  className={twMerge(allFieldClasses?.className, field.class)}
158
184
  name={field.key}
159
185
  {...field.attributes}
160
- error={errors[field.key]}
186
+ error={errors[field.key] as any}
161
187
  {...register(field.key)}
162
188
  />
163
189
  </div>
@@ -187,7 +213,7 @@ export function GenerateFormFields({
187
213
  className={twMerge(allFieldClasses?.className, field?.class)}
188
214
  name={field.key}
189
215
  {...field.attributes}
190
- error={errors[field.key]}
216
+ error={errors[field.key] as any}
191
217
  {...register(field.key, { valueAsNumber: true })}
192
218
  />
193
219
  </div>
@@ -220,7 +246,7 @@ export function GenerateFormFields({
220
246
  />
221
247
  {errors[field.key] && (
222
248
  <span className="mt-1 text-sm text-error">
223
- {errors[field.key].message}
249
+ {String(errors[field.key].message)}
224
250
  </span>
225
251
  )}
226
252
  </div>
@@ -251,7 +277,7 @@ export function GenerateFormFields({
251
277
  className={twMerge(allFieldClasses?.className, field?.class)}
252
278
  name={field.key}
253
279
  {...field.attributes}
254
- error={errors[field.key]}
280
+ error={errors[field.key] as any}
255
281
  {...register(field.key)}
256
282
  />
257
283
  </div>
@@ -285,7 +311,7 @@ export function GenerateFormFields({
285
311
  label: choice
286
312
  }))}
287
313
  {...field.attributes}
288
- error={errors[field.key]}
314
+ error={errors[field.key] as any}
289
315
  {...register(field.key)}
290
316
  />
291
317
  </div>
@@ -316,7 +342,7 @@ export function GenerateFormFields({
316
342
  className={twMerge(allFieldClasses?.className, field?.class)}
317
343
  name={field.key}
318
344
  {...field.attributes}
319
- error={errors[field.key]}
345
+ error={errors[field.key] as any}
320
346
  {...register(field.key)}
321
347
  />
322
348
  </div>
@@ -337,9 +363,22 @@ export function GenerateFormFields({
337
363
  <LoaderSpinner />
338
364
  ) : (
339
365
  <>
366
+ {submitStatus === 'success' && (
367
+ <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
368
+ {submitMessage}
369
+ </div>
370
+ )}
371
+
372
+ {submitStatus === 'error' && (
373
+ <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
374
+ {submitMessage}
375
+ </div>
376
+ )}
377
+
340
378
  {fields.map((field: FormField) => generateField(field))}
341
- <Button type="submit" className="w-full">
342
- {submitButtonText}
379
+
380
+ <Button type="submit" className="w-full" disabled={isSubmitting}>
381
+ {isSubmitting ? t('common.forms.sending') : submitButtonText}
343
382
  </Button>
344
383
  </>
345
384
  )}
@@ -39,8 +39,8 @@ export const Input = forwardRef<
39
39
  const hasFloatingLabel = label && labelStyle === 'floating';
40
40
  const inputClass = twMerge(
41
41
  clsx(
42
- 'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer disabled:bg-gray-50',
43
- 'focus-visible:outline-hidden', // disable outline on focus
42
+ 'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer',
43
+ 'focus-visible:outline-none', // disable outline on focus
44
44
  error
45
45
  ? 'border-error focus:border-error'
46
46
  : 'border-gray-500 hover:border-black focus:border-black'
@@ -48,7 +48,13 @@ export const Input = forwardRef<
48
48
  props.className
49
49
  );
50
50
 
51
- const inputProps: any = {
51
+ const inputProps: {
52
+ id?: string;
53
+ ref?: Ref<HTMLInputElement>;
54
+ className?: string;
55
+ onFocus?: () => void;
56
+ onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
57
+ } = {
52
58
  id,
53
59
  ref,
54
60
  className: inputClass,
@@ -68,7 +74,7 @@ export const Input = forwardRef<
68
74
  className={twMerge(
69
75
  'text-xs text-gray-800 transition-all',
70
76
  clsx({
71
- 'absolute left-2.5 pointer-events-none transform flex items-center h-full top-0! peer-placeholder-shown:-translate-y-2 peer-placeholder-shown:bg-white peer-placeholder-shown:inline-flex peer-placeholder-shown:h-auto':
77
+ 'absolute left-2.5 pointer-events-none transform flex items-center h-full !top-0 peer-placeholder-shown:-translate-y-2 peer-placeholder-shown:bg-white peer-placeholder-shown:inline-flex peer-placeholder-shown:h-auto':
72
78
  hasFloatingLabel,
73
79
  'mb-2': !hasFloatingLabel,
74
80
  '-translate-y-2 bg-white inline-flex h-auto':
@@ -112,7 +118,7 @@ export const Input = forwardRef<
112
118
  )}
113
119
  </div>
114
120
  {error && (
115
- <span className="mt-1 text-sm text-error">{error.message}</span>
121
+ <span className="mt-1 text-sm text-error">{String(error.message)}</span>
116
122
  )}
117
123
  </div>
118
124
  );
@@ -4,16 +4,7 @@ import ReactPortal from './react-portal';
4
4
  import { Icon } from './icon';
5
5
  import { twMerge } from 'tailwind-merge';
6
6
  import { useEffect } from 'react';
7
-
8
- export interface ModalProps {
9
- portalId: string;
10
- children?: React.ReactNode;
11
- open?: boolean;
12
- setOpen?: (open: boolean) => void;
13
- title?: React.ReactNode;
14
- showCloseButton?: React.ReactNode;
15
- className?: string;
16
- }
7
+ import { ModalProps } from '@theme/types';
17
8
 
18
9
  export const Modal = (props: ModalProps) => {
19
10
  const {
@@ -23,7 +14,14 @@ export const Modal = (props: ModalProps) => {
23
14
  setOpen,
24
15
  title = '',
25
16
  showCloseButton = true,
26
- className
17
+ className,
18
+ overlayClassName,
19
+ headerWrapperClassName,
20
+ titleClassName,
21
+ closeButtonClassName,
22
+ iconName = 'close',
23
+ iconSize = 16,
24
+ iconClassName
27
25
  } = props;
28
26
 
29
27
  useEffect(() => {
@@ -38,7 +36,12 @@ export const Modal = (props: ModalProps) => {
38
36
 
39
37
  return (
40
38
  <ReactPortal wrapperId={portalId}>
41
- <div className="fixed top-0 left-0 w-screen h-screen bg-primary/60 z-50" />
39
+ <div
40
+ className={twMerge(
41
+ 'fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50',
42
+ overlayClassName
43
+ )}
44
+ />
42
45
  <section
43
46
  className={twMerge(
44
47
  'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
@@ -46,15 +49,28 @@ export const Modal = (props: ModalProps) => {
46
49
  )}
47
50
  >
48
51
  {(showCloseButton || title) && (
49
- <div className="flex px-6 py-4 border-b border-gray-400">
50
- {title && <h3 className="text-lg font-light">{title}</h3>}
52
+ <div
53
+ className={twMerge(
54
+ 'flex px-6 py-4 border-b border-gray-400',
55
+ headerWrapperClassName
56
+ )}
57
+ >
58
+ {title && (
59
+ <h3 className={twMerge('text-lg font-light', titleClassName)}>
60
+ {title}
61
+ </h3>
62
+ )}
51
63
  {showCloseButton && (
52
64
  <button
53
65
  type="button"
54
66
  onClick={() => setOpen(false)}
55
- className="ml-auto"
67
+ className={twMerge('ml-auto', closeButtonClassName)}
56
68
  >
57
- <Icon name="close" size={16} />
69
+ <Icon
70
+ name={iconName}
71
+ size={iconSize}
72
+ className={iconClassName}
73
+ />
58
74
  </button>
59
75
  )}
60
76
  </div>
@@ -125,6 +125,7 @@ export const Pagination = (props: PaginationProps) => {
125
125
  setPrevPage(1);
126
126
  setNextPage(1);
127
127
  }
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
128
129
  }, [page]);
129
130
 
130
131
  useEffect(() => {
@@ -56,7 +56,7 @@ export const Price = (props: NumericFormatProps & PriceProps) => {
56
56
 
57
57
  const currentCurrencyDecimalScale = Settings.localization.currencies.find(
58
58
  (currency) => currency.code === currencyCode_
59
- ).decimalScale;
59
+ )?.decimalScale;
60
60
 
61
61
  return (
62
62
  <NumericFormat
@@ -2,6 +2,7 @@ import { Metadata } from '@akinon/next/types';
2
2
 
3
3
  const pwaTags: Metadata = {
4
4
  manifest: '/manifest.json',
5
+ themeColor: '#FFFFFF',
5
6
  formatDetection: {
6
7
  telephone: false
7
8
  },
@@ -14,14 +14,18 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) => {
14
14
  error,
15
15
  label,
16
16
  required = false,
17
+ labelClassName,
17
18
  ...rest
18
19
  } = props;
19
20
 
20
21
  return (
21
22
  <label
22
- className={clsx('flex flex-col relative text-xs text-gray-800', {
23
- 'pl-7': icon
24
- })}
23
+ className={twMerge(
24
+ clsx('flex flex-col relative text-xs text-gray-800', {
25
+ 'pl-7': icon
26
+ }),
27
+ labelClassName
28
+ )}
25
29
  >
26
30
  {icon && (
27
31
  <Icon
@@ -32,34 +36,42 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) => {
32
36
  )}
33
37
 
34
38
  {label && (
35
- <span className="mb-1">
39
+ <span className={twMerge('mb-1', labelClassName)}>
36
40
  {label} {required && <span className="text-secondary">*</span>}
37
41
  </span>
38
42
  )}
39
- <select
40
- {...rest}
41
- ref={ref}
42
- className={twMerge(
43
- clsx(
44
- 'cursor-pointer truncate h-10 w-40 px-2.5 shrink-0 outline-hidden',
45
- !borderless &&
46
- 'border border-gray-200 transition-all duration-150 hover:border-primary'
47
- ),
48
- className
49
- )}
50
- >
51
- {options?.map((option) => (
52
- <option
53
- key={option.value}
54
- value={option.value}
55
- className={option.class}
56
- >
57
- {option.label}
58
- </option>
59
- ))}
60
- </select>
43
+ <div className="relative">
44
+ <select
45
+ {...rest}
46
+ ref={ref}
47
+ className={twMerge(
48
+ clsx(
49
+ 'cursor-pointer truncate h-10 w-40 px-2.5 shrink-0 outline-none',
50
+ !borderless &&
51
+ 'border border-gray-200 transition-all duration-150 hover:border-primary',
52
+ 'appearance-none bg-transparent'
53
+ ),
54
+ className
55
+ )}
56
+ >
57
+ {options?.map((option) => (
58
+ <option
59
+ key={option.value}
60
+ value={option.value}
61
+ className={option.class}
62
+ >
63
+ {option.label}
64
+ </option>
65
+ ))}
66
+ </select>
67
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
68
+ <svg className="h-4 w-4 fill-current" viewBox="0 0 20 20">
69
+ <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
70
+ </svg>
71
+ </div>
72
+ </div>
61
73
  {error && (
62
- <span className="mt-1 text-sm text-error">{error.message}</span>
74
+ <span className="mt-1 text-sm text-error">{String(error.message)}</span>
63
75
  )}
64
76
  </label>
65
77
  );
@@ -12,7 +12,7 @@ export const Shimmer: React.FC<ShimmerProps> = ({ className }) => {
12
12
  >
13
13
  <div
14
14
  className={twMerge(
15
- 'w-full h-full bg-black/20 shadow-[0 0 30px 30px rgba(255,255,255,0.2)]',
15
+ 'w-full h-full bg-black bg-opacity-20 shadow-[0 0 30px 30px rgba(255,255,255,0.2)]',
16
16
  className
17
17
  )}
18
18
  ></div>
@@ -29,7 +29,13 @@ export interface PaginationProps {
29
29
  isLoading?: boolean;
30
30
  }
31
31
 
32
- export type FileInputProps = React.HTMLProps<HTMLInputElement>;
32
+ export interface FileInputProps extends React.HTMLProps<HTMLInputElement> {
33
+ fileClassName?: string;
34
+ fileNameWrapperClassName?: string;
35
+ fileInputClassName?: string;
36
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
37
+ buttonClassName?: string;
38
+ }
33
39
 
34
40
  export type RadioProps = React.HTMLProps<HTMLInputElement>;
35
41
 
@@ -58,6 +64,7 @@ export interface SelectProps extends React.HTMLProps<HTMLSelectElement> {
58
64
  iconSize?: number;
59
65
  error?: FieldError | undefined;
60
66
  required?: boolean;
67
+ labelClassName?: string;
61
68
  }
62
69
  export interface IconProps extends React.ComponentPropsWithRef<'i'> {
63
70
  name: string;
@@ -91,3 +98,20 @@ export interface BadgeProps {
91
98
  children: ReactNode;
92
99
  className?: string;
93
100
  }
101
+
102
+ export type AccordionProps = {
103
+ isCollapse?: boolean;
104
+ collapseClassName?: string;
105
+ title?: string;
106
+ subTitle?: string;
107
+ icons?: string[];
108
+ iconSize?: number;
109
+ iconColor?: string;
110
+ children?: ReactNode;
111
+ headerClassName?: string;
112
+ className?: string;
113
+ titleClassName?: string;
114
+ subTitleClassName?: string;
115
+ dataTestId?: string;
116
+ contentClassName?: string;
117
+ };
@@ -35,7 +35,7 @@ const useFavButton = (productPk: number) => {
35
35
  [favorites, productPk]
36
36
  );
37
37
 
38
- const [isActive, setIsActive] = useState(Boolean(favoriteItem));
38
+ const isActive = Boolean(favoriteItem);
39
39
  const [isPushed, setIsPushed] = useState<boolean>(false);
40
40
 
41
41
  const [addFavorite] = useAddFavoriteMutation();
@@ -55,20 +55,16 @@ const useFavButton = (productPk: number) => {
55
55
  }, [favoriteItem, productPk, addFavorite, removeFavorite]);
56
56
 
57
57
  useEffect(() => {
58
- setIsActive(Boolean(favoriteItem));
59
- }, [favoriteItem]);
60
-
61
- useEffect(() => {
62
- if (favoriteItem && !isActive && isPushed) {
63
- setIsActive(true);
58
+ if (favoriteItem && isPushed) {
64
59
  pushAddToWishlist({
65
60
  base_code: favoriteItem?.product?.base_code,
66
61
  name: favoriteItem?.product?.name,
67
62
  price: favoriteItem?.product?.price,
68
63
  currency_type: favoriteItem?.product?.currency_type
69
64
  });
65
+ setIsPushed(false);
70
66
  }
71
- }, [favoriteItem, isActive, isPushed]);
67
+ }, [favoriteItem, isPushed]);
72
68
 
73
69
  const FavButton = useMemo(() => {
74
70
  const View = (props: FavButtonProps) => (
@@ -0,0 +1,77 @@
1
+ import { useState } from 'react';
2
+ import { useAddProductToBasket } from './index';
3
+ import { pushAddToCart } from '@theme/utils/gtm';
4
+ import { validateVariantSelection } from '../utils/variant-validation';
5
+ import { VariantType } from '@akinon/next/types';
6
+
7
+ interface Product {
8
+ pk: number;
9
+ [key: string]: any;
10
+ }
11
+
12
+ interface UseProductCartProps {
13
+ product: Product;
14
+ variants: VariantType[];
15
+ }
16
+
17
+ interface AddToCartError {
18
+ data?: {
19
+ non_field_errors?: string[];
20
+ [key: string]: string[];
21
+ };
22
+ }
23
+
24
+ export const useProductCart = ({ product, variants }: UseProductCartProps) => {
25
+ const [productError, setProductError] = useState<React.ReactNode | null>(null);
26
+ const [addProduct, { isLoading: isAddToCartLoading }] = useAddProductToBasket();
27
+
28
+ const formatError = (error: AddToCartError) => {
29
+ if (error?.data?.non_field_errors) {
30
+ return error.data.non_field_errors;
31
+ }
32
+
33
+ if (error?.data) {
34
+ return Object.keys(error.data).map(
35
+ (key) => `${key}: ${error.data[key].join(', ')}`
36
+ );
37
+ }
38
+
39
+ return 'An error occurred';
40
+ };
41
+
42
+ const addProductToCart = async () => {
43
+ const validation = validateVariantSelection(variants);
44
+
45
+ if (!validation.isValid) {
46
+ setProductError(validation.errorMessage);
47
+ return false;
48
+ }
49
+
50
+ try {
51
+ await addProduct({
52
+ product: product.pk,
53
+ quantity: 1,
54
+ attributes: {}
55
+ });
56
+
57
+ pushAddToCart(product);
58
+ setProductError(null);
59
+ return true;
60
+ } catch (error) {
61
+ const formattedError = formatError(error as AddToCartError);
62
+ setProductError(formattedError);
63
+ return false;
64
+ }
65
+ };
66
+
67
+ const clearProductError = () => {
68
+ setProductError(null);
69
+ };
70
+
71
+ return {
72
+ addProductToCart,
73
+ productError,
74
+ clearProductError,
75
+ isAddToCartLoading
76
+ };
77
+ };