@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.
- package/.eslintrc.js +12 -0
- package/CHANGELOG.md +377 -7
- package/__tests__/next-config.test.ts +83 -0
- package/__tests__/tsconfig.json +23 -0
- package/api/auth.ts +133 -44
- package/api/barcode-search.ts +59 -0
- package/api/cache.ts +41 -5
- package/api/client.ts +21 -4
- package/api/form.ts +85 -0
- package/api/image-proxy.ts +75 -0
- package/api/product-categories.ts +53 -0
- package/api/similar-product-list.ts +63 -0
- package/api/similar-products.ts +111 -0
- package/api/virtual-try-on.ts +382 -0
- package/assets/styles/index.scss +84 -0
- package/babel.config.js +6 -0
- package/bin/pz-generate-routes.js +115 -0
- package/bin/pz-prebuild.js +1 -0
- package/bin/pz-predev.js +1 -0
- package/bin/pz-run-tests.js +99 -0
- package/bin/run-prebuild-tests.js +46 -0
- package/components/accordion.tsx +20 -5
- package/components/button.tsx +51 -36
- package/components/client-root.tsx +138 -2
- package/components/file-input.tsx +65 -3
- package/components/index.ts +1 -0
- package/components/input.tsx +1 -1
- package/components/link.tsx +46 -16
- package/components/logger-popup.tsx +213 -0
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +62 -3
- package/components/price.tsx +2 -2
- package/components/select.tsx +1 -1
- package/components/selected-payment-option-view.tsx +21 -0
- package/components/theme-editor/blocks/accordion-block.tsx +136 -0
- package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
- package/components/theme-editor/blocks/button-block.tsx +593 -0
- package/components/theme-editor/blocks/counter-block.tsx +348 -0
- package/components/theme-editor/blocks/divider-block.tsx +20 -0
- package/components/theme-editor/blocks/embed-block.tsx +208 -0
- package/components/theme-editor/blocks/group-block.tsx +116 -0
- package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
- package/components/theme-editor/blocks/icon-block.tsx +230 -0
- package/components/theme-editor/blocks/image-block.tsx +137 -0
- package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
- package/components/theme-editor/blocks/input-block.tsx +123 -0
- package/components/theme-editor/blocks/link-block.tsx +216 -0
- package/components/theme-editor/blocks/lottie-block.tsx +325 -0
- package/components/theme-editor/blocks/map-block.tsx +89 -0
- package/components/theme-editor/blocks/slider-block.tsx +595 -0
- package/components/theme-editor/blocks/tab-block.tsx +10 -0
- package/components/theme-editor/blocks/text-block.tsx +52 -0
- package/components/theme-editor/blocks/video-block.tsx +122 -0
- package/components/theme-editor/components/action-toolbar.tsx +305 -0
- package/components/theme-editor/components/designer-overlay.tsx +74 -0
- package/components/theme-editor/components/with-designer-features.tsx +142 -0
- package/components/theme-editor/dynamic-font-loader.tsx +79 -0
- package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
- package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
- package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
- package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
- package/components/theme-editor/placeholder-registry.ts +31 -0
- package/components/theme-editor/sections/before-after-section.tsx +245 -0
- package/components/theme-editor/sections/contact-form-section.tsx +563 -0
- package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
- package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
- package/components/theme-editor/sections/divider-section.tsx +62 -0
- package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
- package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
- package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
- package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
- package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
- package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
- package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
- package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
- package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
- package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
- package/components/theme-editor/sections/section-wrapper.tsx +135 -0
- package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
- package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
- package/components/theme-editor/sections/tabs-section.tsx +578 -0
- package/components/theme-editor/theme-block.tsx +102 -0
- package/components/theme-editor/theme-placeholder-client.tsx +218 -0
- package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
- package/components/theme-editor/theme-placeholder.tsx +288 -0
- package/components/theme-editor/theme-section.tsx +1224 -0
- package/components/theme-editor/theme-settings-context.tsx +13 -0
- package/components/theme-editor/utils/index.ts +792 -0
- package/components/theme-editor/utils/iterator-utils.ts +234 -0
- package/components/theme-editor/utils/publish-window.ts +86 -0
- package/components/theme-editor/utils/visibility-rules.ts +188 -0
- package/data/client/account.ts +17 -2
- package/data/client/api.ts +2 -0
- package/data/client/basket.ts +66 -5
- package/data/client/checkout.ts +391 -99
- package/data/client/misc.ts +38 -2
- package/data/client/product.ts +19 -2
- package/data/client/user.ts +16 -8
- package/data/server/category.ts +11 -9
- package/data/server/flatpage.ts +11 -4
- package/data/server/form.ts +15 -4
- package/data/server/landingpage.ts +11 -4
- package/data/server/list.ts +5 -4
- package/data/server/menu.ts +11 -3
- package/data/server/product.ts +111 -55
- package/data/server/seo.ts +14 -4
- package/data/server/special-page.ts +5 -4
- package/data/server/widget.ts +90 -5
- package/data/urls.ts +16 -5
- package/hocs/client/with-segment-defaults.tsx +2 -2
- package/hocs/server/with-segment-defaults.tsx +65 -20
- package/hooks/index.ts +4 -0
- package/hooks/use-localization.ts +24 -10
- package/hooks/use-logger-context.tsx +114 -0
- package/hooks/use-logger.ts +92 -0
- package/hooks/use-loyalty-availability.ts +21 -0
- package/hooks/use-payment-options.ts +2 -1
- package/hooks/use-pz-params.ts +37 -0
- package/hooks/use-router.ts +51 -14
- package/hooks/use-sentry-uncaught-errors.ts +24 -0
- package/instrumentation/index.ts +10 -1
- package/instrumentation/node.ts +2 -20
- package/jest.config.js +25 -0
- package/lib/cache-handler.mjs +534 -16
- package/lib/cache.ts +272 -37
- package/localization/index.ts +2 -1
- package/localization/provider.tsx +2 -5
- package/middlewares/bfcache-headers.ts +18 -0
- package/middlewares/checkout-provider.ts +1 -1
- package/middlewares/complete-gpay.ts +32 -26
- package/middlewares/complete-masterpass.ts +33 -26
- package/middlewares/complete-wallet.ts +182 -0
- package/middlewares/default.ts +360 -215
- package/middlewares/index.ts +10 -2
- package/middlewares/locale.ts +34 -11
- package/middlewares/masterpass-rest-callback.ts +230 -0
- package/middlewares/oauth-login.ts +200 -57
- package/middlewares/pretty-url.ts +21 -8
- package/middlewares/redirection-payment.ts +32 -26
- package/middlewares/saved-card-redirection.ts +33 -26
- package/middlewares/three-d-redirection.ts +32 -26
- package/middlewares/url-redirection.ts +11 -1
- package/middlewares/wallet-complete-redirection.ts +206 -0
- package/package.json +25 -10
- package/plugins.d.ts +19 -4
- package/plugins.js +10 -1
- package/redux/actions.ts +47 -0
- package/redux/middlewares/checkout.ts +63 -138
- package/redux/middlewares/index.ts +14 -10
- package/redux/middlewares/pre-order/address.ts +7 -2
- package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/delivery-option.ts +7 -1
- package/redux/middlewares/pre-order/index.ts +16 -10
- package/redux/middlewares/pre-order/installment-option.ts +8 -1
- package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
- package/redux/middlewares/pre-order/payment-option.ts +7 -1
- package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
- package/redux/middlewares/pre-order/redirection.ts +8 -2
- package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
- package/redux/middlewares/pre-order/shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/shipping-step.ts +5 -1
- package/redux/reducers/checkout.ts +23 -3
- package/redux/reducers/index.ts +11 -3
- package/redux/reducers/root.ts +7 -2
- package/redux/reducers/widget.ts +80 -0
- package/sentry/index.ts +69 -13
- package/tailwind/content.js +16 -0
- package/types/commerce/account.ts +5 -1
- package/types/commerce/checkout.ts +35 -1
- package/types/commerce/widget.ts +33 -0
- package/types/index.ts +101 -6
- package/types/next-auth.d.ts +2 -2
- package/types/widget.ts +80 -0
- package/utils/app-fetch.ts +7 -2
- package/utils/generate-commerce-search-params.ts +3 -2
- package/utils/get-checkout-path.ts +3 -0
- package/utils/get-root-hostname.ts +28 -0
- package/utils/index.ts +64 -10
- package/utils/localization.ts +4 -0
- package/utils/mobile-3d-iframe.ts +8 -2
- package/utils/override-middleware.ts +7 -12
- package/utils/pz-segments.ts +92 -0
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +9 -3
- package/utils/redirection-iframe.ts +8 -2
- package/utils/widget-styles.ts +107 -0
- package/views/error-page.tsx +93 -0
- package/with-pz-config.js +13 -6
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
|
|
7
|
+
import ThemeBlock, { Block } from '../theme-block';
|
|
8
|
+
import { Section } from '../theme-section';
|
|
9
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
10
|
+
import { getCSSStyles, getResponsiveValue } from '../utils';
|
|
11
|
+
|
|
12
|
+
interface StatsCounterSectionProps {
|
|
13
|
+
section: Section;
|
|
14
|
+
currentBreakpoint?: string;
|
|
15
|
+
placeholderId?: string;
|
|
16
|
+
isDesigner?: boolean;
|
|
17
|
+
selectedBlockId?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CounterTarget {
|
|
21
|
+
blockId: string;
|
|
22
|
+
endValue: number;
|
|
23
|
+
decimals: number;
|
|
24
|
+
useGrouping: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type CounterAnimationEasing = 'linear' | 'ease-out' | 'ease-in-out';
|
|
28
|
+
|
|
29
|
+
const NUMBER_TOKEN_REGEX = /-?\d[\d.,]*/;
|
|
30
|
+
|
|
31
|
+
const parseNumber = (value: unknown, fallback: number): number => {
|
|
32
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
33
|
+
if (typeof value === 'number') {
|
|
34
|
+
return Number.isFinite(value) ? value : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (!trimmed) return fallback;
|
|
40
|
+
|
|
41
|
+
const directParsed = Number(trimmed);
|
|
42
|
+
if (Number.isFinite(directParsed)) return directParsed;
|
|
43
|
+
|
|
44
|
+
const numericToken = trimmed
|
|
45
|
+
// Keep only numeric-safe chars so values like `1400ms` or `1,400` can still be parsed.
|
|
46
|
+
.replace(/[^\d.,-]/g, '')
|
|
47
|
+
.replace(/,/g, '');
|
|
48
|
+
const tokenParsed = Number(numericToken);
|
|
49
|
+
|
|
50
|
+
return Number.isFinite(tokenParsed) ? tokenParsed : fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return fallback;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const parseBoolean = (value: unknown, fallback: boolean): boolean => {
|
|
57
|
+
if (typeof value === 'boolean') return value;
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
const normalized = value.trim().toLowerCase();
|
|
60
|
+
if (normalized === 'true') return true;
|
|
61
|
+
if (normalized === 'false') return false;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'number') return value !== 0;
|
|
64
|
+
return fallback;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const readEasingValue = (value: unknown): string | null => {
|
|
68
|
+
if (typeof value === 'string') return value;
|
|
69
|
+
|
|
70
|
+
if (typeof value === 'object' && value !== null) {
|
|
71
|
+
const objectValue = value as Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
if (typeof objectValue.value === 'string') {
|
|
74
|
+
return objectValue.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof objectValue.label === 'string') {
|
|
78
|
+
return objectValue.label;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const firstString = Object.values(objectValue).find(
|
|
82
|
+
(item) => typeof item === 'string'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (typeof firstString === 'string') {
|
|
86
|
+
return firstString;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const parseAnimationEasing = (value: unknown): CounterAnimationEasing => {
|
|
94
|
+
const raw = readEasingValue(value);
|
|
95
|
+
if (!raw) return 'linear';
|
|
96
|
+
|
|
97
|
+
const normalized = raw
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.replace(/_/g, '-')
|
|
101
|
+
.replace(/\s+/g, '-');
|
|
102
|
+
|
|
103
|
+
if (normalized === 'ease-out' || normalized === 'easeout') return 'ease-out';
|
|
104
|
+
if (
|
|
105
|
+
normalized === 'ease-in-out' ||
|
|
106
|
+
normalized === 'easeinout' ||
|
|
107
|
+
normalized === 'ease-inout'
|
|
108
|
+
) {
|
|
109
|
+
return 'ease-in-out';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return 'linear';
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const applyAnimationEasing = (
|
|
116
|
+
progress: number,
|
|
117
|
+
easing: CounterAnimationEasing
|
|
118
|
+
): number => {
|
|
119
|
+
if (easing === 'ease-out') {
|
|
120
|
+
// Cubic ease-out: stronger acceleration contrast for visible difference.
|
|
121
|
+
return 1 - Math.pow(1 - progress, 3);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (easing === 'ease-in-out') {
|
|
125
|
+
// Cubic ease-in-out: noticeably slower start and end.
|
|
126
|
+
return progress < 0.5
|
|
127
|
+
? 4 * progress * progress * progress
|
|
128
|
+
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return progress;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const clampNumber = (value: number, min: number, max: number) =>
|
|
135
|
+
Math.min(Math.max(value, min), max);
|
|
136
|
+
|
|
137
|
+
const stripHtml = (value: string): string =>
|
|
138
|
+
value
|
|
139
|
+
.replace(/<[^>]*>/g, ' ')
|
|
140
|
+
.replace(/ /gi, ' ')
|
|
141
|
+
.replace(/\s+/g, ' ')
|
|
142
|
+
.trim();
|
|
143
|
+
|
|
144
|
+
const resolveTextValue = (raw: unknown): string | null => {
|
|
145
|
+
let value = raw;
|
|
146
|
+
|
|
147
|
+
if (typeof value === 'string' && value.trim().startsWith('{')) {
|
|
148
|
+
try {
|
|
149
|
+
value = JSON.parse(value);
|
|
150
|
+
} catch {
|
|
151
|
+
// Keep original string.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof value === 'string') {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof value === 'object' && value !== null) {
|
|
160
|
+
const objectValue = value as Record<string, unknown>;
|
|
161
|
+
const firstString = Object.values(objectValue).find(
|
|
162
|
+
(item) => typeof item === 'string'
|
|
163
|
+
);
|
|
164
|
+
return typeof firstString === 'string' ? firstString : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const getCounterTargetFromText = (value: string): Omit<CounterTarget, 'blockId'> | null => {
|
|
171
|
+
const plain = stripHtml(value);
|
|
172
|
+
const tokenMatch = plain.match(NUMBER_TOKEN_REGEX);
|
|
173
|
+
|
|
174
|
+
if (!tokenMatch) return null;
|
|
175
|
+
|
|
176
|
+
const token = tokenMatch[0];
|
|
177
|
+
const normalized = token.replace(/,/g, '');
|
|
178
|
+
const endValue = Number(normalized);
|
|
179
|
+
|
|
180
|
+
if (!Number.isFinite(endValue)) return null;
|
|
181
|
+
|
|
182
|
+
const decimals = normalized.includes('.')
|
|
183
|
+
? normalized.split('.')[1]?.length || 0
|
|
184
|
+
: 0;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
endValue,
|
|
188
|
+
decimals,
|
|
189
|
+
useGrouping: token.includes(',')
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const collectCounterTargets = (blocks: Block[]): CounterTarget[] => {
|
|
194
|
+
const targets: CounterTarget[] = [];
|
|
195
|
+
|
|
196
|
+
const walk = (block: Block) => {
|
|
197
|
+
const isCounterValueBlock =
|
|
198
|
+
block.type === 'text' &&
|
|
199
|
+
(block.properties?.statsRole === 'value' || block.label === 'Value');
|
|
200
|
+
|
|
201
|
+
if (isCounterValueBlock) {
|
|
202
|
+
const rawText = resolveTextValue(block.value);
|
|
203
|
+
if (!rawText) return;
|
|
204
|
+
const parsed = getCounterTargetFromText(rawText);
|
|
205
|
+
if (parsed) {
|
|
206
|
+
targets.push({
|
|
207
|
+
blockId: block.id,
|
|
208
|
+
...parsed
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (block.blocks && block.blocks.length > 0) {
|
|
214
|
+
block.blocks.forEach(walk);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
blocks.forEach(walk);
|
|
219
|
+
return targets;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const formatAnimatedNumber = (
|
|
223
|
+
value: number,
|
|
224
|
+
decimals: number,
|
|
225
|
+
useGrouping: boolean
|
|
226
|
+
): string => {
|
|
227
|
+
if (decimals > 0) {
|
|
228
|
+
const rounded = Number(value.toFixed(decimals));
|
|
229
|
+
if (!useGrouping) return rounded.toFixed(decimals);
|
|
230
|
+
|
|
231
|
+
return rounded.toLocaleString(undefined, {
|
|
232
|
+
minimumFractionDigits: decimals,
|
|
233
|
+
maximumFractionDigits: decimals
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rounded = Math.round(value);
|
|
238
|
+
return useGrouping ? rounded.toLocaleString() : String(rounded);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const injectAnimatedValues = (
|
|
242
|
+
blocks: Block[],
|
|
243
|
+
animatedValues: Record<string, string>
|
|
244
|
+
): Block[] => {
|
|
245
|
+
const replaceTokenInContent = (content: string, replacement: string): string => {
|
|
246
|
+
if (content.includes('<') && content.includes('>')) {
|
|
247
|
+
return content.replace(/>([^<]*)</, (fullMatch, innerText: string) => {
|
|
248
|
+
const nextInner = innerText.replace(NUMBER_TOKEN_REGEX, replacement);
|
|
249
|
+
return fullMatch.replace(innerText, nextInner);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return content.replace(NUMBER_TOKEN_REGEX, replacement);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const walk = (block: Block): Block => {
|
|
257
|
+
const cloned: Block = {
|
|
258
|
+
...block,
|
|
259
|
+
properties: block.properties ? { ...block.properties } : block.properties,
|
|
260
|
+
styles: block.styles ? { ...block.styles } : block.styles
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (
|
|
264
|
+
cloned.type === 'text' &&
|
|
265
|
+
animatedValues[cloned.id] !== undefined
|
|
266
|
+
) {
|
|
267
|
+
const replacement = animatedValues[cloned.id];
|
|
268
|
+
|
|
269
|
+
if (typeof cloned.value === 'string') {
|
|
270
|
+
const raw = cloned.value.trim();
|
|
271
|
+
|
|
272
|
+
if (raw.startsWith('{')) {
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(cloned.value) as Record<string, unknown>;
|
|
275
|
+
const nextValue = Object.fromEntries(
|
|
276
|
+
Object.entries(parsed).map(([key, value]) => [
|
|
277
|
+
key,
|
|
278
|
+
typeof value === 'string'
|
|
279
|
+
? replaceTokenInContent(value, replacement)
|
|
280
|
+
: value
|
|
281
|
+
])
|
|
282
|
+
);
|
|
283
|
+
cloned.value = JSON.stringify(nextValue);
|
|
284
|
+
} catch {
|
|
285
|
+
cloned.value = replaceTokenInContent(cloned.value, replacement);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
cloned.value = replaceTokenInContent(cloned.value, replacement);
|
|
289
|
+
}
|
|
290
|
+
} else if (typeof cloned.value === 'object' && cloned.value !== null) {
|
|
291
|
+
const parsed = cloned.value as Record<string, unknown>;
|
|
292
|
+
cloned.value = Object.fromEntries(
|
|
293
|
+
Object.entries(parsed).map(([key, value]) => [
|
|
294
|
+
key,
|
|
295
|
+
typeof value === 'string'
|
|
296
|
+
? replaceTokenInContent(value, replacement)
|
|
297
|
+
: value
|
|
298
|
+
])
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (cloned.blocks && cloned.blocks.length > 0) {
|
|
304
|
+
cloned.blocks = cloned.blocks.map(walk);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return cloned;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return blocks.map(walk);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
|
|
314
|
+
section,
|
|
315
|
+
currentBreakpoint = 'desktop',
|
|
316
|
+
placeholderId = '',
|
|
317
|
+
isDesigner = false,
|
|
318
|
+
selectedBlockId = null
|
|
319
|
+
}) => {
|
|
320
|
+
const themeSettings = useThemeSettingsContext();
|
|
321
|
+
|
|
322
|
+
const sortedBlocks = useMemo(
|
|
323
|
+
() =>
|
|
324
|
+
[...(section.blocks || [])]
|
|
325
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
326
|
+
.filter((block) => (isDesigner ? true : !block.hidden)),
|
|
327
|
+
[section.blocks, isDesigner]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const targets = useMemo(() => collectCounterTargets(sortedBlocks), [sortedBlocks]);
|
|
331
|
+
const targetsKey = useMemo(
|
|
332
|
+
() =>
|
|
333
|
+
targets
|
|
334
|
+
.map(
|
|
335
|
+
(target) =>
|
|
336
|
+
`${target.blockId}:${target.endValue}:${target.decimals}:${target.useGrouping}`
|
|
337
|
+
)
|
|
338
|
+
.join('|'),
|
|
339
|
+
[targets]
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const countUpEnabled = parseBoolean(
|
|
343
|
+
getResponsiveValue(section.properties?.['count-up-enabled'], currentBreakpoint, true),
|
|
344
|
+
true
|
|
345
|
+
);
|
|
346
|
+
const countUpDuration = clampNumber(
|
|
347
|
+
parseNumber(
|
|
348
|
+
getResponsiveValue(section.properties?.['count-up-duration'], currentBreakpoint, 1400),
|
|
349
|
+
1400
|
|
350
|
+
),
|
|
351
|
+
300,
|
|
352
|
+
10000
|
|
353
|
+
);
|
|
354
|
+
const animationEasing = parseAnimationEasing(
|
|
355
|
+
getResponsiveValue(section.properties?.['count-up-easing'], currentBreakpoint, 'linear')
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const [animatedValues, setAnimatedValues] = useState<Record<string, string>>({});
|
|
359
|
+
const targetsRef = useRef<CounterTarget[]>(targets);
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
targetsRef.current = targets;
|
|
363
|
+
}, [targets]);
|
|
364
|
+
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
const currentTargets = targetsRef.current;
|
|
367
|
+
|
|
368
|
+
if (currentTargets.length === 0) {
|
|
369
|
+
setAnimatedValues({});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const buildValues = (progress: number): Record<string, string> => {
|
|
374
|
+
const next: Record<string, string> = {};
|
|
375
|
+
|
|
376
|
+
currentTargets.forEach((target) => {
|
|
377
|
+
const currentValue = target.endValue * progress;
|
|
378
|
+
next[target.blockId] = formatAnimatedNumber(
|
|
379
|
+
currentValue,
|
|
380
|
+
target.decimals,
|
|
381
|
+
target.useGrouping
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return next;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (!countUpEnabled) {
|
|
389
|
+
setAnimatedValues(buildValues(1));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let rafId = 0;
|
|
394
|
+
const startTime = performance.now();
|
|
395
|
+
const duration = Math.max(countUpDuration, 1);
|
|
396
|
+
|
|
397
|
+
const tick = (now: number) => {
|
|
398
|
+
const progress = Math.min((now - startTime) / duration, 1);
|
|
399
|
+
setAnimatedValues(buildValues(applyAnimationEasing(progress, animationEasing)));
|
|
400
|
+
|
|
401
|
+
if (progress < 1) {
|
|
402
|
+
rafId = window.requestAnimationFrame(tick);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
setAnimatedValues(buildValues(0));
|
|
407
|
+
rafId = window.requestAnimationFrame(tick);
|
|
408
|
+
|
|
409
|
+
return () => {
|
|
410
|
+
if (rafId) {
|
|
411
|
+
window.cancelAnimationFrame(rafId);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}, [countUpEnabled, countUpDuration, animationEasing, targetsKey]);
|
|
415
|
+
|
|
416
|
+
const renderedBlocks = useMemo(
|
|
417
|
+
() => injectAnimatedValues(sortedBlocks, animatedValues),
|
|
418
|
+
[sortedBlocks, animatedValues]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const maxWidth = getResponsiveValue(
|
|
422
|
+
section.styles?.['max-width'],
|
|
423
|
+
currentBreakpoint,
|
|
424
|
+
'normal'
|
|
425
|
+
);
|
|
426
|
+
const maxWidthClass =
|
|
427
|
+
maxWidth === 'narrow'
|
|
428
|
+
? 'max-w-4xl'
|
|
429
|
+
: maxWidth === 'normal'
|
|
430
|
+
? 'max-w-7xl'
|
|
431
|
+
: '';
|
|
432
|
+
const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
|
|
433
|
+
|
|
434
|
+
const filteredStyles = Object.fromEntries(
|
|
435
|
+
Object.entries(section.styles || {}).filter(([key]) => !['max-width'].includes(key))
|
|
436
|
+
);
|
|
437
|
+
const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
|
|
438
|
+
|
|
439
|
+
const postBlockAction = (type: string, blockId: string, label?: string) => {
|
|
440
|
+
if (!window.parent) return;
|
|
441
|
+
window.parent.postMessage(
|
|
442
|
+
{
|
|
443
|
+
type,
|
|
444
|
+
data: {
|
|
445
|
+
placeholderId,
|
|
446
|
+
sectionId: section.id,
|
|
447
|
+
blockId,
|
|
448
|
+
...(label ? { label } : {})
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
'*'
|
|
452
|
+
);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const renderBlock = (block: Block) => (
|
|
456
|
+
<ThemeBlock
|
|
457
|
+
key={block.id}
|
|
458
|
+
block={block}
|
|
459
|
+
placeholderId={placeholderId}
|
|
460
|
+
sectionId={section.id}
|
|
461
|
+
isDesigner={isDesigner}
|
|
462
|
+
isSelected={selectedBlockId === block.id}
|
|
463
|
+
selectedBlockId={selectedBlockId}
|
|
464
|
+
currentBreakpoint={currentBreakpoint}
|
|
465
|
+
onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
|
|
466
|
+
onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
|
|
467
|
+
onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
|
|
468
|
+
onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
|
|
469
|
+
onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
|
|
470
|
+
onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
|
|
471
|
+
/>
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<div
|
|
476
|
+
className={twMerge(
|
|
477
|
+
clsx('stats-counter-section w-full', hasMaxWidth && 'mx-auto', maxWidthClass)
|
|
478
|
+
)}
|
|
479
|
+
style={sectionStyles}
|
|
480
|
+
>
|
|
481
|
+
{renderedBlocks.map(renderBlock)}
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export default StatsCounterSection;
|