@akinon/next 1.101.0 → 1.102.0-rc.77

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 (43) hide show
  1. package/CHANGELOG.md +1313 -35
  2. package/__tests__/next-config.test.ts +1 -10
  3. package/__tests__/redirect.test.ts +319 -0
  4. package/api/form.ts +84 -0
  5. package/api/image-proxy.ts +75 -0
  6. package/api/similar-product-list.ts +84 -0
  7. package/api/similar-products.ts +120 -0
  8. package/bin/pz-prebuild.js +1 -0
  9. package/components/accordion.tsx +20 -5
  10. package/components/file-input.tsx +65 -3
  11. package/components/input.tsx +2 -0
  12. package/components/link.tsx +16 -12
  13. package/components/modal.tsx +32 -16
  14. package/components/plugin-module.tsx +32 -4
  15. package/data/client/checkout.ts +4 -2
  16. package/data/server/basket.ts +72 -0
  17. package/data/server/category.ts +44 -24
  18. package/data/server/flatpage.ts +16 -12
  19. package/data/server/landingpage.ts +16 -12
  20. package/data/server/list.ts +23 -13
  21. package/data/server/product.ts +66 -39
  22. package/data/server/special-page.ts +16 -12
  23. package/data/urls.ts +6 -2
  24. package/hocs/server/with-segment-defaults.tsx +5 -2
  25. package/hooks/use-localization.ts +2 -3
  26. package/middlewares/complete-gpay.ts +2 -1
  27. package/middlewares/complete-masterpass.ts +2 -1
  28. package/middlewares/default.ts +50 -13
  29. package/middlewares/locale.ts +9 -1
  30. package/middlewares/redirection-payment.ts +2 -1
  31. package/middlewares/saved-card-redirection.ts +2 -1
  32. package/middlewares/three-d-redirection.ts +2 -2
  33. package/middlewares/url-redirection.ts +8 -14
  34. package/package.json +2 -2
  35. package/plugins.d.ts +8 -5
  36. package/plugins.js +3 -1
  37. package/redux/middlewares/checkout.ts +6 -2
  38. package/types/commerce/order.ts +1 -0
  39. package/types/index.ts +34 -1
  40. package/utils/app-fetch.ts +7 -2
  41. package/utils/redirect-ignore.ts +35 -0
  42. package/utils/redirect.ts +22 -3
  43. package/with-pz-config.js +1 -5
@@ -7,15 +7,19 @@ import { AccordionProps } from '../types';
7
7
 
8
8
  export const Accordion = ({
9
9
  isCollapse = false,
10
+ collapseClassName,
10
11
  title,
11
12
  subTitle,
12
13
  icons = ['chevron-up', 'chevron-down'],
13
14
  iconSize = 16,
14
15
  iconColor = 'fill-[#000000]',
15
16
  children,
17
+ headerClassName,
16
18
  className,
17
19
  titleClassName,
18
- dataTestId
20
+ subTitleClassName,
21
+ dataTestId,
22
+ contentClassName
19
23
  }: AccordionProps) => {
20
24
  const [collapse, setCollapse] = useState(isCollapse);
21
25
 
@@ -27,15 +31,22 @@ export const Accordion = ({
27
31
  )}
28
32
  >
29
33
  <div
30
- className="flex items-center justify-between cursor-pointer"
34
+ className={twMerge(
35
+ 'flex items-center justify-between cursor-pointer',
36
+ headerClassName
37
+ )}
31
38
  onClick={() => setCollapse(!collapse)}
32
39
  data-testid={dataTestId}
33
40
  >
34
- <div className="flex flex-col">
41
+ <div className={twMerge('flex flex-col', contentClassName)}>
35
42
  {title && (
36
43
  <h3 className={twMerge('text-sm', titleClassName)}>{title}</h3>
37
44
  )}
38
- {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
+ )}
39
50
  </div>
40
51
 
41
52
  {icons && (
@@ -46,7 +57,11 @@ export const Accordion = ({
46
57
  />
47
58
  )}
48
59
  </div>
49
- {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
+ )}
50
65
  </div>
51
66
  );
52
67
  };
@@ -1,8 +1,70 @@
1
+ import { useState } from 'react';
1
2
  import { forwardRef } from 'react';
2
- import { FileInputProps } from '../types/index';
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { FileInputProps } from '../types';
3
6
 
4
7
  export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
5
- function fileInput(props, ref) {
6
- return <input type="file" {...props} ref={ref} />;
8
+ function FileInput(
9
+ {
10
+ buttonClassName,
11
+ onChange,
12
+ fileClassName,
13
+ fileNameWrapperClassName,
14
+ fileInputClassName,
15
+ ...props
16
+ },
17
+ ref
18
+ ) {
19
+ const { t } = useLocalization();
20
+ const [fileNames, setFileNames] = useState<string[]>([]);
21
+
22
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
23
+ const files = Array.from(event.target.files || []);
24
+ setFileNames(files.map((file) => file.name));
25
+
26
+ if (onChange) {
27
+ onChange(event);
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="relative">
33
+ <input
34
+ type="file"
35
+ {...props}
36
+ ref={ref}
37
+ className={twMerge(
38
+ 'absolute inset-0 w-full h-full opacity-0 cursor-pointer',
39
+ fileInputClassName
40
+ )}
41
+ onChange={handleFileChange}
42
+ />
43
+ <button
44
+ type="button"
45
+ className={twMerge(
46
+ 'bg-primary text-white py-2 px-4 text-sm',
47
+ buttonClassName
48
+ )}
49
+ >
50
+ {t('common.file_input.select_file')}
51
+ </button>
52
+ <div
53
+ className={twMerge('mt-1 text-gray-500', fileNameWrapperClassName)}
54
+ >
55
+ {fileNames.length > 0 ? (
56
+ <ul className={twMerge('list-disc pl-4 text-xs', fileClassName)}>
57
+ {fileNames.map((name, index) => (
58
+ <li key={index}>{name}</li>
59
+ ))}
60
+ </ul>
61
+ ) : (
62
+ <span className={twMerge('text-xs', fileClassName)}>
63
+ {t('common.file_input.no_file')}
64
+ </span>
65
+ )}
66
+ </div>
67
+ </div>
68
+ );
7
69
  }
8
70
  );
@@ -1,6 +1,8 @@
1
1
  import clsx from 'clsx';
2
2
  import { forwardRef, FocusEvent, useState, Ref } from 'react';
3
3
  import { Controller } from 'react-hook-form';
4
+
5
+ // @ts-ignore
4
6
  import { PatternFormat, PatternFormatProps } from 'react-number-format';
5
7
  import { InputProps } from '../types';
6
8
  import { twMerge } from 'tailwind-merge';
@@ -10,7 +10,9 @@ type LinkProps = Omit<
10
10
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
11
11
  keyof NextLinkProps
12
12
  > &
13
- NextLinkProps;
13
+ NextLinkProps & {
14
+ href: string;
15
+ };
14
16
 
15
17
  export const Link = ({ children, href, ...rest }: LinkProps) => {
16
18
  const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
@@ -26,19 +28,21 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
26
28
  return href;
27
29
  }
28
30
 
29
- const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
30
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
31
-
32
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
33
- return hrefWithLocale;
34
- } else if (
35
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
36
- locale !== defaultLocaleValue
37
- ) {
38
- return hrefWithLocale;
31
+ if (typeof href === 'string' && !href.startsWith('http')) {
32
+ const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
33
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
34
+
35
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
36
+ return hrefWithLocale;
37
+ } else if (
38
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
39
+ locale !== defaultLocaleValue
40
+ ) {
41
+ return hrefWithLocale;
42
+ }
39
43
  }
40
44
 
41
- return href || '#';
45
+ return href;
42
46
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
43
47
 
44
48
  return (
@@ -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 '../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>
@@ -21,7 +21,9 @@ enum Plugin {
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
23
  SavedCard = 'pz-saved-card',
24
- FlowPayment = 'pz-flow-payment'
24
+ Hepsipay = 'pz-hepsipay',
25
+ FlowPayment = 'pz-flow-payment',
26
+ SimilarProducts = 'pz-similar-products'
25
27
  }
26
28
 
27
29
  export enum Component {
@@ -47,7 +49,16 @@ export enum Component {
47
49
  AkifastCheckoutButton = 'CheckoutButton',
48
50
  MultiBasket = 'MultiBasket',
49
51
  SavedCard = 'SavedCardOption',
50
- FlowPayment = 'FlowPayment'
52
+ Hepsipay = 'Hepsipay',
53
+ FlowPayment = 'FlowPayment',
54
+ SimilarProductsModal = 'SimilarProductsModal',
55
+ SimilarProductsFilterSidebar = 'SimilarProductsFilterSidebar',
56
+ SimilarProductsResultsGrid = 'SimilarProductsResultsGrid',
57
+ SimilarProductsPlugin = 'SimilarProductsPlugin',
58
+ ProductImageSearchFeature = 'ProductImageSearchFeature',
59
+ ImageSearchButton = 'ImageSearchButton',
60
+ HeaderImageSearchFeature = 'HeaderImageSearchFeature',
61
+ IyzicoSavedCard = 'IyzicoSavedCardOption'
51
62
  }
52
63
 
53
64
  const PluginComponents = new Map([
@@ -80,8 +91,21 @@ const PluginComponents = new Map([
80
91
  [Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
81
92
  ],
82
93
  [Plugin.MultiBasket, [Component.MultiBasket]],
83
- [Plugin.SavedCard, [Component.SavedCard]],
84
- [Plugin.FlowPayment, [Component.FlowPayment]]
94
+ [Plugin.SavedCard, [Component.SavedCard, Component.IyzicoSavedCard]],
95
+ [Plugin.Hepsipay, [Component.Hepsipay]],
96
+ [Plugin.FlowPayment, [Component.FlowPayment]],
97
+ [
98
+ Plugin.SimilarProducts,
99
+ [
100
+ Component.SimilarProductsModal,
101
+ Component.SimilarProductsFilterSidebar,
102
+ Component.SimilarProductsResultsGrid,
103
+ Component.SimilarProductsPlugin,
104
+ Component.ProductImageSearchFeature,
105
+ Component.ImageSearchButton,
106
+ Component.HeaderImageSearchFeature
107
+ ]
108
+ ]
85
109
  ]);
86
110
 
87
111
  const getPlugin = (component: Component) => {
@@ -146,8 +170,12 @@ export default function PluginModule({
146
170
  promise = import(`${'@akinon/pz-multi-basket'}`);
147
171
  } else if (plugin === Plugin.SavedCard) {
148
172
  promise = import(`${'@akinon/pz-saved-card'}`);
173
+ } else if (plugin === Plugin.Hepsipay) {
174
+ promise = import(`${'@akinon/pz-hepsipay'}`);
149
175
  } else if (plugin === Plugin.FlowPayment) {
150
176
  promise = import(`${'@akinon/pz-flow-payment'}`);
177
+ } else if (plugin === Plugin.SimilarProducts) {
178
+ promise = import(`${'@akinon/pz-similar-products'}`);
151
179
  }
152
180
  } catch (error) {
153
181
  logger.error(error);
@@ -35,8 +35,10 @@ import {
35
35
 
36
36
  interface CheckoutResponse {
37
37
  pre_order?: PreOrder;
38
- errors: {
39
- non_field_errors: string;
38
+ errors?: {
39
+ non_field_errors?: string;
40
+ sample_products?: string[];
41
+ [key: string]: string | string[] | undefined;
40
42
  };
41
43
  context_list?: CheckoutContext[];
42
44
  template_name?: string;
@@ -0,0 +1,72 @@
1
+ import { Cache, CacheKey } from '../../lib/cache';
2
+ import { basket } from '../../data/urls';
3
+ import { Basket } from '../../types';
4
+ import appFetch from '../../utils/app-fetch';
5
+ import { ServerVariables } from '../../utils/server-variables';
6
+ import logger from '../../utils/log';
7
+
8
+ type GetBasketParams = {
9
+ locale?: string;
10
+ currency?: string;
11
+ namespace?: string;
12
+ };
13
+
14
+ const getBasketDataHandler = ({
15
+ locale,
16
+ currency,
17
+ namespace
18
+ }: GetBasketParams) => {
19
+ return async function () {
20
+ try {
21
+ const url = namespace
22
+ ? basket.getBasketDetail(namespace)
23
+ : basket.getBasket;
24
+
25
+ const basketData = await appFetch<{ basket: Basket }>({
26
+ url,
27
+ locale,
28
+ currency,
29
+ init: {
30
+ headers: {
31
+ Accept: 'application/json',
32
+ 'Content-Type': 'application/json'
33
+ }
34
+ }
35
+ });
36
+
37
+ if (!basketData?.basket) {
38
+ logger.warn('Basket data is undefined', {
39
+ handler: 'getBasketDataHandler',
40
+ namespace
41
+ });
42
+ }
43
+
44
+ return basketData;
45
+ } catch (error) {
46
+ logger.error('Error fetching basket data', {
47
+ handler: 'getBasketDataHandler',
48
+ error,
49
+ namespace
50
+ });
51
+ throw error;
52
+ }
53
+ };
54
+ };
55
+
56
+ export const getBasketData = async ({
57
+ locale = ServerVariables.locale,
58
+ currency = ServerVariables.currency,
59
+ namespace
60
+ }: GetBasketParams = {}) => {
61
+ return Cache.wrap(
62
+ CacheKey.Basket(namespace),
63
+ locale,
64
+ getBasketDataHandler({ locale, currency, namespace }),
65
+ {
66
+ expire: 0,
67
+ cache: false
68
+ }
69
+ );
70
+ };
71
+
72
+
@@ -5,7 +5,6 @@ import { category, product } from '../urls';
5
5
  import { Cache, CacheKey } from '../../lib/cache';
6
6
  import { parse } from 'lossless-json';
7
7
  import logger from '../../utils/log';
8
- import { headers as nHeaders } from 'next/headers';
9
8
  import { ServerVariables } from '../../utils/server-variables';
10
9
 
11
10
  function getCategoryDataHandler(
@@ -18,19 +17,30 @@ function getCategoryDataHandler(
18
17
  return async function () {
19
18
  const params = generateCommerceSearchParams(searchParams);
20
19
 
21
- const rawData = await appFetch<string>({
22
- url: `${category.getCategoryByPk(pk)}${params ? params : ''}`,
23
- locale,
24
- currency,
25
- init: {
26
- headers: {
27
- Accept: 'application/json',
28
- 'Content-Type': 'application/json',
29
- ...(headers ?? {})
30
- }
31
- },
32
- responseType: FetchResponseType.TEXT
33
- });
20
+ let rawData: string;
21
+
22
+ try {
23
+ rawData = await appFetch<string>({
24
+ url: `${category.getCategoryByPk(pk)}${params ? params : ''}`,
25
+ locale,
26
+ currency,
27
+ init: {
28
+ headers: {
29
+ Accept: 'application/json',
30
+ 'Content-Type': 'application/json',
31
+ ...(headers ?? {})
32
+ }
33
+ },
34
+ responseType: FetchResponseType.TEXT
35
+ });
36
+ } catch (error) {
37
+ logger.error('Failed to fetch category data', {
38
+ handler: 'getCategoryDataHandler',
39
+ pk,
40
+ error: error.message
41
+ });
42
+ return null;
43
+ }
34
44
 
35
45
  let data: GetCategoryResponse;
36
46
 
@@ -64,17 +74,27 @@ function getCategoryDataHandler(
64
74
  return { data, breadcrumbData: undefined };
65
75
  }
66
76
 
67
- const breadcrumbData = await appFetch<any>({
68
- url: product.breadcrumbUrl(menuItemModel),
69
- locale,
70
- currency,
71
- init: {
72
- headers: {
73
- Accept: 'application/json',
74
- 'Content-Type': 'application/json'
77
+ let breadcrumbData: { menu?: unknown } = {};
78
+
79
+ try {
80
+ breadcrumbData = await appFetch<{ menu?: unknown }>({
81
+ url: product.breadcrumbUrl(menuItemModel),
82
+ locale,
83
+ currency,
84
+ init: {
85
+ headers: {
86
+ Accept: 'application/json',
87
+ 'Content-Type': 'application/json'
88
+ }
75
89
  }
76
- }
77
- });
90
+ });
91
+ } catch (error) {
92
+ logger.warn('Failed to fetch breadcrumb data', {
93
+ handler: 'getCategoryDataHandler',
94
+ pk,
95
+ error: error.message
96
+ });
97
+ }
78
98
 
79
99
  return { data, breadcrumbData: breadcrumbData?.menu };
80
100
  };
@@ -11,20 +11,24 @@ const getFlatPageDataHandler = (
11
11
  headers?: Record<string, string>
12
12
  ) => {
13
13
  return async function () {
14
- const data = await appFetch<FlatPage>({
15
- url: flatpage.getFlatPageByPk(pk),
16
- locale,
17
- currency,
18
- init: {
19
- headers: {
20
- Accept: 'application/json',
21
- 'Content-Type': 'application/json',
22
- ...(headers ?? {})
14
+ try {
15
+ const data = await appFetch<FlatPage>({
16
+ url: flatpage.getFlatPageByPk(pk),
17
+ locale,
18
+ currency,
19
+ init: {
20
+ headers: {
21
+ Accept: 'application/json',
22
+ 'Content-Type': 'application/json',
23
+ ...(headers ?? {})
24
+ }
23
25
  }
24
- }
25
- });
26
+ });
26
27
 
27
- return data;
28
+ return data;
29
+ } catch (error) {
30
+ return null;
31
+ }
28
32
  };
29
33
  };
30
34
 
@@ -11,20 +11,24 @@ const getLandingPageHandler = (
11
11
  headers?: Record<string, string>
12
12
  ) => {
13
13
  return async function () {
14
- const data = await appFetch<LandingPage>({
15
- url: landingpage.getLandingPageByPk(pk),
16
- locale,
17
- currency,
18
- init: {
19
- headers: {
20
- Accept: 'application/json',
21
- 'Content-Type': 'application/json',
22
- ...(headers ?? {})
14
+ try {
15
+ const data = await appFetch<LandingPage>({
16
+ url: landingpage.getLandingPageByPk(pk),
17
+ locale,
18
+ currency,
19
+ init: {
20
+ headers: {
21
+ Accept: 'application/json',
22
+ 'Content-Type': 'application/json',
23
+ ...(headers ?? {})
24
+ }
23
25
  }
24
- }
25
- });
26
+ });
26
27
 
27
- return data;
28
+ return data;
29
+ } catch (error) {
30
+ return null;
31
+ }
28
32
  };
29
33
  };
30
34
 
@@ -16,19 +16,29 @@ const getListDataHandler = (
16
16
  return async function () {
17
17
  const params = generateCommerceSearchParams(searchParams);
18
18
 
19
- const rawData = await appFetch<string>({
20
- url: `${category.list}${params}`,
21
- locale,
22
- currency,
23
- init: {
24
- headers: {
25
- Accept: 'application/json',
26
- 'Content-Type': 'application/json',
27
- ...(headers ?? {})
28
- }
29
- },
30
- responseType: FetchResponseType.TEXT
31
- });
19
+ let rawData: string;
20
+
21
+ try {
22
+ rawData = await appFetch<string>({
23
+ url: `${category.list}${params}`,
24
+ locale,
25
+ currency,
26
+ init: {
27
+ headers: {
28
+ Accept: 'application/json',
29
+ 'Content-Type': 'application/json',
30
+ ...(headers ?? {})
31
+ }
32
+ },
33
+ responseType: FetchResponseType.TEXT
34
+ });
35
+ } catch (error) {
36
+ logger.error('Failed to fetch list data', {
37
+ handler: 'getListDataHandler',
38
+ error: error.message
39
+ });
40
+ return null;
41
+ }
32
42
 
33
43
  let data: GetCategoryResponse;
34
44