@akinon/projectzero 1.100.0 → 1.101.0-rc.74

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 (65) hide show
  1. package/CHANGELOG.md +235 -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 +4998 -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]/category/[pk]/page.tsx +17 -4
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/flat-page/[pk]/page.tsx +12 -1
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/group-product/[pk]/page.tsx +29 -11
  15. package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +12 -1
  16. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +67 -0
  17. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/page.tsx +28 -10
  18. package/app-template/src/app/[commerce]/[locale]/[currency]/special-page/[pk]/page.tsx +12 -1
  19. package/app-template/src/app/api/form/[...id]/route.ts +1 -7
  20. package/app-template/src/app/api/image-proxy/route.ts +1 -0
  21. package/app-template/src/app/api/similar-product-list/route.ts +1 -0
  22. package/app-template/src/app/api/similar-products/route.ts +1 -0
  23. package/app-template/src/assets/fonts/pz-icon.css +3 -0
  24. package/app-template/src/components/__tests__/link.test.tsx +2 -0
  25. package/app-template/src/components/accordion.tsx +22 -19
  26. package/app-template/src/components/currency-select.tsx +1 -0
  27. package/app-template/src/components/file-input.tsx +27 -7
  28. package/app-template/src/components/generate-form-fields.tsx +43 -4
  29. package/app-template/src/components/input.tsx +9 -2
  30. package/app-template/src/components/modal.tsx +32 -16
  31. package/app-template/src/components/pagination.tsx +1 -0
  32. package/app-template/src/components/price.tsx +1 -1
  33. package/app-template/src/components/select.tsx +38 -26
  34. package/app-template/src/components/types/index.ts +25 -1
  35. package/app-template/src/hooks/index.ts +2 -0
  36. package/app-template/src/hooks/use-product-cart.ts +77 -0
  37. package/app-template/src/hooks/use-stock-alert.ts +74 -0
  38. package/app-template/src/plugins.js +3 -1
  39. package/app-template/src/settings.js +8 -2
  40. package/app-template/src/types/index.ts +17 -0
  41. package/app-template/src/utils/variant-validation.ts +41 -0
  42. package/app-template/src/views/account/address-form.tsx +8 -4
  43. package/app-template/src/views/account/contact-form.tsx +1 -1
  44. package/app-template/src/views/account/content-header.tsx +2 -2
  45. package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
  46. package/app-template/src/views/basket/basket-content.tsx +106 -0
  47. package/app-template/src/views/basket/basket-item.tsx +22 -14
  48. package/app-template/src/views/basket/summary.tsx +10 -7
  49. package/app-template/src/views/breadcrumb.tsx +2 -2
  50. package/app-template/src/views/category/category-info.tsx +1 -0
  51. package/app-template/src/views/category/filters/index.tsx +1 -1
  52. package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
  53. package/app-template/src/views/checkout/summary.tsx +10 -0
  54. package/app-template/src/views/guest-login/index.tsx +6 -1
  55. package/app-template/src/views/header/action-menu.tsx +1 -1
  56. package/app-template/src/views/header/search/index.tsx +17 -5
  57. package/app-template/src/views/product/product-actions.tsx +165 -0
  58. package/app-template/src/views/product/product-info.tsx +62 -263
  59. package/app-template/src/views/product/product-share.tsx +56 -0
  60. package/app-template/src/views/product/product-variants.tsx +26 -0
  61. package/app-template/src/views/product/slider.tsx +86 -73
  62. package/app-template/src/widgets/footer-menu.tsx +6 -2
  63. package/commands/plugins.ts +63 -16
  64. package/dist/commands/plugins.js +57 -16
  65. package/package.json +1 -1
@@ -1,7 +1 @@
1
- import { NextResponse } from 'next/server';
2
-
3
- export async function POST() {
4
- // TODO: Handle Form Data
5
-
6
- return NextResponse.json({ message: 'ok' });
7
- }
1
+ export * from '@akinon/next/api/form';
@@ -0,0 +1 @@
1
+ export * from '@akinon/next/api/image-proxy';
@@ -0,0 +1 @@
1
+ export * from '@akinon/next/api/similar-product-list';
@@ -0,0 +1 @@
1
+ export * from '@akinon/next/api/similar-products';
@@ -152,3 +152,6 @@ url("./pz-icon.svg?db4ba799c4ca72f4bb855458e0cd19ee#pz-icon") format("svg");
152
152
  .pz-icon-whatsapp:before {
153
153
  content: "\f12c";
154
154
  }
155
+ .pz-icon-camera:before {
156
+ content: "\f12d";
157
+ }
@@ -30,6 +30,8 @@ describe('Link Component', () => {
30
30
  wrapper = container;
31
31
  });
32
32
 
33
+ wrapper;
34
+
33
35
  it('should render without error', () => {
34
36
  const link = screen.getByRole('link');
35
37
 
@@ -1,33 +1,25 @@
1
1
  'use client';
2
2
 
3
- import { ReactNode, useState } from 'react';
3
+ import { useState } from 'react';
4
4
  import { Icon } from './icon';
5
5
  import { twMerge } from 'tailwind-merge';
6
-
7
- type AccordionProps = {
8
- isCollapse?: boolean;
9
- title?: string;
10
- subTitle?: string;
11
- icons?: string[];
12
- iconSize?: number;
13
- iconColor?: string;
14
- children?: ReactNode;
15
- className?: string;
16
- titleClassName?: string;
17
- dataTestId?: string;
18
- };
6
+ import { AccordionProps } from './types';
19
7
 
20
8
  export const Accordion = ({
21
9
  isCollapse = false,
10
+ collapseClassName,
22
11
  title,
23
12
  subTitle,
24
13
  icons = ['chevron-up', 'chevron-down'],
25
14
  iconSize = 16,
26
15
  iconColor = 'fill-[#000000]',
27
16
  children,
17
+ headerClassName,
28
18
  className,
29
19
  titleClassName,
30
- dataTestId
20
+ subTitleClassName,
21
+ dataTestId,
22
+ contentClassName
31
23
  }: AccordionProps) => {
32
24
  const [collapse, setCollapse] = useState(isCollapse);
33
25
 
@@ -39,15 +31,22 @@ export const Accordion = ({
39
31
  )}
40
32
  >
41
33
  <div
42
- className="flex items-center justify-between cursor-pointer"
34
+ className={twMerge(
35
+ 'flex items-center justify-between cursor-pointer',
36
+ headerClassName
37
+ )}
43
38
  onClick={() => setCollapse(!collapse)}
44
39
  data-testid={dataTestId}
45
40
  >
46
- <div className="flex flex-col">
41
+ <div className={twMerge('flex flex-col', contentClassName)}>
47
42
  {title && (
48
43
  <h3 className={twMerge('text-sm', titleClassName)}>{title}</h3>
49
44
  )}
50
- {subTitle && <h4 className="text-xs text-gray-700">{subTitle}</h4>}
45
+ {subTitle && (
46
+ <h4 className={twMerge('text-xs text-gray-700', subTitleClassName)}>
47
+ {subTitle}
48
+ </h4>
49
+ )}
51
50
  </div>
52
51
 
53
52
  {icons && (
@@ -58,7 +57,11 @@ export const Accordion = ({
58
57
  />
59
58
  )}
60
59
  </div>
61
- {collapse && <div className="mt-3 text-sm">{children}</div>}
60
+ {collapse && (
61
+ <div className={twMerge('mt-3 text-sm', collapseClassName)}>
62
+ {children}
63
+ </div>
64
+ )}
62
65
  </div>
63
66
  );
64
67
  };
@@ -70,6 +70,7 @@ export const CurrencySelect = (props: CurrencySelectProps) => {
70
70
  onClick={confirmModalHandleClick}
71
71
  appearance="filled"
72
72
  className="font-medium px-10 py-4 h-12"
73
+ data-testid="currency-modal-confirm"
73
74
  >
74
75
  {t('common.currency_modal.continue')}
75
76
  </Button>
@@ -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
 
@@ -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
  )}
@@ -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,
@@ -72,7 +78,8 @@ export const Input = forwardRef<
72
78
  hasFloatingLabel,
73
79
  'mb-2': !hasFloatingLabel,
74
80
  '-translate-y-2 bg-white inline-flex h-auto':
75
- hasFloatingLabel && (focused || hasValue)
81
+ hasFloatingLabel &&
82
+ (focused || hasValue || props.value || props.format)
76
83
  })
77
84
  )}
78
85
  >
@@ -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 bg-opacity-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
@@ -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,32 +36,40 @@ 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-none',
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
74
  <span className="mt-1 text-sm text-error">{error.message}</span>
63
75
  )}
@@ -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
+ };
@@ -1 +1,3 @@
1
+ export * from './use-contract';
2
+ export * from './use-fav-button';
1
3
  export * from './use-add-product-to-basket';
@@ -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
+ };