@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,147 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { BlockRendererProps } from './block-renderer-registry';
5
+ import { getResponsiveValue, getCSSStyles } from '../utils';
6
+ import { useThemeSettingsContext } from '../theme-settings-context';
7
+ import { useLocalization } from '@akinon/next/hooks';
8
+ import ThemeBlock from '../theme-block';
9
+
10
+ const HotspotBlock = ({
11
+ block,
12
+ currentBreakpoint = 'desktop',
13
+ isDesigner,
14
+ placeholderId,
15
+ sectionId,
16
+ selectedBlockId
17
+ }: BlockRendererProps) => {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const themeSettings = useThemeSettingsContext();
20
+ const { locale } = useLocalization();
21
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
22
+
23
+ const styles = block.styles || {};
24
+ const properties = block.properties || {};
25
+
26
+ const getLocalizedContent = (content: any): string => {
27
+ if (typeof content === 'string') return content;
28
+ if (typeof content === 'object' && content !== null) {
29
+ return (
30
+ content[locale] || content[defaultLocale] || Object.values(content)[0] || ''
31
+ );
32
+ }
33
+ return '';
34
+ };
35
+
36
+ const title = getLocalizedContent(properties.title);
37
+ const description = getLocalizedContent(properties.description);
38
+ const link = getLocalizedContent(properties.link);
39
+ const linkText = getLocalizedContent(properties.linkText || 'View Details');
40
+
41
+ const backgroundColor = getResponsiveValue(styles['background-color'], currentBreakpoint, '#ffffff');
42
+ const color = getResponsiveValue(styles.color, currentBreakpoint, '#000000');
43
+
44
+ const formatPercentage = (val: string | number | undefined, defaultVal: string): string => {
45
+ if (val === undefined || val === null || val === '') return defaultVal;
46
+ const strVal = String(val);
47
+ if (strVal.endsWith('%')) return strVal;
48
+ const num = parseFloat(strVal);
49
+ return isNaN(num) ? defaultVal : `${num}%`;
50
+ };
51
+
52
+ const top = formatPercentage(getResponsiveValue(styles.top, currentBreakpoint) as string | number | undefined, '50%');
53
+ const left = formatPercentage(getResponsiveValue(styles.left, currentBreakpoint) as string | number | undefined, '50%');
54
+
55
+ const handleToggle = (e: React.MouseEvent) => {
56
+ if (isDesigner) return;
57
+ e.stopPropagation();
58
+ setIsOpen(!isOpen);
59
+ };
60
+
61
+ return (
62
+ <div
63
+ className="absolute group pointer-events-auto"
64
+ style={{ top: top as string, left: left as string }}
65
+ >
66
+ <button
67
+ onClick={handleToggle}
68
+ className="relative flex items-center justify-center w-8 h-8 -ml-4 -mt-4 cursor-pointer outline-none focus:outline-none z-10 pointer-events-auto"
69
+ aria-label={title || "Hotspot"}
70
+ >
71
+ <span
72
+ className="absolute w-full h-full rounded-full animate-ping opacity-75"
73
+ style={{ backgroundColor: backgroundColor as string }}
74
+ ></span>
75
+
76
+ <span
77
+ className="relative w-4 h-4 rounded-full shadow-md"
78
+ style={{ backgroundColor: backgroundColor as string }}
79
+ ></span>
80
+ </button>
81
+
82
+ {(isOpen || (isDesigner && styles.forceOpen)) && (
83
+ <div
84
+ className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 w-64 bg-white p-4 rounded-lg shadow-xl z-20 text-left text-sm pointer-events-auto"
85
+ >
86
+ <div className="flex justify-between items-start mb-2">
87
+ {title && <h3 className="font-bold text-gray-900">{title}</h3>}
88
+ <button onClick={(e) => { e.stopPropagation(); setIsOpen(false); }} className="text-gray-400 hover:text-gray-600">
89
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
90
+ <line x1="18" y1="6" x2="6" y2="18"></line>
91
+ <line x1="6" y1="6" x2="18" y2="18"></line>
92
+ </svg>
93
+ </button>
94
+ </div>
95
+ {description && <p className="text-gray-600 mb-3">{description}</p>}
96
+
97
+ {block.blocks && block.blocks.map((childBlock) => (
98
+ <div key={childBlock.id} className="mb-2">
99
+ <ThemeBlock
100
+ block={childBlock}
101
+ placeholderId={placeholderId}
102
+ sectionId={sectionId}
103
+ isDesigner={isDesigner}
104
+ currentBreakpoint={currentBreakpoint}
105
+ isSelected={selectedBlockId === childBlock.id}
106
+ selectedBlockId={selectedBlockId}
107
+ onMoveUp={() => {
108
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_UP', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
109
+ }}
110
+ onMoveDown={() => {
111
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_DOWN', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
112
+ }}
113
+ onDuplicate={() => {
114
+ if (window.parent) window.parent.postMessage({ type: 'DUPLICATE_BLOCK', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
115
+ }}
116
+ onToggleVisibility={() => {
117
+ if (window.parent) window.parent.postMessage({ type: 'TOGGLE_BLOCK_VISIBILITY', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
118
+ }}
119
+ onDelete={() => {
120
+ if (window.parent) window.parent.postMessage({ type: 'DELETE_BLOCK_FROM_PARENT', data: { placeholderId, sectionId, blockId: childBlock.id, parentBlockId: block.id } }, '*');
121
+ }}
122
+ onRename={(newLabel) => {
123
+ if (window.parent) window.parent.postMessage({ type: 'RENAME_BLOCK', data: { placeholderId, sectionId, blockId: childBlock.id, label: newLabel } }, '*');
124
+ }}
125
+ />
126
+ </div>
127
+ ))}
128
+
129
+ {link && (
130
+ <a
131
+ href={link}
132
+ className="text-blue-600 hover:underline font-medium block mt-2"
133
+ onClick={(e) => e.stopPropagation()}
134
+ >
135
+ {linkText} &rarr;
136
+ </a>
137
+ )}
138
+
139
+ {/* Arrow */}
140
+ <div className="absolute left-1/2 top-full -translate-x-1/2 -mt-1 border-8 border-transparent border-t-white"></div>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export default HotspotBlock;
@@ -0,0 +1,230 @@
1
+ import React from 'react';
2
+ import { getResponsiveValue } from '../utils';
3
+ import { BlockRendererProps } from './block-renderer-registry';
4
+ import { Image } from '../../image';
5
+
6
+ const isValidUrlValue = (value: string): boolean => {
7
+ if (!value) return false;
8
+ if (value.startsWith('data:image')) return true;
9
+
10
+ try {
11
+ new URL(value, 'http://localhost');
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ };
17
+
18
+ const IconBlock = ({ block }: BlockRendererProps) => {
19
+ const iconValue = getResponsiveValue(block.value, 'desktop', '');
20
+ const iconSize = getResponsiveValue(block.styles?.size, 'desktop', '24') as
21
+ | string
22
+ | number;
23
+ const iconColor = getResponsiveValue(
24
+ block.styles?.color,
25
+ 'desktop',
26
+ 'currentColor'
27
+ ) as string;
28
+
29
+ const iconStyles = {
30
+ width: iconSize + 'px',
31
+ height: iconSize + 'px',
32
+ display: 'inline-flex',
33
+ alignItems: 'center',
34
+ justifyContent: 'center'
35
+ } as React.CSSProperties;
36
+
37
+ const isInlineSvg = (value: string): boolean => {
38
+ return (
39
+ typeof value === 'string' &&
40
+ (value.includes('<svg') || value.includes('<?xml'))
41
+ );
42
+ };
43
+
44
+ const modifySVGColor = (svgContent: string, color: string) => {
45
+ if (color === 'currentColor' || !color) return svgContent;
46
+
47
+ return (
48
+ svgContent
49
+ .replace(/fill\s*=\s*["'][^"']*["']/gi, `fill="${color}"`)
50
+ .replace(/stroke\s*=\s*["'][^"']*["']/gi, `stroke="${color}"`)
51
+ // Also handle CSS styles within SVG
52
+ .replace(/fill\s*:\s*[^;}\s]+/gi, `fill: ${color}`)
53
+ .replace(/stroke\s*:\s*[^;}\s]+/gi, `stroke: ${color}`)
54
+ // If no fill/stroke exists, add fill to main elements
55
+ .replace(/<path(?![^>]*fill)/gi, `<path fill="${color}"`)
56
+ .replace(/<circle(?![^>]*fill)/gi, `<circle fill="${color}"`)
57
+ .replace(/<rect(?![^>]*fill)/gi, `<rect fill="${color}"`)
58
+ .replace(/<polygon(?![^>]*fill)/gi, `<polygon fill="${color}"`)
59
+ .replace(/<ellipse(?![^>]*fill)/gi, `<ellipse fill="${color}"`)
60
+ );
61
+ };
62
+
63
+ const processBase64SVG = (base64String: string, color: string) => {
64
+ try {
65
+ const base64Content = base64String.replace(
66
+ /^data:image\/svg\+xml;base64,/,
67
+ ''
68
+ );
69
+
70
+ const svgContent = atob(base64Content);
71
+
72
+ const modifiedSVG = modifySVGColor(svgContent, color);
73
+
74
+ const modifiedBase64 = `data:image/svg+xml;base64,${btoa(modifiedSVG)}`;
75
+
76
+ return modifiedBase64;
77
+ } catch (error) {
78
+ console.error('Error processing SVG base64:', error);
79
+ return base64String;
80
+ }
81
+ };
82
+
83
+ let iconContent;
84
+
85
+ let svgSrc = '';
86
+
87
+ if (iconValue) {
88
+ if (typeof iconValue === 'string') {
89
+ svgSrc = iconValue.trim();
90
+ } else if (typeof iconValue === 'object' && iconValue !== null) {
91
+ if ('url' in iconValue) {
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ svgSrc = String((iconValue as any).url || '').trim();
94
+ } else {
95
+ // Try to find a string value in the object (e.g. localized)
96
+ const values = Object.values(iconValue);
97
+ const stringVal = values.find((v) => typeof v === 'string');
98
+ if (stringVal) {
99
+ svgSrc = (stringVal as string).trim();
100
+ }
101
+ }
102
+ }
103
+
104
+ const isBase64 =
105
+ typeof svgSrc === 'string' && svgSrc.startsWith('data:image');
106
+ const isSvg = isInlineSvg(svgSrc);
107
+ const isAbsolutePath = svgSrc.startsWith('/');
108
+
109
+ if (
110
+ !isBase64 &&
111
+ !isSvg &&
112
+ svgSrc &&
113
+ !isAbsolutePath &&
114
+ !svgSrc.startsWith('http') &&
115
+ !svgSrc.startsWith('//')
116
+ ) {
117
+ const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
118
+ svgSrc = cloudName
119
+ ? `https://${cloudName}/${svgSrc.replace(/^\/+/, '')}`
120
+ : '';
121
+ }
122
+
123
+ if (
124
+ isBase64 &&
125
+ svgSrc.includes('data:image/svg+xml;base64,') &&
126
+ iconColor !== 'currentColor'
127
+ ) {
128
+ svgSrc = processBase64SVG(svgSrc, iconColor);
129
+ }
130
+
131
+ if (svgSrc && svgSrc.trim() !== '') {
132
+ // Check if it's inline SVG content
133
+ if (isInlineSvg(svgSrc)) {
134
+ // Apply color modification to inline SVG
135
+ const coloredSvg =
136
+ iconColor !== 'currentColor'
137
+ ? modifySVGColor(svgSrc, iconColor)
138
+ : svgSrc;
139
+
140
+ iconContent = (
141
+ <div
142
+ style={{
143
+ ...iconStyles,
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ ['--svg-size' as any]: iconSize + 'px'
146
+ }}
147
+ className="[&>svg]:w-[var(--svg-size)] [&>svg]:h-[var(--svg-size)]"
148
+ dangerouslySetInnerHTML={{ __html: coloredSvg }}
149
+ />
150
+ );
151
+ } else {
152
+ // It's a URL or base64, use img tag
153
+ if (isValidUrlValue(svgSrc)) {
154
+ iconContent = (
155
+ <div style={iconStyles}>
156
+ <Image
157
+ src={svgSrc}
158
+ width={Number(iconSize)}
159
+ height={Number(iconSize)}
160
+ alt="Icon"
161
+ />
162
+ </div>
163
+ );
164
+ }
165
+ }
166
+ } else {
167
+ iconContent = (
168
+ <div
169
+ style={{
170
+ ...iconStyles,
171
+ border: '1px dashed #ccc',
172
+ borderRadius: '4px',
173
+ backgroundColor: '#f9f9f9'
174
+ }}
175
+ >
176
+ <span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ if (!iconContent) {
182
+ iconContent = (
183
+ <div
184
+ style={{
185
+ ...iconStyles,
186
+ border: '1px dashed #ccc',
187
+ borderRadius: '4px',
188
+ backgroundColor: '#f9f9f9'
189
+ }}
190
+ >
191
+ <span style={{ fontSize: '10px', color: '#999' }}>No icon</span>
192
+ </div>
193
+ );
194
+ }
195
+ } else {
196
+ iconContent = (
197
+ <div
198
+ style={{
199
+ ...iconStyles,
200
+ border: '2px dashed #e2e8f0',
201
+ borderRadius: '8px',
202
+ backgroundColor: '#f8fafc',
203
+ transition: 'all 0.2s ease-in-out'
204
+ }}
205
+ className="hover:border-slate-300 hover:bg-slate-50"
206
+ >
207
+ <div className="flex flex-col items-center justify-center gap-1 p-2">
208
+ <svg
209
+ width="16"
210
+ height="16"
211
+ viewBox="0 0 24 24"
212
+ fill="none"
213
+ stroke="#94a3b8"
214
+ strokeWidth="2"
215
+ strokeLinecap="round"
216
+ strokeLinejoin="round"
217
+ >
218
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
219
+ <circle cx="8.5" cy="8.5" r="1.5" />
220
+ <polyline points="21,15 16,10 5,21" />
221
+ </svg>
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ return iconContent;
228
+ };
229
+
230
+ export default IconBlock;
@@ -0,0 +1,137 @@
1
+ import React, { useMemo } from 'react';
2
+
3
+ import { getResponsiveValue } from '../utils';
4
+ import { BlockRendererProps } from './block-renderer-registry';
5
+
6
+ const pickImageSource = (
7
+ value: unknown,
8
+ currentBreakpoint: string
9
+ ): string => {
10
+ if (typeof value === 'string') {
11
+ return value.trim();
12
+ }
13
+
14
+ if (typeof value === 'object' && value !== null) {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const objectValue = value as any;
17
+ if (typeof objectValue.url === 'string') {
18
+ return objectValue.url.trim();
19
+ }
20
+
21
+ const responsiveValue = getResponsiveValue(value, currentBreakpoint, '');
22
+ if (typeof responsiveValue === 'string') {
23
+ return responsiveValue.trim();
24
+ }
25
+
26
+ const firstStringValue = Object.values(objectValue).find(
27
+ (item) => typeof item === 'string'
28
+ );
29
+ if (typeof firstStringValue === 'string') {
30
+ return firstStringValue.trim();
31
+ }
32
+ }
33
+
34
+ return '';
35
+ };
36
+
37
+ const normalizeImageSource = (src: string): string => {
38
+ if (!src) return '';
39
+ if (src.startsWith('data:image')) return src;
40
+ if (src.startsWith('/')) return src;
41
+ if (src.startsWith('//')) return src;
42
+ if (src.startsWith('http://') || src.startsWith('https://')) return src;
43
+
44
+ const cloudName = process.env.NEXT_PUBLIC_IMAGE_CLOUD_NAME?.trim();
45
+ if (!cloudName) return '';
46
+
47
+ return `https://${cloudName}/${src.replace(/^\/+/, '')}`;
48
+ };
49
+
50
+ const isValidImageSource = (src: string): boolean => {
51
+ if (!src) return false;
52
+ if (src.startsWith('data:image')) return true;
53
+
54
+ try {
55
+ new URL(src, 'http://localhost');
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ };
61
+
62
+ const ImageBlock = ({
63
+ block,
64
+ currentBreakpoint = 'desktop'
65
+ }: BlockRendererProps) => {
66
+ const imageValue = useMemo(() => {
67
+ const rawUrl = pickImageSource(block.value, currentBreakpoint);
68
+ const url = normalizeImageSource(rawUrl);
69
+ const alt = String(block.properties?.alt || '');
70
+
71
+ return {
72
+ url: isValidImageSource(url) ? url : '',
73
+ alt
74
+ };
75
+ }, [block.value, block.properties?.alt, currentBreakpoint]);
76
+
77
+ const { url: src, alt } = imageValue;
78
+
79
+ const width = getResponsiveValue(
80
+ block.styles?.width,
81
+ currentBreakpoint,
82
+ '100%'
83
+ ) as React.CSSProperties['width'];
84
+ const height = getResponsiveValue(
85
+ block.styles?.height,
86
+ currentBreakpoint,
87
+ 'auto'
88
+ ) as React.CSSProperties['height'];
89
+ const objectFit = getResponsiveValue(
90
+ block.styles?.['object-fit'],
91
+ currentBreakpoint,
92
+ 'cover'
93
+ ) as React.CSSProperties['objectFit'];
94
+ const borderRadius = getResponsiveValue(
95
+ block.styles?.['border-radius'],
96
+ currentBreakpoint,
97
+ '0px'
98
+ ) as React.CSSProperties['borderRadius'];
99
+
100
+ const content = !src ? (
101
+ <div
102
+ style={{
103
+ width: width ?? '100%',
104
+ height: height ?? '100%',
105
+ borderRadius: borderRadius ?? '0px',
106
+ overflow: 'hidden',
107
+ backgroundColor: '#e5e7eb',
108
+ display: 'flex',
109
+ alignItems: 'center',
110
+ justifyContent: 'center',
111
+ textAlign: 'center',
112
+ minHeight:
113
+ typeof height === 'string' && height !== 'auto' ? height : '150px',
114
+ color: '#6b7280'
115
+ }}
116
+ >
117
+ No image uploaded
118
+ </div>
119
+ ) : (
120
+ <img
121
+ key={src}
122
+ src={src}
123
+ alt={alt}
124
+ style={{
125
+ display: 'block',
126
+ width: width ?? '100%',
127
+ height: height ?? 'auto',
128
+ objectFit: objectFit ?? 'cover',
129
+ borderRadius: borderRadius ?? '0px'
130
+ }}
131
+ />
132
+ );
133
+
134
+ return content;
135
+ };
136
+
137
+ export default ImageBlock;