@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,1065 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Image } from '@akinon/next/components/image';
|
|
4
|
+
import { useGetInstallmentsQuery, useGetProductByPkQuery } from '@akinon/next/data/client/product';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { LoaderSpinner } from '../../loader-spinner';
|
|
10
|
+
import { Price } from '../../price';
|
|
11
|
+
import ThemeBlock, { Block } from '../theme-block';
|
|
12
|
+
import { WithDesignerFeatures } from '../components/with-designer-features';
|
|
13
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
14
|
+
import { Section } from '../theme-section';
|
|
15
|
+
import {
|
|
16
|
+
getCSSStyles,
|
|
17
|
+
getResponsiveValue,
|
|
18
|
+
resolveThemeCssVariables
|
|
19
|
+
} from '../utils';
|
|
20
|
+
|
|
21
|
+
interface InstallmentOptionsSectionProps {
|
|
22
|
+
section: Section;
|
|
23
|
+
currentBreakpoint?: string;
|
|
24
|
+
placeholderId?: string;
|
|
25
|
+
isDesigner?: boolean;
|
|
26
|
+
selectedBlockId?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface InstallmentPlan {
|
|
30
|
+
installment_count: number;
|
|
31
|
+
single_installment_amount: string;
|
|
32
|
+
total_amount: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface InstallmentCard {
|
|
36
|
+
pk: number;
|
|
37
|
+
slug: string;
|
|
38
|
+
name: string;
|
|
39
|
+
card_type?: {
|
|
40
|
+
logo?: string;
|
|
41
|
+
};
|
|
42
|
+
installments: InstallmentPlan[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InstallmentResponse {
|
|
46
|
+
results: InstallmentCard[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ProductLike = Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
const PLACEHOLDER_PRODUCT = {
|
|
52
|
+
name: 'Premium Running Sneaker',
|
|
53
|
+
price: '4.999 TL',
|
|
54
|
+
retail_price: '5.699 TL',
|
|
55
|
+
currency_type: 'TRY',
|
|
56
|
+
absolute_url: '#'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const PLACEHOLDER_INSTALLMENTS: InstallmentCard[] = [
|
|
60
|
+
{
|
|
61
|
+
pk: 1,
|
|
62
|
+
slug: 'visa',
|
|
63
|
+
name: 'Visa',
|
|
64
|
+
installments: [
|
|
65
|
+
{
|
|
66
|
+
installment_count: 1,
|
|
67
|
+
single_installment_amount: '4.999',
|
|
68
|
+
total_amount: '4.999'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
installment_count: 3,
|
|
72
|
+
single_installment_amount: '1.716,33',
|
|
73
|
+
total_amount: '5.149'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
installment_count: 6,
|
|
77
|
+
single_installment_amount: '899,83',
|
|
78
|
+
total_amount: '5.399'
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pk: 2,
|
|
84
|
+
slug: 'mastercard',
|
|
85
|
+
name: 'Mastercard',
|
|
86
|
+
installments: [
|
|
87
|
+
{
|
|
88
|
+
installment_count: 1,
|
|
89
|
+
single_installment_amount: '4.999',
|
|
90
|
+
total_amount: '4.999'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
installment_count: 4,
|
|
94
|
+
single_installment_amount: '1.337,25',
|
|
95
|
+
total_amount: '5.349'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
installment_count: 8,
|
|
99
|
+
single_installment_amount: '706,12',
|
|
100
|
+
total_amount: '5.649'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
pk: 3,
|
|
106
|
+
slug: 'amex',
|
|
107
|
+
name: 'Amex',
|
|
108
|
+
installments: [
|
|
109
|
+
{
|
|
110
|
+
installment_count: 1,
|
|
111
|
+
single_installment_amount: '4.999',
|
|
112
|
+
total_amount: '4.999'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
installment_count: 2,
|
|
116
|
+
single_installment_amount: '2.599,50',
|
|
117
|
+
total_amount: '5.199'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
installment_count: 5,
|
|
121
|
+
single_installment_amount: '1.119,80',
|
|
122
|
+
total_amount: '5.599'
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const customStyleKeys = [
|
|
129
|
+
'max-width',
|
|
130
|
+
'panel-background-color',
|
|
131
|
+
'panel-border-color',
|
|
132
|
+
'panel-border-radius',
|
|
133
|
+
'product-card-background-color',
|
|
134
|
+
'product-card-border-color',
|
|
135
|
+
'logo-card-background-color',
|
|
136
|
+
'logo-card-border-color',
|
|
137
|
+
'logo-card-active-background-color',
|
|
138
|
+
'logo-card-active-border-color',
|
|
139
|
+
'table-header-color',
|
|
140
|
+
'table-row-border-color',
|
|
141
|
+
'highlight-row-background-color',
|
|
142
|
+
'highlight-row-text-color'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const readStringProperty = (
|
|
146
|
+
properties: Record<string, unknown> | undefined,
|
|
147
|
+
key: string,
|
|
148
|
+
fallback: string,
|
|
149
|
+
breakpoint: string
|
|
150
|
+
): string => {
|
|
151
|
+
const value = getResponsiveValue(properties?.[key], breakpoint, fallback);
|
|
152
|
+
if (value == null) return fallback;
|
|
153
|
+
return String(value);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const readBooleanProperty = (
|
|
157
|
+
properties: Record<string, unknown> | undefined,
|
|
158
|
+
key: string,
|
|
159
|
+
fallback: boolean,
|
|
160
|
+
breakpoint: string
|
|
161
|
+
): boolean => {
|
|
162
|
+
const value = getResponsiveValue(properties?.[key], breakpoint, fallback);
|
|
163
|
+
if (typeof value === 'boolean') return value;
|
|
164
|
+
if (typeof value === 'string') {
|
|
165
|
+
const normalized = value.trim().toLowerCase();
|
|
166
|
+
if (normalized === 'true') return true;
|
|
167
|
+
if (normalized === 'false') return false;
|
|
168
|
+
}
|
|
169
|
+
return fallback;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const readStyleValue = (
|
|
173
|
+
styles: Record<string, unknown> | undefined,
|
|
174
|
+
key: string,
|
|
175
|
+
fallback: string,
|
|
176
|
+
breakpoint: string,
|
|
177
|
+
themeSettings: Record<string, unknown>
|
|
178
|
+
): string => {
|
|
179
|
+
return resolveThemeCssVariables(
|
|
180
|
+
String(getResponsiveValue(styles?.[key], breakpoint, fallback)),
|
|
181
|
+
themeSettings
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const parsePriceValue = (value: unknown): number | null => {
|
|
186
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof value !== 'string') return null;
|
|
191
|
+
const cleaned = value.trim().replace(/[^\d,.-]/g, '');
|
|
192
|
+
if (!cleaned) return null;
|
|
193
|
+
|
|
194
|
+
let normalized = cleaned;
|
|
195
|
+
const hasComma = normalized.includes(',');
|
|
196
|
+
const hasDot = normalized.includes('.');
|
|
197
|
+
|
|
198
|
+
if (hasComma && hasDot) {
|
|
199
|
+
const lastComma = normalized.lastIndexOf(',');
|
|
200
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
201
|
+
normalized =
|
|
202
|
+
lastComma > lastDot
|
|
203
|
+
? normalized.replace(/\./g, '').replace(',', '.')
|
|
204
|
+
: normalized.replace(/,/g, '');
|
|
205
|
+
} else if (hasComma) {
|
|
206
|
+
const unsigned = normalized.replace(/^-/, '');
|
|
207
|
+
const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
|
|
208
|
+
normalized = isThousandsPattern
|
|
209
|
+
? normalized.replace(/,/g, '')
|
|
210
|
+
: normalized.replace(/,/g, '.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parsed = Number(normalized);
|
|
214
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const normalizeCurrencyLabel = (currency: unknown): string => {
|
|
218
|
+
const raw = String(currency || '').trim().toUpperCase();
|
|
219
|
+
if (!raw) return 'TL';
|
|
220
|
+
|
|
221
|
+
const map: Record<string, string> = {
|
|
222
|
+
TRY: 'TL',
|
|
223
|
+
TL: 'TL',
|
|
224
|
+
USD: 'USD',
|
|
225
|
+
EUR: 'EUR',
|
|
226
|
+
GBP: 'GBP'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return map[raw] || raw;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const formatPriceWithCurrency = (
|
|
233
|
+
value: unknown,
|
|
234
|
+
currency: unknown
|
|
235
|
+
): string | null => {
|
|
236
|
+
if (value == null || value === '') return null;
|
|
237
|
+
|
|
238
|
+
const price = parsePriceValue(value);
|
|
239
|
+
if (price === null) {
|
|
240
|
+
const text = String(value).trim();
|
|
241
|
+
return text || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const hasDecimals = Math.abs(price % 1) > 0.00001;
|
|
245
|
+
const formatted = price.toLocaleString('tr-TR', {
|
|
246
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
247
|
+
maximumFractionDigits: 2
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return `${formatted} ${normalizeCurrencyLabel(currency)}`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const getCollectionProducts = (
|
|
254
|
+
section: Section,
|
|
255
|
+
isDesigner: boolean
|
|
256
|
+
): ProductLike[] => {
|
|
257
|
+
const collectionDetails = section.dataSource?.details?.collection;
|
|
258
|
+
const staticData = section.dataSource?.details?.static?.data;
|
|
259
|
+
const isEditorMode =
|
|
260
|
+
typeof window !== 'undefined' && isDesigner && window.parent !== window;
|
|
261
|
+
|
|
262
|
+
const collectionPayload = isEditorMode
|
|
263
|
+
? collectionDetails?.products || collectionDetails?.data
|
|
264
|
+
: collectionDetails?.data || collectionDetails?.products;
|
|
265
|
+
|
|
266
|
+
if (Array.isArray(collectionPayload)) {
|
|
267
|
+
return collectionPayload as ProductLike[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (Array.isArray((collectionPayload as ProductLike | undefined)?.products)) {
|
|
271
|
+
return (collectionPayload as ProductLike).products as ProductLike[];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (Array.isArray((collectionPayload as ProductLike | undefined)?.items)) {
|
|
275
|
+
return (collectionPayload as ProductLike).items as ProductLike[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (Array.isArray(staticData)) {
|
|
279
|
+
return staticData as ProductLike[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [];
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const parseProductPk = (value: unknown): number | null => {
|
|
286
|
+
const raw = String(value ?? '').trim();
|
|
287
|
+
if (!raw) return null;
|
|
288
|
+
const parsed = Number.parseInt(raw, 10);
|
|
289
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const getProductImage = (product: ProductLike | null): string | null => {
|
|
293
|
+
if (!product) return null;
|
|
294
|
+
|
|
295
|
+
if (typeof product.image === 'string' && product.image) {
|
|
296
|
+
return product.image;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const imageSet = product.productimage_set;
|
|
300
|
+
if (Array.isArray(imageSet)) {
|
|
301
|
+
const firstImage = imageSet[0] as ProductLike | undefined;
|
|
302
|
+
if (typeof firstImage?.image === 'string' && firstImage.image) {
|
|
303
|
+
return firstImage.image;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const getProductPrice = (product: ProductLike | null): string | null => {
|
|
311
|
+
if (!product) return null;
|
|
312
|
+
const activePrice = (product.active_price as ProductLike | undefined) || {};
|
|
313
|
+
return formatPriceWithCurrency(
|
|
314
|
+
activePrice.price ?? product.price,
|
|
315
|
+
activePrice.currency_type ?? product.currency_type
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const getProductRetailPrice = (product: ProductLike | null): string | null => {
|
|
320
|
+
if (!product) return null;
|
|
321
|
+
const activePrice = (product.active_price as ProductLike | undefined) || {};
|
|
322
|
+
const retailValue = activePrice.retail_price ?? product.retail_price;
|
|
323
|
+
const currentValue = activePrice.price ?? product.price;
|
|
324
|
+
const retailPrice = parsePriceValue(retailValue);
|
|
325
|
+
const currentPrice = parsePriceValue(currentValue);
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
retailPrice === null ||
|
|
329
|
+
currentPrice === null ||
|
|
330
|
+
retailPrice <= currentPrice
|
|
331
|
+
) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return formatPriceWithCurrency(
|
|
336
|
+
retailValue,
|
|
337
|
+
activePrice.currency_type ?? product.currency_type
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const getFallbackCardLabel = (name: string): string => {
|
|
342
|
+
const words = name
|
|
343
|
+
.split(/\s+/)
|
|
344
|
+
.map((part) => part.trim())
|
|
345
|
+
.filter(Boolean);
|
|
346
|
+
|
|
347
|
+
if (words.length === 0) return 'CARD';
|
|
348
|
+
if (words.length === 1) return words[0].slice(0, 4).toUpperCase();
|
|
349
|
+
|
|
350
|
+
return words
|
|
351
|
+
.slice(0, 2)
|
|
352
|
+
.map((word) => word[0])
|
|
353
|
+
.join('')
|
|
354
|
+
.toUpperCase();
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const resolveResponsiveString = (
|
|
358
|
+
value: unknown,
|
|
359
|
+
breakpoint: string
|
|
360
|
+
): string => {
|
|
361
|
+
if (typeof value === 'string') return value;
|
|
362
|
+
|
|
363
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
364
|
+
const responsiveValue = getResponsiveValue(value, breakpoint, '');
|
|
365
|
+
if (typeof responsiveValue === 'string') {
|
|
366
|
+
return responsiveValue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return '';
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const getInstallmentRole = (
|
|
374
|
+
block: Pick<Block, 'properties'>,
|
|
375
|
+
breakpoint: string
|
|
376
|
+
): string =>
|
|
377
|
+
resolveResponsiveString(block.properties?.installmentRole, breakpoint);
|
|
378
|
+
|
|
379
|
+
const findInstallmentRoleBlock = (
|
|
380
|
+
blocks: Block[],
|
|
381
|
+
role: string,
|
|
382
|
+
breakpoint: string
|
|
383
|
+
): Block | null => {
|
|
384
|
+
for (const block of blocks) {
|
|
385
|
+
if (getInstallmentRole(block, breakpoint) === role) {
|
|
386
|
+
return block;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (block.blocks?.length) {
|
|
390
|
+
const nestedMatch = findInstallmentRoleBlock(
|
|
391
|
+
block.blocks,
|
|
392
|
+
role,
|
|
393
|
+
breakpoint
|
|
394
|
+
);
|
|
395
|
+
if (nestedMatch) {
|
|
396
|
+
return nestedMatch;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return null;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export default function InstallmentOptionsSection({
|
|
405
|
+
section,
|
|
406
|
+
currentBreakpoint = 'desktop',
|
|
407
|
+
placeholderId = '',
|
|
408
|
+
isDesigner = false,
|
|
409
|
+
selectedBlockId = null
|
|
410
|
+
}: InstallmentOptionsSectionProps) {
|
|
411
|
+
const themeSettings = useThemeSettingsContext();
|
|
412
|
+
const { t } = useLocalization();
|
|
413
|
+
const isMobile = currentBreakpoint === 'mobile';
|
|
414
|
+
|
|
415
|
+
const maxWidth = getResponsiveValue(
|
|
416
|
+
section.styles?.['max-width'],
|
|
417
|
+
currentBreakpoint,
|
|
418
|
+
'normal'
|
|
419
|
+
);
|
|
420
|
+
const maxWidthClass =
|
|
421
|
+
maxWidth === 'narrow'
|
|
422
|
+
? 'max-w-4xl'
|
|
423
|
+
: maxWidth === 'normal'
|
|
424
|
+
? 'max-w-7xl'
|
|
425
|
+
: '';
|
|
426
|
+
const hasMaxWidth = maxWidth !== 'none';
|
|
427
|
+
|
|
428
|
+
const filteredStyles = Object.fromEntries(
|
|
429
|
+
Object.entries(section.styles || {}).filter(
|
|
430
|
+
([key]) => !customStyleKeys.includes(key)
|
|
431
|
+
)
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const sectionStyles = getCSSStyles(
|
|
435
|
+
filteredStyles,
|
|
436
|
+
themeSettings,
|
|
437
|
+
currentBreakpoint
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const showProductCard = readBooleanProperty(
|
|
441
|
+
section.properties,
|
|
442
|
+
'show-product-card',
|
|
443
|
+
true,
|
|
444
|
+
currentBreakpoint
|
|
445
|
+
);
|
|
446
|
+
const showCardLogos = readBooleanProperty(
|
|
447
|
+
section.properties,
|
|
448
|
+
'show-card-logos',
|
|
449
|
+
true,
|
|
450
|
+
currentBreakpoint
|
|
451
|
+
);
|
|
452
|
+
const highlightBestOption = readBooleanProperty(
|
|
453
|
+
section.properties,
|
|
454
|
+
'highlight-best-option',
|
|
455
|
+
true,
|
|
456
|
+
currentBreakpoint
|
|
457
|
+
);
|
|
458
|
+
const emptyStateText = readStringProperty(
|
|
459
|
+
section.properties,
|
|
460
|
+
'empty-state-text',
|
|
461
|
+
'Installment options not available for this product.',
|
|
462
|
+
currentBreakpoint
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const panelBackground = readStyleValue(
|
|
466
|
+
section.styles,
|
|
467
|
+
'panel-background-color',
|
|
468
|
+
'#ffffff',
|
|
469
|
+
currentBreakpoint,
|
|
470
|
+
themeSettings
|
|
471
|
+
);
|
|
472
|
+
const panelBorder = readStyleValue(
|
|
473
|
+
section.styles,
|
|
474
|
+
'panel-border-color',
|
|
475
|
+
'#e2e8f0',
|
|
476
|
+
currentBreakpoint,
|
|
477
|
+
themeSettings
|
|
478
|
+
);
|
|
479
|
+
const panelRadius = readStyleValue(
|
|
480
|
+
section.styles,
|
|
481
|
+
'panel-border-radius',
|
|
482
|
+
'16px',
|
|
483
|
+
currentBreakpoint,
|
|
484
|
+
themeSettings
|
|
485
|
+
);
|
|
486
|
+
const productCardBackground = readStyleValue(
|
|
487
|
+
section.styles,
|
|
488
|
+
'product-card-background-color',
|
|
489
|
+
'#f8fafc',
|
|
490
|
+
currentBreakpoint,
|
|
491
|
+
themeSettings
|
|
492
|
+
);
|
|
493
|
+
const productCardBorder = readStyleValue(
|
|
494
|
+
section.styles,
|
|
495
|
+
'product-card-border-color',
|
|
496
|
+
'#e2e8f0',
|
|
497
|
+
currentBreakpoint,
|
|
498
|
+
themeSettings
|
|
499
|
+
);
|
|
500
|
+
const logoCardBackground = readStyleValue(
|
|
501
|
+
section.styles,
|
|
502
|
+
'logo-card-background-color',
|
|
503
|
+
'#ffffff',
|
|
504
|
+
currentBreakpoint,
|
|
505
|
+
themeSettings
|
|
506
|
+
);
|
|
507
|
+
const logoCardBorder = readStyleValue(
|
|
508
|
+
section.styles,
|
|
509
|
+
'logo-card-border-color',
|
|
510
|
+
'#e2e8f0',
|
|
511
|
+
currentBreakpoint,
|
|
512
|
+
themeSettings
|
|
513
|
+
);
|
|
514
|
+
const logoCardActiveBackground = readStyleValue(
|
|
515
|
+
section.styles,
|
|
516
|
+
'logo-card-active-background-color',
|
|
517
|
+
'#eff6ff',
|
|
518
|
+
currentBreakpoint,
|
|
519
|
+
themeSettings
|
|
520
|
+
);
|
|
521
|
+
const logoCardActiveBorder = readStyleValue(
|
|
522
|
+
section.styles,
|
|
523
|
+
'logo-card-active-border-color',
|
|
524
|
+
'#0f172a',
|
|
525
|
+
currentBreakpoint,
|
|
526
|
+
themeSettings
|
|
527
|
+
);
|
|
528
|
+
const tableHeaderColor = readStyleValue(
|
|
529
|
+
section.styles,
|
|
530
|
+
'table-header-color',
|
|
531
|
+
'#64748b',
|
|
532
|
+
currentBreakpoint,
|
|
533
|
+
themeSettings
|
|
534
|
+
);
|
|
535
|
+
const tableRowBorderColor = readStyleValue(
|
|
536
|
+
section.styles,
|
|
537
|
+
'table-row-border-color',
|
|
538
|
+
'#e2e8f0',
|
|
539
|
+
currentBreakpoint,
|
|
540
|
+
themeSettings
|
|
541
|
+
);
|
|
542
|
+
const highlightRowBackground = readStyleValue(
|
|
543
|
+
section.styles,
|
|
544
|
+
'highlight-row-background-color',
|
|
545
|
+
'#ecfdf5',
|
|
546
|
+
currentBreakpoint,
|
|
547
|
+
themeSettings
|
|
548
|
+
);
|
|
549
|
+
const highlightRowText = readStyleValue(
|
|
550
|
+
section.styles,
|
|
551
|
+
'highlight-row-text-color',
|
|
552
|
+
'#166534',
|
|
553
|
+
currentBreakpoint,
|
|
554
|
+
themeSettings
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const sectionProducts = useMemo(
|
|
558
|
+
() => getCollectionProducts(section, isDesigner),
|
|
559
|
+
[section, isDesigner]
|
|
560
|
+
);
|
|
561
|
+
const collectionProduct = (sectionProducts[0] as ProductLike | undefined) || null;
|
|
562
|
+
const propertyProductPk = parseProductPk(
|
|
563
|
+
readStringProperty(section.properties, 'product-pk', '', currentBreakpoint)
|
|
564
|
+
);
|
|
565
|
+
const resolvedProductPk = parseProductPk(collectionProduct?.pk) ?? propertyProductPk;
|
|
566
|
+
|
|
567
|
+
const shouldFetchProduct = !collectionProduct && resolvedProductPk !== null;
|
|
568
|
+
const { data: productResponse } = useGetProductByPkQuery(
|
|
569
|
+
resolvedProductPk as number,
|
|
570
|
+
{
|
|
571
|
+
skip: !shouldFetchProduct
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
const { data: installmentsData, isLoading } = useGetInstallmentsQuery(
|
|
575
|
+
resolvedProductPk as number,
|
|
576
|
+
{
|
|
577
|
+
skip: resolvedProductPk === null
|
|
578
|
+
}
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const product = useMemo<ProductLike | null>((): ProductLike | null => {
|
|
582
|
+
if (collectionProduct) return collectionProduct;
|
|
583
|
+
const fetchedProduct = (
|
|
584
|
+
productResponse as { product?: ProductLike } | undefined
|
|
585
|
+
)?.product;
|
|
586
|
+
return fetchedProduct ?? null;
|
|
587
|
+
}, [collectionProduct, productResponse]);
|
|
588
|
+
|
|
589
|
+
const usePlaceholderData = isDesigner && resolvedProductPk === null && !product;
|
|
590
|
+
|
|
591
|
+
const installmentCards = useMemo<InstallmentCard[]>(() => {
|
|
592
|
+
if (usePlaceholderData) {
|
|
593
|
+
return PLACEHOLDER_INSTALLMENTS;
|
|
594
|
+
}
|
|
595
|
+
return ((installmentsData as InstallmentResponse | undefined)?.results || []).filter(
|
|
596
|
+
(card): card is InstallmentCard =>
|
|
597
|
+
Boolean(card && card.slug && Array.isArray(card.installments))
|
|
598
|
+
);
|
|
599
|
+
}, [installmentsData, usePlaceholderData]);
|
|
600
|
+
|
|
601
|
+
const [activeCardSlug, setActiveCardSlug] = useState<string | null>(null);
|
|
602
|
+
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
if (installmentCards.length === 0) {
|
|
605
|
+
setActiveCardSlug(null);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const hasCurrentCard = installmentCards.some(
|
|
610
|
+
(card) => card.slug === activeCardSlug
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
if (!hasCurrentCard) {
|
|
614
|
+
setActiveCardSlug(installmentCards[0].slug);
|
|
615
|
+
}
|
|
616
|
+
}, [activeCardSlug, installmentCards]);
|
|
617
|
+
|
|
618
|
+
const activeCard =
|
|
619
|
+
installmentCards.find((card) => card.slug === activeCardSlug) ||
|
|
620
|
+
installmentCards[0] ||
|
|
621
|
+
null;
|
|
622
|
+
|
|
623
|
+
const sortedBlocks = [...(section.blocks || [])]
|
|
624
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
625
|
+
.filter((block) => (isDesigner ? true : !block.hidden));
|
|
626
|
+
|
|
627
|
+
const contentBlocks = sortedBlocks.filter(
|
|
628
|
+
(block) => !getInstallmentRole(block, currentBreakpoint)
|
|
629
|
+
);
|
|
630
|
+
const productCardWrapperBlock = findInstallmentRoleBlock(
|
|
631
|
+
sortedBlocks,
|
|
632
|
+
'product-card-wrapper',
|
|
633
|
+
currentBreakpoint
|
|
634
|
+
);
|
|
635
|
+
const installmentPanelWrapperBlock = findInstallmentRoleBlock(
|
|
636
|
+
sortedBlocks,
|
|
637
|
+
'installment-panel-wrapper',
|
|
638
|
+
currentBreakpoint
|
|
639
|
+
);
|
|
640
|
+
const logoCardWrapperBlock = findInstallmentRoleBlock(
|
|
641
|
+
sortedBlocks,
|
|
642
|
+
'logo-card-wrapper',
|
|
643
|
+
currentBreakpoint
|
|
644
|
+
);
|
|
645
|
+
const tableWrapperBlock = findInstallmentRoleBlock(
|
|
646
|
+
sortedBlocks,
|
|
647
|
+
'table-wrapper',
|
|
648
|
+
currentBreakpoint
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const productName = String(
|
|
652
|
+
product?.name || PLACEHOLDER_PRODUCT.name
|
|
653
|
+
).trim();
|
|
654
|
+
const productUrl = String(
|
|
655
|
+
product?.absolute_url || PLACEHOLDER_PRODUCT.absolute_url
|
|
656
|
+
).trim();
|
|
657
|
+
const productImage = getProductImage(product);
|
|
658
|
+
const productPrice = getProductPrice(product) || PLACEHOLDER_PRODUCT.price;
|
|
659
|
+
const productRetailPrice = getProductRetailPrice(product);
|
|
660
|
+
|
|
661
|
+
const renderBlock = (block: Block) => (
|
|
662
|
+
<ThemeBlock
|
|
663
|
+
key={block.id}
|
|
664
|
+
block={block}
|
|
665
|
+
placeholderId={placeholderId}
|
|
666
|
+
sectionId={section.id}
|
|
667
|
+
isDesigner={isDesigner}
|
|
668
|
+
isSelected={selectedBlockId === block.id}
|
|
669
|
+
selectedBlockId={selectedBlockId}
|
|
670
|
+
currentBreakpoint={currentBreakpoint}
|
|
671
|
+
onMoveUp={() => {
|
|
672
|
+
if (window.parent) {
|
|
673
|
+
window.parent.postMessage(
|
|
674
|
+
{
|
|
675
|
+
type: 'MOVE_BLOCK_UP',
|
|
676
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
677
|
+
},
|
|
678
|
+
'*'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}}
|
|
682
|
+
onMoveDown={() => {
|
|
683
|
+
if (window.parent) {
|
|
684
|
+
window.parent.postMessage(
|
|
685
|
+
{
|
|
686
|
+
type: 'MOVE_BLOCK_DOWN',
|
|
687
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
688
|
+
},
|
|
689
|
+
'*'
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}}
|
|
693
|
+
onDuplicate={() => {
|
|
694
|
+
if (window.parent) {
|
|
695
|
+
window.parent.postMessage(
|
|
696
|
+
{
|
|
697
|
+
type: 'DUPLICATE_BLOCK',
|
|
698
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
699
|
+
},
|
|
700
|
+
'*'
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}}
|
|
704
|
+
onToggleVisibility={() => {
|
|
705
|
+
if (window.parent) {
|
|
706
|
+
window.parent.postMessage(
|
|
707
|
+
{
|
|
708
|
+
type: 'TOGGLE_BLOCK_VISIBILITY',
|
|
709
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
710
|
+
},
|
|
711
|
+
'*'
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}}
|
|
715
|
+
onDelete={() => {
|
|
716
|
+
if (window.parent) {
|
|
717
|
+
window.parent.postMessage(
|
|
718
|
+
{
|
|
719
|
+
type: 'DELETE_BLOCK',
|
|
720
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
721
|
+
},
|
|
722
|
+
'*'
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}}
|
|
726
|
+
onRename={(newLabel) => {
|
|
727
|
+
if (window.parent) {
|
|
728
|
+
window.parent.postMessage(
|
|
729
|
+
{
|
|
730
|
+
type: 'RENAME_BLOCK',
|
|
731
|
+
data: {
|
|
732
|
+
placeholderId,
|
|
733
|
+
sectionId: section.id,
|
|
734
|
+
blockId: block.id,
|
|
735
|
+
label: newLabel
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
'*'
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}}
|
|
742
|
+
/>
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
const postBlockAction = (type: string, blockId: string, label?: string) => {
|
|
746
|
+
if (!window.parent) return;
|
|
747
|
+
window.parent.postMessage(
|
|
748
|
+
{
|
|
749
|
+
type,
|
|
750
|
+
data: {
|
|
751
|
+
placeholderId,
|
|
752
|
+
sectionId: section.id,
|
|
753
|
+
blockId,
|
|
754
|
+
...(label ? { label } : {})
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
'*'
|
|
758
|
+
);
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const renderSelectableWrapper = (
|
|
762
|
+
block: Block | null,
|
|
763
|
+
children: React.ReactNode,
|
|
764
|
+
options: {
|
|
765
|
+
className?: string;
|
|
766
|
+
style?: React.CSSProperties;
|
|
767
|
+
keyOverride?: string;
|
|
768
|
+
} = {}
|
|
769
|
+
) => {
|
|
770
|
+
if (!block) {
|
|
771
|
+
return (
|
|
772
|
+
<div
|
|
773
|
+
key={options.keyOverride}
|
|
774
|
+
className={options.className}
|
|
775
|
+
style={options.style}
|
|
776
|
+
>
|
|
777
|
+
{children}
|
|
778
|
+
</div>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const actionBlockId = block.styleSourceId || block.id;
|
|
783
|
+
const wrapperStyle = {
|
|
784
|
+
...getCSSStyles(block.styles || {}, themeSettings, currentBreakpoint),
|
|
785
|
+
...(options.style || {})
|
|
786
|
+
} as React.CSSProperties;
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
<WithDesignerFeatures
|
|
790
|
+
key={options.keyOverride || block.id}
|
|
791
|
+
block={block}
|
|
792
|
+
placeholderId={placeholderId}
|
|
793
|
+
sectionId={section.id}
|
|
794
|
+
isDesigner={isDesigner}
|
|
795
|
+
isSelected={
|
|
796
|
+
selectedBlockId === actionBlockId || selectedBlockId === block.id
|
|
797
|
+
}
|
|
798
|
+
currentBreakpoint={currentBreakpoint}
|
|
799
|
+
className={options.className}
|
|
800
|
+
style={wrapperStyle}
|
|
801
|
+
onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', actionBlockId)}
|
|
802
|
+
onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', actionBlockId)}
|
|
803
|
+
onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', actionBlockId)}
|
|
804
|
+
onToggleVisibility={() =>
|
|
805
|
+
postBlockAction('TOGGLE_BLOCK_VISIBILITY', actionBlockId)
|
|
806
|
+
}
|
|
807
|
+
onDelete={() => postBlockAction('DELETE_BLOCK', actionBlockId)}
|
|
808
|
+
onRename={(newLabel) =>
|
|
809
|
+
postBlockAction('RENAME_BLOCK', actionBlockId, newLabel)
|
|
810
|
+
}
|
|
811
|
+
>
|
|
812
|
+
{children}
|
|
813
|
+
</WithDesignerFeatures>
|
|
814
|
+
);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
<div
|
|
819
|
+
className={hasMaxWidth ? `mx-auto ${maxWidthClass}` : undefined}
|
|
820
|
+
style={sectionStyles}
|
|
821
|
+
>
|
|
822
|
+
<div className="flex flex-col gap-4">
|
|
823
|
+
{contentBlocks.map(renderBlock)}
|
|
824
|
+
|
|
825
|
+
<div
|
|
826
|
+
className={clsx(
|
|
827
|
+
'grid gap-4',
|
|
828
|
+
!showProductCard || isMobile
|
|
829
|
+
? 'grid-cols-1'
|
|
830
|
+
: 'grid-cols-[300px_1fr]'
|
|
831
|
+
)}
|
|
832
|
+
>
|
|
833
|
+
{showProductCard &&
|
|
834
|
+
renderSelectableWrapper(
|
|
835
|
+
productCardWrapperBlock,
|
|
836
|
+
<>
|
|
837
|
+
<div className="mb-4 flex items-center justify-between gap-3">
|
|
838
|
+
<p className="m-0 text-xs font-bold uppercase tracking-[0.16em] text-[#64748b]">
|
|
839
|
+
Selected Product
|
|
840
|
+
</p>
|
|
841
|
+
{productUrl && productUrl !== '#' && (
|
|
842
|
+
<a
|
|
843
|
+
href={productUrl}
|
|
844
|
+
className="text-xs font-semibold text-[#0f172a] underline underline-offset-4"
|
|
845
|
+
>
|
|
846
|
+
View product
|
|
847
|
+
</a>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<div
|
|
852
|
+
className="rounded-[14px] border bg-white p-3"
|
|
853
|
+
style={{
|
|
854
|
+
borderColor: productCardBorder
|
|
855
|
+
}}
|
|
856
|
+
>
|
|
857
|
+
<div className="relative mb-3 aspect-square w-full overflow-hidden rounded-[12px] bg-[#f8fafc]">
|
|
858
|
+
{productImage ? (
|
|
859
|
+
<Image
|
|
860
|
+
src={productImage}
|
|
861
|
+
alt={productName}
|
|
862
|
+
fill
|
|
863
|
+
sizes="280px"
|
|
864
|
+
aspectRatio={1}
|
|
865
|
+
imageClassName="object-cover"
|
|
866
|
+
/>
|
|
867
|
+
) : (
|
|
868
|
+
<div className="flex h-full w-full items-center justify-center bg-[#e2e8f0] text-sm font-semibold tracking-wide text-[#64748b]">
|
|
869
|
+
Product Preview
|
|
870
|
+
</div>
|
|
871
|
+
)}
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
875
|
+
<p
|
|
876
|
+
className="m-0 overflow-hidden text-[15px] font-medium leading-[1.45] text-[#0f172a]"
|
|
877
|
+
style={{
|
|
878
|
+
display: '-webkit-box',
|
|
879
|
+
WebkitLineClamp: isMobile ? 3 : 2,
|
|
880
|
+
WebkitBoxOrient: 'vertical'
|
|
881
|
+
}}
|
|
882
|
+
>
|
|
883
|
+
{productName}
|
|
884
|
+
</p>
|
|
885
|
+
|
|
886
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
887
|
+
{productRetailPrice && (
|
|
888
|
+
<span className="text-sm font-normal text-[#94a3b8] line-through">
|
|
889
|
+
{productRetailPrice}
|
|
890
|
+
</span>
|
|
891
|
+
)}
|
|
892
|
+
<span className="text-[17px] font-semibold text-[#0f172a]">
|
|
893
|
+
{productPrice}
|
|
894
|
+
</span>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
</>,
|
|
899
|
+
{
|
|
900
|
+
className: 'min-w-0 border p-4',
|
|
901
|
+
style: {
|
|
902
|
+
backgroundColor: productCardBackground,
|
|
903
|
+
borderColor: productCardBorder,
|
|
904
|
+
borderRadius: panelRadius
|
|
905
|
+
},
|
|
906
|
+
keyOverride: 'installment-product-card-wrapper'
|
|
907
|
+
}
|
|
908
|
+
)}
|
|
909
|
+
|
|
910
|
+
{renderSelectableWrapper(
|
|
911
|
+
installmentPanelWrapperBlock,
|
|
912
|
+
<>
|
|
913
|
+
{isLoading && !usePlaceholderData ? (
|
|
914
|
+
<div className="flex min-h-[220px] items-center justify-center">
|
|
915
|
+
<LoaderSpinner />
|
|
916
|
+
</div>
|
|
917
|
+
) : installmentCards.length > 0 ? (
|
|
918
|
+
<>
|
|
919
|
+
{showCardLogos && (
|
|
920
|
+
<div className="flex flex-wrap gap-2">
|
|
921
|
+
{installmentCards.map((card) => {
|
|
922
|
+
const isActive = activeCard?.slug === card.slug;
|
|
923
|
+
return renderSelectableWrapper(
|
|
924
|
+
logoCardWrapperBlock
|
|
925
|
+
? {
|
|
926
|
+
...logoCardWrapperBlock,
|
|
927
|
+
id: `${logoCardWrapperBlock.id}-clone-${card.pk}`,
|
|
928
|
+
styleSourceId:
|
|
929
|
+
logoCardWrapperBlock.styleSourceId ||
|
|
930
|
+
logoCardWrapperBlock.id
|
|
931
|
+
}
|
|
932
|
+
: null,
|
|
933
|
+
(
|
|
934
|
+
<button
|
|
935
|
+
key={card.pk}
|
|
936
|
+
type="button"
|
|
937
|
+
onClick={() => setActiveCardSlug(card.slug)}
|
|
938
|
+
className={clsx(
|
|
939
|
+
'flex min-w-[72px] flex-1 items-center justify-center px-3 py-2 transition-colors lg:min-w-[96px] lg:flex-none',
|
|
940
|
+
!logoCardWrapperBlock && 'border'
|
|
941
|
+
)}
|
|
942
|
+
style={{
|
|
943
|
+
backgroundColor: logoCardWrapperBlock
|
|
944
|
+
? 'transparent'
|
|
945
|
+
: isActive
|
|
946
|
+
? logoCardActiveBackground
|
|
947
|
+
: logoCardBackground,
|
|
948
|
+
borderColor: logoCardWrapperBlock
|
|
949
|
+
? 'transparent'
|
|
950
|
+
: isActive
|
|
951
|
+
? logoCardActiveBorder
|
|
952
|
+
: logoCardBorder,
|
|
953
|
+
borderRadius: logoCardWrapperBlock ? '0px' : '12px'
|
|
954
|
+
}}
|
|
955
|
+
>
|
|
956
|
+
{card.card_type?.logo ? (
|
|
957
|
+
<Image
|
|
958
|
+
src={card.card_type.logo}
|
|
959
|
+
alt={card.name}
|
|
960
|
+
width={56}
|
|
961
|
+
height={20}
|
|
962
|
+
imageClassName={clsx(
|
|
963
|
+
'h-auto max-h-5 w-auto max-w-full object-contain',
|
|
964
|
+
!isActive && 'grayscale'
|
|
965
|
+
)}
|
|
966
|
+
/>
|
|
967
|
+
) : (
|
|
968
|
+
<span className="text-xs font-bold uppercase tracking-[0.16em] text-[#0f172a]">
|
|
969
|
+
{getFallbackCardLabel(card.name)}
|
|
970
|
+
</span>
|
|
971
|
+
)}
|
|
972
|
+
</button>
|
|
973
|
+
),
|
|
974
|
+
{
|
|
975
|
+
keyOverride: `installment-logo-card-${card.pk}`
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
})}
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
|
|
982
|
+
{renderSelectableWrapper(
|
|
983
|
+
tableWrapperBlock,
|
|
984
|
+
<div
|
|
985
|
+
className={clsx(
|
|
986
|
+
'overflow-hidden rounded-[12px]',
|
|
987
|
+
!tableWrapperBlock && 'border'
|
|
988
|
+
)}
|
|
989
|
+
style={{
|
|
990
|
+
borderColor: tableRowBorderColor
|
|
991
|
+
}}
|
|
992
|
+
>
|
|
993
|
+
<div
|
|
994
|
+
className="grid grid-cols-3 gap-4 border-b px-4 py-3 text-xs font-bold uppercase tracking-[0.14em]"
|
|
995
|
+
style={{
|
|
996
|
+
borderColor: tableRowBorderColor,
|
|
997
|
+
color: tableHeaderColor,
|
|
998
|
+
backgroundColor: '#f8fafc'
|
|
999
|
+
}}
|
|
1000
|
+
>
|
|
1001
|
+
<span>{t('product.number_of_installments')}</span>
|
|
1002
|
+
<span className="text-right">{t('product.monthly_amount')}</span>
|
|
1003
|
+
<span className="text-right">{t('product.total_amount')}</span>
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div className="overflow-x-auto">
|
|
1007
|
+
{(activeCard?.installments || []).map((installment, index) => {
|
|
1008
|
+
const isHighlighted = highlightBestOption && index === 0;
|
|
1009
|
+
return (
|
|
1010
|
+
<div
|
|
1011
|
+
key={`${activeCard?.slug || 'card'}-${installment.installment_count}`}
|
|
1012
|
+
className="grid grid-cols-3 gap-4 border-b px-4 py-3 last:border-b-0"
|
|
1013
|
+
style={{
|
|
1014
|
+
borderColor: tableRowBorderColor,
|
|
1015
|
+
backgroundColor: isHighlighted
|
|
1016
|
+
? highlightRowBackground
|
|
1017
|
+
: panelBackground,
|
|
1018
|
+
color: isHighlighted ? highlightRowText : '#0f172a'
|
|
1019
|
+
}}
|
|
1020
|
+
>
|
|
1021
|
+
<div className="min-w-0 text-sm font-semibold">
|
|
1022
|
+
{installment.installment_count === 1
|
|
1023
|
+
? t('product.cash')
|
|
1024
|
+
: `${installment.installment_count} ${t('product.installment')}`}
|
|
1025
|
+
</div>
|
|
1026
|
+
<div className="text-right text-sm font-semibold">
|
|
1027
|
+
<Price value={installment.single_installment_amount} />
|
|
1028
|
+
</div>
|
|
1029
|
+
<div className="text-right text-sm font-semibold">
|
|
1030
|
+
<Price value={installment.total_amount} />
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
);
|
|
1034
|
+
})}
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>,
|
|
1037
|
+
{
|
|
1038
|
+
keyOverride: 'installment-table-wrapper'
|
|
1039
|
+
}
|
|
1040
|
+
)}
|
|
1041
|
+
</>
|
|
1042
|
+
) : (
|
|
1043
|
+
<div
|
|
1044
|
+
className="flex min-h-[220px] items-center justify-center rounded-[12px] border border-dashed px-6 text-center text-sm font-medium text-[#64748b]"
|
|
1045
|
+
style={{ borderColor: panelBorder }}
|
|
1046
|
+
>
|
|
1047
|
+
{emptyStateText}
|
|
1048
|
+
</div>
|
|
1049
|
+
)}
|
|
1050
|
+
</>,
|
|
1051
|
+
{
|
|
1052
|
+
className: 'flex min-w-0 flex-col gap-4 border p-4 lg:p-5',
|
|
1053
|
+
style: {
|
|
1054
|
+
backgroundColor: panelBackground,
|
|
1055
|
+
borderColor: panelBorder,
|
|
1056
|
+
borderRadius: panelRadius
|
|
1057
|
+
},
|
|
1058
|
+
keyOverride: 'installment-panel-wrapper'
|
|
1059
|
+
}
|
|
1060
|
+
)}
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
);
|
|
1065
|
+
}
|