@akinon/next 2.0.0-beta.2 → 2.0.0-beta.21

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 (133) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +400 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +381 -60
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-install-plugins.js +1 -1
  19. package/bin/pz-prebuild.js +1 -0
  20. package/bin/pz-predev.js +1 -0
  21. package/bin/pz-run-tests.js +99 -0
  22. package/bin/run-prebuild-tests.js +46 -0
  23. package/components/accordion.tsx +20 -5
  24. package/components/button.tsx +51 -36
  25. package/components/client-root.tsx +138 -2
  26. package/components/file-input.tsx +65 -3
  27. package/components/index.ts +1 -0
  28. package/components/input.tsx +1 -1
  29. package/components/link.tsx +46 -16
  30. package/components/logger-popup.tsx +213 -0
  31. package/components/modal.tsx +32 -16
  32. package/components/plugin-module.tsx +62 -3
  33. package/components/price.tsx +2 -2
  34. package/components/select.tsx +1 -1
  35. package/components/selected-payment-option-view.tsx +21 -0
  36. package/data/client/account.ts +17 -2
  37. package/data/client/api.ts +2 -0
  38. package/data/client/basket.ts +66 -5
  39. package/data/client/checkout.ts +391 -99
  40. package/data/client/misc.ts +38 -2
  41. package/data/client/product.ts +19 -2
  42. package/data/client/user.ts +16 -8
  43. package/data/server/category.ts +11 -9
  44. package/data/server/flatpage.ts +11 -4
  45. package/data/server/form.ts +15 -4
  46. package/data/server/landingpage.ts +11 -4
  47. package/data/server/list.ts +5 -4
  48. package/data/server/menu.ts +11 -3
  49. package/data/server/product.ts +111 -55
  50. package/data/server/seo.ts +14 -4
  51. package/data/server/special-page.ts +5 -4
  52. package/data/server/widget.ts +90 -5
  53. package/data/urls.ts +16 -5
  54. package/hocs/client/with-segment-defaults.tsx +2 -2
  55. package/hocs/server/with-segment-defaults.tsx +65 -20
  56. package/hooks/index.ts +4 -0
  57. package/hooks/use-localization.ts +24 -10
  58. package/hooks/use-logger-context.tsx +114 -0
  59. package/hooks/use-logger.ts +92 -0
  60. package/hooks/use-loyalty-availability.ts +21 -0
  61. package/hooks/use-payment-options.ts +2 -1
  62. package/hooks/use-pz-params.ts +37 -0
  63. package/hooks/use-router.ts +51 -14
  64. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  65. package/instrumentation/index.ts +10 -1
  66. package/instrumentation/node.ts +2 -20
  67. package/jest.config.js +25 -0
  68. package/lib/cache-handler.mjs +534 -16
  69. package/lib/cache.ts +272 -37
  70. package/localization/index.ts +2 -1
  71. package/localization/provider.tsx +2 -5
  72. package/middlewares/bfcache-headers.ts +18 -0
  73. package/middlewares/checkout-provider.ts +1 -1
  74. package/middlewares/complete-gpay.ts +32 -26
  75. package/middlewares/complete-masterpass.ts +33 -26
  76. package/middlewares/complete-wallet.ts +182 -0
  77. package/middlewares/default.ts +360 -215
  78. package/middlewares/index.ts +10 -2
  79. package/middlewares/locale.ts +34 -11
  80. package/middlewares/masterpass-rest-callback.ts +230 -0
  81. package/middlewares/oauth-login.ts +200 -57
  82. package/middlewares/pretty-url.ts +21 -8
  83. package/middlewares/redirection-payment.ts +32 -26
  84. package/middlewares/saved-card-redirection.ts +33 -26
  85. package/middlewares/three-d-redirection.ts +32 -26
  86. package/middlewares/url-redirection.ts +11 -1
  87. package/middlewares/wallet-complete-redirection.ts +206 -0
  88. package/package.json +24 -9
  89. package/plugins.d.ts +19 -4
  90. package/plugins.js +10 -1
  91. package/redux/actions.ts +47 -0
  92. package/redux/middlewares/checkout.ts +63 -138
  93. package/redux/middlewares/index.ts +14 -10
  94. package/redux/middlewares/pre-order/address.ts +7 -2
  95. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  96. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  97. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  98. package/redux/middlewares/pre-order/index.ts +16 -10
  99. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  100. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  101. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  102. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  103. package/redux/middlewares/pre-order/redirection.ts +8 -2
  104. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  105. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  106. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  107. package/redux/reducers/checkout.ts +23 -3
  108. package/redux/reducers/index.ts +11 -3
  109. package/redux/reducers/root.ts +7 -2
  110. package/redux/reducers/widget.ts +80 -0
  111. package/sentry/index.ts +69 -13
  112. package/tailwind/content.js +16 -0
  113. package/types/commerce/account.ts +5 -1
  114. package/types/commerce/checkout.ts +35 -1
  115. package/types/commerce/widget.ts +33 -0
  116. package/types/index.ts +114 -6
  117. package/types/next-auth.d.ts +2 -2
  118. package/types/widget.ts +80 -0
  119. package/utils/app-fetch.ts +7 -2
  120. package/utils/generate-commerce-search-params.ts +3 -2
  121. package/utils/get-checkout-path.ts +3 -0
  122. package/utils/get-root-hostname.ts +28 -0
  123. package/utils/index.ts +64 -10
  124. package/utils/localization.ts +4 -0
  125. package/utils/mobile-3d-iframe.ts +8 -2
  126. package/utils/override-middleware.ts +7 -12
  127. package/utils/pz-segments.ts +92 -0
  128. package/utils/redirect-ignore.ts +35 -0
  129. package/utils/redirect.ts +9 -3
  130. package/utils/redirection-iframe.ts +8 -2
  131. package/utils/widget-styles.ts +107 -0
  132. package/views/error-page.tsx +93 -0
  133. package/with-pz-config.js +21 -7
@@ -0,0 +1,46 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ function runPrebuildTests() {
5
+ const workspaceRoot = process.cwd()
6
+ const configPath = path.join(workspaceRoot, 'config/prebuild-tests.json')
7
+
8
+ try {
9
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
10
+
11
+ if (config.tests && Array.isArray(config.tests)) {
12
+ for (const testScript of config.tests) {
13
+ console.log(`🧪 Running test script: ${testScript}`)
14
+ const result = require('child_process').spawnSync('yarn', [testScript], {
15
+ stdio: 'inherit',
16
+ shell: true
17
+ })
18
+
19
+ if (result.status !== 0) {
20
+ console.error(`❌ Test script '${testScript}' failed`)
21
+ process.exit(1)
22
+ }
23
+ }
24
+ console.log('✅ All prebuild tests passed successfully!')
25
+ }
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ console.log('🧪 Running default test: test:middleware')
29
+ const result = require('child_process').spawnSync('yarn', ['test:middleware'], {
30
+ stdio: 'inherit',
31
+ shell: true
32
+ })
33
+
34
+ if (result.status !== 0) {
35
+ console.error('❌ Middleware test failed')
36
+ process.exit(1)
37
+ }
38
+ console.log('✅ Default test passed successfully!')
39
+ } else {
40
+ console.error('❌ Error reading test configuration:', error)
41
+ process.exit(1)
42
+ }
43
+ }
44
+ }
45
+
46
+ module.exports = runPrebuildTests
@@ -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,46 +1,61 @@
1
1
  'use client';
2
2
 
3
- import { ButtonProps } from '../types/index';
3
+ import { ButtonProps } from '../types';
4
4
  import clsx from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
6
+ import { Link } from './link';
6
7
 
7
8
  export const Button = (props: ButtonProps) => {
8
- return (
9
- <button
10
- {...props}
11
- className={twMerge(
12
- clsx(
13
- [
14
- 'px-4',
15
- 'h-10',
16
- 'text-xs',
17
- 'bg-primary',
18
- 'text-primary-foreground',
19
- 'border',
20
- 'border-primary',
21
- 'transition-all',
22
- 'hover:bg-white',
23
- 'hover:border-primary',
24
- 'hover:text-primary'
25
- ],
26
- props.appearance === 'outlined' && [
27
- 'bg-transparent ',
28
- 'text-primary ',
29
- 'hover:bg-primary ',
30
- 'hover:text-primary-foreground'
31
- ],
32
- props.appearance === 'ghost' && [
33
- 'bg-transparent',
34
- 'border-transparent',
35
- 'text-primary',
36
- 'hover:bg-primary',
37
- 'hover:text-primary-foreground'
38
- ]
39
- ),
40
- props.className
41
- )}
9
+ const {
10
+ appearance = 'filled',
11
+ size = 'md',
12
+ href,
13
+ target,
14
+ children,
15
+ className,
16
+ ...rest
17
+ } = props;
18
+
19
+ const variants = {
20
+ filled:
21
+ 'bg-primary text-primary-foreground border border-primary hover:bg-white hover:border-primary hover:text-primary',
22
+ outlined:
23
+ 'bg-transparent text-primary hover:bg-primary hover:text-primary-foreground',
24
+ ghost:
25
+ 'bg-transparent border-transparent text-primary hover:bg-primary hover:text-primary-foreground',
26
+ link: 'px-0 h-auto underline underline-offset-2'
27
+ };
28
+
29
+ const sizes = {
30
+ sm: 'h-8',
31
+ md: 'h-10',
32
+ lg: 'h-12',
33
+ xl: 'h-14'
34
+ };
35
+
36
+ const buttonClasses = twMerge(
37
+ clsx(
38
+ 'px-4 text-xs transition-all duration-200',
39
+ 'inline-flex gap-2 justify-center items-center',
40
+ variants[appearance],
41
+ sizes[size],
42
+ className
43
+ ),
44
+ className
45
+ );
46
+
47
+ return props.href ? (
48
+ <Link
49
+ prefetch={false}
50
+ target={target}
51
+ href={href}
52
+ className={buttonClasses}
42
53
  >
43
- {props.children}
54
+ {children}
55
+ </Link>
56
+ ) : (
57
+ <button {...rest} className={buttonClasses}>
58
+ {children}
44
59
  </button>
45
60
  );
46
61
  };
@@ -1,6 +1,24 @@
1
1
  'use client';
2
2
 
3
+ import React from 'react';
4
+ import { useCallback, useEffect } from 'react';
3
5
  import { useMobileIframeHandler } from '../hooks';
6
+ import { useAppDispatch, useAppSelector } from '../redux/hooks';
7
+ import {
8
+ setComponents,
9
+ setDataSources,
10
+ setDesignMode,
11
+ setDraggingActive,
12
+ setPlaceholders,
13
+ setResponsive,
14
+ setSelectedComponentId,
15
+ setSelectedPlaceholder,
16
+ setSelectedWidget
17
+ } from '../redux/reducers/widget';
18
+ import { LoggerPopup } from './logger-popup';
19
+ import { LoggerProvider } from '../hooks/use-logger-context';
20
+ import * as Sentry from '@sentry/nextjs';
21
+ import { initSentry } from '../sentry';
4
22
 
5
23
  export default function ClientRoot({
6
24
  children,
@@ -9,11 +27,129 @@ export default function ClientRoot({
9
27
  children: React.ReactNode;
10
28
  sessionId?: string;
11
29
  }) {
12
- const { preventPageRender } = useMobileIframeHandler({ sessionId });
30
+ const { preventPageRender } = useMobileIframeHandler({
31
+ sessionId: sessionId || ''
32
+ });
33
+ const { components } = useAppSelector((state) => state.widget);
34
+ const dispatch = useAppDispatch();
35
+
36
+ const postMessage = (message: {
37
+ type: string;
38
+ data: Record<string, unknown>;
39
+ }) => {
40
+ window.parent.postMessage(message, '*');
41
+ };
42
+
43
+ const handleWidgetMessage = useCallback(
44
+ (event: MessageEvent) => {
45
+ if (event.data.type === 'UPDATE_COMPONENTS') {
46
+ const { components } = event.data.data;
47
+
48
+ dispatch(setComponents(components));
49
+ } else if (event.data.type === 'UPDATE_WIDGET_SYSTEM') {
50
+ const {
51
+ responsive,
52
+ designMode,
53
+ selectedComponentId,
54
+ placeholders,
55
+ dataSources
56
+ } = event.data.data;
57
+
58
+ dispatch(setResponsive(responsive));
59
+ dispatch(setDesignMode(designMode));
60
+ dispatch(setSelectedComponentId(selectedComponentId));
61
+ dispatch(setPlaceholders(placeholders));
62
+ dispatch(setDataSources(dataSources));
63
+ } else if (event.data.type === 'SELECT_PLACEHOLDER') {
64
+ const { placeholderSlug } = event.data.data;
65
+
66
+ dispatch(setSelectedPlaceholder(placeholderSlug));
67
+ } else if (event.data.type === 'SELECT_WIDGET') {
68
+ const { widgetSlug } = event.data.data;
69
+
70
+ dispatch(setSelectedWidget(widgetSlug));
71
+
72
+ postMessage({
73
+ type: 'SELECT_COMPONENT',
74
+ data: {
75
+ componentId: null
76
+ }
77
+ });
78
+ } else if (event.data.type === 'DRAG_START') {
79
+ dispatch(setDraggingActive(true));
80
+ } else if (event.data.type === 'DRAG_END') {
81
+ dispatch(setDraggingActive(false));
82
+ } else if (event.data.type === 'GET_DROP_TARGET') {
83
+ const { x, y, component } = event.data.data;
84
+ const elements = document.elementsFromPoint(x, y);
85
+ const dropTargetId = elements
86
+ .find((element) => element.hasAttribute('data-component'))
87
+ ?.getAttribute('data-id');
88
+
89
+ postMessage({
90
+ type: 'DROP_TARGET_RESPONSE',
91
+ data: {
92
+ targetId: dropTargetId,
93
+ component
94
+ }
95
+ });
96
+ } else if (event.data.type === 'SET_COOKIE') {
97
+ const { key, value } = event.data.data.value;
98
+
99
+ if (key && value) {
100
+ let cookieString = '';
101
+ if (window.parent !== window) {
102
+ cookieString = `${key}=${value}; path=/; SameSite=None; Secure`;
103
+ } else if (key === 'widget_builder') {
104
+ cookieString = `${key}=false; path=/; SameSite=None; Secure`;
105
+ }
106
+
107
+ if (cookieString) {
108
+ document.cookie = cookieString;
109
+ }
110
+ }
111
+ }
112
+ },
113
+ [dispatch, components]
114
+ );
115
+
116
+ useEffect(() => {
117
+ window.addEventListener('message', handleWidgetMessage);
118
+
119
+ if (window.parent === window) {
120
+ document.cookie = 'widget_builder=false; path=/; SameSite=None; Secure';
121
+ }
122
+
123
+ return () => {
124
+ window.removeEventListener('message', handleWidgetMessage);
125
+ };
126
+ }, [handleWidgetMessage]);
127
+
128
+ const initializeSentry = async () => {
129
+ const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
130
+ const data = await response.json();
131
+
132
+ const options = {
133
+ dsn: data.dsn
134
+ };
135
+
136
+ initSentry('Client', options);
137
+ };
138
+
139
+ useEffect(() => {
140
+ if (!Sentry.isInitialized()) {
141
+ initializeSentry();
142
+ }
143
+ }, []);
13
144
 
14
145
  if (preventPageRender) {
15
146
  return null;
16
147
  }
17
148
 
18
- return <>{children}</>;
149
+ return (
150
+ <LoggerProvider>
151
+ {children}
152
+ <LoggerPopup />
153
+ </LoggerProvider>
154
+ );
19
155
  }
@@ -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
  );
@@ -21,3 +21,4 @@ export * from './link';
21
21
  export * from './pagination';
22
22
  export * from './live-commerce';
23
23
  export * from './file-input';
24
+ export * from './logger-popup';
@@ -112,7 +112,7 @@ export const Input = forwardRef<
112
112
  )}
113
113
  </div>
114
114
  {error && (
115
- <span className="mt-1 text-sm text-error">{error.message}</span>
115
+ <span className="mt-1 text-sm text-error">{String(error.message)}</span>
116
116
  )}
117
117
  </div>
118
118
  );
@@ -19,26 +19,56 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
19
19
  return '#';
20
20
  }
21
21
 
22
- if (
23
- typeof href !== 'string' ||
24
- urlSchemes.some((scheme) => href.startsWith(scheme))
25
- ) {
26
- return href;
27
- }
22
+ if (typeof href === 'string') {
23
+ const trimmedHref = href.trim();
24
+ if (!trimmedHref) {
25
+ return '#';
26
+ }
27
+
28
+ if (urlSchemes.some((scheme) => trimmedHref.startsWith(scheme))) {
29
+ if (
30
+ trimmedHref.startsWith('mailto:') ||
31
+ trimmedHref.startsWith('tel:')
32
+ ) {
33
+ return trimmedHref;
34
+ }
35
+
36
+ try {
37
+ new URL(trimmedHref);
38
+ return trimmedHref;
39
+ } catch {
40
+ return '#';
41
+ }
42
+ }
28
43
 
29
- const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
30
- const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
44
+ try {
45
+ new URL(trimmedHref, 'http://localhost');
46
+ } catch {
47
+ return '#';
48
+ }
31
49
 
32
- if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
33
- return hrefWithLocale;
34
- } else if (
35
- localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
36
- locale !== defaultLocaleValue
37
- ) {
38
- return hrefWithLocale;
50
+ const pathnameWithoutLocale = trimmedHref.replace(
51
+ urlLocaleMatcherRegex,
52
+ ''
53
+ );
54
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
55
+
56
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
57
+ return hrefWithLocale;
58
+ } else if (
59
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
60
+ locale !== defaultLocaleValue
61
+ ) {
62
+ return hrefWithLocale;
63
+ }
64
+
65
+ return trimmedHref || '#';
39
66
  }
40
67
 
41
- return href || '#';
68
+ if (typeof href !== 'string') {
69
+ return href;
70
+ }
71
+ return '#';
42
72
  }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
43
73
 
44
74
  return (