@akinon/pz-theme 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.
- package/CHANGELOG.md +17 -0
- package/package.json +27 -0
- package/readme.md +23 -0
- package/src/blocks/accordion-block.tsx +136 -0
- package/src/blocks/block-renderer-registry.tsx +77 -0
- package/src/blocks/button-block.tsx +593 -0
- package/src/blocks/counter-block.tsx +348 -0
- package/src/blocks/divider-block.tsx +20 -0
- package/src/blocks/embed-block.tsx +208 -0
- package/src/blocks/group-block.tsx +116 -0
- package/src/blocks/hotspot-block.tsx +147 -0
- package/src/blocks/icon-block.tsx +230 -0
- package/src/blocks/image-block.tsx +142 -0
- package/src/blocks/image-gallery-block.tsx +269 -0
- package/src/blocks/input-block.tsx +123 -0
- package/src/blocks/link-block.tsx +216 -0
- package/src/blocks/lottie-block.tsx +325 -0
- package/src/blocks/map-block.tsx +89 -0
- package/src/blocks/slider-block.tsx +595 -0
- package/src/blocks/tab-block.tsx +10 -0
- package/src/blocks/text-block.tsx +52 -0
- package/src/blocks/video-block.tsx +122 -0
- package/src/components/action-toolbar.tsx +305 -0
- package/src/components/designer-overlay.tsx +74 -0
- package/src/components/with-designer-features.tsx +142 -0
- package/src/dynamic-font-loader.tsx +79 -0
- package/src/hooks/use-designer-features.tsx +100 -0
- package/src/hooks/use-visibility-context.ts +27 -0
- package/src/index.ts +21 -0
- package/src/placeholder-registry.ts +31 -0
- package/src/sections/before-after-section.tsx +245 -0
- package/src/sections/contact-form-section.tsx +564 -0
- package/src/sections/countdown-campaign-banner-section.tsx +433 -0
- package/src/sections/coupon-banner-section.tsx +710 -0
- package/src/sections/divider-section.tsx +62 -0
- package/src/sections/featured-product-spotlight-section.tsx +507 -0
- package/src/sections/find-in-store-section.tsx +1995 -0
- package/src/sections/hover-showcase-section.tsx +326 -0
- package/src/sections/image-hotspot-section.tsx +142 -0
- package/src/sections/installment-options-section.tsx +1065 -0
- package/src/sections/notification-banner-section.tsx +173 -0
- package/src/sections/order-tracking-lookup-section.tsx +1379 -0
- package/src/sections/posts-slider-section.tsx +472 -0
- package/src/sections/pre-order-launch-banner-section.tsx +687 -0
- package/src/sections/section-renderer-registry.tsx +89 -0
- package/src/sections/section-wrapper.tsx +135 -0
- package/src/sections/shipping-threshold-progress-section.tsx +586 -0
- package/src/sections/stats-counter-section.tsx +486 -0
- package/src/sections/tabs-section.tsx +578 -0
- package/src/theme-block.tsx +102 -0
- package/src/theme-page-context.tsx +27 -0
- package/src/theme-placeholder-client.tsx +218 -0
- package/src/theme-placeholder-wrapper.tsx +786 -0
- package/src/theme-placeholder.tsx +305 -0
- package/src/theme-section.tsx +1241 -0
- package/src/theme-settings-context.tsx +13 -0
- package/src/utils/index.ts +791 -0
- package/src/utils/iterator-utils.test.ts +224 -0
- package/src/utils/iterator-utils.ts +617 -0
- package/src/utils/page-context-discovery.ts +119 -0
- package/src/utils/publish-window.ts +86 -0
- package/src/utils/visibility-rules.ts +188 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Modal } from '@akinon/next/components/modal';
|
|
6
|
+
import ThemeBlock from '../theme-block';
|
|
7
|
+
import { getResponsiveValue } from '../utils';
|
|
8
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
9
|
+
|
|
10
|
+
type GalleryImage = {
|
|
11
|
+
url: string;
|
|
12
|
+
alt: string;
|
|
13
|
+
id: string;
|
|
14
|
+
objectFit: React.CSSProperties['objectFit'];
|
|
15
|
+
borderRadius: React.CSSProperties['borderRadius'];
|
|
16
|
+
width: React.CSSProperties['width'];
|
|
17
|
+
height: React.CSSProperties['height'];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const resolveImageUrl = (raw: unknown): string => {
|
|
21
|
+
const url = typeof raw === 'string' ? raw : '';
|
|
22
|
+
|
|
23
|
+
const isBase64 = url.startsWith('data:image');
|
|
24
|
+
const isAbsolutePath = url.startsWith('/');
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
!isBase64 &&
|
|
28
|
+
!isAbsolutePath &&
|
|
29
|
+
url &&
|
|
30
|
+
!url.startsWith('http') &&
|
|
31
|
+
!url.startsWith('//')
|
|
32
|
+
) {
|
|
33
|
+
return `https://${process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME ?? ''}/${url}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return url;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ImageGalleryBlock = ({
|
|
40
|
+
block,
|
|
41
|
+
placeholderId,
|
|
42
|
+
sectionId,
|
|
43
|
+
isDesigner,
|
|
44
|
+
selectedBlockId,
|
|
45
|
+
currentBreakpoint = 'desktop'
|
|
46
|
+
}: BlockRendererProps) => {
|
|
47
|
+
const images: GalleryImage[] = useMemo(() => {
|
|
48
|
+
const children = Array.isArray(block.blocks) ? block.blocks : [];
|
|
49
|
+
|
|
50
|
+
return children
|
|
51
|
+
.filter((child) => child.type === 'image')
|
|
52
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
53
|
+
.map((child) => {
|
|
54
|
+
const url = resolveImageUrl(child.value);
|
|
55
|
+
|
|
56
|
+
const rawAlt = child.properties?.alt;
|
|
57
|
+
const alt =
|
|
58
|
+
typeof rawAlt === 'string'
|
|
59
|
+
? rawAlt
|
|
60
|
+
: typeof rawAlt === 'object' && rawAlt
|
|
61
|
+
? String(rawAlt[currentBreakpoint] ?? rawAlt.desktop ?? '')
|
|
62
|
+
: '';
|
|
63
|
+
|
|
64
|
+
const objectFit = getResponsiveValue(
|
|
65
|
+
child.styles?.['object-fit'],
|
|
66
|
+
currentBreakpoint,
|
|
67
|
+
'cover'
|
|
68
|
+
) as React.CSSProperties['objectFit'];
|
|
69
|
+
const borderRadius = getResponsiveValue(
|
|
70
|
+
child.styles?.['border-radius'],
|
|
71
|
+
currentBreakpoint,
|
|
72
|
+
'0px'
|
|
73
|
+
) as React.CSSProperties['borderRadius'];
|
|
74
|
+
const width = getResponsiveValue(
|
|
75
|
+
child.styles?.width,
|
|
76
|
+
currentBreakpoint,
|
|
77
|
+
'100%'
|
|
78
|
+
) as React.CSSProperties['width'];
|
|
79
|
+
const height = getResponsiveValue(
|
|
80
|
+
child.styles?.height,
|
|
81
|
+
currentBreakpoint,
|
|
82
|
+
'auto'
|
|
83
|
+
) as React.CSSProperties['height'];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
url,
|
|
87
|
+
alt,
|
|
88
|
+
id: child.id,
|
|
89
|
+
objectFit: objectFit ?? 'cover',
|
|
90
|
+
borderRadius: borderRadius ?? '0px',
|
|
91
|
+
width: width ?? '100%',
|
|
92
|
+
height: height ?? 'auto'
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
.filter((img) => Boolean(img.url));
|
|
96
|
+
}, [block.blocks, currentBreakpoint]);
|
|
97
|
+
|
|
98
|
+
// Hooks must be called unconditionally. `isDesigner` can flip after mount
|
|
99
|
+
// (theme editor cookie / iframe messages), so keep state/effects above any early returns.
|
|
100
|
+
const [open, setOpen] = useState(false);
|
|
101
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
102
|
+
|
|
103
|
+
const goPrev = () => {
|
|
104
|
+
if (!images.length) return;
|
|
105
|
+
setActiveIndex((i) => (i - 1 + images.length) % images.length);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const goNext = () => {
|
|
109
|
+
if (!images.length) return;
|
|
110
|
+
setActiveIndex((i) => (i + 1) % images.length);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (isDesigner) return;
|
|
115
|
+
if (!open) return;
|
|
116
|
+
|
|
117
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
118
|
+
if (e.key === 'Escape') setOpen(false);
|
|
119
|
+
if (e.key === 'ArrowLeft') goPrev();
|
|
120
|
+
if (e.key === 'ArrowRight') goNext();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
window.addEventListener('keydown', onKeyDown);
|
|
124
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
125
|
+
}, [isDesigner, open, images.length]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (activeIndex >= images.length) {
|
|
129
|
+
setActiveIndex(0);
|
|
130
|
+
}
|
|
131
|
+
}, [activeIndex, images.length]);
|
|
132
|
+
|
|
133
|
+
// In designer mode: keep nested ThemeBlock rendering so the editor can select/edit child images.
|
|
134
|
+
if (isDesigner) {
|
|
135
|
+
if (!block.blocks || block.blocks.length === 0) {
|
|
136
|
+
return (
|
|
137
|
+
<div style={{ padding: '20px', color: '#6b7280' }}>
|
|
138
|
+
Empty image gallery
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="contents">
|
|
145
|
+
{block.blocks
|
|
146
|
+
.filter((child) => (isDesigner ? true : !child.hidden))
|
|
147
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
148
|
+
.map((child) => (
|
|
149
|
+
<ThemeBlock
|
|
150
|
+
key={child.id}
|
|
151
|
+
block={child}
|
|
152
|
+
placeholderId={placeholderId}
|
|
153
|
+
sectionId={sectionId}
|
|
154
|
+
isDesigner={isDesigner}
|
|
155
|
+
isSelected={selectedBlockId === child.id}
|
|
156
|
+
selectedBlockId={selectedBlockId}
|
|
157
|
+
currentBreakpoint={currentBreakpoint}
|
|
158
|
+
/>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!images.length) {
|
|
165
|
+
return (
|
|
166
|
+
<div style={{ padding: '20px', color: '#6b7280' }}>
|
|
167
|
+
No images uploaded
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const active = images[activeIndex];
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<div className="contents">
|
|
177
|
+
{images.map((img, index) => (
|
|
178
|
+
<button
|
|
179
|
+
key={img.id}
|
|
180
|
+
type="button"
|
|
181
|
+
onClick={(e) => {
|
|
182
|
+
e.stopPropagation();
|
|
183
|
+
setActiveIndex(index);
|
|
184
|
+
setOpen(true);
|
|
185
|
+
}}
|
|
186
|
+
className="relative"
|
|
187
|
+
style={{
|
|
188
|
+
all: 'unset',
|
|
189
|
+
cursor: 'pointer',
|
|
190
|
+
display: 'block',
|
|
191
|
+
width: '100%',
|
|
192
|
+
height: '100%'
|
|
193
|
+
}}
|
|
194
|
+
aria-label={img.alt || `Open image ${index + 1}`}
|
|
195
|
+
>
|
|
196
|
+
<img
|
|
197
|
+
src={img.url}
|
|
198
|
+
alt={img.alt}
|
|
199
|
+
style={{
|
|
200
|
+
display: 'block',
|
|
201
|
+
width: img.width ?? '100%',
|
|
202
|
+
height: img.height ?? 'auto',
|
|
203
|
+
objectFit: img.objectFit ?? 'cover',
|
|
204
|
+
borderRadius: img.borderRadius ?? '0px'
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
</button>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<Modal
|
|
212
|
+
portalId="image-gallery-lightbox"
|
|
213
|
+
open={open}
|
|
214
|
+
setOpen={setOpen}
|
|
215
|
+
title=""
|
|
216
|
+
showCloseButton={true}
|
|
217
|
+
className="w-[92vw] max-w-[1024px] max-h-[90vh] overflow-hidden rounded"
|
|
218
|
+
headerWrapperClassName="border-0"
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
className="relative bg-black"
|
|
222
|
+
style={{ width: '100%', height: '80vh' }}
|
|
223
|
+
>
|
|
224
|
+
<img
|
|
225
|
+
src={active.url}
|
|
226
|
+
alt={active.alt}
|
|
227
|
+
style={{
|
|
228
|
+
width: '100%',
|
|
229
|
+
height: '100%',
|
|
230
|
+
objectFit: 'contain'
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={goPrev}
|
|
237
|
+
aria-label="Previous"
|
|
238
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-white"
|
|
239
|
+
style={{
|
|
240
|
+
background: 'rgba(0,0,0,0.45)',
|
|
241
|
+
width: '40px',
|
|
242
|
+
height: '40px',
|
|
243
|
+
borderRadius: '9999px'
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
‹
|
|
247
|
+
</button>
|
|
248
|
+
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
onClick={goNext}
|
|
252
|
+
aria-label="Next"
|
|
253
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white"
|
|
254
|
+
style={{
|
|
255
|
+
background: 'rgba(0,0,0,0.45)',
|
|
256
|
+
width: '40px',
|
|
257
|
+
height: '40px',
|
|
258
|
+
borderRadius: '9999px'
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
›
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</Modal>
|
|
265
|
+
</>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export default ImageGalleryBlock;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { getResponsiveValue, resolveThemeCssVariables } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
8
|
+
|
|
9
|
+
const InputBlock = ({
|
|
10
|
+
block,
|
|
11
|
+
currentBreakpoint = 'desktop',
|
|
12
|
+
isDesigner = false
|
|
13
|
+
}: BlockRendererProps) => {
|
|
14
|
+
const { locale } = useLocalization();
|
|
15
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
16
|
+
const themeSettings = useThemeSettingsContext();
|
|
17
|
+
|
|
18
|
+
const resolveLocalizedValue = (
|
|
19
|
+
value: unknown,
|
|
20
|
+
fallback: string
|
|
21
|
+
): string => {
|
|
22
|
+
if (value == null) return fallback;
|
|
23
|
+
|
|
24
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
25
|
+
const localized = value as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
if (localized[locale] != null) return String(localized[locale]);
|
|
28
|
+
if (localized[defaultLocale] != null)
|
|
29
|
+
return String(localized[defaultLocale]);
|
|
30
|
+
|
|
31
|
+
const responsive = getResponsiveValue(value, currentBreakpoint);
|
|
32
|
+
if (responsive != null && typeof responsive !== 'object') {
|
|
33
|
+
return String(responsive);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const firstValue = Object.values(localized).find(item => item != null);
|
|
37
|
+
if (firstValue != null) return String(firstValue);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const responsive = getResponsiveValue(value, currentBreakpoint, fallback);
|
|
41
|
+
return String(responsive ?? fallback);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const type = resolveLocalizedValue(block.properties?.type, 'text');
|
|
45
|
+
const name = resolveLocalizedValue(block.properties?.name, 'input');
|
|
46
|
+
const placeholder = resolveLocalizedValue(
|
|
47
|
+
block.properties?.placeholder,
|
|
48
|
+
'Enter text...'
|
|
49
|
+
);
|
|
50
|
+
const blockValue = resolveLocalizedValue(block.value, '');
|
|
51
|
+
|
|
52
|
+
const required = Boolean(
|
|
53
|
+
getResponsiveValue(block.properties?.required, currentBreakpoint, false)
|
|
54
|
+
);
|
|
55
|
+
const disabled = Boolean(
|
|
56
|
+
getResponsiveValue(block.properties?.disabled, currentBreakpoint, false)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const [localValue, setLocalValue] = useState(blockValue);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setLocalValue(blockValue);
|
|
63
|
+
}, [blockValue]);
|
|
64
|
+
|
|
65
|
+
const inputStyles: React.CSSProperties = useMemo(() => {
|
|
66
|
+
const computedStyles: React.CSSProperties = {};
|
|
67
|
+
|
|
68
|
+
if (!block.styles) {
|
|
69
|
+
return computedStyles;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Object.keys(block.styles).forEach((key) => {
|
|
73
|
+
const styleValue = getResponsiveValue(block.styles[key], currentBreakpoint);
|
|
74
|
+
if (styleValue === undefined || styleValue === null) return;
|
|
75
|
+
|
|
76
|
+
const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
|
|
77
|
+
letter.toUpperCase()
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let resolvedValue: unknown = styleValue;
|
|
81
|
+
if (typeof styleValue === 'string') {
|
|
82
|
+
resolvedValue = resolveThemeCssVariables(styleValue, themeSettings);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
(computedStyles as Record<string, unknown>)[camelKey] = resolvedValue;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return computedStyles;
|
|
89
|
+
}, [block.styles, currentBreakpoint, themeSettings]);
|
|
90
|
+
|
|
91
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
92
|
+
const nextValue = event.target.value;
|
|
93
|
+
setLocalValue(nextValue);
|
|
94
|
+
|
|
95
|
+
if (isDesigner && window.parent) {
|
|
96
|
+
window.parent.postMessage(
|
|
97
|
+
{
|
|
98
|
+
type: 'UPDATE_BLOCK_VALUE',
|
|
99
|
+
data: {
|
|
100
|
+
blockId: block.id,
|
|
101
|
+
value: nextValue
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
'*'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<input
|
|
111
|
+
type={type}
|
|
112
|
+
name={name}
|
|
113
|
+
placeholder={placeholder}
|
|
114
|
+
value={localValue}
|
|
115
|
+
required={required}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
style={inputStyles}
|
|
118
|
+
onChange={handleChange}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default InputBlock;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import { LocaleUrlStrategy } from '@akinon/next/localization';
|
|
7
|
+
import { urlLocaleMatcherRegex, urlSchemes } from '@akinon/next/utils';
|
|
8
|
+
import { getResponsiveValue } from '../utils';
|
|
9
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
10
|
+
import ThemeBlock from '../theme-block';
|
|
11
|
+
|
|
12
|
+
const LinkBlock = ({
|
|
13
|
+
block,
|
|
14
|
+
placeholderId,
|
|
15
|
+
sectionId,
|
|
16
|
+
selectedBlockId,
|
|
17
|
+
currentBreakpoint = 'desktop',
|
|
18
|
+
isDesigner = false
|
|
19
|
+
}: BlockRendererProps) => {
|
|
20
|
+
const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
|
|
21
|
+
|
|
22
|
+
const href = block.properties?.href
|
|
23
|
+
? String(getResponsiveValue(block.properties.href, currentBreakpoint, '#'))
|
|
24
|
+
: '#';
|
|
25
|
+
|
|
26
|
+
const target = block.properties?.target
|
|
27
|
+
? String(
|
|
28
|
+
getResponsiveValue(block.properties.target, currentBreakpoint, '_self')
|
|
29
|
+
)
|
|
30
|
+
: '_self';
|
|
31
|
+
|
|
32
|
+
const rel = target === '_blank' ? 'noopener noreferrer' : undefined;
|
|
33
|
+
|
|
34
|
+
const formattedHref = useMemo(() => {
|
|
35
|
+
if (!href) {
|
|
36
|
+
return '#';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const trimmedHref = href.trim();
|
|
40
|
+
if (!trimmedHref) {
|
|
41
|
+
return '#';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (urlSchemes.some((scheme) => trimmedHref.startsWith(scheme))) {
|
|
45
|
+
if (trimmedHref.startsWith('mailto:') || trimmedHref.startsWith('tel:')) {
|
|
46
|
+
return trimmedHref;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
new URL(trimmedHref);
|
|
51
|
+
return trimmedHref;
|
|
52
|
+
} catch {
|
|
53
|
+
return '#';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
new URL(trimmedHref, 'http://localhost');
|
|
59
|
+
} catch {
|
|
60
|
+
return '#';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pathnameWithoutLocale = trimmedHref.replace(
|
|
64
|
+
urlLocaleMatcherRegex,
|
|
65
|
+
''
|
|
66
|
+
);
|
|
67
|
+
const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
|
|
68
|
+
|
|
69
|
+
if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
|
|
70
|
+
return hrefWithLocale;
|
|
71
|
+
} else if (
|
|
72
|
+
localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
|
|
73
|
+
locale !== defaultLocaleValue
|
|
74
|
+
) {
|
|
75
|
+
return hrefWithLocale;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return trimmedHref || '#';
|
|
79
|
+
}, [href, defaultLocaleValue, locale, localeUrlStrategy]);
|
|
80
|
+
|
|
81
|
+
const getStyleEntry = (kebabKey: string) => {
|
|
82
|
+
if (!block.styles) return undefined;
|
|
83
|
+
const styles = block.styles as Record<string, unknown>;
|
|
84
|
+
const camelKey = kebabKey.replace(/-([a-z])/g, (_, letter) =>
|
|
85
|
+
letter.toUpperCase()
|
|
86
|
+
);
|
|
87
|
+
return styles[kebabKey] ?? styles[camelKey];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const normalizeKey = (key: string): string => {
|
|
91
|
+
if (key.includes('-')) return key;
|
|
92
|
+
return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const inlineStyles: React.CSSProperties = {};
|
|
96
|
+
if (block.styles) {
|
|
97
|
+
const keys = Object.keys(block.styles);
|
|
98
|
+
keys.forEach((key) => {
|
|
99
|
+
const normalizedKey = normalizeKey(key);
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
normalizedKey === 'hover-flex' ||
|
|
103
|
+
normalizedKey === 'hover-child-flex' ||
|
|
104
|
+
normalizedKey === 'first-child-flex'
|
|
105
|
+
) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const styleEntry = getStyleEntry(normalizedKey);
|
|
110
|
+
if (styleEntry) {
|
|
111
|
+
const val = getResponsiveValue(styleEntry, currentBreakpoint);
|
|
112
|
+
if (val !== undefined && val !== null) {
|
|
113
|
+
const camelKey = normalizedKey.replace(/-([a-z])/g, (_, letter) =>
|
|
114
|
+
letter.toUpperCase()
|
|
115
|
+
);
|
|
116
|
+
(inlineStyles as any)[camelKey] = val;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!block.blocks || block.blocks.length === 0) {
|
|
123
|
+
return (
|
|
124
|
+
<NextLink
|
|
125
|
+
href={isDesigner ? '#' : formattedHref}
|
|
126
|
+
target={isDesigner ? undefined : target}
|
|
127
|
+
rel={isDesigner ? undefined : rel}
|
|
128
|
+
style={inlineStyles}
|
|
129
|
+
onClick={(e) => {
|
|
130
|
+
if (isDesigner) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<div style={{ padding: '20px', color: '#6b7280' }}>
|
|
136
|
+
Empty link block
|
|
137
|
+
</div>
|
|
138
|
+
</NextLink>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<NextLink
|
|
144
|
+
href={isDesigner ? '#' : formattedHref}
|
|
145
|
+
target={isDesigner ? undefined : target}
|
|
146
|
+
rel={isDesigner ? undefined : rel}
|
|
147
|
+
style={inlineStyles}
|
|
148
|
+
onClick={(e) => {
|
|
149
|
+
if (isDesigner) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
}
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{block.blocks
|
|
155
|
+
.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
|
|
156
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
157
|
+
.map((childBlock, index) => {
|
|
158
|
+
const createActionHandler = (actionType: string) => () => {
|
|
159
|
+
if (window.parent) {
|
|
160
|
+
window.parent.postMessage(
|
|
161
|
+
{
|
|
162
|
+
type: actionType,
|
|
163
|
+
data: {
|
|
164
|
+
placeholderId,
|
|
165
|
+
sectionId,
|
|
166
|
+
blockId: childBlock.id
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
'*'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const handleRename = (newLabel: string) => {
|
|
175
|
+
if (window.parent) {
|
|
176
|
+
window.parent.postMessage(
|
|
177
|
+
{
|
|
178
|
+
type: 'RENAME_BLOCK',
|
|
179
|
+
data: {
|
|
180
|
+
placeholderId,
|
|
181
|
+
sectionId,
|
|
182
|
+
blockId: childBlock.id,
|
|
183
|
+
label: newLabel
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
'*'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<ThemeBlock
|
|
193
|
+
key={childBlock.id || `block-${index}`}
|
|
194
|
+
block={childBlock}
|
|
195
|
+
placeholderId={placeholderId}
|
|
196
|
+
sectionId={sectionId}
|
|
197
|
+
isDesigner={isDesigner}
|
|
198
|
+
isSelected={selectedBlockId === childBlock.id}
|
|
199
|
+
selectedBlockId={selectedBlockId}
|
|
200
|
+
currentBreakpoint={currentBreakpoint}
|
|
201
|
+
onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
|
|
202
|
+
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
|
|
203
|
+
onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
|
|
204
|
+
onToggleVisibility={createActionHandler(
|
|
205
|
+
'TOGGLE_BLOCK_VISIBILITY'
|
|
206
|
+
)}
|
|
207
|
+
onDelete={createActionHandler('DELETE_BLOCK')}
|
|
208
|
+
onRename={handleRename}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
</NextLink>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default LinkBlock;
|