@o2vend/theme-cli 1.0.37 → 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.
Files changed (89) hide show
  1. package/lib/lib/dev-server.js +309 -40
  2. package/lib/lib/liquid-engine.js +3 -1
  3. package/lib/lib/mock-data.js +36 -124
  4. package/lib/lib/widget-service.js +12 -4
  5. package/package.json +1 -1
  6. package/test-theme/assets/async-sections.js +32 -24
  7. package/test-theme/assets/cart-drawer.js +20 -22
  8. package/test-theme/assets/cart-manager.js +1 -15
  9. package/test-theme/assets/checkout-price-handler.js +12 -11
  10. package/test-theme/assets/checkout.css +1415 -0
  11. package/test-theme/assets/checkout.js +3174 -0
  12. package/test-theme/assets/components.css +178 -29
  13. package/test-theme/assets/delivery-zone.js +1 -1
  14. package/test-theme/assets/product-detail.css +1050 -0
  15. package/test-theme/assets/product-detail.js +2940 -0
  16. package/test-theme/assets/theme.css +95 -120
  17. package/test-theme/assets/theme.js +781 -186
  18. package/test-theme/layout/theme.liquid +91 -17
  19. package/test-theme/sections/content.liquid +64 -57
  20. package/test-theme/sections/footer-fallback.liquid +57 -7
  21. package/test-theme/sections/footer.liquid +63 -12
  22. package/test-theme/sections/header-fallback.liquid +41 -41
  23. package/test-theme/sections/header.liquid +41 -51
  24. package/test-theme/sections/hero-fallback.liquid +1 -1
  25. package/test-theme/sections/hero.liquid +159 -136
  26. package/test-theme/snippets/account-sidebar.liquid +121 -29
  27. package/test-theme/snippets/add-to-cart-modal.liquid +258 -206
  28. package/test-theme/snippets/breadcrumbs.liquid +98 -11
  29. package/test-theme/snippets/cart-drawer.liquid +93 -0
  30. package/test-theme/snippets/delivery-zone-city-selector.liquid +101 -15
  31. package/test-theme/snippets/delivery-zone-modal.liquid +529 -84
  32. package/test-theme/snippets/delivery-zone-search.liquid +104 -18
  33. package/test-theme/snippets/login-modal.liquid +269 -82
  34. package/test-theme/snippets/mega-menu.liquid +130 -43
  35. package/test-theme/snippets/news-thumbnail.liquid +120 -28
  36. package/test-theme/snippets/pagination.liquid +1 -1
  37. package/test-theme/snippets/price.liquid +100 -9
  38. package/test-theme/snippets/product-card-related.liquid +22 -4
  39. package/test-theme/snippets/product-card-simple.liquid +521 -25
  40. package/test-theme/snippets/product-card.liquid +145 -232
  41. package/test-theme/snippets/rating.liquid +100 -9
  42. package/test-theme/snippets/skeleton-collection-grid.liquid +94 -8
  43. package/test-theme/snippets/skeleton-product-card.liquid +102 -16
  44. package/test-theme/snippets/skeleton-product-grid.liquid +87 -1
  45. package/test-theme/snippets/social-sharing.liquid +133 -32
  46. package/test-theme/templates/account/dashboard.liquid +30 -0
  47. package/test-theme/templates/account/loyalty-redemption.liquid +29 -28
  48. package/test-theme/templates/account/loyalty.liquid +45 -43
  49. package/test-theme/templates/account/order-detail.liquid +15 -8
  50. package/test-theme/templates/account/orders.liquid +189 -35
  51. package/test-theme/templates/account/profile.liquid +509 -114
  52. package/test-theme/templates/account/register.liquid +18 -8
  53. package/test-theme/templates/account/return-orders.liquid +31 -30
  54. package/test-theme/templates/account/store-credit.liquid +27 -26
  55. package/test-theme/templates/account/subscriptions.liquid +22 -5
  56. package/test-theme/templates/account/wishlist.liquid +88 -19
  57. package/test-theme/templates/address-book.liquid +166 -69
  58. package/test-theme/templates/categories.liquid +90 -30
  59. package/test-theme/templates/checkout.liquid +137 -3834
  60. package/test-theme/templates/error.liquid +23 -21
  61. package/test-theme/templates/index.liquid +29 -0
  62. package/test-theme/templates/login.liquid +33 -6
  63. package/test-theme/templates/order-confirmation.liquid +67 -9
  64. package/test-theme/templates/page.liquid +418 -206
  65. package/test-theme/templates/product-detail.liquid +124 -3878
  66. package/test-theme/templates/products.liquid +155 -30
  67. package/test-theme/templates/search.liquid +739 -225
  68. package/test-theme/widgets/brand-carousel.liquid +102 -82
  69. package/test-theme/widgets/brand.liquid +78 -50
  70. package/test-theme/widgets/carousel.liquid +253 -121
  71. package/test-theme/widgets/category-list-carousel.liquid +32 -8
  72. package/test-theme/widgets/category-list.liquid +21 -6
  73. package/test-theme/widgets/category.liquid +104 -37
  74. package/test-theme/widgets/discount-time.liquid +326 -119
  75. package/test-theme/widgets/footer-menu.liquid +115 -23
  76. package/test-theme/widgets/footer.liquid +118 -5
  77. package/test-theme/widgets/gallery.liquid +29 -5
  78. package/test-theme/widgets/header-menu.liquid +25 -13
  79. package/test-theme/widgets/header.liquid +64 -26
  80. package/test-theme/widgets/html.liquid +29 -6
  81. package/test-theme/widgets/news.liquid +6 -0
  82. package/test-theme/widgets/product-canvas.liquid +20 -12
  83. package/test-theme/widgets/product-carousel.liquid +118 -56
  84. package/test-theme/widgets/shared/product-grid.liquid +12 -0
  85. package/test-theme/widgets/single-product.liquid +688 -250
  86. package/test-theme/widgets/spacebar-carousel.liquid +39 -10
  87. package/test-theme/widgets/spacebar.liquid +77 -6
  88. package/test-theme/widgets/splash.liquid +40 -30
  89. package/test-theme/widgets/testimonial-carousel.liquid +111 -67
@@ -3,9 +3,73 @@
3
3
  * Modern, interactive functionality theme
4
4
  */
5
5
 
6
- (function() {
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
- console.log('[Theme] Add to cart button clicked:', addToCartBtn);
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
- } else {
194
- console.log('[Theme] Skipping modal - productType:', productType, 'variantsCount:', variantsCount);
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(`[data-product-id="${productId}"]`);
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
- // Always fetch full cart data after successful add to ensure instant update
356
- // This ensures both cart count and total are updated immediately
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 instantly using CartManager
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
- console.warn('Failed to fetch full cart data after add:', e);
383
- // Fallback: try to fetch just the count if full cart fetch fails
384
- try {
385
- if (window.CartManager && typeof window.CartManager.getCartCount === 'function') {
386
- const cartCount = await window.CartManager.getCartCount(true);
387
- data.data = data.data || {};
388
- data.data.itemCount = cartCount;
389
- // If we don't have total in response, preserve whatever was in the original response
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
- // Check if authentication is required
420
- console.log('[AddToCart] Request failed, checking requiresAuth:', data.requiresAuth);
421
- if (data.requiresAuth) {
422
- console.log('[AddToCart] Authentication required in else block, opening login modal');
423
- // Helper function to open login modal with fallbacks
424
- const openLogin = () => {
425
- if (this.openLoginModal && typeof this.openLoginModal === 'function') {
426
- this.openLoginModal();
427
- return true;
428
- } else if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
429
- window.Theme.openLoginModal();
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(`[data-product-id="${productId}"]`);
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 || 'Error adding product to cart', 'error');
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(`[data-product-id="${productId}"]`);
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
- const quickViewBtns = document.querySelectorAll('.quick-view-btn');
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
- const wishlistBtns = document.querySelectorAll('.wishlist-btn');
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
- // Show loading state
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
- const product = await response.json();
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="${product.images?.[0] || '/assets/placeholder-product.jpg'}" alt="${product.title}">
978
+ <img src="${productImage || fallbackImage}" alt="${productTitle}">
713
979
  </div>
714
980
  <div class="quick-view-info">
715
- <h2 class="quick-view-title">${product.title}</h2>
716
- <div class="quick-view-price">${this.formatMoney(product.prices?.priceString || product.prices?.price || product.price)}</div>
717
- <div class="quick-view-description">${product.description || ''}</div>
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
- Add to Cart
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="/${product.slug || product.id}" class="btn btn-outline">
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
- this.addToCart(product.productId || product.id, 1);
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
- const data = await response.json();
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
- console.log('[AddToCartModal] Opening modal for product card:', productCard);
1077
-
1078
- // Extract product data from card
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 = productCard.querySelector('.product-card__title-link')?.textContent?.trim() ||
1082
- productCard.querySelector('.product-card__title')?.textContent?.trim() ||
1083
- productCard.querySelector('.product-title-link')?.textContent?.trim() ||
1084
- productCard.querySelector('.product-title')?.textContent?.trim() || '';
1085
- const productImage = productCard.querySelector('.product-card__image--primary') ||
1086
- productCard.querySelector('.product-card__image') ||
1087
- productCard.querySelector('.product-image');
1088
- const baseImageSrc = productImage?.src || '';
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
- // Extract image URLs (handle both thumbnailImage1 and images array)
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
- let baseImage = baseImageSrc;
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
- console.log('[AddToCartModal] Using variant data from script tag');
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
- const variantImageUrl = typeof variant.images[0] === 'string'
1632
- ? variant.images[0]
1633
- : (variant.images[0].url || variant.images[0].Url || variant.images[0]);
1634
- modalImage.src = variantImageUrl;
1635
- // Store variant image on modal for later use when adding to cart
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
- console.warn('Failed to store variant image in localStorage:', e);
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 function executedFunction(...args) {
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 function() {
1958
- const args = arguments;
1959
- const context = this;
2323
+ return (...args) => {
1960
2324
  if (!inThrottle) {
1961
- func.apply(context, args);
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(link.getAttribute('href'));
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
- // Always start at method selection with no fields visible
2505
- resetOtpFlows();
2506
- showView('methods');
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
- loginPhoneIti = intlTelInput(phoneInput, {
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
- geoIpLookup: function(callback) {
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
- setTimeout(initializeLoginPhoneInput, 100);
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(function(item) {
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: 20px;
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);