@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,1224 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import ThemeBlock, { Block } from './theme-block';
|
|
4
|
+
import ActionToolbar from './components/action-toolbar';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
8
|
+
import SectionWrapper from './sections/section-wrapper';
|
|
9
|
+
import sectionRendererRegistry from './sections/section-renderer-registry';
|
|
10
|
+
import { buildIteratorBlock } from './utils/iterator-utils';
|
|
11
|
+
|
|
12
|
+
export interface Section {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
properties: any;
|
|
18
|
+
styles: any;
|
|
19
|
+
blocks: Block[];
|
|
20
|
+
order: number;
|
|
21
|
+
hidden: boolean;
|
|
22
|
+
locked?: boolean;
|
|
23
|
+
dataSourceId?: string;
|
|
24
|
+
dataSource?: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ThemeSectionProps {
|
|
28
|
+
section: Section;
|
|
29
|
+
placeholderId: string;
|
|
30
|
+
isDesigner?: boolean;
|
|
31
|
+
isSelected?: boolean;
|
|
32
|
+
selectedBlockId?: string | null;
|
|
33
|
+
currentBreakpoint?: string;
|
|
34
|
+
onSelect?: (sectionId: string) => void;
|
|
35
|
+
onMoveUp?: () => void;
|
|
36
|
+
onMoveDown?: () => void;
|
|
37
|
+
onDuplicate?: () => void;
|
|
38
|
+
onToggleVisibility?: () => void;
|
|
39
|
+
onDelete?: () => void;
|
|
40
|
+
onRename?: (newLabel: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const applyInheritedLocksToBlocks = (
|
|
44
|
+
blocks: Block[],
|
|
45
|
+
parentLocked = false
|
|
46
|
+
): Block[] =>
|
|
47
|
+
blocks.map((block) => {
|
|
48
|
+
const isLocked = parentLocked || block.locked === true;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...block,
|
|
52
|
+
locked: isLocked,
|
|
53
|
+
blocks: block.blocks
|
|
54
|
+
? applyInheritedLocksToBlocks(block.blocks, isLocked)
|
|
55
|
+
: block.blocks
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default function ThemeSection({
|
|
60
|
+
section,
|
|
61
|
+
placeholderId,
|
|
62
|
+
isDesigner = false,
|
|
63
|
+
isSelected = false,
|
|
64
|
+
selectedBlockId = null,
|
|
65
|
+
currentBreakpoint = 'desktop',
|
|
66
|
+
onSelect,
|
|
67
|
+
onMoveUp,
|
|
68
|
+
onMoveDown,
|
|
69
|
+
onDuplicate,
|
|
70
|
+
onToggleVisibility,
|
|
71
|
+
onDelete,
|
|
72
|
+
onRename
|
|
73
|
+
}: ThemeSectionProps) {
|
|
74
|
+
const effectiveSection = useMemo(
|
|
75
|
+
() => ({
|
|
76
|
+
...section,
|
|
77
|
+
blocks: applyInheritedLocksToBlocks(
|
|
78
|
+
section.blocks || [],
|
|
79
|
+
section.locked === true
|
|
80
|
+
)
|
|
81
|
+
}),
|
|
82
|
+
[section]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const readSectionString = (value: unknown): string => {
|
|
86
|
+
if (value == null) return '';
|
|
87
|
+
if (typeof value === 'string') return value;
|
|
88
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
89
|
+
const responsive = value as Record<string, unknown>;
|
|
90
|
+
const picked =
|
|
91
|
+
responsive.desktop ?? responsive.mobile ?? Object.values(responsive)[0];
|
|
92
|
+
return picked == null ? '' : String(picked);
|
|
93
|
+
}
|
|
94
|
+
return String(value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const isNewsletterSection =
|
|
98
|
+
effectiveSection.type === 'newsletter-signup-banner';
|
|
99
|
+
const newsletterEndpoint = readSectionString(
|
|
100
|
+
effectiveSection.properties?.['api-endpoint']
|
|
101
|
+
).trim();
|
|
102
|
+
const newsletterSuccessMessage = readSectionString(
|
|
103
|
+
effectiveSection.properties?.['success-message']
|
|
104
|
+
).trim();
|
|
105
|
+
const newsletterErrorMessage = readSectionString(
|
|
106
|
+
effectiveSection.properties?.['error-message']
|
|
107
|
+
).trim();
|
|
108
|
+
|
|
109
|
+
const sectionRef = useRef<HTMLElement>(null);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!isDesigner) return;
|
|
113
|
+
|
|
114
|
+
const handleMessage = (event: MessageEvent) => {
|
|
115
|
+
if (
|
|
116
|
+
event.data?.type === 'SCROLL_TO_SECTION' &&
|
|
117
|
+
event.data?.data?.sectionId === effectiveSection.id
|
|
118
|
+
) {
|
|
119
|
+
sectionRef.current?.scrollIntoView({
|
|
120
|
+
behavior: 'smooth',
|
|
121
|
+
block: 'center'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
window.addEventListener('message', handleMessage);
|
|
127
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
128
|
+
}, [effectiveSection.id, isDesigner]);
|
|
129
|
+
|
|
130
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
131
|
+
if (isDesigner && onSelect) {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
onSelect(effectiveSection.id);
|
|
134
|
+
|
|
135
|
+
if (window.parent) {
|
|
136
|
+
window.parent.postMessage(
|
|
137
|
+
{
|
|
138
|
+
type: 'SELECT_SECTION',
|
|
139
|
+
data: {
|
|
140
|
+
placeholderId,
|
|
141
|
+
sectionId: effectiveSection.id
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
'*'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const isFrequentlyBoughtTogetherSection =
|
|
151
|
+
effectiveSection.properties?.dataSourceVariant ===
|
|
152
|
+
'frequently-bought-together' ||
|
|
153
|
+
effectiveSection.name === 'Frequently Bought Together';
|
|
154
|
+
|
|
155
|
+
const getProductsFromDataPath = (
|
|
156
|
+
dataPath?: string
|
|
157
|
+
): Record<string, unknown>[] => {
|
|
158
|
+
if (!effectiveSection.dataSource?.details) return [];
|
|
159
|
+
|
|
160
|
+
const isEditorMode = isDesigner && window.parent !== window;
|
|
161
|
+
const collectionData = isEditorMode
|
|
162
|
+
? effectiveSection.dataSource.details.collection?.products ||
|
|
163
|
+
effectiveSection.dataSource.details.collection?.data
|
|
164
|
+
: effectiveSection.dataSource.details.collection?.data ||
|
|
165
|
+
effectiveSection.dataSource.details.collection?.products;
|
|
166
|
+
|
|
167
|
+
if (!collectionData) return [];
|
|
168
|
+
|
|
169
|
+
if (Array.isArray(collectionData)) {
|
|
170
|
+
return collectionData;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Array.isArray((collectionData as any)?.products)) {
|
|
174
|
+
return (collectionData as any).products;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!dataPath) return [];
|
|
178
|
+
|
|
179
|
+
const pathParts = dataPath.split('.');
|
|
180
|
+
let value: unknown = collectionData;
|
|
181
|
+
|
|
182
|
+
for (const part of pathParts) {
|
|
183
|
+
value = (value as Record<string, unknown>)?.[part];
|
|
184
|
+
if (value === undefined) break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return Array.isArray(value) ? (value as Record<string, unknown>[]) : [];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const getIteratorRenderMeta = (iteratorBlock: Block) => {
|
|
191
|
+
const dataPath =
|
|
192
|
+
(iteratorBlock.iteratorDataPath as string | undefined) ||
|
|
193
|
+
(iteratorBlock.properties?.iteratorDataPath as string | undefined);
|
|
194
|
+
const products = getProductsFromDataPath(dataPath);
|
|
195
|
+
|
|
196
|
+
const useIteratorCount =
|
|
197
|
+
iteratorBlock.properties?.useIteratorCount === true ||
|
|
198
|
+
isFrequentlyBoughtTogetherSection;
|
|
199
|
+
const iteratorCount = Number(iteratorBlock.properties?.iteratorCount) || 1;
|
|
200
|
+
const actualCount = useIteratorCount
|
|
201
|
+
? Math.max(1, iteratorCount)
|
|
202
|
+
: products.length > 0
|
|
203
|
+
? products.length
|
|
204
|
+
: iteratorCount;
|
|
205
|
+
|
|
206
|
+
return { products, actualCount };
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const parsePriceValue = (value: unknown): number => {
|
|
210
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
211
|
+
if (typeof value !== 'string') return 0;
|
|
212
|
+
|
|
213
|
+
const cleaned = value.trim().replace(/[^\d,.-]/g, '');
|
|
214
|
+
if (!cleaned) return 0;
|
|
215
|
+
|
|
216
|
+
let normalized = cleaned;
|
|
217
|
+
const hasComma = normalized.includes(',');
|
|
218
|
+
const hasDot = normalized.includes('.');
|
|
219
|
+
|
|
220
|
+
if (hasComma && hasDot) {
|
|
221
|
+
const lastComma = normalized.lastIndexOf(',');
|
|
222
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
223
|
+
normalized =
|
|
224
|
+
lastComma > lastDot
|
|
225
|
+
? normalized.replace(/\./g, '').replace(',', '.')
|
|
226
|
+
: normalized.replace(/,/g, '');
|
|
227
|
+
} else if (hasComma) {
|
|
228
|
+
const unsigned = normalized.replace(/^-/, '');
|
|
229
|
+
const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
|
|
230
|
+
normalized = isThousandsPattern
|
|
231
|
+
? normalized.replace(/,/g, '')
|
|
232
|
+
: normalized.replace(/,/g, '.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parsed = Number(normalized);
|
|
236
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const escapeHtml = (value: string): string =>
|
|
240
|
+
value
|
|
241
|
+
.replace(/&/g, '&')
|
|
242
|
+
.replace(/</g, '<')
|
|
243
|
+
.replace(/>/g, '>')
|
|
244
|
+
.replace(/"/g, '"')
|
|
245
|
+
.replace(/'/g, ''');
|
|
246
|
+
|
|
247
|
+
const formatDisplayPrice = (value: unknown): string => {
|
|
248
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
249
|
+
const hasDecimals = Math.abs(value % 1) > 0.00001;
|
|
250
|
+
return value.toLocaleString('tr-TR', {
|
|
251
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
252
|
+
maximumFractionDigits: 2
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (typeof value !== 'string') return '0';
|
|
257
|
+
const raw = value.trim();
|
|
258
|
+
if (!raw) return '0';
|
|
259
|
+
if (/[A-Za-z]/.test(raw)) return raw;
|
|
260
|
+
|
|
261
|
+
const parsed = parsePriceValue(raw);
|
|
262
|
+
const hasDecimals = Math.abs(parsed % 1) > 0.00001;
|
|
263
|
+
return parsed.toLocaleString('tr-TR', {
|
|
264
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
265
|
+
maximumFractionDigits: 2
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const getPriceWithCurrency = (value: unknown, currency: unknown): string => {
|
|
270
|
+
const amount = formatDisplayPrice(value);
|
|
271
|
+
const currencyLabel = normalizeCurrencyLabel(currency);
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
amount.toUpperCase().includes(currencyLabel) ||
|
|
275
|
+
/[A-Za-z]{2,}/.test(amount)
|
|
276
|
+
) {
|
|
277
|
+
return amount;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return `${amount} ${currencyLabel}`;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const buildFrequentlyBoughtPriceHtml = (
|
|
284
|
+
productData: Record<string, unknown>
|
|
285
|
+
): string => {
|
|
286
|
+
const activePrice =
|
|
287
|
+
((productData as any)?.active_price as
|
|
288
|
+
| Record<string, unknown>
|
|
289
|
+
| undefined) || {};
|
|
290
|
+
const currentRaw = activePrice?.price ?? (productData as any)?.price;
|
|
291
|
+
const retailRaw =
|
|
292
|
+
activePrice?.retail_price ?? (productData as any)?.retail_price;
|
|
293
|
+
const currency =
|
|
294
|
+
activePrice?.currency_type ??
|
|
295
|
+
(productData as any)?.currency_type ??
|
|
296
|
+
(productData as any)?.currency;
|
|
297
|
+
|
|
298
|
+
const currentNumeric = parsePriceValue(currentRaw);
|
|
299
|
+
const retailNumeric = parsePriceValue(retailRaw);
|
|
300
|
+
|
|
301
|
+
const currentText = escapeHtml(getPriceWithCurrency(currentRaw, currency));
|
|
302
|
+
const retailText = escapeHtml(getPriceWithCurrency(retailRaw, currency));
|
|
303
|
+
|
|
304
|
+
if (retailNumeric > 0 && retailNumeric > currentNumeric) {
|
|
305
|
+
return `<p style="margin:0;font-size:14px;line-height:1.3;"><span style='color:#94a3b8;text-decoration:line-through;margin-right:6px;'>${retailText}</span><span style='color:#0f172a;font-weight:700;'>${currentText}</span></p>`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return `<p style="margin:0;font-size:14px;line-height:1.3;"><span style='color:#0f172a;font-weight:700;'>${currentText}</span></p>`;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const formatBundleAmount = (
|
|
312
|
+
amount: number,
|
|
313
|
+
currencyLabel: string
|
|
314
|
+
): string => {
|
|
315
|
+
const isIntegerAmount = Math.abs(amount % 1) < 0.00001;
|
|
316
|
+
const formatted = new Intl.NumberFormat('en-US', {
|
|
317
|
+
minimumFractionDigits: isIntegerAmount ? 0 : 2,
|
|
318
|
+
maximumFractionDigits: 2
|
|
319
|
+
}).format(amount);
|
|
320
|
+
return `${formatted} ${currencyLabel}`;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const normalizeCurrencyLabel = (rawCurrency: unknown): string => {
|
|
324
|
+
if (typeof rawCurrency !== 'string') return 'TL';
|
|
325
|
+
const normalized = rawCurrency.trim().toUpperCase();
|
|
326
|
+
if (!normalized || normalized === 'TRY' || normalized === 'TL') return 'TL';
|
|
327
|
+
return normalized;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const computeFrequentlyBoughtSummary = (
|
|
331
|
+
products: Record<string, unknown>[],
|
|
332
|
+
count: number
|
|
333
|
+
) => {
|
|
334
|
+
const selectedProducts = products.slice(0, Math.max(0, count));
|
|
335
|
+
let bundleTotal = 0;
|
|
336
|
+
let retailTotal = 0;
|
|
337
|
+
let currencyLabel = 'TL';
|
|
338
|
+
|
|
339
|
+
selectedProducts.forEach((product, index) => {
|
|
340
|
+
const activePrice =
|
|
341
|
+
((product as any)?.active_price as
|
|
342
|
+
| Record<string, unknown>
|
|
343
|
+
| undefined) || {};
|
|
344
|
+
const currentPrice = parsePriceValue(
|
|
345
|
+
activePrice?.price ?? (product as any)?.price
|
|
346
|
+
);
|
|
347
|
+
const originalPrice = parsePriceValue(
|
|
348
|
+
activePrice?.retail_price ??
|
|
349
|
+
(product as any)?.retail_price ??
|
|
350
|
+
activePrice?.price ??
|
|
351
|
+
(product as any)?.price
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (index === 0) {
|
|
355
|
+
currencyLabel = normalizeCurrencyLabel(
|
|
356
|
+
activePrice?.currency_type ??
|
|
357
|
+
(product as any)?.currency_type ??
|
|
358
|
+
(product as any)?.currency
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
bundleTotal += currentPrice;
|
|
363
|
+
retailTotal += originalPrice > 0 ? originalPrice : currentPrice;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (retailTotal < bundleTotal) {
|
|
367
|
+
retailTotal = bundleTotal;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const savingsTotal = Math.max(0, retailTotal - bundleTotal);
|
|
371
|
+
const bundleText = formatBundleAmount(bundleTotal, currencyLabel);
|
|
372
|
+
const retailText = formatBundleAmount(retailTotal, currencyLabel);
|
|
373
|
+
const savingsText = formatBundleAmount(savingsTotal, currencyLabel);
|
|
374
|
+
const showRetailPrice = retailTotal > bundleTotal;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
bundlePriceHtml: showRetailPrice
|
|
378
|
+
? `<p style="margin:0;font-size:22px;line-height:1.2;display:flex;align-items:baseline;gap:8px;flex-wrap:nowrap;white-space:nowrap;"><span style='color:#94a3b8;text-decoration-line:line-through;text-decoration-color:#94a3b8;text-decoration-thickness:2px;font-size:16px;white-space:nowrap;display:inline-block;'>${retailText}</span><span style='color:#ffffff;font-weight:700;white-space:nowrap;display:inline-block;'>${bundleText}</span></p>`
|
|
379
|
+
: `<p style="margin:0;font-size:22px;line-height:1.2;"><span style='color:#ffffff;font-weight:700;'>${bundleText}</span></p>`,
|
|
380
|
+
savingsHtml: `<p style="margin:0;color:#22c55e;font-size:13px;font-weight:600;">You save ${savingsText}</p>`
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const applyFrequentlyBoughtSummaryValues = (
|
|
385
|
+
sourceBlock: Block,
|
|
386
|
+
bundlePriceHtml: string,
|
|
387
|
+
savingsHtml: string
|
|
388
|
+
): Block => {
|
|
389
|
+
const normalizedLabel = (sourceBlock.label || '').toLowerCase();
|
|
390
|
+
const nextBlock: Block = { ...sourceBlock };
|
|
391
|
+
|
|
392
|
+
if (sourceBlock.type === 'text') {
|
|
393
|
+
if (normalizedLabel.includes('bundle price')) {
|
|
394
|
+
nextBlock.value = bundlePriceHtml;
|
|
395
|
+
} else if (normalizedLabel.includes('savings')) {
|
|
396
|
+
nextBlock.value = savingsHtml;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (sourceBlock.blocks && sourceBlock.blocks.length > 0) {
|
|
401
|
+
nextBlock.blocks = sourceBlock.blocks.map((childBlock) =>
|
|
402
|
+
applyFrequentlyBoughtSummaryValues(
|
|
403
|
+
childBlock,
|
|
404
|
+
bundlePriceHtml,
|
|
405
|
+
savingsHtml
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return nextBlock;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const findFirstIteratorBlock = (sourceBlock: Block): Block | null => {
|
|
414
|
+
if (sourceBlock.isIterator) return sourceBlock;
|
|
415
|
+
if (!sourceBlock.blocks || sourceBlock.blocks.length === 0) return null;
|
|
416
|
+
|
|
417
|
+
for (const childBlock of sourceBlock.blocks) {
|
|
418
|
+
const found = findFirstIteratorBlock(childBlock);
|
|
419
|
+
if (found) return found;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return null;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const hasTabBlocks = effectiveSection.blocks.some(
|
|
426
|
+
(block) => block.type === 'tab'
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const CustomSectionRenderer = hasTabBlocks
|
|
430
|
+
? sectionRendererRegistry.getRenderer('tabs')
|
|
431
|
+
: sectionRendererRegistry.getRenderer(effectiveSection.type);
|
|
432
|
+
|
|
433
|
+
if (CustomSectionRenderer) {
|
|
434
|
+
return (
|
|
435
|
+
<SectionWrapper
|
|
436
|
+
section={effectiveSection}
|
|
437
|
+
placeholderId={placeholderId}
|
|
438
|
+
isDesigner={isDesigner}
|
|
439
|
+
isSelected={isSelected}
|
|
440
|
+
onSelect={onSelect}
|
|
441
|
+
onMoveUp={onMoveUp}
|
|
442
|
+
onMoveDown={onMoveDown}
|
|
443
|
+
onDuplicate={onDuplicate}
|
|
444
|
+
onToggleVisibility={onToggleVisibility}
|
|
445
|
+
onDelete={onDelete}
|
|
446
|
+
onRename={onRename}
|
|
447
|
+
>
|
|
448
|
+
<CustomSectionRenderer
|
|
449
|
+
section={effectiveSection}
|
|
450
|
+
currentBreakpoint={currentBreakpoint}
|
|
451
|
+
placeholderId={placeholderId}
|
|
452
|
+
isDesigner={isDesigner}
|
|
453
|
+
selectedBlockId={selectedBlockId}
|
|
454
|
+
/>
|
|
455
|
+
</SectionWrapper>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const resolveIteratorsRecursively = (block: Block): Block => {
|
|
460
|
+
const normalizedType = String(effectiveSection.type || '').toLowerCase();
|
|
461
|
+
const normalizedVariant = String(
|
|
462
|
+
effectiveSection.properties?.dataSourceVariant || ''
|
|
463
|
+
).toLowerCase();
|
|
464
|
+
const normalizedLabel = String(effectiveSection.label || '').toLowerCase();
|
|
465
|
+
const normalizedName = String(effectiveSection.name || '').toLowerCase();
|
|
466
|
+
const detectRelatedUpsellStructure = (blocks: Block[] = []) => {
|
|
467
|
+
let hasRelated = false;
|
|
468
|
+
let hasUpsell = false;
|
|
469
|
+
let hasGrid = false;
|
|
470
|
+
|
|
471
|
+
const walk = (items: Block[]) => {
|
|
472
|
+
items.forEach((item) => {
|
|
473
|
+
const itemLabel = String(item.label || '').toLowerCase();
|
|
474
|
+
if (itemLabel.includes('related')) hasRelated = true;
|
|
475
|
+
if (itemLabel.includes('upsell')) hasUpsell = true;
|
|
476
|
+
if (
|
|
477
|
+
item.styles &&
|
|
478
|
+
typeof item.styles === 'object' &&
|
|
479
|
+
item.styles['grid-template-columns'] !== undefined
|
|
480
|
+
) {
|
|
481
|
+
hasGrid = true;
|
|
482
|
+
}
|
|
483
|
+
if (item.blocks && item.blocks.length > 0) {
|
|
484
|
+
walk(item.blocks);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
walk(blocks);
|
|
490
|
+
return hasRelated && hasUpsell && hasGrid;
|
|
491
|
+
};
|
|
492
|
+
const hasRelatedUpsellStructure = detectRelatedUpsellStructure(
|
|
493
|
+
effectiveSection.blocks
|
|
494
|
+
);
|
|
495
|
+
const hasItemsPerColumnProperty =
|
|
496
|
+
effectiveSection.properties?.['items-per-column-desktop'] !== undefined ||
|
|
497
|
+
effectiveSection.properties?.itemsPerColumnDesktop !== undefined ||
|
|
498
|
+
effectiveSection.properties?.['items-per-column-mobile'] !== undefined ||
|
|
499
|
+
effectiveSection.properties?.itemsPerColumnMobile !== undefined ||
|
|
500
|
+
effectiveSection.properties?.['items-per-column'] !== undefined ||
|
|
501
|
+
effectiveSection.properties?.itemsPerColumn !== undefined;
|
|
502
|
+
const isRelatedUpsellSplitSection =
|
|
503
|
+
normalizedType === 'related-upsell-split' ||
|
|
504
|
+
normalizedType.includes('related-upsell') ||
|
|
505
|
+
normalizedVariant === 'related-upsell-split' ||
|
|
506
|
+
normalizedVariant.includes('related-upsell') ||
|
|
507
|
+
normalizedLabel.includes('related + upsell') ||
|
|
508
|
+
(normalizedLabel.includes('related') &&
|
|
509
|
+
normalizedLabel.includes('upsell')) ||
|
|
510
|
+
normalizedName.includes('related + upsell') ||
|
|
511
|
+
(normalizedName.includes('related') &&
|
|
512
|
+
normalizedName.includes('upsell')) ||
|
|
513
|
+
hasRelatedUpsellStructure ||
|
|
514
|
+
hasItemsPerColumnProperty;
|
|
515
|
+
|
|
516
|
+
const readSectionValue = (keys: string[], fallback: unknown) => {
|
|
517
|
+
for (const candidate of keys) {
|
|
518
|
+
const candidateValue = effectiveSection.properties?.[candidate];
|
|
519
|
+
if (candidateValue !== undefined && candidateValue !== null) {
|
|
520
|
+
return candidateValue;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return fallback;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const parseCount = (value: unknown, fallback: number): number => {
|
|
527
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
if (typeof value === 'string') {
|
|
531
|
+
const parsed = Number(value);
|
|
532
|
+
if (Number.isFinite(parsed)) {
|
|
533
|
+
return parsed;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
537
|
+
const responsive = value as Record<string, unknown>;
|
|
538
|
+
const orderedValues = [
|
|
539
|
+
responsive.desktop,
|
|
540
|
+
responsive.tablet,
|
|
541
|
+
responsive.mobile,
|
|
542
|
+
...Object.values(responsive)
|
|
543
|
+
];
|
|
544
|
+
for (const candidate of orderedValues) {
|
|
545
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
546
|
+
return candidate;
|
|
547
|
+
}
|
|
548
|
+
if (typeof candidate === 'string') {
|
|
549
|
+
const parsed = Number(candidate);
|
|
550
|
+
if (Number.isFinite(parsed)) {
|
|
551
|
+
return parsed;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return fallback;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const getSplitItemsPerColumn = () => {
|
|
560
|
+
const desktopCount = Math.max(
|
|
561
|
+
1,
|
|
562
|
+
Math.min(
|
|
563
|
+
6,
|
|
564
|
+
Math.floor(
|
|
565
|
+
parseCount(
|
|
566
|
+
readSectionValue(
|
|
567
|
+
['items-per-column-desktop', 'itemsPerColumnDesktop'],
|
|
568
|
+
readSectionValue(['items-per-column', 'itemsPerColumn'], 3)
|
|
569
|
+
),
|
|
570
|
+
3
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
const mobileCount = Math.max(
|
|
576
|
+
1,
|
|
577
|
+
Math.min(
|
|
578
|
+
4,
|
|
579
|
+
Math.floor(
|
|
580
|
+
parseCount(
|
|
581
|
+
readSectionValue(
|
|
582
|
+
['items-per-column-mobile', 'itemsPerColumnMobile'],
|
|
583
|
+
Math.min(desktopCount, 2)
|
|
584
|
+
),
|
|
585
|
+
Math.min(desktopCount, 2)
|
|
586
|
+
)
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
const tabletCount = Math.max(1, Math.min(desktopCount, 2));
|
|
591
|
+
const activeCount =
|
|
592
|
+
currentBreakpoint === 'mobile' ? mobileCount : desktopCount;
|
|
593
|
+
return {
|
|
594
|
+
desktop: desktopCount,
|
|
595
|
+
tablet: tabletCount,
|
|
596
|
+
mobile: mobileCount,
|
|
597
|
+
active: activeCount
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const inferSplitDataPaths = () => {
|
|
602
|
+
const collectArrayPaths = (
|
|
603
|
+
input: unknown,
|
|
604
|
+
prefix: string,
|
|
605
|
+
depth: number,
|
|
606
|
+
bucket: string[]
|
|
607
|
+
) => {
|
|
608
|
+
if (depth > 2 || input == null) return;
|
|
609
|
+
if (Array.isArray(input)) {
|
|
610
|
+
if (prefix) bucket.push(prefix);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (typeof input !== 'object') return;
|
|
614
|
+
|
|
615
|
+
Object.entries(input as Record<string, unknown>).forEach(
|
|
616
|
+
([key, value]) => {
|
|
617
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
618
|
+
if (Array.isArray(value)) {
|
|
619
|
+
bucket.push(nextPrefix);
|
|
620
|
+
} else if (value && typeof value === 'object') {
|
|
621
|
+
collectArrayPaths(value, nextPrefix, depth + 1, bucket);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const editorPayload =
|
|
628
|
+
effectiveSection.dataSource?.details?.collection?.products;
|
|
629
|
+
const runtimePayload =
|
|
630
|
+
effectiveSection.dataSource?.details?.collection?.data;
|
|
631
|
+
const samplePayload = runtimePayload ?? editorPayload;
|
|
632
|
+
|
|
633
|
+
if (Array.isArray(samplePayload)) {
|
|
634
|
+
return { relatedPath: 'products', upsellPath: 'products' };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const discoveredPaths: string[] = [];
|
|
638
|
+
collectArrayPaths(samplePayload, '', 0, discoveredPaths);
|
|
639
|
+
const uniquePaths = Array.from(new Set(discoveredPaths));
|
|
640
|
+
|
|
641
|
+
if (uniquePaths.length === 0) {
|
|
642
|
+
return { relatedPath: 'products', upsellPath: 'products' };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const pickByKeywords = (keywords: string[], exclude?: string) =>
|
|
646
|
+
uniquePaths.find((path) => {
|
|
647
|
+
if (exclude && path === exclude) return false;
|
|
648
|
+
const lower = path.toLowerCase();
|
|
649
|
+
return keywords.some((keyword) => lower.includes(keyword));
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const relatedPath =
|
|
653
|
+
pickByKeywords(['related', 'similar', 'cross', 'recommend']) ||
|
|
654
|
+
(uniquePaths.includes('products') ? 'products' : uniquePaths[0]);
|
|
655
|
+
const upsellPath =
|
|
656
|
+
pickByKeywords(
|
|
657
|
+
['upsell', 'complement', 'also', 'recommend', 'cross'],
|
|
658
|
+
relatedPath
|
|
659
|
+
) || (uniquePaths.includes('products') ? 'products' : relatedPath);
|
|
660
|
+
|
|
661
|
+
return { relatedPath, upsellPath };
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const isIteratorLike = (candidate: Block): boolean => {
|
|
665
|
+
if (candidate.isIterator) return true;
|
|
666
|
+
if (!isRelatedUpsellSplitSection) return false;
|
|
667
|
+
const normalizedLabel = (candidate.label || '').toLowerCase();
|
|
668
|
+
return (
|
|
669
|
+
normalizedLabel.includes('iterator') ||
|
|
670
|
+
Boolean(candidate.iteratorDataPath) ||
|
|
671
|
+
Boolean(candidate.properties?.iteratorDataPath) ||
|
|
672
|
+
candidate.properties?.iteratorCount !== undefined ||
|
|
673
|
+
candidate.properties?.useIteratorCount !== undefined ||
|
|
674
|
+
candidate.properties?.iteratorOffset !== undefined
|
|
675
|
+
);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const normalizeRelatedUpsellIterator = (iteratorBlock: Block): Block => {
|
|
679
|
+
if (!isRelatedUpsellSplitSection || !isIteratorLike(iteratorBlock)) {
|
|
680
|
+
return iteratorBlock;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const readString = (value: unknown, fallback: string): string => {
|
|
684
|
+
if (typeof value === 'string' && value.trim()) {
|
|
685
|
+
return value.trim();
|
|
686
|
+
}
|
|
687
|
+
return fallback;
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const splitItemsPerColumn = getSplitItemsPerColumn();
|
|
691
|
+
const inferredPaths = inferSplitDataPaths();
|
|
692
|
+
const relatedDataPath = readString(inferredPaths.relatedPath, 'products');
|
|
693
|
+
const upsellDataPath = readString(
|
|
694
|
+
inferredPaths.upsellPath,
|
|
695
|
+
relatedDataPath
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const isUpsellIterator = (iteratorBlock.label || '')
|
|
699
|
+
.toLowerCase()
|
|
700
|
+
.includes('upsell');
|
|
701
|
+
const isSameDataPath = relatedDataPath === upsellDataPath;
|
|
702
|
+
const nextPath = isUpsellIterator ? upsellDataPath : relatedDataPath;
|
|
703
|
+
const nextOffset =
|
|
704
|
+
isUpsellIterator && isSameDataPath ? splitItemsPerColumn.active : 0;
|
|
705
|
+
|
|
706
|
+
const existingStyles = iteratorBlock.styles || {};
|
|
707
|
+
const existingDisplay =
|
|
708
|
+
typeof existingStyles.display === 'object' &&
|
|
709
|
+
existingStyles.display !== null
|
|
710
|
+
? existingStyles.display
|
|
711
|
+
: {};
|
|
712
|
+
const existingColumns =
|
|
713
|
+
typeof existingStyles['grid-template-columns'] === 'object' &&
|
|
714
|
+
existingStyles['grid-template-columns'] !== null
|
|
715
|
+
? existingStyles['grid-template-columns']
|
|
716
|
+
: {};
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
...iteratorBlock,
|
|
720
|
+
isIterator: true,
|
|
721
|
+
iteratorDataPath: nextPath,
|
|
722
|
+
properties: {
|
|
723
|
+
...(iteratorBlock.properties || {}),
|
|
724
|
+
iteratorCount: splitItemsPerColumn.active,
|
|
725
|
+
iteratorDataPath: nextPath,
|
|
726
|
+
useIteratorCount: true,
|
|
727
|
+
iteratorOffset: nextOffset
|
|
728
|
+
},
|
|
729
|
+
styles: {
|
|
730
|
+
...existingStyles,
|
|
731
|
+
display: {
|
|
732
|
+
...existingDisplay,
|
|
733
|
+
desktop: 'grid',
|
|
734
|
+
mobile: 'grid'
|
|
735
|
+
},
|
|
736
|
+
'grid-template-columns': {
|
|
737
|
+
...existingColumns,
|
|
738
|
+
desktop: `repeat(${splitItemsPerColumn.desktop}, minmax(0, 1fr))`,
|
|
739
|
+
tablet:
|
|
740
|
+
existingColumns.tablet ||
|
|
741
|
+
`repeat(${splitItemsPerColumn.tablet}, minmax(0, 1fr))`,
|
|
742
|
+
mobile: `repeat(${splitItemsPerColumn.mobile}, minmax(0, 1fr))`
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const normalizeRelatedUpsellStaticGrid = (gridBlock: Block): Block => {
|
|
749
|
+
if (isIteratorLike(gridBlock) || !isRelatedUpsellSplitSection) {
|
|
750
|
+
return gridBlock;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const normalizedLabel = (gridBlock.label || '').toLowerCase();
|
|
754
|
+
const hasGridTemplateColumns =
|
|
755
|
+
gridBlock.styles &&
|
|
756
|
+
typeof gridBlock.styles === 'object' &&
|
|
757
|
+
gridBlock.styles['grid-template-columns'] !== undefined;
|
|
758
|
+
const hasProductCardChildren = Boolean(
|
|
759
|
+
gridBlock.blocks?.some((child) => {
|
|
760
|
+
const childLabel = String(child.label || '').toLowerCase();
|
|
761
|
+
return child.type === 'group' && childLabel.includes('product card');
|
|
762
|
+
})
|
|
763
|
+
);
|
|
764
|
+
const isSplitGrid =
|
|
765
|
+
normalizedLabel.includes('related grid') ||
|
|
766
|
+
normalizedLabel.includes('upsell grid') ||
|
|
767
|
+
(hasGridTemplateColumns && hasProductCardChildren);
|
|
768
|
+
|
|
769
|
+
if (!isSplitGrid || !gridBlock.blocks || gridBlock.blocks.length === 0) {
|
|
770
|
+
return gridBlock;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const hasIteratorChild = gridBlock.blocks.some((child) =>
|
|
774
|
+
isIteratorLike(child)
|
|
775
|
+
);
|
|
776
|
+
if (hasIteratorChild) {
|
|
777
|
+
return gridBlock;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const splitItemsPerColumn = getSplitItemsPerColumn();
|
|
781
|
+
const itemsPerColumn = splitItemsPerColumn.active;
|
|
782
|
+
|
|
783
|
+
const cardBlocks = gridBlock.blocks
|
|
784
|
+
.filter((child) =>
|
|
785
|
+
(child.label || '').toLowerCase().includes('product card')
|
|
786
|
+
)
|
|
787
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
788
|
+
|
|
789
|
+
if (cardBlocks.length === 0) {
|
|
790
|
+
return gridBlock;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const cloneCard = (index: number): Block => {
|
|
794
|
+
const cloneWithIndex = (target: Block, suffix: string): Block => ({
|
|
795
|
+
...target,
|
|
796
|
+
id: `${target.id}-preview-${suffix}`,
|
|
797
|
+
properties: target.properties
|
|
798
|
+
? { ...target.properties }
|
|
799
|
+
: target.properties,
|
|
800
|
+
styles: target.styles
|
|
801
|
+
? JSON.parse(JSON.stringify(target.styles))
|
|
802
|
+
: target.styles,
|
|
803
|
+
blocks: target.blocks
|
|
804
|
+
? target.blocks.map((child, childIndex) =>
|
|
805
|
+
cloneWithIndex(child, `${suffix}-${childIndex}`)
|
|
806
|
+
)
|
|
807
|
+
: undefined
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const sourceCard = cardBlocks[index % cardBlocks.length];
|
|
811
|
+
const nextCard =
|
|
812
|
+
index < cardBlocks.length
|
|
813
|
+
? {
|
|
814
|
+
...sourceCard,
|
|
815
|
+
properties: sourceCard.properties
|
|
816
|
+
? { ...sourceCard.properties }
|
|
817
|
+
: sourceCard.properties,
|
|
818
|
+
styles: sourceCard.styles
|
|
819
|
+
? JSON.parse(JSON.stringify(sourceCard.styles))
|
|
820
|
+
: sourceCard.styles,
|
|
821
|
+
blocks: sourceCard.blocks
|
|
822
|
+
? sourceCard.blocks.map((child, childIndex) =>
|
|
823
|
+
cloneWithIndex(child, `${index}-${childIndex}`)
|
|
824
|
+
)
|
|
825
|
+
: undefined
|
|
826
|
+
}
|
|
827
|
+
: cloneWithIndex(sourceCard, `${index}`);
|
|
828
|
+
|
|
829
|
+
nextCard.order = index;
|
|
830
|
+
nextCard.hidden = false;
|
|
831
|
+
return nextCard;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const normalizedCards = Array.from(
|
|
835
|
+
{ length: itemsPerColumn },
|
|
836
|
+
(_, index) => cloneCard(index)
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
const existingStyles = gridBlock.styles || {};
|
|
840
|
+
const existingColumns =
|
|
841
|
+
typeof existingStyles['grid-template-columns'] === 'object' &&
|
|
842
|
+
existingStyles['grid-template-columns'] !== null
|
|
843
|
+
? existingStyles['grid-template-columns']
|
|
844
|
+
: {};
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
...gridBlock,
|
|
848
|
+
styles: {
|
|
849
|
+
...existingStyles,
|
|
850
|
+
'grid-template-columns': {
|
|
851
|
+
...existingColumns,
|
|
852
|
+
desktop: `repeat(${splitItemsPerColumn.desktop}, minmax(0, 1fr))`,
|
|
853
|
+
tablet:
|
|
854
|
+
existingColumns.tablet ||
|
|
855
|
+
`repeat(${splitItemsPerColumn.tablet}, minmax(0, 1fr))`,
|
|
856
|
+
mobile: `repeat(${splitItemsPerColumn.mobile}, minmax(0, 1fr))`
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
blocks: normalizedCards
|
|
860
|
+
};
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
const normalizedGridBlock = normalizeRelatedUpsellStaticGrid(block);
|
|
864
|
+
|
|
865
|
+
const normalizedBlock = isIteratorLike(normalizedGridBlock)
|
|
866
|
+
? normalizeRelatedUpsellIterator(normalizedGridBlock)
|
|
867
|
+
: normalizedGridBlock;
|
|
868
|
+
|
|
869
|
+
const splitItemsPerColumn = getSplitItemsPerColumn();
|
|
870
|
+
const forcedItemsPerColumn = splitItemsPerColumn.active;
|
|
871
|
+
const inferredPathsForForce = inferSplitDataPaths();
|
|
872
|
+
const relatedDataPathRaw = String(inferredPathsForForce.relatedPath).trim();
|
|
873
|
+
const upsellDataPathRaw = String(inferredPathsForForce.upsellPath).trim();
|
|
874
|
+
const isUpsellIteratorLike = (normalizedBlock.label || '')
|
|
875
|
+
.toLowerCase()
|
|
876
|
+
.includes('upsell');
|
|
877
|
+
const forceIteratorOffset =
|
|
878
|
+
isUpsellIteratorLike && relatedDataPathRaw === upsellDataPathRaw
|
|
879
|
+
? forcedItemsPerColumn
|
|
880
|
+
: 0;
|
|
881
|
+
|
|
882
|
+
const resolvedBlock = isIteratorLike(normalizedBlock)
|
|
883
|
+
? buildIteratorBlock({
|
|
884
|
+
iteratorBlock: normalizedBlock,
|
|
885
|
+
sectionDataSource: effectiveSection.dataSource,
|
|
886
|
+
isDesigner,
|
|
887
|
+
forceIteratorCount: isRelatedUpsellSplitSection
|
|
888
|
+
? forcedItemsPerColumn
|
|
889
|
+
: undefined,
|
|
890
|
+
forceIteratorOffset: isRelatedUpsellSplitSection
|
|
891
|
+
? forceIteratorOffset
|
|
892
|
+
: undefined
|
|
893
|
+
})
|
|
894
|
+
: normalizedBlock;
|
|
895
|
+
|
|
896
|
+
if (!resolvedBlock.blocks || resolvedBlock.blocks.length === 0) {
|
|
897
|
+
return resolvedBlock;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
...resolvedBlock,
|
|
902
|
+
blocks: resolvedBlock.blocks.map(resolveIteratorsRecursively)
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<section
|
|
908
|
+
ref={sectionRef}
|
|
909
|
+
data-section-id={effectiveSection.id}
|
|
910
|
+
data-newsletter-signup={isNewsletterSection ? 'true' : undefined}
|
|
911
|
+
data-newsletter-endpoint={
|
|
912
|
+
isNewsletterSection ? newsletterEndpoint : undefined
|
|
913
|
+
}
|
|
914
|
+
data-newsletter-success-message={
|
|
915
|
+
isNewsletterSection && newsletterSuccessMessage
|
|
916
|
+
? newsletterSuccessMessage
|
|
917
|
+
: undefined
|
|
918
|
+
}
|
|
919
|
+
data-newsletter-error-message={
|
|
920
|
+
isNewsletterSection && newsletterErrorMessage
|
|
921
|
+
? newsletterErrorMessage
|
|
922
|
+
: undefined
|
|
923
|
+
}
|
|
924
|
+
className={`theme-section relative ${
|
|
925
|
+
isDesigner ? 'cursor-pointer group/section' : ''
|
|
926
|
+
}`}
|
|
927
|
+
onClick={handleClick}
|
|
928
|
+
>
|
|
929
|
+
{isDesigner && (
|
|
930
|
+
<div
|
|
931
|
+
className={twMerge(
|
|
932
|
+
clsx(
|
|
933
|
+
'absolute inset-0 pointer-events-none z-0 border-2 transition-all',
|
|
934
|
+
effectiveSection.locked
|
|
935
|
+
? isSelected
|
|
936
|
+
? 'border-dashed border-amber-400 bg-amber-400/[0.12] shadow-[0_0_0_1px_rgba(251,191,36,0.4)]'
|
|
937
|
+
: 'border-dashed border-amber-300/25 bg-amber-400/[0.03] group-hover/section:border-amber-300/55 group-hover/section:bg-amber-400/[0.07]'
|
|
938
|
+
: 'border-transparent group-hover/section:border-[#4482ff] group-hover/section:bg-[#4482ff]/10',
|
|
939
|
+
!effectiveSection.locked && isSelected && 'border-[#4482ff]'
|
|
940
|
+
)
|
|
941
|
+
)}
|
|
942
|
+
/>
|
|
943
|
+
)}
|
|
944
|
+
{isDesigner && isSelected && (
|
|
945
|
+
<ActionToolbar
|
|
946
|
+
label={effectiveSection.label}
|
|
947
|
+
isLocked={effectiveSection.locked === true}
|
|
948
|
+
zIndex={20}
|
|
949
|
+
onMoveUp={onMoveUp}
|
|
950
|
+
onMoveDown={onMoveDown}
|
|
951
|
+
onDuplicate={onDuplicate}
|
|
952
|
+
onToggleVisibility={onToggleVisibility}
|
|
953
|
+
onDelete={onDelete}
|
|
954
|
+
onRename={onRename}
|
|
955
|
+
/>
|
|
956
|
+
)}
|
|
957
|
+
|
|
958
|
+
<div className={twMerge(clsx('contents'))}>
|
|
959
|
+
{effectiveSection.blocks
|
|
960
|
+
.sort((a, b) => a.order - b.order)
|
|
961
|
+
.filter((block) => (isDesigner ? true : !block.hidden))
|
|
962
|
+
.map((block) => {
|
|
963
|
+
const resolvedBlock = resolveIteratorsRecursively(block);
|
|
964
|
+
const createActionHandler = (actionType: string) => () => {
|
|
965
|
+
if (window.parent) {
|
|
966
|
+
window.parent.postMessage(
|
|
967
|
+
{
|
|
968
|
+
type: actionType,
|
|
969
|
+
data: {
|
|
970
|
+
placeholderId,
|
|
971
|
+
sectionId: effectiveSection.id,
|
|
972
|
+
blockId: block.id
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
'*'
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const handleRename = (newLabel: string) => {
|
|
981
|
+
if (window.parent) {
|
|
982
|
+
window.parent.postMessage(
|
|
983
|
+
{
|
|
984
|
+
type: 'RENAME_BLOCK',
|
|
985
|
+
data: {
|
|
986
|
+
placeholderId,
|
|
987
|
+
sectionId: effectiveSection.id,
|
|
988
|
+
blockId: block.id,
|
|
989
|
+
label: newLabel
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
'*'
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
const renderBlockWithActions = (renderedBlock: Block) => (
|
|
997
|
+
<ThemeBlock
|
|
998
|
+
key={renderedBlock.id}
|
|
999
|
+
block={renderedBlock}
|
|
1000
|
+
placeholderId={placeholderId}
|
|
1001
|
+
sectionId={effectiveSection.id}
|
|
1002
|
+
isDesigner={isDesigner}
|
|
1003
|
+
isSelected={selectedBlockId === block.id}
|
|
1004
|
+
selectedBlockId={selectedBlockId}
|
|
1005
|
+
currentBreakpoint={currentBreakpoint}
|
|
1006
|
+
onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
|
|
1007
|
+
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
|
|
1008
|
+
onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
|
|
1009
|
+
onToggleVisibility={createActionHandler(
|
|
1010
|
+
'TOGGLE_BLOCK_VISIBILITY'
|
|
1011
|
+
)}
|
|
1012
|
+
onDelete={createActionHandler('DELETE_BLOCK')}
|
|
1013
|
+
onRename={handleRename}
|
|
1014
|
+
/>
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
if (!isFrequentlyBoughtTogetherSection) {
|
|
1018
|
+
return renderBlockWithActions(resolvedBlock);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const replaceFrequentlyBoughtBlockValues = (
|
|
1022
|
+
blockToReplace: Block,
|
|
1023
|
+
productData: Record<string, unknown>,
|
|
1024
|
+
productIndex: number
|
|
1025
|
+
): Block => {
|
|
1026
|
+
const newBlock = { ...blockToReplace };
|
|
1027
|
+
const isPlaceholderMode =
|
|
1028
|
+
!productData || Object.keys(productData).length === 0;
|
|
1029
|
+
|
|
1030
|
+
if (newBlock.properties?.dataBinding && !isPlaceholderMode) {
|
|
1031
|
+
const bindingPath = String(
|
|
1032
|
+
newBlock.properties.dataBinding
|
|
1033
|
+
).replace('item.', '');
|
|
1034
|
+
const pathParts = bindingPath.split('.');
|
|
1035
|
+
|
|
1036
|
+
let value: unknown = productData;
|
|
1037
|
+
for (const part of pathParts) {
|
|
1038
|
+
const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
|
|
1039
|
+
if (arrayMatch) {
|
|
1040
|
+
const [, arrayName, indexStr] = arrayMatch;
|
|
1041
|
+
const obj = value as Record<string, unknown>;
|
|
1042
|
+
const arr = obj?.[arrayName];
|
|
1043
|
+
value = Array.isArray(arr)
|
|
1044
|
+
? arr[parseInt(indexStr, 10)]
|
|
1045
|
+
: undefined;
|
|
1046
|
+
} else {
|
|
1047
|
+
value = (value as Record<string, unknown>)?.[part];
|
|
1048
|
+
}
|
|
1049
|
+
if (value === undefined) break;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (value !== undefined) {
|
|
1053
|
+
if (
|
|
1054
|
+
newBlock.type === 'text' &&
|
|
1055
|
+
bindingPath === 'active_price.price'
|
|
1056
|
+
) {
|
|
1057
|
+
newBlock.value =
|
|
1058
|
+
buildFrequentlyBoughtPriceHtml(productData);
|
|
1059
|
+
return newBlock;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (newBlock.properties.tag === 'a') {
|
|
1063
|
+
newBlock.properties = {
|
|
1064
|
+
...newBlock.properties,
|
|
1065
|
+
href: value
|
|
1066
|
+
};
|
|
1067
|
+
} else {
|
|
1068
|
+
newBlock.value = value;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} else if (isPlaceholderMode && newBlock.type === 'image') {
|
|
1072
|
+
const placeholderImages = [
|
|
1073
|
+
'/assets/images/product-placeholder-1.jpg',
|
|
1074
|
+
'/assets/images/product-placeholder-2.jpg',
|
|
1075
|
+
'/assets/images/product-placeholder-3.jpg',
|
|
1076
|
+
'/assets/images/product-placeholder-4.jpg'
|
|
1077
|
+
];
|
|
1078
|
+
const placeholderIndex =
|
|
1079
|
+
productIndex % placeholderImages.length;
|
|
1080
|
+
|
|
1081
|
+
if (
|
|
1082
|
+
newBlock.value &&
|
|
1083
|
+
typeof newBlock.value === 'string' &&
|
|
1084
|
+
newBlock.value.includes('product-placeholder')
|
|
1085
|
+
) {
|
|
1086
|
+
newBlock.value = placeholderImages[placeholderIndex];
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (newBlock.blocks && newBlock.blocks.length > 0) {
|
|
1091
|
+
newBlock.blocks = newBlock.blocks.map((childBlock) =>
|
|
1092
|
+
replaceFrequentlyBoughtBlockValues(
|
|
1093
|
+
childBlock,
|
|
1094
|
+
productData,
|
|
1095
|
+
productIndex
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return newBlock;
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
if (block.isIterator && block.blocks && block.blocks.length > 0) {
|
|
1104
|
+
const template = block.blocks[0];
|
|
1105
|
+
const { products, actualCount } = getIteratorRenderMeta(block);
|
|
1106
|
+
|
|
1107
|
+
const clonedBlocks = Array.from(
|
|
1108
|
+
{ length: actualCount },
|
|
1109
|
+
(_, index) => {
|
|
1110
|
+
const product = products[index] || {};
|
|
1111
|
+
const templateCopy = {
|
|
1112
|
+
...template,
|
|
1113
|
+
id: `${template.id}-clone-${index}`,
|
|
1114
|
+
styleSourceId: template.id,
|
|
1115
|
+
blocks: template.blocks
|
|
1116
|
+
? [
|
|
1117
|
+
...template.blocks.map((childBlock) => ({
|
|
1118
|
+
...childBlock,
|
|
1119
|
+
styleSourceId: childBlock.id
|
|
1120
|
+
}))
|
|
1121
|
+
]
|
|
1122
|
+
: undefined
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
return replaceFrequentlyBoughtBlockValues(
|
|
1126
|
+
templateCopy,
|
|
1127
|
+
product,
|
|
1128
|
+
index
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
return renderBlockWithActions({
|
|
1134
|
+
...block,
|
|
1135
|
+
blocks: clonedBlocks
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const buildNestedIteratorClones = (
|
|
1140
|
+
iteratorBlock: Block
|
|
1141
|
+
): Block[] => {
|
|
1142
|
+
if (!iteratorBlock.blocks || iteratorBlock.blocks.length === 0) {
|
|
1143
|
+
return iteratorBlock.blocks || [];
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const template = iteratorBlock.blocks[0];
|
|
1147
|
+
const { products, actualCount } =
|
|
1148
|
+
getIteratorRenderMeta(iteratorBlock);
|
|
1149
|
+
|
|
1150
|
+
return Array.from({ length: actualCount }, (_, index) => {
|
|
1151
|
+
const product = products[index] || {};
|
|
1152
|
+
const templateCopy = {
|
|
1153
|
+
...template,
|
|
1154
|
+
id: `${template.id}-clone-${index}`,
|
|
1155
|
+
styleSourceId: template.id,
|
|
1156
|
+
blocks: template.blocks
|
|
1157
|
+
? [
|
|
1158
|
+
...template.blocks.map((childBlock) => ({
|
|
1159
|
+
...childBlock,
|
|
1160
|
+
styleSourceId: childBlock.id
|
|
1161
|
+
}))
|
|
1162
|
+
]
|
|
1163
|
+
: undefined
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
return replaceFrequentlyBoughtBlockValues(
|
|
1167
|
+
templateCopy,
|
|
1168
|
+
product,
|
|
1169
|
+
index
|
|
1170
|
+
);
|
|
1171
|
+
});
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const expandNestedIterators = (sourceBlock: Block): Block => {
|
|
1175
|
+
if (!sourceBlock.blocks || sourceBlock.blocks.length === 0) {
|
|
1176
|
+
return sourceBlock;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return {
|
|
1180
|
+
...sourceBlock,
|
|
1181
|
+
blocks: sourceBlock.blocks.map((childBlock) => {
|
|
1182
|
+
if (
|
|
1183
|
+
childBlock.isIterator &&
|
|
1184
|
+
childBlock.blocks &&
|
|
1185
|
+
childBlock.blocks.length > 0
|
|
1186
|
+
) {
|
|
1187
|
+
return {
|
|
1188
|
+
...childBlock,
|
|
1189
|
+
blocks: buildNestedIteratorClones(childBlock)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return expandNestedIterators(childBlock);
|
|
1194
|
+
})
|
|
1195
|
+
};
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const renderedBlock = expandNestedIterators(block);
|
|
1199
|
+
let finalRenderedBlock = renderedBlock;
|
|
1200
|
+
|
|
1201
|
+
if (effectiveSection.dataSourceId || effectiveSection.dataSource) {
|
|
1202
|
+
const iteratorBlockForSummary = findFirstIteratorBlock(block);
|
|
1203
|
+
|
|
1204
|
+
if (iteratorBlockForSummary) {
|
|
1205
|
+
const { products, actualCount } = getIteratorRenderMeta(
|
|
1206
|
+
iteratorBlockForSummary
|
|
1207
|
+
);
|
|
1208
|
+
const { bundlePriceHtml, savingsHtml } =
|
|
1209
|
+
computeFrequentlyBoughtSummary(products, actualCount);
|
|
1210
|
+
|
|
1211
|
+
finalRenderedBlock = applyFrequentlyBoughtSummaryValues(
|
|
1212
|
+
renderedBlock,
|
|
1213
|
+
bundlePriceHtml,
|
|
1214
|
+
savingsHtml
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return renderBlockWithActions(finalRenderedBlock);
|
|
1220
|
+
})}
|
|
1221
|
+
</div>
|
|
1222
|
+
</section>
|
|
1223
|
+
);
|
|
1224
|
+
}
|