@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,586 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } from 'react';
4
+ import clsx from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { useGetBasketQuery } from '@akinon/next/data/client/basket';
7
+
8
+ import ThemeBlock, { Block } from '../theme-block';
9
+ import { Section } from '../theme-section';
10
+ import { useThemeSettingsContext } from '../theme-settings-context';
11
+ import {
12
+ getCSSStyles,
13
+ getResponsiveValue,
14
+ resolveThemeCssVariables
15
+ } from '../utils';
16
+
17
+ interface ShippingThresholdProgressSectionProps {
18
+ section: Section;
19
+ currentBreakpoint?: string;
20
+ placeholderId?: string;
21
+ isDesigner?: boolean;
22
+ selectedBlockId?: string | null;
23
+ }
24
+
25
+ const parseAmount = (value: unknown, fallback: number): number => {
26
+ if (value === undefined || value === null || value === '') return fallback;
27
+
28
+ if (typeof value === 'number') {
29
+ return Number.isFinite(value) ? value : fallback;
30
+ }
31
+
32
+ if (typeof value !== 'string') return fallback;
33
+
34
+ const trimmed = value.trim();
35
+ if (!trimmed) return fallback;
36
+
37
+ const directParsed = Number(trimmed);
38
+ if (Number.isFinite(directParsed)) return directParsed;
39
+
40
+ let normalized = trimmed.replace(/[^\d.,-]/g, '');
41
+ if (!normalized) return fallback;
42
+
43
+ const hasComma = normalized.includes(',');
44
+ const hasDot = normalized.includes('.');
45
+
46
+ if (hasComma && hasDot) {
47
+ const lastComma = normalized.lastIndexOf(',');
48
+ const lastDot = normalized.lastIndexOf('.');
49
+
50
+ if (lastComma > lastDot) {
51
+ // Example: 1.234,56 -> 1234.56
52
+ normalized = normalized.replace(/\./g, '').replace(',', '.');
53
+ } else {
54
+ // Example: 1,234.56 -> 1234.56
55
+ normalized = normalized.replace(/,/g, '');
56
+ }
57
+ } else if (hasComma && !hasDot) {
58
+ const parts = normalized.split(',');
59
+ if (parts.length === 2 && parts[1].length <= 2) {
60
+ normalized = `${parts[0]}.${parts[1]}`;
61
+ } else {
62
+ normalized = normalized.replace(/,/g, '');
63
+ }
64
+ }
65
+
66
+ const parsed = Number(normalized);
67
+ return Number.isFinite(parsed) ? parsed : fallback;
68
+ };
69
+
70
+ const parseBoolean = (value: unknown, fallback: boolean): boolean => {
71
+ if (typeof value === 'boolean') return value;
72
+ if (typeof value === 'string') {
73
+ const normalized = value.trim().toLowerCase();
74
+ if (normalized === 'true') return true;
75
+ if (normalized === 'false') return false;
76
+ }
77
+ if (typeof value === 'number') return value !== 0;
78
+ return fallback;
79
+ };
80
+
81
+ const formatAmount = (value: number): string => {
82
+ const hasDecimal = Math.abs(value - Math.round(value)) >= 0.001;
83
+
84
+ return value.toLocaleString(undefined, {
85
+ minimumFractionDigits: hasDecimal ? 2 : 0,
86
+ maximumFractionDigits: hasDecimal ? 2 : 0
87
+ });
88
+ };
89
+
90
+ const replaceTokens = (
91
+ input: string,
92
+ tokens: Record<string, string>
93
+ ): string => {
94
+ let output = input;
95
+
96
+ Object.entries(tokens).forEach(([key, value]) => {
97
+ output = output.split(`{{${key}}}`).join(value);
98
+ });
99
+
100
+ return output;
101
+ };
102
+
103
+ const replaceTokensInUnknown = (
104
+ value: unknown,
105
+ tokens: Record<string, string>
106
+ ): unknown => {
107
+ if (typeof value === 'string') {
108
+ return replaceTokens(value, tokens);
109
+ }
110
+
111
+ if (Array.isArray(value)) {
112
+ return value.map((item) => replaceTokensInUnknown(item, tokens));
113
+ }
114
+
115
+ if (typeof value === 'object' && value !== null) {
116
+ return Object.fromEntries(
117
+ Object.entries(value).map(([key, nestedValue]) => [
118
+ key,
119
+ replaceTokensInUnknown(nestedValue, tokens)
120
+ ])
121
+ );
122
+ }
123
+
124
+ return value;
125
+ };
126
+
127
+ const toResponsiveStyle = (
128
+ existing: unknown,
129
+ nextValue: string
130
+ ): Record<string, string> => {
131
+ if (typeof existing === 'object' && existing !== null && !Array.isArray(existing)) {
132
+ const existingObject = existing as Record<string, unknown>;
133
+ const responsiveKeys = Object.keys(existingObject).filter((key) =>
134
+ ['desktop', 'tablet', 'mobile'].includes(key)
135
+ );
136
+
137
+ if (responsiveKeys.length > 0) {
138
+ return Object.fromEntries(responsiveKeys.map((key) => [key, nextValue]));
139
+ }
140
+ }
141
+
142
+ return {
143
+ desktop: nextValue,
144
+ tablet: nextValue,
145
+ mobile: nextValue
146
+ };
147
+ };
148
+
149
+ const resolveResponsiveString = (
150
+ value: unknown,
151
+ breakpoint: string
152
+ ): string => {
153
+ if (typeof value === 'string') return value;
154
+
155
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
156
+ const responsiveValue = getResponsiveValue(value, breakpoint, '');
157
+ if (typeof responsiveValue === 'string') return responsiveValue;
158
+ }
159
+
160
+ return '';
161
+ };
162
+
163
+ const getShippingRole = (
164
+ block: Pick<Block, 'label' | 'properties'>,
165
+ breakpoint: string
166
+ ): string => {
167
+ const roleFromProperties = resolveResponsiveString(
168
+ block.properties?.shippingRole,
169
+ breakpoint
170
+ );
171
+ const normalizedLabel = (block.label || '').toLowerCase();
172
+
173
+ return (
174
+ roleFromProperties ||
175
+ (normalizedLabel.includes('progress fill')
176
+ ? 'progress-fill'
177
+ : normalizedLabel.includes('progress marker')
178
+ ? 'progress-marker'
179
+ : normalizedLabel.includes('progress wrapper')
180
+ ? 'progress-wrapper'
181
+ : normalizedLabel.includes('start label')
182
+ ? 'start-label'
183
+ : '')
184
+ );
185
+ };
186
+
187
+ const findProgressBlockIds = (
188
+ blocks: Block[],
189
+ breakpoint: string
190
+ ): {
191
+ fillBlockId: string | null;
192
+ markerBlockId: string | null;
193
+ wrapperBlockId: string | null;
194
+ startLabelBlockId: string | null;
195
+ } => {
196
+ let fillBlockId: string | null = null;
197
+ let markerBlockId: string | null = null;
198
+ let wrapperBlockId: string | null = null;
199
+ let startLabelBlockId: string | null = null;
200
+
201
+ const walk = (block: Block) => {
202
+ const role = getShippingRole(block, breakpoint);
203
+
204
+ if (role === 'progress-wrapper') {
205
+ wrapperBlockId = block.id;
206
+ }
207
+
208
+ if (role === 'progress-fill') {
209
+ fillBlockId = block.id;
210
+ }
211
+
212
+ if (role === 'progress-marker') {
213
+ markerBlockId = block.id;
214
+ }
215
+
216
+ if (role === 'start-label') {
217
+ startLabelBlockId = block.id;
218
+ }
219
+
220
+ if (block.blocks && block.blocks.length > 0) {
221
+ block.blocks.forEach(walk);
222
+ }
223
+ };
224
+
225
+ blocks.forEach(walk);
226
+
227
+ return { fillBlockId, markerBlockId, wrapperBlockId, startLabelBlockId };
228
+ };
229
+
230
+ const escapeCssAttributeValue = (value: string): string => {
231
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
232
+ };
233
+
234
+ const injectDynamicState = (
235
+ blocks: Block[],
236
+ tokens: Record<string, string>,
237
+ currentBreakpoint: string,
238
+ progressPercent: number
239
+ ): Block[] => {
240
+ const walk = (block: Block): Block => {
241
+ const cloned: Block = {
242
+ ...block,
243
+ properties: block.properties ? { ...block.properties } : block.properties,
244
+ styles: block.styles ? { ...block.styles } : block.styles,
245
+ value: replaceTokensInUnknown(block.value, tokens) as Block['value']
246
+ };
247
+
248
+ const role = getShippingRole(cloned, currentBreakpoint);
249
+
250
+ if (role === 'progress-fill') {
251
+ cloned.styles = {
252
+ ...cloned.styles,
253
+ width: toResponsiveStyle(
254
+ cloned.styles?.width,
255
+ `${Math.max(0, Math.min(progressPercent, 100)).toFixed(2)}%`
256
+ )
257
+ };
258
+ }
259
+
260
+ if (cloned.blocks && cloned.blocks.length > 0) {
261
+ cloned.blocks = cloned.blocks.map(walk);
262
+ }
263
+
264
+ return cloned;
265
+ };
266
+
267
+ return blocks.map(walk);
268
+ };
269
+
270
+ const ShippingThresholdProgressSection: React.FC<
271
+ ShippingThresholdProgressSectionProps
272
+ > = ({
273
+ section,
274
+ currentBreakpoint = 'desktop',
275
+ placeholderId = '',
276
+ isDesigner = false,
277
+ selectedBlockId = null
278
+ }) => {
279
+ const themeSettings = useThemeSettingsContext();
280
+
281
+ const sortedBlocks = useMemo(
282
+ () =>
283
+ [...(section.blocks || [])]
284
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
285
+ .filter((block) => (isDesigner ? true : !block.hidden)),
286
+ [section.blocks, isDesigner]
287
+ );
288
+
289
+ const useLiveBasket = parseBoolean(
290
+ getResponsiveValue(section.properties?.['use-live-basket'], currentBreakpoint, true),
291
+ true
292
+ );
293
+
294
+ const { data: basketData } = useGetBasketQuery(undefined, {
295
+ skip: !useLiveBasket
296
+ });
297
+
298
+ const targetAmount = Math.max(
299
+ parseAmount(
300
+ getResponsiveValue(section.properties?.['target-amount'], currentBreakpoint, 1000),
301
+ 1000
302
+ ),
303
+ 1
304
+ );
305
+
306
+ const fallbackCurrentAmount = Math.max(
307
+ parseAmount(
308
+ getResponsiveValue(
309
+ section.properties?.['fallback-current-amount'],
310
+ currentBreakpoint,
311
+ 760
312
+ ),
313
+ 760
314
+ ),
315
+ 0
316
+ );
317
+
318
+ const liveBasketAmount = parseAmount(basketData?.total_amount, Number.NaN);
319
+ const currentAmount =
320
+ useLiveBasket && Number.isFinite(liveBasketAmount)
321
+ ? Math.max(0, liveBasketAmount)
322
+ : fallbackCurrentAmount;
323
+
324
+ const remainingAmount = Math.max(targetAmount - currentAmount, 0);
325
+ const progressPercent = Math.min((currentAmount / targetAmount) * 100, 100);
326
+
327
+ const currencySymbol =
328
+ String(
329
+ getResponsiveValue(section.properties?.['currency-symbol'], currentBreakpoint, 'TL') ||
330
+ 'TL'
331
+ ).trim() || 'TL';
332
+
333
+ const tokenContext = {
334
+ current: formatAmount(currentAmount),
335
+ remaining: formatAmount(remainingAmount),
336
+ target: formatAmount(targetAmount),
337
+ currency: currencySymbol,
338
+ progress_percent: progressPercent.toFixed(2),
339
+ marker_right_percent: (100 - progressPercent).toFixed(2)
340
+ };
341
+
342
+ const remainingMessageTemplate = String(
343
+ getResponsiveValue(
344
+ section.properties?.['remaining-message-template'],
345
+ currentBreakpoint,
346
+ 'Add {{remaining}} {{currency}} more for free shipping'
347
+ ) || 'Add {{remaining}} {{currency}} more for free shipping'
348
+ );
349
+
350
+ const unlockedMessageTemplate = String(
351
+ getResponsiveValue(
352
+ section.properties?.['unlocked-message-template'],
353
+ currentBreakpoint,
354
+ 'Free shipping unlocked'
355
+ ) || 'Free shipping unlocked'
356
+ );
357
+
358
+ const summaryTemplate = String(
359
+ getResponsiveValue(
360
+ section.properties?.['summary-template'],
361
+ currentBreakpoint,
362
+ '{{current}} {{currency}} / {{target}} {{currency}}'
363
+ ) || '{{current}} {{currency}} / {{target}} {{currency}}'
364
+ );
365
+
366
+ const ctaIncompleteText = String(
367
+ getResponsiveValue(
368
+ section.properties?.['cta-incomplete-text'],
369
+ currentBreakpoint,
370
+ 'Add products to unlock'
371
+ ) || 'Add products to unlock'
372
+ );
373
+
374
+ const ctaCompleteText = String(
375
+ getResponsiveValue(
376
+ section.properties?.['cta-complete-text'],
377
+ currentBreakpoint,
378
+ 'Continue to checkout'
379
+ ) || 'Continue to checkout'
380
+ );
381
+ const showStartLabel = parseBoolean(
382
+ getResponsiveValue(section.properties?.['show-start-label'], currentBreakpoint, false),
383
+ false
384
+ );
385
+
386
+ const progressTrackColor = resolveThemeCssVariables(
387
+ String(
388
+ getResponsiveValue(section.styles?.['progress-track-color'], currentBreakpoint, '#e2e8f0')
389
+ ),
390
+ themeSettings
391
+ );
392
+ const progressFillStartColor = resolveThemeCssVariables(
393
+ String(
394
+ getResponsiveValue(
395
+ section.styles?.['progress-fill-start-color'],
396
+ currentBreakpoint,
397
+ '#22c55e'
398
+ )
399
+ ),
400
+ themeSettings
401
+ );
402
+ const progressFillEndColor = resolveThemeCssVariables(
403
+ String(
404
+ getResponsiveValue(
405
+ section.styles?.['progress-fill-end-color'],
406
+ currentBreakpoint,
407
+ '#84cc16'
408
+ )
409
+ ),
410
+ themeSettings
411
+ );
412
+ const progressBarHeight = Math.min(
413
+ Math.max(
414
+ parseAmount(
415
+ getResponsiveValue(section.styles?.['progress-bar-height'], currentBreakpoint, 14),
416
+ 14
417
+ ),
418
+ 8
419
+ ),
420
+ 28
421
+ );
422
+ const progressBarRadius = String(
423
+ getResponsiveValue(
424
+ section.styles?.['progress-bar-radius'],
425
+ currentBreakpoint,
426
+ '999px'
427
+ ) || '999px'
428
+ );
429
+
430
+ const remainingMessage = replaceTokens(
431
+ remainingAmount > 0 ? remainingMessageTemplate : unlockedMessageTemplate,
432
+ tokenContext
433
+ );
434
+
435
+ const summaryMessage = replaceTokens(summaryTemplate, tokenContext);
436
+
437
+ const effectiveTokens: Record<string, string> = {
438
+ ...tokenContext,
439
+ remaining_message: remainingMessage,
440
+ summary_message: summaryMessage,
441
+ cta_text: remainingAmount > 0 ? ctaIncompleteText : ctaCompleteText
442
+ };
443
+
444
+ const renderedBlocks = injectDynamicState(
445
+ sortedBlocks,
446
+ effectiveTokens,
447
+ currentBreakpoint,
448
+ progressPercent
449
+ );
450
+
451
+ const { fillBlockId, markerBlockId, wrapperBlockId, startLabelBlockId } = useMemo(
452
+ () => findProgressBlockIds(sortedBlocks, currentBreakpoint),
453
+ [sortedBlocks, currentBreakpoint]
454
+ );
455
+
456
+ const dynamicProgressCss = useMemo(() => {
457
+ const rules: string[] = [];
458
+
459
+ if (wrapperBlockId) {
460
+ const escapedWrapperId = escapeCssAttributeValue(wrapperBlockId);
461
+ rules.push(
462
+ `[data-block-id="${escapedWrapperId}"]{position:relative !important;height:${progressBarHeight}px !important;background-color:${progressTrackColor} !important;border-radius:${progressBarRadius} !important;overflow:visible !important;}`
463
+ );
464
+ }
465
+
466
+ if (fillBlockId) {
467
+ const escapedFillId = escapeCssAttributeValue(fillBlockId);
468
+ rules.push(
469
+ `[data-block-id="${escapedFillId}"]{width:${Math.max(
470
+ 0,
471
+ Math.min(progressPercent, 100)
472
+ ).toFixed(
473
+ 2
474
+ )}% !important;position:absolute !important;top:0 !important;left:0 !important;height:100% !important;background:linear-gradient(90deg, ${progressFillStartColor} 0%, ${progressFillEndColor} 100%) !important;border-radius:${progressBarRadius} !important;padding:0 !important;min-height:0 !important;font-size:0 !important;line-height:0 !important;color:transparent !important;box-sizing:border-box !important;overflow:hidden !important;}`
475
+ );
476
+ }
477
+
478
+ if (markerBlockId) {
479
+ const escapedMarkerId = escapeCssAttributeValue(markerBlockId);
480
+ rules.push(`[data-block-id="${escapedMarkerId}"]{display:none !important;}`);
481
+ }
482
+
483
+ if (startLabelBlockId && !showStartLabel) {
484
+ const escapedStartLabelId = escapeCssAttributeValue(startLabelBlockId);
485
+ rules.push(`[data-block-id="${escapedStartLabelId}"]{display:none !important;}`);
486
+ }
487
+
488
+ return rules.join('');
489
+ }, [
490
+ fillBlockId,
491
+ markerBlockId,
492
+ wrapperBlockId,
493
+ startLabelBlockId,
494
+ progressPercent,
495
+ progressTrackColor,
496
+ progressFillStartColor,
497
+ progressFillEndColor,
498
+ progressBarHeight,
499
+ progressBarRadius,
500
+ showStartLabel
501
+ ]);
502
+
503
+ const maxWidth = getResponsiveValue(
504
+ section.styles?.['max-width'],
505
+ currentBreakpoint,
506
+ 'normal'
507
+ );
508
+ const maxWidthClass =
509
+ maxWidth === 'narrow'
510
+ ? 'max-w-4xl'
511
+ : maxWidth === 'normal'
512
+ ? 'max-w-7xl'
513
+ : '';
514
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
515
+
516
+ const filteredStyles = Object.fromEntries(
517
+ Object.entries(section.styles || {}).filter(
518
+ ([key]) =>
519
+ ![
520
+ 'max-width',
521
+ 'progress-track-color',
522
+ 'progress-fill-start-color',
523
+ 'progress-fill-end-color',
524
+ 'progress-bar-height',
525
+ 'progress-bar-radius'
526
+ ].includes(key)
527
+ )
528
+ );
529
+
530
+ const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
531
+
532
+ const postBlockAction = (type: string, blockId: string, label?: string) => {
533
+ if (!window.parent) return;
534
+ window.parent.postMessage(
535
+ {
536
+ type,
537
+ data: {
538
+ placeholderId,
539
+ sectionId: section.id,
540
+ blockId,
541
+ ...(label ? { label } : {})
542
+ }
543
+ },
544
+ '*'
545
+ );
546
+ };
547
+
548
+ const renderBlock = (block: Block) => (
549
+ <ThemeBlock
550
+ key={block.id}
551
+ block={block}
552
+ placeholderId={placeholderId}
553
+ sectionId={section.id}
554
+ isDesigner={isDesigner}
555
+ isSelected={selectedBlockId === block.id}
556
+ selectedBlockId={selectedBlockId}
557
+ currentBreakpoint={currentBreakpoint}
558
+ onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
559
+ onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
560
+ onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
561
+ onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
562
+ onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
563
+ onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
564
+ />
565
+ );
566
+
567
+ return (
568
+ <div
569
+ className={twMerge(
570
+ clsx(
571
+ 'shipping-threshold-progress-section relative z-10 w-full',
572
+ hasMaxWidth && 'mx-auto',
573
+ maxWidthClass
574
+ )
575
+ )}
576
+ style={sectionStyles}
577
+ >
578
+ {dynamicProgressCss ? (
579
+ <style dangerouslySetInnerHTML={{ __html: dynamicProgressCss }} />
580
+ ) : null}
581
+ {renderedBlocks.map(renderBlock)}
582
+ </div>
583
+ );
584
+ };
585
+
586
+ export default ShippingThresholdProgressSection;