@akinon/next 1.92.0-rc.9 → 1.92.0-snapshot-ZERO-3449-20250618101111

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 (42) hide show
  1. package/CHANGELOG.md +33 -1180
  2. package/api/similar-product-list.ts +63 -0
  3. package/api/similar-products.ts +109 -0
  4. package/components/accordion.tsx +5 -20
  5. package/components/file-input.tsx +3 -65
  6. package/components/input.tsx +0 -2
  7. package/components/link.tsx +12 -16
  8. package/components/modal.tsx +16 -32
  9. package/components/plugin-module.tsx +3 -13
  10. package/components/selected-payment-option-view.tsx +0 -11
  11. package/data/client/similar-products.ts +122 -0
  12. package/data/urls.ts +5 -1
  13. package/hocs/server/with-segment-defaults.tsx +2 -5
  14. package/hooks/index.ts +2 -0
  15. package/hooks/use-image-cropper.ts +160 -0
  16. package/hooks/use-similar-products.ts +720 -0
  17. package/instrumentation/node.ts +13 -15
  18. package/lib/cache.ts +0 -2
  19. package/middlewares/complete-gpay.ts +1 -2
  20. package/middlewares/complete-masterpass.ts +1 -2
  21. package/middlewares/default.ts +184 -196
  22. package/middlewares/index.ts +1 -3
  23. package/middlewares/redirection-payment.ts +1 -2
  24. package/middlewares/saved-card-redirection.ts +1 -2
  25. package/middlewares/three-d-redirection.ts +1 -2
  26. package/middlewares/url-redirection.ts +14 -8
  27. package/package.json +3 -3
  28. package/plugins.d.ts +0 -2
  29. package/plugins.js +1 -3
  30. package/redux/middlewares/checkout.ts +2 -15
  31. package/redux/reducers/checkout.ts +1 -9
  32. package/sentry/index.ts +17 -54
  33. package/types/commerce/order.ts +0 -1
  34. package/types/index.ts +73 -26
  35. package/utils/app-fetch.ts +2 -2
  36. package/utils/image-validation.ts +303 -0
  37. package/utils/redirect.ts +3 -5
  38. package/with-pz-config.js +5 -1
  39. package/data/server/basket.ts +0 -72
  40. package/hooks/use-loyalty-availability.ts +0 -21
  41. package/middlewares/wallet-complete-redirection.ts +0 -179
  42. package/utils/redirect-ignore.ts +0 -35
@@ -0,0 +1,63 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const dynamicFilter = request.headers.get('x-search-dynamic-filter');
8
+ const dynamicExclude = request.headers.get('x-search-dynamic-exclude');
9
+
10
+ if (!dynamicFilter && !dynamicExclude) {
11
+ return NextResponse.json(
12
+ {
13
+ error:
14
+ 'Missing x-search-dynamic-filter or x-search-dynamic-exclude header'
15
+ },
16
+ { status: 400 }
17
+ );
18
+ }
19
+
20
+ if (Settings.commerceUrl === 'default') {
21
+ return NextResponse.json(
22
+ { error: 'Commerce URL is not configured' },
23
+ { status: 500 }
24
+ );
25
+ }
26
+
27
+ const queryString = searchParams.toString();
28
+ const apiUrl = `${Settings.commerceUrl}/list${
29
+ queryString ? `?${queryString}` : ''
30
+ }`;
31
+
32
+ const headers: Record<string, string> = {
33
+ Accept: 'application/json',
34
+ 'Content-Type': 'application/json'
35
+ };
36
+
37
+ if (dynamicFilter) {
38
+ headers['x-search-dynamic-filter'] = dynamicFilter;
39
+ } else if (dynamicExclude) {
40
+ headers['x-search-dynamic-exclude'] = dynamicExclude;
41
+ }
42
+
43
+ const response = await fetch(apiUrl, {
44
+ method: 'GET',
45
+ headers
46
+ });
47
+
48
+ if (!response.ok) {
49
+ return NextResponse.json(
50
+ { error: `API request failed with status: ${response.status}` },
51
+ { status: response.status }
52
+ );
53
+ }
54
+
55
+ const data = await response.json();
56
+ return NextResponse.json(data);
57
+ } catch (error) {
58
+ return NextResponse.json(
59
+ { error: (error as Error).message },
60
+ { status: 500 }
61
+ );
62
+ }
63
+ }
@@ -0,0 +1,109 @@
1
+ import { NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+
4
+ const IMAGE_SEARCH_API_URL = Settings.commerceUrl + '/image-search/';
5
+
6
+ const errorResponse = (message: string, status: number) => {
7
+ return NextResponse.json({ error: message }, { status });
8
+ };
9
+
10
+ export async function GET(request: Request) {
11
+ const { searchParams } = new URL(request.url);
12
+ const limit = searchParams.get('limit') || '20';
13
+ const url = searchParams.get('url');
14
+ const excludedProductIds = searchParams.get('excluded_product_ids');
15
+
16
+ if (!url) {
17
+ return errorResponse('URL parameter is required', 400);
18
+ }
19
+
20
+ if (Settings.commerceUrl === 'default') {
21
+ return errorResponse('Commerce URL is not configured', 500);
22
+ }
23
+
24
+ const apiParams = new URLSearchParams();
25
+ apiParams.append('limit', limit);
26
+ apiParams.append('url', url);
27
+
28
+ if (excludedProductIds) {
29
+ apiParams.append('excluded_product_ids', excludedProductIds);
30
+ }
31
+
32
+ const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
33
+
34
+ try {
35
+ const response = await fetch(apiUrl, {
36
+ method: 'GET',
37
+ headers: {
38
+ Accept: 'application/json'
39
+ }
40
+ });
41
+
42
+ if (!response.ok) {
43
+ return errorResponse(
44
+ `API request failed with status: ${response.status}`,
45
+ response.status
46
+ );
47
+ }
48
+
49
+ const responseText = await response.text();
50
+
51
+ return NextResponse.json(JSON.parse(responseText));
52
+ } catch (error) {
53
+ return errorResponse((error as Error).message, 500);
54
+ }
55
+ }
56
+
57
+ export async function POST(request: Request) {
58
+ const { searchParams } = new URL(request.url);
59
+ const limit = searchParams.get('limit') || '20';
60
+
61
+ if (Settings.commerceUrl === 'default') {
62
+ return errorResponse('Commerce URL is not configured', 500);
63
+ }
64
+
65
+ let requestBody;
66
+ try {
67
+ requestBody = await request.json();
68
+ } catch (error) {
69
+ return errorResponse('Invalid JSON in request body', 400);
70
+ }
71
+
72
+ if (!requestBody.image) {
73
+ return errorResponse('Image data is required in request body', 400);
74
+ }
75
+
76
+ const apiParams = new URLSearchParams();
77
+ apiParams.append('limit', limit);
78
+
79
+ const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
80
+
81
+ const bodyData: any = { image: requestBody.image };
82
+
83
+ if (requestBody.excluded_product_ids) {
84
+ bodyData.excluded_product_ids = requestBody.excluded_product_ids;
85
+ }
86
+
87
+ try {
88
+ const response = await fetch(apiUrl, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ Accept: 'application/json'
93
+ },
94
+ body: JSON.stringify(bodyData)
95
+ });
96
+
97
+ if (!response.ok) {
98
+ return errorResponse(
99
+ `API request failed with status: ${response.status}`,
100
+ response.status
101
+ );
102
+ }
103
+
104
+ const responseData = await response.json();
105
+ return NextResponse.json(responseData);
106
+ } catch (error) {
107
+ return errorResponse((error as Error).message, 500);
108
+ }
109
+ }
@@ -7,19 +7,15 @@ import { AccordionProps } from '../types';
7
7
 
8
8
  export const Accordion = ({
9
9
  isCollapse = false,
10
- collapseClassName,
11
10
  title,
12
11
  subTitle,
13
12
  icons = ['chevron-up', 'chevron-down'],
14
13
  iconSize = 16,
15
14
  iconColor = 'fill-[#000000]',
16
15
  children,
17
- headerClassName,
18
16
  className,
19
17
  titleClassName,
20
- subTitleClassName,
21
- dataTestId,
22
- contentClassName
18
+ dataTestId
23
19
  }: AccordionProps) => {
24
20
  const [collapse, setCollapse] = useState(isCollapse);
25
21
 
@@ -31,22 +27,15 @@ export const Accordion = ({
31
27
  )}
32
28
  >
33
29
  <div
34
- className={twMerge(
35
- 'flex items-center justify-between cursor-pointer',
36
- headerClassName
37
- )}
30
+ className="flex items-center justify-between cursor-pointer"
38
31
  onClick={() => setCollapse(!collapse)}
39
32
  data-testid={dataTestId}
40
33
  >
41
- <div className={twMerge('flex flex-col', contentClassName)}>
34
+ <div className="flex flex-col">
42
35
  {title && (
43
36
  <h3 className={twMerge('text-sm', titleClassName)}>{title}</h3>
44
37
  )}
45
- {subTitle && (
46
- <h4 className={twMerge('text-xs text-gray-700', subTitleClassName)}>
47
- {subTitle}
48
- </h4>
49
- )}
38
+ {subTitle && <h4 className="text-xs text-gray-700">{subTitle}</h4>}
50
39
  </div>
51
40
 
52
41
  {icons && (
@@ -57,11 +46,7 @@ export const Accordion = ({
57
46
  />
58
47
  )}
59
48
  </div>
60
- {collapse && (
61
- <div className={twMerge('mt-3 text-sm', collapseClassName)}>
62
- {children}
63
- </div>
64
- )}
49
+ {collapse && <div className="mt-3 text-sm">{children}</div>}
65
50
  </div>
66
51
  );
67
52
  };
@@ -1,70 +1,8 @@
1
- import { useState } from 'react';
2
1
  import { forwardRef } from 'react';
3
- import { useLocalization } from '@akinon/next/hooks';
4
- import { twMerge } from 'tailwind-merge';
5
- import { FileInputProps } from '../types';
2
+ import { FileInputProps } from '../types/index';
6
3
 
7
4
  export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
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
- );
5
+ function fileInput(props, ref) {
6
+ return <input type="file" {...props} ref={ref} />;
69
7
  }
70
8
  );
@@ -1,8 +1,6 @@
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
6
4
  import { PatternFormat, PatternFormatProps } from 'react-number-format';
7
5
  import { InputProps } from '../types';
8
6
  import { twMerge } from 'tailwind-merge';
@@ -10,9 +10,7 @@ type LinkProps = Omit<
10
10
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
11
11
  keyof NextLinkProps
12
12
  > &
13
- NextLinkProps & {
14
- href: string;
15
- };
13
+ NextLinkProps;
16
14
 
17
15
  export const Link = ({ children, href, ...rest }: LinkProps) => {
18
16
  const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
@@ -28,21 +26,19 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
28
26
  return href;
29
27
  }
30
28
 
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
- }
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;
43
39
  }
44
40
 
45
- return href;
41
+ return href || '#';
46
42
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
47
43
 
48
44
  return (
@@ -4,7 +4,16 @@ 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
- import { ModalProps } from '../types';
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
+ }
8
17
 
9
18
  export const Modal = (props: ModalProps) => {
10
19
  const {
@@ -14,14 +23,7 @@ export const Modal = (props: ModalProps) => {
14
23
  setOpen,
15
24
  title = '',
16
25
  showCloseButton = true,
17
- className,
18
- overlayClassName,
19
- headerWrapperClassName,
20
- titleClassName,
21
- closeButtonClassName,
22
- iconName = 'close',
23
- iconSize = 16,
24
- iconClassName
26
+ className
25
27
  } = props;
26
28
 
27
29
  useEffect(() => {
@@ -36,12 +38,7 @@ export const Modal = (props: ModalProps) => {
36
38
 
37
39
  return (
38
40
  <ReactPortal wrapperId={portalId}>
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
- />
41
+ <div className="fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50" />
45
42
  <section
46
43
  className={twMerge(
47
44
  'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
@@ -49,28 +46,15 @@ export const Modal = (props: ModalProps) => {
49
46
  )}
50
47
  >
51
48
  {(showCloseButton || title) && (
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
- )}
49
+ <div className="flex px-6 py-4 border-b border-gray-400">
50
+ {title && <h3 className="text-lg font-light">{title}</h3>}
63
51
  {showCloseButton && (
64
52
  <button
65
53
  type="button"
66
54
  onClick={() => setOpen(false)}
67
- className={twMerge('ml-auto', closeButtonClassName)}
55
+ className="ml-auto"
68
56
  >
69
- <Icon
70
- name={iconName}
71
- size={iconSize}
72
- className={iconClassName}
73
- />
57
+ <Icon name="close" size={16} />
74
58
  </button>
75
59
  )}
76
60
  </div>
@@ -20,9 +20,7 @@ enum Plugin {
20
20
  B2B = 'pz-b2b',
21
21
  Akifast = 'pz-akifast',
22
22
  MultiBasket = 'pz-multi-basket',
23
- SavedCard = 'pz-saved-card',
24
- Hepsipay = 'pz-hepsipay',
25
- FlowPayment = 'pz-flow-payment'
23
+ SavedCard = 'pz-saved-card'
26
24
  }
27
25
 
28
26
  export enum Component {
@@ -47,9 +45,7 @@ export enum Component {
47
45
  AkifastQuickLoginButton = 'QuickLoginButton',
48
46
  AkifastCheckoutButton = 'CheckoutButton',
49
47
  MultiBasket = 'MultiBasket',
50
- SavedCard = 'SavedCardOption',
51
- Hepsipay = 'Hepsipay',
52
- FlowPayment = 'FlowPayment'
48
+ SavedCard = 'SavedCardOption'
53
49
  }
54
50
 
55
51
  const PluginComponents = new Map([
@@ -82,9 +78,7 @@ const PluginComponents = new Map([
82
78
  [Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
83
79
  ],
84
80
  [Plugin.MultiBasket, [Component.MultiBasket]],
85
- [Plugin.SavedCard, [Component.SavedCard]],
86
- [Plugin.Hepsipay, [Component.Hepsipay]],
87
- [Plugin.FlowPayment, [Component.FlowPayment]]
81
+ [Plugin.SavedCard, [Component.SavedCard]]
88
82
  ]);
89
83
 
90
84
  const getPlugin = (component: Component) => {
@@ -149,10 +143,6 @@ export default function PluginModule({
149
143
  promise = import(`${'@akinon/pz-multi-basket'}`);
150
144
  } else if (plugin === Plugin.SavedCard) {
151
145
  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'}`);
156
146
  }
157
147
  } catch (error) {
158
148
  logger.error(error);
@@ -62,17 +62,6 @@ 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
-
76
65
  const view = paymentTypeToView[payment_option?.payment_type] || null;
77
66
 
78
67
  if (view) {
@@ -0,0 +1,122 @@
1
+ import { api } from './api';
2
+ import { product } from '../urls';
3
+ import {
4
+ Product,
5
+ Facet,
6
+ FacetChoice,
7
+ SortOption,
8
+ Pagination
9
+ } from '@akinon/next/types/commerce';
10
+
11
+ export interface SimilarProductsResponse {
12
+ product_ids?: number[];
13
+ productIds?: number[];
14
+ similar_products?: Array<{ product_id: number }>;
15
+ [key: string]: any;
16
+ }
17
+
18
+ export interface SimilarProductsListResponse {
19
+ pagination: Pagination;
20
+ facets: Facet[];
21
+ sorters: SortOption[];
22
+ search_text: string | null;
23
+ products: Product[];
24
+ [key: string]: any;
25
+ }
26
+
27
+ export const similarProductsApi = api.injectEndpoints({
28
+ endpoints: (build) => ({
29
+ getSimilarProductsByUrl: build.query<
30
+ SimilarProductsResponse,
31
+ { url: string; limit?: number; excluded_product_ids?: number[] }
32
+ >({
33
+ query: ({ url, limit = 20, excluded_product_ids }) => {
34
+ const params = new URLSearchParams();
35
+ params.append('limit', String(limit));
36
+ params.append('url', url);
37
+
38
+ if (excluded_product_ids && excluded_product_ids.length > 0) {
39
+ params.append('excluded_product_ids', excluded_product_ids.join(','));
40
+ }
41
+
42
+ return {
43
+ url: `/api${product.similarProducts(params.toString())}`,
44
+ method: 'GET',
45
+ headers: {
46
+ Accept: 'application/json'
47
+ }
48
+ };
49
+ }
50
+ }),
51
+
52
+ getSimilarProductsByImage: build.mutation<
53
+ SimilarProductsResponse,
54
+ { image: string; limit?: number; excluded_product_ids?: number[] }
55
+ >({
56
+ query: ({ image, limit = 20, excluded_product_ids }) => {
57
+ const params = new URLSearchParams();
58
+ params.append('limit', String(limit));
59
+
60
+ return {
61
+ url: `/api${product.similarProducts(params.toString())}`,
62
+ method: 'POST',
63
+ headers: {
64
+ 'Content-Type': 'application/json',
65
+ Accept: 'application/json'
66
+ },
67
+ body: JSON.stringify({ image, excluded_product_ids })
68
+ };
69
+ }
70
+ }),
71
+
72
+ getSimilarProductsList: build.query<
73
+ SimilarProductsListResponse,
74
+ {
75
+ filter: string;
76
+ searchParams?: Record<string, string>;
77
+ isExclude?: boolean;
78
+ }
79
+ >({
80
+ query: ({ filter, searchParams = {}, isExclude = false }) => {
81
+ const params = new URLSearchParams(searchParams);
82
+ const queryString = params.toString();
83
+
84
+ const headerName = isExclude
85
+ ? 'x-search-dynamic-exclude'
86
+ : 'x-search-dynamic-filter';
87
+
88
+ return {
89
+ url: `/api${product.similarProductsList(queryString)}`,
90
+ headers: {
91
+ [headerName]: filter
92
+ }
93
+ };
94
+ },
95
+ serializeQueryArgs: ({ queryArgs }) => {
96
+ const { filter, searchParams = {}, isExclude = false } = queryArgs;
97
+ const sortedParams = Object.keys(searchParams)
98
+ .sort()
99
+ .reduce((acc, key) => {
100
+ acc[key] = searchParams[key];
101
+ return acc;
102
+ }, {} as Record<string, string>);
103
+
104
+ return JSON.stringify({
105
+ filter,
106
+ searchParams: sortedParams,
107
+ isExclude
108
+ });
109
+ }
110
+ })
111
+ }),
112
+ overrideExisting: true
113
+ });
114
+
115
+ export const {
116
+ useGetSimilarProductsByUrlQuery,
117
+ useGetSimilarProductsByImageMutation,
118
+ useGetSimilarProductsListQuery,
119
+ useLazyGetSimilarProductsListQuery
120
+ } = similarProductsApi;
121
+
122
+ export type { Product, Facet, FacetChoice, SortOption, Pagination };
package/data/urls.ts CHANGED
@@ -182,7 +182,11 @@ export const product = {
182
182
  breadcrumbUrl: (menuitemmodel: string) =>
183
183
  `/menus/generate_breadcrumb/?item=${menuitemmodel}&generator_name=menu_item`,
184
184
  bundleProduct: (productPk: string, queryString: string) =>
185
- `/bundle-product/${productPk}/?${queryString}`
185
+ `/bundle-product/${productPk}/?${queryString}`,
186
+ similarProducts: (params?: string) =>
187
+ `/similar-products${params ? `?${params}` : ''}`,
188
+ similarProductsList: (params?: string) =>
189
+ `/similar-product-list${params ? `?${params}` : ''}`
186
190
  };
187
191
 
188
192
  export const wishlist = {
@@ -72,13 +72,10 @@ const addRootLayoutProps = async (componentProps: RootLayoutProps) => {
72
72
  const checkRedisVariables = () => {
73
73
  const requiredVariableValues = [
74
74
  process.env.CACHE_HOST,
75
- process.env.CACHE_PORT
75
+ process.env.CACHE_PORT,
76
+ process.env.CACHE_SECRET
76
77
  ];
77
78
 
78
- if (!settings.usePrettyUrlRoute) {
79
- requiredVariableValues.push(process.env.CACHE_SECRET);
80
- }
81
-
82
79
  if (
83
80
  !requiredVariableValues.every((v) => v) &&
84
81
  process.env.NODE_ENV === 'production'
package/hooks/index.ts CHANGED
@@ -11,3 +11,5 @@ export * from './use-payment-options';
11
11
  export * from './use-pagination';
12
12
  export * from './use-message-listener';
13
13
  export * from './use-sentry-uncaught-errors';
14
+ export * from './use-similar-products';
15
+ export * from './use-image-cropper';