@akinon/next 2.0.0-beta.9 → 2.0.0
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.
- package/CHANGELOG.md +434 -23
- package/__tests__/next-config.test.ts +83 -0
- package/__tests__/tsconfig.json +23 -0
- package/api/auth.ts +367 -63
- package/api/barcode-search.ts +59 -0
- package/api/cache.ts +41 -5
- package/api/client.ts +21 -4
- package/api/form.ts +85 -0
- package/api/image-proxy.ts +75 -0
- package/api/product-categories.ts +53 -0
- package/api/similar-product-list.ts +63 -0
- package/api/similar-products.ts +111 -0
- package/api/virtual-try-on.ts +382 -0
- package/assets/styles/index.scss +84 -0
- package/babel.config.js +6 -0
- package/bin/pz-generate-routes.js +115 -0
- package/bin/pz-install-plugins.js +1 -1
- package/bin/pz-prebuild.js +1 -0
- package/bin/pz-predev.js +1 -0
- package/bin/pz-run-tests.js +99 -0
- package/components/accordion.tsx +21 -6
- package/components/client-root.tsx +119 -3
- package/components/file-input.tsx +65 -3
- package/components/index.ts +1 -0
- package/components/input.tsx +2 -2
- package/components/link.tsx +46 -16
- package/components/logger-popup.tsx +213 -0
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +62 -3
- package/components/price.tsx +2 -2
- package/components/select.tsx +3 -3
- package/components/selected-payment-option-view.tsx +21 -0
- package/data/client/account.ts +17 -2
- package/data/client/basket.ts +39 -0
- package/data/client/checkout.ts +336 -99
- package/data/client/misc.ts +13 -1
- package/data/server/category.ts +11 -9
- package/data/server/flatpage.ts +4 -1
- package/data/server/form.ts +4 -1
- package/data/server/landingpage.ts +4 -1
- package/data/server/list.ts +5 -4
- package/data/server/menu.ts +4 -1
- package/data/server/product.ts +97 -52
- package/data/server/seo.ts +4 -1
- package/data/server/special-page.ts +5 -4
- package/data/server/widget.ts +71 -1
- package/data/urls.ts +6 -3
- package/hocs/client/with-segment-defaults.tsx +2 -2
- package/hocs/server/with-segment-defaults.tsx +81 -20
- package/hooks/index.ts +3 -0
- package/hooks/use-localization.ts +24 -10
- package/hooks/use-logger-context.tsx +114 -0
- package/hooks/use-logger.ts +92 -0
- package/hooks/use-loyalty-availability.ts +21 -0
- package/hooks/use-payment-options.ts +2 -1
- package/hooks/use-pz-params.ts +37 -0
- package/hooks/use-router.ts +53 -19
- package/instrumentation/index.ts +0 -1
- package/instrumentation/node.ts +2 -20
- package/jest.config.js +25 -0
- package/lib/cache-handler.mjs +534 -16
- package/lib/cache.ts +269 -34
- package/localization/provider.tsx +2 -5
- package/middlewares/bfcache-headers.ts +18 -0
- package/middlewares/checkout-provider.ts +1 -1
- package/middlewares/complete-gpay.ts +32 -26
- package/middlewares/complete-masterpass.ts +33 -26
- package/middlewares/complete-wallet.ts +182 -0
- package/middlewares/default.ts +357 -203
- package/middlewares/index.ts +10 -2
- package/middlewares/locale.ts +5 -3
- package/middlewares/masterpass-rest-callback.ts +230 -0
- package/middlewares/oauth-login.ts +200 -57
- package/middlewares/pretty-url.ts +21 -8
- package/middlewares/redirection-payment.ts +32 -26
- package/middlewares/saved-card-redirection.ts +33 -26
- package/middlewares/three-d-redirection.ts +32 -26
- package/middlewares/url-redirection.ts +9 -15
- package/middlewares/wallet-complete-redirection.ts +206 -0
- package/package.json +24 -10
- package/plugins.d.ts +19 -4
- package/plugins.js +9 -1
- package/redux/actions.ts +47 -0
- package/redux/middlewares/checkout.ts +61 -8
- package/redux/middlewares/index.ts +14 -10
- package/redux/middlewares/pre-order/address.ts +1 -1
- package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +1 -1
- package/redux/middlewares/pre-order/data-source-shipping-option.ts +1 -1
- package/redux/middlewares/pre-order/delivery-option.ts +1 -1
- package/redux/middlewares/pre-order/index.ts +3 -1
- package/redux/middlewares/pre-order/installment-option.ts +2 -1
- package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
- package/redux/middlewares/pre-order/payment-option.ts +1 -1
- package/redux/middlewares/pre-order/pre-order-validation.ts +4 -3
- package/redux/middlewares/pre-order/redirection.ts +2 -2
- package/redux/middlewares/pre-order/set-pre-order.ts +2 -2
- package/redux/middlewares/pre-order/shipping-option.ts +1 -1
- package/redux/middlewares/pre-order/shipping-step.ts +1 -1
- package/redux/reducers/checkout.ts +15 -1
- package/redux/reducers/index.ts +7 -1
- package/redux/reducers/widget.ts +80 -0
- package/sentry/index.ts +54 -17
- package/tailwind/content.js +16 -0
- package/types/commerce/checkout.ts +26 -1
- package/types/commerce/widget.ts +33 -0
- package/types/index.ts +114 -5
- package/types/next-auth.d.ts +2 -2
- package/types/widget.ts +80 -0
- package/utils/app-fetch.ts +7 -2
- package/utils/generate-commerce-search-params.ts +3 -2
- package/utils/get-checkout-path.ts +3 -0
- package/utils/get-root-hostname.ts +28 -0
- package/utils/index.ts +69 -18
- package/utils/mobile-3d-iframe.ts +8 -2
- package/utils/override-middleware.ts +1 -0
- package/utils/pz-segments.ts +92 -0
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +9 -3
- package/utils/redirection-iframe.ts +8 -2
- package/utils/widget-styles.ts +107 -0
- package/with-pz-config.js +20 -7
package/components/accordion.tsx
CHANGED
|
@@ -7,35 +7,46 @@ 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
|
-
|
|
20
|
+
subTitleClassName,
|
|
21
|
+
dataTestId,
|
|
22
|
+
contentClassName
|
|
19
23
|
}: AccordionProps) => {
|
|
20
24
|
const [collapse, setCollapse] = useState(isCollapse);
|
|
21
25
|
|
|
22
26
|
return (
|
|
23
27
|
<div
|
|
24
28
|
className={twMerge(
|
|
25
|
-
'flex flex-col justify-center border-b
|
|
29
|
+
'flex flex-col justify-center border-b pb-4 mb-4 last:border-none',
|
|
26
30
|
className
|
|
27
31
|
)}
|
|
28
32
|
>
|
|
29
33
|
<div
|
|
30
|
-
className=
|
|
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=
|
|
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 &&
|
|
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 &&
|
|
60
|
+
{collapse && (
|
|
61
|
+
<div className={twMerge('mt-3 text-sm', collapseClassName)}>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
50
65
|
</div>
|
|
51
66
|
);
|
|
52
67
|
};
|
|
@@ -1,9 +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';
|
|
4
20
|
import * as Sentry from '@sentry/nextjs';
|
|
5
21
|
import { initSentry } from '../sentry';
|
|
6
|
-
import { useEffect } from 'react';
|
|
7
22
|
|
|
8
23
|
export default function ClientRoot({
|
|
9
24
|
children,
|
|
@@ -12,7 +27,103 @@ export default function ClientRoot({
|
|
|
12
27
|
children: React.ReactNode;
|
|
13
28
|
sessionId?: string;
|
|
14
29
|
}) {
|
|
15
|
-
const { preventPageRender } = useMobileIframeHandler({
|
|
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]);
|
|
16
127
|
|
|
17
128
|
const initializeSentry = async () => {
|
|
18
129
|
const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
|
|
@@ -35,5 +146,10 @@ export default function ClientRoot({
|
|
|
35
146
|
return null;
|
|
36
147
|
}
|
|
37
148
|
|
|
38
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
<LoggerProvider>
|
|
151
|
+
{children}
|
|
152
|
+
<LoggerPopup />
|
|
153
|
+
</LoggerProvider>
|
|
154
|
+
);
|
|
39
155
|
}
|
|
@@ -1,8 +1,70 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { forwardRef } from 'react';
|
|
2
|
-
import {
|
|
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
|
|
6
|
-
|
|
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
|
);
|
package/components/index.ts
CHANGED
package/components/input.tsx
CHANGED
|
@@ -40,7 +40,7 @@ export const Input = forwardRef<
|
|
|
40
40
|
const inputClass = twMerge(
|
|
41
41
|
clsx(
|
|
42
42
|
'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer',
|
|
43
|
-
'focus-visible:outline-
|
|
43
|
+
'focus-visible:outline-none', // disable outline on focus
|
|
44
44
|
error
|
|
45
45
|
? 'border-error focus:border-error'
|
|
46
46
|
: 'border-gray-500 hover:border-black focus:border-black'
|
|
@@ -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
|
);
|
package/components/link.tsx
CHANGED
|
@@ -19,26 +19,56 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
|
|
|
19
19
|
return '#';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
44
|
+
try {
|
|
45
|
+
new URL(trimmedHref, 'http://localhost');
|
|
46
|
+
} catch {
|
|
47
|
+
return '#';
|
|
48
|
+
}
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
68
|
+
if (typeof href !== 'string') {
|
|
69
|
+
return href;
|
|
70
|
+
}
|
|
71
|
+
return '#';
|
|
42
72
|
}, [href, defaultLocaleValue, locale, localeUrlStrategy]);
|
|
43
73
|
|
|
44
74
|
return (
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback, memo, useMemo } from 'react';
|
|
4
|
+
import { LogEntry } from '../hooks/use-logger';
|
|
5
|
+
import { useLoggerContext } from '../hooks/use-logger-context';
|
|
6
|
+
|
|
7
|
+
const LoggerAnimations = ({ color = '#dc2626' }: { color?: string }) => (
|
|
8
|
+
<style jsx global>{`
|
|
9
|
+
@keyframes pulse {
|
|
10
|
+
0% {
|
|
11
|
+
transform: scale(0.95);
|
|
12
|
+
box-shadow: 0 0 0 0 ${color}80;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
70% {
|
|
16
|
+
transform: scale(1.05);
|
|
17
|
+
box-shadow: 0 0 0 10px ${color}00;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
100% {
|
|
21
|
+
transform: scale(0.95);
|
|
22
|
+
box-shadow: 0 0 0 0 ${color}00;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`}</style>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const LogLevelColors = {
|
|
29
|
+
warn: '#ff9800', // orange
|
|
30
|
+
error: '#f44336' // red
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const LoggerTrigger = memo(
|
|
34
|
+
({
|
|
35
|
+
onClick,
|
|
36
|
+
logCount = 0,
|
|
37
|
+
currentColor
|
|
38
|
+
}: {
|
|
39
|
+
onClick: () => void;
|
|
40
|
+
logCount?: number;
|
|
41
|
+
currentColor: string;
|
|
42
|
+
}) => {
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
className="fixed bottom-4 right-4 w-14 h-14 border border-white rounded-full flex items-center justify-center shadow-lg z-[9999] hover:opacity-90 transition-colors"
|
|
47
|
+
aria-label="Open Logger"
|
|
48
|
+
style={{
|
|
49
|
+
backgroundColor: currentColor,
|
|
50
|
+
...(logCount > 0 && {
|
|
51
|
+
animation: 'pulse 2s infinite'
|
|
52
|
+
})
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
fill="none"
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
strokeWidth="1.5"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
className="text-white size-6"
|
|
62
|
+
>
|
|
63
|
+
<path
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
|
|
67
|
+
/>
|
|
68
|
+
</svg>
|
|
69
|
+
|
|
70
|
+
{logCount > 0 && (
|
|
71
|
+
<span
|
|
72
|
+
className="absolute w-5 h-5 -bottom-[5px] p-1 -left-[5px] border-2 border-white rounded-full flex items-center justify-center text-[8px] text-white font-bold"
|
|
73
|
+
style={{
|
|
74
|
+
backgroundColor: currentColor
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{logCount > 99 ? '99+' : logCount}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
</button>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
LoggerTrigger.displayName = 'LoggerTrigger';
|
|
86
|
+
|
|
87
|
+
const LogItem = memo(({ log }: { log: LogEntry }) => {
|
|
88
|
+
const [expanded, setExpanded] = useState(false);
|
|
89
|
+
const hasPayload = log.payload && Object.keys(log.payload).length > 0;
|
|
90
|
+
|
|
91
|
+
const toggleExpanded = useCallback(() => {
|
|
92
|
+
setExpanded((prev) => !prev);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="border-b border-gray-200 py-2">
|
|
97
|
+
<div className="relative">
|
|
98
|
+
<div
|
|
99
|
+
className="absolute top-0 left-0 w-3 h-3 rounded-full mt-1.5 mr-2 flex-shrink-0"
|
|
100
|
+
style={{
|
|
101
|
+
backgroundColor: LogLevelColors[log.level],
|
|
102
|
+
boxShadow: `0 0 5px ${LogLevelColors[log.level]}`
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
<div className="ml-6">
|
|
106
|
+
<div className="flex justify-between">
|
|
107
|
+
<span
|
|
108
|
+
className="font-medium capitalize"
|
|
109
|
+
style={{
|
|
110
|
+
color: LogLevelColors[log.level]
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{log.level}
|
|
114
|
+
</span>
|
|
115
|
+
<span className="text-xs text-gray-500">
|
|
116
|
+
{log.timestamp.toLocaleTimeString()}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
<p className="text-sm">{log.message}</p>
|
|
120
|
+
{hasPayload && (
|
|
121
|
+
<button
|
|
122
|
+
onClick={toggleExpanded}
|
|
123
|
+
className="text-xs text-blue-500 mt-1 hover:text-blue-700 transition-colors"
|
|
124
|
+
>
|
|
125
|
+
{expanded ? 'Hide Details' : 'Show Details'}
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
{expanded && hasPayload && (
|
|
129
|
+
<pre className="text-xs bg-gray-100 p-2 mt-1 rounded overflow-auto max-h-96 max-w-full">
|
|
130
|
+
{JSON.stringify(log.payload, null, 2)}
|
|
131
|
+
</pre>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
LogItem.displayName = 'LogItem';
|
|
140
|
+
|
|
141
|
+
export const LoggerPopup = () => {
|
|
142
|
+
const {
|
|
143
|
+
logs,
|
|
144
|
+
isVisible,
|
|
145
|
+
toggleVisibility,
|
|
146
|
+
clearLogs,
|
|
147
|
+
isDevelopment,
|
|
148
|
+
hasError,
|
|
149
|
+
hasWarning
|
|
150
|
+
} = useLoggerContext();
|
|
151
|
+
|
|
152
|
+
const currentColor = useMemo(() => {
|
|
153
|
+
if (logs.length === 0) return '#b5afaf';
|
|
154
|
+
if (hasError) return '#dc2626';
|
|
155
|
+
if (hasWarning) return '#ff9800';
|
|
156
|
+
|
|
157
|
+
return '#b5afaf';
|
|
158
|
+
}, [logs.length, hasError, hasWarning]);
|
|
159
|
+
|
|
160
|
+
if (!isDevelopment) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isVisible) {
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<LoggerAnimations color={currentColor} />
|
|
168
|
+
<LoggerTrigger
|
|
169
|
+
onClick={toggleVisibility}
|
|
170
|
+
logCount={logs.length}
|
|
171
|
+
currentColor={currentColor}
|
|
172
|
+
/>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
<LoggerAnimations color={currentColor} />
|
|
180
|
+
<LoggerTrigger
|
|
181
|
+
onClick={toggleVisibility}
|
|
182
|
+
logCount={logs.length}
|
|
183
|
+
currentColor={currentColor}
|
|
184
|
+
/>
|
|
185
|
+
<div className="fixed bottom-20 right-4 w-96 max-w-[calc(100vw-2rem)] bg-white rounded-lg shadow-xl z-50 border-2 border-gray-200 max-h-[70vh] flex flex-col">
|
|
186
|
+
<div className="flex items-center justify-between p-3 border-b border-gray-200">
|
|
187
|
+
<h3 className="font-bold flex items-center">Development Logger</h3>
|
|
188
|
+
<div className="flex space-x-2">
|
|
189
|
+
<button
|
|
190
|
+
onClick={clearLogs}
|
|
191
|
+
className="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded transition-colors"
|
|
192
|
+
>
|
|
193
|
+
Clear
|
|
194
|
+
</button>
|
|
195
|
+
<button
|
|
196
|
+
onClick={toggleVisibility}
|
|
197
|
+
className="text-xs bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded transition-colors"
|
|
198
|
+
>
|
|
199
|
+
Close
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="overflow-y-auto flex-grow p-3">
|
|
204
|
+
{logs.length === 0 ? (
|
|
205
|
+
<p className="text-gray-500 text-center py-4">No logs yet</p>
|
|
206
|
+
) : (
|
|
207
|
+
logs.map((log) => <LogItem key={log.id} log={log} />)
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</>
|
|
212
|
+
);
|
|
213
|
+
};
|
package/components/modal.tsx
CHANGED
|
@@ -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
|
|
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
|
|
50
|
-
|
|
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=
|
|
67
|
+
className={twMerge('ml-auto', closeButtonClassName)}
|
|
56
68
|
>
|
|
57
|
-
<Icon
|
|
69
|
+
<Icon
|
|
70
|
+
name={iconName}
|
|
71
|
+
size={iconSize}
|
|
72
|
+
className={iconClassName}
|
|
73
|
+
/>
|
|
58
74
|
</button>
|
|
59
75
|
)}
|
|
60
76
|
</div>
|