@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.
Files changed (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
@@ -0,0 +1,269 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+
5
+ import { Modal } from '../../modal';
6
+ import ThemeBlock from '../theme-block';
7
+ import { getResponsiveValue } from '../utils';
8
+ import { BlockRendererProps } from './block-renderer-registry';
9
+
10
+ type GalleryImage = {
11
+ url: string;
12
+ alt: string;
13
+ id: string;
14
+ objectFit: React.CSSProperties['objectFit'];
15
+ borderRadius: React.CSSProperties['borderRadius'];
16
+ width: React.CSSProperties['width'];
17
+ height: React.CSSProperties['height'];
18
+ };
19
+
20
+ const resolveImageUrl = (raw: unknown): string => {
21
+ const url = typeof raw === 'string' ? raw : '';
22
+
23
+ const isBase64 = url.startsWith('data:image');
24
+ const isAbsolutePath = url.startsWith('/');
25
+
26
+ if (
27
+ !isBase64 &&
28
+ !isAbsolutePath &&
29
+ url &&
30
+ !url.startsWith('http') &&
31
+ !url.startsWith('//')
32
+ ) {
33
+ return `https://${process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME ?? ''}/${url}`;
34
+ }
35
+
36
+ return url;
37
+ };
38
+
39
+ const ImageGalleryBlock = ({
40
+ block,
41
+ placeholderId,
42
+ sectionId,
43
+ isDesigner,
44
+ selectedBlockId,
45
+ currentBreakpoint = 'desktop'
46
+ }: BlockRendererProps) => {
47
+ const images: GalleryImage[] = useMemo(() => {
48
+ const children = Array.isArray(block.blocks) ? block.blocks : [];
49
+
50
+ return children
51
+ .filter((child) => child.type === 'image')
52
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
53
+ .map((child) => {
54
+ const url = resolveImageUrl(child.value);
55
+
56
+ const rawAlt = child.properties?.alt;
57
+ const alt =
58
+ typeof rawAlt === 'string'
59
+ ? rawAlt
60
+ : typeof rawAlt === 'object' && rawAlt
61
+ ? String(rawAlt[currentBreakpoint] ?? rawAlt.desktop ?? '')
62
+ : '';
63
+
64
+ const objectFit = getResponsiveValue(
65
+ child.styles?.['object-fit'],
66
+ currentBreakpoint,
67
+ 'cover'
68
+ ) as React.CSSProperties['objectFit'];
69
+ const borderRadius = getResponsiveValue(
70
+ child.styles?.['border-radius'],
71
+ currentBreakpoint,
72
+ '0px'
73
+ ) as React.CSSProperties['borderRadius'];
74
+ const width = getResponsiveValue(
75
+ child.styles?.width,
76
+ currentBreakpoint,
77
+ '100%'
78
+ ) as React.CSSProperties['width'];
79
+ const height = getResponsiveValue(
80
+ child.styles?.height,
81
+ currentBreakpoint,
82
+ 'auto'
83
+ ) as React.CSSProperties['height'];
84
+
85
+ return {
86
+ url,
87
+ alt,
88
+ id: child.id,
89
+ objectFit: objectFit ?? 'cover',
90
+ borderRadius: borderRadius ?? '0px',
91
+ width: width ?? '100%',
92
+ height: height ?? 'auto'
93
+ };
94
+ })
95
+ .filter((img) => Boolean(img.url));
96
+ }, [block.blocks, currentBreakpoint]);
97
+
98
+ // Hooks must be called unconditionally. `isDesigner` can flip after mount
99
+ // (theme editor cookie / iframe messages), so keep state/effects above any early returns.
100
+ const [open, setOpen] = useState(false);
101
+ const [activeIndex, setActiveIndex] = useState(0);
102
+
103
+ const goPrev = () => {
104
+ if (!images.length) return;
105
+ setActiveIndex((i) => (i - 1 + images.length) % images.length);
106
+ };
107
+
108
+ const goNext = () => {
109
+ if (!images.length) return;
110
+ setActiveIndex((i) => (i + 1) % images.length);
111
+ };
112
+
113
+ useEffect(() => {
114
+ if (isDesigner) return;
115
+ if (!open) return;
116
+
117
+ const onKeyDown = (e: KeyboardEvent) => {
118
+ if (e.key === 'Escape') setOpen(false);
119
+ if (e.key === 'ArrowLeft') goPrev();
120
+ if (e.key === 'ArrowRight') goNext();
121
+ };
122
+
123
+ window.addEventListener('keydown', onKeyDown);
124
+ return () => window.removeEventListener('keydown', onKeyDown);
125
+ }, [isDesigner, open, images.length]);
126
+
127
+ useEffect(() => {
128
+ if (activeIndex >= images.length) {
129
+ setActiveIndex(0);
130
+ }
131
+ }, [activeIndex, images.length]);
132
+
133
+ // In designer mode: keep nested ThemeBlock rendering so the editor can select/edit child images.
134
+ if (isDesigner) {
135
+ if (!block.blocks || block.blocks.length === 0) {
136
+ return (
137
+ <div style={{ padding: '20px', color: '#6b7280' }}>
138
+ Empty image gallery
139
+ </div>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <div className="contents">
145
+ {block.blocks
146
+ .filter((child) => (isDesigner ? true : !child.hidden))
147
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
148
+ .map((child) => (
149
+ <ThemeBlock
150
+ key={child.id}
151
+ block={child}
152
+ placeholderId={placeholderId}
153
+ sectionId={sectionId}
154
+ isDesigner={isDesigner}
155
+ isSelected={selectedBlockId === child.id}
156
+ selectedBlockId={selectedBlockId}
157
+ currentBreakpoint={currentBreakpoint}
158
+ />
159
+ ))}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ if (!images.length) {
165
+ return (
166
+ <div style={{ padding: '20px', color: '#6b7280' }}>
167
+ No images uploaded
168
+ </div>
169
+ );
170
+ }
171
+
172
+ const active = images[activeIndex];
173
+
174
+ return (
175
+ <>
176
+ <div className="contents">
177
+ {images.map((img, index) => (
178
+ <button
179
+ key={img.id}
180
+ type="button"
181
+ onClick={(e) => {
182
+ e.stopPropagation();
183
+ setActiveIndex(index);
184
+ setOpen(true);
185
+ }}
186
+ className="relative"
187
+ style={{
188
+ all: 'unset',
189
+ cursor: 'pointer',
190
+ display: 'block',
191
+ width: '100%',
192
+ height: '100%'
193
+ }}
194
+ aria-label={img.alt || `Open image ${index + 1}`}
195
+ >
196
+ <img
197
+ src={img.url}
198
+ alt={img.alt}
199
+ style={{
200
+ display: 'block',
201
+ width: img.width ?? '100%',
202
+ height: img.height ?? 'auto',
203
+ objectFit: img.objectFit ?? 'cover',
204
+ borderRadius: img.borderRadius ?? '0px'
205
+ }}
206
+ />
207
+ </button>
208
+ ))}
209
+ </div>
210
+
211
+ <Modal
212
+ portalId="image-gallery-lightbox"
213
+ open={open}
214
+ setOpen={setOpen}
215
+ title=""
216
+ showCloseButton={true}
217
+ className="w-[92vw] max-w-[1024px] max-h-[90vh] overflow-hidden rounded"
218
+ headerWrapperClassName="border-0"
219
+ >
220
+ <div
221
+ className="relative bg-black"
222
+ style={{ width: '100%', height: '80vh' }}
223
+ >
224
+ <img
225
+ src={active.url}
226
+ alt={active.alt}
227
+ style={{
228
+ width: '100%',
229
+ height: '100%',
230
+ objectFit: 'contain'
231
+ }}
232
+ />
233
+
234
+ <button
235
+ type="button"
236
+ onClick={goPrev}
237
+ aria-label="Previous"
238
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-white"
239
+ style={{
240
+ background: 'rgba(0,0,0,0.45)',
241
+ width: '40px',
242
+ height: '40px',
243
+ borderRadius: '9999px'
244
+ }}
245
+ >
246
+
247
+ </button>
248
+
249
+ <button
250
+ type="button"
251
+ onClick={goNext}
252
+ aria-label="Next"
253
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-white"
254
+ style={{
255
+ background: 'rgba(0,0,0,0.45)',
256
+ width: '40px',
257
+ height: '40px',
258
+ borderRadius: '9999px'
259
+ }}
260
+ >
261
+
262
+ </button>
263
+ </div>
264
+ </Modal>
265
+ </>
266
+ );
267
+ };
268
+
269
+ export default ImageGalleryBlock;
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useMemo, useState } from 'react';
4
+ import { useLocalization } from '@akinon/next/hooks';
5
+ import { getResponsiveValue, resolveThemeCssVariables } from '../utils';
6
+ import { BlockRendererProps } from './block-renderer-registry';
7
+ import { useThemeSettingsContext } from '../theme-settings-context';
8
+
9
+ const InputBlock = ({
10
+ block,
11
+ currentBreakpoint = 'desktop',
12
+ isDesigner = false
13
+ }: BlockRendererProps) => {
14
+ const { locale } = useLocalization();
15
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
16
+ const themeSettings = useThemeSettingsContext();
17
+
18
+ const resolveLocalizedValue = (
19
+ value: unknown,
20
+ fallback: string
21
+ ): string => {
22
+ if (value == null) return fallback;
23
+
24
+ if (typeof value === 'object' && !Array.isArray(value)) {
25
+ const localized = value as Record<string, unknown>;
26
+
27
+ if (localized[locale] != null) return String(localized[locale]);
28
+ if (localized[defaultLocale] != null)
29
+ return String(localized[defaultLocale]);
30
+
31
+ const responsive = getResponsiveValue(value, currentBreakpoint);
32
+ if (responsive != null && typeof responsive !== 'object') {
33
+ return String(responsive);
34
+ }
35
+
36
+ const firstValue = Object.values(localized).find(item => item != null);
37
+ if (firstValue != null) return String(firstValue);
38
+ }
39
+
40
+ const responsive = getResponsiveValue(value, currentBreakpoint, fallback);
41
+ return String(responsive ?? fallback);
42
+ };
43
+
44
+ const type = resolveLocalizedValue(block.properties?.type, 'text');
45
+ const name = resolveLocalizedValue(block.properties?.name, 'input');
46
+ const placeholder = resolveLocalizedValue(
47
+ block.properties?.placeholder,
48
+ 'Enter text...'
49
+ );
50
+ const blockValue = resolveLocalizedValue(block.value, '');
51
+
52
+ const required = Boolean(
53
+ getResponsiveValue(block.properties?.required, currentBreakpoint, false)
54
+ );
55
+ const disabled = Boolean(
56
+ getResponsiveValue(block.properties?.disabled, currentBreakpoint, false)
57
+ );
58
+
59
+ const [localValue, setLocalValue] = useState(blockValue);
60
+
61
+ useEffect(() => {
62
+ setLocalValue(blockValue);
63
+ }, [blockValue]);
64
+
65
+ const inputStyles: React.CSSProperties = useMemo(() => {
66
+ const computedStyles: React.CSSProperties = {};
67
+
68
+ if (!block.styles) {
69
+ return computedStyles;
70
+ }
71
+
72
+ Object.keys(block.styles).forEach((key) => {
73
+ const styleValue = getResponsiveValue(block.styles[key], currentBreakpoint);
74
+ if (styleValue === undefined || styleValue === null) return;
75
+
76
+ const camelKey = key.replace(/-([a-z])/g, (_, letter) =>
77
+ letter.toUpperCase()
78
+ );
79
+
80
+ let resolvedValue: unknown = styleValue;
81
+ if (typeof styleValue === 'string') {
82
+ resolvedValue = resolveThemeCssVariables(styleValue, themeSettings);
83
+ }
84
+
85
+ (computedStyles as Record<string, unknown>)[camelKey] = resolvedValue;
86
+ });
87
+
88
+ return computedStyles;
89
+ }, [block.styles, currentBreakpoint, themeSettings]);
90
+
91
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
92
+ const nextValue = event.target.value;
93
+ setLocalValue(nextValue);
94
+
95
+ if (isDesigner && window.parent) {
96
+ window.parent.postMessage(
97
+ {
98
+ type: 'UPDATE_BLOCK_VALUE',
99
+ data: {
100
+ blockId: block.id,
101
+ value: nextValue
102
+ }
103
+ },
104
+ '*'
105
+ );
106
+ }
107
+ };
108
+
109
+ return (
110
+ <input
111
+ type={type}
112
+ name={name}
113
+ placeholder={placeholder}
114
+ value={localValue}
115
+ required={required}
116
+ disabled={disabled}
117
+ style={inputStyles}
118
+ onChange={handleChange}
119
+ />
120
+ );
121
+ };
122
+
123
+ export default InputBlock;
@@ -0,0 +1,216 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo } from 'react';
4
+ import NextLink from 'next/link';
5
+ import { useLocalization } from '@akinon/next/hooks';
6
+ import { LocaleUrlStrategy } from '@akinon/next/localization';
7
+ import { urlLocaleMatcherRegex, urlSchemes } from '@akinon/next/utils';
8
+ import { getResponsiveValue } from '../utils';
9
+ import { BlockRendererProps } from './block-renderer-registry';
10
+ import ThemeBlock from '../theme-block';
11
+
12
+ const LinkBlock = ({
13
+ block,
14
+ placeholderId,
15
+ sectionId,
16
+ selectedBlockId,
17
+ currentBreakpoint = 'desktop',
18
+ isDesigner = false
19
+ }: BlockRendererProps) => {
20
+ const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
21
+
22
+ const href = block.properties?.href
23
+ ? String(getResponsiveValue(block.properties.href, currentBreakpoint, '#'))
24
+ : '#';
25
+
26
+ const target = block.properties?.target
27
+ ? String(
28
+ getResponsiveValue(block.properties.target, currentBreakpoint, '_self')
29
+ )
30
+ : '_self';
31
+
32
+ const rel = target === '_blank' ? 'noopener noreferrer' : undefined;
33
+
34
+ const formattedHref = useMemo(() => {
35
+ if (!href) {
36
+ return '#';
37
+ }
38
+
39
+ const trimmedHref = href.trim();
40
+ if (!trimmedHref) {
41
+ return '#';
42
+ }
43
+
44
+ if (urlSchemes.some((scheme) => trimmedHref.startsWith(scheme))) {
45
+ if (trimmedHref.startsWith('mailto:') || trimmedHref.startsWith('tel:')) {
46
+ return trimmedHref;
47
+ }
48
+
49
+ try {
50
+ new URL(trimmedHref);
51
+ return trimmedHref;
52
+ } catch {
53
+ return '#';
54
+ }
55
+ }
56
+
57
+ try {
58
+ new URL(trimmedHref, 'http://localhost');
59
+ } catch {
60
+ return '#';
61
+ }
62
+
63
+ const pathnameWithoutLocale = trimmedHref.replace(
64
+ urlLocaleMatcherRegex,
65
+ ''
66
+ );
67
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
68
+
69
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
70
+ return hrefWithLocale;
71
+ } else if (
72
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
73
+ locale !== defaultLocaleValue
74
+ ) {
75
+ return hrefWithLocale;
76
+ }
77
+
78
+ return trimmedHref || '#';
79
+ }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
80
+
81
+ const getStyleEntry = (kebabKey: string) => {
82
+ if (!block.styles) return undefined;
83
+ const styles = block.styles as Record<string, unknown>;
84
+ const camelKey = kebabKey.replace(/-([a-z])/g, (_, letter) =>
85
+ letter.toUpperCase()
86
+ );
87
+ return styles[kebabKey] ?? styles[camelKey];
88
+ };
89
+
90
+ const normalizeKey = (key: string): string => {
91
+ if (key.includes('-')) return key;
92
+ return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
93
+ };
94
+
95
+ const inlineStyles: React.CSSProperties = {};
96
+ if (block.styles) {
97
+ const keys = Object.keys(block.styles);
98
+ keys.forEach((key) => {
99
+ const normalizedKey = normalizeKey(key);
100
+
101
+ if (
102
+ normalizedKey === 'hover-flex' ||
103
+ normalizedKey === 'hover-child-flex' ||
104
+ normalizedKey === 'first-child-flex'
105
+ ) {
106
+ return;
107
+ }
108
+
109
+ const styleEntry = getStyleEntry(normalizedKey);
110
+ if (styleEntry) {
111
+ const val = getResponsiveValue(styleEntry, currentBreakpoint);
112
+ if (val !== undefined && val !== null) {
113
+ const camelKey = normalizedKey.replace(/-([a-z])/g, (_, letter) =>
114
+ letter.toUpperCase()
115
+ );
116
+ (inlineStyles as any)[camelKey] = val;
117
+ }
118
+ }
119
+ });
120
+ }
121
+
122
+ if (!block.blocks || block.blocks.length === 0) {
123
+ return (
124
+ <NextLink
125
+ href={isDesigner ? '#' : formattedHref}
126
+ target={isDesigner ? undefined : target}
127
+ rel={isDesigner ? undefined : rel}
128
+ style={inlineStyles}
129
+ onClick={(e) => {
130
+ if (isDesigner) {
131
+ e.preventDefault();
132
+ }
133
+ }}
134
+ >
135
+ <div style={{ padding: '20px', color: '#6b7280' }}>
136
+ Empty link block
137
+ </div>
138
+ </NextLink>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <NextLink
144
+ href={isDesigner ? '#' : formattedHref}
145
+ target={isDesigner ? undefined : target}
146
+ rel={isDesigner ? undefined : rel}
147
+ style={inlineStyles}
148
+ onClick={(e) => {
149
+ if (isDesigner) {
150
+ e.preventDefault();
151
+ }
152
+ }}
153
+ >
154
+ {block.blocks
155
+ .filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
156
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
157
+ .map((childBlock, index) => {
158
+ const createActionHandler = (actionType: string) => () => {
159
+ if (window.parent) {
160
+ window.parent.postMessage(
161
+ {
162
+ type: actionType,
163
+ data: {
164
+ placeholderId,
165
+ sectionId,
166
+ blockId: childBlock.id
167
+ }
168
+ },
169
+ '*'
170
+ );
171
+ }
172
+ };
173
+
174
+ const handleRename = (newLabel: string) => {
175
+ if (window.parent) {
176
+ window.parent.postMessage(
177
+ {
178
+ type: 'RENAME_BLOCK',
179
+ data: {
180
+ placeholderId,
181
+ sectionId,
182
+ blockId: childBlock.id,
183
+ label: newLabel
184
+ }
185
+ },
186
+ '*'
187
+ );
188
+ }
189
+ };
190
+
191
+ return (
192
+ <ThemeBlock
193
+ key={childBlock.id || `block-${index}`}
194
+ block={childBlock}
195
+ placeholderId={placeholderId}
196
+ sectionId={sectionId}
197
+ isDesigner={isDesigner}
198
+ isSelected={selectedBlockId === childBlock.id}
199
+ selectedBlockId={selectedBlockId}
200
+ currentBreakpoint={currentBreakpoint}
201
+ onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
202
+ onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
203
+ onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
204
+ onToggleVisibility={createActionHandler(
205
+ 'TOGGLE_BLOCK_VISIBILITY'
206
+ )}
207
+ onDelete={createActionHandler('DELETE_BLOCK')}
208
+ onRename={handleRename}
209
+ />
210
+ );
211
+ })}
212
+ </NextLink>
213
+ );
214
+ };
215
+
216
+ export default LinkBlock;