@akinon/projectzero 2.0.0-beta.20 → 2.0.0-beta.21

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 (138) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app-template/CHANGELOG.md +138 -0
  3. package/app-template/next.config.mjs +0 -1
  4. package/app-template/package.json +31 -30
  5. package/app-template/src/app/[pz]/[...prettyurl]/page.tsx +2 -2
  6. package/app-template/src/app/[pz]/account/layout.tsx +2 -1
  7. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/blog/[slug]/page.tsx +4 -2
  8. package/app-template/src/app/[pz]/category/[pk]/page.tsx +11 -1
  9. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +2 -2
  10. package/app-template/src/app/[pz]/layout.tsx +3 -1
  11. package/app-template/src/app/[pz]/list/page.tsx +11 -1
  12. package/app-template/src/app/[pz]/page.tsx +13 -35
  13. package/app-template/src/app/[pz]/pages/[slug]/page.tsx +19 -0
  14. package/app-template/src/app/[pz]/product/[pk]/page.tsx +2 -2
  15. package/app-template/src/app/api/barcode-search/route.ts +1 -1
  16. package/app-template/src/app/api/cache/route.ts +1 -1
  17. package/app-template/src/app/api/image-proxy/route.ts +1 -1
  18. package/app-template/src/app/api/logout/route.ts +1 -1
  19. package/app-template/src/app/api/product-categories/route.ts +1 -1
  20. package/app-template/src/app/api/similar-product-list/route.ts +1 -1
  21. package/app-template/src/app/api/similar-products/route.ts +1 -1
  22. package/app-template/src/app/api/virtual-try-on/route.ts +1 -1
  23. package/app-template/src/app/api/web-vitals/route.ts +1 -1
  24. package/app-template/src/components/quantity-selector.tsx +16 -4
  25. package/app-template/src/components/widget-content.tsx +3 -3
  26. package/app-template/src/routes/index.ts +6 -6
  27. package/app-template/src/utils/__tests__/theme-page-context.test.ts +145 -0
  28. package/app-template/src/utils/theme-page-context.ts +309 -0
  29. package/app-template/src/views/basket/basket-item.tsx +107 -691
  30. package/app-template/src/views/basket/index.ts +0 -2
  31. package/app-template/src/views/basket/summary.tsx +75 -496
  32. package/app-template/src/views/breadcrumb.tsx +38 -13
  33. package/app-template/src/views/category/category-header.tsx +66 -289
  34. package/app-template/src/views/category/category-info.tsx +24 -173
  35. package/app-template/src/views/category/filters/index.tsx +48 -208
  36. package/app-template/src/views/category/layout.tsx +5 -7
  37. package/app-template/src/views/checkout/index.tsx +0 -5
  38. package/app-template/src/views/checkout/steps/payment/index.tsx +2 -5
  39. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -72
  40. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +40 -171
  41. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +12 -74
  42. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +45 -128
  43. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +27 -232
  44. package/app-template/src/views/checkout/summary.tsx +29 -303
  45. package/app-template/src/views/footer.tsx +13 -415
  46. package/app-template/src/views/guest-login/index.tsx +1 -1
  47. package/app-template/src/views/header/action-menu.tsx +45 -277
  48. package/app-template/src/views/header/band.tsx +21 -6
  49. package/app-template/src/views/header/index.tsx +47 -109
  50. package/app-template/src/views/header/mini-basket.tsx +45 -820
  51. package/app-template/src/views/header/navbar.tsx +111 -178
  52. package/app-template/src/views/header/search/index.tsx +32 -71
  53. package/app-template/src/views/header/search/results.tsx +65 -127
  54. package/app-template/src/views/product/accordion-wrapper.tsx +43 -135
  55. package/app-template/src/views/product/index.ts +1 -1
  56. package/app-template/src/views/product/layout.tsx +7 -2
  57. package/app-template/src/views/product/misc-buttons.tsx +25 -339
  58. package/app-template/src/views/product/product-actions.tsx +8 -137
  59. package/app-template/src/views/product/product-info.tsx +31 -69
  60. package/app-template/src/views/product/product-share.tsx +8 -11
  61. package/app-template/src/views/product/slider.tsx +79 -117
  62. package/app-template/src/views/product-item/index.tsx +46 -119
  63. package/app-template/src/widgets/footer-social.tsx +16 -47
  64. package/app-template/src/widgets/footer-subscription/index.tsx +17 -183
  65. package/dist/commands/plugins.js +23 -2
  66. package/package.json +1 -1
  67. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
  68. package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
  69. package/app-template/src/views/basket/designer-context.tsx +0 -617
  70. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
  71. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
  72. package/app-template/src/views/breadcrumb/constants.ts +0 -15
  73. package/app-template/src/views/breadcrumb/index.tsx +0 -127
  74. package/app-template/src/views/category/native-widget-context.tsx +0 -257
  75. package/app-template/src/views/category/product-list-registrar.tsx +0 -665
  76. package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
  77. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
  78. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
  79. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
  80. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
  81. package/app-template/src/views/checkout/constants.ts +0 -5
  82. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
  83. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
  84. package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
  85. package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
  86. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
  87. package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
  88. package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
  89. package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
  90. package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
  91. package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
  92. package/app-template/src/views/footer/footer-social-context.tsx +0 -254
  93. package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
  94. package/app-template/src/views/footer/footer-utils.ts +0 -43
  95. package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
  96. package/app-template/src/views/footer/logo-settings.ts +0 -183
  97. package/app-template/src/views/footer/native-widget-config.ts +0 -262
  98. package/app-template/src/views/footer/subscription-settings.ts +0 -122
  99. package/app-template/src/views/footer/use-footer-logo.ts +0 -162
  100. package/app-template/src/views/header/designer-context.tsx +0 -261
  101. package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
  102. package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
  103. package/app-template/src/views/header/header-content.tsx +0 -1026
  104. package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
  105. package/app-template/src/views/header/header-icons-context.tsx +0 -262
  106. package/app-template/src/views/header/header-language-registrar.tsx +0 -348
  107. package/app-template/src/views/header/header-layout-context.tsx +0 -143
  108. package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
  109. package/app-template/src/views/header/header-logo-context.tsx +0 -228
  110. package/app-template/src/views/header/header-logo.tsx +0 -118
  111. package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
  112. package/app-template/src/views/header/header-search-registrar.tsx +0 -511
  113. package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
  114. package/app-template/src/views/header/inline-search.tsx +0 -262
  115. package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
  116. package/app-template/src/views/header/search/search-input.tsx +0 -61
  117. package/app-template/src/views/header/server-settings-parser.ts +0 -1105
  118. package/app-template/src/views/header/use-header-icons.ts +0 -241
  119. package/app-template/src/views/header/use-header-logo.ts +0 -213
  120. package/app-template/src/views/header/use-navbar-menu.ts +0 -179
  121. package/app-template/src/views/product/accordion-section.tsx +0 -61
  122. package/app-template/src/views/product/custom-button-group.tsx +0 -69
  123. package/app-template/src/views/product/favorites-button-section.tsx +0 -69
  124. package/app-template/src/views/product/find-in-store-section.tsx +0 -60
  125. package/app-template/src/views/product/product-info-section.tsx +0 -140
  126. package/app-template/src/views/product/quantity-section.tsx +0 -73
  127. package/app-template/src/views/product/sale-tag.tsx +0 -10
  128. package/app-template/src/views/product/share-section.tsx +0 -357
  129. package/app-template/src/views/product/variants-section.tsx +0 -126
  130. package/app-template/src/views/product-detail/constants.ts +0 -272
  131. package/app-template/src/views/product-detail/index.ts +0 -10
  132. package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
  133. package/app-template/src/widgets/footer-app-banner.tsx +0 -444
  134. package/app-template/src/widgets/footer-bottom.tsx +0 -127
  135. package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
  136. package/app-template/src/widgets/footer-menu-two.tsx +0 -298
  137. package/app-template/src/widgets/footer-social-client.tsx +0 -251
  138. package/app-template/src/widgets/footer-value-props.tsx +0 -201
@@ -1,342 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Footer Layout Section Registrar
5
- *
6
- * This component registers the "Layout" section for the footer placeholder.
7
- * It's a minimal client component that only handles native widget registration
8
- * and applies selection highlight to the footer when Layout section is selected.
9
- *
10
- * When the Layout section is selected in Theme Editor, the entire footer
11
- * gets highlighted.
12
- *
13
- * It also provides the current layout type to child components via context.
14
- */
15
-
16
- import {
17
- createContext,
18
- useContext,
19
- useEffect,
20
- useRef,
21
- useState,
22
- useCallback,
23
- ReactNode
24
- } from 'react';
25
-
26
- // Import constants for use within this component
27
- import {
28
- FOOTER_LAYOUT_PLACEHOLDER_ID,
29
- FOOTER_LAYOUT_SECTION_ID,
30
- FOOTER_LAYOUT_BLOCKS,
31
- type FooterLayoutType
32
- } from './footer-layout-constants';
33
-
34
- // Re-export constants from shared file for backward compatibility
35
- export {
36
- FOOTER_LAYOUT_PLACEHOLDER_ID,
37
- FOOTER_LAYOUT_SECTION_ID,
38
- FOOTER_LAYOUT_WIDGET_SLUG,
39
- FOOTER_LAYOUT_BLOCKS,
40
- FOOTER_LAYOUT_BLOCK_IDS,
41
- type FooterLayoutType
42
- } from './footer-layout-constants';
43
-
44
- // Global flag to track if registration has been done (survives component remount)
45
- declare global {
46
- interface Window {
47
- __footerLayoutRegistered?: boolean;
48
- }
49
- }
50
-
51
- export interface FooterLayoutProperties {
52
- layout?: FooterLayoutType | Record<string, string>;
53
- }
54
-
55
- // Block styles type
56
- type BlockStyles = Record<string, Record<string, unknown>>;
57
-
58
- // Context for sharing layout type and block styles with other components
59
- interface FooterLayoutContextValue {
60
- layout: FooterLayoutType;
61
- isDesigner: boolean;
62
- selectedBlockId: string | null;
63
- getBlockStyles: (blockId: string) => Record<string, unknown>;
64
- }
65
-
66
- const FooterLayoutContext = createContext<FooterLayoutContextValue>({
67
- layout: 'default',
68
- isDesigner: false,
69
- selectedBlockId: null,
70
- getBlockStyles: () => ({})
71
- });
72
-
73
- export const useFooterLayout = () => useContext(FooterLayoutContext);
74
-
75
- /**
76
- * FooterLayoutRegistrar
77
- *
78
- * Registers the Layout native section with Theme Editor so it appears in the sidebar,
79
- * handles footer highlight when Layout section is selected, and provides
80
- * layout type to children via context.
81
- */
82
- interface FooterLayoutRegistrarProps {
83
- children?: ReactNode;
84
- /** Initial layout from server to avoid layout flash */
85
- initialLayout?: FooterLayoutType;
86
- /** Initial block styles from server */
87
- initialBlockStyles?: BlockStyles;
88
- }
89
-
90
- export default function FooterLayoutRegistrar({
91
- children,
92
- initialLayout = 'default',
93
- initialBlockStyles = {}
94
- }: FooterLayoutRegistrarProps) {
95
- // Initialize with server-provided value to avoid flash
96
- const [sectionProperties, setSectionProperties] =
97
- useState<FooterLayoutProperties>({ layout: initialLayout });
98
- const [blockStyles, setBlockStyles] =
99
- useState<BlockStyles>(initialBlockStyles);
100
- const [isLayoutSelected, setIsLayoutSelected] = useState(false);
101
- const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
102
- const [isDesigner, setIsDesigner] = useState(false);
103
- const hasReceivedThemeProps = useRef(false);
104
-
105
- // Register native widget with Theme Editor - ONLY ONCE per page session
106
- // Properties are managed by Theme Editor, not sent with registration
107
- // Uses window flag to survive component remount
108
- useEffect(() => {
109
- // Check if in iframe (designer mode)
110
- const isInIframe =
111
- typeof window !== 'undefined' && window.self !== window.top;
112
- setIsDesigner(isInIframe);
113
-
114
- // Skip if already registered in this page session
115
- if (typeof window !== 'undefined' && window.__footerLayoutRegistered) {
116
- return;
117
- }
118
-
119
- if (!isInIframe || !window.parent) {
120
- return;
121
- }
122
-
123
- // Send native widget registration to Theme Editor
124
- // Includes row blocks for styling (MAIN_ROW, BOTTOM_ROW)
125
- // Properties are NOT included - Theme Editor manages them
126
- const nativeWidgetConfig = {
127
- placeholderId: FOOTER_LAYOUT_PLACEHOLDER_ID,
128
- section: {
129
- id: FOOTER_LAYOUT_SECTION_ID,
130
- type: 'native',
131
- label: 'Layout',
132
- blocks: [
133
- {
134
- id: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.id,
135
- type: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.type,
136
- label: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.label
137
- },
138
- {
139
- id: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.id,
140
- type: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.type,
141
- label: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.label
142
- }
143
- ]
144
- // Don't send properties - Theme Editor already has them
145
- }
146
- };
147
-
148
- window.parent.postMessage(
149
- {
150
- type: 'REGISTER_NATIVE_WIDGETS',
151
- data: { widgets: [nativeWidgetConfig] }
152
- },
153
- '*'
154
- );
155
-
156
- // Mark as registered in window to survive component remount
157
- window.__footerLayoutRegistered = true;
158
- }, []);
159
-
160
- // Apply highlight style to footer when Layout section is selected
161
- useEffect(() => {
162
- if (typeof window === 'undefined') return;
163
-
164
- const footerElement = document.querySelector('footer');
165
- if (!footerElement) return;
166
-
167
- if (isLayoutSelected) {
168
- // Apply selection highlight
169
- footerElement.style.outline = '2px solid #3b82f6';
170
- footerElement.style.outlineOffset = '-2px';
171
- } else {
172
- // Remove selection highlight
173
- footerElement.style.outline = '';
174
- footerElement.style.outlineOffset = '';
175
- }
176
-
177
- return () => {
178
- // Cleanup on unmount
179
- footerElement.style.outline = '';
180
- footerElement.style.outlineOffset = '';
181
- };
182
- }, [isLayoutSelected]);
183
-
184
- // Listen for theme updates and selection changes from Theme Editor
185
- useEffect(() => {
186
- if (typeof window === 'undefined') return;
187
-
188
- const handleMessage = (event: MessageEvent) => {
189
- const { type, data } = event.data || {};
190
-
191
- // Handle theme updates
192
- if (
193
- (type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
194
- data?.theme?.placeholders
195
- ) {
196
- const placeholder = data.theme.placeholders?.find(
197
- (p: { slug: string }) => p.slug === FOOTER_LAYOUT_PLACEHOLDER_ID
198
- );
199
-
200
- const layoutSection = placeholder?.sections?.find(
201
- (s: { id: string }) => s.id === FOOTER_LAYOUT_SECTION_ID
202
- );
203
-
204
- if (layoutSection) {
205
- hasReceivedThemeProps.current = true;
206
- if (layoutSection.properties) {
207
- setSectionProperties(layoutSection.properties);
208
- }
209
-
210
- // Extract block styles from section blocks
211
- if (layoutSection.blocks) {
212
- const newBlockStyles: BlockStyles = {};
213
- layoutSection.blocks.forEach(
214
- (block: { id: string; styles?: Record<string, unknown> }) => {
215
- if (block.styles) {
216
- newBlockStyles[block.id] = block.styles;
217
- }
218
- }
219
- );
220
- setBlockStyles(newBlockStyles);
221
- }
222
- }
223
- }
224
-
225
- // Handle property updates (when user changes dropdown in Theme Editor)
226
- if (type === 'UPDATE_SECTION_PROPERTY' || type === 'UPDATE_PROPERTY') {
227
- const { sectionId, placeholderId, key, value, properties } = data || {};
228
-
229
- // Check if this is for our section
230
- if (
231
- sectionId === FOOTER_LAYOUT_SECTION_ID ||
232
- placeholderId === FOOTER_LAYOUT_PLACEHOLDER_ID
233
- ) {
234
- // If we get individual key/value
235
- if (key && value !== undefined) {
236
- setSectionProperties((prev) => ({
237
- ...prev,
238
- [key]: value
239
- }));
240
- }
241
- // If we get full properties object
242
- if (properties) {
243
- setSectionProperties(properties);
244
- }
245
- }
246
- }
247
-
248
- // Handle selection changes
249
- if (type === 'SELECT_SECTION') {
250
- const { placeholderId, sectionId } = data || {};
251
-
252
- // Check if Layout section is selected
253
- const isSelected =
254
- placeholderId === FOOTER_LAYOUT_PLACEHOLDER_ID &&
255
- sectionId === FOOTER_LAYOUT_SECTION_ID;
256
-
257
- setIsLayoutSelected(isSelected);
258
- if (isSelected) {
259
- setSelectedBlockId(null);
260
- }
261
- }
262
-
263
- // Handle block selection
264
- if (type === 'SELECT_BLOCK') {
265
- const { sectionId, blockId } = data || {};
266
-
267
- // If block in our section is selected
268
- if (sectionId === FOOTER_LAYOUT_SECTION_ID) {
269
- setSelectedBlockId(blockId);
270
- setIsLayoutSelected(false);
271
- } else {
272
- // If a block is selected in another section, deselect our blocks
273
- setSelectedBlockId(null);
274
- setIsLayoutSelected(false);
275
- }
276
- }
277
-
278
- // Handle deselection
279
- if (type === 'DESELECT' || type === 'CLEAR_SELECTION') {
280
- setIsLayoutSelected(false);
281
- setSelectedBlockId(null);
282
- }
283
- };
284
-
285
- window.addEventListener('message', handleMessage);
286
- return () => window.removeEventListener('message', handleMessage);
287
- }, []);
288
-
289
- // Helper to extract layout value from potentially responsive property
290
- const extractLayoutValue = (layoutProp: unknown): FooterLayoutType => {
291
- if (!layoutProp) return 'default';
292
-
293
- // If it's a direct string value
294
- if (typeof layoutProp === 'string') {
295
- return layoutProp as FooterLayoutType;
296
- }
297
-
298
- // If it's a responsive object (e.g., { desktop: 'compact' })
299
- if (typeof layoutProp === 'object' && layoutProp !== null) {
300
- const obj = layoutProp as Record<string, string>;
301
- // Try desktop first, then mobile, then any first value
302
- return (obj.desktop ||
303
- obj.mobile ||
304
- Object.values(obj)[0] ||
305
- 'default') as FooterLayoutType;
306
- }
307
-
308
- return 'default';
309
- };
310
-
311
- // Get block styles helper
312
- const getBlockStyles = useCallback(
313
- (blockId: string): Record<string, unknown> => {
314
- return blockStyles[blockId] || {};
315
- },
316
- [blockStyles]
317
- );
318
-
319
- // Get the current layout type
320
- const currentLayout: FooterLayoutType = extractLayoutValue(
321
- sectionProperties.layout
322
- );
323
-
324
- // If no children, just return null (registration-only mode)
325
- if (!children) {
326
- return null;
327
- }
328
-
329
- // Provide layout context to children
330
- return (
331
- <FooterLayoutContext.Provider
332
- value={{
333
- layout: currentLayout,
334
- isDesigner,
335
- selectedBlockId,
336
- getBlockStyles
337
- }}
338
- >
339
- {children}
340
- </FooterLayoutContext.Provider>
341
- );
342
- }
@@ -1,110 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Footer Layout Switcher
5
- *
6
- * A client component that switches between pre-rendered footer layouts
7
- * based on the current layout setting from FooterLayoutRegistrar.
8
- *
9
- * This component receives the layouts as React nodes (pre-rendered by the server)
10
- * and simply shows/hides them based on the layout context.
11
- */
12
-
13
- import { ReactNode, useMemo, useCallback } from 'react';
14
- import clsx from 'clsx';
15
- import {
16
- useFooterLayout,
17
- FOOTER_LAYOUT_BLOCKS,
18
- FOOTER_LAYOUT_PLACEHOLDER_ID,
19
- FOOTER_LAYOUT_SECTION_ID
20
- } from './footer-layout-registrar';
21
- import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
22
- import { convertBlockStyles } from './footer-utils';
23
-
24
- /**
25
- * Selectable Row Container
26
- * Wraps row content with theme editor selection support
27
- */
28
- interface SelectableRowProps {
29
- blockId: string;
30
- blockLabel: string;
31
- children: ReactNode;
32
- className?: string;
33
- style?: React.CSSProperties;
34
- }
35
-
36
- function SelectableRow({
37
- blockId,
38
- blockLabel,
39
- children,
40
- className,
41
- style
42
- }: SelectableRowProps) {
43
- const { isDesigner, selectedBlockId, getBlockStyles } = useFooterLayout();
44
-
45
- const { handleClick } = useDesignerFeatures({
46
- blockId,
47
- placeholderId: FOOTER_LAYOUT_PLACEHOLDER_ID,
48
- sectionId: FOOTER_LAYOUT_SECTION_ID,
49
- isDesigner,
50
- blockInfo: {
51
- id: blockId,
52
- type: 'container',
53
- label: blockLabel
54
- }
55
- });
56
-
57
- const isSelected = selectedBlockId === blockId;
58
- const blockStyles = getBlockStyles(blockId);
59
- const computedStyles = useMemo(
60
- () => convertBlockStyles(blockStyles),
61
- [blockStyles]
62
- );
63
-
64
- const handleContainerClick = useCallback(
65
- (e: React.MouseEvent) => {
66
- if (isDesigner) {
67
- e.preventDefault();
68
- e.stopPropagation();
69
- handleClick(e);
70
- }
71
- },
72
- [isDesigner, handleClick]
73
- );
74
-
75
- return (
76
- <div
77
- data-block-id={blockId}
78
- onClick={handleContainerClick}
79
- className={clsx(
80
- className,
81
- isDesigner && 'cursor-pointer',
82
- isSelected && 'ring-2 ring-blue-500 ring-inset'
83
- )}
84
- style={{ ...style, ...computedStyles }}
85
- >
86
- {children}
87
- </div>
88
- );
89
- }
90
-
91
- interface FooterLayoutSwitcherProps {
92
- defaultLayout: ReactNode;
93
- compactLayout: ReactNode;
94
- }
95
-
96
- export default function FooterLayoutSwitcher({
97
- defaultLayout,
98
- compactLayout
99
- }: FooterLayoutSwitcherProps) {
100
- const { layout } = useFooterLayout();
101
-
102
- return (
103
- <SelectableRow
104
- blockId={FOOTER_LAYOUT_BLOCKS.MAIN_ROW.id}
105
- blockLabel={FOOTER_LAYOUT_BLOCKS.MAIN_ROW.label}
106
- >
107
- {layout === 'compact' ? compactLayout : defaultLayout}
108
- </SelectableRow>
109
- );
110
- }
@@ -1,211 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- createContext,
5
- useCallback,
6
- useContext,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- type PropsWithChildren
12
- } from 'react';
13
- import { useExternalDesigner } from '@akinon/next/components/theme-editor/hooks/use-external-designer';
14
- import { useNativeWidgetData } from '@akinon/next/components/theme-editor/hooks/use-native-widget-data';
15
-
16
- import {
17
- FOOTER_PLACEHOLDER_ID,
18
- FOOTER_MENU_SECTION_ID,
19
- FOOTER_MENU_WIDGET_SLUG,
20
- FOOTER_MENU_HEADING_BLOCK_ID,
21
- FOOTER_MENU_LINK_BLOCK_ID,
22
- type FooterNativeWidgetBlock
23
- } from './native-widget-config';
24
-
25
- export { FOOTER_MENU_HEADING_BLOCK_ID, FOOTER_MENU_LINK_BLOCK_ID };
26
-
27
- type FooterMenuBlockState = FooterNativeWidgetBlock;
28
-
29
- interface FooterMenuContextValue {
30
- isDesigner: boolean;
31
- selectedBlockId: string | null;
32
- getBlock: (blockId: string) => FooterMenuBlockState | undefined;
33
- blockVersion: number;
34
- }
35
-
36
- const FooterMenuContext = createContext<FooterMenuContextValue>({
37
- isDesigner: false,
38
- selectedBlockId: null,
39
- getBlock: () => undefined,
40
- blockVersion: 0
41
- });
42
-
43
- const BLOCK_META = [
44
- { id: FOOTER_MENU_HEADING_BLOCK_ID, type: 'text', label: 'Menu Headings' },
45
- { id: FOOTER_MENU_LINK_BLOCK_ID, type: 'text', label: 'Menu Links' }
46
- ];
47
-
48
- const toBlockState = (
49
- block: Partial<FooterNativeWidgetBlock>
50
- ): FooterMenuBlockState => {
51
- const fallback = BLOCK_META.find((meta) => meta.id === block.id);
52
- return {
53
- id: block.id ?? fallback?.id ?? '',
54
- type: block.type ?? fallback?.type,
55
- label: block.label ?? fallback?.label,
56
- styles: block.styles,
57
- properties: block.properties,
58
- value: block.value
59
- } as FooterMenuBlockState;
60
- };
61
-
62
- const mapFromSnapshot = (
63
- blocks?: FooterNativeWidgetBlock[]
64
- ): Map<string, FooterMenuBlockState> => {
65
- const map = new Map<string, FooterMenuBlockState>();
66
- blocks?.forEach((block) => {
67
- map.set(block.id, toBlockState(block));
68
- });
69
-
70
- BLOCK_META.forEach((meta) => {
71
- if (!map.has(meta.id)) {
72
- map.set(meta.id, toBlockState(meta));
73
- }
74
- });
75
-
76
- return map;
77
- };
78
-
79
- interface FooterMenuProviderProps {
80
- initialBlocks?: FooterNativeWidgetBlock[];
81
- }
82
-
83
- export function FooterMenuProvider({
84
- initialBlocks,
85
- children
86
- }: PropsWithChildren<FooterMenuProviderProps>) {
87
- const designerState = useExternalDesigner({
88
- placeholderId: FOOTER_PLACEHOLDER_ID
89
- });
90
-
91
- const [blockMap, setBlockMap] = useState(() =>
92
- mapFromSnapshot(initialBlocks)
93
- );
94
- const blockMapRef = useRef(blockMap);
95
- const [blockVersion, setBlockVersion] = useState(0);
96
-
97
- const isDesignerRef = useRef(false);
98
- const [isDesignerChecked, setIsDesignerChecked] = useState(false);
99
-
100
- useEffect(() => {
101
- if (typeof window === 'undefined') return;
102
- isDesignerRef.current = window.self !== window.top;
103
- setIsDesignerChecked(true);
104
- }, []);
105
-
106
- const isDesigner = isDesignerRef.current;
107
-
108
- const widgetData = useNativeWidgetData({
109
- widgetSlug: FOOTER_MENU_WIDGET_SLUG,
110
- sectionId: FOOTER_MENU_SECTION_ID,
111
- skip: !isDesignerChecked || isDesigner,
112
- blockMeta: BLOCK_META
113
- });
114
-
115
- const mergeBlocks = useCallback(
116
- (blocks: Partial<FooterNativeWidgetBlock>[] | undefined) => {
117
- if (!blocks?.length) return;
118
-
119
- setBlockMap((prev) => {
120
- const next = new Map(prev);
121
- blocks.forEach((block) => {
122
- if (!block.id) return;
123
- const existing = next.get(block.id);
124
-
125
- next.set(
126
- block.id,
127
- toBlockState({
128
- ...existing,
129
- ...block,
130
- styles:
131
- block.styles && Object.keys(block.styles).length > 0
132
- ? block.styles
133
- : existing?.styles,
134
- properties:
135
- block.properties && Object.keys(block.properties).length > 0
136
- ? block.properties
137
- : existing?.properties,
138
- value: block.value !== undefined ? block.value : existing?.value
139
- })
140
- );
141
- });
142
- blockMapRef.current = next;
143
- return next;
144
- });
145
- setBlockVersion((prev) => prev + 1);
146
- },
147
- []
148
- );
149
-
150
- useEffect(() => {
151
- if (!isDesignerChecked || isDesigner || widgetData.isLoading) return;
152
-
153
- const blocksToMerge: Partial<FooterNativeWidgetBlock>[] = [];
154
- widgetData.blocks.forEach((block) => {
155
- blocksToMerge.push(block as FooterNativeWidgetBlock);
156
- });
157
-
158
- if (blocksToMerge.length > 0) {
159
- mergeBlocks(blocksToMerge);
160
- }
161
- }, [
162
- isDesigner,
163
- isDesignerChecked,
164
- widgetData.isLoading,
165
- widgetData.blocks,
166
- mergeBlocks
167
- ]);
168
-
169
- useEffect(() => {
170
- const handleMessage = (event: MessageEvent) => {
171
- const { type, data } = event.data || {};
172
- if (
173
- (type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
174
- data?.theme?.placeholders
175
- ) {
176
- const placeholder = data.theme.placeholders.find(
177
- (p: { slug: string }) => p.slug === FOOTER_PLACEHOLDER_ID
178
- );
179
- const section = placeholder?.sections?.find(
180
- (s: { id: string }) => s.id === FOOTER_MENU_SECTION_ID
181
- );
182
- mergeBlocks(section?.blocks);
183
- }
184
- };
185
-
186
- window.addEventListener('message', handleMessage);
187
- return () => window.removeEventListener('message', handleMessage);
188
- }, [mergeBlocks]);
189
-
190
- const getBlock = useCallback(
191
- (blockId: string) => blockMapRef.current.get(blockId),
192
- []
193
- );
194
-
195
- const contextValue = useMemo(
196
- () => ({
197
- ...designerState,
198
- getBlock,
199
- blockVersion
200
- }),
201
- [designerState, getBlock, blockVersion]
202
- );
203
-
204
- return (
205
- <FooterMenuContext.Provider value={contextValue}>
206
- {children}
207
- </FooterMenuContext.Provider>
208
- );
209
- }
210
-
211
- export const useFooterMenuDesigner = () => useContext(FooterMenuContext);