@o2vend/theme-cli 1.0.36 → 1.0.38
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 +4 -0
- package/lib/lib/dev-server.js +344 -48
- package/lib/lib/liquid-engine.js +3 -1
- package/lib/lib/mock-data.js +473 -119
- package/lib/lib/widget-service.js +12 -4
- package/package.json +2 -2
- package/test-theme/assets/async-sections.js +32 -24
- package/test-theme/assets/cart-drawer.js +20 -22
- package/test-theme/assets/cart-manager.js +1 -15
- package/test-theme/assets/checkout-price-handler.js +12 -11
- package/test-theme/assets/checkout.css +1415 -0
- package/test-theme/assets/checkout.js +3174 -0
- package/test-theme/assets/components.css +178 -29
- package/test-theme/assets/delivery-zone.js +1 -1
- package/test-theme/assets/product-detail.css +1050 -0
- package/test-theme/assets/product-detail.js +2940 -0
- package/test-theme/assets/theme.css +95 -120
- package/test-theme/assets/theme.js +781 -186
- package/test-theme/layout/theme.liquid +91 -17
- package/test-theme/sections/content.liquid +64 -57
- package/test-theme/sections/footer-fallback.liquid +57 -7
- package/test-theme/sections/footer.liquid +63 -12
- package/test-theme/sections/header-fallback.liquid +41 -41
- package/test-theme/sections/header.liquid +41 -51
- package/test-theme/sections/hero-fallback.liquid +1 -1
- package/test-theme/sections/hero.liquid +159 -136
- package/test-theme/snippets/account-sidebar.liquid +121 -29
- package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
- package/test-theme/snippets/breadcrumbs.liquid +98 -11
- package/test-theme/snippets/cart-drawer.liquid +93 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
- package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
- package/test-theme/snippets/delivery-zone-search.liquid +104 -18
- package/test-theme/snippets/login-modal.liquid +269 -82
- package/test-theme/snippets/mega-menu.liquid +130 -43
- package/test-theme/snippets/news-thumbnail.liquid +120 -28
- package/test-theme/snippets/pagination.liquid +1 -1
- package/test-theme/snippets/price.liquid +100 -9
- package/test-theme/snippets/product-card-related.liquid +22 -4
- package/test-theme/snippets/product-card-simple.liquid +521 -25
- package/test-theme/snippets/product-card.liquid +145 -232
- package/test-theme/snippets/rating.liquid +100 -9
- package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
- package/test-theme/snippets/skeleton-product-card.liquid +102 -16
- package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
- package/test-theme/snippets/social-sharing.liquid +133 -32
- package/test-theme/templates/account/dashboard.liquid +30 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
- package/test-theme/templates/account/loyalty.liquid +45 -43
- package/test-theme/templates/account/order-detail.liquid +15 -8
- package/test-theme/templates/account/orders.liquid +189 -35
- package/test-theme/templates/account/profile.liquid +509 -114
- package/test-theme/templates/account/register.liquid +18 -8
- package/test-theme/templates/account/return-orders.liquid +31 -30
- package/test-theme/templates/account/store-credit.liquid +27 -26
- package/test-theme/templates/account/subscriptions.liquid +22 -5
- package/test-theme/templates/account/wishlist.liquid +88 -19
- package/test-theme/templates/address-book.liquid +166 -69
- package/test-theme/templates/categories.liquid +90 -30
- package/test-theme/templates/checkout.liquid +137 -3834
- package/test-theme/templates/error.liquid +23 -21
- package/test-theme/templates/index.liquid +29 -0
- package/test-theme/templates/login.liquid +33 -6
- package/test-theme/templates/order-confirmation.liquid +67 -9
- package/test-theme/templates/page.liquid +418 -206
- package/test-theme/templates/product-detail.liquid +124 -3878
- package/test-theme/templates/products.liquid +155 -30
- package/test-theme/templates/search.liquid +739 -225
- package/test-theme/widgets/brand-carousel.liquid +102 -82
- package/test-theme/widgets/brand.liquid +78 -50
- package/test-theme/widgets/carousel.liquid +253 -121
- package/test-theme/widgets/category-list-carousel.liquid +32 -8
- package/test-theme/widgets/category-list.liquid +21 -6
- package/test-theme/widgets/category.liquid +104 -37
- package/test-theme/widgets/discount-time.liquid +326 -119
- package/test-theme/widgets/footer-menu.liquid +115 -23
- package/test-theme/widgets/footer.liquid +118 -5
- package/test-theme/widgets/gallery.liquid +29 -5
- package/test-theme/widgets/header-menu.liquid +25 -13
- package/test-theme/widgets/header.liquid +64 -26
- package/test-theme/widgets/html.liquid +29 -6
- package/test-theme/widgets/news.liquid +6 -0
- package/test-theme/widgets/product-canvas.liquid +20 -12
- package/test-theme/widgets/product-carousel.liquid +118 -56
- package/test-theme/widgets/shared/product-grid.liquid +12 -0
- package/test-theme/widgets/single-product.liquid +688 -250
- package/test-theme/widgets/spacebar-carousel.liquid +39 -10
- package/test-theme/widgets/spacebar.liquid +77 -6
- package/test-theme/widgets/splash.liquid +40 -30
- package/test-theme/widgets/testimonial-carousel.liquid +111 -67
|
@@ -3,9 +3,73 @@
|
|
|
3
3
|
* Modern, interactive functionality theme
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
(
|
|
6
|
+
(() => {
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
// Initialize payment gateway event system globally (for payment gateway apps)
|
|
10
|
+
// This must be initialized before payment gateway apps try to use it
|
|
11
|
+
if (!window.checkoutPaymentEvents) {
|
|
12
|
+
window.checkoutPaymentEvents = (() => {
|
|
13
|
+
const events = {};
|
|
14
|
+
return {
|
|
15
|
+
emit: (eventName, data) => {
|
|
16
|
+
if (!events[eventName]) {
|
|
17
|
+
events[eventName] = [];
|
|
18
|
+
}
|
|
19
|
+
events[eventName].forEach((handler) => {
|
|
20
|
+
try {
|
|
21
|
+
handler(data);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[CHECKOUT] Error in payment event handler:', error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
on: (eventName, handler) => {
|
|
28
|
+
if (!events[eventName]) {
|
|
29
|
+
events[eventName] = [];
|
|
30
|
+
}
|
|
31
|
+
events[eventName].push(handler);
|
|
32
|
+
},
|
|
33
|
+
off: (eventName, handler) => {
|
|
34
|
+
if (events[eventName]) {
|
|
35
|
+
events[eventName] = events[eventName].filter(h => h !== handler);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Initialize payment gateway apps registry globally (for payment gateway apps)
|
|
43
|
+
if (!window.paymentGatewayApps) {
|
|
44
|
+
window.paymentGatewayApps = (() => {
|
|
45
|
+
const apps = {};
|
|
46
|
+
return {
|
|
47
|
+
register: (appId, handlers) => {
|
|
48
|
+
if (!appId || !handlers || !handlers.handleCheckoutSubmit) {
|
|
49
|
+
console.error('[PAYMENT-GATEWAY] Invalid app registration:', appId);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
apps[appId] = handlers;
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
isGatewayMethod: (methodId) => {
|
|
56
|
+
return Object.keys(apps).some(appId =>
|
|
57
|
+
methodId === appId || methodId.toLowerCase() === appId.toLowerCase()
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
handleSubmit: async (methodId, checkoutToken) => {
|
|
61
|
+
const appId = Object.keys(apps).find(id =>
|
|
62
|
+
methodId === id || methodId.toLowerCase() === id.toLowerCase()
|
|
63
|
+
);
|
|
64
|
+
if (appId && apps[appId].handleCheckoutSubmit) {
|
|
65
|
+
return await apps[appId].handleCheckoutSubmit(methodId, checkoutToken);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`No payment gateway app found for method: ${methodId}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
})();
|
|
71
|
+
}
|
|
72
|
+
|
|
9
73
|
// Theme object to hold all functionality
|
|
10
74
|
const Theme = {
|
|
11
75
|
// Initialize all theme functionality
|
|
@@ -171,8 +235,12 @@
|
|
|
171
235
|
document.addEventListener('click', (e) => {
|
|
172
236
|
const addToCartBtn = e.target.closest('.add-to-cart-btn');
|
|
173
237
|
if (!addToCartBtn) return;
|
|
174
|
-
|
|
175
|
-
|
|
238
|
+
|
|
239
|
+
// Quick view modal has its own add-to-cart logic.
|
|
240
|
+
// Avoid double-handling through global delegation.
|
|
241
|
+
if (addToCartBtn.closest('#quick-view-modal')) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
176
244
|
|
|
177
245
|
// Check if this is a product card button (has a product-card ancestor)
|
|
178
246
|
const productCard = addToCartBtn.closest('.product-card');
|
|
@@ -185,16 +253,19 @@
|
|
|
185
253
|
// 1. productType != 0 (always show modal)
|
|
186
254
|
// 2. productType == 0 AND variants count > 0 (show modal for variant selection)
|
|
187
255
|
if (productType !== 0 || (productType === 0 && variantsCount > 0)) {
|
|
188
|
-
// Show modal
|
|
189
256
|
e.preventDefault();
|
|
190
257
|
e.stopPropagation();
|
|
191
|
-
console.log('[Theme] Showing modal - productType:', productType, 'variantsCount:', variantsCount);
|
|
192
258
|
this.showAddToCartModal(productCard, addToCartBtn);
|
|
193
|
-
|
|
194
|
-
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Type == 0 && variants == 0: direct add for simple products
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
e.stopPropagation();
|
|
265
|
+
const productId = addToCartBtn.getAttribute('data-product-id');
|
|
266
|
+
if (productId) {
|
|
267
|
+
this.addToCart(productId, 1);
|
|
195
268
|
}
|
|
196
|
-
// Type == 0 && variants == 0: Don't prevent default - let product-card's own handler work
|
|
197
|
-
// The product-card.liquid script will handle type 0 products with no variants directly
|
|
198
269
|
} else {
|
|
199
270
|
// Direct add for non-product-card buttons (e.g., product page)
|
|
200
271
|
e.preventDefault();
|
|
@@ -241,7 +312,7 @@
|
|
|
241
312
|
// Only update button state if not skipping (e.g., when called from modal)
|
|
242
313
|
let btn = null;
|
|
243
314
|
if (!skipButtonUpdate) {
|
|
244
|
-
btn = document.querySelector(
|
|
315
|
+
btn = document.querySelector(`.add-to-cart-btn[data-product-id="${productId}"]`);
|
|
245
316
|
if (btn) {
|
|
246
317
|
btn.disabled = true;
|
|
247
318
|
btn.innerHTML = '<span class="loading-spinner"></span> Adding...';
|
|
@@ -266,17 +337,13 @@
|
|
|
266
337
|
|
|
267
338
|
// Helper function to open login modal with fallbacks
|
|
268
339
|
const openLogin = () => {
|
|
269
|
-
console.log('[AddToCart] Attempting to open login modal');
|
|
270
340
|
if (this.openLoginModal && typeof this.openLoginModal === 'function') {
|
|
271
|
-
console.log('[AddToCart] Using this.openLoginModal');
|
|
272
341
|
this.openLoginModal();
|
|
273
342
|
return true;
|
|
274
343
|
} else if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
|
|
275
|
-
console.log('[AddToCart] Using window.Theme.openLoginModal');
|
|
276
344
|
window.Theme.openLoginModal();
|
|
277
345
|
return true;
|
|
278
346
|
} else {
|
|
279
|
-
console.log('[AddToCart] Using fallback: triggering login modal via data attribute');
|
|
280
347
|
// Fallback: trigger login modal via data attribute
|
|
281
348
|
const loginTrigger = document.querySelector('[data-login-modal-trigger]');
|
|
282
349
|
if (loginTrigger) {
|
|
@@ -291,9 +358,7 @@
|
|
|
291
358
|
|
|
292
359
|
// If response is HTML (error page), treat as authentication required for 404/401
|
|
293
360
|
if (isHtml && !response.ok) {
|
|
294
|
-
console.log('[AddToCart] HTML error page received, status:', response.status);
|
|
295
361
|
if (response.status === 404 || response.status === 401) {
|
|
296
|
-
console.log('[AddToCart] HTML error page with 404/401, opening login modal');
|
|
297
362
|
openLogin();
|
|
298
363
|
if (!skipButtonUpdate && btn) {
|
|
299
364
|
btn.innerHTML = 'Add to Cart';
|
|
@@ -310,7 +375,6 @@
|
|
|
310
375
|
} catch (parseError) {
|
|
311
376
|
// If JSON parsing fails and we have a 404/401, treat as auth required
|
|
312
377
|
if (!response.ok && (response.status === 404 || response.status === 401)) {
|
|
313
|
-
console.log('[AddToCart] JSON parse error with 404/401 status, opening login modal');
|
|
314
378
|
openLogin();
|
|
315
379
|
if (!skipButtonUpdate && btn) {
|
|
316
380
|
btn.innerHTML = 'Add to Cart';
|
|
@@ -322,14 +386,8 @@
|
|
|
322
386
|
throw parseError;
|
|
323
387
|
}
|
|
324
388
|
|
|
325
|
-
// Debug logging
|
|
326
|
-
console.log('[AddToCart] Response status:', response.status, 'Response OK:', response.ok);
|
|
327
|
-
console.log('[AddToCart] Response data:', data);
|
|
328
|
-
console.log('[AddToCart] requiresAuth:', data.requiresAuth, 'openLoginModal exists:', !!this.openLoginModal);
|
|
329
|
-
|
|
330
389
|
// Check if response indicates authentication is required (check both status and data)
|
|
331
390
|
if ((!response.ok || !data.success) && data.requiresAuth) {
|
|
332
|
-
console.log('[AddToCart] Authentication required, opening login modal');
|
|
333
391
|
openLogin();
|
|
334
392
|
// Reset button state
|
|
335
393
|
if (!skipButtonUpdate && btn) {
|
|
@@ -341,7 +399,6 @@
|
|
|
341
399
|
|
|
342
400
|
// Also check for 401 or 404 status code even if requiresAuth flag is not set
|
|
343
401
|
if (!response.ok && (response.status === 401 || response.status === 404)) {
|
|
344
|
-
console.log('[AddToCart] 401/404 status detected, opening login modal');
|
|
345
402
|
openLogin();
|
|
346
403
|
// Reset button state
|
|
347
404
|
if (!skipButtonUpdate && btn) {
|
|
@@ -352,10 +409,26 @@
|
|
|
352
409
|
}
|
|
353
410
|
|
|
354
411
|
if (data.success) {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
412
|
+
// Update cart badge immediately using quantity API for instant feedback
|
|
413
|
+
// Then fetch full cart data for UI updates (total, etc.)
|
|
414
|
+
try {
|
|
415
|
+
// First, update badge immediately using CartManager (uses /carts/quantity API)
|
|
416
|
+
if (window.CartManager && typeof window.CartManager.getCartCount === 'function') {
|
|
417
|
+
const cartCount = await window.CartManager.getCartCount(true);
|
|
418
|
+
// Update badge immediately
|
|
419
|
+
if (window.CartManager.dispatchCartUpdated) {
|
|
420
|
+
window.CartManager.dispatchCartUpdated({ itemCount: cartCount });
|
|
421
|
+
}
|
|
422
|
+
// Store count for later use
|
|
423
|
+
data.data = data.data || {};
|
|
424
|
+
data.data.itemCount = cartCount;
|
|
425
|
+
}
|
|
426
|
+
} catch (countError) {
|
|
427
|
+
// Silently handle cart count fetch failure
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Then fetch full cart data for UI updates (total, items, etc.)
|
|
357
431
|
try {
|
|
358
|
-
// Fetch full cart data to get updated count, total, and all cart information
|
|
359
432
|
const cartResponse = await fetch('/webstoreapi/carts', {
|
|
360
433
|
method: 'GET',
|
|
361
434
|
credentials: 'same-origin',
|
|
@@ -368,7 +441,7 @@
|
|
|
368
441
|
// Use the full cart data from the API response (includes total, itemCount, etc.)
|
|
369
442
|
data.data = cartData.data;
|
|
370
443
|
|
|
371
|
-
// Update cart count badge
|
|
444
|
+
// Update cart count badge again with full cart data (ensures consistency)
|
|
372
445
|
if (window.CartManager && typeof window.CartManager.dispatchCartUpdated === 'function') {
|
|
373
446
|
const cartCount = cartData.data.itemCount || 0;
|
|
374
447
|
window.CartManager.dispatchCartUpdated({
|
|
@@ -379,26 +452,14 @@
|
|
|
379
452
|
}
|
|
380
453
|
}
|
|
381
454
|
} catch (e) {
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!data.data.total && data.data.total !== 0) {
|
|
391
|
-
// Keep the existing total from original response or default to 0
|
|
392
|
-
data.data.total = data.data.total || 0;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
} catch (countError) {
|
|
396
|
-
console.warn('Failed to fetch cart count after add:', countError);
|
|
397
|
-
// Use itemCount from original response if available
|
|
398
|
-
if (data.data && (data.data.itemCount === undefined || !data.data.items)) {
|
|
399
|
-
data.data = data.data || {};
|
|
400
|
-
data.data.itemCount = data.data.itemCount || (data.data.items ? data.data.items.length : 0);
|
|
401
|
-
}
|
|
455
|
+
// If full cart fetch fails, we already have the count from quantity API above
|
|
456
|
+
// Ensure we have at least basic data structure
|
|
457
|
+
if (!data.data) {
|
|
458
|
+
data.data = {};
|
|
459
|
+
}
|
|
460
|
+
// If we don't have total in response, preserve whatever was in the original response
|
|
461
|
+
if (!data.data.total && data.data.total !== 0) {
|
|
462
|
+
data.data.total = data.data.total || 0;
|
|
402
463
|
}
|
|
403
464
|
}
|
|
404
465
|
// Update cart UI with the latest data (includes total and count)
|
|
@@ -416,27 +477,26 @@
|
|
|
416
477
|
}, 2000);
|
|
417
478
|
}
|
|
418
479
|
} else {
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
480
|
+
// Helper function to open login modal with fallbacks
|
|
481
|
+
const openLogin = () => {
|
|
482
|
+
if (this.openLoginModal && typeof this.openLoginModal === 'function') {
|
|
483
|
+
this.openLoginModal();
|
|
484
|
+
return true;
|
|
485
|
+
} else if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
|
|
486
|
+
window.Theme.openLoginModal();
|
|
487
|
+
return true;
|
|
488
|
+
} else {
|
|
489
|
+
const loginTrigger = document.querySelector('[data-login-modal-trigger]');
|
|
490
|
+
if (loginTrigger) {
|
|
491
|
+
loginTrigger.click();
|
|
430
492
|
return true;
|
|
431
|
-
} else {
|
|
432
|
-
const loginTrigger = document.querySelector('[data-login-modal-trigger]');
|
|
433
|
-
if (loginTrigger) {
|
|
434
|
-
loginTrigger.click();
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
return false;
|
|
438
493
|
}
|
|
439
|
-
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Check if authentication is required
|
|
499
|
+
if (data.requiresAuth) {
|
|
440
500
|
openLogin();
|
|
441
501
|
// Reset button state
|
|
442
502
|
if (!skipButtonUpdate && btn) {
|
|
@@ -445,6 +505,21 @@
|
|
|
445
505
|
}
|
|
446
506
|
return;
|
|
447
507
|
}
|
|
508
|
+
|
|
509
|
+
// If not explicitly marked as requiresAuth, check if user is logged in
|
|
510
|
+
// If not logged in, open login modal instead of showing error
|
|
511
|
+
const isLoggedIn = document.cookie.includes('O2VENDIsUserLoggedin=true') ||
|
|
512
|
+
document.cookie.includes('O2VENDUserToken=');
|
|
513
|
+
if (!isLoggedIn) {
|
|
514
|
+
openLogin();
|
|
515
|
+
// Reset button state
|
|
516
|
+
if (!skipButtonUpdate && btn) {
|
|
517
|
+
btn.innerHTML = 'Add to Cart';
|
|
518
|
+
btn.disabled = false;
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
448
523
|
throw new Error(data.error || data.message || 'Failed to add product to cart');
|
|
449
524
|
}
|
|
450
525
|
} catch (error) {
|
|
@@ -475,11 +550,26 @@
|
|
|
475
550
|
error.message.includes('Please sign in') ||
|
|
476
551
|
error.message.includes('unauthorized')
|
|
477
552
|
)) {
|
|
478
|
-
console.log('[AddToCart] Authentication required detected in error message');
|
|
479
553
|
openLogin();
|
|
480
554
|
// Reset button state (only if not skipping updates)
|
|
481
555
|
if (!skipButtonUpdate) {
|
|
482
|
-
const btn = document.querySelector(
|
|
556
|
+
const btn = document.querySelector(`.add-to-cart-btn[data-product-id="${productId}"]`);
|
|
557
|
+
if (btn) {
|
|
558
|
+
btn.innerHTML = 'Add to Cart';
|
|
559
|
+
btn.disabled = false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Check if user is not logged in - if so, open login modal instead of showing error
|
|
566
|
+
const isLoggedIn = document.cookie.includes('O2VENDIsUserLoggedin=true') ||
|
|
567
|
+
document.cookie.includes('O2VENDUserToken=');
|
|
568
|
+
if (!isLoggedIn) {
|
|
569
|
+
openLogin();
|
|
570
|
+
// Reset button state (only if not skipping updates)
|
|
571
|
+
if (!skipButtonUpdate) {
|
|
572
|
+
const btn = document.querySelector(`.add-to-cart-btn[data-product-id="${productId}"]`);
|
|
483
573
|
if (btn) {
|
|
484
574
|
btn.innerHTML = 'Add to Cart';
|
|
485
575
|
btn.disabled = false;
|
|
@@ -488,11 +578,11 @@
|
|
|
488
578
|
return;
|
|
489
579
|
}
|
|
490
580
|
|
|
491
|
-
this.showNotification(error.message || '
|
|
581
|
+
this.showNotification(error.message || 'Failed to add product to cart. Please try again.', 'error');
|
|
492
582
|
|
|
493
583
|
// Reset button state (only if not skipping updates)
|
|
494
584
|
if (!skipButtonUpdate) {
|
|
495
|
-
const btn = document.querySelector(
|
|
585
|
+
const btn = document.querySelector(`.add-to-cart-btn[data-product-id="${productId}"]`);
|
|
496
586
|
if (btn) {
|
|
497
587
|
btn.innerHTML = 'Add to Cart';
|
|
498
588
|
btn.disabled = false;
|
|
@@ -577,8 +667,6 @@
|
|
|
577
667
|
0;
|
|
578
668
|
}
|
|
579
669
|
|
|
580
|
-
console.log('[Theme] updateCartUI called with count:', count, 'cart:', cart);
|
|
581
|
-
|
|
582
670
|
// Use CartManager as single source of truth for all cart count updates
|
|
583
671
|
// This ensures header badge and drawer badge stay in sync
|
|
584
672
|
// Both now use [data-cart-count] attribute
|
|
@@ -649,23 +737,40 @@
|
|
|
649
737
|
// Product actions (quick view, wishlist, etc.)
|
|
650
738
|
initProductActions() {
|
|
651
739
|
// Quick view functionality
|
|
652
|
-
|
|
740
|
+
// Guard against attaching duplicate listeners when async sections re-initialize
|
|
741
|
+
const quickViewBtns = document.querySelectorAll('.quick-view-btn:not([data-quick-view-initialized="true"])');
|
|
653
742
|
|
|
654
743
|
quickViewBtns.forEach(btn => {
|
|
744
|
+
btn.setAttribute('data-quick-view-initialized', 'true');
|
|
655
745
|
btn.addEventListener('click', (e) => {
|
|
656
746
|
e.preventDefault();
|
|
657
747
|
const productId = btn.getAttribute('data-product-id');
|
|
748
|
+
const productCard = btn.closest('.product-card') || btn.closest('[data-product-id]');
|
|
749
|
+
|
|
750
|
+
if (productCard) {
|
|
751
|
+
const rawType = productCard.dataset ? productCard.dataset.productType : '';
|
|
752
|
+
const parsedType = rawType !== '' ? parseInt(rawType, 10) : NaN;
|
|
753
|
+
const rawVariantsCount = productCard.dataset ? productCard.dataset.variantsCount : '';
|
|
754
|
+
const parsedVariantsCount = rawVariantsCount !== '' ? parseInt(rawVariantsCount, 10) : NaN;
|
|
755
|
+
|
|
756
|
+
if (!Number.isNaN(parsedType) && !Number.isNaN(parsedVariantsCount) && (parsedType !== 0 || parsedVariantsCount > 0)) {
|
|
757
|
+
this.showAddToCartModal(productCard, btn);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
658
761
|
|
|
659
762
|
if (productId) {
|
|
660
|
-
this.openQuickView(productId);
|
|
763
|
+
this.openQuickView(productId, productCard);
|
|
661
764
|
}
|
|
662
765
|
});
|
|
663
766
|
});
|
|
664
767
|
|
|
665
768
|
// Wishlist functionality
|
|
666
|
-
|
|
769
|
+
// Same pattern to avoid duplicate listeners on dynamically loaded content
|
|
770
|
+
const wishlistBtns = document.querySelectorAll('.wishlist-btn:not([data-wishlist-initialized="true"])');
|
|
667
771
|
|
|
668
772
|
wishlistBtns.forEach(btn => {
|
|
773
|
+
btn.setAttribute('data-wishlist-initialized', 'true');
|
|
669
774
|
btn.addEventListener('click', (e) => {
|
|
670
775
|
e.preventDefault();
|
|
671
776
|
const productId = btn.getAttribute('data-product-id');
|
|
@@ -677,13 +782,118 @@
|
|
|
677
782
|
});
|
|
678
783
|
},
|
|
679
784
|
|
|
680
|
-
async openQuickView(productId) {
|
|
785
|
+
async openQuickView(productId, productCard = null) {
|
|
786
|
+
let domFallbackProduct = null;
|
|
681
787
|
try {
|
|
682
|
-
//
|
|
788
|
+
// First, try to build product data from the DOM card itself.
|
|
789
|
+
// This avoids unnecessary API calls and ensures consistency with what the user sees.
|
|
790
|
+
if (productCard) {
|
|
791
|
+
const titleEl = productCard.querySelector('.product-title, .product-card-title, .product-card__title-link, .product-card__title, .product-title-link');
|
|
792
|
+
const imageEl = productCard.querySelector('.product-card__image--primary, .product-card__image, .product-image, img');
|
|
793
|
+
const priceEl = productCard.querySelector('.product-price-current, .product-card__price-current, .price-current, .product-price [data-price-current]');
|
|
794
|
+
|
|
795
|
+
const title =
|
|
796
|
+
(productCard.dataset && productCard.dataset.title) ||
|
|
797
|
+
(titleEl ? titleEl.textContent.trim() : '');
|
|
798
|
+
const imageSrc =
|
|
799
|
+
(productCard.dataset && productCard.dataset.image) ||
|
|
800
|
+
(imageEl ? imageEl.getAttribute('src') || imageEl.src : '');
|
|
801
|
+
const priceText =
|
|
802
|
+
(productCard.dataset && productCard.dataset.priceString) ||
|
|
803
|
+
(priceEl ? priceEl.textContent.trim() : '');
|
|
804
|
+
const linkEl = productCard.querySelector('.product-card__link, .product-card__title-link, a[href]');
|
|
805
|
+
const href = linkEl ? linkEl.getAttribute('href') : '';
|
|
806
|
+
const productUrl =
|
|
807
|
+
(productCard.dataset && (productCard.dataset.productUrl || productCard.dataset.url)) ||
|
|
808
|
+
href ||
|
|
809
|
+
'';
|
|
810
|
+
const rawProductType = productCard.dataset ? productCard.dataset.productType : '';
|
|
811
|
+
const parsedProductType = rawProductType !== '' ? parseInt(rawProductType, 10) : NaN;
|
|
812
|
+
const productType = Number.isNaN(parsedProductType) ? 0 : parsedProductType;
|
|
813
|
+
const rawVariantsCount = productCard.dataset ? productCard.dataset.variantsCount : '';
|
|
814
|
+
const parsedVariantsCount = rawVariantsCount !== '' ? parseInt(rawVariantsCount, 10) : NaN;
|
|
815
|
+
const variantsCount = Number.isNaN(parsedVariantsCount) ? 0 : parsedVariantsCount;
|
|
816
|
+
const hasProductTypeMetadata = !Number.isNaN(parsedProductType);
|
|
817
|
+
const hasVariantMetadata = !Number.isNaN(parsedVariantsCount);
|
|
818
|
+
const shouldFetchFullProduct = true;
|
|
819
|
+
const baseProductId = (productCard.dataset && productCard.dataset.baseProductId) || productId;
|
|
820
|
+
const availability = productCard.dataset && productCard.dataset.availability
|
|
821
|
+
? String(productCard.dataset.availability).toLowerCase()
|
|
822
|
+
: '';
|
|
823
|
+
const isInStock = availability === 'in-stock' || availability === 'available';
|
|
824
|
+
const showCallForPricing = productCard && productCard.dataset
|
|
825
|
+
? (productCard.dataset.showCallForPricing === 'true' || productCard.dataset.showCallForPricing === '1')
|
|
826
|
+
: false;
|
|
827
|
+
|
|
828
|
+
const rawPrice = productCard.dataset && productCard.dataset.price ? parseFloat(productCard.dataset.price) : NaN;
|
|
829
|
+
let numericPrice = Number.isNaN(rawPrice) ? 0 : rawPrice;
|
|
830
|
+
|
|
831
|
+
if (!numericPrice && priceText) {
|
|
832
|
+
const normalized = priceText.replace(/[^0-9.,-]/g, '').replace(',', '');
|
|
833
|
+
const parsed = parseFloat(normalized);
|
|
834
|
+
if (!Number.isNaN(parsed)) {
|
|
835
|
+
numericPrice = parsed;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const isCallForPricingFromText = /call\s*for\s*pricing/i.test(priceText || '');
|
|
839
|
+
const resolvedShowCallForPricing = showCallForPricing || isCallForPricingFromText;
|
|
840
|
+
|
|
841
|
+
if (title || imageSrc || numericPrice || priceText) {
|
|
842
|
+
domFallbackProduct = {
|
|
843
|
+
productId: productId,
|
|
844
|
+
id: productId,
|
|
845
|
+
title: title || '',
|
|
846
|
+
name: title || '',
|
|
847
|
+
url: productUrl || '',
|
|
848
|
+
productUrl: productUrl || '',
|
|
849
|
+
productType: Number.isNaN(productType) ? 0 : productType,
|
|
850
|
+
variantsCount: Number.isNaN(variantsCount) ? 0 : variantsCount,
|
|
851
|
+
baseProductId: baseProductId,
|
|
852
|
+
availability: availability,
|
|
853
|
+
inStock: isInStock,
|
|
854
|
+
available: isInStock,
|
|
855
|
+
images: imageSrc ? [imageSrc] : [],
|
|
856
|
+
prices: {
|
|
857
|
+
price: numericPrice || 0,
|
|
858
|
+
priceString: priceText || ''
|
|
859
|
+
},
|
|
860
|
+
showCallForPricing: resolvedShowCallForPricing,
|
|
861
|
+
description: ''
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
if (!shouldFetchFullProduct) {
|
|
865
|
+
this.showQuickViewModal(domFallbackProduct);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Fallback: Fetch full product data from API if DOM data is insufficient
|
|
683
872
|
this.showNotification('Loading product...', 'info');
|
|
684
873
|
|
|
685
|
-
const response = await fetch(`/webstoreapi/products/${productId}
|
|
686
|
-
|
|
874
|
+
const response = await fetch(`/webstoreapi/products/${productId}`, {
|
|
875
|
+
method: 'GET',
|
|
876
|
+
headers: {
|
|
877
|
+
'Content-Type': 'application/json',
|
|
878
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
if (!response.ok) {
|
|
883
|
+
// Try to log structured error if present
|
|
884
|
+
let errorPayload = null;
|
|
885
|
+
try {
|
|
886
|
+
errorPayload = await response.json();
|
|
887
|
+
} catch (_) {
|
|
888
|
+
// ignore JSON parse errors here
|
|
889
|
+
}
|
|
890
|
+
console.error('[QuickView] API response not OK:', response.status, errorPayload || await response.text());
|
|
891
|
+
throw new Error(errorPayload?.error || `Failed to load product (${response.status})`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const result = await response.json();
|
|
895
|
+
// Endpoint returns: { success: true, data: product }
|
|
896
|
+
const product = result && result.success ? result.data : null;
|
|
687
897
|
|
|
688
898
|
if (product) {
|
|
689
899
|
this.showQuickViewModal(product);
|
|
@@ -691,12 +901,68 @@
|
|
|
691
901
|
throw new Error('Product not found');
|
|
692
902
|
}
|
|
693
903
|
} catch (error) {
|
|
694
|
-
console.error('Error loading product:', error);
|
|
904
|
+
console.error('[QuickView] Error loading product:', error);
|
|
905
|
+
if (domFallbackProduct) {
|
|
906
|
+
this.showQuickViewModal(domFallbackProduct);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
695
909
|
this.showNotification('Error loading product', 'error');
|
|
696
910
|
}
|
|
697
911
|
},
|
|
698
912
|
|
|
699
913
|
showQuickViewModal(product) {
|
|
914
|
+
// Ensure only a single quick view modal exists at a time
|
|
915
|
+
this.closeQuickViewModal();
|
|
916
|
+
|
|
917
|
+
const productTitle = product.title || product.name || product.slug || 'Product';
|
|
918
|
+
const productImage = this.getImageUrls(product)[0] || '';
|
|
919
|
+
const rawUrl =
|
|
920
|
+
product.url ||
|
|
921
|
+
product.Url ||
|
|
922
|
+
product.productUrl ||
|
|
923
|
+
product.link ||
|
|
924
|
+
product.Link ||
|
|
925
|
+
product.slug ||
|
|
926
|
+
product.id ||
|
|
927
|
+
product.productId ||
|
|
928
|
+
'';
|
|
929
|
+
const normalizedUrl = String(rawUrl || '').trim();
|
|
930
|
+
const productUrl = !normalizedUrl
|
|
931
|
+
? '#'
|
|
932
|
+
: (/^https?:\/\//i.test(normalizedUrl)
|
|
933
|
+
? normalizedUrl
|
|
934
|
+
: `/${normalizedUrl.replace(/^\/+/, '')}`);
|
|
935
|
+
const fallbackImage =
|
|
936
|
+
'data:image/svg+xml;utf8,' +
|
|
937
|
+
encodeURIComponent(
|
|
938
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>'
|
|
939
|
+
);
|
|
940
|
+
const rawPriceString = product.prices?.priceString || product.priceString || '';
|
|
941
|
+
const rawPriceValue = product.prices?.price ?? product.price ?? null;
|
|
942
|
+
const showCallForPricing = this.isCallForPricingEnabled(product);
|
|
943
|
+
const displayPrice = showCallForPricing
|
|
944
|
+
? 'Call for pricing'
|
|
945
|
+
: (rawPriceString
|
|
946
|
+
? rawPriceString
|
|
947
|
+
: (rawPriceValue !== null && rawPriceValue !== undefined
|
|
948
|
+
? this.formatMoneyProductCard(rawPriceValue)
|
|
949
|
+
: ''));
|
|
950
|
+
const rawType = product.productType ?? product.type ?? 0;
|
|
951
|
+
const productType = Number.isNaN(Number(rawType)) ? 0 : Number(rawType);
|
|
952
|
+
const apiVariants = Array.isArray(product.variations)
|
|
953
|
+
? product.variations
|
|
954
|
+
: (Array.isArray(product.variants) ? product.variants : []);
|
|
955
|
+
const rawVariantCount = product.variantsCount ?? product.variationCount ?? apiVariants.length ?? 0;
|
|
956
|
+
const variantsCount = Number.isNaN(Number(rawVariantCount)) ? 0 : Number(rawVariantCount);
|
|
957
|
+
const hasVariants = variantsCount > 0;
|
|
958
|
+
const rawAvailability = (product.availability || product.stockStatus || '').toString().toLowerCase();
|
|
959
|
+
const isInStock = (product.inStock !== false && product.available !== false) &&
|
|
960
|
+
(product.stockQuantity === undefined || product.stockQuantity === null || Number(product.stockQuantity) > 0) &&
|
|
961
|
+
rawAvailability !== 'out-of-stock' && rawAvailability !== 'sold-out';
|
|
962
|
+
const availabilityText = isInStock ? 'In Stock' : 'Out of Stock';
|
|
963
|
+
const addToCartLabel = isInStock ? 'Add to Cart' : 'Out of Stock';
|
|
964
|
+
const quickDescription = product.shortDescription || product.short_description || product.description || '';
|
|
965
|
+
|
|
700
966
|
// Create modal HTML
|
|
701
967
|
const modalHTML = `
|
|
702
968
|
<div class="quick-view-modal" id="quick-view-modal">
|
|
@@ -709,17 +975,17 @@
|
|
|
709
975
|
</button>
|
|
710
976
|
<div class="quick-view-body">
|
|
711
977
|
<div class="quick-view-image">
|
|
712
|
-
<img src="${
|
|
978
|
+
<img src="${productImage || fallbackImage}" alt="${productTitle}">
|
|
713
979
|
</div>
|
|
714
980
|
<div class="quick-view-info">
|
|
715
|
-
<h2 class="quick-view-title">${
|
|
716
|
-
<div class="quick-view-price">${
|
|
717
|
-
<div class="quick-view-description">${
|
|
981
|
+
<h2 class="quick-view-title">${productTitle}</h2>
|
|
982
|
+
<div class="quick-view-price">${displayPrice}</div>
|
|
983
|
+
<div class="quick-view-description">${quickDescription}</div>
|
|
718
984
|
<div class="quick-view-actions">
|
|
719
|
-
<button class="btn btn-primary add-to-cart-btn" data-product-id="${product.productId || product.id}">
|
|
720
|
-
|
|
985
|
+
<button class="btn btn-primary add-to-cart-btn" data-product-id="${product.productId || product.id}" ${!isInStock ? 'disabled' : ''}>
|
|
986
|
+
${addToCartLabel}
|
|
721
987
|
</button>
|
|
722
|
-
<a href="
|
|
988
|
+
<a href="${productUrl}" class="btn btn-outline quick-view-view-details">
|
|
723
989
|
View Details
|
|
724
990
|
</a>
|
|
725
991
|
</div>
|
|
@@ -749,11 +1015,32 @@
|
|
|
749
1015
|
|
|
750
1016
|
// Add to cart functionality
|
|
751
1017
|
const addToCartBtn = modal.querySelector('.add-to-cart-btn');
|
|
752
|
-
addToCartBtn.addEventListener('click', (e) => {
|
|
1018
|
+
addToCartBtn.addEventListener('click', async (e) => {
|
|
753
1019
|
e.preventDefault();
|
|
754
|
-
|
|
1020
|
+
e.stopPropagation();
|
|
1021
|
+
|
|
1022
|
+
if (!isInStock) {
|
|
1023
|
+
this.showNotification('This product is currently out of stock', 'warning');
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const baseProductId = product.baseProductId || product.productId || product.id;
|
|
755
1028
|
this.closeQuickViewModal();
|
|
1029
|
+
|
|
1030
|
+
if (productType !== 0 || variantsCount > 0) {
|
|
1031
|
+
this.showAddToCartModal(baseProductId, addToCartBtn, product);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
await this.addToCart(product.productId || product.id, 1, true);
|
|
756
1036
|
});
|
|
1037
|
+
|
|
1038
|
+
const viewDetailsBtn = modal.querySelector('.quick-view-view-details');
|
|
1039
|
+
if (viewDetailsBtn) {
|
|
1040
|
+
viewDetailsBtn.addEventListener('click', () => {
|
|
1041
|
+
this.closeQuickViewModal();
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
757
1044
|
},
|
|
758
1045
|
|
|
759
1046
|
closeQuickViewModal() {
|
|
@@ -765,7 +1052,39 @@
|
|
|
765
1052
|
},
|
|
766
1053
|
|
|
767
1054
|
async toggleWishlist(productId, btn) {
|
|
1055
|
+
const openLogin = () => {
|
|
1056
|
+
if (this.openLoginModal && typeof this.openLoginModal === 'function') {
|
|
1057
|
+
this.openLoginModal();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
|
|
1061
|
+
window.Theme.openLoginModal();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const loginTrigger = document.querySelector('[data-login-modal-trigger]');
|
|
1065
|
+
if (loginTrigger) loginTrigger.click();
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
const isAuthError = (message = '') => {
|
|
1069
|
+
const lower = String(message).toLowerCase();
|
|
1070
|
+
return (
|
|
1071
|
+
lower.includes('auth') ||
|
|
1072
|
+
lower.includes('sign in') ||
|
|
1073
|
+
lower.includes('signin') ||
|
|
1074
|
+
lower.includes('login') ||
|
|
1075
|
+
lower.includes('unauthorized')
|
|
1076
|
+
);
|
|
1077
|
+
};
|
|
1078
|
+
|
|
768
1079
|
try {
|
|
1080
|
+
// Avoid avoidable API call when user is clearly not logged in.
|
|
1081
|
+
const isLoggedIn = document.cookie.includes('O2VENDIsUserLoggedin=true') ||
|
|
1082
|
+
document.cookie.includes('O2VENDUserToken=');
|
|
1083
|
+
if (!isLoggedIn) {
|
|
1084
|
+
openLogin();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
769
1088
|
const isInWishlist = btn.classList.contains('in-wishlist');
|
|
770
1089
|
|
|
771
1090
|
const response = await fetch('/webstoreapi/wishlist/toggle', {
|
|
@@ -778,8 +1097,29 @@
|
|
|
778
1097
|
productId: productId
|
|
779
1098
|
})
|
|
780
1099
|
});
|
|
1100
|
+
const contentType = response.headers.get('content-type') || '';
|
|
1101
|
+
const isHtml = contentType.includes('text/html');
|
|
781
1102
|
|
|
782
|
-
|
|
1103
|
+
if (!response.ok && isHtml && (response.status === 401 || response.status === 404)) {
|
|
1104
|
+
openLogin();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
let data;
|
|
1109
|
+
try {
|
|
1110
|
+
data = await response.json();
|
|
1111
|
+
} catch (parseError) {
|
|
1112
|
+
if (!response.ok && (response.status === 401 || response.status === 404)) {
|
|
1113
|
+
openLogin();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
throw parseError;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if ((!response.ok || !data.success) && (data.requiresAuth || response.status === 401 || response.status === 404 || isAuthError(data.error || data.message))) {
|
|
1120
|
+
openLogin();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
783
1123
|
|
|
784
1124
|
if (data.success) {
|
|
785
1125
|
if (isInWishlist) {
|
|
@@ -796,6 +1136,10 @@
|
|
|
796
1136
|
}
|
|
797
1137
|
} catch (error) {
|
|
798
1138
|
console.error('Error updating wishlist:', error);
|
|
1139
|
+
if (isAuthError(error && error.message)) {
|
|
1140
|
+
openLogin();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
799
1143
|
this.showNotification('Error updating wishlist', 'error');
|
|
800
1144
|
}
|
|
801
1145
|
},
|
|
@@ -1064,28 +1408,84 @@
|
|
|
1064
1408
|
const formatted = num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
1065
1409
|
return currencySymbol + formatted;
|
|
1066
1410
|
},
|
|
1411
|
+
|
|
1412
|
+
isCallForPricingEnabled(entity) {
|
|
1413
|
+
if (!entity || typeof entity !== 'object') return false;
|
|
1414
|
+
const value = entity.showCallForPricing ??
|
|
1415
|
+
entity.ShowCallForPricing ??
|
|
1416
|
+
entity.isCallForPricing ??
|
|
1417
|
+
entity.IsCallForPricing;
|
|
1418
|
+
return value === true || value === 'true' || value === 1 || value === '1';
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1421
|
+
// Normalize mixed image payloads (string/object) to a usable URL.
|
|
1422
|
+
resolveImageUrl(image) {
|
|
1423
|
+
if (!image) return '';
|
|
1424
|
+
if (typeof image === 'string') return image.trim();
|
|
1425
|
+
if (typeof image !== 'object') return '';
|
|
1426
|
+
return (
|
|
1427
|
+
image.url ||
|
|
1428
|
+
image.Url ||
|
|
1429
|
+
image.src ||
|
|
1430
|
+
image.Src ||
|
|
1431
|
+
image.imageUrl ||
|
|
1432
|
+
image.ImageUrl ||
|
|
1433
|
+
image.thumbnailImage1?.url ||
|
|
1434
|
+
image.thumbnailImage1?.Url ||
|
|
1435
|
+
image.ThumbnailImage1?.url ||
|
|
1436
|
+
image.ThumbnailImage1?.Url ||
|
|
1437
|
+
''
|
|
1438
|
+
);
|
|
1439
|
+
},
|
|
1440
|
+
|
|
1441
|
+
// Collect all possible image URLs from product/variant payload shapes.
|
|
1442
|
+
getImageUrls(entity) {
|
|
1443
|
+
if (!entity || typeof entity !== 'object') return [];
|
|
1444
|
+
const urls = [];
|
|
1445
|
+
const addUrl = (value) => {
|
|
1446
|
+
const url = this.resolveImageUrl(value);
|
|
1447
|
+
if (url && !urls.includes(url)) urls.push(url);
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
if (Array.isArray(entity.images)) {
|
|
1451
|
+
entity.images.forEach(addUrl);
|
|
1452
|
+
}
|
|
1453
|
+
addUrl(entity.thumbnailImage1);
|
|
1454
|
+
addUrl(entity.ThumbnailImage1);
|
|
1455
|
+
addUrl(entity.thumbnailImage);
|
|
1456
|
+
addUrl(entity.ThumbnailImage);
|
|
1457
|
+
addUrl(entity.imageUrl);
|
|
1458
|
+
addUrl(entity.ImageUrl);
|
|
1459
|
+
addUrl(entity.image);
|
|
1460
|
+
|
|
1461
|
+
return urls;
|
|
1462
|
+
},
|
|
1067
1463
|
|
|
1068
1464
|
// Show add to cart modal for product cards
|
|
1069
|
-
async showAddToCartModal(productCard, addToCartBtn) {
|
|
1465
|
+
async showAddToCartModal(productCard, addToCartBtn, productData = null) {
|
|
1070
1466
|
const modal = document.getElementById('add-to-cart-modal');
|
|
1071
1467
|
if (!modal) {
|
|
1072
1468
|
console.error('[AddToCartModal] Modal element not found');
|
|
1073
1469
|
return;
|
|
1074
1470
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const baseProductId = productCard.dataset.productId;
|
|
1471
|
+
|
|
1472
|
+
const isElement = productCard && productCard.nodeType === 1;
|
|
1473
|
+
const baseProductId = isElement ? productCard.dataset.productId : String(productCard || '');
|
|
1474
|
+
|
|
1080
1475
|
// Support both product-card classes and generic product classes
|
|
1081
|
-
const productTitle =
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1476
|
+
const productTitle = isElement
|
|
1477
|
+
? (productCard.querySelector('.product-card__title-link')?.textContent?.trim() ||
|
|
1478
|
+
productCard.querySelector('.product-card__title')?.textContent?.trim() ||
|
|
1479
|
+
productCard.querySelector('.product-title-link')?.textContent?.trim() ||
|
|
1480
|
+
productCard.querySelector('.product-title')?.textContent?.trim() ||
|
|
1481
|
+
'')
|
|
1482
|
+
: (productData?.title || productData?.name || productData?.slug || '');
|
|
1483
|
+
const productImage = isElement
|
|
1484
|
+
? (productCard.querySelector('.product-card__image--primary') ||
|
|
1485
|
+
productCard.querySelector('.product-card__image') ||
|
|
1486
|
+
productCard.querySelector('.product-image'))
|
|
1487
|
+
: null;
|
|
1488
|
+
const baseImageSrc = productImage?.src || this.getImageUrls(productData || {})[0] || '';
|
|
1089
1489
|
|
|
1090
1490
|
// Fetch full product data using getProductById API endpoint (routes/api.js:3455)
|
|
1091
1491
|
// This endpoint calls req.apiClient.getProductById() to get complete product details
|
|
@@ -1100,12 +1500,10 @@
|
|
|
1100
1500
|
});
|
|
1101
1501
|
if (response.ok) {
|
|
1102
1502
|
const result = await response.json();
|
|
1103
|
-
console.log('[AddToCartModal] Product API response:', result);
|
|
1104
1503
|
|
|
1105
1504
|
// Endpoint returns: { success: true, data: product }
|
|
1106
1505
|
if (result.success && result.data) {
|
|
1107
1506
|
fullProductData = result.data;
|
|
1108
|
-
console.log('[AddToCartModal] Full product data retrieved:', fullProductData);
|
|
1109
1507
|
|
|
1110
1508
|
// If product has combinations or subscriptions, redirect to product detail page
|
|
1111
1509
|
const hasCombinations = fullProductData.combinations && fullProductData.combinations.length > 0;
|
|
@@ -1116,8 +1514,6 @@
|
|
|
1116
1514
|
window.location.href = `/${productSlug}`;
|
|
1117
1515
|
return;
|
|
1118
1516
|
}
|
|
1119
|
-
} else {
|
|
1120
|
-
console.warn('[AddToCartModal] API response missing success or data:', result);
|
|
1121
1517
|
}
|
|
1122
1518
|
} else {
|
|
1123
1519
|
// Response not OK - try to parse error
|
|
@@ -1142,22 +1538,9 @@
|
|
|
1142
1538
|
// API might return either 'variants' or 'variations' - handle both
|
|
1143
1539
|
const apiVariants = fullProductData?.variants || fullProductData?.variations || null;
|
|
1144
1540
|
if (fullProductData && apiVariants && apiVariants.length > 0) {
|
|
1145
|
-
console.log('[AddToCartModal] Using variant data from API response, variant count:', apiVariants.length);
|
|
1146
1541
|
// Transform API variants to match expected format (same as product-card JSON structure)
|
|
1147
1542
|
const transformedVariants = apiVariants.map(variant => {
|
|
1148
|
-
|
|
1149
|
-
const imageUrls = [];
|
|
1150
|
-
if (variant.thumbnailImage1?.url) {
|
|
1151
|
-
imageUrls.push(variant.thumbnailImage1.url);
|
|
1152
|
-
} else if (variant.ThumbnailImage1?.Url) {
|
|
1153
|
-
imageUrls.push(variant.ThumbnailImage1.Url);
|
|
1154
|
-
}
|
|
1155
|
-
if (variant.images && Array.isArray(variant.images)) {
|
|
1156
|
-
variant.images.forEach(img => {
|
|
1157
|
-
if (img.url && !imageUrls.includes(img.url)) imageUrls.push(img.url);
|
|
1158
|
-
else if (img.Url && !imageUrls.includes(img.Url)) imageUrls.push(img.Url);
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1543
|
+
const imageUrls = this.getImageUrls(variant);
|
|
1161
1544
|
|
|
1162
1545
|
// Determine availability - check multiple possible fields
|
|
1163
1546
|
const inStock = variant.inStock !== false && variant.inStock !== undefined ? variant.inStock : true;
|
|
@@ -1175,25 +1558,16 @@
|
|
|
1175
1558
|
});
|
|
1176
1559
|
|
|
1177
1560
|
// Get base product image
|
|
1178
|
-
|
|
1179
|
-
if (fullProductData.images && fullProductData.images.length > 0) {
|
|
1180
|
-
baseImage = fullProductData.images[0].url || fullProductData.images[0].Url || baseImageSrc;
|
|
1181
|
-
} else if (fullProductData.thumbnailImage?.url) {
|
|
1182
|
-
baseImage = fullProductData.thumbnailImage.url;
|
|
1183
|
-
} else if (fullProductData.thumbnailImage?.Url) {
|
|
1184
|
-
baseImage = fullProductData.thumbnailImage.Url;
|
|
1185
|
-
}
|
|
1561
|
+
const baseImage = this.getImageUrls(fullProductData)[0] || baseImageSrc;
|
|
1186
1562
|
|
|
1187
1563
|
variantData = {
|
|
1188
1564
|
variants: transformedVariants,
|
|
1189
1565
|
baseProductImage: baseImage,
|
|
1190
1566
|
baseProductId: fullProductData.productId || fullProductData.id || baseProductId
|
|
1191
1567
|
};
|
|
1192
|
-
console.log('[AddToCartModal] Transformed variant data:', variantData);
|
|
1193
1568
|
} else {
|
|
1194
1569
|
// Priority 2: Fall back to script tag data
|
|
1195
|
-
|
|
1196
|
-
const variantDataScript = productCard.querySelector('.product-card-variant-data[data-product-id="' + baseProductId + '"]');
|
|
1570
|
+
const variantDataScript = (isElement ? productCard : document).querySelector('.product-card-variant-data[data-product-id="' + baseProductId + '"]');
|
|
1197
1571
|
if (variantDataScript) {
|
|
1198
1572
|
try {
|
|
1199
1573
|
variantData = JSON.parse(variantDataScript.textContent);
|
|
@@ -1481,7 +1855,6 @@
|
|
|
1481
1855
|
// CRITICAL: Prevent variant selection during active add-to-cart request
|
|
1482
1856
|
// This fixes the issue where modal closes when selecting second option during request
|
|
1483
1857
|
if (modal._isAddingToCart) {
|
|
1484
|
-
console.log('[AddToCartModal] Variant selection blocked - add-to-cart in progress');
|
|
1485
1858
|
return;
|
|
1486
1859
|
}
|
|
1487
1860
|
|
|
@@ -1627,13 +2000,12 @@
|
|
|
1627
2000
|
// Update image
|
|
1628
2001
|
const modalImage = modal.querySelector('#add-to-cart-modal-image');
|
|
1629
2002
|
if (modalImage && variant.images && variant.images.length > 0 && variant.images[0]) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
modal.dataset.variantImageUrl = variantImageUrl;
|
|
2003
|
+
const variantImageUrl = this.resolveImageUrl(variant.images[0]);
|
|
2004
|
+
if (variantImageUrl) {
|
|
2005
|
+
modalImage.src = variantImageUrl;
|
|
2006
|
+
// Store variant image on modal for later use when adding to cart
|
|
2007
|
+
modal.dataset.variantImageUrl = variantImageUrl;
|
|
2008
|
+
}
|
|
1637
2009
|
} else if (modalImage && variantData.baseProductImage) {
|
|
1638
2010
|
modalImage.src = variantData.baseProductImage;
|
|
1639
2011
|
// Clear variant image if using base product image
|
|
@@ -1746,14 +2118,11 @@
|
|
|
1746
2118
|
this.findModalMatchingVariant(modal, modal._variantData);
|
|
1747
2119
|
const matchedProductId = modal.dataset.productId;
|
|
1748
2120
|
if (matchedProductId && matchedProductId !== currentProductId) {
|
|
1749
|
-
console.log('[AddToCartModal] Updated productId from', currentProductId, 'to', matchedProductId, 'based on selected options:', modal._selectedOptions);
|
|
1750
2121
|
currentProductId = matchedProductId;
|
|
1751
2122
|
}
|
|
1752
2123
|
}
|
|
1753
2124
|
}
|
|
1754
2125
|
|
|
1755
|
-
console.log('[AddToCartModal] Adding to cart - productId:', currentProductId, 'quantity:', quantity, 'selectedOptions:', modal._selectedOptions);
|
|
1756
|
-
|
|
1757
2126
|
// CRITICAL: Set loading state and flag BEFORE any async operations
|
|
1758
2127
|
// This prevents modal from closing during the request
|
|
1759
2128
|
modal._isAddingToCart = true;
|
|
@@ -1770,7 +2139,7 @@
|
|
|
1770
2139
|
try {
|
|
1771
2140
|
localStorage.setItem(imageKey, variantImageUrl);
|
|
1772
2141
|
} catch (e) {
|
|
1773
|
-
|
|
2142
|
+
// Silently handle localStorage failures
|
|
1774
2143
|
}
|
|
1775
2144
|
}
|
|
1776
2145
|
|
|
@@ -1832,7 +2201,6 @@
|
|
|
1832
2201
|
// CRITICAL: Prevent closing if add-to-cart request is in progress
|
|
1833
2202
|
// Check both strict equality and truthy check for safety
|
|
1834
2203
|
if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
|
|
1835
|
-
console.log('[AddToCartModal] Close blocked - add-to-cart request in progress');
|
|
1836
2204
|
e.preventDefault();
|
|
1837
2205
|
e.stopPropagation();
|
|
1838
2206
|
e.stopImmediatePropagation();
|
|
@@ -1866,7 +2234,6 @@
|
|
|
1866
2234
|
// CRITICAL: Prevent closing if add-to-cart request is in progress
|
|
1867
2235
|
// Check both strict equality and truthy check for safety
|
|
1868
2236
|
if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
|
|
1869
|
-
console.log('[AddToCartModal] Escape key blocked - add-to-cart request in progress');
|
|
1870
2237
|
e.preventDefault();
|
|
1871
2238
|
e.stopPropagation();
|
|
1872
2239
|
e.stopImmediatePropagation();
|
|
@@ -1889,7 +2256,6 @@
|
|
|
1889
2256
|
// CRITICAL: Prevent closing if add-to-cart request is in progress
|
|
1890
2257
|
// Check both strict equality and truthy check for safety
|
|
1891
2258
|
if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
|
|
1892
|
-
console.log('[AddToCartModal] Cannot close modal while add-to-cart is in progress');
|
|
1893
2259
|
return;
|
|
1894
2260
|
}
|
|
1895
2261
|
|
|
@@ -1942,7 +2308,7 @@
|
|
|
1942
2308
|
|
|
1943
2309
|
debounce(func, wait) {
|
|
1944
2310
|
let timeout;
|
|
1945
|
-
return
|
|
2311
|
+
return (...args) => {
|
|
1946
2312
|
const later = () => {
|
|
1947
2313
|
clearTimeout(timeout);
|
|
1948
2314
|
func(...args);
|
|
@@ -1954,11 +2320,9 @@
|
|
|
1954
2320
|
|
|
1955
2321
|
throttle(func, limit) {
|
|
1956
2322
|
let inThrottle;
|
|
1957
|
-
return
|
|
1958
|
-
const args = arguments;
|
|
1959
|
-
const context = this;
|
|
2323
|
+
return (...args) => {
|
|
1960
2324
|
if (!inThrottle) {
|
|
1961
|
-
func.apply(
|
|
2325
|
+
func.apply(null, args);
|
|
1962
2326
|
inThrottle = true;
|
|
1963
2327
|
setTimeout(() => inThrottle = false, limit);
|
|
1964
2328
|
}
|
|
@@ -2320,9 +2684,6 @@
|
|
|
2320
2684
|
});
|
|
2321
2685
|
}
|
|
2322
2686
|
|
|
2323
|
-
// Preload critical resources
|
|
2324
|
-
this.preloadCriticalResources();
|
|
2325
|
-
|
|
2326
2687
|
// Optimize animations for performance
|
|
2327
2688
|
this.optimizeAnimations();
|
|
2328
2689
|
},
|
|
@@ -2394,8 +2755,12 @@
|
|
|
2394
2755
|
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
|
2395
2756
|
anchorLinks.forEach(link => {
|
|
2396
2757
|
link.addEventListener('click', (e) => {
|
|
2758
|
+
const href = link.getAttribute('href');
|
|
2759
|
+
// Skip if href is just '#' without an ID
|
|
2760
|
+
if (!href || href === '#' || href.length <= 1) return;
|
|
2761
|
+
|
|
2397
2762
|
e.preventDefault();
|
|
2398
|
-
const target = document.querySelector(
|
|
2763
|
+
const target = document.querySelector(href);
|
|
2399
2764
|
if (target) {
|
|
2400
2765
|
smoothScroll(target.offsetTop);
|
|
2401
2766
|
}
|
|
@@ -2438,6 +2803,49 @@
|
|
|
2438
2803
|
let userGUIDForOtp = null;
|
|
2439
2804
|
let userGUIDForPhoneOtp = null;
|
|
2440
2805
|
let loginPhoneIti = null;
|
|
2806
|
+
const intlTelInputCssUrl = 'https://cdn.jsdelivr.net/npm/intl-tel-input@23.0.0/build/css/intlTelInput.min.css';
|
|
2807
|
+
const intlTelInputJsUrl = 'https://cdn.jsdelivr.net/npm/intl-tel-input@23.0.0/build/js/intlTelInput.min.js';
|
|
2808
|
+
let intlTelInputLoadPromise = null;
|
|
2809
|
+
|
|
2810
|
+
const ensureIntlTelInputLoaded = () => {
|
|
2811
|
+
if (typeof window.intlTelInput !== 'undefined') {
|
|
2812
|
+
return Promise.resolve(true);
|
|
2813
|
+
}
|
|
2814
|
+
if (intlTelInputLoadPromise) {
|
|
2815
|
+
return intlTelInputLoadPromise;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
intlTelInputLoadPromise = new Promise((resolve) => {
|
|
2819
|
+
const onLoaded = () => resolve(typeof window.intlTelInput !== 'undefined');
|
|
2820
|
+
const onFailed = () => resolve(false);
|
|
2821
|
+
|
|
2822
|
+
if (!document.querySelector('link[data-iti-login="true"]')) {
|
|
2823
|
+
const cssLink = document.createElement('link');
|
|
2824
|
+
cssLink.rel = 'stylesheet';
|
|
2825
|
+
cssLink.href = intlTelInputCssUrl;
|
|
2826
|
+
cssLink.setAttribute('data-iti-login', 'true');
|
|
2827
|
+
document.head.appendChild(cssLink);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
const existingScript = document.querySelector('script[data-iti-login="true"]');
|
|
2831
|
+
if (existingScript) {
|
|
2832
|
+
existingScript.addEventListener('load', onLoaded, { once: true });
|
|
2833
|
+
existingScript.addEventListener('error', onFailed, { once: true });
|
|
2834
|
+
setTimeout(onLoaded, 2500);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
const script = document.createElement('script');
|
|
2839
|
+
script.src = intlTelInputJsUrl;
|
|
2840
|
+
script.defer = true;
|
|
2841
|
+
script.setAttribute('data-iti-login', 'true');
|
|
2842
|
+
script.onload = onLoaded;
|
|
2843
|
+
script.onerror = onFailed;
|
|
2844
|
+
document.head.appendChild(script);
|
|
2845
|
+
});
|
|
2846
|
+
|
|
2847
|
+
return intlTelInputLoadPromise;
|
|
2848
|
+
};
|
|
2441
2849
|
|
|
2442
2850
|
const setStep = (step) => {
|
|
2443
2851
|
stepLabels.forEach(label => {
|
|
@@ -2501,9 +2909,18 @@
|
|
|
2501
2909
|
});
|
|
2502
2910
|
successView.hidden = true;
|
|
2503
2911
|
|
|
2504
|
-
//
|
|
2505
|
-
|
|
2506
|
-
|
|
2912
|
+
// Check if only one login method is available
|
|
2913
|
+
const methodButtons = modal.querySelectorAll('[data-login-method]');
|
|
2914
|
+
if (methodButtons.length === 1) {
|
|
2915
|
+
// Auto-select the only available method
|
|
2916
|
+
const method = methodButtons[0].getAttribute('data-login-method');
|
|
2917
|
+
resetOtpFlows();
|
|
2918
|
+
selectMethod(method);
|
|
2919
|
+
} else {
|
|
2920
|
+
// Show method selection
|
|
2921
|
+
resetOtpFlows();
|
|
2922
|
+
showView('methods');
|
|
2923
|
+
}
|
|
2507
2924
|
};
|
|
2508
2925
|
|
|
2509
2926
|
// Initialize intl-tel-input for login phone
|
|
@@ -2516,19 +2933,85 @@
|
|
|
2516
2933
|
loginPhoneIti = null;
|
|
2517
2934
|
}
|
|
2518
2935
|
|
|
2519
|
-
|
|
2936
|
+
// Check if we're in development/localhost - skip geoIpLookup to avoid CORS issues
|
|
2937
|
+
const isLocalhost = window.location.hostname === 'localhost' ||
|
|
2938
|
+
window.location.hostname === '127.0.0.1' ||
|
|
2939
|
+
window.location.hostname === '';
|
|
2940
|
+
|
|
2941
|
+
const itiOptions = {
|
|
2520
2942
|
utilsScript: 'https://cdn.jsdelivr.net/npm/intl-tel-input@23.0.0/build/js/utils.js',
|
|
2521
|
-
initialCountry: 'auto',
|
|
2522
|
-
|
|
2523
|
-
fetch('https://ipapi.co/json/')
|
|
2524
|
-
.then(res => res.json())
|
|
2525
|
-
.then(data => callback(data.country_code ? data.country_code.toLowerCase() : 'us'))
|
|
2526
|
-
.catch(() => callback('us'));
|
|
2527
|
-
},
|
|
2528
|
-
preferredCountries: ['us', 'gb', 'ca', 'au', 'in'],
|
|
2943
|
+
initialCountry: isLocalhost ? 'in' : 'auto',
|
|
2944
|
+
preferredCountries: ['in', 'us', 'gb', 'ca', 'au'],
|
|
2529
2945
|
separateDialCode: true,
|
|
2530
|
-
nationalMode: false
|
|
2531
|
-
|
|
2946
|
+
nationalMode: false,
|
|
2947
|
+
allowDropdown: true,
|
|
2948
|
+
autoHideDialCode: false,
|
|
2949
|
+
dropdownContainer: document.body // Append dropdown to body to escape modal stacking context
|
|
2950
|
+
};
|
|
2951
|
+
|
|
2952
|
+
// Only add geoIpLookup if not on localhost (to avoid CORS issues)
|
|
2953
|
+
if (!isLocalhost) {
|
|
2954
|
+
itiOptions.geoIpLookup = (callback) => {
|
|
2955
|
+
// Quick timeout to avoid hanging
|
|
2956
|
+
const timeout = setTimeout(() => {
|
|
2957
|
+
callback('in');
|
|
2958
|
+
}, 2000);
|
|
2959
|
+
|
|
2960
|
+
// Try geolocation API (may fail due to CORS in some environments)
|
|
2961
|
+
fetch('https://ipapi.co/json/')
|
|
2962
|
+
.then(res => {
|
|
2963
|
+
clearTimeout(timeout);
|
|
2964
|
+
if (!res.ok) throw new Error('API error');
|
|
2965
|
+
return res.json();
|
|
2966
|
+
})
|
|
2967
|
+
.then(data => {
|
|
2968
|
+
const countryCode = data.country_code ? data.country_code.toLowerCase() : 'in';
|
|
2969
|
+
callback(countryCode);
|
|
2970
|
+
})
|
|
2971
|
+
.catch(() => {
|
|
2972
|
+
clearTimeout(timeout);
|
|
2973
|
+
// Default to India if API fails
|
|
2974
|
+
callback('in');
|
|
2975
|
+
});
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
loginPhoneIti = intlTelInput(phoneInput, itiOptions);
|
|
2980
|
+
|
|
2981
|
+
// Ensure dropdown is enabled and working on mobile
|
|
2982
|
+
if (loginPhoneIti) {
|
|
2983
|
+
// Wait for DOM to be ready, then ensure flag container is clickable
|
|
2984
|
+
setTimeout(() => {
|
|
2985
|
+
const flagContainer = phoneInput.parentElement?.querySelector('.iti__flag-container');
|
|
2986
|
+
const selectedFlag = phoneInput.parentElement?.querySelector('.iti__selected-flag');
|
|
2987
|
+
|
|
2988
|
+
if (flagContainer) {
|
|
2989
|
+
flagContainer.style.pointerEvents = 'auto';
|
|
2990
|
+
flagContainer.style.cursor = 'pointer';
|
|
2991
|
+
flagContainer.setAttribute('role', 'button');
|
|
2992
|
+
flagContainer.setAttribute('tabindex', '0');
|
|
2993
|
+
|
|
2994
|
+
// Add explicit click/touch handlers for mobile
|
|
2995
|
+
const handleFlagClick = (e) => {
|
|
2996
|
+
e.stopPropagation();
|
|
2997
|
+
// Trigger click on the selected flag element
|
|
2998
|
+
if (selectedFlag) {
|
|
2999
|
+
selectedFlag.click();
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
flagContainer.addEventListener('click', handleFlagClick, { passive: true });
|
|
3004
|
+
flagContainer.addEventListener('touchend', handleFlagClick, { passive: true });
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
if (selectedFlag) {
|
|
3008
|
+
selectedFlag.style.pointerEvents = 'auto';
|
|
3009
|
+
selectedFlag.style.cursor = 'pointer';
|
|
3010
|
+
selectedFlag.setAttribute('role', 'button');
|
|
3011
|
+
selectedFlag.setAttribute('tabindex', '0');
|
|
3012
|
+
}
|
|
3013
|
+
}, 100);
|
|
3014
|
+
}
|
|
2532
3015
|
|
|
2533
3016
|
// Store instance globally for validation
|
|
2534
3017
|
window.loginPhoneIti = loginPhoneIti;
|
|
@@ -2544,8 +3027,11 @@
|
|
|
2544
3027
|
showView('email-otp');
|
|
2545
3028
|
} else if (method === 'phone-otp') {
|
|
2546
3029
|
showView('phone-otp');
|
|
2547
|
-
// Initialize phone input when phone OTP method is selected
|
|
2548
|
-
|
|
3030
|
+
// Initialize phone input when phone OTP method is selected.
|
|
3031
|
+
// Load intl-tel-input on demand on pages where it's not globally included.
|
|
3032
|
+
ensureIntlTelInputLoaded().finally(() => {
|
|
3033
|
+
setTimeout(initializeLoginPhoneInput, 100);
|
|
3034
|
+
});
|
|
2549
3035
|
}
|
|
2550
3036
|
};
|
|
2551
3037
|
|
|
@@ -2874,6 +3360,120 @@
|
|
|
2874
3360
|
});
|
|
2875
3361
|
}
|
|
2876
3362
|
|
|
3363
|
+
// Resend Email OTP handler function
|
|
3364
|
+
async function resendEmailOtp(btn) {
|
|
3365
|
+
if (!emailForOtp) {
|
|
3366
|
+
console.log('Resend Email OTP: No email stored');
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
console.log('Resend Email OTP clicked, email:', emailForOtp);
|
|
3371
|
+
clearError('email-otp-verify');
|
|
3372
|
+
if (btn) {
|
|
3373
|
+
btn.disabled = true;
|
|
3374
|
+
btn.textContent = 'Sending...';
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
try {
|
|
3378
|
+
const response = await fetch('/webstoreapi/auth/email/send-otp', {
|
|
3379
|
+
method: 'POST',
|
|
3380
|
+
headers: {
|
|
3381
|
+
'Content-Type': 'application/json',
|
|
3382
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
3383
|
+
},
|
|
3384
|
+
body: JSON.stringify({ email: emailForOtp })
|
|
3385
|
+
});
|
|
3386
|
+
const data = await response.json();
|
|
3387
|
+
console.log('Resend Email OTP response:', data);
|
|
3388
|
+
if (!response.ok || !data.success) {
|
|
3389
|
+
showError('email-otp-verify', data.error || 'Unable to resend OTP. Please try again.');
|
|
3390
|
+
} else {
|
|
3391
|
+
userGUIDForOtp = data.userGUID || userGUIDForOtp;
|
|
3392
|
+
}
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
console.error('Resend email OTP error:', err);
|
|
3395
|
+
showError('email-otp-verify', 'Unable to resend OTP. Please try again.');
|
|
3396
|
+
} finally {
|
|
3397
|
+
if (btn) {
|
|
3398
|
+
btn.disabled = false;
|
|
3399
|
+
btn.textContent = 'Resend code';
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// Resend Phone OTP handler function
|
|
3405
|
+
async function resendPhoneOtp(btn) {
|
|
3406
|
+
if (!phoneForOtp) {
|
|
3407
|
+
console.log('Resend Phone OTP: No phone stored');
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
console.log('Resend Phone OTP clicked, phone:', phoneForOtp);
|
|
3412
|
+
clearError('phone-otp-verify');
|
|
3413
|
+
if (btn) {
|
|
3414
|
+
btn.disabled = true;
|
|
3415
|
+
btn.textContent = 'Sending...';
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
try {
|
|
3419
|
+
const response = await fetch('/webstoreapi/auth/phone/send-otp', {
|
|
3420
|
+
method: 'POST',
|
|
3421
|
+
headers: {
|
|
3422
|
+
'Content-Type': 'application/json',
|
|
3423
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
3424
|
+
},
|
|
3425
|
+
body: JSON.stringify({ phoneNumber: phoneForOtp })
|
|
3426
|
+
});
|
|
3427
|
+
const data = await response.json();
|
|
3428
|
+
console.log('Resend Phone OTP response:', data);
|
|
3429
|
+
if (!response.ok || !data.success) {
|
|
3430
|
+
showError('phone-otp-verify', data.error || 'Unable to resend OTP. Please try again.');
|
|
3431
|
+
} else {
|
|
3432
|
+
userGUIDForPhoneOtp = data.userGUID || userGUIDForPhoneOtp;
|
|
3433
|
+
}
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
console.error('Resend phone OTP error:', err);
|
|
3436
|
+
showError('phone-otp-verify', 'Unable to resend OTP. Please try again.');
|
|
3437
|
+
} finally {
|
|
3438
|
+
if (btn) {
|
|
3439
|
+
btn.disabled = false;
|
|
3440
|
+
btn.textContent = 'Resend code';
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// Attach click handlers using ID
|
|
3446
|
+
const resendEmailBtnById = document.getElementById('resend-email-otp-btn');
|
|
3447
|
+
if (resendEmailBtnById) {
|
|
3448
|
+
resendEmailBtnById.addEventListener('click', (e) => {
|
|
3449
|
+
e.preventDefault();
|
|
3450
|
+
resendEmailOtp(resendEmailBtnById);
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
const resendPhoneBtnById = document.getElementById('resend-phone-otp-btn');
|
|
3455
|
+
if (resendPhoneBtnById) {
|
|
3456
|
+
resendPhoneBtnById.addEventListener('click', (e) => {
|
|
3457
|
+
e.preventDefault();
|
|
3458
|
+
resendPhoneOtp(resendPhoneBtnById);
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
// Also use event delegation as fallback
|
|
3463
|
+
modal.addEventListener('click', (e) => {
|
|
3464
|
+
const resendEmailBtn = e.target.closest('[data-login-resend-email-otp]');
|
|
3465
|
+
if (resendEmailBtn && resendEmailBtn.id !== 'resend-email-otp-btn') {
|
|
3466
|
+
e.preventDefault();
|
|
3467
|
+
resendEmailOtp(resendEmailBtn);
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
const resendPhoneBtn = e.target.closest('[data-login-resend-phone-otp]');
|
|
3471
|
+
if (resendPhoneBtn && resendPhoneBtn.id !== 'resend-phone-otp-btn') {
|
|
3472
|
+
e.preventDefault();
|
|
3473
|
+
resendPhoneOtp(resendPhoneBtn);
|
|
3474
|
+
}
|
|
3475
|
+
});
|
|
3476
|
+
|
|
2877
3477
|
// ESC to close
|
|
2878
3478
|
document.addEventListener('keydown', (e) => {
|
|
2879
3479
|
if (e.key === 'Escape' && modal.classList.contains('login-modal--active')) {
|
|
@@ -3045,7 +3645,7 @@
|
|
|
3045
3645
|
const currentPath = window.location.pathname;
|
|
3046
3646
|
const navItems = document.querySelectorAll('.mobile-bottom-nav__item');
|
|
3047
3647
|
|
|
3048
|
-
navItems.forEach(
|
|
3648
|
+
navItems.forEach((item) => {
|
|
3049
3649
|
const href = item.getAttribute('href');
|
|
3050
3650
|
const dataItem = item.getAttribute('data-nav-item');
|
|
3051
3651
|
|
|
@@ -3276,7 +3876,7 @@ style.textContent = `
|
|
|
3276
3876
|
font-size: 20px;
|
|
3277
3877
|
font-weight: 600;
|
|
3278
3878
|
color: #000;
|
|
3279
|
-
margin-bottom:
|
|
3879
|
+
margin-bottom: 14px;
|
|
3280
3880
|
}
|
|
3281
3881
|
|
|
3282
3882
|
.quick-view-description {
|
|
@@ -3338,11 +3938,6 @@ style.textContent = `
|
|
|
3338
3938
|
opacity: 1;
|
|
3339
3939
|
}
|
|
3340
3940
|
|
|
3341
|
-
/* Product card animations */
|
|
3342
|
-
/*.product-card {
|
|
3343
|
-
animation: fadeInScale 0.2s var(--ease-out);
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
3941
|
/* Product card hover effects handled by components.css */
|
|
3347
3942
|
|
|
3348
3943
|
/* Staggered animations for product grids */
|
|
@@ -3683,18 +4278,18 @@ style.textContent = `
|
|
|
3683
4278
|
transform: translateY(0);
|
|
3684
4279
|
box-shadow: var(--shadow-sm);
|
|
3685
4280
|
}
|
|
3686
|
-
|
|
4281
|
+
{% comment %}
|
|
3687
4282
|
/* Card hover effects */
|
|
3688
4283
|
.collection-card,
|
|
3689
4284
|
.blog-card {
|
|
3690
4285
|
transition: all var(--transition-fast);
|
|
3691
4286
|
}
|
|
3692
|
-
|
|
3693
|
-
.collection-card:hover,
|
|
4287
|
+
{% endcomment %}
|
|
4288
|
+
{% comment %} .collection-card:hover,
|
|
3694
4289
|
.blog-card:hover {
|
|
3695
4290
|
transform: translateY(-8px) scale(1.02);
|
|
3696
4291
|
box-shadow: var(--shadow-xl);
|
|
3697
|
-
}
|
|
4292
|
+
} {% endcomment %}
|
|
3698
4293
|
|
|
3699
4294
|
/* Form focus effects */
|
|
3700
4295
|
.form-group {
|
|
@@ -3742,4 +4337,4 @@ style.textContent = `
|
|
|
3742
4337
|
will-change: transform;
|
|
3743
4338
|
}
|
|
3744
4339
|
`;
|
|
3745
|
-
document.head.appendChild(style);
|
|
4340
|
+
document.head.appendChild(style);
|