@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +27 -0
  3. package/readme.md +23 -0
  4. package/src/blocks/accordion-block.tsx +136 -0
  5. package/src/blocks/block-renderer-registry.tsx +77 -0
  6. package/src/blocks/button-block.tsx +593 -0
  7. package/src/blocks/counter-block.tsx +348 -0
  8. package/src/blocks/divider-block.tsx +20 -0
  9. package/src/blocks/embed-block.tsx +208 -0
  10. package/src/blocks/group-block.tsx +116 -0
  11. package/src/blocks/hotspot-block.tsx +147 -0
  12. package/src/blocks/icon-block.tsx +230 -0
  13. package/src/blocks/image-block.tsx +142 -0
  14. package/src/blocks/image-gallery-block.tsx +269 -0
  15. package/src/blocks/input-block.tsx +123 -0
  16. package/src/blocks/link-block.tsx +216 -0
  17. package/src/blocks/lottie-block.tsx +325 -0
  18. package/src/blocks/map-block.tsx +89 -0
  19. package/src/blocks/slider-block.tsx +595 -0
  20. package/src/blocks/tab-block.tsx +10 -0
  21. package/src/blocks/text-block.tsx +52 -0
  22. package/src/blocks/video-block.tsx +122 -0
  23. package/src/components/action-toolbar.tsx +305 -0
  24. package/src/components/designer-overlay.tsx +74 -0
  25. package/src/components/with-designer-features.tsx +142 -0
  26. package/src/dynamic-font-loader.tsx +79 -0
  27. package/src/hooks/use-designer-features.tsx +100 -0
  28. package/src/hooks/use-visibility-context.ts +27 -0
  29. package/src/index.ts +21 -0
  30. package/src/placeholder-registry.ts +31 -0
  31. package/src/sections/before-after-section.tsx +245 -0
  32. package/src/sections/contact-form-section.tsx +564 -0
  33. package/src/sections/countdown-campaign-banner-section.tsx +433 -0
  34. package/src/sections/coupon-banner-section.tsx +710 -0
  35. package/src/sections/divider-section.tsx +62 -0
  36. package/src/sections/featured-product-spotlight-section.tsx +507 -0
  37. package/src/sections/find-in-store-section.tsx +1995 -0
  38. package/src/sections/hover-showcase-section.tsx +326 -0
  39. package/src/sections/image-hotspot-section.tsx +142 -0
  40. package/src/sections/installment-options-section.tsx +1065 -0
  41. package/src/sections/notification-banner-section.tsx +173 -0
  42. package/src/sections/order-tracking-lookup-section.tsx +1379 -0
  43. package/src/sections/posts-slider-section.tsx +472 -0
  44. package/src/sections/pre-order-launch-banner-section.tsx +687 -0
  45. package/src/sections/section-renderer-registry.tsx +89 -0
  46. package/src/sections/section-wrapper.tsx +135 -0
  47. package/src/sections/shipping-threshold-progress-section.tsx +586 -0
  48. package/src/sections/stats-counter-section.tsx +486 -0
  49. package/src/sections/tabs-section.tsx +578 -0
  50. package/src/theme-block.tsx +102 -0
  51. package/src/theme-page-context.tsx +27 -0
  52. package/src/theme-placeholder-client.tsx +218 -0
  53. package/src/theme-placeholder-wrapper.tsx +786 -0
  54. package/src/theme-placeholder.tsx +305 -0
  55. package/src/theme-section.tsx +1241 -0
  56. package/src/theme-settings-context.tsx +13 -0
  57. package/src/utils/index.ts +791 -0
  58. package/src/utils/iterator-utils.test.ts +224 -0
  59. package/src/utils/iterator-utils.ts +617 -0
  60. package/src/utils/page-context-discovery.ts +119 -0
  61. package/src/utils/publish-window.ts +86 -0
  62. package/src/utils/visibility-rules.ts +188 -0
@@ -0,0 +1,687 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
5
+
6
+ import ThemeBlock, { Block } from '../theme-block';
7
+ import { useThemeSettingsContext } from '../theme-settings-context';
8
+ import { Section } from '../theme-section';
9
+ import { getCSSStyles, getResponsiveValue } from '../utils';
10
+
11
+ interface PreOrderLaunchBannerSectionProps {
12
+ section: Section;
13
+ currentBreakpoint?: string;
14
+ placeholderId?: string;
15
+ isDesigner?: boolean;
16
+ selectedBlockId?: string | null;
17
+ }
18
+
19
+ type CountdownUnit = 'days' | 'hours' | 'minutes' | 'seconds';
20
+
21
+ type CountdownValues = Record<CountdownUnit, string>;
22
+
23
+ const parseBoolean = (value: unknown, fallback: boolean): boolean => {
24
+ if (typeof value === 'boolean') return value;
25
+ if (typeof value === 'string') {
26
+ const normalized = value.trim().toLowerCase();
27
+ if (normalized === 'true') return true;
28
+ if (normalized === 'false') return false;
29
+ }
30
+ if (typeof value === 'number') return value !== 0;
31
+ return fallback;
32
+ };
33
+
34
+ const resolvePath = (
35
+ source: Record<string, unknown> | undefined,
36
+ path: string
37
+ ): unknown => {
38
+ if (!source || !path) return undefined;
39
+
40
+ const normalizedPath = path.replace(/^item\./, '');
41
+ const pathParts = normalizedPath.split('.');
42
+ let value: unknown = source;
43
+
44
+ for (const part of pathParts) {
45
+ const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
46
+ if (arrayMatch) {
47
+ const [, arrayKey, indexText] = arrayMatch;
48
+ const arrayValue = (value as Record<string, unknown>)?.[arrayKey];
49
+ value = Array.isArray(arrayValue)
50
+ ? arrayValue[Number(indexText)]
51
+ : undefined;
52
+ } else {
53
+ value = (value as Record<string, unknown>)?.[part];
54
+ }
55
+
56
+ if (value === undefined || value === null) {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ return value;
62
+ };
63
+
64
+ const parsePriceValue = (value: unknown): number | null => {
65
+ if (typeof value === 'number' && Number.isFinite(value)) {
66
+ return value;
67
+ }
68
+
69
+ if (typeof value !== 'string') return null;
70
+
71
+ const cleaned = value.trim().replace(/[^\d,.-]/g, '');
72
+ if (!cleaned) return null;
73
+
74
+ let normalized = cleaned;
75
+ const hasComma = normalized.includes(',');
76
+ const hasDot = normalized.includes('.');
77
+
78
+ if (hasComma && hasDot) {
79
+ const lastComma = normalized.lastIndexOf(',');
80
+ const lastDot = normalized.lastIndexOf('.');
81
+ normalized =
82
+ lastComma > lastDot
83
+ ? normalized.replace(/\./g, '').replace(',', '.')
84
+ : normalized.replace(/,/g, '');
85
+ } else if (hasComma) {
86
+ const unsigned = normalized.replace(/^-/, '');
87
+ const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
88
+ normalized = isThousandsPattern
89
+ ? normalized.replace(/,/g, '')
90
+ : normalized.replace(/,/g, '.');
91
+ }
92
+
93
+ const parsed = Number(normalized);
94
+ return Number.isFinite(parsed) ? parsed : null;
95
+ };
96
+
97
+ const parsePositiveInt = (value: unknown): number | null => {
98
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
99
+ return value;
100
+ }
101
+
102
+ if (typeof value === 'string') {
103
+ const trimmed = value.trim();
104
+ if (!/^\d+$/.test(trimmed)) return null;
105
+ const parsed = Number.parseInt(trimmed, 10);
106
+ return parsed > 0 ? parsed : null;
107
+ }
108
+
109
+ return null;
110
+ };
111
+
112
+ const normalizeCurrencyLabel = (currency: unknown): string => {
113
+ const raw = String(currency || '')
114
+ .trim()
115
+ .toUpperCase();
116
+ if (!raw) return 'TL';
117
+
118
+ const map: Record<string, string> = {
119
+ TRY: 'TL',
120
+ TL: 'TL',
121
+ USD: 'USD',
122
+ EUR: 'EUR',
123
+ GBP: 'GBP'
124
+ };
125
+
126
+ return map[raw] || raw;
127
+ };
128
+
129
+ const formatPriceWithCurrency = (
130
+ value: unknown,
131
+ currency: unknown
132
+ ): string | null => {
133
+ if (value == null || value === '') return null;
134
+
135
+ const price = parsePriceValue(value);
136
+ if (price === null) {
137
+ const text = String(value).trim();
138
+ return text || null;
139
+ }
140
+
141
+ const hasDecimals = Math.abs(price % 1) > 0.00001;
142
+ const formatted = price.toLocaleString('tr-TR', {
143
+ minimumFractionDigits: hasDecimals ? 2 : 0,
144
+ maximumFractionDigits: 2
145
+ });
146
+
147
+ return `${formatted} ${normalizeCurrencyLabel(currency)}`;
148
+ };
149
+
150
+ const parseDateValue = (value: unknown): Date | null => {
151
+ if (value === undefined || value === null || value === '') return null;
152
+
153
+ if (value instanceof Date) {
154
+ return Number.isNaN(value.getTime()) ? null : value;
155
+ }
156
+
157
+ if (typeof value === 'number') {
158
+ const timestamp = value < 10_000_000_000 ? value * 1000 : value;
159
+ const date = new Date(timestamp);
160
+ return Number.isNaN(date.getTime()) ? null : date;
161
+ }
162
+
163
+ if (typeof value !== 'string') return null;
164
+
165
+ const trimmed = value.trim();
166
+ if (!trimmed) return null;
167
+
168
+ if (/^\d+$/.test(trimmed)) {
169
+ const numeric = Number(trimmed);
170
+ if (!Number.isFinite(numeric)) return null;
171
+ const timestamp = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
172
+ const date = new Date(timestamp);
173
+ return Number.isNaN(date.getTime()) ? null : date;
174
+ }
175
+
176
+ const parsed = new Date(trimmed);
177
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
178
+ };
179
+
180
+ const formatTwoDigits = (value: number) =>
181
+ String(Math.max(0, value)).padStart(2, '0');
182
+
183
+ const getRemainingTime = (
184
+ launchDate: Date | null
185
+ ): { isLaunched: boolean; values: CountdownValues } => {
186
+ const emptyValues = {
187
+ days: '00',
188
+ hours: '00',
189
+ minutes: '00',
190
+ seconds: '00'
191
+ };
192
+
193
+ if (!launchDate) {
194
+ return { isLaunched: false, values: emptyValues };
195
+ }
196
+
197
+ const diffMs = launchDate.getTime() - Date.now();
198
+ if (diffMs <= 0) {
199
+ return { isLaunched: true, values: emptyValues };
200
+ }
201
+
202
+ const totalSeconds = Math.floor(diffMs / 1000);
203
+ const days = Math.floor(totalSeconds / (60 * 60 * 24));
204
+ const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
205
+ const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
206
+ const seconds = totalSeconds % 60;
207
+
208
+ return {
209
+ isLaunched: false,
210
+ values: {
211
+ days: formatTwoDigits(days),
212
+ hours: formatTwoDigits(hours),
213
+ minutes: formatTwoDigits(minutes),
214
+ seconds: formatTwoDigits(seconds)
215
+ }
216
+ };
217
+ };
218
+
219
+ const formatLaunchDate = (value: Date | null): string | null => {
220
+ if (!value) return null;
221
+
222
+ return new Intl.DateTimeFormat(undefined, {
223
+ month: 'long',
224
+ day: 'numeric',
225
+ year: 'numeric'
226
+ }).format(value);
227
+ };
228
+
229
+ const getCollectionProducts = (
230
+ section: Section,
231
+ isDesigner: boolean
232
+ ): Record<string, unknown>[] => {
233
+ const collectionDetails = section.dataSource?.details?.collection;
234
+ const staticData = section.dataSource?.details?.static?.data;
235
+ const isEditorMode =
236
+ typeof window !== 'undefined' && isDesigner && window.parent !== window;
237
+
238
+ const collectionPayload = isEditorMode
239
+ ? collectionDetails?.products || collectionDetails?.data
240
+ : collectionDetails?.data || collectionDetails?.products;
241
+
242
+ if (Array.isArray(collectionPayload)) {
243
+ return collectionPayload as Record<string, unknown>[];
244
+ }
245
+
246
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.products)) {
247
+ return (collectionPayload as Record<string, unknown>).products as Record<
248
+ string,
249
+ unknown
250
+ >[];
251
+ }
252
+
253
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.items)) {
254
+ return (collectionPayload as Record<string, unknown>).items as Record<
255
+ string,
256
+ unknown
257
+ >[];
258
+ }
259
+
260
+ if (Array.isArray(staticData)) {
261
+ return staticData as Record<string, unknown>[];
262
+ }
263
+
264
+ return [];
265
+ };
266
+
267
+ const getProductLaunchDate = (
268
+ product: Record<string, unknown> | undefined
269
+ ): Date | null => {
270
+ if (!product) return null;
271
+
272
+ const candidatePaths = [
273
+ 'pre_order.launch_date',
274
+ 'pre_order.release_date',
275
+ 'launch_date',
276
+ 'release_date',
277
+ 'expected_release_date',
278
+ 'expected_ship_date',
279
+ 'available_at',
280
+ 'extra_attributes.launch_date',
281
+ 'extra_attributes.release_date',
282
+ 'extra_attributes.expected_release_date',
283
+ 'extra_data.launch_date',
284
+ 'extra_data.release_date',
285
+ 'extra_data.expected_release_date'
286
+ ];
287
+
288
+ for (const path of candidatePaths) {
289
+ const resolved = resolvePath(product, path);
290
+ const parsed = parseDateValue(resolved);
291
+ if (parsed) {
292
+ return parsed;
293
+ }
294
+ }
295
+
296
+ return null;
297
+ };
298
+
299
+ export default function PreOrderLaunchBannerSection({
300
+ section,
301
+ currentBreakpoint = 'desktop',
302
+ placeholderId = '',
303
+ isDesigner = false,
304
+ selectedBlockId = null
305
+ }: PreOrderLaunchBannerSectionProps) {
306
+ const themeSettings = useThemeSettingsContext();
307
+
308
+ const maxWidth = getResponsiveValue(
309
+ section.styles?.['max-width'],
310
+ currentBreakpoint,
311
+ 'normal'
312
+ );
313
+ const maxWidthClass =
314
+ maxWidth === 'narrow'
315
+ ? 'max-w-4xl'
316
+ : maxWidth === 'normal'
317
+ ? 'max-w-7xl'
318
+ : maxWidth === 'full'
319
+ ? 'w-full'
320
+ : '';
321
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
322
+
323
+ const filteredStyles = Object.fromEntries(
324
+ Object.entries(section.styles || {}).filter(([key]) => key !== 'max-width')
325
+ );
326
+
327
+ const sectionStyles = useMemo<React.CSSProperties>(() => {
328
+ const baseStyles = getCSSStyles(
329
+ filteredStyles,
330
+ themeSettings,
331
+ currentBreakpoint
332
+ );
333
+ const reverseLayout = parseBoolean(
334
+ getResponsiveValue(section.properties?.reverse, currentBreakpoint, false),
335
+ false
336
+ );
337
+
338
+ if (!reverseLayout) {
339
+ return baseStyles;
340
+ }
341
+
342
+ return {
343
+ ...baseStyles,
344
+ flexDirection:
345
+ currentBreakpoint === 'mobile' ? 'column-reverse' : 'row-reverse'
346
+ };
347
+ }, [
348
+ currentBreakpoint,
349
+ filteredStyles,
350
+ section.properties?.reverse,
351
+ themeSettings
352
+ ]);
353
+
354
+ const products = useMemo(
355
+ () => getCollectionProducts(section, isDesigner),
356
+ [section, isDesigner]
357
+ );
358
+ const fallbackProductPk = parsePositiveInt(
359
+ getResponsiveValue(
360
+ section.properties?.['product-pk'],
361
+ currentBreakpoint,
362
+ ''
363
+ )
364
+ );
365
+ const { data: fallbackProductResponse } = useGetProductByPkQuery(
366
+ fallbackProductPk as number,
367
+ {
368
+ skip: products.length > 0 || !fallbackProductPk
369
+ }
370
+ );
371
+ const product =
372
+ products[0] ||
373
+ ((fallbackProductResponse?.product as
374
+ | Record<string, unknown>
375
+ | undefined) ??
376
+ undefined);
377
+
378
+ const propertyLaunchDate = getResponsiveValue(
379
+ section.properties?.['launch-date'],
380
+ currentBreakpoint,
381
+ ''
382
+ );
383
+ const launchDate = useMemo(
384
+ () => parseDateValue(propertyLaunchDate) || getProductLaunchDate(product),
385
+ [product, propertyLaunchDate]
386
+ );
387
+ const launchDateText = useMemo(
388
+ () => formatLaunchDate(launchDate),
389
+ [launchDate]
390
+ );
391
+
392
+ const showCountdown = parseBoolean(
393
+ getResponsiveValue(
394
+ section.properties?.['show-countdown'],
395
+ currentBreakpoint,
396
+ true
397
+ ),
398
+ true
399
+ );
400
+ const showPrice = parseBoolean(
401
+ getResponsiveValue(
402
+ section.properties?.['show-price'],
403
+ currentBreakpoint,
404
+ true
405
+ ),
406
+ true
407
+ );
408
+ const showOldPrice = parseBoolean(
409
+ getResponsiveValue(
410
+ section.properties?.['show-old-price'],
411
+ currentBreakpoint,
412
+ true
413
+ ),
414
+ true
415
+ );
416
+
417
+ const upcomingStatusText = String(
418
+ getResponsiveValue(
419
+ section.properties?.['upcoming-status-text'],
420
+ currentBreakpoint,
421
+ 'Pre-order open'
422
+ ) || 'Pre-order open'
423
+ );
424
+ const launchedStatusText = String(
425
+ getResponsiveValue(
426
+ section.properties?.['launched-status-text'],
427
+ currentBreakpoint,
428
+ 'Now shipping'
429
+ ) || 'Now shipping'
430
+ );
431
+
432
+ const [countdown, setCountdown] = useState(() =>
433
+ getRemainingTime(launchDate)
434
+ );
435
+
436
+ useEffect(() => {
437
+ const tick = () => setCountdown(getRemainingTime(launchDate));
438
+ tick();
439
+
440
+ if (!launchDate || !showCountdown) return;
441
+
442
+ const intervalId = window.setInterval(tick, 1000);
443
+ return () => window.clearInterval(intervalId);
444
+ }, [launchDate, showCountdown]);
445
+
446
+ const activePrice =
447
+ (product?.active_price as Record<string, unknown> | undefined) || {};
448
+ const currentPriceRaw = activePrice?.price ?? product?.price;
449
+ const retailPriceRaw = activePrice?.retail_price ?? product?.retail_price;
450
+ const currency =
451
+ activePrice?.currency_type ?? product?.currency_type ?? product?.currency;
452
+ const currentPrice = parsePriceValue(currentPriceRaw);
453
+ const retailPrice = parsePriceValue(retailPriceRaw);
454
+ const currentPriceText = formatPriceWithCurrency(currentPriceRaw, currency);
455
+ const retailPriceText = formatPriceWithCurrency(retailPriceRaw, currency);
456
+ const hasRetailPrice =
457
+ Boolean(retailPriceText) &&
458
+ retailPrice !== null &&
459
+ currentPrice !== null &&
460
+ retailPrice > currentPrice;
461
+
462
+ const normalizedBlocks = useMemo(() => {
463
+ const cloneWithProduct = (block: Block): Block | null => {
464
+ const normalizedLabel = String(block.label || '').toLowerCase();
465
+
466
+ if (
467
+ normalizedLabel === 'countdown group' &&
468
+ (!showCountdown || !launchDate)
469
+ ) {
470
+ return null;
471
+ }
472
+
473
+ if (normalizedLabel === 'price group' && !showPrice) {
474
+ return null;
475
+ }
476
+
477
+ if (
478
+ normalizedLabel === 'product old price' &&
479
+ (!showOldPrice || !hasRetailPrice)
480
+ ) {
481
+ return null;
482
+ }
483
+
484
+ const nextBlock: Block = {
485
+ ...block,
486
+ properties: block.properties
487
+ ? { ...block.properties }
488
+ : block.properties,
489
+ styles: block.styles
490
+ ? JSON.parse(JSON.stringify(block.styles))
491
+ : block.styles,
492
+ blocks: undefined
493
+ };
494
+
495
+ if (normalizedLabel === 'launch status badge') {
496
+ nextBlock.value = countdown.isLaunched
497
+ ? launchedStatusText
498
+ : upcomingStatusText;
499
+ } else if (normalizedLabel === 'launch date value' && launchDateText) {
500
+ nextBlock.value = launchDateText;
501
+ } else if (normalizedLabel === 'days value' && launchDate) {
502
+ nextBlock.value = countdown.values.days;
503
+ } else if (normalizedLabel === 'hours value' && launchDate) {
504
+ nextBlock.value = countdown.values.hours;
505
+ } else if (normalizedLabel === 'minutes value' && launchDate) {
506
+ nextBlock.value = countdown.values.minutes;
507
+ } else if (normalizedLabel === 'seconds value' && launchDate) {
508
+ nextBlock.value = countdown.values.seconds;
509
+ } else if (normalizedLabel === 'product price' && currentPriceText) {
510
+ nextBlock.value = currentPriceText;
511
+ } else if (normalizedLabel === 'product old price' && retailPriceText) {
512
+ nextBlock.value = retailPriceText;
513
+ } else if (normalizedLabel === 'primary cta' && product?.absolute_url) {
514
+ nextBlock.properties = {
515
+ ...(nextBlock.properties || {}),
516
+ url: product.absolute_url
517
+ };
518
+ }
519
+
520
+ const bindingPath = nextBlock.properties?.dataBinding;
521
+ if (bindingPath && product) {
522
+ const boundValue = resolvePath(product, String(bindingPath));
523
+
524
+ if (boundValue !== undefined) {
525
+ if (nextBlock.type === 'button') {
526
+ nextBlock.properties = {
527
+ ...(nextBlock.properties || {}),
528
+ url: boundValue
529
+ };
530
+ } else if (
531
+ nextBlock.type === 'text' &&
532
+ ![
533
+ 'launch date value',
534
+ 'launch status badge',
535
+ 'days value',
536
+ 'hours value',
537
+ 'minutes value',
538
+ 'seconds value',
539
+ 'product price',
540
+ 'product old price'
541
+ ].includes(normalizedLabel)
542
+ ) {
543
+ nextBlock.value = boundValue;
544
+ } else if (nextBlock.type === 'image') {
545
+ nextBlock.value = boundValue;
546
+ }
547
+ } else if (nextBlock.type === 'image') {
548
+ const fallbackImage = resolvePath(
549
+ product,
550
+ 'productimage_set[0].image'
551
+ );
552
+ if (fallbackImage !== undefined) {
553
+ nextBlock.value = fallbackImage;
554
+ }
555
+ }
556
+ }
557
+
558
+ if (block.blocks?.length) {
559
+ nextBlock.blocks = block.blocks
560
+ .map(cloneWithProduct)
561
+ .filter(Boolean) as Block[];
562
+ }
563
+
564
+ return nextBlock;
565
+ };
566
+
567
+ return (section.blocks || [])
568
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
569
+ .map(cloneWithProduct)
570
+ .filter(Boolean) as Block[];
571
+ }, [
572
+ countdown.isLaunched,
573
+ countdown.values.days,
574
+ countdown.values.hours,
575
+ countdown.values.minutes,
576
+ countdown.values.seconds,
577
+ currentPriceText,
578
+ hasRetailPrice,
579
+ launchDate,
580
+ launchDateText,
581
+ launchedStatusText,
582
+ product,
583
+ retailPriceText,
584
+ section.blocks,
585
+ showCountdown,
586
+ showOldPrice,
587
+ showPrice,
588
+ upcomingStatusText
589
+ ]);
590
+
591
+ const renderBlock = (block: Block) => (
592
+ <ThemeBlock
593
+ key={block.id}
594
+ block={block}
595
+ placeholderId={placeholderId}
596
+ sectionId={section.id}
597
+ isDesigner={isDesigner}
598
+ isSelected={selectedBlockId === block.id}
599
+ selectedBlockId={selectedBlockId}
600
+ currentBreakpoint={currentBreakpoint}
601
+ onMoveUp={() => {
602
+ if (window.parent) {
603
+ window.parent.postMessage(
604
+ {
605
+ type: 'MOVE_BLOCK_UP',
606
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
607
+ },
608
+ '*'
609
+ );
610
+ }
611
+ }}
612
+ onMoveDown={() => {
613
+ if (window.parent) {
614
+ window.parent.postMessage(
615
+ {
616
+ type: 'MOVE_BLOCK_DOWN',
617
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
618
+ },
619
+ '*'
620
+ );
621
+ }
622
+ }}
623
+ onDuplicate={() => {
624
+ if (window.parent) {
625
+ window.parent.postMessage(
626
+ {
627
+ type: 'DUPLICATE_BLOCK',
628
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
629
+ },
630
+ '*'
631
+ );
632
+ }
633
+ }}
634
+ onToggleVisibility={() => {
635
+ if (window.parent) {
636
+ window.parent.postMessage(
637
+ {
638
+ type: 'TOGGLE_BLOCK_VISIBILITY',
639
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
640
+ },
641
+ '*'
642
+ );
643
+ }
644
+ }}
645
+ onDelete={() => {
646
+ if (window.parent) {
647
+ window.parent.postMessage(
648
+ {
649
+ type: 'DELETE_BLOCK',
650
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
651
+ },
652
+ '*'
653
+ );
654
+ }
655
+ }}
656
+ onRename={(newLabel) => {
657
+ if (window.parent) {
658
+ window.parent.postMessage(
659
+ {
660
+ type: 'RENAME_BLOCK',
661
+ data: {
662
+ placeholderId,
663
+ sectionId: section.id,
664
+ blockId: block.id,
665
+ label: newLabel
666
+ }
667
+ },
668
+ '*'
669
+ );
670
+ }
671
+ }}
672
+ />
673
+ );
674
+
675
+ return (
676
+ <div
677
+ className={`w-full ${
678
+ hasMaxWidth ? `mx-auto ${maxWidthClass}` : maxWidthClass || ''
679
+ }`.trim()}
680
+ style={sectionStyles}
681
+ >
682
+ {normalizedBlocks
683
+ .filter((block) => (isDesigner ? true : !block.hidden))
684
+ .map(renderBlock)}
685
+ </div>
686
+ );
687
+ }