@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.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- 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>
|