@o2vend/theme-cli 1.0.32

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 (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,695 @@
1
+ {% comment %}
2
+ Footer Menu Widget
3
+
4
+ Renders footer navigation links dynamically from API.
5
+ Fetches menus using /webstoreapi/menus endpoint
6
+ Filters by Type = "Footer Menu" (case-insensitive)
7
+ Gracefully handles missing/failed menus
8
+
9
+ FIXES:
10
+ - Proper error handling (footer still shows)
11
+ - Correct API endpoint structure
12
+ - Handles no menu items gracefully
13
+ - No XSS vulnerabilities
14
+ {% endcomment %}
15
+
16
+ {% liquid
17
+ if widget
18
+ assign widget_settings = widget.settings
19
+ else
20
+ assign widget_settings = nil
21
+ endif
22
+ %}
23
+
24
+ <nav class="widget widget-footer-menu" data-widget-id="{{ widget.id | default: 'auto-fetch' }}" data-footer-menu-nav role="navigation" aria-label="Footer navigation">
25
+ <div class="widget-footer-menu__loading" data-menu-loading>
26
+ <span>Loading menu...</span>
27
+ </div>
28
+ <div class="widget-footer-menu__error" data-menu-error style="display: none;">
29
+ <span data-error-message>Menu items unavailable</span>
30
+ </div>
31
+ <div class="widget-footer-menu__columns" data-menu-columns>
32
+ <!-- Menu columns will be dynamically inserted here -->
33
+ </div>
34
+ </nav>
35
+
36
+ <style>
37
+ .widget-footer-menu {
38
+ padding: 0;
39
+ margin: 0 auto;
40
+ max-width: 100%;
41
+ }
42
+
43
+ .widget-footer-menu__loading,
44
+ .widget-footer-menu__error {
45
+ padding: 1rem;
46
+ text-align: center;
47
+ font-size: 0.875rem;
48
+ color: #9ca3af;
49
+ }
50
+
51
+ .widget-footer-menu__error {
52
+ color: #ef4444;
53
+ }
54
+
55
+ .widget-footer-menu__columns {
56
+ display: grid;
57
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
58
+ gap: 2.5rem;
59
+ list-style: none;
60
+ margin: 0;
61
+ padding: 0;
62
+ min-height: 30px;
63
+ }
64
+
65
+ .widget-footer-menu__column {
66
+ display: flex;
67
+ flex-direction: column;
68
+ }
69
+
70
+ .widget-footer-menu__column > .widget-footer-menu__title {
71
+ display: block;
72
+ }
73
+
74
+ .widget-footer-menu__title {
75
+ margin: 0;
76
+ font-size: 0.875rem;
77
+ text-transform: uppercase;
78
+ letter-spacing: 0.05em;
79
+ font-weight: 600;
80
+ color: {{ widget_settings.title_color | default: '#ffffff' }};
81
+ display: none;
82
+ }
83
+
84
+ .widget-footer-menu__list {
85
+ list-style: none;
86
+ margin: 0;
87
+ padding: 0;
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 0.75rem;
91
+ margin-top: 1rem;
92
+ }
93
+
94
+ .widget-footer-menu__child-list {
95
+ list-style: none;
96
+ margin: 0;
97
+ padding: 0;
98
+ padding-left: 1rem;
99
+ margin-top: 0.5rem;
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 0.5rem;
103
+ }
104
+
105
+ .widget-footer-menu__item--has-children {
106
+ display: flex;
107
+ flex-direction: column;
108
+ }
109
+
110
+ .widget-footer-menu__item {
111
+ display: inline-block;
112
+ margin: 0;
113
+ }
114
+
115
+ .widget-footer-menu__link {
116
+ color: {{ widget_settings.link_color | default: '#9ca3af' }};
117
+ text-decoration: none;
118
+ font-size: 0.9375rem;
119
+ font-weight: 400;
120
+ transition: color 0.2s ease;
121
+ display: inline-block;
122
+ padding: 0;
123
+ white-space: nowrap;
124
+ }
125
+
126
+ .widget-footer-menu__link:hover {
127
+ color: {{ widget_settings.link_hover_color | default: '#ffffff' }};
128
+ }
129
+
130
+ @media (max-width: 768px) {
131
+ .widget-footer-menu__columns {
132
+ gap: 2rem;
133
+ }
134
+
135
+ .widget-footer-menu__list {
136
+ gap: 2rem;
137
+ }
138
+
139
+ .widget-footer-menu__link {
140
+ font-size: 0.875rem;
141
+ }
142
+ }
143
+
144
+ @media (max-width: 480px) {
145
+ .widget-footer-menu__columns {
146
+ flex-direction: column;
147
+ align-items: center;
148
+ gap: 1.5rem;
149
+ }
150
+
151
+ .widget-footer-menu__list {
152
+ flex-direction: column;
153
+ align-items: center;
154
+ gap: 1rem;
155
+ text-align: center;
156
+ }
157
+ }
158
+ </style>
159
+
160
+ <script>
161
+ (function() {
162
+ 'use strict';
163
+
164
+ // Configuration
165
+ const MENU_API_ENDPOINT = '/webstoreapi/menus';
166
+ const MENU_TYPE_FOOTER = 'footermenu'; // Normalized comparison
167
+ const INIT_DELAY_MS = 100;
168
+ const API_TIMEOUT_MS = 5000;
169
+
170
+ /**
171
+ * Find the menu nav element for this widget
172
+ */
173
+ function findMenuNav() {
174
+ const widgetId = '{{ widget.id }}';
175
+
176
+ // Try multiple selectors for robustness
177
+ let menuNav = document.querySelector(`[data-footer-menu-nav][data-widget-id="${widgetId}"]`);
178
+
179
+ if (!menuNav) {
180
+ menuNav = document.querySelector(`[data-widget-id="${widgetId}"][data-footer-menu-nav]`);
181
+ }
182
+
183
+ if (!menuNav) {
184
+ menuNav = document.querySelector('.widget-footer-menu[data-widget-id="{{ widget.id }}"]');
185
+ }
186
+
187
+ if (!menuNav) {
188
+ menuNav = document.querySelector('[data-footer-menu-nav]');
189
+ }
190
+
191
+ if (!menuNav) {
192
+ console.warn('[FooterMenu] Menu nav element not found for widget ID: {{ widget.id }}');
193
+ }
194
+
195
+ return menuNav;
196
+ }
197
+
198
+ /**
199
+ * Load footer menu from API with timeout
200
+ */
201
+ async function loadFooterMenu(menuNav) {
202
+ console.log('[FooterMenu] loadFooterMenu called');
203
+
204
+ const menuColumns = menuNav.querySelector('[data-menu-columns]');
205
+ const loadingEl = menuNav.querySelector('[data-menu-loading]');
206
+ const errorEl = menuNav.querySelector('[data-menu-error]');
207
+
208
+ console.log('[FooterMenu] Found elements:', {
209
+ menuColumns: !!menuColumns,
210
+ loadingEl: !!loadingEl,
211
+ errorEl: !!errorEl
212
+ });
213
+
214
+ if (!menuColumns) {
215
+ console.error('[FooterMenu] Menu columns container not found');
216
+ console.error('[FooterMenu] Available data attributes in menuNav:', Array.from(menuNav.querySelectorAll('*')).map(el => ({
217
+ tag: el.tagName,
218
+ dataAttributes: Array.from(el.attributes).filter(attr => attr.name.startsWith('data-')).map(attr => attr.name)
219
+ })));
220
+ if (errorEl) {
221
+ const errorMessageEl = errorEl.querySelector('[data-error-message]');
222
+ if (errorMessageEl) {
223
+ errorMessageEl.textContent = 'Menu container not found. Please refresh the page.';
224
+ }
225
+ errorEl.style.display = 'block';
226
+ }
227
+ return;
228
+ }
229
+
230
+ try {
231
+ // Show loading state
232
+ console.log('[FooterMenu] Showing loading state');
233
+ if (loadingEl) loadingEl.style.display = 'block';
234
+ if (errorEl) errorEl.style.display = 'none';
235
+ menuColumns.innerHTML = '';
236
+
237
+ // Fetch all menus with timeout
238
+ console.log('[FooterMenu] Fetching menus from', MENU_API_ENDPOINT);
239
+ console.log('[FooterMenu] Request configuration:', {
240
+ method: 'GET',
241
+ headers: {
242
+ 'Accept': 'application/json',
243
+ 'X-Requested-With': 'XMLHttpRequest'
244
+ },
245
+ timeout: API_TIMEOUT_MS
246
+ });
247
+
248
+ const controller = new AbortController();
249
+ const timeoutId = setTimeout(() => {
250
+ console.error('[FooterMenu] Request timeout after', API_TIMEOUT_MS, 'ms');
251
+ controller.abort();
252
+ }, API_TIMEOUT_MS);
253
+
254
+ const startTime = performance.now();
255
+ const menusResponse = await fetch(MENU_API_ENDPOINT, {
256
+ method: 'GET',
257
+ headers: {
258
+ 'Accept': 'application/json',
259
+ 'X-Requested-With': 'XMLHttpRequest'
260
+ },
261
+ signal: controller.signal
262
+ });
263
+ const fetchTime = performance.now() - startTime;
264
+
265
+ clearTimeout(timeoutId);
266
+
267
+ console.log('[FooterMenu] Menus API response received:', {
268
+ status: menusResponse.status,
269
+ statusText: menusResponse.statusText,
270
+ ok: menusResponse.ok,
271
+ contentType: menusResponse.headers.get('content-type'),
272
+ fetchTime: `${fetchTime.toFixed(2)}ms`
273
+ });
274
+
275
+ if (!menusResponse.ok) {
276
+ const errorText = await menusResponse.text().catch(() => 'Unable to read error response');
277
+ console.error('[FooterMenu] API error response:', errorText);
278
+ throw new Error(`API Error: ${menusResponse.status} ${menusResponse.statusText}`);
279
+ }
280
+
281
+ const menus = await menusResponse.json();
282
+
283
+ console.log('[FooterMenu] Parsed menus response:', {
284
+ isArray: Array.isArray(menus),
285
+ length: Array.isArray(menus) ? menus.length : 'N/A',
286
+ sample: Array.isArray(menus) && menus.length > 0 ? menus[0] : null
287
+ });
288
+
289
+ if (!Array.isArray(menus)) {
290
+ console.error('[FooterMenu] Response is not an array:', typeof menus, menus);
291
+ throw new Error('Invalid response: menus is not an array');
292
+ }
293
+
294
+ console.log('[FooterMenu] Retrieved', menus.length, 'total menus');
295
+ if (menus.length > 0) {
296
+ console.log('[FooterMenu] Sample menu types:', menus.slice(0, 3).map(m => ({
297
+ id: m.id,
298
+ type: m.type || m.Type,
299
+ name: m.name || m.Name
300
+ })));
301
+ }
302
+
303
+ // Filter for "Footer Menu" type (case-insensitive, whitespace-tolerant)
304
+ const footerMenus = menus.filter(menu => {
305
+ const menuType = (menu.type || menu.Type || '').toString().toLowerCase().trim();
306
+ const normalizedType = menuType.replace(/\s+/g, '');
307
+ const matches = normalizedType === MENU_TYPE_FOOTER;
308
+ if (matches) {
309
+ console.log('[FooterMenu] Found footer menu:', {
310
+ id: menu.id,
311
+ type: menu.type || menu.Type,
312
+ name: menu.name || menu.Name
313
+ });
314
+ }
315
+ return matches;
316
+ });
317
+
318
+ console.log('[FooterMenu] Found', footerMenus.length, 'footer menus after filtering');
319
+ console.log('[FooterMenu] Footer menu IDs:', footerMenus.map(m => m.id));
320
+
321
+ if (footerMenus.length === 0) {
322
+ console.warn('[FooterMenu] No footer menus found - available menu types:',
323
+ menus.map(m => (m.type || m.Type || 'unknown').toString().toLowerCase()));
324
+ if (loadingEl) loadingEl.style.display = 'none';
325
+ // Don't show error - just let it be empty, footer still renders
326
+ return;
327
+ }
328
+
329
+ // Fetch each footer menu's items
330
+ // Use a separate AbortController for menu item fetches to avoid conflicts
331
+ // with the initial menus fetch
332
+ const itemsController = new AbortController();
333
+ const itemsTimeoutId = setTimeout(() => {
334
+ console.warn('[FooterMenu] Menu items fetch timeout after', API_TIMEOUT_MS, 'ms');
335
+ itemsController.abort();
336
+ }, API_TIMEOUT_MS * 2); // Give more time for multiple menu fetches
337
+
338
+ // Store menu data with their items to render grouped menus
339
+ const menuGroups = [];
340
+ console.log('[FooterMenu] Starting to fetch menu items for', footerMenus.length, 'footer menu(s)');
341
+
342
+ for (let i = 0; i < footerMenus.length; i++) {
343
+ const footerMenu = footerMenus[i];
344
+ try {
345
+ console.log(`[FooterMenu] [${i + 1}/${footerMenus.length}] Fetching menu items for ID:`, footerMenu.id);
346
+ console.log('[FooterMenu] Menu details:', {
347
+ id: footerMenu.id,
348
+ name: footerMenu.name || footerMenu.Name,
349
+ type: footerMenu.type || footerMenu.Type
350
+ });
351
+
352
+ const menuUrl = `${MENU_API_ENDPOINT}/${footerMenu.id}`;
353
+ console.log('[FooterMenu] Attempting fetch to:', menuUrl);
354
+
355
+ const itemStartTime = performance.now();
356
+ let menuResponse;
357
+ try {
358
+ menuResponse = await fetch(menuUrl, {
359
+ method: 'GET',
360
+ headers: {
361
+ 'Accept': 'application/json',
362
+ 'X-Requested-With': 'XMLHttpRequest'
363
+ },
364
+ signal: itemsController.signal // Use separate controller for menu item fetches
365
+ });
366
+ } catch (fetchError) {
367
+ // Catch network errors, abort errors, etc.
368
+ console.error('[FooterMenu] Fetch error for menu', footerMenu.id, ':', fetchError);
369
+ if (fetchError.name === 'AbortError') {
370
+ console.error('[FooterMenu] Fetch was aborted - check timeout or signal');
371
+ }
372
+ throw fetchError; // Re-throw to be caught by outer catch
373
+ }
374
+ const itemFetchTime = performance.now() - itemStartTime;
375
+
376
+ console.log('[FooterMenu] Menu items API response:', {
377
+ menuId: footerMenu.id,
378
+ status: menuResponse.status,
379
+ statusText: menuResponse.statusText,
380
+ ok: menuResponse.ok,
381
+ fetchTime: `${itemFetchTime.toFixed(2)}ms`
382
+ });
383
+
384
+ if (!menuResponse.ok) {
385
+ const errorText = await menuResponse.text().catch(() => 'Unable to read error response');
386
+ console.error('[FooterMenu] Error response for menu', footerMenu.id, ':', errorText);
387
+ throw new Error(`Failed to fetch menu ${footerMenu.id}: ${menuResponse.status}`);
388
+ }
389
+
390
+ const menuData = await menuResponse.json();
391
+
392
+ console.log('[FooterMenu] Parsed menu data for', footerMenu.id, ':', {
393
+ hasItems: !!(menuData && menuData.items),
394
+ itemsIsArray: !!(menuData && Array.isArray(menuData.items)),
395
+ itemsLength: menuData && Array.isArray(menuData.items) ? menuData.items.length : 0
396
+ });
397
+
398
+ if (!menuData || !Array.isArray(menuData.items)) {
399
+ console.warn(`[FooterMenu] Menu ${footerMenu.id} has no items array. Menu data:`, menuData);
400
+ continue;
401
+ }
402
+
403
+ console.log(`[FooterMenu] Menu ${footerMenu.id} has`, menuData.items.length, 'items');
404
+ if (menuData.items.length > 0) {
405
+ console.log('[FooterMenu] Sample menu items:', menuData.items.slice(0, 3).map(item => ({
406
+ name: item.name || item.title,
407
+ link: item.link,
408
+ hasChildren: !!(item.childItems && item.childItems.length > 0)
409
+ })));
410
+ }
411
+
412
+ // Store menu group with title and items
413
+ menuGroups.push({
414
+ id: footerMenu.id,
415
+ name: footerMenu.name || footerMenu.Name || `Menu ${i + 1}`,
416
+ items: menuData.items || []
417
+ });
418
+
419
+ console.log('[FooterMenu] Stored menu group:', {
420
+ name: footerMenu.name || footerMenu.Name,
421
+ itemsCount: menuData.items.length
422
+ });
423
+
424
+ } catch (itemError) {
425
+ console.error('[FooterMenu] Error fetching menu items for', footerMenu.id, ':', itemError);
426
+ console.error('[FooterMenu] Error details:', {
427
+ name: itemError.name,
428
+ message: itemError.message,
429
+ stack: itemError.stack
430
+ });
431
+ // Check if it's an abort error
432
+ if (itemError.name === 'AbortError') {
433
+ console.error('[FooterMenu] Menu items fetch was aborted - likely timeout');
434
+ }
435
+ // Continue with other menus even if one fails
436
+ }
437
+ }
438
+
439
+ // Clear the timeout since all fetches are complete
440
+ clearTimeout(itemsTimeoutId);
441
+
442
+ console.log('[FooterMenu] Finished fetching all menus. Total menu groups:', menuGroups.length);
443
+
444
+ if (menuGroups.length === 0) {
445
+ console.warn('[FooterMenu] No menu groups found');
446
+ if (loadingEl) loadingEl.style.display = 'none';
447
+ return;
448
+ }
449
+
450
+ // Render grouped menu columns
451
+ console.log('[FooterMenu] Rendering', menuGroups.length, 'menu groups');
452
+ renderMenuGroups(menuColumns, menuGroups);
453
+ console.log('[FooterMenu] Menu groups rendered successfully');
454
+
455
+ // Hide loading state
456
+ if (loadingEl) {
457
+ loadingEl.style.display = 'none';
458
+ console.log('[FooterMenu] Loading state hidden');
459
+ }
460
+
461
+ } catch (error) {
462
+ console.error('[FooterMenu] Error loading menu:', error);
463
+ console.error('[FooterMenu] Error details:', {
464
+ name: error.name,
465
+ message: error.message,
466
+ stack: error.stack,
467
+ cause: error.cause
468
+ });
469
+
470
+ // Hide loading, show error
471
+ if (loadingEl) {
472
+ loadingEl.style.display = 'none';
473
+ }
474
+
475
+ if (errorEl) {
476
+ // Update error message with more details
477
+ const errorMessageEl = errorEl.querySelector('[data-error-message]');
478
+ let errorMessage = 'Menu items unavailable';
479
+
480
+ // Provide more specific error messages
481
+ if (error.name === 'AbortError') {
482
+ errorMessage = 'Request timed out. Please refresh the page.';
483
+ console.error('[FooterMenu] Request aborted - likely timeout');
484
+ } else if (error.message && error.message.includes('404')) {
485
+ errorMessage = 'Menu endpoint not found.';
486
+ console.error('[FooterMenu] 404 error - endpoint may not exist');
487
+ } else if (error.message && error.message.includes('401') || error.message.includes('403')) {
488
+ errorMessage = 'Access denied. Please check permissions.';
489
+ console.error('[FooterMenu] Authentication/authorization error');
490
+ } else if (error.message && error.message.includes('500')) {
491
+ errorMessage = 'Server error. Please try again later.';
492
+ console.error('[FooterMenu] Server error');
493
+ } else if (error.message) {
494
+ errorMessage = `Error: ${error.message}`;
495
+ }
496
+
497
+ if (errorMessageEl) {
498
+ errorMessageEl.textContent = errorMessage;
499
+ } else {
500
+ errorEl.textContent = errorMessage;
501
+ }
502
+
503
+ errorEl.style.display = 'block';
504
+ console.log('[FooterMenu] Error message displayed to user:', errorMessage);
505
+ }
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Render grouped menu columns - Each footer menu as a separate column with title
511
+ */
512
+ function renderMenuGroups(container, menuGroups) {
513
+ console.log('[FooterMenu] renderMenuGroups called with', menuGroups ? menuGroups.length : 0, 'menu groups');
514
+
515
+ if (!menuGroups || menuGroups.length === 0) {
516
+ console.warn('[FooterMenu] renderMenuGroups: No menu groups to render');
517
+ return;
518
+ }
519
+
520
+ menuGroups.forEach((menuGroup, groupIndex) => {
521
+ console.log(`[FooterMenu] Rendering menu group ${groupIndex + 1}:`, menuGroup.name);
522
+
523
+ // Create a column for each menu group
524
+ const column = document.createElement('div');
525
+ column.className = 'widget-footer-menu__column';
526
+
527
+ // Create and add title
528
+ const title = document.createElement('h4');
529
+ title.className = 'widget-footer-menu__title';
530
+ title.textContent = menuGroup.name || `Menu ${groupIndex + 1}`;
531
+ column.appendChild(title);
532
+
533
+ // Create list for menu items
534
+ const list = document.createElement('ul');
535
+ list.className = 'widget-footer-menu__list';
536
+
537
+ // Sort items by displayOrder
538
+ const sortedItems = [...(menuGroup.items || [])].sort((a, b) => {
539
+ const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
540
+ const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
541
+ return orderA - orderB;
542
+ });
543
+
544
+ // Render items with their child items
545
+ sortedItems.forEach((item) => {
546
+ const listItem = createMenuItemWithChildren(item);
547
+ if (listItem) {
548
+ list.appendChild(listItem);
549
+ }
550
+ });
551
+
552
+ column.appendChild(list);
553
+ container.appendChild(column);
554
+
555
+ console.log(`[FooterMenu] Rendered menu group "${menuGroup.name}" with ${sortedItems.length} items`);
556
+ });
557
+
558
+ console.log('[FooterMenu] All menu groups rendered');
559
+ }
560
+
561
+ /**
562
+ * Create a menu item element with child items support
563
+ */
564
+ function createMenuItemWithChildren(item) {
565
+ try {
566
+ const listItem = document.createElement('li');
567
+ listItem.className = 'widget-footer-menu__item';
568
+
569
+ const itemName = (item.name || item.title || '').trim();
570
+ if (!itemName) {
571
+ console.warn('[FooterMenu] Menu item has no name or title:', item);
572
+ return null;
573
+ }
574
+
575
+ // Create link for the main item
576
+ const link = document.createElement('a');
577
+ link.href = item.link || '#';
578
+ link.className = 'widget-footer-menu__link';
579
+ link.textContent = itemName;
580
+ link.setAttribute('aria-label', itemName);
581
+
582
+ // Handle external links
583
+ if (item.link && (item.link.startsWith('http://') || item.link.startsWith('https://'))) {
584
+ try {
585
+ const currentDomain = window.location.hostname;
586
+ const linkDomain = new URL(item.link).hostname;
587
+ if (linkDomain !== currentDomain) {
588
+ link.setAttribute('target', '_blank');
589
+ link.setAttribute('rel', 'noopener noreferrer');
590
+ }
591
+ } catch (e) {
592
+ console.warn('[FooterMenu] Invalid URL:', item.link, 'Error:', e.message);
593
+ }
594
+ }
595
+
596
+ listItem.appendChild(link);
597
+
598
+ // Add child items if they exist
599
+ const childItems = item.childItems;
600
+ if (childItems && Array.isArray(childItems) && childItems.length > 0) {
601
+ const childList = document.createElement('ul');
602
+ childList.className = 'widget-footer-menu__child-list';
603
+
604
+ // Sort child items by displayOrder
605
+ const sortedChildren = [...childItems].sort((a, b) => {
606
+ const orderA = a.displayOrder !== undefined ? a.displayOrder : 999;
607
+ const orderB = b.displayOrder !== undefined ? b.displayOrder : 999;
608
+ return orderA - orderB;
609
+ });
610
+
611
+ sortedChildren.forEach((childItem) => {
612
+ const childListItem = createMenuItemWithChildren(childItem);
613
+ if (childListItem) {
614
+ childList.appendChild(childListItem);
615
+ }
616
+ });
617
+
618
+ if (childList.children.length > 0) {
619
+ listItem.appendChild(childList);
620
+ listItem.classList.add('widget-footer-menu__item--has-children');
621
+ }
622
+ }
623
+
624
+ return listItem;
625
+ } catch (error) {
626
+ console.error('[FooterMenu] Error creating menu item with children:', error, 'Item:', item);
627
+ return null;
628
+ }
629
+ }
630
+
631
+
632
+ /**
633
+ * Initialize footer menu with retry logic
634
+ */
635
+ function initFooterMenu(retryCount = 0) {
636
+ const MAX_RETRIES = 5;
637
+ const RETRY_DELAY_MS = 200;
638
+
639
+ console.log(`[FooterMenu] Initialization attempt ${retryCount + 1}/${MAX_RETRIES + 1}`);
640
+
641
+ const menuNav = findMenuNav();
642
+
643
+ if (!menuNav) {
644
+ if (retryCount < MAX_RETRIES) {
645
+ console.warn(`[FooterMenu] Menu nav element not found, retrying in ${RETRY_DELAY_MS}ms...`);
646
+ setTimeout(() => {
647
+ initFooterMenu(retryCount + 1);
648
+ }, RETRY_DELAY_MS);
649
+ return;
650
+ } else {
651
+ console.error('[FooterMenu] Cannot initialize - menu nav element not found after maximum retries');
652
+ console.error('[FooterMenu] Widget ID:', '{{ widget.id }}');
653
+ console.error('[FooterMenu] Available elements:', {
654
+ allNavElements: document.querySelectorAll('[data-footer-menu-nav]').length,
655
+ allWidgetElements: document.querySelectorAll('[data-widget-id]').length,
656
+ readyState: document.readyState
657
+ });
658
+ return;
659
+ }
660
+ }
661
+
662
+ console.log('[FooterMenu] Menu nav element found successfully');
663
+ console.log('[FooterMenu] Initializing footer menu widget with ID:', '{{ widget.id }}');
664
+ loadFooterMenu(menuNav);
665
+ }
666
+
667
+ // Enhanced initialization that handles multiple scenarios
668
+ function startInitialization() {
669
+ console.log('[FooterMenu] Starting footer menu initialization');
670
+ console.log('[FooterMenu] Document ready state:', document.readyState);
671
+ console.log('[FooterMenu] Widget ID:', '{{ widget.id }}');
672
+
673
+ // If DOM is already loaded or complete, initialize immediately
674
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
675
+ console.log('[FooterMenu] DOM already loaded, initializing immediately');
676
+ // Use setTimeout to ensure DOM is fully ready even if state is 'interactive'
677
+ setTimeout(() => {
678
+ initFooterMenu();
679
+ }, INIT_DELAY_MS);
680
+ } else {
681
+ // Wait for DOMContentLoaded
682
+ console.log('[FooterMenu] Waiting for DOMContentLoaded event');
683
+ document.addEventListener('DOMContentLoaded', () => {
684
+ console.log('[FooterMenu] DOMContentLoaded fired');
685
+ setTimeout(() => {
686
+ initFooterMenu();
687
+ }, INIT_DELAY_MS);
688
+ });
689
+ }
690
+ }
691
+
692
+ // Start initialization
693
+ startInitialization();
694
+ })();
695
+ </script>