@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,595 @@
1
+ import React from 'react';
2
+ import Carousel from 'react-multi-carousel';
3
+ import 'react-multi-carousel/lib/styles.css';
4
+ import clsx from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+
7
+ import ThemeBlock from '../theme-block';
8
+ import { useThemeSettingsContext } from '../theme-settings-context';
9
+ import {
10
+ getCSSStyles,
11
+ getResponsiveValue,
12
+ resolveThemeCssVariables
13
+ } from '../utils';
14
+ import { BlockRendererProps } from './block-renderer-registry';
15
+
16
+ const parseNumber = (value: unknown, fallback: number) => {
17
+ if (value === undefined || value === null || value === '') return fallback;
18
+ const parsed = typeof value === 'string' ? Number(value) : Number(value);
19
+ return Number.isFinite(parsed) ? parsed : fallback;
20
+ };
21
+
22
+ const clampNumber = (value: number, min: number, max: number) =>
23
+ Math.min(Math.max(value, min), max);
24
+
25
+ const SliderBlock: React.FC<BlockRendererProps> = ({
26
+ block,
27
+ placeholderId,
28
+ sectionId,
29
+ isDesigner = false,
30
+ selectedBlockId = null,
31
+ currentBreakpoint = 'desktop',
32
+ onMoveUp,
33
+ onMoveDown,
34
+ onDuplicate,
35
+ onToggleVisibility,
36
+ onDelete,
37
+ onRename
38
+ }) => {
39
+ const themeSettings = useThemeSettingsContext();
40
+ const carouselRef = React.useRef<Carousel>(null);
41
+ const containerRef = React.useRef<HTMLDivElement>(null);
42
+
43
+ const leftArrowIconBlock = block.blocks?.find(
44
+ (b: any) => b.type === 'icon' && b.label === 'Left Arrow Icon'
45
+ );
46
+
47
+ const rightArrowIconBlock = block.blocks?.find(
48
+ (b: any) => b.type === 'icon' && b.label === 'Right Arrow Icon'
49
+ );
50
+
51
+ const getConfigEntry = (key: string) =>
52
+ block.properties?.[key] ?? block.styles?.[key];
53
+
54
+ const normalizeResponsiveNumber = (
55
+ value: unknown,
56
+ fallback: { desktop: number; mobile: number }
57
+ ) => {
58
+ if (value === undefined || value === null || value === '') {
59
+ return { ...fallback };
60
+ }
61
+
62
+ if (typeof value === 'number' || typeof value === 'string') {
63
+ const parsed = parseNumber(value, fallback.desktop);
64
+ return { desktop: parsed, mobile: parsed };
65
+ }
66
+
67
+ if (typeof value === 'object') {
68
+ const responsiveValue = value as Record<string, unknown>;
69
+ const candidate =
70
+ typeof responsiveValue.desktop === 'object' &&
71
+ responsiveValue.desktop !== null
72
+ ? (responsiveValue.desktop as Record<string, unknown>)
73
+ : responsiveValue;
74
+
75
+ return {
76
+ desktop: parseNumber(candidate.desktop, fallback.desktop),
77
+ mobile: parseNumber(candidate.mobile, fallback.mobile)
78
+ };
79
+ }
80
+
81
+ return { ...fallback };
82
+ };
83
+
84
+ const itemsConfig = normalizeResponsiveNumber(
85
+ getConfigEntry('items-per-slide'),
86
+ { desktop: 4, mobile: 1 }
87
+ );
88
+ const slidesConfig = normalizeResponsiveNumber(
89
+ getConfigEntry('slides-to-slide'),
90
+ { desktop: 1, mobile: 1 }
91
+ );
92
+
93
+ const sortedBlocks = [...(block.blocks || [])]
94
+ .filter(
95
+ (childBlock) =>
96
+ childBlock.type !== 'icon' && (isDesigner ? true : !childBlock.hidden)
97
+ )
98
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
99
+ const visibleCount = sortedBlocks.length;
100
+
101
+ const desktopItems = clampNumber(itemsConfig.desktop, 1, 8);
102
+ const mobileItems = clampNumber(itemsConfig.mobile, 1, 4);
103
+ const desktopSlides = clampNumber(slidesConfig.desktop, 1, 8);
104
+ const mobileSlides = clampNumber(slidesConfig.mobile, 1, 8);
105
+
106
+ const carouselResponsive = {
107
+ desktop: {
108
+ breakpoint: { max: 4000, min: 768 },
109
+ items: desktopItems
110
+ },
111
+ mobile: {
112
+ breakpoint: { max: 768, min: 0 },
113
+ items: mobileItems
114
+ }
115
+ };
116
+
117
+ const currentSlidesToSlide =
118
+ currentBreakpoint === 'mobile' ? mobileSlides : desktopSlides;
119
+
120
+ const showArrows = getResponsiveValue(
121
+ getConfigEntry('show-arrows'),
122
+ currentBreakpoint,
123
+ true
124
+ );
125
+ const showDots = getResponsiveValue(
126
+ getConfigEntry('show-dots'),
127
+ currentBreakpoint,
128
+ false
129
+ );
130
+ const useAutoPlay = getResponsiveValue(
131
+ getConfigEntry('autoplay'),
132
+ currentBreakpoint,
133
+ false
134
+ );
135
+
136
+ const useInfinite = useAutoPlay;
137
+
138
+ const autoPlaySpeedRaw = getConfigEntry('autoplay-speed');
139
+ const autoPlaySpeed = parseNumber(autoPlaySpeedRaw, 3000);
140
+
141
+ const slideGap = parseNumber(getConfigEntry('slide-gap'), 16);
142
+ const slidePadding = slideGap / 2;
143
+
144
+ const blockStyles = block.styles || {};
145
+ const sectionStyles = getCSSStyles(
146
+ typeof blockStyles === 'object' && blockStyles !== null
147
+ ? (blockStyles as Record<string, unknown>)
148
+ : {},
149
+ themeSettings,
150
+ currentBreakpoint
151
+ );
152
+
153
+ const containerStylesRaw = blockStyles['max-width'];
154
+ const hasMaxWidth =
155
+ containerStylesRaw !== undefined &&
156
+ containerStylesRaw !== null &&
157
+ containerStylesRaw !== '';
158
+ const maxWidthValue = getResponsiveValue(
159
+ containerStylesRaw,
160
+ currentBreakpoint,
161
+ '1200px'
162
+ );
163
+ const maxWidthClass = hasMaxWidth ? `max-w-[${maxWidthValue}]` : '';
164
+
165
+ const blockAction = (action: string, blockId: string, label?: string) => {
166
+ if (!isDesigner) return;
167
+ window.parent.postMessage(
168
+ {
169
+ type: 'BLOCK_ACTION',
170
+ action,
171
+ blockId,
172
+ sectionId,
173
+ placeholderId,
174
+ label
175
+ },
176
+ '*'
177
+ );
178
+ };
179
+
180
+ const modifySVGColor = (svgContent: string, color: string) => {
181
+ if (color === 'currentColor' || !color) return svgContent;
182
+
183
+ return svgContent
184
+ .replace(/fill\s*=\s*["'][^"']*["']/gi, `fill="${color}"`)
185
+ .replace(/stroke\s*=\s*["'][^"']*["']/gi, `stroke="${color}"`)
186
+ .replace(/fill\s*:\s*[^;}\s]+/gi, `fill: ${color}`)
187
+ .replace(/stroke\s*:\s*[^;}\s]+/gi, `stroke: ${color}`)
188
+ .replace(/<path(?![^>]*fill)/gi, `<path fill="${color}"`)
189
+ .replace(/<circle(?![^>]*fill)/gi, `<circle fill="${color}"`)
190
+ .replace(/<rect(?![^>]*fill)/gi, `<rect fill="${color}"`)
191
+ .replace(/<polygon(?![^>]*fill)/gi, `<polygon fill="${color}"`)
192
+ .replace(/<ellipse(?![^>]*fill)/gi, `<ellipse fill="${color}"`);
193
+ };
194
+
195
+ const isInlineSvg = (value: string): boolean => {
196
+ return (
197
+ typeof value === 'string' &&
198
+ (value.includes('<svg') || value.includes('<?xml'))
199
+ );
200
+ };
201
+
202
+ const processInlineSVG = (svgContent: string, color: string) => {
203
+ try {
204
+ const modifiedSVG = modifySVGColor(svgContent, color);
205
+ const base64 = btoa(modifiedSVG);
206
+ return `data:image/svg+xml;base64,${base64}`;
207
+ } catch (error) {
208
+ console.error('Error processing inline SVG:', error);
209
+ return '';
210
+ }
211
+ };
212
+
213
+ const processBase64SVG = (base64String: string, color: string) => {
214
+ try {
215
+ const base64Content = base64String.replace(
216
+ /^data:image\/svg\+xml;base64,/,
217
+ ''
218
+ );
219
+ const svgContent = atob(base64Content);
220
+ const modifiedSVG = modifySVGColor(svgContent, color);
221
+ const modifiedBase64 = `data:image/svg+xml;base64,${btoa(modifiedSVG)}`;
222
+ return modifiedBase64;
223
+ } catch (error) {
224
+ console.error('Error processing SVG base64:', error);
225
+ return base64String;
226
+ }
227
+ };
228
+
229
+ const getIconUrl = (iconBlock: any): string => {
230
+ if (!iconBlock?.value) return '';
231
+ const value = iconBlock.value;
232
+
233
+ if (typeof value === 'string') {
234
+ return value;
235
+ }
236
+
237
+ if (typeof value === 'object') {
238
+ // Handle responsive value structure
239
+ const responsiveUrl = getResponsiveValue(value, currentBreakpoint, '');
240
+ if (typeof responsiveUrl === 'string') {
241
+ return responsiveUrl;
242
+ }
243
+ // Handle {url: '...', alt: '...'} structure
244
+ if (value.url) {
245
+ return value.url;
246
+ }
247
+ }
248
+
249
+ return '';
250
+ };
251
+
252
+ const defaultLeftArrowSvg =
253
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDZMOSAxMkwxNSAxOCIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==';
254
+ const defaultRightArrowSvg =
255
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkgNkwxNSAxMkw5IDE4IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+';
256
+
257
+ const leftArrowIconUrl = getIconUrl(leftArrowIconBlock);
258
+ const rightArrowIconUrl = getIconUrl(rightArrowIconBlock);
259
+
260
+ const leftArrowIconSize = String(
261
+ getResponsiveValue(leftArrowIconBlock?.styles?.size, '24')
262
+ );
263
+ const rightArrowIconSize = String(
264
+ getResponsiveValue(rightArrowIconBlock?.styles?.size, '24')
265
+ );
266
+
267
+ const leftArrowIconColor = resolveThemeCssVariables(
268
+ String(
269
+ getResponsiveValue(leftArrowIconBlock?.styles?.['color'], '#ffffff')
270
+ ),
271
+ themeSettings
272
+ );
273
+ const rightArrowIconColor = resolveThemeCssVariables(
274
+ String(
275
+ getResponsiveValue(rightArrowIconBlock?.styles?.['color'], '#ffffff')
276
+ ),
277
+ themeSettings
278
+ );
279
+
280
+ const leftArrowIconOpacity =
281
+ parseFloat(
282
+ String(getResponsiveValue(leftArrowIconBlock?.styles?.['opacity'], '1'))
283
+ ) || 1;
284
+ const rightArrowIconOpacity =
285
+ parseFloat(
286
+ String(getResponsiveValue(rightArrowIconBlock?.styles?.['opacity'], '1'))
287
+ ) || 1;
288
+
289
+ // Process SVGs with color
290
+ const processedLeftArrowUrl = isInlineSvg(leftArrowIconUrl)
291
+ ? processInlineSVG(leftArrowIconUrl, leftArrowIconColor)
292
+ : (leftArrowIconUrl || defaultLeftArrowSvg).includes(
293
+ 'data:image/svg+xml;base64,'
294
+ )
295
+ ? processBase64SVG(
296
+ leftArrowIconUrl || defaultLeftArrowSvg,
297
+ leftArrowIconColor
298
+ )
299
+ : leftArrowIconUrl || defaultLeftArrowSvg;
300
+
301
+ const processedRightArrowUrl = isInlineSvg(rightArrowIconUrl)
302
+ ? processInlineSVG(rightArrowIconUrl, rightArrowIconColor)
303
+ : (rightArrowIconUrl || defaultRightArrowSvg).includes(
304
+ 'data:image/svg+xml;base64,'
305
+ )
306
+ ? processBase64SVG(
307
+ rightArrowIconUrl || defaultRightArrowSvg,
308
+ rightArrowIconColor
309
+ )
310
+ : rightArrowIconUrl || defaultRightArrowSvg;
311
+
312
+ const leftArrowIcon = {
313
+ url: processedLeftArrowUrl,
314
+ alt: 'Previous'
315
+ };
316
+ const rightArrowIcon = {
317
+ url: processedRightArrowUrl,
318
+ alt: 'Next'
319
+ };
320
+
321
+ const leftArrowBackground = resolveThemeCssVariables(
322
+ String(
323
+ getResponsiveValue(
324
+ leftArrowIconBlock?.styles?.['background'] ??
325
+ leftArrowIconBlock?.styles?.['backgroundColor'],
326
+ 'rgba(0, 0, 0, 0.5)'
327
+ )
328
+ ),
329
+ themeSettings
330
+ );
331
+
332
+ const rightArrowBackground = resolveThemeCssVariables(
333
+ String(
334
+ getResponsiveValue(
335
+ rightArrowIconBlock?.styles?.['background'] ??
336
+ rightArrowIconBlock?.styles?.['backgroundColor'],
337
+ 'rgba(0, 0, 0, 0.5)'
338
+ )
339
+ ),
340
+ themeSettings
341
+ );
342
+
343
+ const handleLeftArrowClick = (e: React.MouseEvent) => {
344
+ e.preventDefault();
345
+ e.stopPropagation();
346
+
347
+ carouselRef.current?.previous?.(1);
348
+ };
349
+
350
+ const handleRightArrowClick = (e: React.MouseEvent) => {
351
+ e.preventDefault();
352
+ e.stopPropagation();
353
+
354
+ carouselRef.current?.next?.(1);
355
+ };
356
+
357
+ const CustomLeftArrow = ({ onClick }: any) => (
358
+ <button
359
+ onClick={(e) => {
360
+ e.stopPropagation();
361
+ if (!isDesigner) {
362
+ onClick?.();
363
+ }
364
+ handleLeftArrowClick(e);
365
+ }}
366
+ className={clsx(
367
+ 'absolute left-4 top-1/2 -translate-y-1/2 z-10',
368
+ isDesigner &&
369
+ selectedBlockId === leftArrowIconBlock?.id &&
370
+ 'ring-2 ring-blue-500 ring-offset-2'
371
+ )}
372
+ style={{
373
+ backgroundColor: leftArrowBackground,
374
+ border: 'none',
375
+ borderRadius: '50%',
376
+ width: '40px',
377
+ height: '40px',
378
+ display: 'flex',
379
+ alignItems: 'center',
380
+ justifyContent: 'center',
381
+ cursor: 'pointer',
382
+ padding: 0
383
+ }}
384
+ aria-label="Previous"
385
+ >
386
+ <img
387
+ src={leftArrowIcon.url}
388
+ alt={leftArrowIcon.alt}
389
+ style={{
390
+ width: `${leftArrowIconSize}px`,
391
+ height: `${leftArrowIconSize}px`,
392
+ opacity: leftArrowIconOpacity,
393
+ pointerEvents: 'none'
394
+ }}
395
+ />
396
+ </button>
397
+ );
398
+
399
+ const CustomRightArrow = ({ onClick }: any) => (
400
+ <button
401
+ onClick={(e) => {
402
+ e.stopPropagation();
403
+ if (!isDesigner) {
404
+ onClick?.();
405
+ }
406
+ handleRightArrowClick(e);
407
+ }}
408
+ className={clsx(
409
+ 'absolute right-4 top-1/2 -translate-y-1/2 z-10',
410
+ isDesigner &&
411
+ selectedBlockId === rightArrowIconBlock?.id &&
412
+ 'ring-2 ring-blue-500 ring-offset-2'
413
+ )}
414
+ style={{
415
+ backgroundColor: rightArrowBackground,
416
+ border: 'none',
417
+ borderRadius: '50%',
418
+ width: '40px',
419
+ height: '40px',
420
+ display: 'flex',
421
+ alignItems: 'center',
422
+ justifyContent: 'center',
423
+ cursor: 'pointer',
424
+ padding: 0
425
+ }}
426
+ aria-label="Next"
427
+ >
428
+ <img
429
+ src={rightArrowIcon.url}
430
+ alt={rightArrowIcon.alt}
431
+ style={{
432
+ width: `${rightArrowIconSize}px`,
433
+ height: `${rightArrowIconSize}px`,
434
+ opacity: rightArrowIconOpacity,
435
+ pointerEvents: 'none'
436
+ }}
437
+ />
438
+ </button>
439
+ );
440
+
441
+ if (visibleCount === 0) {
442
+ return (
443
+ <div
444
+ className="p-4 text-gray-400 border border-dashed border-gray-300 rounded"
445
+ style={sectionStyles}
446
+ >
447
+ No items available
448
+ </div>
449
+ );
450
+ }
451
+
452
+ return (
453
+ <div
454
+ className={twMerge(
455
+ clsx('relative z-10 w-full', hasMaxWidth && 'mx-auto', maxWidthClass)
456
+ )}
457
+ style={sectionStyles}
458
+ >
459
+ <div style={{ position: 'relative' }}>
460
+ <Carousel
461
+ ref={carouselRef}
462
+ key={`${desktopItems}-${mobileItems}-${desktopSlides}-${mobileSlides}-${visibleCount}-${useInfinite}-${useAutoPlay}-${autoPlaySpeed}`}
463
+ responsive={carouselResponsive}
464
+ slidesToSlide={currentSlidesToSlide}
465
+ swipeable={true}
466
+ draggable={true}
467
+ arrows={isDesigner ? false : Boolean(showArrows)}
468
+ showDots={Boolean(showDots)}
469
+ renderDotsOutside={Boolean(showDots)}
470
+ infinite={Boolean(useInfinite)}
471
+ rewind={Boolean(useInfinite)}
472
+ rewindWithAnimation={Boolean(useInfinite)}
473
+ autoPlay={Boolean(useAutoPlay)}
474
+ autoPlaySpeed={autoPlaySpeed}
475
+ containerClass="slider-carousel"
476
+ dotListClass="slider-dots"
477
+ customLeftArrow={
478
+ !isDesigner && showArrows ? <CustomLeftArrow /> : undefined
479
+ }
480
+ customRightArrow={
481
+ !isDesigner && showArrows ? <CustomRightArrow /> : undefined
482
+ }
483
+ >
484
+ {sortedBlocks.map((childBlock) => (
485
+ <div
486
+ key={childBlock.id}
487
+ style={{
488
+ paddingLeft: slidePadding,
489
+ paddingRight: slidePadding
490
+ }}
491
+ >
492
+ <ThemeBlock
493
+ block={childBlock}
494
+ placeholderId={placeholderId}
495
+ sectionId={sectionId}
496
+ isDesigner={isDesigner}
497
+ isSelected={selectedBlockId === childBlock.id}
498
+ selectedBlockId={selectedBlockId}
499
+ currentBreakpoint={currentBreakpoint}
500
+ onMoveUp={() => blockAction('MOVE_BLOCK_UP', childBlock.id)}
501
+ onMoveDown={() => blockAction('MOVE_BLOCK_DOWN', childBlock.id)}
502
+ onDuplicate={() =>
503
+ blockAction('DUPLICATE_BLOCK', childBlock.id)
504
+ }
505
+ onToggleVisibility={() =>
506
+ blockAction('TOGGLE_BLOCK_VISIBILITY', childBlock.id)
507
+ }
508
+ onDelete={() => blockAction('DELETE_BLOCK', childBlock.id)}
509
+ onRename={(newLabel: string) =>
510
+ blockAction('RENAME_BLOCK', childBlock.id, newLabel)
511
+ }
512
+ />
513
+ </div>
514
+ ))}
515
+ </Carousel>
516
+
517
+ {isDesigner &&
518
+ showArrows &&
519
+ sortedBlocks.length > 0 &&
520
+ leftArrowIconBlock &&
521
+ rightArrowIconBlock ? (
522
+ <>
523
+ <button
524
+ onClick={handleLeftArrowClick}
525
+ className={clsx(
526
+ 'absolute left-4 top-1/2 -translate-y-1/2 z-10',
527
+ selectedBlockId === leftArrowIconBlock?.id &&
528
+ 'ring-2 ring-blue-500 ring-offset-2'
529
+ )}
530
+ style={{
531
+ backgroundColor: leftArrowBackground,
532
+ border: 'none',
533
+ borderRadius: '50%',
534
+ width: '40px',
535
+ height: '40px',
536
+ display: 'flex',
537
+ alignItems: 'center',
538
+ justifyContent: 'center',
539
+ cursor: 'pointer',
540
+ padding: 0
541
+ }}
542
+ aria-label="Previous"
543
+ >
544
+ <img
545
+ src={leftArrowIcon.url}
546
+ alt={leftArrowIcon.alt}
547
+ style={{
548
+ width: `${leftArrowIconSize}px`,
549
+ height: `${leftArrowIconSize}px`,
550
+ opacity: leftArrowIconOpacity,
551
+ pointerEvents: 'none'
552
+ }}
553
+ />
554
+ </button>
555
+
556
+ <button
557
+ onClick={handleRightArrowClick}
558
+ className={clsx(
559
+ 'absolute right-4 top-1/2 -translate-y-1/2 z-10',
560
+ selectedBlockId === rightArrowIconBlock?.id &&
561
+ 'ring-2 ring-blue-500 ring-offset-2'
562
+ )}
563
+ style={{
564
+ backgroundColor: rightArrowBackground,
565
+ border: 'none',
566
+ borderRadius: '50%',
567
+ width: '40px',
568
+ height: '40px',
569
+ display: 'flex',
570
+ alignItems: 'center',
571
+ justifyContent: 'center',
572
+ cursor: 'pointer',
573
+ padding: 0
574
+ }}
575
+ aria-label="Next"
576
+ >
577
+ <img
578
+ src={rightArrowIcon.url}
579
+ alt={rightArrowIcon.alt}
580
+ style={{
581
+ width: `${rightArrowIconSize}px`,
582
+ height: `${rightArrowIconSize}px`,
583
+ opacity: rightArrowIconOpacity,
584
+ pointerEvents: 'none'
585
+ }}
586
+ />
587
+ </button>
588
+ </>
589
+ ) : null}
590
+ </div>
591
+ </div>
592
+ );
593
+ };
594
+
595
+ export default SliderBlock;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { BlockRendererProps } from './block-renderer-registry';
3
+
4
+ // Tab blocks are rendered by TabsSection, not individually
5
+ // This component should not be used directly
6
+ const TabBlock: React.FC<BlockRendererProps> = () => {
7
+ return null;
8
+ };
9
+
10
+ export default TabBlock;
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+ import React, { useMemo } from 'react';
5
+ import { BlockRendererProps } from './block-renderer-registry';
6
+ import { getCSSStyles } from '../utils';
7
+ import { useThemeSettingsContext } from '../theme-settings-context';
8
+
9
+ const TextBlock = ({
10
+ block,
11
+ currentBreakpoint = 'desktop'
12
+ }: BlockRendererProps) => {
13
+ const { locale } = useLocalization();
14
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
15
+ const themeSettings = useThemeSettingsContext();
16
+
17
+ const getLocalizedContent = (): string => {
18
+ let value = block.value;
19
+
20
+ if (typeof value === 'string' && value.startsWith('{')) {
21
+ try {
22
+ value = JSON.parse(value);
23
+ } catch (e) {
24
+ return value;
25
+ }
26
+ }
27
+
28
+ if (typeof value === 'object' && value !== null) {
29
+ return (
30
+ value[locale] || value[defaultLocale] || Object.values(value)[0] || ''
31
+ );
32
+ }
33
+
34
+ if (typeof value === 'string') {
35
+ return value;
36
+ }
37
+
38
+ return '';
39
+ };
40
+
41
+ const content = getLocalizedContent();
42
+
43
+ const wrapperStyles = useMemo(() => {
44
+ return getCSSStyles(block.styles || {}, themeSettings, currentBreakpoint);
45
+ }, [block.styles, themeSettings, currentBreakpoint]);
46
+
47
+ return (
48
+ <div style={wrapperStyles} dangerouslySetInnerHTML={{ __html: content }} />
49
+ );
50
+ };
51
+
52
+ export default TextBlock;