@akinon/next 2.0.0-beta.2 → 2.0.0-beta.20
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/.eslintrc.js +12 -0
- package/CHANGELOG.md +377 -7
- package/__tests__/next-config.test.ts +83 -0
- package/__tests__/tsconfig.json +23 -0
- package/api/auth.ts +133 -44
- 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-prebuild.js +1 -0
- package/bin/pz-predev.js +1 -0
- package/bin/pz-run-tests.js +99 -0
- package/bin/run-prebuild-tests.js +46 -0
- package/components/accordion.tsx +20 -5
- package/components/button.tsx +51 -36
- package/components/client-root.tsx +138 -2
- package/components/file-input.tsx +65 -3
- package/components/index.ts +1 -0
- package/components/input.tsx +1 -1
- 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 +1 -1
- package/components/selected-payment-option-view.tsx +21 -0
- package/components/theme-editor/blocks/accordion-block.tsx +136 -0
- package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
- package/components/theme-editor/blocks/button-block.tsx +593 -0
- package/components/theme-editor/blocks/counter-block.tsx +348 -0
- package/components/theme-editor/blocks/divider-block.tsx +20 -0
- package/components/theme-editor/blocks/embed-block.tsx +208 -0
- package/components/theme-editor/blocks/group-block.tsx +116 -0
- package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
- package/components/theme-editor/blocks/icon-block.tsx +230 -0
- package/components/theme-editor/blocks/image-block.tsx +137 -0
- package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
- package/components/theme-editor/blocks/input-block.tsx +123 -0
- package/components/theme-editor/blocks/link-block.tsx +216 -0
- package/components/theme-editor/blocks/lottie-block.tsx +325 -0
- package/components/theme-editor/blocks/map-block.tsx +89 -0
- package/components/theme-editor/blocks/slider-block.tsx +595 -0
- package/components/theme-editor/blocks/tab-block.tsx +10 -0
- package/components/theme-editor/blocks/text-block.tsx +52 -0
- package/components/theme-editor/blocks/video-block.tsx +122 -0
- package/components/theme-editor/components/action-toolbar.tsx +305 -0
- package/components/theme-editor/components/designer-overlay.tsx +74 -0
- package/components/theme-editor/components/with-designer-features.tsx +142 -0
- package/components/theme-editor/dynamic-font-loader.tsx +79 -0
- package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
- package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
- package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
- package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
- package/components/theme-editor/placeholder-registry.ts +31 -0
- package/components/theme-editor/sections/before-after-section.tsx +245 -0
- package/components/theme-editor/sections/contact-form-section.tsx +563 -0
- package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
- package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
- package/components/theme-editor/sections/divider-section.tsx +62 -0
- package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
- package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
- package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
- package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
- package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
- package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
- package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
- package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
- package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
- package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
- package/components/theme-editor/sections/section-wrapper.tsx +135 -0
- package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
- package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
- package/components/theme-editor/sections/tabs-section.tsx +578 -0
- package/components/theme-editor/theme-block.tsx +102 -0
- package/components/theme-editor/theme-placeholder-client.tsx +218 -0
- package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
- package/components/theme-editor/theme-placeholder.tsx +288 -0
- package/components/theme-editor/theme-section.tsx +1224 -0
- package/components/theme-editor/theme-settings-context.tsx +13 -0
- package/components/theme-editor/utils/index.ts +792 -0
- package/components/theme-editor/utils/iterator-utils.ts +234 -0
- package/components/theme-editor/utils/publish-window.ts +86 -0
- package/components/theme-editor/utils/visibility-rules.ts +188 -0
- package/data/client/account.ts +17 -2
- package/data/client/api.ts +2 -0
- package/data/client/basket.ts +66 -5
- package/data/client/checkout.ts +391 -99
- package/data/client/misc.ts +38 -2
- package/data/client/product.ts +19 -2
- package/data/client/user.ts +16 -8
- package/data/server/category.ts +11 -9
- package/data/server/flatpage.ts +11 -4
- package/data/server/form.ts +15 -4
- package/data/server/landingpage.ts +11 -4
- package/data/server/list.ts +5 -4
- package/data/server/menu.ts +11 -3
- package/data/server/product.ts +111 -55
- package/data/server/seo.ts +14 -4
- package/data/server/special-page.ts +5 -4
- package/data/server/widget.ts +90 -5
- package/data/urls.ts +16 -5
- package/hocs/client/with-segment-defaults.tsx +2 -2
- package/hocs/server/with-segment-defaults.tsx +65 -20
- package/hooks/index.ts +4 -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 +51 -14
- package/hooks/use-sentry-uncaught-errors.ts +24 -0
- package/instrumentation/index.ts +10 -1
- package/instrumentation/node.ts +2 -20
- package/jest.config.js +25 -0
- package/lib/cache-handler.mjs +534 -16
- package/lib/cache.ts +272 -37
- package/localization/index.ts +2 -1
- 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 +360 -215
- package/middlewares/index.ts +10 -2
- package/middlewares/locale.ts +34 -11
- 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 +11 -1
- package/middlewares/wallet-complete-redirection.ts +206 -0
- package/package.json +25 -10
- package/plugins.d.ts +19 -4
- package/plugins.js +10 -1
- package/redux/actions.ts +47 -0
- package/redux/middlewares/checkout.ts +63 -138
- package/redux/middlewares/index.ts +14 -10
- package/redux/middlewares/pre-order/address.ts +7 -2
- package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/delivery-option.ts +7 -1
- package/redux/middlewares/pre-order/index.ts +16 -10
- package/redux/middlewares/pre-order/installment-option.ts +8 -1
- package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
- package/redux/middlewares/pre-order/payment-option.ts +7 -1
- package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
- package/redux/middlewares/pre-order/redirection.ts +8 -2
- package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
- package/redux/middlewares/pre-order/shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/shipping-step.ts +5 -1
- package/redux/reducers/checkout.ts +23 -3
- package/redux/reducers/index.ts +11 -3
- package/redux/reducers/root.ts +7 -2
- package/redux/reducers/widget.ts +80 -0
- package/sentry/index.ts +69 -13
- package/tailwind/content.js +16 -0
- package/types/commerce/account.ts +5 -1
- package/types/commerce/checkout.ts +35 -1
- package/types/commerce/widget.ts +33 -0
- package/types/index.ts +101 -6
- 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 +64 -10
- package/utils/localization.ts +4 -0
- package/utils/mobile-3d-iframe.ts +8 -2
- package/utils/override-middleware.ts +7 -12
- 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/views/error-page.tsx +93 -0
- package/with-pz-config.js +13 -6
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
5
|
+
import { getResponsiveValue, getCSSStyles } from '../utils';
|
|
6
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
7
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
8
|
+
import ThemeBlock from '../theme-block';
|
|
9
|
+
|
|
10
|
+
const HotspotBlock = ({
|
|
11
|
+
block,
|
|
12
|
+
currentBreakpoint = 'desktop',
|
|
13
|
+
isDesigner,
|
|
14
|
+
placeholderId,
|
|
15
|
+
sectionId,
|
|
16
|
+
selectedBlockId
|
|
17
|
+
}: BlockRendererProps) => {
|
|
18
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
19
|
+
const themeSettings = useThemeSettingsContext();
|
|
20
|
+
const { locale } = useLocalization();
|
|
21
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
22
|
+
|
|
23
|
+
const styles = block.styles || {};
|
|
24
|
+
const properties = block.properties || {};
|
|
25
|
+
|
|
26
|
+
const getLocalizedContent = (content: any): string => {
|
|
27
|
+
if (typeof content === 'string') return content;
|
|
28
|
+
if (typeof content === 'object' && content !== null) {
|
|
29
|
+
return (
|
|
30
|
+
content[locale] || content[defaultLocale] || Object.values(content)[0] || ''
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const title = getLocalizedContent(properties.title);
|
|
37
|
+
const description = getLocalizedContent(properties.description);
|
|
38
|
+
const link = getLocalizedContent(properties.link);
|
|
39
|
+
const linkText = getLocalizedContent(properties.linkText || 'View Details');
|
|
40
|
+
|
|
41
|
+
const backgroundColor = getResponsiveValue(styles['background-color'], currentBreakpoint, '#ffffff');
|
|
42
|
+
const color = getResponsiveValue(styles.color, currentBreakpoint, '#000000');
|
|
43
|
+
|
|
44
|
+
const formatPercentage = (val: string | number | undefined, defaultVal: string): string => {
|
|
45
|
+
if (val === undefined || val === null || val === '') return defaultVal;
|
|
46
|
+
const strVal = String(val);
|
|
47
|
+
if (strVal.endsWith('%')) return strVal;
|
|
48
|
+
const num = parseFloat(strVal);
|
|
49
|
+
return isNaN(num) ? defaultVal : `${num}%`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const top = formatPercentage(getResponsiveValue(styles.top, currentBreakpoint) as string | number | undefined, '50%');
|
|
53
|
+
const left = formatPercentage(getResponsiveValue(styles.left, currentBreakpoint) as string | number | undefined, '50%');
|
|
54
|
+
|
|
55
|
+
const handleToggle = (e: React.MouseEvent) => {
|
|
56
|
+
if (isDesigner) return;
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
setIsOpen(!isOpen);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className="absolute group pointer-events-auto"
|
|
64
|
+
style={{ top: top as string, left: left as string }}
|
|
65
|
+
>
|
|
66
|
+
<button
|
|
67
|
+
onClick={handleToggle}
|
|
68
|
+
className="relative flex items-center justify-center w-8 h-8 -ml-4 -mt-4 cursor-pointer outline-none focus:outline-none z-10 pointer-events-auto"
|
|
69
|
+
aria-label={title || "Hotspot"}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
className="absolute w-full h-full rounded-full animate-ping opacity-75"
|
|
73
|
+
style={{ backgroundColor: backgroundColor as string }}
|
|
74
|
+
></span>
|
|
75
|
+
|
|
76
|
+
<span
|
|
77
|
+
className="relative w-4 h-4 rounded-full shadow-md"
|
|
78
|
+
style={{ backgroundColor: backgroundColor as string }}
|
|
79
|
+
></span>
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
{(isOpen || (isDesigner && styles.forceOpen)) && (
|
|
83
|
+
<div
|
|
84
|
+
className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 w-64 bg-white p-4 rounded-lg shadow-xl z-20 text-left text-sm pointer-events-auto"
|
|
85
|
+
>
|
|
86
|
+
<div className="flex justify-between items-start mb-2">
|
|
87
|
+
{title && <h3 className="font-bold text-gray-900">{title}</h3>}
|
|
88
|
+
<button onClick={(e) => { e.stopPropagation(); setIsOpen(false); }} className="text-gray-400 hover:text-gray-600">
|
|
89
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
90
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
91
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
92
|
+
</svg>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
{description && <p className="text-gray-600 mb-3">{description}</p>}
|
|
96
|
+
|
|
97
|
+
{block.blocks && block.blocks.map((childBlock) => (
|
|
98
|
+
<div key={childBlock.id} className="mb-2">
|
|
99
|
+
<ThemeBlock
|
|
100
|
+
block={childBlock}
|
|
101
|
+
placeholderId={placeholderId}
|
|
102
|
+
sectionId={sectionId}
|
|
103
|
+
isDesigner={isDesigner}
|
|
104
|
+
currentBreakpoint={currentBreakpoint}
|
|
105
|
+
isSelected={selectedBlockId === childBlock.id}
|
|
106
|
+
selectedBlockId={selectedBlockId}
|
|
107
|
+
onMoveUp={() => {
|
|
108
|
+
if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_UP', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
|
|
109
|
+
}}
|
|
110
|
+
onMoveDown={() => {
|
|
111
|
+
if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_DOWN', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
|
|
112
|
+
}}
|
|
113
|
+
onDuplicate={() => {
|
|
114
|
+
if (window.parent) window.parent.postMessage({ type: 'DUPLICATE_BLOCK', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
|
|
115
|
+
}}
|
|
116
|
+
onToggleVisibility={() => {
|
|
117
|
+
if (window.parent) window.parent.postMessage({ type: 'TOGGLE_BLOCK_VISIBILITY', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
|
|
118
|
+
}}
|
|
119
|
+
onDelete={() => {
|
|
120
|
+
if (window.parent) window.parent.postMessage({ type: 'DELETE_BLOCK_FROM_PARENT', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
|
|
121
|
+
}}
|
|
122
|
+
onRename={(newLabel) => {
|
|
123
|
+
if (window.parent) window.parent.postMessage({ type: 'RENAME_BLOCK', data: { placeholderId, sectionId, blockId: childBlock.id, label: newLabel } }, '*');
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
|
|
129
|
+
{link && (
|
|
130
|
+
<a
|
|
131
|
+
href={link}
|
|
132
|
+
className="text-blue-600 hover:underline font-medium block mt-2"
|
|
133
|
+
onClick={(e) => e.stopPropagation()}
|
|
134
|
+
>
|
|
135
|
+
{linkText} →
|
|
136
|
+
</a>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Arrow */}
|
|
140
|
+
<div className="absolute left-1/2 top-full -translate-x-1/2 -mt-1 border-8 border-transparent border-t-white"></div>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default HotspotBlock;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getResponsiveValue } from '../utils';
|
|
3
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
4
|
+
import { Image } from '../../image';
|
|
5
|
+
|
|
6
|
+
const isValidUrlValue = (value: string): boolean => {
|
|
7
|
+
if (!value) return false;
|
|
8
|
+
if (value.startsWith('data:image')) return true;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
new URL(value, 'http://localhost');
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const IconBlock = ({ block }: BlockRendererProps) => {
|
|
19
|
+
const iconValue = getResponsiveValue(block.value, 'desktop', '');
|
|
20
|
+
const iconSize = getResponsiveValue(block.styles?.size, 'desktop', '24') as
|
|
21
|
+
| string
|
|
22
|
+
| number;
|
|
23
|
+
const iconColor = getResponsiveValue(
|
|
24
|
+
block.styles?.color,
|
|
25
|
+
'desktop',
|
|
26
|
+
'currentColor'
|
|
27
|
+
) as string;
|
|
28
|
+
|
|
29
|
+
const iconStyles = {
|
|
30
|
+
width: iconSize + 'px',
|
|
31
|
+
height: iconSize + 'px',
|
|
32
|
+
display: 'inline-flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'center'
|
|
35
|
+
} as React.CSSProperties;
|
|
36
|
+
|
|
37
|
+
const isInlineSvg = (value: string): boolean => {
|
|
38
|
+
return (
|
|
39
|
+
typeof value === 'string' &&
|
|
40
|
+
(value.includes('<svg') || value.includes('<?xml'))
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const modifySVGColor = (svgContent: string, color: string) => {
|
|
45
|
+
if (color === 'currentColor' || !color) return svgContent;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
svgContent
|
|
49
|
+
.replace(/fill\s*=\s*["'][^"']*["']/gi, `fill="${color}"`)
|
|
50
|
+
.replace(/stroke\s*=\s*["'][^"']*["']/gi, `stroke="${color}"`)
|
|
51
|
+
// Also handle CSS styles within SVG
|
|
52
|
+
.replace(/fill\s*:\s*[^;}\s]+/gi, `fill: ${color}`)
|
|
53
|
+
.replace(/stroke\s*:\s*[^;}\s]+/gi, `stroke: ${color}`)
|
|
54
|
+
// If no fill/stroke exists, add fill to main elements
|
|
55
|
+
.replace(/<path(?![^>]*fill)/gi, `<path fill="${color}"`)
|
|
56
|
+
.replace(/<circle(?![^>]*fill)/gi, `<circle fill="${color}"`)
|
|
57
|
+
.replace(/<rect(?![^>]*fill)/gi, `<rect fill="${color}"`)
|
|
58
|
+
.replace(/<polygon(?![^>]*fill)/gi, `<polygon fill="${color}"`)
|
|
59
|
+
.replace(/<ellipse(?![^>]*fill)/gi, `<ellipse fill="${color}"`)
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const processBase64SVG = (base64String: string, color: string) => {
|
|
64
|
+
try {
|
|
65
|
+
const base64Content = base64String.replace(
|
|
66
|
+
/^data:image\/svg\+xml;base64,/,
|
|
67
|
+
''
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const svgContent = atob(base64Content);
|
|
71
|
+
|
|
72
|
+
const modifiedSVG = modifySVGColor(svgContent, color);
|
|
73
|
+
|
|
74
|
+
const modifiedBase64 = `data:image/svg+xml;base64,${btoa(modifiedSVG)}`;
|
|
75
|
+
|
|
76
|
+
return modifiedBase64;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error processing SVG base64:', error);
|
|
79
|
+
return base64String;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let iconContent;
|
|
84
|
+
|
|
85
|
+
let svgSrc = '';
|
|
86
|
+
|
|
87
|
+
if (iconValue) {
|
|
88
|
+
if (typeof iconValue === 'string') {
|
|
89
|
+
svgSrc = iconValue.trim();
|
|
90
|
+
} else if (typeof iconValue === 'object' && iconValue !== null) {
|
|
91
|
+
if ('url' in iconValue) {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
svgSrc = String((iconValue as any).url || '').trim();
|
|
94
|
+
} else {
|
|
95
|
+
// Try to find a string value in the object (e.g. localized)
|
|
96
|
+
const values = Object.values(iconValue);
|
|
97
|
+
const stringVal = values.find((v) => typeof v === 'string');
|
|
98
|
+
if (stringVal) {
|
|
99
|
+
svgSrc = (stringVal as string).trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isBase64 =
|
|
105
|
+
typeof svgSrc === 'string' && svgSrc.startsWith('data:image');
|
|
106
|
+
const isSvg = isInlineSvg(svgSrc);
|
|
107
|
+
const isAbsolutePath = svgSrc.startsWith('/');
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
!isBase64 &&
|
|
111
|
+
!isSvg &&
|
|
112
|
+
svgSrc &&
|
|
113
|
+
!isAbsolutePath &&
|
|
114
|
+
!svgSrc.startsWith('http') &&
|
|
115
|
+
!svgSrc.startsWith('//')
|
|
116
|
+
) {
|
|
117
|
+
const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
|
|
118
|
+
svgSrc = cloudName
|
|
119
|
+
? `https://${cloudName}/${svgSrc.replace(/^\/+/, '')}`
|
|
120
|
+
: '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
isBase64 &&
|
|
125
|
+
svgSrc.includes('data:image/svg+xml;base64,') &&
|
|
126
|
+
iconColor !== 'currentColor'
|
|
127
|
+
) {
|
|
128
|
+
svgSrc = processBase64SVG(svgSrc, iconColor);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (svgSrc && svgSrc.trim() !== '') {
|
|
132
|
+
// Check if it's inline SVG content
|
|
133
|
+
if (isInlineSvg(svgSrc)) {
|
|
134
|
+
// Apply color modification to inline SVG
|
|
135
|
+
const coloredSvg =
|
|
136
|
+
iconColor !== 'currentColor'
|
|
137
|
+
? modifySVGColor(svgSrc, iconColor)
|
|
138
|
+
: svgSrc;
|
|
139
|
+
|
|
140
|
+
iconContent = (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
...iconStyles,
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
145
|
+
['--svg-size' as any]: iconSize + 'px'
|
|
146
|
+
}}
|
|
147
|
+
className="[&>svg]:w-[var(--svg-size)] [&>svg]:h-[var(--svg-size)]"
|
|
148
|
+
dangerouslySetInnerHTML={{ __html: coloredSvg }}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
// It's a URL or base64, use img tag
|
|
153
|
+
if (isValidUrlValue(svgSrc)) {
|
|
154
|
+
iconContent = (
|
|
155
|
+
<div style={iconStyles}>
|
|
156
|
+
<Image
|
|
157
|
+
src={svgSrc}
|
|
158
|
+
width={Number(iconSize)}
|
|
159
|
+
height={Number(iconSize)}
|
|
160
|
+
alt="Icon"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
iconContent = (
|
|
168
|
+
<div
|
|
169
|
+
style={{
|
|
170
|
+
...iconStyles,
|
|
171
|
+
border: '1px dashed #ccc',
|
|
172
|
+
borderRadius: '4px',
|
|
173
|
+
backgroundColor: '#f9f9f9'
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
<span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!iconContent) {
|
|
182
|
+
iconContent = (
|
|
183
|
+
<div
|
|
184
|
+
style={{
|
|
185
|
+
...iconStyles,
|
|
186
|
+
border: '1px dashed #ccc',
|
|
187
|
+
borderRadius: '4px',
|
|
188
|
+
backgroundColor: '#f9f9f9'
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
iconContent = (
|
|
197
|
+
<div
|
|
198
|
+
style={{
|
|
199
|
+
...iconStyles,
|
|
200
|
+
border: '2px dashed #e2e8f0',
|
|
201
|
+
borderRadius: '8px',
|
|
202
|
+
backgroundColor: '#f8fafc',
|
|
203
|
+
transition: 'all 0.2s ease-in-out'
|
|
204
|
+
}}
|
|
205
|
+
className="hover:border-slate-300 hover:bg-slate-50"
|
|
206
|
+
>
|
|
207
|
+
<div className="flex flex-col items-center justify-center gap-1 p-2">
|
|
208
|
+
<svg
|
|
209
|
+
width="16"
|
|
210
|
+
height="16"
|
|
211
|
+
viewBox="0 0 24 24"
|
|
212
|
+
fill="none"
|
|
213
|
+
stroke="#94a3b8"
|
|
214
|
+
strokeWidth="2"
|
|
215
|
+
strokeLinecap="round"
|
|
216
|
+
strokeLinejoin="round"
|
|
217
|
+
>
|
|
218
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
219
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
220
|
+
<polyline points="21,15 16,10 5,21" />
|
|
221
|
+
</svg>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return iconContent;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export default IconBlock;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getResponsiveValue } from '../utils';
|
|
4
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
5
|
+
|
|
6
|
+
const pickImageSource = (
|
|
7
|
+
value: unknown,
|
|
8
|
+
currentBreakpoint: string
|
|
9
|
+
): string => {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
return value.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value === 'object' && value !== null) {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const objectValue = value as any;
|
|
17
|
+
if (typeof objectValue.url === 'string') {
|
|
18
|
+
return objectValue.url.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const responsiveValue = getResponsiveValue(value, currentBreakpoint, '');
|
|
22
|
+
if (typeof responsiveValue === 'string') {
|
|
23
|
+
return responsiveValue.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const firstStringValue = Object.values(objectValue).find(
|
|
27
|
+
(item) => typeof item === 'string'
|
|
28
|
+
);
|
|
29
|
+
if (typeof firstStringValue === 'string') {
|
|
30
|
+
return firstStringValue.trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return '';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const normalizeImageSource = (src: string): string => {
|
|
38
|
+
if (!src) return '';
|
|
39
|
+
if (src.startsWith('data:image')) return src;
|
|
40
|
+
if (src.startsWith('/')) return src;
|
|
41
|
+
if (src.startsWith('//')) return src;
|
|
42
|
+
if (src.startsWith('http://') || src.startsWith('https://')) return src;
|
|
43
|
+
|
|
44
|
+
const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
|
|
45
|
+
if (!cloudName) return '';
|
|
46
|
+
|
|
47
|
+
return `https://${cloudName}/${src.replace(/^\/+/, '')}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isValidImageSource = (src: string): boolean => {
|
|
51
|
+
if (!src) return false;
|
|
52
|
+
if (src.startsWith('data:image')) return true;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
new URL(src, 'http://localhost');
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const ImageBlock = ({
|
|
63
|
+
block,
|
|
64
|
+
currentBreakpoint = 'desktop'
|
|
65
|
+
}: BlockRendererProps) => {
|
|
66
|
+
const imageValue = useMemo(() => {
|
|
67
|
+
const rawUrl = pickImageSource(block.value, currentBreakpoint);
|
|
68
|
+
const url = normalizeImageSource(rawUrl);
|
|
69
|
+
const alt = String(block.properties?.alt || '');
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
url: isValidImageSource(url) ? url : '',
|
|
73
|
+
alt
|
|
74
|
+
};
|
|
75
|
+
}, [block.value, block.properties?.alt, currentBreakpoint]);
|
|
76
|
+
|
|
77
|
+
const { url: src, alt } = imageValue;
|
|
78
|
+
|
|
79
|
+
const width = getResponsiveValue(
|
|
80
|
+
block.styles?.width,
|
|
81
|
+
currentBreakpoint,
|
|
82
|
+
'100%'
|
|
83
|
+
) as React.CSSProperties['width'];
|
|
84
|
+
const height = getResponsiveValue(
|
|
85
|
+
block.styles?.height,
|
|
86
|
+
currentBreakpoint,
|
|
87
|
+
'auto'
|
|
88
|
+
) as React.CSSProperties['height'];
|
|
89
|
+
const objectFit = getResponsiveValue(
|
|
90
|
+
block.styles?.['object-fit'],
|
|
91
|
+
currentBreakpoint,
|
|
92
|
+
'cover'
|
|
93
|
+
) as React.CSSProperties['objectFit'];
|
|
94
|
+
const borderRadius = getResponsiveValue(
|
|
95
|
+
block.styles?.['border-radius'],
|
|
96
|
+
currentBreakpoint,
|
|
97
|
+
'0px'
|
|
98
|
+
) as React.CSSProperties['borderRadius'];
|
|
99
|
+
|
|
100
|
+
const content = !src ? (
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
width: width ?? '100%',
|
|
104
|
+
height: height ?? '100%',
|
|
105
|
+
borderRadius: borderRadius ?? '0px',
|
|
106
|
+
overflow: 'hidden',
|
|
107
|
+
backgroundColor: '#e5e7eb',
|
|
108
|
+
display: 'flex',
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
textAlign: 'center',
|
|
112
|
+
minHeight:
|
|
113
|
+
typeof height === 'string' && height !== 'auto' ? height : '150px',
|
|
114
|
+
color: '#6b7280'
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
No image uploaded
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<img
|
|
121
|
+
key={src}
|
|
122
|
+
src={src}
|
|
123
|
+
alt={alt}
|
|
124
|
+
style={{
|
|
125
|
+
display: 'block',
|
|
126
|
+
width: width ?? '100%',
|
|
127
|
+
height: height ?? 'auto',
|
|
128
|
+
objectFit: objectFit ?? 'cover',
|
|
129
|
+
borderRadius: borderRadius ?? '0px'
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return content;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default ImageBlock;
|