@akinon/projectzero 2.0.0-beta.19 → 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 (223) hide show
  1. package/CHANGELOG.md +9 -7
  2. package/app-template/CHANGELOG.md +251 -204
  3. package/app-template/akinon.json +1 -1
  4. package/app-template/package.json +28 -28
  5. package/app-template/public/amex.svg +12 -0
  6. package/app-template/public/apple-pay.svg +16 -0
  7. package/app-template/public/assets/images/product-placeholder-1.jpg +0 -0
  8. package/app-template/public/assets/images/product-placeholder-2.jpg +0 -0
  9. package/app-template/public/assets/images/product-placeholder-3.jpg +0 -0
  10. package/app-template/public/assets/images/product-placeholder-4.jpg +0 -0
  11. package/app-template/public/google-pay.svg +16 -0
  12. package/app-template/public/locales/en/account.json +6 -3
  13. package/app-template/public/locales/en/auth.json +6 -7
  14. package/app-template/public/locales/en/basket.json +6 -6
  15. package/app-template/public/locales/en/blog.json +7 -0
  16. package/app-template/public/locales/en/category.json +3 -1
  17. package/app-template/public/locales/en/checkout.json +5 -4
  18. package/app-template/public/locales/en/common.json +11 -2
  19. package/app-template/public/locales/en/forgot_password.json +6 -7
  20. package/app-template/public/locales/en/product.json +4 -3
  21. package/app-template/public/locales/tr/account.json +6 -3
  22. package/app-template/public/locales/tr/auth.json +16 -17
  23. package/app-template/public/locales/tr/basket.json +4 -4
  24. package/app-template/public/locales/tr/blog.json +7 -0
  25. package/app-template/public/locales/tr/category.json +3 -1
  26. package/app-template/public/locales/tr/checkout.json +39 -38
  27. package/app-template/public/locales/tr/common.json +10 -1
  28. package/app-template/public/locales/tr/forgot_password.json +12 -13
  29. package/app-template/public/locales/tr/product.json +1 -0
  30. package/app-template/public/logo.svg +3 -27
  31. package/app-template/public/mastercard.svg +14 -0
  32. package/app-template/public/promotion-banner.jpg +0 -0
  33. package/app-template/public/shop-pay.svg +12 -0
  34. package/app-template/public/visa.svg +12 -0
  35. package/app-template/src/app/[commerce]/[locale]/[currency]/blog/[slug]/page.tsx +118 -0
  36. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +15 -0
  37. package/app-template/src/app/api/theme-settings/route.ts +12 -0
  38. package/app-template/src/assets/fonts/pz-icon.css +211 -49
  39. package/app-template/src/assets/fonts/pz-icon.eot +0 -0
  40. package/app-template/src/assets/fonts/pz-icon.html +486 -0
  41. package/app-template/src/assets/fonts/pz-icon.scss +373 -49
  42. package/app-template/src/assets/fonts/pz-icon.svg +215 -53
  43. package/app-template/src/assets/fonts/pz-icon.ttf +0 -0
  44. package/app-template/src/assets/fonts/pz-icon.woff +0 -0
  45. package/app-template/src/assets/fonts/pz-icon.woff2 +0 -0
  46. package/app-template/src/assets/globals.scss +4 -0
  47. package/app-template/src/assets/icons/arrow-right.svg +3 -0
  48. package/app-template/src/assets/icons/cart.svg +4 -12
  49. package/app-template/src/assets/icons/check.svg +2 -18
  50. package/app-template/src/assets/icons/chevron-down.svg +2 -7
  51. package/app-template/src/assets/icons/delete.svg +3 -0
  52. package/app-template/src/assets/icons/facebook.svg +2 -8
  53. package/app-template/src/assets/icons/fav-off.svg +5 -0
  54. package/app-template/src/assets/icons/fav-on.svg +5 -0
  55. package/app-template/src/assets/icons/filter-and-sort.svg +3 -0
  56. package/app-template/src/assets/icons/heart.svg +3 -0
  57. package/app-template/src/assets/icons/instagram.svg +2 -13
  58. package/app-template/src/assets/icons/materials.svg +3 -0
  59. package/app-template/src/assets/icons/person.svg +4 -0
  60. package/app-template/src/assets/icons/pinterest.svg +5 -11
  61. package/app-template/src/assets/icons/ruler.svg +3 -0
  62. package/app-template/src/assets/icons/search.svg +8 -11
  63. package/app-template/src/assets/icons/share.svg +2 -9
  64. package/app-template/src/assets/icons/snapchat.svg +3 -0
  65. package/app-template/src/assets/icons/tiktok.svg +3 -0
  66. package/app-template/src/assets/icons/tumblr.svg +6 -0
  67. package/app-template/src/assets/icons/twitter.svg +2 -10
  68. package/app-template/src/assets/icons/vimeo.svg +3 -0
  69. package/app-template/src/assets/icons/youtube.svg +3 -0
  70. package/app-template/src/assets/icons/zoom.svg +8 -0
  71. package/app-template/src/components/accordion.tsx +33 -11
  72. package/app-template/src/components/action-tooltip.tsx +160 -0
  73. package/app-template/src/components/currency-select.tsx +149 -4
  74. package/app-template/src/components/icon.tsx +5 -6
  75. package/app-template/src/components/index.ts +4 -1
  76. package/app-template/src/components/language-select.tsx +88 -2
  77. package/app-template/src/components/pagination.tsx +132 -20
  78. package/app-template/src/components/quantity-input.tsx +63 -0
  79. package/app-template/src/components/quantity-selector.tsx +203 -0
  80. package/app-template/src/components/route-handler.tsx +50 -0
  81. package/app-template/src/components/select.tsx +89 -69
  82. package/app-template/src/components/types/index.ts +26 -0
  83. package/app-template/src/components/widget-content.tsx +323 -0
  84. package/app-template/src/data/server/theme.ts +70 -0
  85. package/app-template/src/hooks/use-fav-button.tsx +5 -2
  86. package/app-template/src/hooks/use-product-cart.ts +11 -8
  87. package/app-template/src/hooks/use-theme-settings.ts +42 -0
  88. package/app-template/src/lib/fonts.ts +149 -0
  89. package/app-template/src/settings.js +2 -2
  90. package/app-template/src/types/hookform-resolvers-yup.d.ts +28 -0
  91. package/app-template/src/types/widget.ts +169 -0
  92. package/app-template/src/utils/formatDate.ts +48 -0
  93. package/app-template/src/utils/styles.ts +71 -0
  94. package/app-template/src/views/account/contact-form.tsx +147 -130
  95. package/app-template/src/views/basket/basket-item.tsx +691 -107
  96. package/app-template/src/views/basket/basket-summary-context.tsx +560 -0
  97. package/app-template/src/views/basket/designer-context.tsx +617 -0
  98. package/app-template/src/views/basket/index.ts +2 -0
  99. package/app-template/src/views/basket/summary.tsx +496 -75
  100. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +190 -0
  101. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +286 -0
  102. package/app-template/src/views/breadcrumb/constants.ts +15 -0
  103. package/app-template/src/views/breadcrumb/index.tsx +127 -0
  104. package/app-template/src/views/breadcrumb.tsx +13 -38
  105. package/app-template/src/views/category/category-banner.tsx +4 -23
  106. package/app-template/src/views/category/category-header.tsx +289 -66
  107. package/app-template/src/views/category/category-info.tsx +173 -24
  108. package/app-template/src/views/category/filters/filter-item.tsx +138 -42
  109. package/app-template/src/views/category/filters/index.tsx +208 -48
  110. package/app-template/src/views/category/layout.tsx +7 -4
  111. package/app-template/src/views/category/native-widget-context.tsx +257 -0
  112. package/app-template/src/views/category/product-list-registrar.tsx +665 -0
  113. package/app-template/src/views/checkout/auth.tsx +64 -40
  114. package/app-template/src/views/checkout/checkout-address-registrar.tsx +254 -0
  115. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +183 -0
  116. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +259 -0
  117. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +253 -0
  118. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +183 -0
  119. package/app-template/src/views/checkout/constants.ts +5 -0
  120. package/app-template/src/views/checkout/index.tsx +5 -0
  121. package/app-template/src/views/checkout/layout/header.tsx +9 -5
  122. package/app-template/src/views/checkout/steps/payment/index.tsx +5 -2
  123. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +72 -1
  124. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +15 -0
  125. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +18 -0
  126. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +171 -40
  127. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +74 -12
  128. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +128 -45
  129. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +232 -27
  130. package/app-template/src/views/checkout/summary.tsx +303 -29
  131. package/app-template/src/views/footer/footer-app-banner-context.tsx +326 -0
  132. package/app-template/src/views/footer/footer-bottom-context.tsx +215 -0
  133. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +74 -0
  134. package/app-template/src/views/footer/footer-layout-constants.ts +35 -0
  135. package/app-template/src/views/footer/footer-layout-registrar.tsx +342 -0
  136. package/app-template/src/views/footer/footer-layout-switcher.tsx +110 -0
  137. package/app-template/src/views/footer/footer-menu-context.tsx +211 -0
  138. package/app-template/src/views/footer/footer-native-widgets.tsx +60 -0
  139. package/app-template/src/views/footer/footer-social-context.tsx +254 -0
  140. package/app-template/src/views/footer/footer-subscription-context.tsx +210 -0
  141. package/app-template/src/views/footer/footer-utils.ts +43 -0
  142. package/app-template/src/views/footer/footer-value-props-context.tsx +326 -0
  143. package/app-template/src/views/footer/logo-settings.ts +183 -0
  144. package/app-template/src/views/footer/native-widget-config.ts +262 -0
  145. package/app-template/src/views/footer/subscription-settings.ts +122 -0
  146. package/app-template/src/views/footer/use-footer-logo.ts +162 -0
  147. package/app-template/src/views/footer.tsx +415 -13
  148. package/app-template/src/views/guest-login/index.tsx +62 -58
  149. package/app-template/src/views/header/action-menu.tsx +277 -45
  150. package/app-template/src/views/header/band.tsx +6 -21
  151. package/app-template/src/views/header/designer-context.tsx +261 -0
  152. package/app-template/src/views/header/header-announcement-registrar.tsx +267 -0
  153. package/app-template/src/views/header/header-client-wrapper.tsx +496 -0
  154. package/app-template/src/views/header/header-content.tsx +1026 -0
  155. package/app-template/src/views/header/header-currency-registrar.tsx +348 -0
  156. package/app-template/src/views/header/header-icons-context.tsx +262 -0
  157. package/app-template/src/views/header/header-language-registrar.tsx +348 -0
  158. package/app-template/src/views/header/header-layout-context.tsx +143 -0
  159. package/app-template/src/views/header/header-layout-registrar.tsx +658 -0
  160. package/app-template/src/views/header/header-logo-context.tsx +228 -0
  161. package/app-template/src/views/header/header-logo.tsx +118 -0
  162. package/app-template/src/views/header/header-mini-basket-context.tsx +524 -0
  163. package/app-template/src/views/header/header-search-registrar.tsx +511 -0
  164. package/app-template/src/views/header/header-text-slider-registrar.tsx +382 -0
  165. package/app-template/src/views/header/index.tsx +109 -47
  166. package/app-template/src/views/header/inline-search.tsx +262 -0
  167. package/app-template/src/views/header/mini-basket.tsx +819 -44
  168. package/app-template/src/views/header/mobile-hamburger-button.tsx +5 -8
  169. package/app-template/src/views/header/mobile-menu.tsx +12 -0
  170. package/app-template/src/views/header/navbar-menu-context.tsx +219 -0
  171. package/app-template/src/views/header/navbar.tsx +178 -111
  172. package/app-template/src/views/header/search/index.tsx +71 -32
  173. package/app-template/src/views/header/search/results.tsx +127 -65
  174. package/app-template/src/views/header/search/search-input.tsx +61 -0
  175. package/app-template/src/views/header/server-settings-parser.ts +1105 -0
  176. package/app-template/src/views/header/use-header-icons.ts +241 -0
  177. package/app-template/src/views/header/use-header-logo.ts +213 -0
  178. package/app-template/src/views/header/use-navbar-menu.ts +179 -0
  179. package/app-template/src/views/login/index.tsx +54 -46
  180. package/app-template/src/views/product/accordion-section.tsx +61 -0
  181. package/app-template/src/views/product/accordion-wrapper.tsx +135 -43
  182. package/app-template/src/views/product/custom-button-group.tsx +69 -0
  183. package/app-template/src/views/product/favorites-button-section.tsx +69 -0
  184. package/app-template/src/views/product/find-in-store-section.tsx +60 -0
  185. package/app-template/src/views/product/index.ts +1 -0
  186. package/app-template/src/views/product/layout.tsx +6 -5
  187. package/app-template/src/views/product/misc-buttons.tsx +339 -25
  188. package/app-template/src/views/product/price-wrapper.tsx +3 -29
  189. package/app-template/src/views/product/product-actions.tsx +137 -8
  190. package/app-template/src/views/product/product-info-section.tsx +140 -0
  191. package/app-template/src/views/product/product-info.tsx +69 -31
  192. package/app-template/src/views/product/product-share.tsx +13 -8
  193. package/app-template/src/views/product/product-variants.tsx +2 -2
  194. package/app-template/src/views/product/quantity-section.tsx +73 -0
  195. package/app-template/src/views/product/sale-tag.tsx +10 -0
  196. package/app-template/src/views/product/share-section.tsx +357 -0
  197. package/app-template/src/views/product/slider.tsx +117 -79
  198. package/app-template/src/views/product/variant.tsx +69 -41
  199. package/app-template/src/views/product/variants-section.tsx +126 -0
  200. package/app-template/src/views/product-detail/constants.ts +272 -0
  201. package/app-template/src/views/product-detail/index.ts +10 -0
  202. package/app-template/src/views/product-detail/product-detail-registrar.tsx +616 -0
  203. package/app-template/src/views/product-item/index.tsx +119 -46
  204. package/app-template/src/views/register/index.tsx +14 -25
  205. package/app-template/src/views/share/index.tsx +9 -6
  206. package/app-template/src/views/widgets/home-hero-slider-content.tsx +41 -39
  207. package/app-template/src/widgets/flatpages/about-us/index.tsx +78 -0
  208. package/app-template/src/widgets/flatpages/blog-list/index.tsx +129 -0
  209. package/app-template/src/widgets/footer-app-banner.tsx +444 -0
  210. package/app-template/src/widgets/footer-bottom.tsx +127 -0
  211. package/app-template/src/widgets/footer-menu-compact.tsx +238 -0
  212. package/app-template/src/widgets/footer-menu-two.tsx +298 -0
  213. package/app-template/src/widgets/footer-social-client.tsx +251 -0
  214. package/app-template/src/widgets/footer-social.tsx +47 -16
  215. package/app-template/src/widgets/footer-subscription/footer-subscription-form.tsx +17 -14
  216. package/app-template/src/widgets/footer-subscription/index.tsx +183 -17
  217. package/app-template/src/widgets/footer-value-props.tsx +201 -0
  218. package/app-template/src/widgets/index.ts +7 -0
  219. package/app-template/src/widgets/schemas/about-us.json +46 -0
  220. package/app-template/src/widgets/schemas/blog-list.json +37 -0
  221. package/app-template/src/widgets/schemas/blog.json +29 -0
  222. package/app-template/tailwind.config.js +18 -2
  223. package/package.json +1 -1
@@ -0,0 +1,342 @@
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
+ }
@@ -0,0 +1,110 @@
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
+ }
@@ -0,0 +1,211 @@
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);