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