@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,663 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
|
|
5
|
+
|
|
6
|
+
import ThemeBlock, { Block } from '../theme-block';
|
|
7
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
8
|
+
import { Section } from '../theme-section';
|
|
9
|
+
import { getCSSStyles, getResponsiveValue } from '../utils';
|
|
10
|
+
|
|
11
|
+
interface PreOrderLaunchBannerSectionProps {
|
|
12
|
+
section: Section;
|
|
13
|
+
currentBreakpoint?: string;
|
|
14
|
+
placeholderId?: string;
|
|
15
|
+
isDesigner?: boolean;
|
|
16
|
+
selectedBlockId?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CountdownUnit = 'days' | 'hours' | 'minutes' | 'seconds';
|
|
20
|
+
|
|
21
|
+
type CountdownValues = Record<CountdownUnit, string>;
|
|
22
|
+
|
|
23
|
+
const parseBoolean = (value: unknown, fallback: boolean): boolean => {
|
|
24
|
+
if (typeof value === 'boolean') return value;
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
const normalized = value.trim().toLowerCase();
|
|
27
|
+
if (normalized === 'true') return true;
|
|
28
|
+
if (normalized === 'false') return false;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'number') return value !== 0;
|
|
31
|
+
return fallback;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const resolvePath = (
|
|
35
|
+
source: Record<string, unknown> | undefined,
|
|
36
|
+
path: string
|
|
37
|
+
): unknown => {
|
|
38
|
+
if (!source || !path) return undefined;
|
|
39
|
+
|
|
40
|
+
const normalizedPath = path.replace(/^item\./, '');
|
|
41
|
+
const pathParts = normalizedPath.split('.');
|
|
42
|
+
let value: unknown = source;
|
|
43
|
+
|
|
44
|
+
for (const part of pathParts) {
|
|
45
|
+
const arrayMatch = part.match(/^(.+)\[(\d+)\]$/);
|
|
46
|
+
if (arrayMatch) {
|
|
47
|
+
const [, arrayKey, indexText] = arrayMatch;
|
|
48
|
+
const arrayValue = (value as Record<string, unknown>)?.[arrayKey];
|
|
49
|
+
value = Array.isArray(arrayValue)
|
|
50
|
+
? arrayValue[Number(indexText)]
|
|
51
|
+
: undefined;
|
|
52
|
+
} else {
|
|
53
|
+
value = (value as Record<string, unknown>)?.[part];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value === undefined || value === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const parsePriceValue = (value: unknown): number | null => {
|
|
65
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof value !== 'string') return null;
|
|
70
|
+
|
|
71
|
+
const cleaned = value.trim().replace(/[^\d,.-]/g, '');
|
|
72
|
+
if (!cleaned) return null;
|
|
73
|
+
|
|
74
|
+
let normalized = cleaned;
|
|
75
|
+
const hasComma = normalized.includes(',');
|
|
76
|
+
const hasDot = normalized.includes('.');
|
|
77
|
+
|
|
78
|
+
if (hasComma && hasDot) {
|
|
79
|
+
const lastComma = normalized.lastIndexOf(',');
|
|
80
|
+
const lastDot = normalized.lastIndexOf('.');
|
|
81
|
+
normalized =
|
|
82
|
+
lastComma > lastDot
|
|
83
|
+
? normalized.replace(/\./g, '').replace(',', '.')
|
|
84
|
+
: normalized.replace(/,/g, '');
|
|
85
|
+
} else if (hasComma) {
|
|
86
|
+
const unsigned = normalized.replace(/^-/, '');
|
|
87
|
+
const isThousandsPattern = /^\d{1,3}(,\d{3})+$/.test(unsigned);
|
|
88
|
+
normalized = isThousandsPattern
|
|
89
|
+
? normalized.replace(/,/g, '')
|
|
90
|
+
: normalized.replace(/,/g, '.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const parsed = Number(normalized);
|
|
94
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const parsePositiveInt = (value: unknown): number | null => {
|
|
98
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof value === 'string') {
|
|
103
|
+
const trimmed = value.trim();
|
|
104
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
105
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
106
|
+
return parsed > 0 ? parsed : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const normalizeCurrencyLabel = (currency: unknown): string => {
|
|
113
|
+
const raw = String(currency || '').trim().toUpperCase();
|
|
114
|
+
if (!raw) return 'TL';
|
|
115
|
+
|
|
116
|
+
const map: Record<string, string> = {
|
|
117
|
+
TRY: 'TL',
|
|
118
|
+
TL: 'TL',
|
|
119
|
+
USD: 'USD',
|
|
120
|
+
EUR: 'EUR',
|
|
121
|
+
GBP: 'GBP'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return map[raw] || raw;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const formatPriceWithCurrency = (
|
|
128
|
+
value: unknown,
|
|
129
|
+
currency: unknown
|
|
130
|
+
): string | null => {
|
|
131
|
+
if (value == null || value === '') return null;
|
|
132
|
+
|
|
133
|
+
const price = parsePriceValue(value);
|
|
134
|
+
if (price === null) {
|
|
135
|
+
const text = String(value).trim();
|
|
136
|
+
return text || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const hasDecimals = Math.abs(price % 1) > 0.00001;
|
|
140
|
+
const formatted = price.toLocaleString('tr-TR', {
|
|
141
|
+
minimumFractionDigits: hasDecimals ? 2 : 0,
|
|
142
|
+
maximumFractionDigits: 2
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return `${formatted} ${normalizeCurrencyLabel(currency)}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const parseDateValue = (value: unknown): Date | null => {
|
|
149
|
+
if (value === undefined || value === null || value === '') return null;
|
|
150
|
+
|
|
151
|
+
if (value instanceof Date) {
|
|
152
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof value === 'number') {
|
|
156
|
+
const timestamp = value < 10_000_000_000 ? value * 1000 : value;
|
|
157
|
+
const date = new Date(timestamp);
|
|
158
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof value !== 'string') return null;
|
|
162
|
+
|
|
163
|
+
const trimmed = value.trim();
|
|
164
|
+
if (!trimmed) return null;
|
|
165
|
+
|
|
166
|
+
if (/^\d+$/.test(trimmed)) {
|
|
167
|
+
const numeric = Number(trimmed);
|
|
168
|
+
if (!Number.isFinite(numeric)) return null;
|
|
169
|
+
const timestamp = numeric < 10_000_000_000 ? numeric * 1000 : numeric;
|
|
170
|
+
const date = new Date(timestamp);
|
|
171
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const parsed = new Date(trimmed);
|
|
175
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const formatTwoDigits = (value: number) =>
|
|
179
|
+
String(Math.max(0, value)).padStart(2, '0');
|
|
180
|
+
|
|
181
|
+
const getRemainingTime = (
|
|
182
|
+
launchDate: Date | null
|
|
183
|
+
): { isLaunched: boolean; values: CountdownValues } => {
|
|
184
|
+
const emptyValues = {
|
|
185
|
+
days: '00',
|
|
186
|
+
hours: '00',
|
|
187
|
+
minutes: '00',
|
|
188
|
+
seconds: '00'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (!launchDate) {
|
|
192
|
+
return { isLaunched: false, values: emptyValues };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const diffMs = launchDate.getTime() - Date.now();
|
|
196
|
+
if (diffMs <= 0) {
|
|
197
|
+
return { isLaunched: true, values: emptyValues };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const totalSeconds = Math.floor(diffMs / 1000);
|
|
201
|
+
const days = Math.floor(totalSeconds / (60 * 60 * 24));
|
|
202
|
+
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
|
|
203
|
+
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
|
|
204
|
+
const seconds = totalSeconds % 60;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
isLaunched: false,
|
|
208
|
+
values: {
|
|
209
|
+
days: formatTwoDigits(days),
|
|
210
|
+
hours: formatTwoDigits(hours),
|
|
211
|
+
minutes: formatTwoDigits(minutes),
|
|
212
|
+
seconds: formatTwoDigits(seconds)
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const formatLaunchDate = (value: Date | null): string | null => {
|
|
218
|
+
if (!value) return null;
|
|
219
|
+
|
|
220
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
221
|
+
month: 'long',
|
|
222
|
+
day: 'numeric',
|
|
223
|
+
year: 'numeric'
|
|
224
|
+
}).format(value);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const getCollectionProducts = (
|
|
228
|
+
section: Section,
|
|
229
|
+
isDesigner: boolean
|
|
230
|
+
): Record<string, unknown>[] => {
|
|
231
|
+
const collectionDetails = section.dataSource?.details?.collection;
|
|
232
|
+
const staticData = section.dataSource?.details?.static?.data;
|
|
233
|
+
const isEditorMode =
|
|
234
|
+
typeof window !== 'undefined' && isDesigner && window.parent !== window;
|
|
235
|
+
|
|
236
|
+
const collectionPayload = isEditorMode
|
|
237
|
+
? collectionDetails?.products || collectionDetails?.data
|
|
238
|
+
: collectionDetails?.data || collectionDetails?.products;
|
|
239
|
+
|
|
240
|
+
if (Array.isArray(collectionPayload)) {
|
|
241
|
+
return collectionPayload as Record<string, unknown>[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (Array.isArray((collectionPayload as Record<string, unknown>)?.products)) {
|
|
245
|
+
return (collectionPayload as Record<string, unknown>).products as Record<
|
|
246
|
+
string,
|
|
247
|
+
unknown
|
|
248
|
+
>[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (Array.isArray((collectionPayload as Record<string, unknown>)?.items)) {
|
|
252
|
+
return (collectionPayload as Record<string, unknown>).items as Record<
|
|
253
|
+
string,
|
|
254
|
+
unknown
|
|
255
|
+
>[];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (Array.isArray(staticData)) {
|
|
259
|
+
return staticData as Record<string, unknown>[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return [];
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getProductLaunchDate = (
|
|
266
|
+
product: Record<string, unknown> | undefined
|
|
267
|
+
): Date | null => {
|
|
268
|
+
if (!product) return null;
|
|
269
|
+
|
|
270
|
+
const candidatePaths = [
|
|
271
|
+
'pre_order.launch_date',
|
|
272
|
+
'pre_order.release_date',
|
|
273
|
+
'launch_date',
|
|
274
|
+
'release_date',
|
|
275
|
+
'expected_release_date',
|
|
276
|
+
'expected_ship_date',
|
|
277
|
+
'available_at',
|
|
278
|
+
'extra_attributes.launch_date',
|
|
279
|
+
'extra_attributes.release_date',
|
|
280
|
+
'extra_attributes.expected_release_date',
|
|
281
|
+
'extra_data.launch_date',
|
|
282
|
+
'extra_data.release_date',
|
|
283
|
+
'extra_data.expected_release_date'
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
for (const path of candidatePaths) {
|
|
287
|
+
const resolved = resolvePath(product, path);
|
|
288
|
+
const parsed = parseDateValue(resolved);
|
|
289
|
+
if (parsed) {
|
|
290
|
+
return parsed;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export default function PreOrderLaunchBannerSection({
|
|
298
|
+
section,
|
|
299
|
+
currentBreakpoint = 'desktop',
|
|
300
|
+
placeholderId = '',
|
|
301
|
+
isDesigner = false,
|
|
302
|
+
selectedBlockId = null
|
|
303
|
+
}: PreOrderLaunchBannerSectionProps) {
|
|
304
|
+
const themeSettings = useThemeSettingsContext();
|
|
305
|
+
|
|
306
|
+
const maxWidth = getResponsiveValue(
|
|
307
|
+
section.styles?.['max-width'],
|
|
308
|
+
currentBreakpoint,
|
|
309
|
+
'normal'
|
|
310
|
+
);
|
|
311
|
+
const maxWidthClass =
|
|
312
|
+
maxWidth === 'narrow'
|
|
313
|
+
? 'max-w-4xl'
|
|
314
|
+
: maxWidth === 'normal'
|
|
315
|
+
? 'max-w-7xl'
|
|
316
|
+
: maxWidth === 'full'
|
|
317
|
+
? 'w-full'
|
|
318
|
+
: '';
|
|
319
|
+
const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
|
|
320
|
+
|
|
321
|
+
const filteredStyles = Object.fromEntries(
|
|
322
|
+
Object.entries(section.styles || {}).filter(([key]) => key !== 'max-width')
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const sectionStyles = useMemo(() => {
|
|
326
|
+
const baseStyles = getCSSStyles(
|
|
327
|
+
filteredStyles,
|
|
328
|
+
themeSettings,
|
|
329
|
+
currentBreakpoint
|
|
330
|
+
);
|
|
331
|
+
const reverseLayout = parseBoolean(
|
|
332
|
+
getResponsiveValue(section.properties?.reverse, currentBreakpoint, false),
|
|
333
|
+
false
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (!reverseLayout) {
|
|
337
|
+
return baseStyles;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
...baseStyles,
|
|
342
|
+
flexDirection: currentBreakpoint === 'mobile' ? 'column-reverse' : 'row-reverse'
|
|
343
|
+
};
|
|
344
|
+
}, [
|
|
345
|
+
currentBreakpoint,
|
|
346
|
+
filteredStyles,
|
|
347
|
+
section.properties?.reverse,
|
|
348
|
+
themeSettings
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
const products = useMemo(
|
|
352
|
+
() => getCollectionProducts(section, isDesigner),
|
|
353
|
+
[section, isDesigner]
|
|
354
|
+
);
|
|
355
|
+
const fallbackProductPk = parsePositiveInt(
|
|
356
|
+
getResponsiveValue(section.properties?.['product-pk'], currentBreakpoint, '')
|
|
357
|
+
);
|
|
358
|
+
const { data: fallbackProductResponse } = useGetProductByPkQuery(
|
|
359
|
+
fallbackProductPk as number,
|
|
360
|
+
{
|
|
361
|
+
skip: products.length > 0 || !fallbackProductPk
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
const product =
|
|
365
|
+
products[0] ||
|
|
366
|
+
((fallbackProductResponse?.product as Record<string, unknown> | undefined) ??
|
|
367
|
+
undefined);
|
|
368
|
+
|
|
369
|
+
const propertyLaunchDate = getResponsiveValue(
|
|
370
|
+
section.properties?.['launch-date'],
|
|
371
|
+
currentBreakpoint,
|
|
372
|
+
''
|
|
373
|
+
);
|
|
374
|
+
const launchDate = useMemo(
|
|
375
|
+
() => parseDateValue(propertyLaunchDate) || getProductLaunchDate(product),
|
|
376
|
+
[product, propertyLaunchDate]
|
|
377
|
+
);
|
|
378
|
+
const launchDateText = useMemo(() => formatLaunchDate(launchDate), [launchDate]);
|
|
379
|
+
|
|
380
|
+
const showCountdown = parseBoolean(
|
|
381
|
+
getResponsiveValue(
|
|
382
|
+
section.properties?.['show-countdown'],
|
|
383
|
+
currentBreakpoint,
|
|
384
|
+
true
|
|
385
|
+
),
|
|
386
|
+
true
|
|
387
|
+
);
|
|
388
|
+
const showPrice = parseBoolean(
|
|
389
|
+
getResponsiveValue(section.properties?.['show-price'], currentBreakpoint, true),
|
|
390
|
+
true
|
|
391
|
+
);
|
|
392
|
+
const showOldPrice = parseBoolean(
|
|
393
|
+
getResponsiveValue(
|
|
394
|
+
section.properties?.['show-old-price'],
|
|
395
|
+
currentBreakpoint,
|
|
396
|
+
true
|
|
397
|
+
),
|
|
398
|
+
true
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const upcomingStatusText = String(
|
|
402
|
+
getResponsiveValue(
|
|
403
|
+
section.properties?.['upcoming-status-text'],
|
|
404
|
+
currentBreakpoint,
|
|
405
|
+
'Pre-order open'
|
|
406
|
+
) || 'Pre-order open'
|
|
407
|
+
);
|
|
408
|
+
const launchedStatusText = String(
|
|
409
|
+
getResponsiveValue(
|
|
410
|
+
section.properties?.['launched-status-text'],
|
|
411
|
+
currentBreakpoint,
|
|
412
|
+
'Now shipping'
|
|
413
|
+
) || 'Now shipping'
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const [countdown, setCountdown] = useState(() => getRemainingTime(launchDate));
|
|
417
|
+
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
const tick = () => setCountdown(getRemainingTime(launchDate));
|
|
420
|
+
tick();
|
|
421
|
+
|
|
422
|
+
if (!launchDate || !showCountdown) return;
|
|
423
|
+
|
|
424
|
+
const intervalId = window.setInterval(tick, 1000);
|
|
425
|
+
return () => window.clearInterval(intervalId);
|
|
426
|
+
}, [launchDate, showCountdown]);
|
|
427
|
+
|
|
428
|
+
const activePrice =
|
|
429
|
+
((product?.active_price as Record<string, unknown> | undefined) || {});
|
|
430
|
+
const currentPriceRaw = activePrice?.price ?? product?.price;
|
|
431
|
+
const retailPriceRaw = activePrice?.retail_price ?? product?.retail_price;
|
|
432
|
+
const currency =
|
|
433
|
+
activePrice?.currency_type ?? product?.currency_type ?? product?.currency;
|
|
434
|
+
const currentPrice = parsePriceValue(currentPriceRaw);
|
|
435
|
+
const retailPrice = parsePriceValue(retailPriceRaw);
|
|
436
|
+
const currentPriceText = formatPriceWithCurrency(currentPriceRaw, currency);
|
|
437
|
+
const retailPriceText = formatPriceWithCurrency(retailPriceRaw, currency);
|
|
438
|
+
const hasRetailPrice =
|
|
439
|
+
Boolean(retailPriceText) &&
|
|
440
|
+
retailPrice !== null &&
|
|
441
|
+
currentPrice !== null &&
|
|
442
|
+
retailPrice > currentPrice;
|
|
443
|
+
|
|
444
|
+
const normalizedBlocks = useMemo(() => {
|
|
445
|
+
const cloneWithProduct = (block: Block): Block | null => {
|
|
446
|
+
const normalizedLabel = String(block.label || '').toLowerCase();
|
|
447
|
+
|
|
448
|
+
if (normalizedLabel === 'countdown group' && (!showCountdown || !launchDate)) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (normalizedLabel === 'price group' && !showPrice) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (
|
|
457
|
+
normalizedLabel === 'product old price' &&
|
|
458
|
+
(!showOldPrice || !hasRetailPrice)
|
|
459
|
+
) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const nextBlock: Block = {
|
|
464
|
+
...block,
|
|
465
|
+
properties: block.properties ? { ...block.properties } : block.properties,
|
|
466
|
+
styles: block.styles
|
|
467
|
+
? JSON.parse(JSON.stringify(block.styles))
|
|
468
|
+
: block.styles,
|
|
469
|
+
blocks: undefined
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (normalizedLabel === 'launch status badge') {
|
|
473
|
+
nextBlock.value = countdown.isLaunched
|
|
474
|
+
? launchedStatusText
|
|
475
|
+
: upcomingStatusText;
|
|
476
|
+
} else if (normalizedLabel === 'launch date value' && launchDateText) {
|
|
477
|
+
nextBlock.value = launchDateText;
|
|
478
|
+
} else if (normalizedLabel === 'days value' && launchDate) {
|
|
479
|
+
nextBlock.value = countdown.values.days;
|
|
480
|
+
} else if (normalizedLabel === 'hours value' && launchDate) {
|
|
481
|
+
nextBlock.value = countdown.values.hours;
|
|
482
|
+
} else if (normalizedLabel === 'minutes value' && launchDate) {
|
|
483
|
+
nextBlock.value = countdown.values.minutes;
|
|
484
|
+
} else if (normalizedLabel === 'seconds value' && launchDate) {
|
|
485
|
+
nextBlock.value = countdown.values.seconds;
|
|
486
|
+
} else if (normalizedLabel === 'product price' && currentPriceText) {
|
|
487
|
+
nextBlock.value = currentPriceText;
|
|
488
|
+
} else if (normalizedLabel === 'product old price' && retailPriceText) {
|
|
489
|
+
nextBlock.value = retailPriceText;
|
|
490
|
+
} else if (normalizedLabel === 'primary cta' && product?.absolute_url) {
|
|
491
|
+
nextBlock.properties = {
|
|
492
|
+
...(nextBlock.properties || {}),
|
|
493
|
+
url: product.absolute_url
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const bindingPath = nextBlock.properties?.dataBinding;
|
|
498
|
+
if (bindingPath && product) {
|
|
499
|
+
const boundValue = resolvePath(product, String(bindingPath));
|
|
500
|
+
|
|
501
|
+
if (boundValue !== undefined) {
|
|
502
|
+
if (nextBlock.type === 'button') {
|
|
503
|
+
nextBlock.properties = {
|
|
504
|
+
...(nextBlock.properties || {}),
|
|
505
|
+
url: boundValue
|
|
506
|
+
};
|
|
507
|
+
} else if (
|
|
508
|
+
nextBlock.type === 'text' &&
|
|
509
|
+
![
|
|
510
|
+
'launch date value',
|
|
511
|
+
'launch status badge',
|
|
512
|
+
'days value',
|
|
513
|
+
'hours value',
|
|
514
|
+
'minutes value',
|
|
515
|
+
'seconds value',
|
|
516
|
+
'product price',
|
|
517
|
+
'product old price'
|
|
518
|
+
].includes(normalizedLabel)
|
|
519
|
+
) {
|
|
520
|
+
nextBlock.value = boundValue;
|
|
521
|
+
} else if (nextBlock.type === 'image') {
|
|
522
|
+
nextBlock.value = boundValue;
|
|
523
|
+
}
|
|
524
|
+
} else if (nextBlock.type === 'image') {
|
|
525
|
+
const fallbackImage = resolvePath(product, 'productimage_set[0].image');
|
|
526
|
+
if (fallbackImage !== undefined) {
|
|
527
|
+
nextBlock.value = fallbackImage;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (block.blocks?.length) {
|
|
533
|
+
nextBlock.blocks = block.blocks
|
|
534
|
+
.map(cloneWithProduct)
|
|
535
|
+
.filter(Boolean) as Block[];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return nextBlock;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
return (section.blocks || [])
|
|
542
|
+
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
|
543
|
+
.map(cloneWithProduct)
|
|
544
|
+
.filter(Boolean) as Block[];
|
|
545
|
+
}, [
|
|
546
|
+
countdown.isLaunched,
|
|
547
|
+
countdown.values.days,
|
|
548
|
+
countdown.values.hours,
|
|
549
|
+
countdown.values.minutes,
|
|
550
|
+
countdown.values.seconds,
|
|
551
|
+
currentPriceText,
|
|
552
|
+
hasRetailPrice,
|
|
553
|
+
launchDate,
|
|
554
|
+
launchDateText,
|
|
555
|
+
launchedStatusText,
|
|
556
|
+
product,
|
|
557
|
+
retailPriceText,
|
|
558
|
+
section.blocks,
|
|
559
|
+
showCountdown,
|
|
560
|
+
showOldPrice,
|
|
561
|
+
showPrice,
|
|
562
|
+
upcomingStatusText
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
const renderBlock = (block: Block) => (
|
|
566
|
+
<ThemeBlock
|
|
567
|
+
key={block.id}
|
|
568
|
+
block={block}
|
|
569
|
+
placeholderId={placeholderId}
|
|
570
|
+
sectionId={section.id}
|
|
571
|
+
isDesigner={isDesigner}
|
|
572
|
+
isSelected={selectedBlockId === block.id}
|
|
573
|
+
selectedBlockId={selectedBlockId}
|
|
574
|
+
currentBreakpoint={currentBreakpoint}
|
|
575
|
+
onMoveUp={() => {
|
|
576
|
+
if (window.parent) {
|
|
577
|
+
window.parent.postMessage(
|
|
578
|
+
{
|
|
579
|
+
type: 'MOVE_BLOCK_UP',
|
|
580
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
581
|
+
},
|
|
582
|
+
'*'
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}}
|
|
586
|
+
onMoveDown={() => {
|
|
587
|
+
if (window.parent) {
|
|
588
|
+
window.parent.postMessage(
|
|
589
|
+
{
|
|
590
|
+
type: 'MOVE_BLOCK_DOWN',
|
|
591
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
592
|
+
},
|
|
593
|
+
'*'
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}}
|
|
597
|
+
onDuplicate={() => {
|
|
598
|
+
if (window.parent) {
|
|
599
|
+
window.parent.postMessage(
|
|
600
|
+
{
|
|
601
|
+
type: 'DUPLICATE_BLOCK',
|
|
602
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
603
|
+
},
|
|
604
|
+
'*'
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}}
|
|
608
|
+
onToggleVisibility={() => {
|
|
609
|
+
if (window.parent) {
|
|
610
|
+
window.parent.postMessage(
|
|
611
|
+
{
|
|
612
|
+
type: 'TOGGLE_BLOCK_VISIBILITY',
|
|
613
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
614
|
+
},
|
|
615
|
+
'*'
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}}
|
|
619
|
+
onDelete={() => {
|
|
620
|
+
if (window.parent) {
|
|
621
|
+
window.parent.postMessage(
|
|
622
|
+
{
|
|
623
|
+
type: 'DELETE_BLOCK',
|
|
624
|
+
data: { placeholderId, sectionId: section.id, blockId: block.id }
|
|
625
|
+
},
|
|
626
|
+
'*'
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}}
|
|
630
|
+
onRename={(newLabel) => {
|
|
631
|
+
if (window.parent) {
|
|
632
|
+
window.parent.postMessage(
|
|
633
|
+
{
|
|
634
|
+
type: 'RENAME_BLOCK',
|
|
635
|
+
data: {
|
|
636
|
+
placeholderId,
|
|
637
|
+
sectionId: section.id,
|
|
638
|
+
blockId: block.id,
|
|
639
|
+
label: newLabel
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
'*'
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}}
|
|
646
|
+
/>
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
<div
|
|
651
|
+
className={
|
|
652
|
+
hasMaxWidth
|
|
653
|
+
? `mx-auto ${maxWidthClass}`
|
|
654
|
+
: maxWidthClass || undefined
|
|
655
|
+
}
|
|
656
|
+
style={sectionStyles}
|
|
657
|
+
>
|
|
658
|
+
{normalizedBlocks
|
|
659
|
+
.filter((block) => (isDesigner ? true : !block.hidden))
|
|
660
|
+
.map(renderBlock)}
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
}
|