@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,326 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useLocalization } from '@akinon/next/hooks';
5
+ import ThemeBlock, { Block } from '../theme-block';
6
+ import { useThemeSettingsContext } from '../theme-settings-context';
7
+ import {
8
+ getCSSStyles,
9
+ getResponsiveValue,
10
+ resolveThemeCssVariables
11
+ } from '../utils';
12
+
13
+ import { Section } from '../theme-section';
14
+ import { WithDesignerFeatures } from '../components/with-designer-features';
15
+
16
+ interface HoverShowcaseSectionProps {
17
+ section: Section;
18
+ currentBreakpoint?: string;
19
+ placeholderId?: string;
20
+ isDesigner?: boolean;
21
+ selectedBlockId?: string | null;
22
+ }
23
+
24
+ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
25
+ section,
26
+ currentBreakpoint = 'desktop',
27
+ placeholderId = '',
28
+ isDesigner = false,
29
+ selectedBlockId = null
30
+ }) => {
31
+ const { locale } = useLocalization();
32
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || 'en';
33
+ const themeSettings = useThemeSettingsContext();
34
+
35
+ const [activeIndex, setActiveIndex] = useState(0);
36
+
37
+ useEffect(() => {
38
+ if (isDesigner && selectedBlockId && section.blocks) {
39
+ const index = section.blocks.findIndex(
40
+ (b: Block) => b.id === selectedBlockId || b.blocks?.some((child: Block) => child.id === selectedBlockId)
41
+ );
42
+ if (index !== -1) {
43
+ setActiveIndex(index);
44
+ }
45
+ }
46
+ }, [isDesigner, selectedBlockId, section.blocks]);
47
+
48
+ const resolveLocalizedValue = (
49
+ value: unknown,
50
+ fallback: string
51
+ ): string => {
52
+ if (value == null) return fallback;
53
+
54
+ if (typeof value === 'object' && !Array.isArray(value)) {
55
+ const localized = value as Record<string, unknown>;
56
+
57
+ if (localized[locale] != null) return String(localized[locale]);
58
+ if (localized[defaultLocale] != null) return String(localized[defaultLocale]);
59
+
60
+ const responsive = getResponsiveValue(value, currentBreakpoint);
61
+ if (responsive != null && typeof responsive !== 'object') {
62
+ return String(responsive);
63
+ }
64
+
65
+ const firstValue = Object.values(localized).find((item) => item != null);
66
+ if (firstValue != null) return String(firstValue);
67
+ }
68
+
69
+ const responsive = getResponsiveValue(value, currentBreakpoint, fallback);
70
+ return String(responsive ?? fallback);
71
+ };
72
+
73
+ const getStyleEntry = (kebabKey: string) => {
74
+ if (!section.styles) return undefined;
75
+ const styles = section.styles as Record<string, unknown>;
76
+ const camelKey = kebabKey.replace(/-([a-z])/g, (_, letter) =>
77
+ letter.toUpperCase()
78
+ );
79
+ return styles[kebabKey] ?? styles[camelKey];
80
+ };
81
+
82
+ const maxWidth = getResponsiveValue(getStyleEntry('max-width'), currentBreakpoint, 'normal');
83
+ const maxWidthClass =
84
+ maxWidth === 'narrow'
85
+ ? 'max-w-4xl'
86
+ : maxWidth === 'normal'
87
+ ? 'max-w-7xl'
88
+ : '';
89
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
90
+
91
+ const inactiveColor = resolveThemeCssVariables(
92
+ String(getResponsiveValue(getStyleEntry('inactive-color'), currentBreakpoint, '#9ca3af')),
93
+ themeSettings
94
+ );
95
+ const activeColor = resolveThemeCssVariables(
96
+ String(getResponsiveValue(getStyleEntry('active-color'), currentBreakpoint, '#111827')),
97
+ themeSettings
98
+ );
99
+
100
+ const isMobile = currentBreakpoint === 'mobile';
101
+
102
+ const items = (section.blocks || []).filter((b: Block) => b.type === 'hover-showcase-item');
103
+
104
+ if (items.length === 0) {
105
+ if (isDesigner) {
106
+ return (
107
+ <div className="w-full flex items-center justify-center p-8 border border-dashed border-gray-300 rounded-lg">
108
+ <p className="text-gray-500">Add Hover Showcase Item blocks to see them here.</p>
109
+ </div>
110
+ );
111
+ }
112
+ return null;
113
+ }
114
+
115
+ const sendDesignerMessage = (type: string, blockId: string, label?: string) => {
116
+ if (isDesigner && window.parent) {
117
+ window.parent.postMessage(
118
+ {
119
+ type,
120
+ data: {
121
+ placeholderId,
122
+ sectionId: section.id,
123
+ blockId,
124
+ ...(label && { label })
125
+ }
126
+ },
127
+ '*'
128
+ );
129
+ }
130
+ };
131
+ const renderMobile = isDesigner ? isMobile : true;
132
+ const renderDesktop = isDesigner ? !isMobile : true;
133
+
134
+ return (
135
+ <div
136
+ className={`w-full mx-auto ${hasMaxWidth ? maxWidthClass : ''}`}
137
+ style={getCSSStyles(section.styles as Record<string, unknown>, themeSettings as Record<string, unknown>, currentBreakpoint)}
138
+ >
139
+ {renderMobile && (
140
+ <div className={`flex flex-col gap-6 px-4 ${isDesigner ? '' : 'block md:hidden'}`}>
141
+ {items.map((item: Block, index: number) => {
142
+ const props = item.properties || {};
143
+ const itemStyles = item.styles || {};
144
+ const title = resolveLocalizedValue(props.title, `Item ${index + 1}`);
145
+ const subtitle = resolveLocalizedValue(props.subtitle, '');
146
+ const link = resolveLocalizedValue(props.link, '');
147
+ const isActive = index === activeIndex;
148
+
149
+ // Inside mobile render block, force evaluation of values for 'mobile' instead of the generic currentBreakpoint
150
+ const evalBreakpoint = isDesigner ? currentBreakpoint : 'mobile';
151
+ const mobileLayout = getResponsiveValue(getStyleEntry('mobile-layout'), evalBreakpoint, 'list');
152
+ const titleFontSize = getResponsiveValue(itemStyles['title-font-size'], evalBreakpoint, '24px');
153
+ const subtitleFontSize = getResponsiveValue(itemStyles['subtitle-font-size'], evalBreakpoint, '14px');
154
+
155
+ return (
156
+ <WithDesignerFeatures
157
+ key={item.id}
158
+ block={item}
159
+ placeholderId={placeholderId}
160
+ sectionId={section.id}
161
+ isDesigner={isDesigner}
162
+ isSelected={selectedBlockId === item.id}
163
+ currentBreakpoint={currentBreakpoint}
164
+ className="flex flex-col gap-3 cursor-pointer p-2 rounded relative"
165
+ onMoveUp={() => sendDesignerMessage('MOVE_BLOCK_UP', item.id)}
166
+ onMoveDown={() => sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)}
167
+ onDuplicate={() => sendDesignerMessage('DUPLICATE_BLOCK', item.id)}
168
+ onToggleVisibility={() => sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)}
169
+ onDelete={() => sendDesignerMessage('DELETE_BLOCK', item.id)}
170
+ onRename={(newLabel) => sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)}
171
+ >
172
+ <div className="flex flex-col" onClick={() => setActiveIndex(index)}>
173
+ {link ? (
174
+ <a href={link} className="decoration-none">
175
+ <h3 style={{ fontSize: String(titleFontSize), color: activeColor, transition: 'color 0.3s' }} className="font-semibold leading-tight m-0">{title}</h3>
176
+ </a>
177
+ ) : (
178
+ <h3 style={{ fontSize: String(titleFontSize), color: activeColor, transition: 'color 0.3s' }} className="font-semibold leading-tight m-0">{title}</h3>
179
+ )}
180
+ {subtitle && (
181
+ <p style={{ fontSize: String(subtitleFontSize), color: inactiveColor }} className="mt-1 mb-0 opacity-80">{subtitle}</p>
182
+ )}
183
+ </div>
184
+
185
+ {mobileLayout === 'accordion' ? (
186
+ <div className={`overflow-hidden transition-all duration-300 ${isActive ? 'max-h-[600px] opacity-100 mt-2' : 'max-h-0 opacity-0'}`}>
187
+ {item.blocks?.map((block: Block) => (
188
+ <ThemeBlock
189
+ key={block.id}
190
+ block={block}
191
+ currentBreakpoint={currentBreakpoint}
192
+ isDesigner={isDesigner}
193
+ placeholderId={placeholderId}
194
+ sectionId={section.id}
195
+ selectedBlockId={selectedBlockId}
196
+ />
197
+ ))}
198
+ </div>
199
+ ) : (
200
+ <div className="mt-2">
201
+ {item.blocks?.map((block: Block) => (
202
+ <ThemeBlock
203
+ key={block.id}
204
+ block={block}
205
+ currentBreakpoint={currentBreakpoint}
206
+ isDesigner={isDesigner}
207
+ placeholderId={placeholderId}
208
+ sectionId={section.id}
209
+ selectedBlockId={selectedBlockId}
210
+ />
211
+ ))}
212
+ </div>
213
+ )}
214
+ </WithDesignerFeatures>
215
+ );
216
+ })}
217
+ </div>
218
+ )}
219
+
220
+ {renderDesktop && (
221
+ <div className={`px-4 md:px-8 lg:px-12 items-center min-h-[500px] ${isDesigner ? 'flex' : 'hidden md:flex'}`}>
222
+ <div className="w-1/2 flex flex-col justify-center gap-1 relative z-10 pr-8">
223
+ {items.map((item: Block, index: number) => {
224
+ const props = item.properties || {};
225
+ const itemStyles = item.styles || {};
226
+ const title = resolveLocalizedValue(props.title, `Item ${index + 1}`);
227
+ const subtitle = resolveLocalizedValue(props.subtitle, '');
228
+ const link = resolveLocalizedValue(props.link, '');
229
+ const isActive = index === activeIndex;
230
+
231
+ const evalBreakpoint = isDesigner ? currentBreakpoint : 'desktop';
232
+ const titleFontSize = getResponsiveValue(itemStyles['title-font-size'], evalBreakpoint, '32px');
233
+ const subtitleFontSize = getResponsiveValue(itemStyles['subtitle-font-size'], evalBreakpoint, '16px');
234
+ const pt = getResponsiveValue(itemStyles['padding-top'], evalBreakpoint, 16);
235
+ const pb = getResponsiveValue(itemStyles['padding-bottom'], evalBreakpoint, 16);
236
+
237
+ return (
238
+ <WithDesignerFeatures
239
+ key={item.id}
240
+ block={item}
241
+ placeholderId={placeholderId}
242
+ sectionId={section.id}
243
+ isDesigner={isDesigner}
244
+ isSelected={selectedBlockId === item.id}
245
+ currentBreakpoint={currentBreakpoint}
246
+ className="flex flex-col cursor-pointer group w-max rounded px-2 relative"
247
+ style={{ paddingTop: `${pt}px`, paddingBottom: `${pb}px` }}
248
+ onMoveUp={() => sendDesignerMessage('MOVE_BLOCK_UP', item.id)}
249
+ onMoveDown={() => sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)}
250
+ onDuplicate={() => sendDesignerMessage('DUPLICATE_BLOCK', item.id)}
251
+ onToggleVisibility={() => sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)}
252
+ onDelete={() => sendDesignerMessage('DELETE_BLOCK', item.id)}
253
+ onRename={(newLabel) => sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)}
254
+ >
255
+ <div
256
+ className="flex flex-col relative w-full"
257
+ onMouseEnter={() => setActiveIndex(index)}
258
+ >
259
+ {link ? (
260
+ <a href={link} className="decoration-none">
261
+ <h3
262
+ style={{ fontSize: String(titleFontSize), color: isActive ? activeColor : inactiveColor, transition: 'color 0.4s ease' }}
263
+ className="font-semibold leading-tight m-0 hover:opacity-80"
264
+ >
265
+ {title}
266
+ </h3>
267
+ </a>
268
+ ) : (
269
+ <h3
270
+ style={{ fontSize: String(titleFontSize), color: isActive ? activeColor : inactiveColor, transition: 'color 0.4s ease' }}
271
+ className="font-semibold leading-tight m-0"
272
+ >
273
+ {title}
274
+ </h3>
275
+ )}
276
+ {subtitle && (
277
+ <p
278
+ style={{
279
+ fontSize: String(subtitleFontSize),
280
+ color: inactiveColor,
281
+ opacity: isActive ? 1 : 0,
282
+ maxHeight: isActive ? '40px' : '0px',
283
+ transform: isActive ? 'translateY(0)' : 'translateY(-10px)'
284
+ }}
285
+ className="mt-2 mb-0 overflow-hidden transition-all duration-400 ease-out"
286
+ >
287
+ {subtitle}
288
+ </p>
289
+ )}
290
+ </div>
291
+ </WithDesignerFeatures>
292
+ );
293
+ })}
294
+ </div>
295
+
296
+ <div className="w-1/2 relative h-full min-h-[400px] flex items-center justify-center">
297
+ {items.map((item: Block, index: number) => {
298
+ const isActive = index === activeIndex;
299
+
300
+ return (
301
+ <div
302
+ key={item.id}
303
+ className={`absolute inset-0 transition-opacity duration-500 ease-in-out flex items-center justify-center ${isActive ? 'opacity-100 z-10' : 'opacity-0 z-0'}`}
304
+ >
305
+ {item.blocks?.map((block: Block) => (
306
+ <ThemeBlock
307
+ key={block.id}
308
+ block={block}
309
+ currentBreakpoint={currentBreakpoint}
310
+ isDesigner={isDesigner}
311
+ placeholderId={placeholderId}
312
+ sectionId={section.id}
313
+ selectedBlockId={selectedBlockId}
314
+ />
315
+ ))}
316
+ </div>
317
+ );
318
+ })}
319
+ </div>
320
+ </div>
321
+ )}
322
+ </div>
323
+ );
324
+ };
325
+
326
+ export default HoverShowcaseSection;
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { getResponsiveValue } from '../utils';
5
+ import { Section } from '../theme-section';
6
+ import { useThemeSettingsContext } from '../theme-settings-context';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import clsx from 'clsx';
9
+ import ThemeBlock from '../theme-block';
10
+
11
+ interface ImageHotspotSectionProps {
12
+ section: Section;
13
+ currentBreakpoint?: string;
14
+ isDesigner?: boolean;
15
+ placeholderId?: string;
16
+ selectedBlockId?: string | null;
17
+ }
18
+
19
+ export default function ImageHotspotSection({
20
+ section,
21
+ currentBreakpoint = 'desktop',
22
+ isDesigner = false,
23
+ placeholderId = '',
24
+ selectedBlockId = null
25
+ }: ImageHotspotSectionProps) {
26
+ const themeSettings = useThemeSettingsContext();
27
+
28
+ const styles = section.styles || {};
29
+ const properties = section.properties || {};
30
+
31
+ const imageBlock = section.blocks?.find(b => b.type === 'image');
32
+
33
+ const hotspotBlocks = section.blocks?.filter(b => b.type === 'hotspot') || [];
34
+
35
+ const height = getResponsiveValue(styles.height, currentBreakpoint, '600px') as string | number;
36
+ const width = getResponsiveValue(styles.width, currentBreakpoint, '100%') as string;
37
+ const maxWidth = getResponsiveValue(styles['max-width'], currentBreakpoint, 'normal') as string;
38
+
39
+ const maxWidthClass =
40
+ maxWidth === 'narrow'
41
+ ? 'max-w-4xl'
42
+ : maxWidth === 'normal'
43
+ ? 'max-w-7xl'
44
+ : maxWidth === 'full'
45
+ ? 'w-full'
46
+ : '';
47
+
48
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
49
+
50
+ return (
51
+ <div
52
+ className={twMerge(
53
+ clsx(
54
+ 'relative mx-auto overflow-hidden group/hotspot-section',
55
+ hasMaxWidth && 'mx-auto',
56
+ maxWidthClass
57
+ )
58
+ )}
59
+ style={{
60
+ height: typeof height === 'number' ? `${height}px` : height,
61
+ width: width === 'fill' ? '100%' : width,
62
+ maxWidth: maxWidth === 'none' ? 'none' : undefined,
63
+ position: 'relative'
64
+ }}
65
+ >
66
+
67
+ <div className="absolute inset-0 w-full h-full z-0">
68
+ {imageBlock ? (
69
+ <div className="w-full h-full">
70
+ <ThemeBlock
71
+ key={imageBlock.id}
72
+ block={imageBlock}
73
+ placeholderId={placeholderId}
74
+ sectionId={section.id}
75
+ isDesigner={isDesigner}
76
+ isSelected={selectedBlockId === imageBlock.id}
77
+ selectedBlockId={selectedBlockId}
78
+ currentBreakpoint={currentBreakpoint}
79
+
80
+ onMoveUp={() => {
81
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_UP', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id } }, '*');
82
+ }}
83
+ onMoveDown={() => {
84
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_DOWN', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id } }, '*');
85
+ }}
86
+ onDuplicate={() => {
87
+ if (window.parent) window.parent.postMessage({ type: 'DUPLICATE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id } }, '*');
88
+ }}
89
+ onToggleVisibility={() => {
90
+ if (window.parent) window.parent.postMessage({ type: 'TOGGLE_BLOCK_VISIBILITY', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id } }, '*');
91
+ }}
92
+ onDelete={() => {
93
+ if (window.parent) window.parent.postMessage({ type: 'DELETE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id } }, '*');
94
+ }}
95
+ onRename={(newLabel) => {
96
+ if (window.parent) window.parent.postMessage({ type: 'RENAME_BLOCK', data: { placeholderId, sectionId: section.id, blockId: imageBlock.id, label: newLabel } }, '*');
97
+ }}
98
+ />
99
+ </div>
100
+ ) : (
101
+ <div className="w-full h-full bg-gray-200 flex items-center justify-center text-gray-400 select-none">
102
+ No Image Selected
103
+ </div>
104
+ )}
105
+ </div>
106
+
107
+ <div className="absolute inset-0 w-full h-full z-10 pointer-events-none">
108
+ {hotspotBlocks.map(block => (
109
+ <div key={block.id} className="absolute pointer-events-none" style={{ top: 0, left: 0, width: '100%', height: '100%' }}>
110
+ <ThemeBlock
111
+ block={block}
112
+ placeholderId={placeholderId}
113
+ sectionId={section.id}
114
+ isDesigner={isDesigner}
115
+ isSelected={selectedBlockId === block.id}
116
+ selectedBlockId={selectedBlockId}
117
+ currentBreakpoint={currentBreakpoint}
118
+ onMoveUp={() => {
119
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_UP', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
120
+ }}
121
+ onMoveDown={() => {
122
+ if (window.parent) window.parent.postMessage({ type: 'MOVE_BLOCK_DOWN', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
123
+ }}
124
+ onDuplicate={() => {
125
+ if (window.parent) window.parent.postMessage({ type: 'DUPLICATE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
126
+ }}
127
+ onToggleVisibility={() => {
128
+ if (window.parent) window.parent.postMessage({ type: 'TOGGLE_BLOCK_VISIBILITY', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
129
+ }}
130
+ onDelete={() => {
131
+ if (window.parent) window.parent.postMessage({ type: 'DELETE_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id } }, '*');
132
+ }}
133
+ onRename={(newLabel) => {
134
+ if (window.parent) window.parent.postMessage({ type: 'RENAME_BLOCK', data: { placeholderId, sectionId: section.id, blockId: block.id, label: newLabel } }, '*');
135
+ }}
136
+ />
137
+ </div>
138
+ ))}
139
+ </div>
140
+ </div>
141
+ );
142
+ }