@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,62 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { getResponsiveValue, resolveThemeCssVariables } from '../utils';
5
+ import { Section } from '../theme-section';
6
+ import { useThemeSettingsContext } from '../theme-settings-context';
7
+
8
+ interface DividerSectionProps {
9
+ section: Section;
10
+ currentBreakpoint?: string;
11
+ }
12
+
13
+ export default function DividerSection({
14
+ section,
15
+ currentBreakpoint = 'desktop'
16
+ }: DividerSectionProps) {
17
+ const themeSettings = useThemeSettingsContext();
18
+
19
+ const containerStyles = {
20
+ paddingTop: getResponsiveValue(
21
+ section.styles?.['padding-top'],
22
+ currentBreakpoint
23
+ ),
24
+ paddingRight: getResponsiveValue(
25
+ section.styles?.['padding-right'],
26
+ currentBreakpoint
27
+ ),
28
+ paddingBottom: getResponsiveValue(
29
+ section.styles?.['padding-bottom'],
30
+ currentBreakpoint
31
+ ),
32
+ paddingLeft: getResponsiveValue(
33
+ section.styles?.['padding-left'],
34
+ currentBreakpoint
35
+ ),
36
+ position: 'relative'
37
+ } as React.CSSProperties;
38
+
39
+ const spanStyles = {
40
+ display: 'block',
41
+ height: getResponsiveValue(
42
+ section.styles?.height,
43
+ currentBreakpoint,
44
+ '1px'
45
+ ),
46
+ backgroundColor: resolveThemeCssVariables(
47
+ getResponsiveValue(
48
+ section.styles?.['background-color'],
49
+ currentBreakpoint,
50
+ '#e0e0e0'
51
+ ) as string,
52
+ themeSettings
53
+ ),
54
+ width: getResponsiveValue(section.styles?.width, currentBreakpoint, '100%')
55
+ } as React.CSSProperties;
56
+
57
+ return (
58
+ <div style={containerStyles}>
59
+ <span style={spanStyles} />
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,507 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } 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 FeaturedProductSpotlightSectionProps {
12
+ section: Section;
13
+ currentBreakpoint?: string;
14
+ placeholderId?: string;
15
+ isDesigner?: boolean;
16
+ selectedBlockId?: string | null;
17
+ }
18
+
19
+ const parseBoolean = (value: unknown, fallback: boolean): boolean => {
20
+ if (typeof value === 'boolean') return value;
21
+ if (typeof value === 'string') {
22
+ const normalized = value.trim().toLowerCase();
23
+ if (normalized === 'true') return true;
24
+ if (normalized === 'false') return false;
25
+ }
26
+ return fallback;
27
+ };
28
+
29
+ const resolvePath = (
30
+ source: Record<string, unknown> | undefined,
31
+ path: string
32
+ ): unknown => {
33
+ if (!source || !path) return undefined;
34
+
35
+ const normalizedPath = path.replace(/^item\./, '');
36
+ const pathParts = normalizedPath.split('.');
37
+ let value: unknown = source;
38
+
39
+ for (const part of pathParts) {
40
+ const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
41
+ if (arrayMatch) {
42
+ const [, arrayKey, indexText] = arrayMatch;
43
+ const arrayValue = (value as Record<string, unknown>)?.[arrayKey];
44
+ value = Array.isArray(arrayValue)
45
+ ? arrayValue[Number(indexText)]
46
+ : undefined;
47
+ } else {
48
+ value = (value as Record<string, unknown>)?.[part];
49
+ }
50
+
51
+ if (value === undefined || value === null) {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ return value;
57
+ };
58
+
59
+ const parsePriceValue = (value: unknown): number | null => {
60
+ if (typeof value === 'number' && Number.isFinite(value)) {
61
+ return value;
62
+ }
63
+
64
+ if (typeof value !== 'string') return null;
65
+
66
+ const cleaned = value.trim().replace(/[^\d,.-]/g, '');
67
+ if (!cleaned) return null;
68
+
69
+ let normalized = cleaned;
70
+ const hasComma = normalized.includes(',');
71
+ const hasDot = normalized.includes('.');
72
+
73
+ if (hasComma && hasDot) {
74
+ const lastComma = normalized.lastIndexOf(',');
75
+ const lastDot = normalized.lastIndexOf('.');
76
+ normalized =
77
+ lastComma > lastDot
78
+ ? normalized.replace(/\./g, '').replace(',', '.')
79
+ : normalized.replace(/,/g, '');
80
+ } else if (hasComma) {
81
+ const unsigned = normalized.replace(/^-/, '');
82
+ const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
83
+ normalized = isThousandsPattern
84
+ ? normalized.replace(/,/g, '')
85
+ : normalized.replace(/,/g, '.');
86
+ }
87
+
88
+ const parsed = Number(normalized);
89
+ return Number.isFinite(parsed) ? parsed : null;
90
+ };
91
+
92
+ const parsePositiveInt = (value: unknown): number | null => {
93
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
94
+ return value;
95
+ }
96
+
97
+ if (typeof value === 'string') {
98
+ const trimmed = value.trim();
99
+ if (!/^\d+$/.test(trimmed)) return null;
100
+ const parsed = Number.parseInt(trimmed, 10);
101
+ return parsed > 0 ? parsed : null;
102
+ }
103
+
104
+ return null;
105
+ };
106
+
107
+ const normalizeCurrencyLabel = (currency: unknown): string => {
108
+ const raw = String(currency || '').trim().toUpperCase();
109
+ if (!raw) return 'TL';
110
+
111
+ const map: Record<string, string> = {
112
+ TRY: 'TL',
113
+ TL: 'TL',
114
+ USD: 'USD',
115
+ EUR: 'EUR',
116
+ GBP: 'GBP'
117
+ };
118
+
119
+ return map[raw] || raw;
120
+ };
121
+
122
+ const formatPriceWithCurrency = (
123
+ value: unknown,
124
+ currency: unknown
125
+ ): string | null => {
126
+ if (value == null || value === '') return null;
127
+
128
+ const price = parsePriceValue(value);
129
+ if (price === null) {
130
+ const text = String(value).trim();
131
+ return text || null;
132
+ }
133
+
134
+ const hasDecimals = Math.abs(price % 1) > 0.00001;
135
+ const formatted = price.toLocaleString('tr-TR', {
136
+ minimumFractionDigits: hasDecimals ? 2 : 0,
137
+ maximumFractionDigits: 2
138
+ });
139
+
140
+ return `${formatted} ${normalizeCurrencyLabel(currency)}`;
141
+ };
142
+
143
+ const getCollectionProducts = (
144
+ section: Section,
145
+ isDesigner: boolean
146
+ ): Record<string, unknown>[] => {
147
+ const collectionDetails = section.dataSource?.details?.collection;
148
+ const staticData = section.dataSource?.details?.static?.data;
149
+ const isEditorMode =
150
+ typeof window !== 'undefined' && isDesigner && window.parent !== window;
151
+
152
+ const collectionPayload = isEditorMode
153
+ ? collectionDetails?.products || collectionDetails?.data
154
+ : collectionDetails?.data || collectionDetails?.products;
155
+
156
+ if (Array.isArray(collectionPayload)) {
157
+ return collectionPayload as Record<string, unknown>[];
158
+ }
159
+
160
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.products)) {
161
+ return (collectionPayload as Record<string, unknown>).products as Record<
162
+ string,
163
+ unknown
164
+ >[];
165
+ }
166
+
167
+ if (Array.isArray((collectionPayload as Record<string, unknown>)?.items)) {
168
+ return (collectionPayload as Record<string, unknown>).items as Record<
169
+ string,
170
+ unknown
171
+ >[];
172
+ }
173
+
174
+ if (Array.isArray(staticData)) {
175
+ return staticData as Record<string, unknown>[];
176
+ }
177
+
178
+ return [];
179
+ };
180
+
181
+ const getDiscountBadgeText = (
182
+ currentPrice: number | null,
183
+ retailPrice: number | null
184
+ ): string | null => {
185
+ if (
186
+ currentPrice === null ||
187
+ retailPrice === null ||
188
+ retailPrice <= currentPrice ||
189
+ retailPrice <= 0
190
+ ) {
191
+ return null;
192
+ }
193
+
194
+ const percentage = Math.round(((retailPrice - currentPrice) / retailPrice) * 100);
195
+ if (percentage <= 0) return null;
196
+ return `Save ${percentage}%`;
197
+ };
198
+
199
+ export default function FeaturedProductSpotlightSection({
200
+ section,
201
+ currentBreakpoint = 'desktop',
202
+ placeholderId = '',
203
+ isDesigner = false,
204
+ selectedBlockId = null
205
+ }: FeaturedProductSpotlightSectionProps) {
206
+ const themeSettings = useThemeSettingsContext();
207
+
208
+ const maxWidth = getResponsiveValue(
209
+ section.styles?.['max-width'],
210
+ currentBreakpoint,
211
+ 'normal'
212
+ );
213
+ const maxWidthClass =
214
+ maxWidth === 'narrow'
215
+ ? 'max-w-4xl'
216
+ : maxWidth === 'normal'
217
+ ? 'max-w-7xl'
218
+ : maxWidth === 'full'
219
+ ? 'w-full'
220
+ : '';
221
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
222
+
223
+ const filteredStyles = Object.fromEntries(
224
+ Object.entries(section.styles || {}).filter(([key]) => key !== 'max-width')
225
+ );
226
+
227
+ const sectionStyles = useMemo(() => {
228
+ const baseStyles = getCSSStyles(
229
+ filteredStyles,
230
+ themeSettings,
231
+ currentBreakpoint
232
+ );
233
+ const reverseLayout = parseBoolean(
234
+ getResponsiveValue(section.properties?.reverse, currentBreakpoint, false),
235
+ false
236
+ );
237
+
238
+ if (!reverseLayout) {
239
+ return baseStyles;
240
+ }
241
+
242
+ return {
243
+ ...baseStyles,
244
+ flexDirection: currentBreakpoint === 'mobile' ? 'column-reverse' : 'row-reverse'
245
+ };
246
+ }, [
247
+ currentBreakpoint,
248
+ filteredStyles,
249
+ section.properties?.reverse,
250
+ themeSettings
251
+ ]);
252
+
253
+ const products = useMemo(
254
+ () => getCollectionProducts(section, isDesigner),
255
+ [section, isDesigner]
256
+ );
257
+ const fallbackProductPk = parsePositiveInt(
258
+ getResponsiveValue(section.properties?.['product-pk'], currentBreakpoint, '')
259
+ );
260
+ const { data: fallbackProductResponse } = useGetProductByPkQuery(
261
+ fallbackProductPk as number,
262
+ {
263
+ skip: products.length > 0 || !fallbackProductPk
264
+ }
265
+ );
266
+ const product =
267
+ products[0] ||
268
+ ((fallbackProductResponse?.product as Record<string, unknown> | undefined) ??
269
+ undefined);
270
+
271
+ const activePrice =
272
+ ((product?.active_price as Record<string, unknown> | undefined) || {});
273
+ const currentPriceRaw = activePrice?.price ?? product?.price;
274
+ const retailPriceRaw = activePrice?.retail_price ?? product?.retail_price;
275
+ const currency =
276
+ activePrice?.currency_type ?? product?.currency_type ?? product?.currency;
277
+ const currentPrice = parsePriceValue(currentPriceRaw);
278
+ const retailPrice = parsePriceValue(retailPriceRaw);
279
+
280
+ const currentPriceText = formatPriceWithCurrency(currentPriceRaw, currency);
281
+ const retailPriceText = formatPriceWithCurrency(retailPriceRaw, currency);
282
+ const discountBadgeText = getDiscountBadgeText(currentPrice, retailPrice);
283
+ const hasRetailPrice =
284
+ Boolean(retailPriceText) &&
285
+ retailPrice !== null &&
286
+ currentPrice !== null &&
287
+ retailPrice > currentPrice;
288
+
289
+ const showBadge = parseBoolean(
290
+ getResponsiveValue(section.properties?.['show-badge'], currentBreakpoint, true),
291
+ true
292
+ );
293
+ const showOldPrice = parseBoolean(
294
+ getResponsiveValue(
295
+ section.properties?.['show-old-price'],
296
+ currentBreakpoint,
297
+ true
298
+ ),
299
+ true
300
+ );
301
+ const showThumbnails = parseBoolean(
302
+ getResponsiveValue(
303
+ section.properties?.['show-thumbnails'],
304
+ currentBreakpoint,
305
+ true
306
+ ),
307
+ true
308
+ );
309
+
310
+ const normalizedBlocks = useMemo(() => {
311
+ const cloneWithProduct = (block: Block): Block | null => {
312
+ const normalizedLabel = String(block.label || '').toLowerCase();
313
+
314
+ if (normalizedLabel === 'thumbnail row' && !showThumbnails) {
315
+ return null;
316
+ }
317
+
318
+ if (
319
+ normalizedLabel === 'discount badge' &&
320
+ (!showBadge || !discountBadgeText)
321
+ ) {
322
+ return null;
323
+ }
324
+
325
+ if (
326
+ normalizedLabel === 'old price' &&
327
+ (!showOldPrice || !hasRetailPrice)
328
+ ) {
329
+ return null;
330
+ }
331
+
332
+ const nextBlock: Block = {
333
+ ...block,
334
+ properties: block.properties ? { ...block.properties } : block.properties,
335
+ styles: block.styles
336
+ ? JSON.parse(JSON.stringify(block.styles))
337
+ : block.styles,
338
+ blocks: undefined
339
+ };
340
+
341
+ if (normalizedLabel === 'discount badge' && discountBadgeText) {
342
+ nextBlock.value = discountBadgeText;
343
+ } else if (normalizedLabel === 'current price' && currentPriceText) {
344
+ nextBlock.value = currentPriceText;
345
+ } else if (normalizedLabel === 'old price' && retailPriceText) {
346
+ nextBlock.value = retailPriceText;
347
+ } else if (normalizedLabel === 'primary cta' && product?.absolute_url) {
348
+ nextBlock.properties = {
349
+ ...(nextBlock.properties || {}),
350
+ url: product.absolute_url
351
+ };
352
+ }
353
+
354
+ const bindingPath = nextBlock.properties?.dataBinding;
355
+ if (bindingPath && product) {
356
+ const boundValue = resolvePath(product, String(bindingPath));
357
+
358
+ if (boundValue !== undefined) {
359
+ if (nextBlock.type === 'button') {
360
+ nextBlock.properties = {
361
+ ...(nextBlock.properties || {}),
362
+ url: boundValue
363
+ };
364
+ } else if (
365
+ nextBlock.type === 'text' &&
366
+ normalizedLabel !== 'current price' &&
367
+ normalizedLabel !== 'old price'
368
+ ) {
369
+ nextBlock.value = boundValue;
370
+ } else if (nextBlock.type === 'image') {
371
+ nextBlock.value = boundValue;
372
+ }
373
+ } else if (
374
+ nextBlock.type === 'image' &&
375
+ normalizedLabel.includes('thumbnail')
376
+ ) {
377
+ const fallbackImage = resolvePath(product, 'productimage_set[0].image');
378
+ if (fallbackImage !== undefined) {
379
+ nextBlock.value = fallbackImage;
380
+ }
381
+ }
382
+ }
383
+
384
+ if (block.blocks?.length) {
385
+ nextBlock.blocks = block.blocks
386
+ .map(cloneWithProduct)
387
+ .filter(Boolean) as Block[];
388
+ }
389
+
390
+ return nextBlock;
391
+ };
392
+
393
+ return (section.blocks || [])
394
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
395
+ .map(cloneWithProduct)
396
+ .filter(Boolean) as Block[];
397
+ }, [
398
+ currentPriceText,
399
+ discountBadgeText,
400
+ hasRetailPrice,
401
+ product,
402
+ retailPriceText,
403
+ section.blocks,
404
+ showBadge,
405
+ showOldPrice,
406
+ showThumbnails
407
+ ]);
408
+
409
+ const renderBlock = (block: Block) => (
410
+ <ThemeBlock
411
+ key={block.id}
412
+ block={block}
413
+ placeholderId={placeholderId}
414
+ sectionId={section.id}
415
+ isDesigner={isDesigner}
416
+ isSelected={selectedBlockId === block.id}
417
+ selectedBlockId={selectedBlockId}
418
+ currentBreakpoint={currentBreakpoint}
419
+ onMoveUp={() => {
420
+ if (window.parent) {
421
+ window.parent.postMessage(
422
+ {
423
+ type: 'MOVE_BLOCK_UP',
424
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
425
+ },
426
+ '*'
427
+ );
428
+ }
429
+ }}
430
+ onMoveDown={() => {
431
+ if (window.parent) {
432
+ window.parent.postMessage(
433
+ {
434
+ type: 'MOVE_BLOCK_DOWN',
435
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
436
+ },
437
+ '*'
438
+ );
439
+ }
440
+ }}
441
+ onDuplicate={() => {
442
+ if (window.parent) {
443
+ window.parent.postMessage(
444
+ {
445
+ type: 'DUPLICATE_BLOCK',
446
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
447
+ },
448
+ '*'
449
+ );
450
+ }
451
+ }}
452
+ onToggleVisibility={() => {
453
+ if (window.parent) {
454
+ window.parent.postMessage(
455
+ {
456
+ type: 'TOGGLE_BLOCK_VISIBILITY',
457
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
458
+ },
459
+ '*'
460
+ );
461
+ }
462
+ }}
463
+ onDelete={() => {
464
+ if (window.parent) {
465
+ window.parent.postMessage(
466
+ {
467
+ type: 'DELETE_BLOCK',
468
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
469
+ },
470
+ '*'
471
+ );
472
+ }
473
+ }}
474
+ onRename={(newLabel) => {
475
+ if (window.parent) {
476
+ window.parent.postMessage(
477
+ {
478
+ type: 'RENAME_BLOCK',
479
+ data: {
480
+ placeholderId,
481
+ sectionId: section.id,
482
+ blockId: block.id,
483
+ label: newLabel
484
+ }
485
+ },
486
+ '*'
487
+ );
488
+ }
489
+ }}
490
+ />
491
+ );
492
+
493
+ return (
494
+ <div
495
+ className={
496
+ hasMaxWidth
497
+ ? `mx-auto ${maxWidthClass}`
498
+ : maxWidthClass || undefined
499
+ }
500
+ style={sectionStyles}
501
+ >
502
+ {normalizedBlocks
503
+ .filter((block) => (isDesigner ? true : !block.hidden))
504
+ .map(renderBlock)}
505
+ </div>
506
+ );
507
+ }