@o2vend/theme-cli 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,3745 @@
1
+ /**
2
+ * O2VEND Default Theme - JavaScript
3
+ * Modern, interactive functionality theme
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // Theme object to hold all functionality
10
+ const Theme = {
11
+ // Initialize all theme functionality
12
+ init() {
13
+ this.initMobileMenu();
14
+ this.initSearch();
15
+ this.initCart();
16
+ this.initProductActions();
17
+ this.initNotifications();
18
+ this.initLazyLoading();
19
+ this.initScrollEffects();
20
+ this.initFormValidation();
21
+ this.initHeaderScroll();
22
+ this.initSmoothScrolling();
23
+ this.initIntersectionObserver();
24
+ this.initLoginModal();
25
+ this.initMobileBottomNav();
26
+ },
27
+
28
+ // Mobile menu functionality
29
+ initMobileMenu() {
30
+ const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
31
+ const mainNav = document.querySelector('.main-nav');
32
+
33
+ if (mobileMenuToggle && mainNav) {
34
+ mobileMenuToggle.addEventListener('click', () => {
35
+ const isOpen = mainNav.classList.contains('mobile-nav-open');
36
+
37
+ if (isOpen) {
38
+ this.closeMobileMenu();
39
+ } else {
40
+ this.openMobileMenu();
41
+ }
42
+ });
43
+
44
+ // Close mobile menu when clicking outside
45
+ document.addEventListener('click', (e) => {
46
+ if (!mainNav.contains(e.target) && !mobileMenuToggle.contains(e.target)) {
47
+ this.closeMobileMenu();
48
+ }
49
+ });
50
+
51
+ // Close mobile menu on escape key
52
+ document.addEventListener('keydown', (e) => {
53
+ if (e.key === 'Escape') {
54
+ this.closeMobileMenu();
55
+ }
56
+ });
57
+ }
58
+ },
59
+
60
+ openMobileMenu() {
61
+ const mainNav = document.querySelector('.main-nav');
62
+ const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
63
+
64
+ if (mainNav && mobileMenuToggle) {
65
+ mainNav.classList.add('mobile-nav-open');
66
+ mobileMenuToggle.classList.add('mobile-menu-open');
67
+ document.body.classList.add('mobile-menu-open');
68
+
69
+ // Animate hamburger
70
+ const hamburgers = mobileMenuToggle.querySelectorAll('.hamburger');
71
+ hamburgers[0].style.transform = 'rotate(45deg) translate(5px, 5px)';
72
+ hamburgers[1].style.opacity = '0';
73
+ hamburgers[2].style.transform = 'rotate(-45deg) translate(7px, -6px)';
74
+ }
75
+ },
76
+
77
+ closeMobileMenu() {
78
+ const mainNav = document.querySelector('.main-nav');
79
+ const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
80
+
81
+ if (mainNav && mobileMenuToggle) {
82
+ mainNav.classList.remove('mobile-nav-open');
83
+ mobileMenuToggle.classList.remove('mobile-menu-open');
84
+ document.body.classList.remove('mobile-menu-open');
85
+
86
+ // Reset hamburger
87
+ const hamburgers = mobileMenuToggle.querySelectorAll('.hamburger');
88
+ hamburgers[0].style.transform = '';
89
+ hamburgers[1].style.opacity = '';
90
+ hamburgers[2].style.transform = '';
91
+ }
92
+ },
93
+
94
+ // Search functionality
95
+ initSearch() {
96
+ const searchToggle = document.querySelector('.search-toggle');
97
+ const searchOverlay = document.querySelector('.search-overlay');
98
+ const searchClose = document.querySelector('.search-close');
99
+ const searchInput = document.querySelector('.search-input');
100
+
101
+ if (searchToggle && searchOverlay) {
102
+ searchToggle.addEventListener('click', () => {
103
+ this.openSearch();
104
+ });
105
+ }
106
+
107
+ if (searchClose) {
108
+ searchClose.addEventListener('click', () => {
109
+ this.closeSearch();
110
+ });
111
+ }
112
+
113
+ if (searchOverlay) {
114
+ searchOverlay.addEventListener('click', (e) => {
115
+ if (e.target === searchOverlay) {
116
+ this.closeSearch();
117
+ }
118
+ });
119
+ }
120
+
121
+ // Close search on escape key
122
+ document.addEventListener('keydown', (e) => {
123
+ if (e.key === 'Escape') {
124
+ this.closeSearch();
125
+ }
126
+ });
127
+
128
+ // Focus search input when opened
129
+ if (searchInput) {
130
+ searchInput.addEventListener('focus', () => {
131
+ this.openSearch();
132
+ });
133
+ }
134
+ },
135
+
136
+ openSearch() {
137
+ const searchOverlay = document.querySelector('.search-overlay');
138
+ const searchInput = document.querySelector('.search-input');
139
+
140
+ if (searchOverlay) {
141
+ searchOverlay.classList.add('active');
142
+ document.body.classList.add('search-open');
143
+
144
+ // Focus search input after animation
145
+ setTimeout(() => {
146
+ if (searchInput) {
147
+ searchInput.focus();
148
+ }
149
+ }, 100);
150
+ }
151
+ },
152
+
153
+ closeSearch() {
154
+ const searchOverlay = document.querySelector('.search-overlay');
155
+ const searchInput = document.querySelector('.search-input');
156
+
157
+ if (searchOverlay) {
158
+ searchOverlay.classList.remove('active');
159
+ document.body.classList.remove('search-open');
160
+
161
+ if (searchInput) {
162
+ searchInput.value = '';
163
+ }
164
+ }
165
+ },
166
+
167
+ // Cart functionality
168
+ initCart() {
169
+ // Use event delegation for add-to-cart buttons to handle dynamically loaded content (widgets, async sections)
170
+ // This avoids needing to re-initialize when new product cards are added
171
+ document.addEventListener('click', (e) => {
172
+ const addToCartBtn = e.target.closest('.add-to-cart-btn');
173
+ if (!addToCartBtn) return;
174
+
175
+ console.log('[Theme] Add to cart button clicked:', addToCartBtn);
176
+
177
+ // Check if this is a product card button (has a product-card ancestor)
178
+ const productCard = addToCartBtn.closest('.product-card');
179
+ if (productCard) {
180
+ // Check product type and variants count
181
+ const productType = parseInt(productCard.dataset.productType || addToCartBtn.dataset.productType || '0', 10);
182
+ const variantsCount = parseInt(productCard.dataset.variantsCount || '0', 10);
183
+
184
+ // Show modal if:
185
+ // 1. productType != 0 (always show modal)
186
+ // 2. productType == 0 AND variants count > 0 (show modal for variant selection)
187
+ if (productType !== 0 || (productType === 0 && variantsCount > 0)) {
188
+ // Show modal
189
+ e.preventDefault();
190
+ e.stopPropagation();
191
+ console.log('[Theme] Showing modal - productType:', productType, 'variantsCount:', variantsCount);
192
+ this.showAddToCartModal(productCard, addToCartBtn);
193
+ } else {
194
+ console.log('[Theme] Skipping modal - productType:', productType, 'variantsCount:', variantsCount);
195
+ }
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
+ } else {
199
+ // Direct add for non-product-card buttons (e.g., product page)
200
+ e.preventDefault();
201
+ const productId = addToCartBtn.getAttribute('data-product-id');
202
+ const quantity = addToCartBtn.getAttribute('data-quantity') || 1;
203
+
204
+ if (productId) {
205
+ this.addToCart(productId, parseInt(quantity));
206
+ }
207
+ }
208
+ });
209
+
210
+ // Quantity selectors
211
+ const quantityInputs = document.querySelectorAll('.quantity-input');
212
+
213
+ quantityInputs.forEach(input => {
214
+ input.addEventListener('change', (e) => {
215
+ const productId = input.getAttribute('data-product-id');
216
+ const quantity = parseInt(e.target.value);
217
+
218
+ if (productId && quantity > 0) {
219
+ this.updateCartItem(productId, quantity);
220
+ }
221
+ });
222
+ });
223
+
224
+ // Remove from cart buttons
225
+ const removeFromCartBtns = document.querySelectorAll('.remove-from-cart-btn');
226
+
227
+ removeFromCartBtns.forEach(btn => {
228
+ btn.addEventListener('click', (e) => {
229
+ e.preventDefault();
230
+ const productId = btn.getAttribute('data-product-id');
231
+
232
+ if (productId) {
233
+ this.removeFromCart(productId);
234
+ }
235
+ });
236
+ });
237
+ },
238
+
239
+ async addToCart(productId, quantity = 1, skipButtonUpdate = false) {
240
+ try {
241
+ // Only update button state if not skipping (e.g., when called from modal)
242
+ let btn = null;
243
+ if (!skipButtonUpdate) {
244
+ btn = document.querySelector(`[data-product-id="${productId}"]`);
245
+ if (btn) {
246
+ btn.disabled = true;
247
+ btn.innerHTML = '<span class="loading-spinner"></span> Adding...';
248
+ }
249
+ }
250
+
251
+ const response = await fetch('/webstoreapi/carts/add', {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ 'X-Requested-With': 'XMLHttpRequest'
256
+ },
257
+ body: JSON.stringify({
258
+ productId: productId,
259
+ quantity: quantity
260
+ })
261
+ });
262
+
263
+ // Check content type before parsing - handle HTML responses
264
+ const contentType = response.headers.get('content-type') || '';
265
+ const isHtml = contentType.includes('text/html');
266
+
267
+ // Helper function to open login modal with fallbacks
268
+ const openLogin = () => {
269
+ console.log('[AddToCart] Attempting to open login modal');
270
+ if (this.openLoginModal && typeof this.openLoginModal === 'function') {
271
+ console.log('[AddToCart] Using this.openLoginModal');
272
+ this.openLoginModal();
273
+ return true;
274
+ } else if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
275
+ console.log('[AddToCart] Using window.Theme.openLoginModal');
276
+ window.Theme.openLoginModal();
277
+ return true;
278
+ } else {
279
+ console.log('[AddToCart] Using fallback: triggering login modal via data attribute');
280
+ // Fallback: trigger login modal via data attribute
281
+ const loginTrigger = document.querySelector('[data-login-modal-trigger]');
282
+ if (loginTrigger) {
283
+ loginTrigger.click();
284
+ return true;
285
+ } else {
286
+ console.error('[AddToCart] No login trigger found and openLoginModal not available');
287
+ return false;
288
+ }
289
+ }
290
+ };
291
+
292
+ // If response is HTML (error page), treat as authentication required for 404/401
293
+ if (isHtml && !response.ok) {
294
+ console.log('[AddToCart] HTML error page received, status:', response.status);
295
+ if (response.status === 404 || response.status === 401) {
296
+ console.log('[AddToCart] HTML error page with 404/401, opening login modal');
297
+ openLogin();
298
+ if (!skipButtonUpdate && btn) {
299
+ btn.innerHTML = 'Add to Cart';
300
+ btn.disabled = false;
301
+ }
302
+ return;
303
+ }
304
+ }
305
+
306
+ // Try to parse JSON response
307
+ let data;
308
+ try {
309
+ data = await response.json();
310
+ } catch (parseError) {
311
+ // If JSON parsing fails and we have a 404/401, treat as auth required
312
+ if (!response.ok && (response.status === 404 || response.status === 401)) {
313
+ console.log('[AddToCart] JSON parse error with 404/401 status, opening login modal');
314
+ openLogin();
315
+ if (!skipButtonUpdate && btn) {
316
+ btn.innerHTML = 'Add to Cart';
317
+ btn.disabled = false;
318
+ }
319
+ return;
320
+ }
321
+ // Re-throw if it's not a 404/401
322
+ throw parseError;
323
+ }
324
+
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
+ // Check if response indicates authentication is required (check both status and data)
331
+ if ((!response.ok || !data.success) && data.requiresAuth) {
332
+ console.log('[AddToCart] Authentication required, opening login modal');
333
+ openLogin();
334
+ // Reset button state
335
+ if (!skipButtonUpdate && btn) {
336
+ btn.innerHTML = 'Add to Cart';
337
+ btn.disabled = false;
338
+ }
339
+ return;
340
+ }
341
+
342
+ // Also check for 401 or 404 status code even if requiresAuth flag is not set
343
+ if (!response.ok && (response.status === 401 || response.status === 404)) {
344
+ console.log('[AddToCart] 401/404 status detected, opening login modal');
345
+ openLogin();
346
+ // Reset button state
347
+ if (!skipButtonUpdate && btn) {
348
+ btn.innerHTML = 'Add to Cart';
349
+ btn.disabled = false;
350
+ }
351
+ return;
352
+ }
353
+
354
+ 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
357
+ try {
358
+ // Fetch full cart data to get updated count, total, and all cart information
359
+ const cartResponse = await fetch('/webstoreapi/carts', {
360
+ method: 'GET',
361
+ credentials: 'same-origin',
362
+ headers: { 'Accept': 'application/json' }
363
+ });
364
+
365
+ if (cartResponse.ok) {
366
+ const cartData = await cartResponse.json();
367
+ if (cartData.success && cartData.data) {
368
+ // Use the full cart data from the API response (includes total, itemCount, etc.)
369
+ data.data = cartData.data;
370
+
371
+ // Update cart count badge instantly using CartManager
372
+ if (window.CartManager && typeof window.CartManager.dispatchCartUpdated === 'function') {
373
+ const cartCount = cartData.data.itemCount || 0;
374
+ window.CartManager.dispatchCartUpdated({
375
+ itemCount: cartCount,
376
+ cart: cartData.data
377
+ });
378
+ }
379
+ }
380
+ }
381
+ } 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
+ }
402
+ }
403
+ }
404
+ // Update cart UI with the latest data (includes total and count)
405
+ this.updateCartUI(data.data);
406
+ this.showNotification('Product added to cart!', 'success');
407
+
408
+ // Update button state (only if not skipping updates)
409
+ if (!skipButtonUpdate && btn) {
410
+ btn.innerHTML = 'Added to Cart';
411
+ btn.classList.add('btn-success');
412
+ setTimeout(() => {
413
+ btn.innerHTML = 'Add to Cart';
414
+ btn.classList.remove('btn-success');
415
+ btn.disabled = false;
416
+ }, 2000);
417
+ }
418
+ } 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();
430
+ 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
+ }
439
+ };
440
+ openLogin();
441
+ // Reset button state
442
+ if (!skipButtonUpdate && btn) {
443
+ btn.innerHTML = 'Add to Cart';
444
+ btn.disabled = false;
445
+ }
446
+ return;
447
+ }
448
+ throw new Error(data.error || data.message || 'Failed to add product to cart');
449
+ }
450
+ } catch (error) {
451
+ console.error('Error adding to cart:', error);
452
+ console.error('Error details:', {
453
+ message: error.message,
454
+ stack: error.stack,
455
+ name: error.name
456
+ });
457
+
458
+ // Helper function to open login modal
459
+ const openLogin = () => {
460
+ if (this.openLoginModal && typeof this.openLoginModal === 'function') {
461
+ this.openLoginModal();
462
+ } else if (window.Theme && window.Theme.openLoginModal && typeof window.Theme.openLoginModal === 'function') {
463
+ window.Theme.openLoginModal();
464
+ } else {
465
+ const loginTrigger = document.querySelector('[data-login-modal-trigger]');
466
+ if (loginTrigger) {
467
+ loginTrigger.click();
468
+ }
469
+ }
470
+ };
471
+
472
+ // Check for authentication-related errors
473
+ if (error.message && (
474
+ error.message.includes('Authentication required') ||
475
+ error.message.includes('Please sign in') ||
476
+ error.message.includes('unauthorized')
477
+ )) {
478
+ console.log('[AddToCart] Authentication required detected in error message');
479
+ openLogin();
480
+ // Reset button state (only if not skipping updates)
481
+ if (!skipButtonUpdate) {
482
+ const btn = document.querySelector(`[data-product-id="${productId}"]`);
483
+ if (btn) {
484
+ btn.innerHTML = 'Add to Cart';
485
+ btn.disabled = false;
486
+ }
487
+ }
488
+ return;
489
+ }
490
+
491
+ this.showNotification(error.message || 'Error adding product to cart', 'error');
492
+
493
+ // Reset button state (only if not skipping updates)
494
+ if (!skipButtonUpdate) {
495
+ const btn = document.querySelector(`[data-product-id="${productId}"]`);
496
+ if (btn) {
497
+ btn.innerHTML = 'Add to Cart';
498
+ btn.disabled = false;
499
+ }
500
+ }
501
+ }
502
+ },
503
+
504
+ async updateCartItem(productId, quantity) {
505
+ try {
506
+ const response = await fetch('/webstoreapi/carts/update', {
507
+ method: 'PUT',
508
+ headers: {
509
+ 'Content-Type': 'application/json',
510
+ 'X-Requested-With': 'XMLHttpRequest'
511
+ },
512
+ body: JSON.stringify({
513
+ productId: productId,
514
+ quantity: quantity
515
+ })
516
+ });
517
+
518
+ const data = await response.json();
519
+
520
+ if (data.success) {
521
+ // Update cart UI and dispatch event
522
+ this.updateCartUI(data.data);
523
+ this.showNotification('Cart updated', 'success');
524
+ } else {
525
+ throw new Error(data.message || 'Failed to update cart');
526
+ }
527
+ } catch (error) {
528
+ console.error('Error updating cart:', error);
529
+ this.showNotification('Error updating cart', 'error');
530
+ }
531
+ },
532
+
533
+ async removeFromCart(productId) {
534
+ try {
535
+ const response = await fetch('/webstoreapi/carts/remove', {
536
+ method: 'DELETE',
537
+ headers: {
538
+ 'Content-Type': 'application/json',
539
+ 'X-Requested-With': 'XMLHttpRequest'
540
+ },
541
+ body: JSON.stringify({
542
+ productId: productId
543
+ })
544
+ });
545
+
546
+ const data = await response.json();
547
+
548
+ if (data.success) {
549
+ // Update cart UI and dispatch event
550
+ this.updateCartUI(data.data);
551
+ this.showNotification('Product removed from cart', 'success');
552
+
553
+ // Remove product element from cart page
554
+ const productElement = document.querySelector(`[data-product-id="${productId}"]`)?.closest('.cart-item');
555
+ if (productElement) {
556
+ productElement.style.opacity = '0';
557
+ setTimeout(() => {
558
+ productElement.remove();
559
+ }, 300);
560
+ }
561
+ } else {
562
+ throw new Error(data.message || 'Failed to remove product from cart');
563
+ }
564
+ } catch (error) {
565
+ console.error('Error removing from cart:', error);
566
+ this.showNotification('Error removing product from cart', 'error');
567
+ }
568
+ },
569
+
570
+ updateCartUI(cart) {
571
+ // Extract count from various possible locations in the response
572
+ let count = 0;
573
+ if (cart) {
574
+ count = cart.itemCount ||
575
+ cart.cartQuantity ||
576
+ (cart.items && cart.items.length) ||
577
+ 0;
578
+ }
579
+
580
+ console.log('[Theme] updateCartUI called with count:', count, 'cart:', cart);
581
+
582
+ // Use CartManager as single source of truth for all cart count updates
583
+ // This ensures header badge and drawer badge stay in sync
584
+ // Both now use [data-cart-count] attribute
585
+ if (window.CartManager) {
586
+ // CartManager will update all [data-cart-count] elements (header and drawer)
587
+ window.CartManager.updateCartBadge(count);
588
+ } else {
589
+ // Fallback if CartManager not loaded yet
590
+ const cartCountElements = document.querySelectorAll('[data-cart-count]');
591
+ cartCountElements.forEach(element => {
592
+ element.textContent = count;
593
+ element.setAttribute('data-cart-count', count.toString());
594
+ const isDrawerTitle = element.closest('.cart-drawer-title');
595
+ if (count > 0) {
596
+ element.removeAttribute('style');
597
+ } else {
598
+ // Hide header badges when count is 0, but keep drawer title visible
599
+ if (!isDrawerTitle) {
600
+ element.style.display = 'none';
601
+ } else {
602
+ element.removeAttribute('style');
603
+ }
604
+ }
605
+ });
606
+ }
607
+
608
+ // Also update legacy .cart-count selector for backward compatibility
609
+ const cartCount = document.querySelector('.cart-count');
610
+ if (cartCount) {
611
+ cartCount.textContent = count;
612
+ }
613
+
614
+ // Update cart total
615
+ const cartTotal = document.querySelector('.cart-total');
616
+ if (cartTotal) {
617
+ cartTotal.textContent = this.formatMoney(cart.total || 0);
618
+ }
619
+
620
+ // Hide cart text (no MRP/price display - only show icon and quantity badge)
621
+ const cartTextElements = document.querySelectorAll('.site-header__cart-text');
622
+ cartTextElements.forEach(el => {
623
+ el.style.display = 'none';
624
+ });
625
+
626
+ // Update cart link
627
+ const cartLink = document.querySelector('.cart-link');
628
+ if (cartLink) {
629
+ cartLink.setAttribute('aria-label', `Shopping cart with ${count} item${count !== 1 ? 's' : ''}`);
630
+ }
631
+
632
+ // Dispatch cart:updated event for global synchronization
633
+ if (window.CartManager) {
634
+ window.CartManager.dispatchCartUpdated(cart);
635
+ } else {
636
+ // Fallback: dispatch event directly if CartManager not loaded yet
637
+ const event = new CustomEvent('cart:updated', {
638
+ detail: {
639
+ count: count,
640
+ cart: cart
641
+ },
642
+ bubbles: true,
643
+ cancelable: true
644
+ });
645
+ document.dispatchEvent(event);
646
+ }
647
+ },
648
+
649
+ // Product actions (quick view, wishlist, etc.)
650
+ initProductActions() {
651
+ // Quick view functionality
652
+ const quickViewBtns = document.querySelectorAll('.quick-view-btn');
653
+
654
+ quickViewBtns.forEach(btn => {
655
+ btn.addEventListener('click', (e) => {
656
+ e.preventDefault();
657
+ const productId = btn.getAttribute('data-product-id');
658
+
659
+ if (productId) {
660
+ this.openQuickView(productId);
661
+ }
662
+ });
663
+ });
664
+
665
+ // Wishlist functionality
666
+ const wishlistBtns = document.querySelectorAll('.wishlist-btn');
667
+
668
+ wishlistBtns.forEach(btn => {
669
+ btn.addEventListener('click', (e) => {
670
+ e.preventDefault();
671
+ const productId = btn.getAttribute('data-product-id');
672
+
673
+ if (productId) {
674
+ this.toggleWishlist(productId, btn);
675
+ }
676
+ });
677
+ });
678
+ },
679
+
680
+ async openQuickView(productId) {
681
+ try {
682
+ // Show loading state
683
+ this.showNotification('Loading product...', 'info');
684
+
685
+ const response = await fetch(`/webstoreapi/products/${productId}`);
686
+ const product = await response.json();
687
+
688
+ if (product) {
689
+ this.showQuickViewModal(product);
690
+ } else {
691
+ throw new Error('Product not found');
692
+ }
693
+ } catch (error) {
694
+ console.error('Error loading product:', error);
695
+ this.showNotification('Error loading product', 'error');
696
+ }
697
+ },
698
+
699
+ showQuickViewModal(product) {
700
+ // Create modal HTML
701
+ const modalHTML = `
702
+ <div class="quick-view-modal" id="quick-view-modal">
703
+ <div class="quick-view-content">
704
+ <button class="quick-view-close" aria-label="Close quick view">
705
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
706
+ <line x1="18" y1="6" x2="6" y2="18"></line>
707
+ <line x1="6" y1="6" x2="18" y2="18"></line>
708
+ </svg>
709
+ </button>
710
+ <div class="quick-view-body">
711
+ <div class="quick-view-image">
712
+ <img src="${product.images?.[0] || '/assets/placeholder-product.jpg'}" alt="${product.title}">
713
+ </div>
714
+ <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>
718
+ <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
721
+ </button>
722
+ <a href="/${product.slug || product.id}" class="btn btn-outline">
723
+ View Details
724
+ </a>
725
+ </div>
726
+ </div>
727
+ </div>
728
+ </div>
729
+ </div>
730
+ `;
731
+
732
+ // Add modal to page
733
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
734
+ document.body.classList.add('modal-open');
735
+
736
+ // Add event listeners
737
+ const modal = document.getElementById('quick-view-modal');
738
+ const closeBtn = modal.querySelector('.quick-view-close');
739
+
740
+ closeBtn.addEventListener('click', () => {
741
+ this.closeQuickViewModal();
742
+ });
743
+
744
+ modal.addEventListener('click', (e) => {
745
+ if (e.target === modal) {
746
+ this.closeQuickViewModal();
747
+ }
748
+ });
749
+
750
+ // Add to cart functionality
751
+ const addToCartBtn = modal.querySelector('.add-to-cart-btn');
752
+ addToCartBtn.addEventListener('click', (e) => {
753
+ e.preventDefault();
754
+ this.addToCart(product.productId || product.id, 1);
755
+ this.closeQuickViewModal();
756
+ });
757
+ },
758
+
759
+ closeQuickViewModal() {
760
+ const modal = document.getElementById('quick-view-modal');
761
+ if (modal) {
762
+ modal.remove();
763
+ document.body.classList.remove('modal-open');
764
+ }
765
+ },
766
+
767
+ async toggleWishlist(productId, btn) {
768
+ try {
769
+ const isInWishlist = btn.classList.contains('in-wishlist');
770
+
771
+ const response = await fetch('/webstoreapi/wishlist/toggle', {
772
+ method: 'POST',
773
+ headers: {
774
+ 'Content-Type': 'application/json',
775
+ 'X-Requested-With': 'XMLHttpRequest'
776
+ },
777
+ body: JSON.stringify({
778
+ productId: productId
779
+ })
780
+ });
781
+
782
+ const data = await response.json();
783
+
784
+ if (data.success) {
785
+ if (isInWishlist) {
786
+ btn.classList.remove('in-wishlist');
787
+ btn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>';
788
+ this.showNotification('Removed from wishlist', 'success');
789
+ } else {
790
+ btn.classList.add('in-wishlist');
791
+ btn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>';
792
+ this.showNotification('Added to wishlist', 'success');
793
+ }
794
+ } else {
795
+ throw new Error(data.message || 'Failed to update wishlist');
796
+ }
797
+ } catch (error) {
798
+ console.error('Error updating wishlist:', error);
799
+ this.showNotification('Error updating wishlist', 'error');
800
+ }
801
+ },
802
+
803
+ // Notification system
804
+ /**
805
+ * Initialize global notification container used for toast messages.
806
+ * The container is positioned at the bottom center of the viewport so that
807
+ * all entry points (product cards, product detail, quick view, etc.)
808
+ * share the same visual behavior.
809
+ */
810
+ initNotifications() {
811
+ // Create notification container if it doesn't exist
812
+ if (!document.querySelector('.notifications-container')) {
813
+ const container = document.createElement('div');
814
+ container.className = 'notifications-container';
815
+ container.style.cssText = `
816
+ position: fixed;
817
+ left: 50%;
818
+ bottom: 16px;
819
+ transform: translateX(-50%);
820
+ z-index: var(--z-toast, 1080);
821
+ pointer-events: none;
822
+ display: flex;
823
+ flex-direction: column;
824
+ align-items: center;
825
+ gap: 8px;
826
+ `;
827
+ document.body.appendChild(container);
828
+ }
829
+ },
830
+
831
+ /**
832
+ * Show a toast notification at the bottom center of the viewport.
833
+ * @param {string} message - Message to display in the toast
834
+ * @param {('success'|'error'|'warning'|'info')} [type='success'] - Notification type
835
+ * @param {number} [duration=3000] - Time in ms before the toast auto-dismisses
836
+ */
837
+ showNotification(message, type = 'success', duration = 3000) {
838
+ const container = document.querySelector('.notifications-container');
839
+ if (!container) return;
840
+
841
+ const notification = document.createElement('div');
842
+ notification.className = `notification notification-${type}`;
843
+ notification.textContent = message;
844
+ notification.style.cssText = `
845
+ pointer-events: auto;
846
+ margin: 0;
847
+ animation: toastInUp 0.25s var(--ease-out, ease-out) forwards;
848
+ `;
849
+
850
+ container.appendChild(notification);
851
+
852
+ // Auto remove after duration
853
+ setTimeout(() => {
854
+ notification.style.animation = 'toastOutDown 0.2s var(--ease-in, ease-in) forwards';
855
+ setTimeout(() => {
856
+ if (notification.parentNode) {
857
+ notification.parentNode.removeChild(notification);
858
+ }
859
+ }, 220);
860
+ }, duration);
861
+ },
862
+
863
+ // Lazy loading for images
864
+ initLazyLoading() {
865
+ if ('IntersectionObserver' in window) {
866
+ const imageObserver = new IntersectionObserver((entries, observer) => {
867
+ entries.forEach(entry => {
868
+ if (entry.isIntersecting) {
869
+ const img = entry.target;
870
+ img.src = img.dataset.src;
871
+ img.classList.remove('lazy');
872
+ img.classList.add('loaded');
873
+ observer.unobserve(img);
874
+ }
875
+ });
876
+ });
877
+
878
+ const lazyImages = document.querySelectorAll('img[data-src]');
879
+ lazyImages.forEach(img => imageObserver.observe(img));
880
+ }
881
+ },
882
+
883
+ // Scroll effects
884
+ initScrollEffects() {
885
+ // Sticky header
886
+ const header = document.querySelector('.site-header');
887
+ if (header) {
888
+ let lastScrollY = window.scrollY;
889
+
890
+ window.addEventListener('scroll', () => {
891
+ const currentScrollY = window.scrollY;
892
+
893
+ if (currentScrollY > 100) {
894
+ header.classList.add('header-scrolled');
895
+ } else {
896
+ header.classList.remove('header-scrolled');
897
+ }
898
+
899
+ lastScrollY = currentScrollY;
900
+ });
901
+ }
902
+
903
+ // Fade in animations
904
+ const fadeElements = document.querySelectorAll('.fade-in');
905
+ if (fadeElements.length > 0 && 'IntersectionObserver' in window) {
906
+ const fadeObserver = new IntersectionObserver((entries) => {
907
+ entries.forEach(entry => {
908
+ if (entry.isIntersecting) {
909
+ entry.target.classList.add('fade-in-visible');
910
+ }
911
+ });
912
+ });
913
+
914
+ fadeElements.forEach(el => fadeObserver.observe(el));
915
+ }
916
+ },
917
+
918
+ // Form validation
919
+ initFormValidation() {
920
+ const forms = document.querySelectorAll('form[data-validate]');
921
+
922
+ forms.forEach(form => {
923
+ form.addEventListener('submit', (e) => {
924
+ if (!this.validateForm(form)) {
925
+ e.preventDefault();
926
+ }
927
+ });
928
+
929
+ // Real-time validation
930
+ const inputs = form.querySelectorAll('input, select, textarea');
931
+ inputs.forEach(input => {
932
+ input.addEventListener('blur', () => {
933
+ this.validateField(input);
934
+ });
935
+ });
936
+ });
937
+ },
938
+
939
+ validateForm(form) {
940
+ let isValid = true;
941
+ const inputs = form.querySelectorAll('input[required], select[required], textarea[required]');
942
+
943
+ inputs.forEach(input => {
944
+ if (!this.validateField(input)) {
945
+ isValid = false;
946
+ }
947
+ });
948
+
949
+ return isValid;
950
+ },
951
+
952
+ validateField(field) {
953
+ const value = field.value.trim();
954
+ const type = field.type;
955
+ const required = field.hasAttribute('required');
956
+ let isValid = true;
957
+ let message = '';
958
+
959
+ // Clear previous error
960
+ this.clearFieldError(field);
961
+
962
+ // Required validation
963
+ if (required && !value) {
964
+ isValid = false;
965
+ message = 'This field is required';
966
+ }
967
+
968
+ // Type-specific validation
969
+ if (value && type === 'email') {
970
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
971
+ if (!emailRegex.test(value)) {
972
+ isValid = false;
973
+ message = 'Please enter a valid email address';
974
+ }
975
+ }
976
+
977
+ if (value && type === 'tel') {
978
+ // Check if this field has intl-tel-input instance
979
+ let phoneIsValid = false;
980
+
981
+ // Check for intl-tel-input instances
982
+ if (field.id === 'shipping-phone' && window.shippingPhoneIti) {
983
+ phoneIsValid = window.shippingPhoneIti.isValidNumber();
984
+ } else if (field.id === 'address-phone' && window.addressPhoneIti) {
985
+ phoneIsValid = window.addressPhoneIti.isValidNumber();
986
+ } else if (field.id === 'profile-phone' && window.profilePhoneIti) {
987
+ phoneIsValid = window.profilePhoneIti.isValidNumber();
988
+ } else if (field.id === 'login-phone-otp' && window.loginPhoneIti) {
989
+ phoneIsValid = window.loginPhoneIti.isValidNumber();
990
+ } else {
991
+ // Fallback to regex validation for fields without intl-tel-input
992
+ const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
993
+ phoneIsValid = phoneRegex.test(value.replace(/[\s\-\(\)]/g, ''));
994
+ }
995
+
996
+ if (!phoneIsValid) {
997
+ isValid = false;
998
+ message = 'Please enter a valid phone number';
999
+ }
1000
+ }
1001
+
1002
+ if (value && field.hasAttribute('minlength')) {
1003
+ const minLength = parseInt(field.getAttribute('minlength'));
1004
+ if (value.length < minLength) {
1005
+ isValid = false;
1006
+ message = `Must be at least ${minLength} characters`;
1007
+ }
1008
+ }
1009
+
1010
+ if (value && field.hasAttribute('maxlength')) {
1011
+ const maxLength = parseInt(field.getAttribute('maxlength'));
1012
+ if (value.length > maxLength) {
1013
+ isValid = false;
1014
+ message = `Must be no more than ${maxLength} characters`;
1015
+ }
1016
+ }
1017
+
1018
+ // Show error if invalid
1019
+ if (!isValid) {
1020
+ this.showFieldError(field, message);
1021
+ }
1022
+
1023
+ return isValid;
1024
+ },
1025
+
1026
+ showFieldError(field, message) {
1027
+ field.classList.add('error');
1028
+
1029
+ const errorElement = document.createElement('div');
1030
+ errorElement.className = 'form-error';
1031
+ errorElement.textContent = message;
1032
+
1033
+ field.parentNode.appendChild(errorElement);
1034
+ },
1035
+
1036
+ clearFieldError(field) {
1037
+ field.classList.remove('error');
1038
+
1039
+ const errorElement = field.parentNode.querySelector('.form-error');
1040
+ if (errorElement) {
1041
+ errorElement.remove();
1042
+ }
1043
+ },
1044
+
1045
+ // Utility functions
1046
+ formatMoney(amount, currency = 'USD') {
1047
+ return new Intl.NumberFormat('en-US', {
1048
+ style: 'currency',
1049
+ currency: currency
1050
+ }).format(amount / 100);
1051
+ },
1052
+
1053
+ // Format money helper for product cards (amounts not in cents)
1054
+ formatMoneyProductCard(amount) {
1055
+ if (amount === null || amount === undefined || isNaN(amount)) {
1056
+ return '0.00';
1057
+ }
1058
+ const num = parseFloat(amount);
1059
+ if (isNaN(num)) return String(amount);
1060
+ // Get currency symbol from shop settings
1061
+ const currencySymbol = window.__SHOP_CURRENCY_SYMBOL__ ||
1062
+ (document.body && document.body.dataset.shopCurrencySymbol) ||
1063
+ '$';
1064
+ const formatted = num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
1065
+ return currencySymbol + formatted;
1066
+ },
1067
+
1068
+ // Show add to cart modal for product cards
1069
+ async showAddToCartModal(productCard, addToCartBtn) {
1070
+ const modal = document.getElementById('add-to-cart-modal');
1071
+ if (!modal) {
1072
+ console.error('[AddToCartModal] Modal element not found');
1073
+ return;
1074
+ }
1075
+
1076
+ console.log('[AddToCartModal] Opening modal for product card:', productCard);
1077
+
1078
+ // Extract product data from card
1079
+ const baseProductId = productCard.dataset.productId;
1080
+ // 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 || '';
1089
+
1090
+ // Fetch full product data using getProductById API endpoint (routes/api.js:3455)
1091
+ // This endpoint calls req.apiClient.getProductById() to get complete product details
1092
+ let fullProductData = null;
1093
+ try {
1094
+ const response = await fetch(`/webstoreapi/products/${baseProductId}`, {
1095
+ method: 'GET',
1096
+ headers: {
1097
+ 'Content-Type': 'application/json',
1098
+ 'X-Requested-With': 'XMLHttpRequest'
1099
+ }
1100
+ });
1101
+ if (response.ok) {
1102
+ const result = await response.json();
1103
+ console.log('[AddToCartModal] Product API response:', result);
1104
+
1105
+ // Endpoint returns: { success: true, data: product }
1106
+ if (result.success && result.data) {
1107
+ fullProductData = result.data;
1108
+ console.log('[AddToCartModal] Full product data retrieved:', fullProductData);
1109
+
1110
+ // If product has combinations or subscriptions, redirect to product detail page
1111
+ const hasCombinations = fullProductData.combinations && fullProductData.combinations.length > 0;
1112
+ const hasSubscriptions = fullProductData.subscriptions && fullProductData.subscriptions.length > 0;
1113
+
1114
+ if (hasCombinations || hasSubscriptions) {
1115
+ const productSlug = fullProductData.slug || fullProductData.id || baseProductId;
1116
+ window.location.href = `/${productSlug}`;
1117
+ return;
1118
+ }
1119
+ } else {
1120
+ console.warn('[AddToCartModal] API response missing success or data:', result);
1121
+ }
1122
+ } else {
1123
+ // Response not OK - try to parse error
1124
+ const errorText = await response.text();
1125
+ console.error('[AddToCartModal] API response not OK:', response.status, errorText);
1126
+ try {
1127
+ const errorJson = JSON.parse(errorText);
1128
+ console.error('[AddToCartModal] API error details:', errorJson);
1129
+ } catch (e) {
1130
+ // Error text is not JSON, already logged
1131
+ }
1132
+ }
1133
+ } catch (error) {
1134
+ console.error('Error fetching product data:', error);
1135
+ // Continue with modal if fetch fails
1136
+ }
1137
+
1138
+ // Build variantData from fullProductData if available, otherwise from script tag
1139
+ let variantData = null;
1140
+
1141
+ // Priority 1: Use fullProductData from API if it has variants/variations
1142
+ // API might return either 'variants' or 'variations' - handle both
1143
+ const apiVariants = fullProductData?.variants || fullProductData?.variations || null;
1144
+ if (fullProductData && apiVariants && apiVariants.length > 0) {
1145
+ console.log('[AddToCartModal] Using variant data from API response, variant count:', apiVariants.length);
1146
+ // Transform API variants to match expected format (same as product-card JSON structure)
1147
+ 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
+ }
1161
+
1162
+ // Determine availability - check multiple possible fields
1163
+ const inStock = variant.inStock !== false && variant.inStock !== undefined ? variant.inStock : true;
1164
+ const available = variant.available !== false && variant.available !== undefined ? variant.available : inStock;
1165
+
1166
+ return {
1167
+ productId: variant.productId || variant.id,
1168
+ price: variant.prices?.price || variant.price || 0,
1169
+ mrp: variant.prices?.mrp || variant.mrp || 0,
1170
+ inStock: inStock,
1171
+ available: available,
1172
+ options: variant.options || [],
1173
+ images: imageUrls
1174
+ };
1175
+ });
1176
+
1177
+ // 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
+ }
1186
+
1187
+ variantData = {
1188
+ variants: transformedVariants,
1189
+ baseProductImage: baseImage,
1190
+ baseProductId: fullProductData.productId || fullProductData.id || baseProductId
1191
+ };
1192
+ console.log('[AddToCartModal] Transformed variant data:', variantData);
1193
+ } else {
1194
+ // 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 + '"]');
1197
+ if (variantDataScript) {
1198
+ try {
1199
+ variantData = JSON.parse(variantDataScript.textContent);
1200
+ } catch (e) {
1201
+ console.error('Error parsing variant data from script tag:', e);
1202
+ }
1203
+ }
1204
+ }
1205
+
1206
+ // Update modal content
1207
+ const modalTitle = modal.querySelector('#add-to-cart-modal-title');
1208
+ const modalImage = modal.querySelector('#add-to-cart-modal-image');
1209
+ const modalPriceCurrent = modal.querySelector('#add-to-cart-modal-price-current');
1210
+ const modalPriceOriginal = modal.querySelector('#add-to-cart-modal-price-original');
1211
+ const modalQtyInput = modal.querySelector('#add-to-cart-modal-qty-input');
1212
+ const modalVariantOptions = modal.querySelector('#add-to-cart-modal-variant-options');
1213
+
1214
+ // CRITICAL: Reset all modal state FIRST before setting up new product
1215
+ // This ensures clean state when opening modal for second variation product
1216
+ modal._isAddingToCart = false;
1217
+ modal._selectedOptions = {};
1218
+ if (modal._variantData) {
1219
+ delete modal._variantData;
1220
+ }
1221
+
1222
+ // Reset confirm button state immediately (in case it was left in "Adding..." state from previous action)
1223
+ const confirmBtn = modal.querySelector('#add-to-cart-modal-confirm-btn');
1224
+ if (confirmBtn) {
1225
+ confirmBtn.disabled = false;
1226
+ confirmBtn.textContent = 'ADD TO CART';
1227
+ confirmBtn.innerHTML = 'ADD TO CART'; // Also reset innerHTML in case it had loading spinner
1228
+ }
1229
+
1230
+ // Re-enable variant selection buttons if they exist
1231
+ this.disableModalVariantSelection(modal, false);
1232
+
1233
+ if (modalTitle) modalTitle.textContent = productTitle;
1234
+ if (modalImage && baseImageSrc) {
1235
+ modalImage.src = baseImageSrc;
1236
+ modalImage.alt = productTitle;
1237
+ }
1238
+
1239
+ // Store variant data in modal for later use
1240
+ modal._variantData = variantData;
1241
+
1242
+ // Get initial variant (first available or first)
1243
+ let initialVariant = null;
1244
+ let initialPrice = 0;
1245
+ let initialMrp = 0;
1246
+ if (variantData && variantData.variants && variantData.variants.length > 0) {
1247
+ initialVariant = variantData.variants.find(v => v.inStock) || variantData.variants[0];
1248
+ if (initialVariant) {
1249
+ initialPrice = initialVariant.price || 0;
1250
+ initialMrp = initialVariant.mrp || 0;
1251
+ modal.dataset.productId = initialVariant.productId;
1252
+ }
1253
+ } else {
1254
+ // No variants, use base product
1255
+ const priceCurrent = productCard.querySelector('.product-price-current');
1256
+ const priceOriginal = productCard.querySelector('.product-price-original');
1257
+ initialPrice = priceCurrent?.getAttribute('data-price-current') ||
1258
+ parseFloat(priceCurrent?.textContent?.replace(/[^0-9.]/g, '')) || 0;
1259
+ initialMrp = priceOriginal?.getAttribute('data-price-original') || 0;
1260
+ modal.dataset.productId = baseProductId;
1261
+ }
1262
+
1263
+ // Update initial price
1264
+ if (modalPriceCurrent) {
1265
+ modalPriceCurrent.textContent = this.formatMoneyProductCard(initialPrice);
1266
+ }
1267
+ if (modalPriceOriginal && initialMrp > initialPrice) {
1268
+ modalPriceOriginal.textContent = this.formatMoneyProductCard(initialMrp);
1269
+ modalPriceOriginal.style.display = 'inline';
1270
+ } else if (modalPriceOriginal) {
1271
+ modalPriceOriginal.style.display = 'none';
1272
+ }
1273
+ if (modalQtyInput) {
1274
+ modalQtyInput.value = '1';
1275
+ }
1276
+
1277
+ // Render variant options if variants exist
1278
+ if (variantData && variantData.variants && variantData.variants.length > 0 && modalVariantOptions) {
1279
+ this.renderModalVariantOptions(modal, variantData);
1280
+ } else if (modalVariantOptions) {
1281
+ modalVariantOptions.innerHTML = '';
1282
+ }
1283
+
1284
+ // Setup quantity controls
1285
+ this.setupModalQuantityControls(modal);
1286
+
1287
+ // Setup confirm button
1288
+ this.setupModalConfirmButton(modal, modal.dataset.productId);
1289
+
1290
+ // Setup modal close handlers
1291
+ this.setupModalCloseHandlers(modal);
1292
+
1293
+ // Show modal
1294
+ modal.classList.add('active');
1295
+ document.body.classList.add('modal-open');
1296
+
1297
+ // Focus on first variant option or quantity input for accessibility
1298
+ const firstOptionBtn = modalVariantOptions?.querySelector('.option-value:not(:disabled)');
1299
+ if (firstOptionBtn) {
1300
+ setTimeout(() => firstOptionBtn.focus(), 100);
1301
+ } else if (modalQtyInput) {
1302
+ setTimeout(() => modalQtyInput.focus(), 100);
1303
+ }
1304
+ },
1305
+
1306
+ // Render variant options in modal (exact same as product page buildOptionGroups and renderOptionGroups)
1307
+ renderModalVariantOptions(modal, variantData) {
1308
+ const optionsContainer = modal.querySelector('#add-to-cart-modal-variant-options');
1309
+ if (!optionsContainer || !variantData || !variantData.variants) return;
1310
+
1311
+ const variants = variantData.variants || [];
1312
+ if (variants.length === 0) return;
1313
+
1314
+ // Build option groups (exact same logic as product page buildOptionGroups)
1315
+ const optionGroups = {};
1316
+
1317
+ variants.forEach(variation => {
1318
+ const options = variation.options || [];
1319
+
1320
+ options.forEach(option => {
1321
+ const optionName = (option.optionName || 'Option').toLowerCase();
1322
+ const cleanName = optionName.replace(/[^a-z]/g, ''); // Remove non-alphabetic chars
1323
+
1324
+ // Map common option names
1325
+ let mappedName = cleanName;
1326
+ if (cleanName.includes('color') || cleanName.includes('colour')) {
1327
+ mappedName = 'color';
1328
+ } else if (cleanName.includes('size')) {
1329
+ mappedName = 'size';
1330
+ }
1331
+
1332
+ if (!optionGroups[mappedName]) {
1333
+ optionGroups[mappedName] = {
1334
+ name: mappedName === 'color' ? 'Color' : (mappedName === 'size' ? 'Size' : option.optionName || 'Option'),
1335
+ type: option.displayType || (mappedName === 'color' ? 'color' : 'text'),
1336
+ values: new Map()
1337
+ };
1338
+ }
1339
+
1340
+ const value = option.value || '';
1341
+ if (!optionGroups[mappedName].values.has(value)) {
1342
+ // Check availability (same logic as product page)
1343
+ const isInStock = variation.inStock !== false;
1344
+ const isAvailable = (variation.available !== false && variation.available !== undefined) ? variation.available : isInStock;
1345
+ optionGroups[mappedName].values.set(value, {
1346
+ value: value,
1347
+ available: isAvailable,
1348
+ images: variation.images || []
1349
+ });
1350
+ }
1351
+ });
1352
+ });
1353
+
1354
+ // Clear container
1355
+ optionsContainer.innerHTML = '';
1356
+
1357
+ // Sort options: color first, then size, then others (same as product page)
1358
+ const sortedKeys = Object.keys(optionGroups).sort((a, b) => {
1359
+ const order = { color: 1, size: 2 };
1360
+ return (order[a] || 99) - (order[b] || 99);
1361
+ });
1362
+
1363
+ // Render option groups (exact same structure as product page renderOptionGroups)
1364
+ sortedKeys.forEach((key, groupIndex) => {
1365
+ const group = optionGroups[key];
1366
+ const optionDiv = document.createElement('div');
1367
+ optionDiv.className = 'product-option';
1368
+
1369
+ const label = document.createElement('label');
1370
+ label.className = 'option-label';
1371
+ label.textContent = group.name;
1372
+ optionDiv.appendChild(label);
1373
+
1374
+ const valuesDiv = document.createElement('div');
1375
+ valuesDiv.className = 'option-values';
1376
+
1377
+ const valuesArray = Array.from(group.values.values());
1378
+ valuesArray.forEach((valueObj, index) => {
1379
+ const isFirst = groupIndex === 0 && index === 0;
1380
+ if (isFirst && !modal._selectedOptions[key]) {
1381
+ modal._selectedOptions[key] = valueObj.value;
1382
+ }
1383
+
1384
+ const button = document.createElement('button');
1385
+ button.type = 'button';
1386
+ // Use same class structure as product page: option-value option-value-${group.type}
1387
+ button.className = `option-value option-value-${group.type} ${(modal._selectedOptions[key] === valueObj.value) ? 'selected' : ''} ${!valueObj.available ? 'disabled' : ''}`;
1388
+ button.dataset.optionKey = key;
1389
+ button.dataset.optionValue = valueObj.value;
1390
+ button.dataset.available = valueObj.available;
1391
+
1392
+ if (group.type === 'color') {
1393
+ // Color swatch (exact same as product page)
1394
+ const colorValue = valueObj.value.toLowerCase().trim();
1395
+ const colorMap = {
1396
+ 'red': '#ef4444',
1397
+ 'blue': '#3b82f6',
1398
+ 'green': '#10b981',
1399
+ 'yellow': '#fbbf24',
1400
+ 'black': '#000000',
1401
+ 'white': '#ffffff',
1402
+ 'gray': '#6b7280',
1403
+ 'grey': '#6b7280',
1404
+ 'pink': '#ec4899',
1405
+ 'purple': '#a855f7',
1406
+ 'orange': '#f97316',
1407
+ 'brown': '#92400e',
1408
+ 'navy': '#1e3a8a',
1409
+ 'tan': '#d4a574',
1410
+ 'beige': '#f5f5dc',
1411
+ 'cream': '#fffdd0',
1412
+ 'pumice': '#c8c5b9'
1413
+ };
1414
+
1415
+ const color = colorMap[colorValue] || colorValue;
1416
+ button.style.backgroundColor = color;
1417
+ button.style.borderColor = (color === '#ffffff' || color === '#fffdd0' || color === '#f5f5dc') ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.1)';
1418
+
1419
+ // Add screen reader text
1420
+ const srText = document.createElement('span');
1421
+ srText.className = 'sr-only';
1422
+ srText.textContent = valueObj.value;
1423
+ button.appendChild(srText);
1424
+ } else {
1425
+ // Text/Size button
1426
+ button.textContent = valueObj.value;
1427
+ }
1428
+
1429
+ if (!valueObj.available) {
1430
+ button.disabled = true;
1431
+ }
1432
+
1433
+ valuesDiv.appendChild(button);
1434
+ });
1435
+
1436
+ optionDiv.appendChild(valuesDiv);
1437
+ optionsContainer.appendChild(optionDiv);
1438
+ });
1439
+
1440
+ // Initialize default selection for remaining groups
1441
+ sortedKeys.forEach((key, groupIndex) => {
1442
+ if (groupIndex > 0 && !modal._selectedOptions[key]) {
1443
+ const firstBtn = optionsContainer.querySelector(`[data-option-key="${key}"]:not(:disabled)`);
1444
+ if (firstBtn) {
1445
+ modal._selectedOptions[key] = firstBtn.dataset.optionValue;
1446
+ firstBtn.classList.add('selected');
1447
+ }
1448
+ }
1449
+ });
1450
+
1451
+ // Setup event handler (same as product page)
1452
+ this.setupModalVariantEventHandlers(modal, variantData);
1453
+
1454
+ // Find initial variant
1455
+ this.findModalMatchingVariant(modal, variantData);
1456
+ },
1457
+
1458
+ // Setup variant event handlers (same as product page)
1459
+ setupModalVariantEventHandlers(modal, variantData) {
1460
+ const optionsContainer = modal.querySelector('#add-to-cart-modal-variant-options');
1461
+ if (!optionsContainer) return;
1462
+
1463
+ // Initialize flag if not exists
1464
+ if (modal._isAddingToCart === undefined) {
1465
+ modal._isAddingToCart = false;
1466
+ }
1467
+
1468
+ // Remove existing handler if any
1469
+ if (modal._variantClickHandler) {
1470
+ optionsContainer.removeEventListener('click', modal._variantClickHandler);
1471
+ }
1472
+
1473
+ // Create new handler (same logic as product page)
1474
+ modal._variantClickHandler = (e) => {
1475
+ // CRITICAL: Always prevent default and stop propagation first
1476
+ // This prevents the click from bubbling up and triggering add-to-cart or closing modal
1477
+ e.preventDefault();
1478
+ e.stopPropagation();
1479
+ e.stopImmediatePropagation();
1480
+
1481
+ // CRITICAL: Prevent variant selection during active add-to-cart request
1482
+ // This fixes the issue where modal closes when selecting second option during request
1483
+ if (modal._isAddingToCart) {
1484
+ console.log('[AddToCartModal] Variant selection blocked - add-to-cart in progress');
1485
+ return;
1486
+ }
1487
+
1488
+ const optionBtn = e.target.closest('.option-value');
1489
+ if (!optionBtn || optionBtn.disabled) return;
1490
+
1491
+ const optionKey = optionBtn.dataset.optionKey;
1492
+ const optionValue = optionBtn.dataset.optionValue;
1493
+
1494
+ if (!optionKey || !optionValue) return;
1495
+
1496
+ // Deselect other options in same group
1497
+ const optionGroup = optionBtn.closest('.product-option');
1498
+ optionGroup.querySelectorAll('.option-value').forEach(btn => {
1499
+ btn.classList.remove('selected');
1500
+ });
1501
+
1502
+ // Select clicked option
1503
+ optionBtn.classList.add('selected');
1504
+
1505
+ // Update selected options
1506
+ if (!modal._selectedOptions) {
1507
+ modal._selectedOptions = {};
1508
+ }
1509
+ modal._selectedOptions[optionKey] = optionValue;
1510
+
1511
+ // Find matching variant
1512
+ this.findModalMatchingVariant(modal, variantData);
1513
+ };
1514
+
1515
+ // Attach handler to container (event delegation)
1516
+ optionsContainer.addEventListener('click', modal._variantClickHandler);
1517
+ },
1518
+
1519
+ // Find matching variant (exact same logic as product page findMatchingVariant)
1520
+ findModalMatchingVariant(modal, variantData) {
1521
+ if (!variantData || !variantData.variants) return;
1522
+
1523
+ const variants = variantData.variants || [];
1524
+
1525
+ // If no variants, use base product
1526
+ if (variants.length === 0) {
1527
+ this.updateModalVariantUI(modal, {
1528
+ productId: variantData.baseProductId,
1529
+ price: 0,
1530
+ mrp: 0,
1531
+ inStock: true,
1532
+ available: true,
1533
+ images: []
1534
+ }, variantData);
1535
+ return;
1536
+ }
1537
+
1538
+ // Find matching variation
1539
+ for (const variation of variants) {
1540
+ const options = variation.options || [];
1541
+ let matches = true;
1542
+
1543
+ for (const [key, value] of Object.entries(modal._selectedOptions)) {
1544
+ const hasMatchingOption = options.some(opt => {
1545
+ const optName = (opt.optionName || 'Option').toLowerCase().replace(/[^a-z]/g, '');
1546
+ let mappedName = optName;
1547
+ if (optName.includes('color') || optName.includes('colour')) {
1548
+ mappedName = 'color';
1549
+ } else if (optName.includes('size')) {
1550
+ mappedName = 'size';
1551
+ }
1552
+ const optValue = opt.value || '';
1553
+ return mappedName === key && optValue === value;
1554
+ });
1555
+
1556
+ if (!hasMatchingOption) {
1557
+ matches = false;
1558
+ break;
1559
+ }
1560
+ }
1561
+
1562
+ if (matches) {
1563
+ // Check availability (same as product page logic)
1564
+ const isInStock = variation.inStock !== false;
1565
+ const isAvailable = (variation.available !== false && variation.available !== undefined) ? variation.available : isInStock;
1566
+
1567
+ this.updateModalVariantUI(modal, {
1568
+ productId: variation.productId,
1569
+ price: variation.price || 0,
1570
+ mrp: variation.mrp || 0,
1571
+ inStock: isInStock,
1572
+ available: isAvailable,
1573
+ images: variation.images || []
1574
+ }, variantData);
1575
+ return;
1576
+ }
1577
+ }
1578
+
1579
+ // If no match found, use first variation
1580
+ if (variants.length > 0) {
1581
+ const firstVar = variants[0];
1582
+ const isInStock = firstVar.inStock !== false;
1583
+ const isAvailable = (firstVar.available !== false && firstVar.available !== undefined) ? firstVar.available : isInStock;
1584
+
1585
+ this.updateModalVariantUI(modal, {
1586
+ productId: firstVar.productId,
1587
+ price: firstVar.price || 0,
1588
+ mrp: firstVar.mrp || 0,
1589
+ inStock: isInStock,
1590
+ available: isAvailable,
1591
+ images: firstVar.images || []
1592
+ }, variantData);
1593
+ } else {
1594
+ this.updateModalVariantUI(modal, {
1595
+ productId: variantData.baseProductId,
1596
+ price: 0,
1597
+ mrp: 0,
1598
+ inStock: true,
1599
+ available: true,
1600
+ images: []
1601
+ }, variantData);
1602
+ }
1603
+ },
1604
+
1605
+ // Update modal variant UI (same as product page updateVariantUI)
1606
+ updateModalVariantUI(modal, variant, variantData) {
1607
+ if (!variant) return;
1608
+
1609
+ // Update product ID
1610
+ modal.dataset.productId = variant.productId;
1611
+
1612
+ // Update price
1613
+ const modalPriceCurrent = modal.querySelector('#add-to-cart-modal-price-current');
1614
+ const modalPriceOriginal = modal.querySelector('#add-to-cart-modal-price-original');
1615
+
1616
+ if (modalPriceCurrent) {
1617
+ modalPriceCurrent.textContent = this.formatMoneyProductCard(variant.price || 0);
1618
+ }
1619
+
1620
+ if (modalPriceOriginal && variant.mrp && variant.mrp > variant.price) {
1621
+ modalPriceOriginal.textContent = this.formatMoneyProductCard(variant.mrp);
1622
+ modalPriceOriginal.style.display = 'inline';
1623
+ } else if (modalPriceOriginal) {
1624
+ modalPriceOriginal.style.display = 'none';
1625
+ }
1626
+
1627
+ // Update image
1628
+ const modalImage = modal.querySelector('#add-to-cart-modal-image');
1629
+ 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;
1637
+ } else if (modalImage && variantData.baseProductImage) {
1638
+ modalImage.src = variantData.baseProductImage;
1639
+ // Clear variant image if using base product image
1640
+ delete modal.dataset.variantImageUrl;
1641
+ }
1642
+
1643
+ // Update confirm button disabled state
1644
+ const confirmBtn = modal.querySelector('#add-to-cart-modal-confirm-btn');
1645
+ if (confirmBtn) {
1646
+ confirmBtn.disabled = !(variant.available !== false);
1647
+ }
1648
+ },
1649
+
1650
+
1651
+ // Setup quantity controls in modal
1652
+ setupModalQuantityControls(modal) {
1653
+ const qtyInput = modal.querySelector('#add-to-cart-modal-qty-input');
1654
+ const qtyDecrease = modal.querySelector('#add-to-cart-modal-qty-decrease');
1655
+ const qtyIncrease = modal.querySelector('#add-to-cart-modal-qty-increase');
1656
+
1657
+ if (!qtyInput || !qtyDecrease || !qtyIncrease) return;
1658
+
1659
+ // Store handlers to prevent duplicates
1660
+ if (qtyDecrease._qtyHandler) {
1661
+ qtyDecrease.removeEventListener('click', qtyDecrease._qtyHandler);
1662
+ }
1663
+ if (qtyIncrease._qtyHandler) {
1664
+ qtyIncrease.removeEventListener('click', qtyIncrease._qtyHandler);
1665
+ }
1666
+
1667
+ // Decrease button handler
1668
+ qtyDecrease._qtyHandler = () => {
1669
+ const currentValue = parseInt(qtyInput.value) || 1;
1670
+ if (currentValue > 1) {
1671
+ qtyInput.value = currentValue - 1;
1672
+ }
1673
+ qtyDecrease.disabled = parseInt(qtyInput.value) <= 1;
1674
+ };
1675
+
1676
+ // Increase button handler
1677
+ qtyIncrease._qtyHandler = () => {
1678
+ const currentValue = parseInt(qtyInput.value) || 1;
1679
+ const max = parseInt(qtyInput.getAttribute('max')) || 99;
1680
+ if (currentValue < max) {
1681
+ qtyInput.value = currentValue + 1;
1682
+ }
1683
+ qtyDecrease.disabled = false;
1684
+ qtyIncrease.disabled = parseInt(qtyInput.value) >= max;
1685
+ };
1686
+
1687
+ qtyDecrease.addEventListener('click', qtyDecrease._qtyHandler);
1688
+ qtyIncrease.addEventListener('click', qtyIncrease._qtyHandler);
1689
+
1690
+ // Input validation
1691
+ if (qtyInput._changeHandler) {
1692
+ qtyInput.removeEventListener('change', qtyInput._changeHandler);
1693
+ }
1694
+ qtyInput._changeHandler = () => {
1695
+ let value = parseInt(qtyInput.value) || 1;
1696
+ const min = parseInt(qtyInput.getAttribute('min')) || 1;
1697
+ const max = parseInt(qtyInput.getAttribute('max')) || 99;
1698
+
1699
+ if (value < min) value = min;
1700
+ if (value > max) value = max;
1701
+
1702
+ qtyInput.value = value;
1703
+ qtyDecrease.disabled = value <= min;
1704
+ qtyIncrease.disabled = value >= max;
1705
+ };
1706
+ qtyInput.addEventListener('change', qtyInput._changeHandler);
1707
+
1708
+ // Initialize button states
1709
+ qtyDecrease.disabled = parseInt(qtyInput.value) <= 1;
1710
+ },
1711
+
1712
+ // Setup confirm button handler
1713
+ setupModalConfirmButton(modal, productId) {
1714
+ const confirmBtn = modal.querySelector('#add-to-cart-modal-confirm-btn');
1715
+ if (!confirmBtn) return;
1716
+
1717
+ // Initialize flag to track active add-to-cart request
1718
+ if (modal._isAddingToCart === undefined) {
1719
+ modal._isAddingToCart = false;
1720
+ }
1721
+
1722
+ // Remove existing listener if any
1723
+ if (confirmBtn._confirmHandler) {
1724
+ confirmBtn.removeEventListener('click', confirmBtn._confirmHandler);
1725
+ }
1726
+
1727
+ // Add new listener
1728
+ confirmBtn._confirmHandler = async () => {
1729
+ // Prevent multiple simultaneous requests
1730
+ if (confirmBtn.disabled || modal._isAddingToCart) return;
1731
+
1732
+ const qtyInput = modal.querySelector('#add-to-cart-modal-qty-input');
1733
+ const quantity = parseInt(qtyInput?.value) || 1;
1734
+
1735
+ // Get current product ID (might have changed due to variant selection)
1736
+ // CRITICAL: Use the productId from modal.dataset which is updated by findModalMatchingVariant
1737
+ let currentProductId = modal.dataset.productId || productId;
1738
+
1739
+ // Verify we have a valid variant productId, not the parent product
1740
+ // If variants exist and options are selected, ensure we're using the variant's productId
1741
+ if (modal._variantData && modal._variantData.variants && modal._variantData.variants.length > 0) {
1742
+ const selectedOptionsCount = Object.keys(modal._selectedOptions || {}).length;
1743
+ // If options are selected, re-match to ensure we have the correct variant productId
1744
+ if (selectedOptionsCount > 0) {
1745
+ // Re-match variant to ensure correct productId before adding to cart
1746
+ this.findModalMatchingVariant(modal, modal._variantData);
1747
+ const matchedProductId = modal.dataset.productId;
1748
+ if (matchedProductId && matchedProductId !== currentProductId) {
1749
+ console.log('[AddToCartModal] Updated productId from', currentProductId, 'to', matchedProductId, 'based on selected options:', modal._selectedOptions);
1750
+ currentProductId = matchedProductId;
1751
+ }
1752
+ }
1753
+ }
1754
+
1755
+ console.log('[AddToCartModal] Adding to cart - productId:', currentProductId, 'quantity:', quantity, 'selectedOptions:', modal._selectedOptions);
1756
+
1757
+ // CRITICAL: Set loading state and flag BEFORE any async operations
1758
+ // This prevents modal from closing during the request
1759
+ modal._isAddingToCart = true;
1760
+ confirmBtn.disabled = true;
1761
+ confirmBtn.innerHTML = 'Adding...';
1762
+
1763
+ // Disable variant selection during request
1764
+ this.disableModalVariantSelection(modal, true);
1765
+
1766
+ // Store variation image in localStorage before adding to cart
1767
+ const variantImageUrl = modal.dataset.variantImageUrl;
1768
+ if (variantImageUrl) {
1769
+ const imageKey = `variantImage_${currentProductId}`;
1770
+ try {
1771
+ localStorage.setItem(imageKey, variantImageUrl);
1772
+ } catch (e) {
1773
+ console.warn('Failed to store variant image in localStorage:', e);
1774
+ }
1775
+ }
1776
+
1777
+ try {
1778
+ // Pass skipButtonUpdate: true to prevent addToCart from updating product card buttons
1779
+ // Modal manages its own button state
1780
+ await this.addToCart(currentProductId, quantity, true);
1781
+ // Clear flag before closing to allow modal to close
1782
+ modal._isAddingToCart = false;
1783
+ this.closeAddToCartModal(modal);
1784
+ } catch (error) {
1785
+ console.error('Error adding to cart from modal:', error);
1786
+ // Reset button state on error
1787
+ confirmBtn.disabled = false;
1788
+ confirmBtn.textContent = 'ADD TO CART';
1789
+ // Re-enable variant selection
1790
+ this.disableModalVariantSelection(modal, false);
1791
+ // Clear the flag on error
1792
+ modal._isAddingToCart = false;
1793
+ }
1794
+ };
1795
+
1796
+ confirmBtn.addEventListener('click', confirmBtn._confirmHandler);
1797
+ },
1798
+
1799
+ // Disable/enable variant selection buttons
1800
+ disableModalVariantSelection(modal, disable) {
1801
+ const optionsContainer = modal.querySelector('#add-to-cart-modal-variant-options');
1802
+ if (!optionsContainer) return;
1803
+
1804
+ const variantButtons = optionsContainer.querySelectorAll('.option-value');
1805
+ variantButtons.forEach(btn => {
1806
+ if (disable) {
1807
+ btn.disabled = true;
1808
+ btn.style.opacity = '0.5';
1809
+ btn.style.cursor = 'not-allowed';
1810
+ btn.style.pointerEvents = 'none';
1811
+ } else {
1812
+ btn.disabled = false;
1813
+ btn.style.opacity = '';
1814
+ btn.style.cursor = '';
1815
+ btn.style.pointerEvents = '';
1816
+ }
1817
+ });
1818
+ },
1819
+
1820
+ // Setup modal close handlers
1821
+ setupModalCloseHandlers(modal) {
1822
+ const closeButtons = modal.querySelectorAll('[data-add-to-cart-modal-close]');
1823
+ const overlay = modal.querySelector('.add-to-cart-modal-overlay');
1824
+
1825
+ // Initialize flag if not exists
1826
+ if (modal._isAddingToCart === undefined) {
1827
+ modal._isAddingToCart = false;
1828
+ }
1829
+
1830
+ // Remove existing listeners and add new ones
1831
+ const closeHandler = (e) => {
1832
+ // CRITICAL: Prevent closing if add-to-cart request is in progress
1833
+ // Check both strict equality and truthy check for safety
1834
+ if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
1835
+ console.log('[AddToCartModal] Close blocked - add-to-cart request in progress');
1836
+ e.preventDefault();
1837
+ e.stopPropagation();
1838
+ e.stopImmediatePropagation();
1839
+ return false;
1840
+ }
1841
+ this.closeAddToCartModal(modal);
1842
+ };
1843
+
1844
+ closeButtons.forEach(btn => {
1845
+ if (btn._closeHandler) {
1846
+ btn.removeEventListener('click', btn._closeHandler);
1847
+ }
1848
+ btn._closeHandler = closeHandler;
1849
+ btn.addEventListener('click', btn._closeHandler);
1850
+ });
1851
+
1852
+ if (overlay) {
1853
+ if (overlay._closeHandler) {
1854
+ overlay.removeEventListener('click', overlay._closeHandler);
1855
+ }
1856
+ overlay._closeHandler = closeHandler;
1857
+ overlay.addEventListener('click', overlay._closeHandler);
1858
+ }
1859
+
1860
+ // Escape key handler - remove old one if exists
1861
+ if (modal._escapeHandler) {
1862
+ document.removeEventListener('keydown', modal._escapeHandler);
1863
+ }
1864
+ modal._escapeHandler = (e) => {
1865
+ if (e.key === 'Escape' && modal.classList.contains('active')) {
1866
+ // CRITICAL: Prevent closing if add-to-cart request is in progress
1867
+ // Check both strict equality and truthy check for safety
1868
+ if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
1869
+ console.log('[AddToCartModal] Escape key blocked - add-to-cart request in progress');
1870
+ e.preventDefault();
1871
+ e.stopPropagation();
1872
+ e.stopImmediatePropagation();
1873
+ return false;
1874
+ }
1875
+ this.closeAddToCartModal(modal);
1876
+ }
1877
+ };
1878
+ document.addEventListener('keydown', modal._escapeHandler);
1879
+ },
1880
+
1881
+ // Close add to cart modal
1882
+ closeAddToCartModal(modal) {
1883
+ if (!modal) {
1884
+ // Safety: ensure body scroll is restored even if modal is null
1885
+ document.body.classList.remove('modal-open');
1886
+ return;
1887
+ }
1888
+
1889
+ // CRITICAL: Prevent closing if add-to-cart request is in progress
1890
+ // Check both strict equality and truthy check for safety
1891
+ if (modal._isAddingToCart === true || modal._isAddingToCart === 'true') {
1892
+ console.log('[AddToCartModal] Cannot close modal while add-to-cart is in progress');
1893
+ return;
1894
+ }
1895
+
1896
+ modal.classList.remove('active');
1897
+ // Always remove modal-open class to restore body scroll
1898
+ document.body.classList.remove('modal-open');
1899
+
1900
+ // Reset confirm button state
1901
+ const confirmBtn = modal.querySelector('#add-to-cart-modal-confirm-btn');
1902
+ if (confirmBtn) {
1903
+ confirmBtn.disabled = false;
1904
+ confirmBtn.textContent = 'ADD TO CART';
1905
+ }
1906
+
1907
+ // Re-enable variant selection
1908
+ this.disableModalVariantSelection(modal, false);
1909
+
1910
+ // Clean up variant data
1911
+ if (modal._variantData) {
1912
+ delete modal._variantData;
1913
+ }
1914
+ if (modal._selectedOptions) {
1915
+ delete modal._selectedOptions;
1916
+ }
1917
+
1918
+ // Reset flag (always reset to ensure state is clean)
1919
+ modal._isAddingToCart = false;
1920
+
1921
+ // Safety: Ensure body scroll is restored (double-check)
1922
+ if (document.body.classList.contains('modal-open')) {
1923
+ document.body.classList.remove('modal-open');
1924
+ }
1925
+
1926
+ // Clean up variant click handler
1927
+ if (modal._variantClickHandler) {
1928
+ const optionsContainer = modal.querySelector('#add-to-cart-modal-variant-options');
1929
+ if (optionsContainer) {
1930
+ optionsContainer.removeEventListener('click', modal._variantClickHandler);
1931
+ }
1932
+ delete modal._variantClickHandler;
1933
+ }
1934
+
1935
+ // Clean up escape key handler
1936
+ if (modal._escapeHandler) {
1937
+ document.removeEventListener('keydown', modal._escapeHandler);
1938
+ delete modal._escapeHandler;
1939
+ }
1940
+ },
1941
+
1942
+
1943
+ debounce(func, wait) {
1944
+ let timeout;
1945
+ return function executedFunction(...args) {
1946
+ const later = () => {
1947
+ clearTimeout(timeout);
1948
+ func(...args);
1949
+ };
1950
+ clearTimeout(timeout);
1951
+ timeout = setTimeout(later, wait);
1952
+ };
1953
+ },
1954
+
1955
+ throttle(func, limit) {
1956
+ let inThrottle;
1957
+ return function() {
1958
+ const args = arguments;
1959
+ const context = this;
1960
+ if (!inThrottle) {
1961
+ func.apply(context, args);
1962
+ inThrottle = true;
1963
+ setTimeout(() => inThrottle = false, limit);
1964
+ }
1965
+ };
1966
+ },
1967
+
1968
+ // Header scroll effects
1969
+ initHeaderScroll() {
1970
+ const header = document.getElementById('site-header');
1971
+ if (!header) return;
1972
+
1973
+ let lastScrollY = window.scrollY;
1974
+ let ticking = false;
1975
+
1976
+ const updateHeader = () => {
1977
+ const scrollY = window.scrollY;
1978
+
1979
+ if (scrollY > 100) {
1980
+ header.classList.add('scrolled');
1981
+ } else {
1982
+ header.classList.remove('scrolled');
1983
+ }
1984
+
1985
+ // Hide/show header on scroll
1986
+ if (scrollY > lastScrollY && scrollY > 200) {
1987
+ header.style.transform = 'translateY(-100%)';
1988
+ } else {
1989
+ header.style.transform = 'translateY(0)';
1990
+ }
1991
+
1992
+ lastScrollY = scrollY;
1993
+ ticking = false;
1994
+ };
1995
+
1996
+ const requestTick = () => {
1997
+ if (!ticking) {
1998
+ requestAnimationFrame(updateHeader);
1999
+ ticking = true;
2000
+ }
2001
+ };
2002
+
2003
+ window.addEventListener('scroll', requestTick, { passive: true });
2004
+ },
2005
+
2006
+ // Smooth scrolling for anchor links
2007
+ initSmoothScrolling() {
2008
+ const links = document.querySelectorAll('a[href^="#"]');
2009
+
2010
+ links.forEach(link => {
2011
+ link.addEventListener('click', (e) => {
2012
+ const href = link.getAttribute('href');
2013
+ if (href === '#') return;
2014
+
2015
+ const target = document.querySelector(href);
2016
+ if (target) {
2017
+ e.preventDefault();
2018
+ target.scrollIntoView({
2019
+ behavior: 'smooth',
2020
+ block: 'start'
2021
+ });
2022
+ }
2023
+ });
2024
+ });
2025
+ },
2026
+
2027
+ // Intersection Observer for animations
2028
+ initIntersectionObserver() {
2029
+ if (!('IntersectionObserver' in window)) return;
2030
+
2031
+ const observerOptions = {
2032
+ threshold: 0.1,
2033
+ rootMargin: '0px 0px -50px 0px'
2034
+ };
2035
+
2036
+ const observer = new IntersectionObserver((entries) => {
2037
+ entries.forEach(entry => {
2038
+ if (entry.isIntersecting) {
2039
+ entry.target.classList.add('fade-in-visible');
2040
+ observer.unobserve(entry.target);
2041
+ }
2042
+ });
2043
+ }, observerOptions);
2044
+
2045
+ // Observe elements with fade-in class
2046
+ const fadeElements = document.querySelectorAll('.fade-in');
2047
+ fadeElements.forEach(el => observer.observe(el));
2048
+
2049
+ // Observe product cards for staggered animation
2050
+ const productCards = document.querySelectorAll('.product-card');
2051
+ productCards.forEach((card, index) => {
2052
+ card.style.animationDelay = `${index * 100}ms`;
2053
+ observer.observe(card);
2054
+ });
2055
+ },
2056
+
2057
+ // Enhanced add to cart with visual feedback
2058
+ async addToCartWithFeedback(productId, btn) {
2059
+ const originalText = btn.textContent;
2060
+ const originalHTML = btn.innerHTML;
2061
+
2062
+ // Show loading state
2063
+ btn.innerHTML = `
2064
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="animate-spin">
2065
+ <line x1="12" y1="2" x2="12" y2="6"></line>
2066
+ <line x1="12" y1="18" x2="12" y2="22"></line>
2067
+ <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
2068
+ <line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
2069
+ <line x1="2" y1="12" x2="6" y2="12"></line>
2070
+ <line x1="18" y1="12" x2="22" y2="12"></line>
2071
+ <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
2072
+ <line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
2073
+ </svg>
2074
+ Adding...
2075
+ `;
2076
+ btn.disabled = true;
2077
+
2078
+ try {
2079
+ await this.addToCart(productId, 1);
2080
+
2081
+ // Show success state
2082
+ btn.innerHTML = `
2083
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2084
+ <polyline points="20,6 9,17 4,12"></polyline>
2085
+ </svg>
2086
+ Added!
2087
+ `;
2088
+ btn.classList.add('btn-success');
2089
+
2090
+ // Reset after delay
2091
+ setTimeout(() => {
2092
+ btn.innerHTML = originalHTML;
2093
+ btn.classList.remove('btn-success');
2094
+ btn.disabled = false;
2095
+ }, 2000);
2096
+
2097
+ } catch (error) {
2098
+ // Show error state
2099
+ btn.innerHTML = `
2100
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2101
+ <line x1="18" y1="6" x2="6" y2="18"></line>
2102
+ <line x1="6" y1="6" x2="18" y2="18"></line>
2103
+ </svg>
2104
+ Error
2105
+ `;
2106
+ btn.classList.add('btn-error');
2107
+
2108
+ // Reset after delay
2109
+ setTimeout(() => {
2110
+ btn.innerHTML = originalHTML;
2111
+ btn.classList.remove('btn-error');
2112
+ btn.disabled = false;
2113
+ }, 2000);
2114
+ }
2115
+ },
2116
+
2117
+ // scroll-triggered animations
2118
+ initScrollAnimations() {
2119
+ if (!('IntersectionObserver' in window)) return;
2120
+
2121
+ const animationObserver = new IntersectionObserver((entries) => {
2122
+ entries.forEach(entry => {
2123
+ if (entry.isIntersecting) {
2124
+ const element = entry.target;
2125
+ const animationType = element.dataset.animation || 'fadeInUp';
2126
+ const delay = element.dataset.delay || 0;
2127
+
2128
+ setTimeout(() => {
2129
+ element.classList.add('animate-in');
2130
+ element.classList.add(`animate-${animationType}`);
2131
+ }, delay);
2132
+
2133
+ animationObserver.unobserve(element);
2134
+ }
2135
+ });
2136
+ }, {
2137
+ threshold: 0.1,
2138
+ rootMargin: '0px 0px -100px 0px'
2139
+ });
2140
+
2141
+ // Observe all elements with animation data attributes
2142
+ const animatedElements = document.querySelectorAll('[data-animation]');
2143
+ animatedElements.forEach(el => animationObserver.observe(el));
2144
+ },
2145
+
2146
+ // Parallax scrolling effects
2147
+ initParallaxEffects() {
2148
+ if (!window.matchMedia('(prefers-reduced-motion: no-preference)').matches) return;
2149
+
2150
+ const parallaxElements = document.querySelectorAll('[data-parallax]');
2151
+
2152
+ if (parallaxElements.length === 0) return;
2153
+
2154
+ let ticking = false;
2155
+
2156
+ const updateParallax = () => {
2157
+ const scrolled = window.pageYOffset;
2158
+
2159
+ parallaxElements.forEach(element => {
2160
+ const speed = parseFloat(element.dataset.parallax) || 0.5;
2161
+ const yPos = -(scrolled * speed);
2162
+ element.style.transform = `translateY(${yPos}px)`;
2163
+ });
2164
+
2165
+ ticking = false;
2166
+ };
2167
+
2168
+ const requestTick = () => {
2169
+ if (!ticking) {
2170
+ requestAnimationFrame(updateParallax);
2171
+ ticking = true;
2172
+ }
2173
+ };
2174
+
2175
+ window.addEventListener('scroll', requestTick, { passive: true });
2176
+ },
2177
+
2178
+ // Smooth page transitions - disabled to prevent white flash
2179
+ initPageTransitions() {
2180
+ // Page transitions disabled to prevent jarring white flash
2181
+ // Navigation now uses standard browser behavior for better UX
2182
+ return;
2183
+ },
2184
+
2185
+ // Micro-interactions for buttons and interactive elements
2186
+ initMicroInteractions() {
2187
+ // Button ripple effect
2188
+ const buttons = document.querySelectorAll('.btn');
2189
+ buttons.forEach(button => {
2190
+ button.addEventListener('click', (e) => {
2191
+ const ripple = document.createElement('span');
2192
+ const rect = button.getBoundingClientRect();
2193
+ const size = Math.max(rect.width, rect.height);
2194
+ const x = e.clientX - rect.left - size / 2;
2195
+ const y = e.clientY - rect.top - size / 2;
2196
+
2197
+ ripple.style.cssText = `
2198
+ position: absolute;
2199
+ width: ${size}px;
2200
+ height: ${size}px;
2201
+ left: ${x}px;
2202
+ top: ${y}px;
2203
+ background: rgba(255, 255, 255, 0.3);
2204
+ border-radius: 50%;
2205
+ transform: scale(0);
2206
+ animation: ripple 0.6s linear;
2207
+ pointer-events: none;
2208
+ `;
2209
+
2210
+ button.style.position = 'relative';
2211
+ button.style.overflow = 'hidden';
2212
+ button.appendChild(ripple);
2213
+
2214
+ setTimeout(() => ripple.remove(), 600);
2215
+ });
2216
+ });
2217
+
2218
+ // Hover effects for cards - handled by CSS for better performance
2219
+ // Removed JavaScript hover effects to prevent conflicts with CSS
2220
+
2221
+ // Form input focus effects
2222
+ const inputs = document.querySelectorAll('input, textarea, select');
2223
+ inputs.forEach(input => {
2224
+ input.addEventListener('focus', () => {
2225
+ input.parentElement.classList.add('focused');
2226
+ });
2227
+
2228
+ input.addEventListener('blur', () => {
2229
+ input.parentElement.classList.remove('focused');
2230
+ });
2231
+ });
2232
+ },
2233
+
2234
+ // Staggered animations for lists and grids
2235
+ initStaggeredAnimations() {
2236
+ const staggerContainers = document.querySelectorAll('[data-stagger]');
2237
+
2238
+ staggerContainers.forEach(container => {
2239
+ const items = container.children;
2240
+ const staggerDelay = parseInt(container.dataset.stagger) || 100;
2241
+
2242
+ Array.from(items).forEach((item, index) => {
2243
+ item.style.animationDelay = `${index * staggerDelay}ms`;
2244
+ item.classList.add('stagger-item');
2245
+ });
2246
+ });
2247
+ },
2248
+
2249
+ // Loading states and skeleton screens
2250
+ initLoadingStates() {
2251
+ // Show skeleton loading for dynamic content
2252
+ const skeletonElements = document.querySelectorAll('[data-skeleton]');
2253
+
2254
+ skeletonElements.forEach(element => {
2255
+ element.classList.add('loading-skeleton');
2256
+
2257
+ // Simulate loading completion
2258
+ setTimeout(() => {
2259
+ element.classList.remove('loading-skeleton');
2260
+ element.classList.add('loaded');
2261
+ }, 2000);
2262
+ });
2263
+ },
2264
+
2265
+ // Enhanced intersection observer with more animation types
2266
+ initAdvancedIntersectionObserver() {
2267
+ if (!('IntersectionObserver' in window)) return;
2268
+
2269
+ const observerOptions = {
2270
+ threshold: [0, 0.1, 0.5, 1],
2271
+ rootMargin: '0px 0px -50px 0px'
2272
+ };
2273
+
2274
+ const observer = new IntersectionObserver((entries) => {
2275
+ entries.forEach(entry => {
2276
+ const element = entry.target;
2277
+ const ratio = entry.intersectionRatio;
2278
+
2279
+ if (ratio > 0.1) {
2280
+ element.classList.add('in-view');
2281
+ }
2282
+
2283
+ if (ratio > 0.5) {
2284
+ element.classList.add('fully-visible');
2285
+ }
2286
+
2287
+ if (ratio === 1) {
2288
+ element.classList.add('completely-visible');
2289
+ }
2290
+ });
2291
+ }, observerOptions);
2292
+
2293
+ // Observe all elements with animation classes
2294
+ const animatedElements = document.querySelectorAll('.fade-in, .slide-in, .scale-in, .rotate-in');
2295
+ animatedElements.forEach(el => observer.observe(el));
2296
+ },
2297
+
2298
+ // Performance optimizations
2299
+ initPerformanceOptimizations() {
2300
+ // Lazy loading for images
2301
+ if ('IntersectionObserver' in window) {
2302
+ const imageObserver = new IntersectionObserver((entries) => {
2303
+ entries.forEach(entry => {
2304
+ if (entry.isIntersecting) {
2305
+ const img = entry.target;
2306
+ if (img.dataset.src) {
2307
+ img.src = img.dataset.src;
2308
+ img.classList.remove('lazy');
2309
+ img.classList.add('loaded');
2310
+ imageObserver.unobserve(img);
2311
+ }
2312
+ }
2313
+ });
2314
+ });
2315
+
2316
+ const lazyImages = document.querySelectorAll('img[data-src]');
2317
+ lazyImages.forEach(img => {
2318
+ img.classList.add('lazy');
2319
+ imageObserver.observe(img);
2320
+ });
2321
+ }
2322
+
2323
+ // Preload critical resources
2324
+ this.preloadCriticalResources();
2325
+
2326
+ // Optimize animations for performance
2327
+ this.optimizeAnimations();
2328
+ },
2329
+
2330
+ // Preload critical resources
2331
+ preloadCriticalResources() {
2332
+ // Preload critical fonts
2333
+ const fontPreloads = [
2334
+ 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
2335
+ 'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&display=swap'
2336
+ ];
2337
+
2338
+ fontPreloads.forEach(href => {
2339
+ const link = document.createElement('link');
2340
+ link.rel = 'preload';
2341
+ link.as = 'style';
2342
+ link.href = href;
2343
+ document.head.appendChild(link);
2344
+ });
2345
+
2346
+ // Preload critical images
2347
+ const criticalImages = document.querySelectorAll('.hero-image, .logo-image');
2348
+ criticalImages.forEach(img => {
2349
+ if (img.src) {
2350
+ const link = document.createElement('link');
2351
+ link.rel = 'preload';
2352
+ link.as = 'image';
2353
+ link.href = img.src;
2354
+ document.head.appendChild(link);
2355
+ }
2356
+ });
2357
+ },
2358
+
2359
+ // Optimize animations for performance
2360
+ optimizeAnimations() {
2361
+ // Check for reduced motion preference
2362
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2363
+
2364
+ if (prefersReducedMotion) {
2365
+ // Disable animations for users who prefer reduced motion
2366
+ document.documentElement.style.setProperty('--transition-duration', '0.01ms');
2367
+ document.documentElement.style.setProperty('--animation-duration', '0.01ms');
2368
+ }
2369
+
2370
+ // Use requestAnimationFrame for smooth animations
2371
+ let animationFrameId;
2372
+
2373
+ const smoothScroll = (target, duration = 300) => {
2374
+ const start = window.pageYOffset;
2375
+ const distance = target - start;
2376
+ const startTime = performance.now();
2377
+
2378
+ const animation = (currentTime) => {
2379
+ const elapsed = currentTime - startTime;
2380
+ const progress = Math.min(elapsed / duration, 1);
2381
+ const ease = this.easeInOutCubic(progress);
2382
+
2383
+ window.scrollTo(0, start + distance * ease);
2384
+
2385
+ if (progress < 1) {
2386
+ animationFrameId = requestAnimationFrame(animation);
2387
+ }
2388
+ };
2389
+
2390
+ animationFrameId = requestAnimationFrame(animation);
2391
+ };
2392
+
2393
+ // Smooth scroll for anchor links
2394
+ const anchorLinks = document.querySelectorAll('a[href^="#"]');
2395
+ anchorLinks.forEach(link => {
2396
+ link.addEventListener('click', (e) => {
2397
+ e.preventDefault();
2398
+ const target = document.querySelector(link.getAttribute('href'));
2399
+ if (target) {
2400
+ smoothScroll(target.offsetTop);
2401
+ }
2402
+ });
2403
+ });
2404
+ },
2405
+
2406
+ // Easing function for smooth animations
2407
+ easeInOutCubic(t) {
2408
+ return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
2409
+ },
2410
+
2411
+ // Login modal (email/password, email OTP, phone OTP)
2412
+ initLoginModal() {
2413
+ const modal = document.getElementById('login-modal');
2414
+ if (!modal) return;
2415
+
2416
+ const overlay = modal.querySelector('[data-login-overlay]');
2417
+ const closeButtons = modal.querySelectorAll('[data-login-close]');
2418
+ const triggers = document.querySelectorAll('[data-login-modal-trigger]');
2419
+ const methodButtons = modal.querySelectorAll('[data-login-method]');
2420
+
2421
+ const views = modal.querySelectorAll('[data-login-view]');
2422
+ const stepLabels = modal.querySelectorAll('[data-login-step-label]');
2423
+
2424
+ const passwordForm = modal.querySelector('[data-login-view="password"]');
2425
+ const emailFlow = modal.querySelector('[data-login-view="email-otp"]');
2426
+ const phoneFlow = modal.querySelector('[data-login-view="phone-otp"]');
2427
+
2428
+ const emailInputForm = emailFlow?.querySelector('[data-login-step="email-input"]');
2429
+ const emailVerifyForm = emailFlow?.querySelector('[data-login-step="email-verify"]');
2430
+ const phoneInputForm = phoneFlow?.querySelector('[data-login-step="phone-input"]');
2431
+ const phoneVerifyForm = phoneFlow?.querySelector('[data-login-step="phone-verify"]');
2432
+
2433
+ const successView = modal.querySelector('[data-login-view="success"]');
2434
+
2435
+ let selectedMethod = null;
2436
+ let emailForOtp = '';
2437
+ let phoneForOtp = '';
2438
+ let userGUIDForOtp = null;
2439
+ let userGUIDForPhoneOtp = null;
2440
+ let loginPhoneIti = null;
2441
+
2442
+ const setStep = (step) => {
2443
+ stepLabels.forEach(label => {
2444
+ const key = label.getAttribute('data-login-step-label');
2445
+ label.classList.toggle('login-modal__step--active', key === step);
2446
+ });
2447
+ };
2448
+
2449
+ const showView = (name) => {
2450
+ views.forEach(view => {
2451
+ const key = view.getAttribute('data-login-view');
2452
+ if (key === name) {
2453
+ view.hidden = false;
2454
+ } else if (view !== successView) {
2455
+ view.hidden = true;
2456
+ }
2457
+ });
2458
+ if (name === 'methods') {
2459
+ setStep('method');
2460
+ } else {
2461
+ setStep('verify');
2462
+ }
2463
+ };
2464
+
2465
+ const resetOtpFlows = () => {
2466
+ if (emailInputForm && emailVerifyForm) {
2467
+ emailInputForm.style.display = '';
2468
+ emailVerifyForm.style.display = 'none';
2469
+ }
2470
+ if (phoneInputForm && phoneVerifyForm) {
2471
+ phoneInputForm.style.display = '';
2472
+ phoneVerifyForm.style.display = 'none';
2473
+ }
2474
+ // Destroy phone input instance when resetting
2475
+ if (loginPhoneIti) {
2476
+ loginPhoneIti.destroy();
2477
+ loginPhoneIti = null;
2478
+ }
2479
+ };
2480
+
2481
+ const openModal = () => {
2482
+ // Check if user is already logged in
2483
+ const isLoggedIn = document.cookie.split(';').some(cookie => {
2484
+ const [name] = cookie.trim().split('=');
2485
+ return name === 'O2VENDIsUserLoggedin' && cookie.includes('true');
2486
+ });
2487
+
2488
+ if (isLoggedIn) {
2489
+ // User is already logged in, redirect to account page or do nothing
2490
+ window.location.href = '/account';
2491
+ return;
2492
+ }
2493
+
2494
+ selectedMethod = null;
2495
+ document.body.classList.add('modal-open');
2496
+ modal.classList.add('login-modal--active');
2497
+ // Reset errors
2498
+ modal.querySelectorAll('.login-modal__error').forEach(el => {
2499
+ el.classList.remove('login-modal__error--visible');
2500
+ el.textContent = '';
2501
+ });
2502
+ successView.hidden = true;
2503
+
2504
+ // Always start at method selection with no fields visible
2505
+ resetOtpFlows();
2506
+ showView('methods');
2507
+ };
2508
+
2509
+ // Initialize intl-tel-input for login phone
2510
+ const initializeLoginPhoneInput = () => {
2511
+ const phoneInput = phoneInputForm?.querySelector('#login-phone-otp');
2512
+ if (phoneInput && typeof intlTelInput !== 'undefined') {
2513
+ // Destroy existing instance if any
2514
+ if (loginPhoneIti) {
2515
+ loginPhoneIti.destroy();
2516
+ loginPhoneIti = null;
2517
+ }
2518
+
2519
+ loginPhoneIti = intlTelInput(phoneInput, {
2520
+ 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'],
2529
+ separateDialCode: true,
2530
+ nationalMode: false
2531
+ });
2532
+
2533
+ // Store instance globally for validation
2534
+ window.loginPhoneIti = loginPhoneIti;
2535
+ }
2536
+ };
2537
+
2538
+ const selectMethod = (method) => {
2539
+ selectedMethod = method;
2540
+ resetOtpFlows();
2541
+ if (method === 'password') {
2542
+ showView('password');
2543
+ } else if (method === 'email-otp') {
2544
+ showView('email-otp');
2545
+ } else if (method === 'phone-otp') {
2546
+ showView('phone-otp');
2547
+ // Initialize phone input when phone OTP method is selected
2548
+ setTimeout(initializeLoginPhoneInput, 100);
2549
+ }
2550
+ };
2551
+
2552
+ const closeModal = () => {
2553
+ modal.classList.remove('login-modal--active');
2554
+ document.body.classList.remove('modal-open');
2555
+ };
2556
+
2557
+ triggers.forEach(trigger => {
2558
+ trigger.addEventListener('click', (e) => {
2559
+ e.preventDefault();
2560
+ openModal();
2561
+ });
2562
+ });
2563
+
2564
+ if (overlay) {
2565
+ overlay.addEventListener('click', closeModal);
2566
+ }
2567
+ closeButtons.forEach(btn => {
2568
+ btn.addEventListener('click', closeModal);
2569
+ });
2570
+
2571
+ methodButtons.forEach(btn => {
2572
+ btn.addEventListener('click', () => {
2573
+ const method = btn.getAttribute('data-login-method');
2574
+ selectMethod(method);
2575
+ });
2576
+ });
2577
+
2578
+ const showError = (key, message) => {
2579
+ const el = modal.querySelector(`[data-login-error="${key}"]`);
2580
+ if (!el) return;
2581
+ el.textContent = message;
2582
+ el.classList.add('login-modal__error--visible');
2583
+ };
2584
+
2585
+ const clearError = (key) => {
2586
+ const el = modal.querySelector(`[data-login-error="${key}"]`);
2587
+ if (!el) return;
2588
+ el.textContent = '';
2589
+ el.classList.remove('login-modal__error--visible');
2590
+ };
2591
+
2592
+ // Email/password submit
2593
+ if (passwordForm) {
2594
+ passwordForm.addEventListener('submit', async (e) => {
2595
+ e.preventDefault();
2596
+ clearError('password');
2597
+ const emailInput = passwordForm.querySelector('#login-email');
2598
+ const passwordInput = passwordForm.querySelector('#login-password');
2599
+ const rememberInput = passwordForm.querySelector('#login-remember');
2600
+
2601
+ const email = emailInput?.value.trim();
2602
+ const password = passwordInput?.value.trim();
2603
+
2604
+ if (!email || !password) {
2605
+ showError('password', 'Email and password are required.');
2606
+ return;
2607
+ }
2608
+
2609
+ const submitBtn = passwordForm.querySelector('[data-login-submit-password]');
2610
+ const originalText = submitBtn?.textContent;
2611
+ if (submitBtn) {
2612
+ submitBtn.disabled = true;
2613
+ submitBtn.textContent = 'Signing in...';
2614
+ }
2615
+
2616
+ try {
2617
+ const response = await fetch('/webstoreapi/customer/login', {
2618
+ method: 'POST',
2619
+ headers: {
2620
+ 'Content-Type': 'application/json',
2621
+ 'X-Requested-With': 'XMLHttpRequest'
2622
+ },
2623
+ body: JSON.stringify({
2624
+ email,
2625
+ password,
2626
+ remember: rememberInput?.checked || false
2627
+ })
2628
+ });
2629
+
2630
+ const data = await response.json();
2631
+ if (!response.ok || !data.success) {
2632
+ showError('password', data.error || 'Unable to sign in. Please try again.');
2633
+ } else {
2634
+ views.forEach(v => (v.hidden = true));
2635
+ successView.hidden = false;
2636
+ }
2637
+ } catch (err) {
2638
+ console.error('Login error:', err);
2639
+ showError('password', 'Unable to sign in. Please try again.');
2640
+ } finally {
2641
+ if (submitBtn) {
2642
+ submitBtn.disabled = false;
2643
+ submitBtn.textContent = originalText;
2644
+ }
2645
+ }
2646
+ });
2647
+ }
2648
+
2649
+ // Email OTP send
2650
+ if (emailInputForm) {
2651
+ emailInputForm.addEventListener('submit', async (e) => {
2652
+ e.preventDefault();
2653
+ clearError('email-otp-input');
2654
+ const emailInput = emailInputForm.querySelector('#login-email-otp');
2655
+ const email = emailInput?.value.trim();
2656
+ if (!email) {
2657
+ showError('email-otp-input', 'Email is required.');
2658
+ return;
2659
+ }
2660
+
2661
+ const btn = emailInputForm.querySelector('[data-login-send-email-otp]');
2662
+ const original = btn?.textContent;
2663
+ if (btn) {
2664
+ btn.disabled = true;
2665
+ btn.textContent = 'Sending...';
2666
+ }
2667
+
2668
+ try {
2669
+ const response = await fetch('/webstoreapi/auth/email/send-otp', {
2670
+ method: 'POST',
2671
+ headers: {
2672
+ 'Content-Type': 'application/json',
2673
+ 'X-Requested-With': 'XMLHttpRequest'
2674
+ },
2675
+ body: JSON.stringify({ email })
2676
+ });
2677
+ const data = await response.json();
2678
+ if (!response.ok || !data.success) {
2679
+ showError('email-otp-input', data.error || 'Unable to send OTP. Please try again.');
2680
+ } else {
2681
+ emailForOtp = email;
2682
+ userGUIDForOtp = data.userGUID || null;
2683
+ const emailDisplay = emailFlow.querySelector('[data-login-email-display]');
2684
+ if (emailDisplay) {
2685
+ emailDisplay.textContent = email;
2686
+ }
2687
+ emailInputForm.style.display = 'none';
2688
+ emailVerifyForm.style.display = '';
2689
+ }
2690
+ } catch (err) {
2691
+ console.error('Send email OTP error:', err);
2692
+ showError('email-otp-input', 'Unable to send OTP. Please try again.');
2693
+ } finally {
2694
+ if (btn) {
2695
+ btn.disabled = false;
2696
+ btn.textContent = original;
2697
+ }
2698
+ }
2699
+ });
2700
+ }
2701
+
2702
+ // Email OTP verify
2703
+ if (emailVerifyForm) {
2704
+ emailVerifyForm.addEventListener('submit', async (e) => {
2705
+ e.preventDefault();
2706
+ clearError('email-otp-verify');
2707
+ const codeInput = emailVerifyForm.querySelector('#login-email-otp-code');
2708
+ const otp = codeInput?.value.trim();
2709
+ if (!otp) {
2710
+ showError('email-otp-verify', 'OTP is required.');
2711
+ return;
2712
+ }
2713
+
2714
+ const btn = emailVerifyForm.querySelector('[data-login-verify-email-otp]');
2715
+ const original = btn?.textContent;
2716
+ if (btn) {
2717
+ btn.disabled = true;
2718
+ btn.textContent = 'Verifying...';
2719
+ }
2720
+
2721
+ try {
2722
+ const response = await fetch('/webstoreapi/auth/email/verify-otp', {
2723
+ method: 'POST',
2724
+ headers: {
2725
+ 'Content-Type': 'application/json',
2726
+ 'X-Requested-With': 'XMLHttpRequest'
2727
+ },
2728
+ body: JSON.stringify({
2729
+ email: emailForOtp,
2730
+ otp,
2731
+ userGUID: userGUIDForOtp
2732
+ })
2733
+ });
2734
+ const data = await response.json();
2735
+ if (!response.ok || !data.success) {
2736
+ showError('email-otp-verify', data.error || 'Invalid or expired OTP.');
2737
+ } else {
2738
+ views.forEach(v => (v.hidden = true));
2739
+ successView.hidden = false;
2740
+ }
2741
+ } catch (err) {
2742
+ console.error('Verify email OTP error:', err);
2743
+ showError('email-otp-verify', 'Unable to verify OTP. Please try again.');
2744
+ } finally {
2745
+ if (btn) {
2746
+ btn.disabled = false;
2747
+ btn.textContent = original;
2748
+ }
2749
+ }
2750
+ });
2751
+ }
2752
+
2753
+ // Phone OTP send
2754
+ if (phoneInputForm) {
2755
+ phoneInputForm.addEventListener('submit', async (e) => {
2756
+ e.preventDefault();
2757
+ clearError('phone-otp-input');
2758
+ const phoneInput = phoneInputForm.querySelector('#login-phone-otp');
2759
+
2760
+ // Get phone number from intl-tel-input if available
2761
+ let phone = '';
2762
+ if (loginPhoneIti) {
2763
+ const fullPhoneNumber = loginPhoneIti.getNumber();
2764
+ if (fullPhoneNumber) {
2765
+ // Remove leading + sign
2766
+ phone = fullPhoneNumber.replace(/^\+/, '');
2767
+ } else {
2768
+ phone = phoneInput?.value.trim();
2769
+ }
2770
+ } else {
2771
+ phone = phoneInput?.value.trim();
2772
+ }
2773
+
2774
+ if (!phone) {
2775
+ showError('phone-otp-input', 'Mobile number is required.');
2776
+ return;
2777
+ }
2778
+
2779
+ // Validate phone number if intl-tel-input is available
2780
+ if (loginPhoneIti && !loginPhoneIti.isValidNumber()) {
2781
+ showError('phone-otp-input', 'Please enter a valid phone number.');
2782
+ return;
2783
+ }
2784
+
2785
+ const btn = phoneInputForm.querySelector('[data-login-send-phone-otp]');
2786
+ const original = btn?.textContent;
2787
+ if (btn) {
2788
+ btn.disabled = true;
2789
+ btn.textContent = 'Sending...';
2790
+ }
2791
+
2792
+ try {
2793
+ const response = await fetch('/webstoreapi/auth/phone/send-otp', {
2794
+ method: 'POST',
2795
+ headers: {
2796
+ 'Content-Type': 'application/json',
2797
+ 'X-Requested-With': 'XMLHttpRequest'
2798
+ },
2799
+ body: JSON.stringify({ phoneNumber: phone })
2800
+ });
2801
+ const data = await response.json();
2802
+ if (!response.ok || !data.success) {
2803
+ showError('phone-otp-input', data.error || 'Unable to send OTP. Please try again.');
2804
+ } else {
2805
+ phoneForOtp = phone;
2806
+ userGUIDForPhoneOtp = data.userGUID || null;
2807
+ const phoneDisplay = phoneFlow.querySelector('[data-login-phone-display]');
2808
+ if (phoneDisplay) {
2809
+ phoneDisplay.textContent = phone;
2810
+ }
2811
+ phoneInputForm.style.display = 'none';
2812
+ phoneVerifyForm.style.display = '';
2813
+ }
2814
+ } catch (err) {
2815
+ console.error('Send phone OTP error:', err);
2816
+ showError('phone-otp-input', 'Unable to send OTP. Please try again.');
2817
+ } finally {
2818
+ if (btn) {
2819
+ btn.disabled = false;
2820
+ btn.textContent = original;
2821
+ }
2822
+ }
2823
+ });
2824
+ }
2825
+
2826
+ // Phone OTP verify
2827
+ if (phoneVerifyForm) {
2828
+ phoneVerifyForm.addEventListener('submit', async (e) => {
2829
+ e.preventDefault();
2830
+ clearError('phone-otp-verify');
2831
+ const codeInput = phoneVerifyForm.querySelector('#login-phone-otp-code');
2832
+ const otp = codeInput?.value.trim();
2833
+ if (!otp) {
2834
+ showError('phone-otp-verify', 'OTP is required.');
2835
+ return;
2836
+ }
2837
+
2838
+ const btn = phoneVerifyForm.querySelector('[data-login-verify-phone-otp]');
2839
+ const original = btn?.textContent;
2840
+ if (btn) {
2841
+ btn.disabled = true;
2842
+ btn.textContent = 'Verifying...';
2843
+ }
2844
+
2845
+ try {
2846
+ const response = await fetch('/webstoreapi/auth/phone/verify-otp', {
2847
+ method: 'POST',
2848
+ headers: {
2849
+ 'Content-Type': 'application/json',
2850
+ 'X-Requested-With': 'XMLHttpRequest'
2851
+ },
2852
+ body: JSON.stringify({
2853
+ phoneNumber: phoneForOtp,
2854
+ otp,
2855
+ userGUID: userGUIDForPhoneOtp
2856
+ })
2857
+ });
2858
+ const data = await response.json();
2859
+ if (!response.ok || !data.success) {
2860
+ showError('phone-otp-verify', data.error || 'Invalid or expired OTP.');
2861
+ } else {
2862
+ views.forEach(v => (v.hidden = true));
2863
+ successView.hidden = false;
2864
+ }
2865
+ } catch (err) {
2866
+ console.error('Verify phone OTP error:', err);
2867
+ showError('phone-otp-verify', 'Unable to verify OTP. Please try again.');
2868
+ } finally {
2869
+ if (btn) {
2870
+ btn.disabled = false;
2871
+ btn.textContent = original;
2872
+ }
2873
+ }
2874
+ });
2875
+ }
2876
+
2877
+ // ESC to close
2878
+ document.addEventListener('keydown', (e) => {
2879
+ if (e.key === 'Escape' && modal.classList.contains('login-modal--active')) {
2880
+ closeModal();
2881
+ }
2882
+ });
2883
+
2884
+ // Expose helper
2885
+ this.openLoginModal = openModal;
2886
+ },
2887
+
2888
+ // Accessibility enhancements
2889
+ initAccessibility() {
2890
+ // Skip links
2891
+ this.initSkipLinks();
2892
+
2893
+ // Keyboard navigation
2894
+ this.initKeyboardNavigation();
2895
+
2896
+ // ARIA attributes
2897
+ this.initARIA();
2898
+
2899
+ // Focus management
2900
+ this.initFocusManagement();
2901
+ },
2902
+
2903
+ // Initialize skip links
2904
+ initSkipLinks() {
2905
+ const skipLink = document.createElement('a');
2906
+ skipLink.href = '#main-content';
2907
+ skipLink.textContent = 'Skip to main content';
2908
+ skipLink.className = 'skip-link';
2909
+ document.body.insertBefore(skipLink, document.body.firstChild);
2910
+ },
2911
+
2912
+ // Initialize keyboard navigation
2913
+ initKeyboardNavigation() {
2914
+ // Escape key handling
2915
+ document.addEventListener('keydown', (e) => {
2916
+ if (e.key === 'Escape') {
2917
+ // Close mobile menu
2918
+ this.closeMobileMenu();
2919
+
2920
+ // Close search overlay
2921
+ this.closeSearch();
2922
+
2923
+ // Close any open modals
2924
+ const modals = document.querySelectorAll('.modal-open');
2925
+ modals.forEach(modal => {
2926
+ modal.classList.remove('modal-open');
2927
+ document.body.classList.remove('modal-open');
2928
+ });
2929
+ }
2930
+ });
2931
+
2932
+ // Tab navigation for dropdowns
2933
+ const dropdowns = document.querySelectorAll('.nav-dropdown');
2934
+ dropdowns.forEach(dropdown => {
2935
+ const trigger = dropdown.querySelector('.nav-link');
2936
+ const menu = dropdown.querySelector('.dropdown-content');
2937
+
2938
+ if (trigger && menu) {
2939
+ trigger.addEventListener('keydown', (e) => {
2940
+ if (e.key === 'Enter' || e.key === ' ') {
2941
+ e.preventDefault();
2942
+ dropdown.classList.toggle('active');
2943
+ trigger.setAttribute('aria-expanded', dropdown.classList.contains('active'));
2944
+ }
2945
+ });
2946
+ }
2947
+ });
2948
+ },
2949
+
2950
+ // Initialize ARIA attributes
2951
+ initARIA() {
2952
+ // Mobile menu toggle
2953
+ const mobileToggle = document.querySelector('.mobile-menu-toggle');
2954
+ if (mobileToggle) {
2955
+ mobileToggle.setAttribute('aria-label', 'Toggle mobile menu');
2956
+ mobileToggle.setAttribute('aria-expanded', 'false');
2957
+ }
2958
+
2959
+ // Search toggle
2960
+ const searchToggle = document.querySelector('.search-toggle');
2961
+ if (searchToggle) {
2962
+ searchToggle.setAttribute('aria-label', 'Open search');
2963
+ searchToggle.setAttribute('aria-expanded', 'false');
2964
+ }
2965
+
2966
+ // Cart link
2967
+ const cartLink = document.querySelector('.cart-link');
2968
+ if (cartLink) {
2969
+ cartLink.setAttribute('aria-label', 'View shopping cart');
2970
+ }
2971
+
2972
+ // Product action buttons
2973
+ const actionButtons = document.querySelectorAll('.product-action-btn');
2974
+ actionButtons.forEach(btn => {
2975
+ const icon = btn.querySelector('svg');
2976
+ if (icon) {
2977
+ const label = btn.getAttribute('aria-label') || icon.getAttribute('aria-label') || 'Product action';
2978
+ btn.setAttribute('aria-label', label);
2979
+ }
2980
+ });
2981
+ },
2982
+
2983
+ // Initialize focus management
2984
+ initFocusManagement() {
2985
+ // Trap focus in mobile menu
2986
+ const mobileMenu = document.querySelector('.mobile-nav');
2987
+ if (mobileMenu) {
2988
+ const focusableElements = mobileMenu.querySelectorAll(
2989
+ 'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
2990
+ );
2991
+
2992
+ const firstElement = focusableElements[0];
2993
+ const lastElement = focusableElements[focusableElements.length - 1];
2994
+
2995
+ mobileMenu.addEventListener('keydown', (e) => {
2996
+ if (e.key === 'Tab') {
2997
+ if (e.shiftKey) {
2998
+ if (document.activeElement === firstElement) {
2999
+ e.preventDefault();
3000
+ lastElement.focus();
3001
+ }
3002
+ } else {
3003
+ if (document.activeElement === lastElement) {
3004
+ e.preventDefault();
3005
+ firstElement.focus();
3006
+ }
3007
+ }
3008
+ }
3009
+ });
3010
+ }
3011
+
3012
+ // Return focus to trigger when closing modals
3013
+ let lastFocusedElement = null;
3014
+
3015
+ document.addEventListener('focusin', (e) => {
3016
+ if (e.target.closest('.mobile-nav, .search-overlay, .modal')) {
3017
+ lastFocusedElement = e.target;
3018
+ }
3019
+ });
3020
+
3021
+ // Restore focus when closing modals
3022
+ const restoreFocus = () => {
3023
+ if (lastFocusedElement && lastFocusedElement.focus) {
3024
+ lastFocusedElement.focus();
3025
+ }
3026
+ };
3027
+
3028
+ // Add restore focus to close methods
3029
+ const originalCloseMobileMenu = this.closeMobileMenu;
3030
+ this.closeMobileMenu = () => {
3031
+ originalCloseMobileMenu.call(this);
3032
+ restoreFocus();
3033
+ };
3034
+
3035
+ const originalCloseSearch = this.closeSearch;
3036
+ this.closeSearch = () => {
3037
+ originalCloseSearch.call(this);
3038
+ restoreFocus();
3039
+ };
3040
+ },
3041
+
3042
+ // Mobile bottom navigation initialization
3043
+ initMobileBottomNav() {
3044
+ // Set active state on bottom nav based on current page
3045
+ const currentPath = window.location.pathname;
3046
+ const navItems = document.querySelectorAll('.mobile-bottom-nav__item');
3047
+
3048
+ navItems.forEach(function(item) {
3049
+ const href = item.getAttribute('href');
3050
+ const dataItem = item.getAttribute('data-nav-item');
3051
+
3052
+ if (href && href !== '#' && href !== 'https://wa.me/1234567890') {
3053
+ const itemPath = new URL(href, window.location.origin).pathname;
3054
+
3055
+ // Handle home page
3056
+ if (dataItem === 'home' && (currentPath === '/' || currentPath === '/index')) {
3057
+ item.classList.add('active');
3058
+ item.setAttribute('aria-current', 'page');
3059
+ }
3060
+ // Handle other paths
3061
+ else if (currentPath.startsWith(itemPath) && itemPath !== '/') {
3062
+ item.classList.add('active');
3063
+ item.setAttribute('aria-current', 'page');
3064
+ }
3065
+ // Handle cart toggle button (special case)
3066
+ else if (dataItem === 'cart' && item.hasAttribute('data-cart-toggle')) {
3067
+ // Cart button doesn't get active state based on path
3068
+ // It's a toggle button, not a navigation link
3069
+ }
3070
+ }
3071
+ });
3072
+ }
3073
+ };
3074
+
3075
+ // Initialize theme when DOM is ready
3076
+ if (document.readyState === 'loading') {
3077
+ document.addEventListener('DOMContentLoaded', () => {
3078
+ Theme.init();
3079
+
3080
+ // Initialize additional animation features
3081
+ Theme.initScrollAnimations();
3082
+ Theme.initParallaxEffects();
3083
+ Theme.initPageTransitions();
3084
+ Theme.initMicroInteractions();
3085
+ Theme.initStaggeredAnimations();
3086
+ Theme.initLoadingStates();
3087
+ Theme.initAdvancedIntersectionObserver();
3088
+
3089
+ // Initialize performance and accessibility features
3090
+ Theme.initPerformanceOptimizations();
3091
+ Theme.initAccessibility();
3092
+ });
3093
+ } else {
3094
+ Theme.init();
3095
+
3096
+ // Initialize additional animation features
3097
+ Theme.initScrollAnimations();
3098
+ Theme.initParallaxEffects();
3099
+ Theme.initPageTransitions();
3100
+ Theme.initMicroInteractions();
3101
+ Theme.initStaggeredAnimations();
3102
+ Theme.initLoadingStates();
3103
+ Theme.initAdvancedIntersectionObserver();
3104
+
3105
+ // Initialize performance and accessibility features
3106
+ Theme.initPerformanceOptimizations();
3107
+ Theme.initAccessibility();
3108
+ }
3109
+
3110
+ // Make Theme available globally
3111
+ window.Theme = Theme;
3112
+
3113
+ })();
3114
+
3115
+ // Add CSS animations
3116
+ const style = document.createElement('style');
3117
+ style.textContent = `
3118
+ /* Animations */
3119
+ @keyframes slideOutRight {
3120
+ from { transform: translateX(0); opacity: 1; }
3121
+ to { transform: translateX(100%); opacity: 0; }
3122
+ }
3123
+
3124
+ @keyframes slideInRight {
3125
+ from { transform: translateX(100%); opacity: 0; }
3126
+ to { transform: translateX(0); opacity: 1; }
3127
+ }
3128
+
3129
+ @keyframes fadeInUp {
3130
+ from {
3131
+ opacity: 0;
3132
+ transform: translateY(30px);
3133
+ }
3134
+ to {
3135
+ opacity: 1;
3136
+ transform: translateY(0);
3137
+ }
3138
+ }
3139
+
3140
+ @keyframes fadeInScale {
3141
+ from {
3142
+ opacity: 0;
3143
+ transform: scale(0.9);
3144
+ }
3145
+ to {
3146
+ opacity: 1;
3147
+ transform: scale(1);
3148
+ }
3149
+ }
3150
+
3151
+ @keyframes spin {
3152
+ from { transform: rotate(0deg); }
3153
+ to { transform: rotate(360deg); }
3154
+ }
3155
+
3156
+ @keyframes pulse {
3157
+ 0%, 100% { transform: scale(1); }
3158
+ 50% { transform: scale(1.05); }
3159
+ }
3160
+
3161
+ @keyframes bounce {
3162
+ 0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
3163
+ 40%, 43% { transform: translateY(-10px); }
3164
+ 70% { transform: translateY(-5px); }
3165
+ 90% { transform: translateY(-2px); }
3166
+ }
3167
+
3168
+ @keyframes shake {
3169
+ 0%, 100% { transform: translateX(0); }
3170
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
3171
+ 20%, 40%, 60%, 80% { transform: translateX(2px); }
3172
+ }
3173
+
3174
+ .fade-in {
3175
+ opacity: 0;
3176
+ transform: translateY(20px);
3177
+ transition: opacity 0.6s ease, transform 0.6s ease;
3178
+ }
3179
+
3180
+ .fade-in-visible {
3181
+ opacity: 1;
3182
+ transform: translateY(0);
3183
+ }
3184
+
3185
+ .header-scrolled {
3186
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
3187
+ }
3188
+
3189
+ .mobile-nav-open {
3190
+ display: block !important;
3191
+ position: fixed;
3192
+ top: 0;
3193
+ left: 0;
3194
+ right: 0;
3195
+ bottom: 0;
3196
+ background-color: white;
3197
+ z-index: 1000;
3198
+ padding: 80px 20px 20px;
3199
+ overflow-y: auto;
3200
+ }
3201
+
3202
+ .mobile-nav-open .nav-list {
3203
+ flex-direction: column;
3204
+ gap: 20px;
3205
+ }
3206
+
3207
+ .mobile-menu-open .hamburger {
3208
+ transition: all 0.3s ease;
3209
+ }
3210
+
3211
+ .modal-open {
3212
+ overflow: hidden;
3213
+ }
3214
+
3215
+ .quick-view-modal {
3216
+ position: fixed;
3217
+ top: 0;
3218
+ left: 0;
3219
+ right: 0;
3220
+ bottom: 0;
3221
+ background-color: rgba(0, 0, 0, 0.5);
3222
+ z-index: 9999;
3223
+ display: flex;
3224
+ align-items: center;
3225
+ justify-content: center;
3226
+ padding: 20px;
3227
+ }
3228
+
3229
+ .quick-view-content {
3230
+ background-color: white;
3231
+ border-radius: 12px;
3232
+ max-width: 800px;
3233
+ width: 100%;
3234
+ max-height: 90vh;
3235
+ overflow-y: auto;
3236
+ position: relative;
3237
+ }
3238
+
3239
+ .quick-view-close {
3240
+ position: absolute;
3241
+ top: 15px;
3242
+ right: 15px;
3243
+ background: none;
3244
+ border: none;
3245
+ cursor: pointer;
3246
+ padding: 8px;
3247
+ border-radius: 50%;
3248
+ transition: background-color 0.2s ease;
3249
+ }
3250
+
3251
+ .quick-view-close:hover {
3252
+ background-color: #f3f4f6;
3253
+ }
3254
+
3255
+ .quick-view-body {
3256
+ display: grid;
3257
+ grid-template-columns: 1fr 1fr;
3258
+ gap: 30px;
3259
+ padding: 30px;
3260
+ }
3261
+
3262
+ .quick-view-image img {
3263
+ width: 100%;
3264
+ height: 400px;
3265
+ object-fit: cover;
3266
+ border-radius: 8px;
3267
+ }
3268
+
3269
+ .quick-view-title {
3270
+ font-size: 24px;
3271
+ font-weight: 700;
3272
+ margin-bottom: 15px;
3273
+ }
3274
+
3275
+ .quick-view-price {
3276
+ font-size: 20px;
3277
+ font-weight: 600;
3278
+ color: #000;
3279
+ margin-bottom: 20px;
3280
+ }
3281
+
3282
+ .quick-view-description {
3283
+ color: #6b7280;
3284
+ line-height: 1.6;
3285
+ margin-bottom: 30px;
3286
+ }
3287
+
3288
+ .quick-view-actions {
3289
+ display: flex;
3290
+ gap: 15px;
3291
+ }
3292
+
3293
+ @media (max-width: 768px) {
3294
+ .quick-view-body {
3295
+ grid-template-columns: 1fr;
3296
+ gap: 20px;
3297
+ padding: 20px;
3298
+ }
3299
+
3300
+ .quick-view-image img {
3301
+ height: 250px;
3302
+ }
3303
+ }
3304
+
3305
+ /* Button states */
3306
+ .btn-success {
3307
+ background-color: var(--color-success) !important;
3308
+ color: white !important;
3309
+ animation: pulse 0.6s var(--ease-out);
3310
+ }
3311
+
3312
+ .btn-error {
3313
+ background-color: var(--color-error) !important;
3314
+ color: white !important;
3315
+ animation: shake 0.6s var(--ease-out);
3316
+ }
3317
+
3318
+ /* Loading animations */
3319
+ .animate-spin {
3320
+ animation: spin 1s linear infinite;
3321
+ }
3322
+
3323
+ .animate-pulse {
3324
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
3325
+ }
3326
+
3327
+ .animate-bounce {
3328
+ animation: bounce 1s infinite;
3329
+ }
3330
+
3331
+ /* Lazy loading */
3332
+ .lazy {
3333
+ opacity: 0;
3334
+ transition: opacity var(--transition);
3335
+ }
3336
+
3337
+ .loaded {
3338
+ opacity: 1;
3339
+ }
3340
+
3341
+ /* Product card animations */
3342
+ /*.product-card {
3343
+ animation: fadeInScale 0.2s var(--ease-out);
3344
+ }
3345
+
3346
+ /* Product card hover effects handled by components.css */
3347
+
3348
+ /* Staggered animations for product grids */
3349
+ .products-grid .product-card:nth-child(1) { animation-delay: 0ms; }
3350
+ .products-grid .product-card:nth-child(2) { animation-delay: 100ms; }
3351
+ .products-grid .product-card:nth-child(3) { animation-delay: 200ms; }
3352
+ .products-grid .product-card:nth-child(4) { animation-delay: 300ms; }
3353
+ .products-grid .product-card:nth-child(5) { animation-delay: 400ms; }
3354
+ .products-grid .product-card:nth-child(6) { animation-delay: 500ms; }
3355
+
3356
+ /* Reduced motion preferences */
3357
+ @media (prefers-reduced-motion: reduce) {
3358
+ .fade-in,
3359
+ .product-card,
3360
+ .quick-view-content,
3361
+ .quick-view-body,
3362
+ .quick-view-title,
3363
+ .quick-view-price,
3364
+ .quick-view-description,
3365
+ .quick-view-actions {
3366
+ animation: none;
3367
+ transition: none;
3368
+ }
3369
+ }
3370
+
3371
+ /* Advanced Animation Keyframes */
3372
+ @keyframes fadeInDown {
3373
+ from {
3374
+ opacity: 0;
3375
+ transform: translateY(-30px);
3376
+ }
3377
+ to {
3378
+ opacity: 1;
3379
+ transform: translateY(0);
3380
+ }
3381
+ }
3382
+
3383
+ @keyframes fadeInLeft {
3384
+ from {
3385
+ opacity: 0;
3386
+ transform: translateX(-30px);
3387
+ }
3388
+ to {
3389
+ opacity: 1;
3390
+ transform: translateX(0);
3391
+ }
3392
+ }
3393
+
3394
+ @keyframes fadeInRight {
3395
+ from {
3396
+ opacity: 0;
3397
+ transform: translateX(30px);
3398
+ }
3399
+ to {
3400
+ opacity: 1;
3401
+ transform: translateX(0);
3402
+ }
3403
+ }
3404
+
3405
+ @keyframes scaleIn {
3406
+ from {
3407
+ opacity: 0;
3408
+ transform: scale(0.8);
3409
+ }
3410
+ to {
3411
+ opacity: 1;
3412
+ transform: scale(1);
3413
+ }
3414
+ }
3415
+
3416
+ @keyframes rotateIn {
3417
+ from {
3418
+ opacity: 0;
3419
+ transform: rotate(-10deg) scale(0.8);
3420
+ }
3421
+ to {
3422
+ opacity: 1;
3423
+ transform: rotate(0deg) scale(1);
3424
+ }
3425
+ }
3426
+
3427
+ @keyframes slideInUp {
3428
+ from {
3429
+ transform: translateY(100%);
3430
+ }
3431
+ to {
3432
+ transform: translateY(0);
3433
+ }
3434
+ }
3435
+
3436
+ @keyframes slideInDown {
3437
+ from {
3438
+ transform: translateY(-100%);
3439
+ }
3440
+ to {
3441
+ transform: translateY(0);
3442
+ }
3443
+ }
3444
+
3445
+ @keyframes slideInLeft {
3446
+ from {
3447
+ transform: translateX(-100%);
3448
+ }
3449
+ to {
3450
+ transform: translateX(0);
3451
+ }
3452
+ }
3453
+
3454
+ @keyframes zoomIn {
3455
+ from {
3456
+ opacity: 0;
3457
+ transform: scale(0.3);
3458
+ }
3459
+ to {
3460
+ opacity: 1;
3461
+ transform: scale(1);
3462
+ }
3463
+ }
3464
+
3465
+ @keyframes zoomOut {
3466
+ from {
3467
+ opacity: 1;
3468
+ transform: scale(1);
3469
+ }
3470
+ to {
3471
+ opacity: 0;
3472
+ transform: scale(0.3);
3473
+ }
3474
+ }
3475
+
3476
+ @keyframes flipInX {
3477
+ from {
3478
+ opacity: 0;
3479
+ transform: perspective(400px) rotateX(90deg);
3480
+ }
3481
+ to {
3482
+ opacity: 1;
3483
+ transform: perspective(400px) rotateX(0deg);
3484
+ }
3485
+ }
3486
+
3487
+ @keyframes flipInY {
3488
+ from {
3489
+ opacity: 0;
3490
+ transform: perspective(400px) rotateY(90deg);
3491
+ }
3492
+ to {
3493
+ opacity: 1;
3494
+ transform: perspective(400px) rotateY(0deg);
3495
+ }
3496
+ }
3497
+
3498
+ @keyframes bounceIn {
3499
+ 0% {
3500
+ opacity: 0;
3501
+ transform: scale(0.3);
3502
+ }
3503
+ 50% {
3504
+ opacity: 1;
3505
+ transform: scale(1.05);
3506
+ }
3507
+ 70% {
3508
+ transform: scale(0.9);
3509
+ }
3510
+ 100% {
3511
+ opacity: 1;
3512
+ transform: scale(1);
3513
+ }
3514
+ }
3515
+
3516
+ @keyframes wobble {
3517
+ 0% { transform: translateX(0%); }
3518
+ 15% { transform: translateX(-25%) rotate(-5deg); }
3519
+ 30% { transform: translateX(20%) rotate(3deg); }
3520
+ 45% { transform: translateX(-15%) rotate(-3deg); }
3521
+ 60% { transform: translateX(10%) rotate(2deg); }
3522
+ 75% { transform: translateX(-5%) rotate(-1deg); }
3523
+ 100% { transform: translateX(0%); }
3524
+ }
3525
+
3526
+ @keyframes ripple {
3527
+ 0% {
3528
+ transform: scale(0);
3529
+ opacity: 1;
3530
+ }
3531
+ 100% {
3532
+ transform: scale(4);
3533
+ opacity: 0;
3534
+ }
3535
+ }
3536
+
3537
+ @keyframes float {
3538
+ 0%, 100% { transform: translateY(0px); }
3539
+ 50% { transform: translateY(-20px); }
3540
+ }
3541
+
3542
+ @keyframes glow {
3543
+ 0%, 100% { box-shadow: 0 0 5px rgba(139, 92, 246, 0.5); }
3544
+ 50% { box-shadow: 0 0 20px rgba(139, 92, 246, 0.8), 0 0 30px rgba(139, 92, 246, 0.6); }
3545
+ }
3546
+
3547
+ @keyframes typewriter {
3548
+ from { width: 0; }
3549
+ to { width: 100%; }
3550
+ }
3551
+
3552
+ @keyframes blink {
3553
+ 0%, 50% { opacity: 1; }
3554
+ 51%, 100% { opacity: 0; }
3555
+ }
3556
+
3557
+ /* Animation Classes */
3558
+ .animate-fadeInDown {
3559
+ animation: fadeInDown 0.6s var(--ease-out);
3560
+ }
3561
+
3562
+ .animate-fadeInLeft {
3563
+ animation: fadeInLeft 0.6s var(--ease-out);
3564
+ }
3565
+
3566
+ .animate-fadeInRight {
3567
+ animation: fadeInRight 0.6s var(--ease-out);
3568
+ }
3569
+
3570
+ .animate-scaleIn {
3571
+ animation: scaleIn 0.5s var(--ease-out);
3572
+ }
3573
+
3574
+ .animate-rotateIn {
3575
+ animation: rotateIn 0.6s var(--ease-out);
3576
+ }
3577
+
3578
+ .animate-slideInUp {
3579
+ animation: slideInUp 0.6s var(--ease-out);
3580
+ }
3581
+
3582
+ .animate-slideInDown {
3583
+ animation: slideInDown 0.6s var(--ease-out);
3584
+ }
3585
+
3586
+ .animate-slideInLeft {
3587
+ animation: slideInLeft 0.6s var(--ease-out);
3588
+ }
3589
+
3590
+ .animate-zoomIn {
3591
+ animation: zoomIn 0.5s var(--ease-out);
3592
+ }
3593
+
3594
+ .animate-zoomOut {
3595
+ animation: zoomOut 0.5s var(--ease-out);
3596
+ }
3597
+
3598
+ .animate-flipInX {
3599
+ animation: flipInX 0.6s var(--ease-out);
3600
+ }
3601
+
3602
+ .animate-flipInY {
3603
+ animation: flipInY 0.6s var(--ease-out);
3604
+ }
3605
+
3606
+ .animate-bounceIn {
3607
+ animation: bounceIn 0.6s var(--ease-out);
3608
+ }
3609
+
3610
+ .animate-wobble {
3611
+ animation: wobble 1s var(--ease-out);
3612
+ }
3613
+
3614
+ .animate-float {
3615
+ animation: float 3s ease-in-out infinite;
3616
+ }
3617
+
3618
+ .animate-glow {
3619
+ animation: glow 2s ease-in-out infinite;
3620
+ }
3621
+
3622
+ /* Scroll-triggered animations */
3623
+ .slide-in {
3624
+ opacity: 0;
3625
+ transform: translateX(-30px);
3626
+ transition: all 0.6s var(--ease-out);
3627
+ }
3628
+
3629
+ .slide-in.animate-in {
3630
+ opacity: 1;
3631
+ transform: translateX(0);
3632
+ }
3633
+
3634
+ .scale-in {
3635
+ opacity: 0;
3636
+ transform: scale(0.8);
3637
+ transition: all 0.6s var(--ease-out);
3638
+ }
3639
+
3640
+ .scale-in.animate-in {
3641
+ opacity: 1;
3642
+ transform: scale(1);
3643
+ }
3644
+
3645
+ .rotate-in {
3646
+ opacity: 0;
3647
+ transform: rotate(-10deg) scale(0.8);
3648
+ transition: all 0.6s var(--ease-out);
3649
+ }
3650
+
3651
+ .rotate-in.animate-in {
3652
+ opacity: 1;
3653
+ transform: rotate(0deg) scale(1);
3654
+ }
3655
+
3656
+ /* Staggered animations */
3657
+ .stagger-item {
3658
+ opacity: 0;
3659
+ transform: translateY(20px);
3660
+ transition: all 0.6s var(--ease-out);
3661
+ }
3662
+
3663
+ .stagger-item.animate-in {
3664
+ opacity: 1;
3665
+ transform: translateY(0);
3666
+ }
3667
+
3668
+ /* Page transitions - removed to prevent white flash */
3669
+
3670
+ /* Micro-interactions */
3671
+ .btn {
3672
+ position: relative;
3673
+ overflow: hidden;
3674
+ transition: all var(--transition-fast);
3675
+ }
3676
+
3677
+ .btn:hover {
3678
+ transform: none !important;
3679
+ box-shadow: none !important;
3680
+ }
3681
+
3682
+ .btn:active {
3683
+ transform: translateY(0);
3684
+ box-shadow: var(--shadow-sm);
3685
+ }
3686
+
3687
+ /* Card hover effects */
3688
+ .collection-card,
3689
+ .blog-card {
3690
+ transition: all var(--transition-fast);
3691
+ }
3692
+
3693
+ .collection-card:hover,
3694
+ .blog-card:hover {
3695
+ transform: translateY(-8px) scale(1.02);
3696
+ box-shadow: var(--shadow-xl);
3697
+ }
3698
+
3699
+ /* Form focus effects */
3700
+ .form-group {
3701
+ position: relative;
3702
+ transition: all var(--transition-fast);
3703
+ }
3704
+
3705
+ .form-group.focused input,
3706
+ .form-group.focused textarea,
3707
+ .form-group.focused select {
3708
+ border-color: var(--color-accent);
3709
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
3710
+ }
3711
+
3712
+ /* Loading states */
3713
+ .loading-skeleton {
3714
+ background: linear-gradient(90deg, var(--color-gray-200) 25%, var(--color-gray-100) 50%, var(--color-gray-200) 75%);
3715
+ background-size: 200% 100%;
3716
+ animation: skeleton-loading 1.5s infinite;
3717
+ }
3718
+
3719
+ @keyframes skeleton-loading {
3720
+ 0% { background-position: 200% 0; }
3721
+ 100% { background-position: -200% 0; }
3722
+ }
3723
+
3724
+ .loaded {
3725
+ opacity: 1;
3726
+ transform: translateY(0);
3727
+ }
3728
+
3729
+ /* Parallax elements */
3730
+ [data-parallax] {
3731
+ will-change: transform;
3732
+ }
3733
+
3734
+ /* Performance optimizations */
3735
+ .optimize-animations * {
3736
+ will-change: auto;
3737
+ }
3738
+
3739
+ .optimize-animations .btn:hover,
3740
+ .optimize-animations .product-card:hover,
3741
+ .optimize-animations [data-parallax] {
3742
+ will-change: transform;
3743
+ }
3744
+ `;
3745
+ document.head.appendChild(style);