@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,663 @@
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 || '').trim().toUpperCase();
114
+ if (!raw) return 'TL';
115
+
116
+ const map: Record<string, string> = {
117
+ TRY: 'TL',
118
+ TL: 'TL',
119
+ USD: 'USD',
120
+ EUR: 'EUR',
121
+ GBP: 'GBP'
122
+ };
123
+
124
+ return map[raw] || raw;
125
+ };
126
+
127
+ const formatPriceWithCurrency = (
128
+ value: unknown,
129
+ currency: unknown
130
+ ): string | null => {
131
+ if (value == null || value === '') return null;
132
+
133
+ const price = parsePriceValue(value);
134
+ if (price === null) {
135
+ const text = String(value).trim();
136
+ return text || null;
137
+ }
138
+
139
+ const hasDecimals = Math.abs(price % 1) > 0.00001;
140
+ const formatted = price.toLocaleString('tr-TR', {
141
+ minimumFractionDigits: hasDecimals ? 2 : 0,
142
+ maximumFractionDigits: 2
143
+ });
144
+
145
+ return `${formatted} ${normalizeCurrencyLabel(currency)}`;
146
+ };
147
+
148
+ const parseDateValue = (value: unknown): Date | null => {
149
+ if (value === undefined || value === null || value === '') return null;
150
+
151
+ if (value instanceof Date) {
152
+ return Number.isNaN(value.getTime()) ? null : value;
153
+ }
154
+
155
+ if (typeof value === 'number') {
156
+ const timestamp = value < 10_000_000_000 ? value * 1000 : value;
157
+ const date = new Date(timestamp);
158
+ return Number.isNaN(date.getTime()) ? null : date;
159
+ }
160
+
161
+ if (typeof value !== 'string') return null;
162
+
163
+ const trimmed = value.trim();
164
+ if (!trimmed) return null;
165
+
166
+ if (/^\d+$/.test(trimmed)) {
167
+ const numeric = Number(trimmed);
168
+ if (!Number.isFinite(numeric)) return null;
169
+ const timestamp = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
170
+ const date = new Date(timestamp);
171
+ return Number.isNaN(date.getTime()) ? null : date;
172
+ }
173
+
174
+ const parsed = new Date(trimmed);
175
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
176
+ };
177
+
178
+ const formatTwoDigits = (value: number) =>
179
+ String(Math.max(0, value)).padStart(2, '0');
180
+
181
+ const getRemainingTime = (
182
+ launchDate: Date | null
183
+ ): { isLaunched: boolean; values: CountdownValues } => {
184
+ const emptyValues = {
185
+ days: '00',
186
+ hours: '00',
187
+ minutes: '00',
188
+ seconds: '00'
189
+ };
190
+
191
+ if (!launchDate) {
192
+ return { isLaunched: false, values: emptyValues };
193
+ }
194
+
195
+ const diffMs = launchDate.getTime() - Date.now();
196
+ if (diffMs <= 0) {
197
+ return { isLaunched: true, values: emptyValues };
198
+ }
199
+
200
+ const totalSeconds = Math.floor(diffMs / 1000);
201
+ const days = Math.floor(totalSeconds / (60 * 60 * 24));
202
+ const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
203
+ const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
204
+ const seconds = totalSeconds % 60;
205
+
206
+ return {
207
+ isLaunched: false,
208
+ values: {
209
+ days: formatTwoDigits(days),
210
+ hours: formatTwoDigits(hours),
211
+ minutes: formatTwoDigits(minutes),
212
+ seconds: formatTwoDigits(seconds)
213
+ }
214
+ };
215
+ };
216
+
217
+ const formatLaunchDate = (value: Date | null): string | null => {
218
+ if (!value) return null;
219
+
220
+ return new Intl.DateTimeFormat(undefined, {
221
+ month: 'long',
222
+ day: 'numeric',
223
+ year: 'numeric'
224
+ }).format(value);
225
+ };
226
+
227
+ const getCollectionProducts = (
228
+ section: Section,
229
+ isDesigner: boolean
230
+ ): Record<string, unknown>[] => {
231
+ const collectionDetails = section.dataSource?.details?.collection;
232
+ const staticData = section.dataSource?.details?.static?.data;
233
+ const isEditorMode =
234
+ typeof window !== 'undefined' && isDesigner && window.parent !== window;
235
+
236
+ const collectionPayload = isEditorMode
237
+ ? collectionDetails?.products || collectionDetails?.data
238
+ : collectionDetails?.data || collectionDetails?.products;
239
+
240
+ if (Array.isArray(collectionPayload)) {
241
+ return collectionPayload as Record<string, unknown>[];
242
+ }
243
+
244
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.products)) {
245
+ return (collectionPayload as Record<string, unknown>).products as Record<
246
+ string,
247
+ unknown
248
+ >[];
249
+ }
250
+
251
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.items)) {
252
+ return (collectionPayload as Record<string, unknown>).items as Record<
253
+ string,
254
+ unknown
255
+ >[];
256
+ }
257
+
258
+ if (Array.isArray(staticData)) {
259
+ return staticData as Record<string, unknown>[];
260
+ }
261
+
262
+ return [];
263
+ };
264
+
265
+ const getProductLaunchDate = (
266
+ product: Record<string, unknown> | undefined
267
+ ): Date | null => {
268
+ if (!product) return null;
269
+
270
+ const candidatePaths = [
271
+ 'pre_order.launch_date',
272
+ 'pre_order.release_date',
273
+ 'launch_date',
274
+ 'release_date',
275
+ 'expected_release_date',
276
+ 'expected_ship_date',
277
+ 'available_at',
278
+ 'extra_attributes.launch_date',
279
+ 'extra_attributes.release_date',
280
+ 'extra_attributes.expected_release_date',
281
+ 'extra_data.launch_date',
282
+ 'extra_data.release_date',
283
+ 'extra_data.expected_release_date'
284
+ ];
285
+
286
+ for (const path of candidatePaths) {
287
+ const resolved = resolvePath(product, path);
288
+ const parsed = parseDateValue(resolved);
289
+ if (parsed) {
290
+ return parsed;
291
+ }
292
+ }
293
+
294
+ return null;
295
+ };
296
+
297
+ export default function PreOrderLaunchBannerSection({
298
+ section,
299
+ currentBreakpoint = 'desktop',
300
+ placeholderId = '',
301
+ isDesigner = false,
302
+ selectedBlockId = null
303
+ }: PreOrderLaunchBannerSectionProps) {
304
+ const themeSettings = useThemeSettingsContext();
305
+
306
+ const maxWidth = getResponsiveValue(
307
+ section.styles?.['max-width'],
308
+ currentBreakpoint,
309
+ 'normal'
310
+ );
311
+ const maxWidthClass =
312
+ maxWidth === 'narrow'
313
+ ? 'max-w-4xl'
314
+ : maxWidth === 'normal'
315
+ ? 'max-w-7xl'
316
+ : maxWidth === 'full'
317
+ ? 'w-full'
318
+ : '';
319
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
320
+
321
+ const filteredStyles = Object.fromEntries(
322
+ Object.entries(section.styles || {}).filter(([key]) => key !== 'max-width')
323
+ );
324
+
325
+ const sectionStyles = useMemo(() => {
326
+ const baseStyles = getCSSStyles(
327
+ filteredStyles,
328
+ themeSettings,
329
+ currentBreakpoint
330
+ );
331
+ const reverseLayout = parseBoolean(
332
+ getResponsiveValue(section.properties?.reverse, currentBreakpoint, false),
333
+ false
334
+ );
335
+
336
+ if (!reverseLayout) {
337
+ return baseStyles;
338
+ }
339
+
340
+ return {
341
+ ...baseStyles,
342
+ flexDirection: currentBreakpoint === 'mobile' ? 'column-reverse' : 'row-reverse'
343
+ };
344
+ }, [
345
+ currentBreakpoint,
346
+ filteredStyles,
347
+ section.properties?.reverse,
348
+ themeSettings
349
+ ]);
350
+
351
+ const products = useMemo(
352
+ () => getCollectionProducts(section, isDesigner),
353
+ [section, isDesigner]
354
+ );
355
+ const fallbackProductPk = parsePositiveInt(
356
+ getResponsiveValue(section.properties?.['product-pk'], currentBreakpoint, '')
357
+ );
358
+ const { data: fallbackProductResponse } = useGetProductByPkQuery(
359
+ fallbackProductPk as number,
360
+ {
361
+ skip: products.length > 0 || !fallbackProductPk
362
+ }
363
+ );
364
+ const product =
365
+ products[0] ||
366
+ ((fallbackProductResponse?.product as Record<string, unknown> | undefined) ??
367
+ undefined);
368
+
369
+ const propertyLaunchDate = getResponsiveValue(
370
+ section.properties?.['launch-date'],
371
+ currentBreakpoint,
372
+ ''
373
+ );
374
+ const launchDate = useMemo(
375
+ () => parseDateValue(propertyLaunchDate) || getProductLaunchDate(product),
376
+ [product, propertyLaunchDate]
377
+ );
378
+ const launchDateText = useMemo(() => formatLaunchDate(launchDate), [launchDate]);
379
+
380
+ const showCountdown = parseBoolean(
381
+ getResponsiveValue(
382
+ section.properties?.['show-countdown'],
383
+ currentBreakpoint,
384
+ true
385
+ ),
386
+ true
387
+ );
388
+ const showPrice = parseBoolean(
389
+ getResponsiveValue(section.properties?.['show-price'], currentBreakpoint, true),
390
+ true
391
+ );
392
+ const showOldPrice = parseBoolean(
393
+ getResponsiveValue(
394
+ section.properties?.['show-old-price'],
395
+ currentBreakpoint,
396
+ true
397
+ ),
398
+ true
399
+ );
400
+
401
+ const upcomingStatusText = String(
402
+ getResponsiveValue(
403
+ section.properties?.['upcoming-status-text'],
404
+ currentBreakpoint,
405
+ 'Pre-order open'
406
+ ) || 'Pre-order open'
407
+ );
408
+ const launchedStatusText = String(
409
+ getResponsiveValue(
410
+ section.properties?.['launched-status-text'],
411
+ currentBreakpoint,
412
+ 'Now shipping'
413
+ ) || 'Now shipping'
414
+ );
415
+
416
+ const [countdown, setCountdown] = useState(() => getRemainingTime(launchDate));
417
+
418
+ useEffect(() => {
419
+ const tick = () => setCountdown(getRemainingTime(launchDate));
420
+ tick();
421
+
422
+ if (!launchDate || !showCountdown) return;
423
+
424
+ const intervalId = window.setInterval(tick, 1000);
425
+ return () => window.clearInterval(intervalId);
426
+ }, [launchDate, showCountdown]);
427
+
428
+ const activePrice =
429
+ ((product?.active_price as Record<string, unknown> | undefined) || {});
430
+ const currentPriceRaw = activePrice?.price ?? product?.price;
431
+ const retailPriceRaw = activePrice?.retail_price ?? product?.retail_price;
432
+ const currency =
433
+ activePrice?.currency_type ?? product?.currency_type ?? product?.currency;
434
+ const currentPrice = parsePriceValue(currentPriceRaw);
435
+ const retailPrice = parsePriceValue(retailPriceRaw);
436
+ const currentPriceText = formatPriceWithCurrency(currentPriceRaw, currency);
437
+ const retailPriceText = formatPriceWithCurrency(retailPriceRaw, currency);
438
+ const hasRetailPrice =
439
+ Boolean(retailPriceText) &&
440
+ retailPrice !== null &&
441
+ currentPrice !== null &&
442
+ retailPrice > currentPrice;
443
+
444
+ const normalizedBlocks = useMemo(() => {
445
+ const cloneWithProduct = (block: Block): Block | null => {
446
+ const normalizedLabel = String(block.label || '').toLowerCase();
447
+
448
+ if (normalizedLabel === 'countdown group' && (!showCountdown || !launchDate)) {
449
+ return null;
450
+ }
451
+
452
+ if (normalizedLabel === 'price group' && !showPrice) {
453
+ return null;
454
+ }
455
+
456
+ if (
457
+ normalizedLabel === 'product old price' &&
458
+ (!showOldPrice || !hasRetailPrice)
459
+ ) {
460
+ return null;
461
+ }
462
+
463
+ const nextBlock: Block = {
464
+ ...block,
465
+ properties: block.properties ? { ...block.properties } : block.properties,
466
+ styles: block.styles
467
+ ? JSON.parse(JSON.stringify(block.styles))
468
+ : block.styles,
469
+ blocks: undefined
470
+ };
471
+
472
+ if (normalizedLabel === 'launch status badge') {
473
+ nextBlock.value = countdown.isLaunched
474
+ ? launchedStatusText
475
+ : upcomingStatusText;
476
+ } else if (normalizedLabel === 'launch date value' && launchDateText) {
477
+ nextBlock.value = launchDateText;
478
+ } else if (normalizedLabel === 'days value' && launchDate) {
479
+ nextBlock.value = countdown.values.days;
480
+ } else if (normalizedLabel === 'hours value' && launchDate) {
481
+ nextBlock.value = countdown.values.hours;
482
+ } else if (normalizedLabel === 'minutes value' && launchDate) {
483
+ nextBlock.value = countdown.values.minutes;
484
+ } else if (normalizedLabel === 'seconds value' && launchDate) {
485
+ nextBlock.value = countdown.values.seconds;
486
+ } else if (normalizedLabel === 'product price' && currentPriceText) {
487
+ nextBlock.value = currentPriceText;
488
+ } else if (normalizedLabel === 'product old price' && retailPriceText) {
489
+ nextBlock.value = retailPriceText;
490
+ } else if (normalizedLabel === 'primary cta' && product?.absolute_url) {
491
+ nextBlock.properties = {
492
+ ...(nextBlock.properties || {}),
493
+ url: product.absolute_url
494
+ };
495
+ }
496
+
497
+ const bindingPath = nextBlock.properties?.dataBinding;
498
+ if (bindingPath && product) {
499
+ const boundValue = resolvePath(product, String(bindingPath));
500
+
501
+ if (boundValue !== undefined) {
502
+ if (nextBlock.type === 'button') {
503
+ nextBlock.properties = {
504
+ ...(nextBlock.properties || {}),
505
+ url: boundValue
506
+ };
507
+ } else if (
508
+ nextBlock.type === 'text' &&
509
+ ![
510
+ 'launch date value',
511
+ 'launch status badge',
512
+ 'days value',
513
+ 'hours value',
514
+ 'minutes value',
515
+ 'seconds value',
516
+ 'product price',
517
+ 'product old price'
518
+ ].includes(normalizedLabel)
519
+ ) {
520
+ nextBlock.value = boundValue;
521
+ } else if (nextBlock.type === 'image') {
522
+ nextBlock.value = boundValue;
523
+ }
524
+ } else if (nextBlock.type === 'image') {
525
+ const fallbackImage = resolvePath(product, 'productimage_set[0].image');
526
+ if (fallbackImage !== undefined) {
527
+ nextBlock.value = fallbackImage;
528
+ }
529
+ }
530
+ }
531
+
532
+ if (block.blocks?.length) {
533
+ nextBlock.blocks = block.blocks
534
+ .map(cloneWithProduct)
535
+ .filter(Boolean) as Block[];
536
+ }
537
+
538
+ return nextBlock;
539
+ };
540
+
541
+ return (section.blocks || [])
542
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
543
+ .map(cloneWithProduct)
544
+ .filter(Boolean) as Block[];
545
+ }, [
546
+ countdown.isLaunched,
547
+ countdown.values.days,
548
+ countdown.values.hours,
549
+ countdown.values.minutes,
550
+ countdown.values.seconds,
551
+ currentPriceText,
552
+ hasRetailPrice,
553
+ launchDate,
554
+ launchDateText,
555
+ launchedStatusText,
556
+ product,
557
+ retailPriceText,
558
+ section.blocks,
559
+ showCountdown,
560
+ showOldPrice,
561
+ showPrice,
562
+ upcomingStatusText
563
+ ]);
564
+
565
+ const renderBlock = (block: Block) => (
566
+ <ThemeBlock
567
+ key={block.id}
568
+ block={block}
569
+ placeholderId={placeholderId}
570
+ sectionId={section.id}
571
+ isDesigner={isDesigner}
572
+ isSelected={selectedBlockId === block.id}
573
+ selectedBlockId={selectedBlockId}
574
+ currentBreakpoint={currentBreakpoint}
575
+ onMoveUp={() => {
576
+ if (window.parent) {
577
+ window.parent.postMessage(
578
+ {
579
+ type: 'MOVE_BLOCK_UP',
580
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
581
+ },
582
+ '*'
583
+ );
584
+ }
585
+ }}
586
+ onMoveDown={() => {
587
+ if (window.parent) {
588
+ window.parent.postMessage(
589
+ {
590
+ type: 'MOVE_BLOCK_DOWN',
591
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
592
+ },
593
+ '*'
594
+ );
595
+ }
596
+ }}
597
+ onDuplicate={() => {
598
+ if (window.parent) {
599
+ window.parent.postMessage(
600
+ {
601
+ type: 'DUPLICATE_BLOCK',
602
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
603
+ },
604
+ '*'
605
+ );
606
+ }
607
+ }}
608
+ onToggleVisibility={() => {
609
+ if (window.parent) {
610
+ window.parent.postMessage(
611
+ {
612
+ type: 'TOGGLE_BLOCK_VISIBILITY',
613
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
614
+ },
615
+ '*'
616
+ );
617
+ }
618
+ }}
619
+ onDelete={() => {
620
+ if (window.parent) {
621
+ window.parent.postMessage(
622
+ {
623
+ type: 'DELETE_BLOCK',
624
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
625
+ },
626
+ '*'
627
+ );
628
+ }
629
+ }}
630
+ onRename={(newLabel) => {
631
+ if (window.parent) {
632
+ window.parent.postMessage(
633
+ {
634
+ type: 'RENAME_BLOCK',
635
+ data: {
636
+ placeholderId,
637
+ sectionId: section.id,
638
+ blockId: block.id,
639
+ label: newLabel
640
+ }
641
+ },
642
+ '*'
643
+ );
644
+ }
645
+ }}
646
+ />
647
+ );
648
+
649
+ return (
650
+ <div
651
+ className={
652
+ hasMaxWidth
653
+ ? `mx-auto ${maxWidthClass}`
654
+ : maxWidthClass || undefined
655
+ }
656
+ style={sectionStyles}
657
+ >
658
+ {normalizedBlocks
659
+ .filter((block) => (isDesigner ? true : !block.hidden))
660
+ .map(renderBlock)}
661
+ </div>
662
+ );
663
+ }