@akinon/next 2.0.0-beta.19 → 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/CHANGELOG.md +20 -13
- package/assets/styles/index.scss +84 -0
- package/components/client-root.tsx +107 -1
- package/components/link.tsx +46 -16
- 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/misc.ts +13 -1
- package/data/server/widget.ts +68 -1
- package/data/urls.ts +3 -1
- package/hooks/use-router.ts +53 -19
- package/lib/cache.ts +1 -0
- package/package.json +4 -2
- package/redux/reducers/index.ts +2 -0
- package/redux/reducers/widget.ts +80 -0
- package/types/commerce/widget.ts +33 -0
- package/types/widget.ts +80 -0
- package/utils/widget-styles.ts +107 -0
- package/with-pz-config.js +1 -1
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { useEmailSubscriptionMutation } from '../../../data/client/misc';
|
|
6
|
+
import { Modal } from '../../modal';
|
|
7
|
+
import {
|
|
8
|
+
colorToRgba,
|
|
9
|
+
getResponsiveValue,
|
|
10
|
+
resolveThemeCssVariables
|
|
11
|
+
} from '../utils';
|
|
12
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
13
|
+
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
14
|
+
|
|
15
|
+
const ButtonBlock = ({
|
|
16
|
+
block,
|
|
17
|
+
currentBreakpoint = 'desktop',
|
|
18
|
+
isDesigner = false
|
|
19
|
+
}: BlockRendererProps) => {
|
|
20
|
+
const themeSettings = useThemeSettingsContext();
|
|
21
|
+
const { locale } = useLocalization();
|
|
22
|
+
const [emailSubscription] = useEmailSubscriptionMutation();
|
|
23
|
+
const [feedbackModal, setFeedbackModal] = useState({
|
|
24
|
+
open: false,
|
|
25
|
+
title: '',
|
|
26
|
+
message: ''
|
|
27
|
+
});
|
|
28
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
|
|
29
|
+
|
|
30
|
+
const getStyleEntry = (kebabKey: string) => {
|
|
31
|
+
if (!block.styles) return undefined;
|
|
32
|
+
const styles = block.styles as Record<string, unknown>;
|
|
33
|
+
const camelKey = kebabKey.replace(/-([a-z])/g, (_, letter) =>
|
|
34
|
+
letter.toUpperCase()
|
|
35
|
+
);
|
|
36
|
+
return styles[kebabKey] ?? styles[camelKey];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const normalizeKey = (key: string): string => {
|
|
40
|
+
if (key.includes('-')) return key;
|
|
41
|
+
return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getLocalizedText = (textValue: unknown): string => {
|
|
45
|
+
let value = textValue;
|
|
46
|
+
|
|
47
|
+
if (typeof value === 'string' && value.startsWith('{')) {
|
|
48
|
+
try {
|
|
49
|
+
value = JSON.parse(value);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value === 'object' && value !== null) {
|
|
56
|
+
const localized = value as Record<string, unknown>;
|
|
57
|
+
if (localized[locale]) {
|
|
58
|
+
return String(localized[locale]);
|
|
59
|
+
}
|
|
60
|
+
if (localized[defaultLocale]) {
|
|
61
|
+
return String(localized[defaultLocale]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const responsiveValue = getResponsiveValue(
|
|
65
|
+
value,
|
|
66
|
+
currentBreakpoint,
|
|
67
|
+
'Click Me'
|
|
68
|
+
);
|
|
69
|
+
if (responsiveValue && responsiveValue !== 'Click Me') {
|
|
70
|
+
return String(responsiveValue);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const firstValue = Object.values(localized)[0];
|
|
74
|
+
return typeof firstValue === 'string' ? firstValue : 'Click Me';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return 'Click Me';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const textValue = block.properties?.text || block.value;
|
|
85
|
+
const text = getLocalizedText(textValue);
|
|
86
|
+
|
|
87
|
+
const url = block.properties?.url
|
|
88
|
+
? String(getResponsiveValue(block.properties.url, currentBreakpoint, '#'))
|
|
89
|
+
: '#';
|
|
90
|
+
|
|
91
|
+
const target = block.properties?.target
|
|
92
|
+
? String(
|
|
93
|
+
getResponsiveValue(block.properties.target, currentBreakpoint, '_self')
|
|
94
|
+
)
|
|
95
|
+
: '_self';
|
|
96
|
+
|
|
97
|
+
const tag = block.properties?.tag
|
|
98
|
+
? String(getResponsiveValue(block.properties.tag, currentBreakpoint, 'a'))
|
|
99
|
+
: 'a';
|
|
100
|
+
|
|
101
|
+
const normalizeButtonType = (
|
|
102
|
+
value: string
|
|
103
|
+
): 'button' | 'submit' | 'reset' => {
|
|
104
|
+
if (value === 'submit' || value === 'reset') {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return 'button';
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const buttonType =
|
|
112
|
+
tag === 'button'
|
|
113
|
+
? normalizeButtonType(
|
|
114
|
+
String(
|
|
115
|
+
getResponsiveValue(
|
|
116
|
+
block.properties?.type,
|
|
117
|
+
currentBreakpoint,
|
|
118
|
+
'button'
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
: 'button';
|
|
123
|
+
|
|
124
|
+
const iconUrl = block.properties?.icon
|
|
125
|
+
? String(getResponsiveValue(block.properties.icon, currentBreakpoint, ''))
|
|
126
|
+
: '';
|
|
127
|
+
|
|
128
|
+
const iconPosition = block.properties?.iconPosition
|
|
129
|
+
? String(
|
|
130
|
+
getResponsiveValue(
|
|
131
|
+
block.properties.iconPosition,
|
|
132
|
+
currentBreakpoint,
|
|
133
|
+
'left'
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
: 'left';
|
|
137
|
+
|
|
138
|
+
const iconSize = block.properties?.iconSize
|
|
139
|
+
? String(
|
|
140
|
+
getResponsiveValue(block.properties.iconSize, currentBreakpoint, '16')
|
|
141
|
+
)
|
|
142
|
+
: '16';
|
|
143
|
+
|
|
144
|
+
const iconGap = block.properties?.iconGap
|
|
145
|
+
? String(
|
|
146
|
+
getResponsiveValue(block.properties.iconGap, currentBreakpoint, '8px')
|
|
147
|
+
)
|
|
148
|
+
: '8px';
|
|
149
|
+
|
|
150
|
+
const showIcon = iconUrl && iconPosition !== 'none';
|
|
151
|
+
|
|
152
|
+
const fontFamily = block.styles?.['font-family']
|
|
153
|
+
? String(
|
|
154
|
+
getResponsiveValue(
|
|
155
|
+
block.styles['font-family'],
|
|
156
|
+
currentBreakpoint,
|
|
157
|
+
'inherit'
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
: 'inherit';
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!fontFamily || fontFamily === 'inherit') return;
|
|
164
|
+
|
|
165
|
+
const cleanFontFamily = fontFamily.replace(/['"]/g, '').trim();
|
|
166
|
+
|
|
167
|
+
const systemFonts = [
|
|
168
|
+
'Arial',
|
|
169
|
+
'Helvetica',
|
|
170
|
+
'Times New Roman',
|
|
171
|
+
'Georgia',
|
|
172
|
+
'Courier New',
|
|
173
|
+
'Verdana'
|
|
174
|
+
];
|
|
175
|
+
if (systemFonts.includes(cleanFontFamily)) return;
|
|
176
|
+
|
|
177
|
+
const fontParam = `${cleanFontFamily.replace(
|
|
178
|
+
/\s+/g,
|
|
179
|
+
'+'
|
|
180
|
+
)}:wght@100;300;400;500;600;700;800;900`;
|
|
181
|
+
const fontUrl = `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`;
|
|
182
|
+
|
|
183
|
+
const linkId = `google-font-${cleanFontFamily
|
|
184
|
+
.replace(/\s+/g, '-')
|
|
185
|
+
.toLowerCase()}`;
|
|
186
|
+
const existingLink = document.getElementById(linkId);
|
|
187
|
+
if (existingLink) return;
|
|
188
|
+
|
|
189
|
+
const link = document.createElement('link');
|
|
190
|
+
link.href = fontUrl;
|
|
191
|
+
link.rel = 'stylesheet';
|
|
192
|
+
link.id = linkId;
|
|
193
|
+
|
|
194
|
+
document.head.appendChild(link);
|
|
195
|
+
}, [fontFamily]);
|
|
196
|
+
|
|
197
|
+
const toAlpha = (value: unknown, fallback = 100): number | null => {
|
|
198
|
+
const numeric = Number(
|
|
199
|
+
value === undefined || value === null || value === '' ? fallback : value
|
|
200
|
+
);
|
|
201
|
+
if (Number.isNaN(numeric)) return null;
|
|
202
|
+
return Math.max(0, Math.min(1, numeric / 100));
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const getButtonStyles = (): React.CSSProperties => {
|
|
206
|
+
if (!block.styles) return {};
|
|
207
|
+
|
|
208
|
+
const styles: React.CSSProperties = {};
|
|
209
|
+
|
|
210
|
+
const textOpacityValue = getStyleEntry('text-opacity')
|
|
211
|
+
? getResponsiveValue(
|
|
212
|
+
getStyleEntry('text-opacity'),
|
|
213
|
+
currentBreakpoint,
|
|
214
|
+
'100'
|
|
215
|
+
)
|
|
216
|
+
: '100';
|
|
217
|
+
const textOpacity = Number(textOpacityValue);
|
|
218
|
+
|
|
219
|
+
const backgroundOpacityValue = getStyleEntry('background-opacity')
|
|
220
|
+
? getResponsiveValue(
|
|
221
|
+
getStyleEntry('background-opacity'),
|
|
222
|
+
currentBreakpoint,
|
|
223
|
+
'100'
|
|
224
|
+
)
|
|
225
|
+
: '100';
|
|
226
|
+
const backgroundAlpha = toAlpha(backgroundOpacityValue, 100);
|
|
227
|
+
|
|
228
|
+
Object.keys(block.styles).forEach((key) => {
|
|
229
|
+
const value = getResponsiveValue(block.styles[key], currentBreakpoint);
|
|
230
|
+
if (value === undefined || value === null) return;
|
|
231
|
+
|
|
232
|
+
const normalizedKey = normalizeKey(key);
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
normalizedKey === 'text-opacity' ||
|
|
236
|
+
normalizedKey === 'background-opacity' ||
|
|
237
|
+
normalizedKey === 'hover-opacity' ||
|
|
238
|
+
normalizedKey === 'hover-color'
|
|
239
|
+
) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
|
|
244
|
+
letter.toUpperCase()
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
let resolvedValue = value;
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
resolvedValue = resolveThemeCssVariables(value, themeSettings);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (normalizedKey === 'width' && resolvedValue === 'fill') {
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
254
|
+
(styles as any)[camelKey] = '100%';
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (normalizedKey === 'width' && resolvedValue === 'fit') {
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
(styles as any)[camelKey] = 'fit-content';
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (normalizedKey === 'height' && resolvedValue === 'fill') {
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
266
|
+
(styles as any)[camelKey] = '100%';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (normalizedKey === 'height' && resolvedValue === 'fit') {
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
|
+
(styles as any)[camelKey] = 'fit-content';
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
normalizedKey === 'color' &&
|
|
278
|
+
typeof resolvedValue === 'string' &&
|
|
279
|
+
!isNaN(textOpacity)
|
|
280
|
+
) {
|
|
281
|
+
const alpha = toAlpha(textOpacityValue, 100);
|
|
282
|
+
if (alpha !== null) {
|
|
283
|
+
const rgba = colorToRgba(resolvedValue, alpha);
|
|
284
|
+
if (rgba) {
|
|
285
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
286
|
+
(styles as any)[camelKey] = rgba;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Fallback if not a valid hex (e.g. named color or rgb)
|
|
292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
293
|
+
(styles as any)[camelKey] = resolvedValue;
|
|
294
|
+
} else if (
|
|
295
|
+
(normalizedKey === 'background-color' ||
|
|
296
|
+
normalizedKey === 'background') &&
|
|
297
|
+
typeof resolvedValue === 'string' &&
|
|
298
|
+
backgroundAlpha !== null
|
|
299
|
+
) {
|
|
300
|
+
if (backgroundAlpha === 0) {
|
|
301
|
+
// Ensure background disappears even if color can't be parsed (e.g. unresolved CSS var).
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
303
|
+
(styles as any).backgroundColor = 'transparent';
|
|
304
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
305
|
+
(styles as any).backgroundImage = 'none';
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
307
|
+
delete (styles as any).background;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const rgba = colorToRgba(resolvedValue, backgroundAlpha);
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
313
|
+
(styles as any).backgroundColor = rgba || resolvedValue;
|
|
314
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
315
|
+
delete (styles as any).background;
|
|
316
|
+
} else {
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
318
|
+
(styles as any)[camelKey] = resolvedValue;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return styles;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const buttonStyles = getButtonStyles();
|
|
326
|
+
|
|
327
|
+
const finalStyles: React.CSSProperties = {
|
|
328
|
+
textDecoration: 'none',
|
|
329
|
+
display: showIcon ? 'inline-flex' : buttonStyles.display || 'inline-block',
|
|
330
|
+
alignItems: showIcon ? 'center' : undefined,
|
|
331
|
+
justifyContent: showIcon ? 'center' : undefined,
|
|
332
|
+
gap: showIcon ? iconGap : undefined,
|
|
333
|
+
...buttonStyles
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
337
|
+
|
|
338
|
+
const extractMessage = (payload: unknown, fallback: string): string => {
|
|
339
|
+
if (typeof payload === 'string' && payload.trim()) {
|
|
340
|
+
return payload;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!payload || typeof payload !== 'object') {
|
|
344
|
+
return fallback;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const payloadObject = payload as Record<string, unknown>;
|
|
348
|
+
|
|
349
|
+
const directKeys = ['message', 'detail', 'non_field_errors', 'error'];
|
|
350
|
+
for (const key of directKeys) {
|
|
351
|
+
const value = payloadObject[key];
|
|
352
|
+
if (typeof value === 'string' && value.trim()) {
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(value) && typeof value[0] === 'string') {
|
|
356
|
+
return value[0];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const emailValue = payloadObject.email;
|
|
361
|
+
if (Array.isArray(emailValue) && typeof emailValue[0] === 'string') {
|
|
362
|
+
return emailValue[0];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const dataValue = payloadObject.data;
|
|
366
|
+
if (dataValue && typeof dataValue === 'object') {
|
|
367
|
+
return extractMessage(dataValue, fallback);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return fallback;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const showPopup = (message: string, success = false) => {
|
|
374
|
+
if (!message?.trim()) return;
|
|
375
|
+
|
|
376
|
+
setFeedbackModal({
|
|
377
|
+
open: true,
|
|
378
|
+
title: success ? 'Başarılı' : 'Bilgilendirme',
|
|
379
|
+
message
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const handleClick = async (e: React.MouseEvent<HTMLElement>) => {
|
|
384
|
+
if (isDesigner) {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (tag === 'button' && buttonType === 'submit') {
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (tag !== 'button' || buttonType !== 'submit') {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const currentTarget = e.currentTarget as HTMLElement;
|
|
398
|
+
const newsletterContainer = currentTarget.closest(
|
|
399
|
+
'[data-newsletter-signup="true"]'
|
|
400
|
+
) as HTMLElement | null;
|
|
401
|
+
|
|
402
|
+
if (!newsletterContainer) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const rawEndpoint =
|
|
407
|
+
newsletterContainer.getAttribute('data-newsletter-endpoint')?.trim() || '';
|
|
408
|
+
if (!rawEndpoint) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const endpoint =
|
|
413
|
+
rawEndpoint.startsWith('/api/client') ||
|
|
414
|
+
rawEndpoint.startsWith('http://') ||
|
|
415
|
+
rawEndpoint.startsWith('https://')
|
|
416
|
+
? rawEndpoint
|
|
417
|
+
: rawEndpoint.startsWith('/')
|
|
418
|
+
? `/api/client${rawEndpoint}`
|
|
419
|
+
: `/api/client/${rawEndpoint}`;
|
|
420
|
+
|
|
421
|
+
const isEmailSubscriptionEndpoint =
|
|
422
|
+
rawEndpoint.includes('email-subscription') ||
|
|
423
|
+
endpoint.includes('/api/client/email-subscription');
|
|
424
|
+
|
|
425
|
+
const inputElement = newsletterContainer.querySelector(
|
|
426
|
+
'input[type="email"], input[name="email"], input'
|
|
427
|
+
) as HTMLInputElement | null;
|
|
428
|
+
|
|
429
|
+
const email = inputElement?.value?.trim() || '';
|
|
430
|
+
if (!email) {
|
|
431
|
+
inputElement?.focus();
|
|
432
|
+
showPopup('Email is required.');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!EMAIL_REGEX.test(email)) {
|
|
437
|
+
inputElement?.focus();
|
|
438
|
+
showPopup('Enter a valid email address.');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const successMessage =
|
|
443
|
+
newsletterContainer.getAttribute('data-newsletter-success-message') ||
|
|
444
|
+
'Successfully subscribed.';
|
|
445
|
+
const errorMessage =
|
|
446
|
+
newsletterContainer.getAttribute('data-newsletter-error-message') ||
|
|
447
|
+
'Subscription failed. Please try again.';
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
let popupMessage = successMessage;
|
|
451
|
+
|
|
452
|
+
if (isEmailSubscriptionEndpoint) {
|
|
453
|
+
const result = await emailSubscription({
|
|
454
|
+
email,
|
|
455
|
+
subscribe_contract: false
|
|
456
|
+
}).unwrap();
|
|
457
|
+
popupMessage = extractMessage(result, successMessage);
|
|
458
|
+
} else {
|
|
459
|
+
const response = await fetch(endpoint, {
|
|
460
|
+
method: 'POST',
|
|
461
|
+
headers: {
|
|
462
|
+
Accept: '*/*',
|
|
463
|
+
'Content-Type': 'application/json'
|
|
464
|
+
},
|
|
465
|
+
credentials: 'include',
|
|
466
|
+
body: JSON.stringify({ email })
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
let responsePayload: unknown = null;
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const contentType = response.headers.get('content-type') || '';
|
|
473
|
+
if (contentType.includes('application/json')) {
|
|
474
|
+
responsePayload = await response.json();
|
|
475
|
+
} else {
|
|
476
|
+
const textPayload = await response.text();
|
|
477
|
+
responsePayload = textPayload || null;
|
|
478
|
+
}
|
|
479
|
+
} catch {
|
|
480
|
+
responsePayload = null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
throw new Error(extractMessage(responsePayload, errorMessage));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
popupMessage = extractMessage(responsePayload, successMessage);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
newsletterContainer.dispatchEvent(
|
|
491
|
+
new CustomEvent('newsletter:submit', {
|
|
492
|
+
bubbles: true,
|
|
493
|
+
detail: {
|
|
494
|
+
success: true,
|
|
495
|
+
message: popupMessage,
|
|
496
|
+
email
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
);
|
|
500
|
+
showPopup(popupMessage, true);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
const message =
|
|
503
|
+
error instanceof Error && error.message
|
|
504
|
+
? error.message
|
|
505
|
+
: extractMessage(error, errorMessage);
|
|
506
|
+
|
|
507
|
+
newsletterContainer.dispatchEvent(
|
|
508
|
+
new CustomEvent('newsletter:submit', {
|
|
509
|
+
bubbles: true,
|
|
510
|
+
detail: {
|
|
511
|
+
success: false,
|
|
512
|
+
message,
|
|
513
|
+
email
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
showPopup(message);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const shouldShowText = text && text.trim() !== '';
|
|
522
|
+
|
|
523
|
+
const iconElement = (position: 'left' | 'right') =>
|
|
524
|
+
showIcon &&
|
|
525
|
+
iconPosition === position && (
|
|
526
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
527
|
+
<img
|
|
528
|
+
src={iconUrl}
|
|
529
|
+
alt="icon"
|
|
530
|
+
style={{
|
|
531
|
+
width: `${iconSize}px`,
|
|
532
|
+
height: `${iconSize}px`,
|
|
533
|
+
flexShrink: 0
|
|
534
|
+
}}
|
|
535
|
+
/>
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const content = (
|
|
539
|
+
<>
|
|
540
|
+
{iconElement('left')}
|
|
541
|
+
{shouldShowText && <span>{text}</span>}
|
|
542
|
+
{iconElement('right')}
|
|
543
|
+
</>
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const feedbackModalElement = (
|
|
547
|
+
<Modal
|
|
548
|
+
portalId={`newsletter-feedback-modal-${block.id}`}
|
|
549
|
+
open={feedbackModal.open}
|
|
550
|
+
setOpen={(open) =>
|
|
551
|
+
setFeedbackModal((prev) => ({
|
|
552
|
+
...prev,
|
|
553
|
+
open
|
|
554
|
+
}))
|
|
555
|
+
}
|
|
556
|
+
title={feedbackModal.title}
|
|
557
|
+
showCloseButton={true}
|
|
558
|
+
className="w-[92vw] max-w-[420px] rounded"
|
|
559
|
+
>
|
|
560
|
+
<div className="p-6 text-sm leading-6 text-center text-black">
|
|
561
|
+
{feedbackModal.message}
|
|
562
|
+
</div>
|
|
563
|
+
</Modal>
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
if (tag === 'button') {
|
|
567
|
+
return (
|
|
568
|
+
<>
|
|
569
|
+
<button type={buttonType} style={finalStyles} onClick={handleClick}>
|
|
570
|
+
{content}
|
|
571
|
+
</button>
|
|
572
|
+
{feedbackModalElement}
|
|
573
|
+
</>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
<>
|
|
579
|
+
<a
|
|
580
|
+
href={url}
|
|
581
|
+
target={target}
|
|
582
|
+
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
|
583
|
+
style={finalStyles}
|
|
584
|
+
onClick={handleClick}
|
|
585
|
+
>
|
|
586
|
+
{content}
|
|
587
|
+
</a>
|
|
588
|
+
{feedbackModalElement}
|
|
589
|
+
</>
|
|
590
|
+
);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
export default ButtonBlock;
|