@akinon/next 2.0.0-beta.19 → 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/CHANGELOG.md +20 -13
- package/assets/styles/index.scss +84 -0
- package/components/client-root.tsx +107 -1
- package/components/link.tsx +46 -16
- 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/misc.ts +13 -1
- package/data/server/widget.ts +68 -1
- package/data/urls.ts +3 -1
- package/hooks/use-router.ts +53 -19
- package/lib/cache.ts +1 -0
- package/package.json +4 -2
- package/redux/reducers/index.ts +2 -0
- package/redux/reducers/widget.ts +80 -0
- package/types/commerce/widget.ts +33 -0
- package/types/widget.ts +80 -0
- package/utils/widget-styles.ts +107 -0
- package/with-pz-config.js +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import ThemeBlock, { Block } from '../theme-block';
|
|
6
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
7
|
+
import { getCSSStyles, getResponsiveValue } from '../utils';
|
|
8
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
9
|
+
|
|
10
|
+
type CounterUnit = 'days' | 'hours' | 'minutes' | 'seconds';
|
|
11
|
+
|
|
12
|
+
type CounterValues = Record<CounterUnit, string>;
|
|
13
|
+
|
|
14
|
+
type OnCompleteAction = 'none' | 'hide_section' | 'hide_self';
|
|
15
|
+
|
|
16
|
+
const pad2 = (n: number) => String(Math.max(0, n)).padStart(2, '0');
|
|
17
|
+
|
|
18
|
+
const parseEndAt = (raw: unknown): Date | null => {
|
|
19
|
+
if (raw === undefined || raw === null) return null;
|
|
20
|
+
|
|
21
|
+
if (raw instanceof Date) {
|
|
22
|
+
return isNaN(raw.getTime()) ? null : raw;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof raw === 'number') {
|
|
26
|
+
// Heuristic: seconds vs milliseconds
|
|
27
|
+
const ms = raw < 10_000_000_000 ? raw * 1000 : raw;
|
|
28
|
+
const d = new Date(ms);
|
|
29
|
+
return isNaN(d.getTime()) ? null : d;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof raw !== 'string') return null;
|
|
33
|
+
const trimmed = raw.trim();
|
|
34
|
+
if (!trimmed) return null;
|
|
35
|
+
|
|
36
|
+
// If numeric string, treat as timestamp
|
|
37
|
+
if (/^\d+$/.test(trimmed)) {
|
|
38
|
+
const num = Number(trimmed);
|
|
39
|
+
if (!Number.isFinite(num)) return null;
|
|
40
|
+
const ms = num < 10_000_000_000 ? num * 1000 : num;
|
|
41
|
+
const d = new Date(ms);
|
|
42
|
+
return isNaN(d.getTime()) ? null : d;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ISO or Date-parsable string
|
|
46
|
+
const d = new Date(trimmed);
|
|
47
|
+
return isNaN(d.getTime()) ? null : d;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const computeRemaining = (
|
|
51
|
+
endAt: Date | null
|
|
52
|
+
): { done: boolean; values: CounterValues } => {
|
|
53
|
+
if (!endAt) {
|
|
54
|
+
return {
|
|
55
|
+
done: false,
|
|
56
|
+
values: { days: '00', hours: '00', minutes: '00', seconds: '00' }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const diffMs = endAt.getTime() - Date.now();
|
|
61
|
+
if (diffMs <= 0) {
|
|
62
|
+
return {
|
|
63
|
+
done: true,
|
|
64
|
+
values: { days: '00', hours: '00', minutes: '00', seconds: '00' }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const totalSeconds = Math.floor(diffMs / 1000);
|
|
69
|
+
const days = Math.floor(totalSeconds / (60 * 60 * 24));
|
|
70
|
+
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
|
|
71
|
+
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
|
|
72
|
+
const seconds = totalSeconds % 60;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
done: false,
|
|
76
|
+
values: {
|
|
77
|
+
days: pad2(days),
|
|
78
|
+
hours: pad2(hours),
|
|
79
|
+
minutes: pad2(minutes),
|
|
80
|
+
seconds: pad2(seconds)
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const cloneAndInjectCounterValues = (
|
|
86
|
+
blocks: Block[],
|
|
87
|
+
counter: CounterValues
|
|
88
|
+
): Block[] => {
|
|
89
|
+
const walk = (b: Block): Block => {
|
|
90
|
+
const next: Block = {
|
|
91
|
+
...b,
|
|
92
|
+
properties: b.properties ? { ...b.properties } : b.properties,
|
|
93
|
+
styles: b.styles ? { ...b.styles } : b.styles
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const unit = next.properties?.counterUnit as CounterUnit | undefined;
|
|
97
|
+
const role = next.properties?.counterRole as string | undefined;
|
|
98
|
+
|
|
99
|
+
if (next.type === 'text' && unit && role === 'value') {
|
|
100
|
+
next.value = counter[unit];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (next.blocks && next.blocks.length > 0) {
|
|
104
|
+
next.blocks = next.blocks.map(walk);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return next;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return blocks.map(walk);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const CounterBlock = ({
|
|
114
|
+
block,
|
|
115
|
+
placeholderId,
|
|
116
|
+
sectionId,
|
|
117
|
+
isDesigner,
|
|
118
|
+
selectedBlockId,
|
|
119
|
+
currentBreakpoint = 'desktop'
|
|
120
|
+
}: BlockRendererProps) => {
|
|
121
|
+
const themeSettings = useThemeSettingsContext();
|
|
122
|
+
|
|
123
|
+
const endAtRaw = useMemo(() => {
|
|
124
|
+
const raw = getResponsiveValue(
|
|
125
|
+
block.properties?.endAt,
|
|
126
|
+
currentBreakpoint,
|
|
127
|
+
''
|
|
128
|
+
);
|
|
129
|
+
return raw;
|
|
130
|
+
}, [block.properties?.endAt, currentBreakpoint]);
|
|
131
|
+
|
|
132
|
+
const endAt = useMemo(() => parseEndAt(endAtRaw), [endAtRaw]);
|
|
133
|
+
|
|
134
|
+
const onCompleteAction = useMemo(() => {
|
|
135
|
+
const raw = getResponsiveValue(
|
|
136
|
+
block.properties?.onCompleteAction,
|
|
137
|
+
currentBreakpoint,
|
|
138
|
+
'none'
|
|
139
|
+
);
|
|
140
|
+
return (raw as OnCompleteAction) || 'none';
|
|
141
|
+
}, [block.properties?.onCompleteAction, currentBreakpoint]);
|
|
142
|
+
|
|
143
|
+
const [{ done, values }, setRemaining] = useState(() =>
|
|
144
|
+
computeRemaining(endAt)
|
|
145
|
+
);
|
|
146
|
+
const didRunActionRef = useRef(false);
|
|
147
|
+
const didAutoShowRef = useRef(false);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
didRunActionRef.current = false;
|
|
151
|
+
setRemaining(computeRemaining(endAt));
|
|
152
|
+
|
|
153
|
+
if (!endAt) return;
|
|
154
|
+
|
|
155
|
+
const tick = () => setRemaining(computeRemaining(endAt));
|
|
156
|
+
const intervalId = window.setInterval(tick, 1000);
|
|
157
|
+
tick();
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
window.clearInterval(intervalId);
|
|
161
|
+
};
|
|
162
|
+
}, [endAt]);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!done) return;
|
|
166
|
+
if (didRunActionRef.current) return;
|
|
167
|
+
|
|
168
|
+
didRunActionRef.current = true;
|
|
169
|
+
|
|
170
|
+
if (onCompleteAction === 'hide_self') {
|
|
171
|
+
// In designer (iframe) mode: use editor's show/hide so the user can still select/manage it
|
|
172
|
+
if (isDesigner && window.parent && window.parent !== window) {
|
|
173
|
+
window.parent.postMessage(
|
|
174
|
+
{
|
|
175
|
+
type: 'SET_BLOCK_VISIBILITY',
|
|
176
|
+
data: {
|
|
177
|
+
placeholderId,
|
|
178
|
+
sectionId,
|
|
179
|
+
blockId: block.id,
|
|
180
|
+
hidden: true
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
'*'
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Production: hide this block's wrapper in DOM
|
|
189
|
+
const blockEl = document.querySelector(
|
|
190
|
+
`[data-block-id="${CSS.escape(block.id)}"]`
|
|
191
|
+
) as HTMLElement | null;
|
|
192
|
+
if (blockEl) {
|
|
193
|
+
blockEl.style.display = 'none';
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (onCompleteAction === 'hide_section') {
|
|
199
|
+
// In designer (iframe) mode: use editor's show/hide
|
|
200
|
+
if (isDesigner && window.parent && window.parent !== window) {
|
|
201
|
+
window.parent.postMessage(
|
|
202
|
+
{
|
|
203
|
+
type: 'SET_SECTION_VISIBILITY',
|
|
204
|
+
data: {
|
|
205
|
+
placeholderId,
|
|
206
|
+
sectionId,
|
|
207
|
+
hidden: true
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
'*'
|
|
211
|
+
);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Production: hide section in DOM
|
|
216
|
+
const sectionEl = document.querySelector(
|
|
217
|
+
`[data-section-id="${CSS.escape(sectionId)}"]`
|
|
218
|
+
) as HTMLElement | null;
|
|
219
|
+
|
|
220
|
+
if (sectionEl) {
|
|
221
|
+
sectionEl.style.display = 'none';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}, [block.id, done, isDesigner, onCompleteAction, placeholderId, sectionId]);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
// If the counter was hidden (likely by a previous completion), but endAt is now in the future,
|
|
228
|
+
// ensure it becomes visible again in designer mode.
|
|
229
|
+
if (!isDesigner || !window.parent || window.parent === window) return;
|
|
230
|
+
if (onCompleteAction !== 'hide_self') return;
|
|
231
|
+
if (!endAt) return;
|
|
232
|
+
if (done) {
|
|
233
|
+
didAutoShowRef.current = false;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!block.hidden) {
|
|
238
|
+
didAutoShowRef.current = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (didAutoShowRef.current) return;
|
|
243
|
+
didAutoShowRef.current = true;
|
|
244
|
+
|
|
245
|
+
window.parent.postMessage(
|
|
246
|
+
{
|
|
247
|
+
type: 'SET_BLOCK_VISIBILITY',
|
|
248
|
+
data: {
|
|
249
|
+
placeholderId,
|
|
250
|
+
sectionId,
|
|
251
|
+
blockId: block.id,
|
|
252
|
+
hidden: false
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
'*'
|
|
256
|
+
);
|
|
257
|
+
}, [
|
|
258
|
+
block.hidden,
|
|
259
|
+
block.id,
|
|
260
|
+
done,
|
|
261
|
+
endAt,
|
|
262
|
+
isDesigner,
|
|
263
|
+
onCompleteAction,
|
|
264
|
+
placeholderId,
|
|
265
|
+
sectionId
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const allStyles = getCSSStyles(
|
|
269
|
+
block.styles,
|
|
270
|
+
themeSettings,
|
|
271
|
+
currentBreakpoint
|
|
272
|
+
);
|
|
273
|
+
const { position, top, right, bottom, left, zIndex, ...innerStyles } =
|
|
274
|
+
allStyles;
|
|
275
|
+
|
|
276
|
+
if (!block.blocks || block.blocks.length === 0) {
|
|
277
|
+
return (
|
|
278
|
+
<div style={{ padding: '20px', color: '#6b7280' }}>Empty counter</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const renderedBlocks = cloneAndInjectCounterValues(
|
|
283
|
+
block.blocks
|
|
284
|
+
.filter((b) => !b.hidden)
|
|
285
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0)),
|
|
286
|
+
values
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div style={innerStyles}>
|
|
291
|
+
{renderedBlocks.map((childBlock) => {
|
|
292
|
+
const createActionHandler = (actionType: string) => () => {
|
|
293
|
+
if (window.parent) {
|
|
294
|
+
window.parent.postMessage(
|
|
295
|
+
{
|
|
296
|
+
type: actionType,
|
|
297
|
+
data: {
|
|
298
|
+
placeholderId,
|
|
299
|
+
sectionId,
|
|
300
|
+
blockId: childBlock.id
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
'*'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const handleRename = (newLabel: string) => {
|
|
309
|
+
if (window.parent) {
|
|
310
|
+
window.parent.postMessage(
|
|
311
|
+
{
|
|
312
|
+
type: 'RENAME_BLOCK',
|
|
313
|
+
data: {
|
|
314
|
+
placeholderId,
|
|
315
|
+
sectionId,
|
|
316
|
+
blockId: childBlock.id,
|
|
317
|
+
label: newLabel
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
'*'
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<ThemeBlock
|
|
327
|
+
key={childBlock.id}
|
|
328
|
+
block={childBlock}
|
|
329
|
+
placeholderId={placeholderId}
|
|
330
|
+
sectionId={sectionId}
|
|
331
|
+
isDesigner={isDesigner}
|
|
332
|
+
isSelected={selectedBlockId === childBlock.id}
|
|
333
|
+
selectedBlockId={selectedBlockId}
|
|
334
|
+
currentBreakpoint={currentBreakpoint}
|
|
335
|
+
onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
|
|
336
|
+
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
|
|
337
|
+
onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
|
|
338
|
+
onToggleVisibility={createActionHandler('TOGGLE_BLOCK_VISIBILITY')}
|
|
339
|
+
onDelete={createActionHandler('DELETE_BLOCK')}
|
|
340
|
+
onRename={handleRename}
|
|
341
|
+
/>
|
|
342
|
+
);
|
|
343
|
+
})}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export default CounterBlock;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getResponsiveValue } from '../utils';
|
|
3
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
4
|
+
|
|
5
|
+
const DividerBlock = ({ block, currentBreakpoint = 'desktop' }: BlockRendererProps) => {
|
|
6
|
+
const spanStyles = {
|
|
7
|
+
display: 'block',
|
|
8
|
+
height: getResponsiveValue(block.styles?.height, currentBreakpoint, '1px'),
|
|
9
|
+
backgroundColor: getResponsiveValue(
|
|
10
|
+
block.styles?.['background-color'],
|
|
11
|
+
currentBreakpoint,
|
|
12
|
+
'#e0e0e0'
|
|
13
|
+
),
|
|
14
|
+
width: getResponsiveValue(block.styles?.width, currentBreakpoint, '100%')
|
|
15
|
+
} as React.CSSProperties;
|
|
16
|
+
|
|
17
|
+
return <span style={spanStyles} />;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default DividerBlock;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
3
|
+
|
|
4
|
+
// Platform-specific embed URL converters
|
|
5
|
+
const getEmbedUrl = (url: string): string | null => {
|
|
6
|
+
if (!url) return null;
|
|
7
|
+
|
|
8
|
+
// Twitter/X
|
|
9
|
+
if (url.includes('twitter.com') || url.includes('x.com')) {
|
|
10
|
+
const tweetId = url.match(/status\/(\d+)/)?.[1];
|
|
11
|
+
if (tweetId) {
|
|
12
|
+
return `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Spotify
|
|
17
|
+
if (url.includes('spotify.com')) {
|
|
18
|
+
const spotifyMatch = url.match(/spotify\.com\/(track|playlist|album|episode)\/([a-zA-Z0-9]+)/);
|
|
19
|
+
if (spotifyMatch) {
|
|
20
|
+
const [, type, id] = spotifyMatch;
|
|
21
|
+
return `https://open.spotify.com/embed/${type}/${id}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CodePen
|
|
26
|
+
if (url.includes('codepen.io')) {
|
|
27
|
+
const penMatch = url.match(/codepen\.io\/([^/]+)\/pen\/([^/?]+)/);
|
|
28
|
+
if (penMatch) {
|
|
29
|
+
const [, user, penId] = penMatch;
|
|
30
|
+
return `https://codepen.io/${user}/embed/${penId}?default-tab=result`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Instagram
|
|
35
|
+
if (url.includes('instagram.com')) {
|
|
36
|
+
const postMatch = url.match(/instagram\.com\/(p|reel)\/([^/?]+)/);
|
|
37
|
+
if (postMatch) {
|
|
38
|
+
const [, type, id] = postMatch;
|
|
39
|
+
return `https://www.instagram.com/${type}/${id}/embed`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// TikTok
|
|
44
|
+
if (url.includes('tiktok.com')) {
|
|
45
|
+
const videoMatch = url.match(/tiktok\.com\/.*\/video\/(\d+)/);
|
|
46
|
+
if (videoMatch) {
|
|
47
|
+
const [, videoId] = videoMatch;
|
|
48
|
+
return `https://www.tiktok.com/embed/${videoId}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SoundCloud
|
|
53
|
+
if (url.includes('soundcloud.com')) {
|
|
54
|
+
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(url)}&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Google Forms
|
|
58
|
+
if (url.includes('docs.google.com/forms')) {
|
|
59
|
+
return url.replace('/viewform', '/viewform?embedded=true');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Google Sheets
|
|
63
|
+
if (url.includes('docs.google.com/spreadsheets')) {
|
|
64
|
+
return url.replace('/edit', '/preview');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Figma
|
|
68
|
+
if (url.includes('figma.com')) {
|
|
69
|
+
const fileMatch = url.match(/figma\.com\/file\/([^/]+)/);
|
|
70
|
+
if (fileMatch) {
|
|
71
|
+
return `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Calendly
|
|
76
|
+
if (url.includes('calendly.com')) {
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Typeform
|
|
81
|
+
if (url.includes('typeform.com')) {
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If URL looks like an iframe src, use it directly
|
|
86
|
+
if (url.startsWith('https://') || url.startsWith('http://')) {
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Extract iframe src from raw iframe code
|
|
94
|
+
const extractIframeSrc = (code: string): string | null => {
|
|
95
|
+
const srcMatch = code.match(/src=["']([^"']+)["']/);
|
|
96
|
+
return srcMatch ? srcMatch[1] : null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const EmbedBlock = ({ block }: BlockRendererProps) => {
|
|
100
|
+
const embedInput =
|
|
101
|
+
typeof block.value === 'object' && block.value !== null
|
|
102
|
+
? (block.value as Record<string, string>).en ||
|
|
103
|
+
(block.value as Record<string, string>).tr ||
|
|
104
|
+
Object.values(block.value as Record<string, string>)[0] ||
|
|
105
|
+
''
|
|
106
|
+
: (block.value as string) || '';
|
|
107
|
+
|
|
108
|
+
const properties = block.properties || {};
|
|
109
|
+
|
|
110
|
+
const resolveValue = <T,>(val: unknown, fallback: T): T => {
|
|
111
|
+
if (val && typeof val === 'object' && 'desktop' in (val as Record<string, unknown>)) {
|
|
112
|
+
return ((val as Record<string, T>).desktop as T) ?? fallback;
|
|
113
|
+
}
|
|
114
|
+
return (val as T) ?? fallback;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const title = resolveValue<string>(properties.title, '');
|
|
118
|
+
const allowFullscreen = resolveValue<boolean>(properties.allowFullscreen, true);
|
|
119
|
+
const loading = resolveValue<string>(properties.loading, 'lazy');
|
|
120
|
+
|
|
121
|
+
// Try to extract iframe src if user pasted full iframe code
|
|
122
|
+
let embedUrl: string | null = null;
|
|
123
|
+
if (embedInput.includes('<iframe')) {
|
|
124
|
+
embedUrl = extractIframeSrc(embedInput);
|
|
125
|
+
} else {
|
|
126
|
+
embedUrl = getEmbedUrl(embedInput);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!embedInput) {
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
style={{
|
|
133
|
+
minHeight: '300px',
|
|
134
|
+
display: 'flex',
|
|
135
|
+
flexDirection: 'column',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
justifyContent: 'center',
|
|
138
|
+
backgroundColor: '#f5f5f5',
|
|
139
|
+
color: '#666',
|
|
140
|
+
fontSize: '14px',
|
|
141
|
+
gap: '8px'
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<svg
|
|
145
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
146
|
+
width="32"
|
|
147
|
+
height="32"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
strokeWidth="1.5"
|
|
152
|
+
strokeLinecap="round"
|
|
153
|
+
strokeLinejoin="round"
|
|
154
|
+
style={{ opacity: 0.4 }}
|
|
155
|
+
>
|
|
156
|
+
<polyline points="16 18 22 12 16 6" />
|
|
157
|
+
<polyline points="8 6 2 12 8 18" />
|
|
158
|
+
</svg>
|
|
159
|
+
<span>Enter an embed URL or paste iframe code</span>
|
|
160
|
+
<span style={{ fontSize: '12px', opacity: 0.6 }}>
|
|
161
|
+
Supported: Twitter, Spotify, CodePen, Instagram, TikTok, Google Forms, and more
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!embedUrl) {
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
style={{
|
|
171
|
+
minHeight: '300px',
|
|
172
|
+
display: 'flex',
|
|
173
|
+
flexDirection: 'column',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'center',
|
|
176
|
+
backgroundColor: '#fef2f2',
|
|
177
|
+
color: '#dc2626',
|
|
178
|
+
fontSize: '14px',
|
|
179
|
+
gap: '8px'
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<span>⚠ Invalid embed URL or code</span>
|
|
183
|
+
<span style={{ fontSize: '12px', opacity: 0.8 }}>
|
|
184
|
+
Please check the URL format or paste a valid iframe code
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<iframe
|
|
192
|
+
src={embedUrl}
|
|
193
|
+
title={title || 'Embedded content'}
|
|
194
|
+
style={{
|
|
195
|
+
width: '100%',
|
|
196
|
+
height: '100%',
|
|
197
|
+
border: 'none',
|
|
198
|
+
minHeight: '300px'
|
|
199
|
+
}}
|
|
200
|
+
loading={loading as 'lazy' | 'eager' | undefined}
|
|
201
|
+
allowFullScreen={allowFullscreen}
|
|
202
|
+
referrerPolicy="no-referrer-when-downgrade"
|
|
203
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default EmbedBlock;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getResponsiveValue } from '../utils';
|
|
3
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
4
|
+
import ThemeBlock from '../theme-block';
|
|
5
|
+
|
|
6
|
+
const GroupBlock = ({
|
|
7
|
+
block,
|
|
8
|
+
placeholderId,
|
|
9
|
+
sectionId,
|
|
10
|
+
isDesigner,
|
|
11
|
+
selectedBlockId,
|
|
12
|
+
currentBreakpoint = 'desktop'
|
|
13
|
+
}: BlockRendererProps) => {
|
|
14
|
+
const tag = getResponsiveValue(block.properties?.tag, 'desktop', 'div');
|
|
15
|
+
const Tag = tag as keyof JSX.IntrinsicElements;
|
|
16
|
+
const href = tag === 'a' ? block.properties?.href : undefined;
|
|
17
|
+
|
|
18
|
+
const tagProps: Record<string, unknown> = {
|
|
19
|
+
className: 'contents'
|
|
20
|
+
};
|
|
21
|
+
if (tag === 'a' && href) {
|
|
22
|
+
tagProps.href = href;
|
|
23
|
+
}
|
|
24
|
+
if (tag === 'form') {
|
|
25
|
+
tagProps.onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
|
|
28
|
+
const submitButton = event.currentTarget.querySelector(
|
|
29
|
+
'button[type="submit"]'
|
|
30
|
+
) as HTMLButtonElement | null;
|
|
31
|
+
|
|
32
|
+
submitButton?.click();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const shippingRole = String(
|
|
37
|
+
getResponsiveValue(block.properties?.shippingRole, currentBreakpoint, '')
|
|
38
|
+
);
|
|
39
|
+
const isVisualUtilityGroup =
|
|
40
|
+
shippingRole === 'progress-fill' || shippingRole === 'progress-marker';
|
|
41
|
+
|
|
42
|
+
if (!block.blocks || block.blocks.length === 0) {
|
|
43
|
+
if (isVisualUtilityGroup) {
|
|
44
|
+
return <Tag {...tagProps} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ padding: '20px', color: '#6b7280' }}>Empty group block</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Tag {...tagProps}>
|
|
54
|
+
{block.blocks
|
|
55
|
+
.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
|
|
56
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
57
|
+
.map((childBlock, index) => {
|
|
58
|
+
const createActionHandler = (actionType: string) => () => {
|
|
59
|
+
if (window.parent) {
|
|
60
|
+
window.parent.postMessage(
|
|
61
|
+
{
|
|
62
|
+
type: actionType,
|
|
63
|
+
data: {
|
|
64
|
+
placeholderId,
|
|
65
|
+
sectionId,
|
|
66
|
+
blockId: childBlock.id
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
'*'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleRename = (newLabel: string) => {
|
|
75
|
+
if (window.parent) {
|
|
76
|
+
window.parent.postMessage(
|
|
77
|
+
{
|
|
78
|
+
type: 'RENAME_BLOCK',
|
|
79
|
+
data: {
|
|
80
|
+
placeholderId,
|
|
81
|
+
sectionId,
|
|
82
|
+
blockId: childBlock.id,
|
|
83
|
+
label: newLabel
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
'*'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ThemeBlock
|
|
93
|
+
key={childBlock.id || `block-${index}`}
|
|
94
|
+
block={childBlock}
|
|
95
|
+
placeholderId={placeholderId}
|
|
96
|
+
sectionId={sectionId}
|
|
97
|
+
isDesigner={isDesigner}
|
|
98
|
+
isSelected={selectedBlockId === childBlock.id}
|
|
99
|
+
selectedBlockId={selectedBlockId}
|
|
100
|
+
currentBreakpoint={currentBreakpoint}
|
|
101
|
+
onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
|
|
102
|
+
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
|
|
103
|
+
onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
|
|
104
|
+
onToggleVisibility={createActionHandler(
|
|
105
|
+
'TOGGLE_BLOCK_VISIBILITY'
|
|
106
|
+
)}
|
|
107
|
+
onDelete={createActionHandler('DELETE_BLOCK')}
|
|
108
|
+
onRename={handleRename}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</Tag>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default GroupBlock;
|