@akinon/next 2.0.0-beta.20 → 2.0.0-beta.22

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/api/auth.ts +292 -60
  3. package/bin/pz-install-plugins.js +1 -1
  4. package/package.json +3 -3
  5. package/types/index.ts +19 -6
  6. package/types/next-auth.d.ts +1 -1
  7. package/with-pz-config.js +8 -1
  8. package/components/theme-editor/blocks/accordion-block.tsx +0 -136
  9. package/components/theme-editor/blocks/block-renderer-registry.tsx +0 -77
  10. package/components/theme-editor/blocks/button-block.tsx +0 -593
  11. package/components/theme-editor/blocks/counter-block.tsx +0 -348
  12. package/components/theme-editor/blocks/divider-block.tsx +0 -20
  13. package/components/theme-editor/blocks/embed-block.tsx +0 -208
  14. package/components/theme-editor/blocks/group-block.tsx +0 -116
  15. package/components/theme-editor/blocks/hotspot-block.tsx +0 -147
  16. package/components/theme-editor/blocks/icon-block.tsx +0 -230
  17. package/components/theme-editor/blocks/image-block.tsx +0 -137
  18. package/components/theme-editor/blocks/image-gallery-block.tsx +0 -269
  19. package/components/theme-editor/blocks/input-block.tsx +0 -123
  20. package/components/theme-editor/blocks/link-block.tsx +0 -216
  21. package/components/theme-editor/blocks/lottie-block.tsx +0 -325
  22. package/components/theme-editor/blocks/map-block.tsx +0 -89
  23. package/components/theme-editor/blocks/slider-block.tsx +0 -595
  24. package/components/theme-editor/blocks/tab-block.tsx +0 -10
  25. package/components/theme-editor/blocks/text-block.tsx +0 -52
  26. package/components/theme-editor/blocks/video-block.tsx +0 -122
  27. package/components/theme-editor/components/action-toolbar.tsx +0 -305
  28. package/components/theme-editor/components/designer-overlay.tsx +0 -74
  29. package/components/theme-editor/components/with-designer-features.tsx +0 -142
  30. package/components/theme-editor/dynamic-font-loader.tsx +0 -79
  31. package/components/theme-editor/hooks/use-designer-features.tsx +0 -100
  32. package/components/theme-editor/hooks/use-external-designer.tsx +0 -95
  33. package/components/theme-editor/hooks/use-native-widget-data.ts +0 -188
  34. package/components/theme-editor/hooks/use-visibility-context.ts +0 -27
  35. package/components/theme-editor/placeholder-registry.ts +0 -31
  36. package/components/theme-editor/sections/before-after-section.tsx +0 -245
  37. package/components/theme-editor/sections/contact-form-section.tsx +0 -563
  38. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +0 -433
  39. package/components/theme-editor/sections/coupon-banner-section.tsx +0 -710
  40. package/components/theme-editor/sections/divider-section.tsx +0 -62
  41. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +0 -507
  42. package/components/theme-editor/sections/find-in-store-section.tsx +0 -1995
  43. package/components/theme-editor/sections/hover-showcase-section.tsx +0 -326
  44. package/components/theme-editor/sections/image-hotspot-section.tsx +0 -142
  45. package/components/theme-editor/sections/installment-options-section.tsx +0 -1065
  46. package/components/theme-editor/sections/notification-banner-section.tsx +0 -173
  47. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +0 -1379
  48. package/components/theme-editor/sections/posts-slider-section.tsx +0 -472
  49. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +0 -663
  50. package/components/theme-editor/sections/section-renderer-registry.tsx +0 -89
  51. package/components/theme-editor/sections/section-wrapper.tsx +0 -135
  52. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +0 -586
  53. package/components/theme-editor/sections/stats-counter-section.tsx +0 -486
  54. package/components/theme-editor/sections/tabs-section.tsx +0 -578
  55. package/components/theme-editor/theme-block.tsx +0 -102
  56. package/components/theme-editor/theme-placeholder-client.tsx +0 -218
  57. package/components/theme-editor/theme-placeholder-wrapper.tsx +0 -732
  58. package/components/theme-editor/theme-placeholder.tsx +0 -288
  59. package/components/theme-editor/theme-section.tsx +0 -1224
  60. package/components/theme-editor/theme-settings-context.tsx +0 -13
  61. package/components/theme-editor/utils/index.ts +0 -792
  62. package/components/theme-editor/utils/iterator-utils.ts +0 -234
  63. package/components/theme-editor/utils/publish-window.ts +0 -86
  64. package/components/theme-editor/utils/visibility-rules.ts +0 -188
@@ -1,593 +0,0 @@
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;