@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.
Files changed (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
@@ -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;