@akinon/next 1.91.0 → 1.92.0-rc.10

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.
@@ -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>
@@ -20,7 +20,9 @@ enum Plugin {
20
20
  B2B = 'pz-b2b',
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
- SavedCard = 'pz-saved-card'
23
+ SavedCard = 'pz-saved-card',
24
+ Hepsipay = 'pz-hepsipay',
25
+ FlowPayment = 'pz-flow-payment'
24
26
  }
25
27
 
26
28
  export enum Component {
@@ -45,7 +47,9 @@ export enum Component {
45
47
  AkifastQuickLoginButton = 'QuickLoginButton',
46
48
  AkifastCheckoutButton = 'CheckoutButton',
47
49
  MultiBasket = 'MultiBasket',
48
- SavedCard = 'SavedCardOption'
50
+ SavedCard = 'SavedCardOption',
51
+ Hepsipay = 'Hepsipay',
52
+ FlowPayment = 'FlowPayment'
49
53
  }
50
54
 
51
55
  const PluginComponents = new Map([
@@ -78,7 +82,9 @@ const PluginComponents = new Map([
78
82
  [Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
79
83
  ],
80
84
  [Plugin.MultiBasket, [Component.MultiBasket]],
81
- [Plugin.SavedCard, [Component.SavedCard]]
85
+ [Plugin.SavedCard, [Component.SavedCard]],
86
+ [Plugin.Hepsipay, [Component.Hepsipay]],
87
+ [Plugin.FlowPayment, [Component.FlowPayment]]
82
88
  ]);
83
89
 
84
90
  const getPlugin = (component: Component) => {
@@ -143,6 +149,10 @@ export default function PluginModule({
143
149
  promise = import(`${'@akinon/pz-multi-basket'}`);
144
150
  } else if (plugin === Plugin.SavedCard) {
145
151
  promise = import(`${'@akinon/pz-saved-card'}`);
152
+ } else if (plugin === Plugin.Hepsipay) {
153
+ promise = import(`${'@akinon/pz-hepsipay'}`);
154
+ } else if (plugin === Plugin.FlowPayment) {
155
+ promise = import(`${'@akinon/pz-flow-payment'}`);
146
156
  }
147
157
  } catch (error) {
148
158
  logger.error(error);
@@ -62,6 +62,17 @@ export default function SelectedPaymentOptionView({
62
62
  : fallbackView;
63
63
  }
64
64
 
65
+ if (
66
+ payment_option?.payment_type === 'wallet' &&
67
+ wallet_method === 'checkout_flow'
68
+ ) {
69
+ const mod = await import('@akinon/pz-flow-payment');
70
+
71
+ return typeof mod?.default === 'function'
72
+ ? mod.default
73
+ : fallbackView;
74
+ }
75
+
65
76
  const view = paymentTypeToView[payment_option?.payment_type] || null;
66
77
 
67
78
  if (view) {
@@ -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
+
@@ -72,10 +72,13 @@ const addRootLayoutProps = async (componentProps: RootLayoutProps) => {
72
72
  const checkRedisVariables = () => {
73
73
  const requiredVariableValues = [
74
74
  process.env.CACHE_HOST,
75
- process.env.CACHE_PORT,
76
- process.env.CACHE_SECRET
75
+ process.env.CACHE_PORT
77
76
  ];
78
77
 
78
+ if (!settings.usePrettyUrlRoute) {
79
+ requiredVariableValues.push(process.env.CACHE_SECRET);
80
+ }
81
+
79
82
  if (
80
83
  !requiredVariableValues.every((v) => v) &&
81
84
  process.env.NODE_ENV === 'production'
@@ -0,0 +1,21 @@
1
+ import { useAppSelector } from '../redux/hooks';
2
+
3
+ export const useLoyaltyAvailability = () => {
4
+ const { paymentOptions, unavailablePaymentOptions } = useAppSelector(
5
+ (state) => state.checkout
6
+ );
7
+
8
+ const hasLoyaltyInAvailable = paymentOptions.some(
9
+ (option) =>
10
+ option.payment_type === 'loyalty_money' ||
11
+ option.payment_type === 'loyalty'
12
+ );
13
+
14
+ const hasLoyaltyInUnavailable = unavailablePaymentOptions.some(
15
+ (option) =>
16
+ option.payment_type === 'loyalty_money' ||
17
+ option.payment_type === 'loyalty'
18
+ );
19
+
20
+ return hasLoyaltyInAvailable || hasLoyaltyInUnavailable;
21
+ };
@@ -4,17 +4,19 @@ import { Resource } from '@opentelemetry/resources';
4
4
  import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
5
5
  import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
6
6
 
7
- const sdk = new NodeSDK({
8
- resource: new Resource({
9
- [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
10
- }),
11
- spanProcessor: new SimpleSpanProcessor(
12
- new OTLPTraceExporter({
13
- url: `${
14
- process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
15
- }/api/traces`
16
- })
17
- )
18
- });
7
+ if (process.env.NODE_ENV === 'development') {
8
+ const sdk = new NodeSDK({
9
+ resource: new Resource({
10
+ [SemanticResourceAttributes.SERVICE_NAME]: 'pz-next-app'
11
+ }),
12
+ spanProcessor: new SimpleSpanProcessor(
13
+ new OTLPTraceExporter({
14
+ url: `${
15
+ process.env.PZ_DASHBOARD_URL ?? 'http://localhost:3005'
16
+ }/api/traces`
17
+ })
18
+ )
19
+ });
19
20
 
20
- sdk.start();
21
+ sdk.start();
22
+ }
package/lib/cache.ts CHANGED
@@ -31,6 +31,8 @@ export const CacheKey = {
31
31
  `category_${pk}_${encodeURIComponent(
32
32
  JSON.stringify(searchParams)
33
33
  )}${hashCacheKey(headers)}`,
34
+ Basket: (namespace?: string) => `basket${namespace ? `_${namespace}` : ''}`,
35
+ AllBaskets: () => 'all_baskets',
34
36
  CategorySlug: (slug: string) => `category_${slug}`,
35
37
  SpecialPage: (
36
38
  pk: number,
@@ -64,7 +64,7 @@ const withCheckoutProvider =
64
64
  const location = request.headers.get('location');
65
65
  const redirectUrl = new URL(
66
66
  request.headers.get('location'),
67
- location.startsWith('http') ? '' : process.env.NEXT_PUBLIC_URL
67
+ location.startsWith('http') ? '' : url.origin
68
68
  );
69
69
 
70
70
  redirectUrl.pathname = getUrlPathWithLocale(
@@ -145,7 +145,8 @@ const withCompleteGpay =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'complete-gpay',
147
147
  redirectUrlWithLocale,
148
- ip
148
+ ip,
149
+ setCookie: request.headers.get('set-cookie')
149
150
  });
150
151
 
151
152
  // Using POST method while redirecting causes an error,
@@ -145,7 +145,8 @@ const withCompleteMasterpass =
145
145
  logger.info('Redirecting to order success page', {
146
146
  middleware: 'complete-masterpass',
147
147
  redirectUrlWithLocale,
148
- ip
148
+ ip,
149
+ setCookie: request.headers.get('set-cookie')
149
150
  });
150
151
 
151
152
  // Using POST method while redirecting causes an error,