@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,433 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import clsx from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+
7
+ import ThemeBlock, { Block } from '../theme-block';
8
+ import { useThemeSettingsContext } from '../theme-settings-context';
9
+ import {
10
+ applyAlphaToColor,
11
+ getCSSStyles,
12
+ getResponsiveValue,
13
+ resolveThemeCssVariables
14
+ } from '../utils';
15
+ import { Section } from '../theme-section';
16
+
17
+ interface CountdownCampaignBannerSectionProps {
18
+ section: Section;
19
+ currentBreakpoint?: string;
20
+ placeholderId?: string;
21
+ isDesigner?: boolean;
22
+ selectedBlockId?: string | null;
23
+ }
24
+
25
+ type CountdownUnit = 'days' | 'hours' | 'minutes' | 'seconds';
26
+
27
+ type CountdownValues = Record<CountdownUnit, string>;
28
+
29
+ const pickImageSource = (value: unknown, breakpoint: string): string => {
30
+ if (typeof value === 'string') {
31
+ return value.trim();
32
+ }
33
+
34
+ if (typeof value === 'object' && value !== null) {
35
+ const objectValue = value as Record<string, unknown>;
36
+ if (typeof objectValue.url === 'string') {
37
+ return objectValue.url.trim();
38
+ }
39
+
40
+ const responsiveValue = getResponsiveValue(value, breakpoint, '');
41
+ if (typeof responsiveValue === 'string') {
42
+ return responsiveValue.trim();
43
+ }
44
+
45
+ const firstStringValue = Object.values(objectValue).find(
46
+ (item) => typeof item === 'string'
47
+ );
48
+ if (typeof firstStringValue === 'string') {
49
+ return firstStringValue.trim();
50
+ }
51
+ }
52
+
53
+ return '';
54
+ };
55
+
56
+ const normalizeImageSource = (src: string): string => {
57
+ if (!src) return '';
58
+ if (src.startsWith('data:image')) return src;
59
+ if (src.startsWith('/')) return src;
60
+ if (src.startsWith('//')) return src;
61
+ if (src.startsWith('http://') || src.startsWith('https://')) return src;
62
+
63
+ const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
64
+ if (!cloudName) return '';
65
+
66
+ return `https://${cloudName}/${src.replace(/^\/+/, '')}`;
67
+ };
68
+
69
+ const hasValidImageSource = (src: string): boolean => {
70
+ if (!src) return false;
71
+ if (src.startsWith('data:image')) return true;
72
+
73
+ try {
74
+ new URL(src, 'http://localhost');
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+
81
+ const parseNumber = (value: unknown, fallback: number): number => {
82
+ if (value === undefined || value === null || value === '') return fallback;
83
+ const parsed = typeof value === 'string' ? Number(value) : Number(value);
84
+ return Number.isFinite(parsed) ? parsed : fallback;
85
+ };
86
+
87
+ const clampNumber = (value: number, min: number, max: number) =>
88
+ Math.min(Math.max(value, min), max);
89
+
90
+ const formatTwoDigits = (value: number) => String(Math.max(0, value)).padStart(2, '0');
91
+
92
+ const parseBoolean = (value: unknown, fallback: boolean): boolean => {
93
+ if (typeof value === 'boolean') return value;
94
+ if (typeof value === 'string') {
95
+ const normalized = value.trim().toLowerCase();
96
+ if (normalized === 'true') return true;
97
+ if (normalized === 'false') return false;
98
+ }
99
+ if (typeof value === 'number') return value !== 0;
100
+ return fallback;
101
+ };
102
+
103
+ const parseEndDate = (value: unknown): Date | null => {
104
+ if (value === undefined || value === null || value === '') return null;
105
+
106
+ if (value instanceof Date) {
107
+ return Number.isNaN(value.getTime()) ? null : value;
108
+ }
109
+
110
+ if (typeof value === 'number') {
111
+ const timestamp = value < 10_000_000_000 ? value * 1000 : value;
112
+ const date = new Date(timestamp);
113
+ return Number.isNaN(date.getTime()) ? null : date;
114
+ }
115
+
116
+ if (typeof value !== 'string') return null;
117
+
118
+ const trimmed = value.trim();
119
+ if (!trimmed) return null;
120
+
121
+ if (/^\d+$/.test(trimmed)) {
122
+ const numeric = Number(trimmed);
123
+ if (!Number.isFinite(numeric)) return null;
124
+ const timestamp = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
125
+ const date = new Date(timestamp);
126
+ return Number.isNaN(date.getTime()) ? null : date;
127
+ }
128
+
129
+ const parsed = new Date(trimmed);
130
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
131
+ };
132
+
133
+ const getRemainingTime = (
134
+ endDate: Date | null
135
+ ): { isExpired: boolean; values: CountdownValues } => {
136
+ const empty = {
137
+ days: '00',
138
+ hours: '00',
139
+ minutes: '00',
140
+ seconds: '00'
141
+ };
142
+
143
+ if (!endDate) {
144
+ return { isExpired: false, values: empty };
145
+ }
146
+
147
+ const diffMs = endDate.getTime() - Date.now();
148
+ if (diffMs <= 0) {
149
+ return { isExpired: true, values: empty };
150
+ }
151
+
152
+ const totalSeconds = Math.floor(diffMs / 1000);
153
+ const days = Math.floor(totalSeconds / (60 * 60 * 24));
154
+ const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
155
+ const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
156
+ const seconds = totalSeconds % 60;
157
+
158
+ return {
159
+ isExpired: false,
160
+ values: {
161
+ days: formatTwoDigits(days),
162
+ hours: formatTwoDigits(hours),
163
+ minutes: formatTwoDigits(minutes),
164
+ seconds: formatTwoDigits(seconds)
165
+ }
166
+ };
167
+ };
168
+
169
+ const CountdownCampaignBannerSection: React.FC<CountdownCampaignBannerSectionProps> = ({
170
+ section,
171
+ currentBreakpoint = 'desktop',
172
+ placeholderId = '',
173
+ isDesigner = false,
174
+ selectedBlockId = null
175
+ }) => {
176
+ const themeSettings = useThemeSettingsContext();
177
+
178
+ const sortedBlocks = [...(section.blocks || [])]
179
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
180
+ .filter((block) => (isDesigner ? true : !block.hidden));
181
+
182
+ const backgroundBlock = sortedBlocks.find((block) => block.type === 'image');
183
+ const foregroundBlocks = backgroundBlock
184
+ ? sortedBlocks.filter((block) => block.id !== backgroundBlock.id)
185
+ : sortedBlocks;
186
+
187
+ const firstButtonIndex = foregroundBlocks.findIndex(
188
+ (block) => block.type === 'button'
189
+ );
190
+
191
+ const leadingBlocks =
192
+ firstButtonIndex >= 0
193
+ ? foregroundBlocks.slice(0, firstButtonIndex)
194
+ : foregroundBlocks;
195
+ const trailingBlocks =
196
+ firstButtonIndex >= 0 ? foregroundBlocks.slice(firstButtonIndex) : [];
197
+
198
+ const endDateRaw = getResponsiveValue(
199
+ section.properties?.['end-date'],
200
+ currentBreakpoint,
201
+ ''
202
+ );
203
+ const endDate = useMemo(() => parseEndDate(endDateRaw), [endDateRaw]);
204
+
205
+ const showSeconds = parseBoolean(
206
+ getResponsiveValue(section.properties?.['show-seconds'], currentBreakpoint, true),
207
+ true
208
+ );
209
+
210
+ const expiredMessage = String(
211
+ getResponsiveValue(
212
+ section.properties?.['expired-message'],
213
+ currentBreakpoint,
214
+ 'Campaign ended'
215
+ ) || 'Campaign ended'
216
+ );
217
+
218
+ const [countdown, setCountdown] = useState(() => getRemainingTime(endDate));
219
+
220
+ useEffect(() => {
221
+ const tick = () => setCountdown(getRemainingTime(endDate));
222
+ tick();
223
+
224
+ if (!endDate) return;
225
+
226
+ const intervalId = window.setInterval(tick, 1000);
227
+ return () => window.clearInterval(intervalId);
228
+ }, [endDate]);
229
+
230
+ const maxWidth = getResponsiveValue(
231
+ section.styles?.['max-width'],
232
+ currentBreakpoint,
233
+ 'normal'
234
+ );
235
+ const maxWidthClass =
236
+ maxWidth === 'narrow'
237
+ ? 'max-w-4xl'
238
+ : maxWidth === 'normal'
239
+ ? 'max-w-7xl'
240
+ : '';
241
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
242
+
243
+ const filteredStyles = Object.fromEntries(
244
+ Object.entries(section.styles || {}).filter(
245
+ ([key]) =>
246
+ ![
247
+ 'max-width',
248
+ 'overlay-color',
249
+ 'overlay-opacity',
250
+ 'timer-background-color',
251
+ 'timer-text-color',
252
+ 'timer-label-color',
253
+ 'timer-border-radius',
254
+ 'timer-gap'
255
+ ].includes(key)
256
+ )
257
+ );
258
+
259
+ const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
260
+ const backgroundImageSrc = useMemo(() => {
261
+ if (!backgroundBlock) return '';
262
+ const raw = pickImageSource(backgroundBlock.value, currentBreakpoint);
263
+ const normalized = normalizeImageSource(raw);
264
+ return hasValidImageSource(normalized) ? normalized : '';
265
+ }, [backgroundBlock, currentBreakpoint]);
266
+
267
+ const overlayColor = resolveThemeCssVariables(
268
+ String(
269
+ getResponsiveValue(section.styles?.['overlay-color'], currentBreakpoint, '#0f172a')
270
+ ),
271
+ themeSettings
272
+ );
273
+ const overlayOpacity =
274
+ clampNumber(
275
+ parseNumber(
276
+ getResponsiveValue(section.styles?.['overlay-opacity'], currentBreakpoint, 45),
277
+ 45
278
+ ),
279
+ 0,
280
+ 100
281
+ ) / 100;
282
+ const overlayBackground = applyAlphaToColor(overlayColor, overlayOpacity);
283
+
284
+ const timerBackgroundColor = resolveThemeCssVariables(
285
+ String(
286
+ getResponsiveValue(
287
+ section.styles?.['timer-background-color'],
288
+ currentBreakpoint,
289
+ '#ffffff'
290
+ )
291
+ ),
292
+ themeSettings
293
+ );
294
+ const timerTextColor = resolveThemeCssVariables(
295
+ String(
296
+ getResponsiveValue(section.styles?.['timer-text-color'], currentBreakpoint, '#0f172a')
297
+ ),
298
+ themeSettings
299
+ );
300
+ const timerLabelColor = resolveThemeCssVariables(
301
+ String(
302
+ getResponsiveValue(section.styles?.['timer-label-color'], currentBreakpoint, '#64748b')
303
+ ),
304
+ themeSettings
305
+ );
306
+ const timerBorderRadius = String(
307
+ getResponsiveValue(section.styles?.['timer-border-radius'], currentBreakpoint, '10px')
308
+ );
309
+ const timerGap = clampNumber(
310
+ parseNumber(getResponsiveValue(section.styles?.['timer-gap'], currentBreakpoint, 10), 10),
311
+ 0,
312
+ 32
313
+ );
314
+
315
+ const postBlockAction = (type: string, blockId: string, label?: string) => {
316
+ if (!window.parent) return;
317
+ window.parent.postMessage(
318
+ {
319
+ type,
320
+ data: {
321
+ placeholderId,
322
+ sectionId: section.id,
323
+ blockId,
324
+ ...(label ? { label } : {})
325
+ }
326
+ },
327
+ '*'
328
+ );
329
+ };
330
+
331
+ const renderBlock = (block: Block) => (
332
+ <ThemeBlock
333
+ key={block.id}
334
+ block={block}
335
+ placeholderId={placeholderId}
336
+ sectionId={section.id}
337
+ isDesigner={isDesigner}
338
+ isSelected={selectedBlockId === block.id}
339
+ selectedBlockId={selectedBlockId}
340
+ currentBreakpoint={currentBreakpoint}
341
+ onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
342
+ onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
343
+ onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
344
+ onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
345
+ onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
346
+ onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
347
+ />
348
+ );
349
+
350
+ const timerUnits: Array<{ key: CountdownUnit; label: string }> = showSeconds
351
+ ? [
352
+ { key: 'days', label: 'Days' },
353
+ { key: 'hours', label: 'Hours' },
354
+ { key: 'minutes', label: 'Minutes' },
355
+ { key: 'seconds', label: 'Seconds' }
356
+ ]
357
+ : [
358
+ { key: 'days', label: 'Days' },
359
+ { key: 'hours', label: 'Hours' },
360
+ { key: 'minutes', label: 'Minutes' }
361
+ ];
362
+
363
+ return (
364
+ <div
365
+ className={twMerge(
366
+ clsx(
367
+ 'countdown-campaign-banner relative z-10 w-full overflow-hidden',
368
+ hasMaxWidth && 'mx-auto',
369
+ maxWidthClass
370
+ )
371
+ )}
372
+ style={{ ...sectionStyles, position: 'relative' }}
373
+ >
374
+ {backgroundBlock && backgroundImageSrc ? (
375
+ <div className="absolute inset-0 z-0">{renderBlock(backgroundBlock)}</div>
376
+ ) : (
377
+ <div className="absolute inset-0 z-0 bg-slate-600" />
378
+ )}
379
+
380
+ <div
381
+ className="absolute inset-0 z-10 pointer-events-none"
382
+ style={{ backgroundColor: overlayBackground }}
383
+ />
384
+
385
+ <div className="relative z-20 flex w-full max-w-3xl flex-col gap-4">
386
+ {leadingBlocks.map(renderBlock)}
387
+
388
+ {countdown.isExpired ? (
389
+ <div
390
+ className="inline-flex w-fit items-center justify-center px-4 py-2 text-sm font-semibold"
391
+ style={{
392
+ color: timerTextColor,
393
+ backgroundColor: timerBackgroundColor,
394
+ borderRadius: timerBorderRadius
395
+ }}
396
+ >
397
+ {expiredMessage}
398
+ </div>
399
+ ) : (
400
+ <div className="flex flex-wrap items-stretch" style={{ gap: `${timerGap}px` }}>
401
+ {timerUnits.map((unit) => (
402
+ <div
403
+ key={unit.key}
404
+ className="min-w-[64px] px-3 py-2 text-center"
405
+ style={{
406
+ backgroundColor: timerBackgroundColor,
407
+ borderRadius: timerBorderRadius
408
+ }}
409
+ >
410
+ <div
411
+ className="text-lg font-semibold leading-none"
412
+ style={{ color: timerTextColor }}
413
+ >
414
+ {countdown.values[unit.key]}
415
+ </div>
416
+ <div
417
+ className="mt-1 text-[11px] font-medium uppercase tracking-[0.04em] leading-none"
418
+ style={{ color: timerLabelColor }}
419
+ >
420
+ {unit.label}
421
+ </div>
422
+ </div>
423
+ ))}
424
+ </div>
425
+ )}
426
+
427
+ {trailingBlocks.map(renderBlock)}
428
+ </div>
429
+ </div>
430
+ );
431
+ };
432
+
433
+ export default CountdownCampaignBannerSection;