// ========================================
// APP.JS â Shared Components & Logic
// ========================================
// SVG Icons
const ICONS = {
search: '',
cart: '',
heart: '',
heartFill: '',
user: '',
eye: '',
minus: '',
plus: '',
trash: '',
x: '',
star: 'â
',
starHalf: 'â
',
filter: '',
grid: '',
list: '',
check: '',
arrowRight: '',
truck: 'đ', shield: 'đĄī¸', clock: 'â°', refresh: 'đ',
instagram: '',
telegram: '',
facebook: '',
};
function getStarsHTML(rating) {
let html = '';
for (let i = 1; i <= 5; i++) {
html += i <= Math.round(rating) ? 'â
' : 'â
';
}
return html;
}
// Current page detection
function getCurrentPage() {
const path = window.location.pathname.split('/').pop().split('?')[0];
if (path === 'catalog.html') return 'catalog';
if (path === 'product.html') return 'product';
if (path === 'cart.html') return 'cart';
if (path === 'checkout.html') return 'checkout';
if (path === 'wishlist.html') return 'wishlist';
if (path === 'profile.html') return 'profile';
return 'home';
}
// ========================================
// NAVBAR
// ========================================
function renderNavbar() {
const page = getCurrentPage();
const nav = document.createElement('nav');
nav.className = 'navbar';
nav.id = 'navbar';
nav.innerHTML = `
`;
document.body.prepend(nav);
// Mobile nav
const mobileNav = document.createElement('div');
mobileNav.className = 'mobile-nav';
mobileNav.id = 'mobileNav';
mobileNav.innerHTML = `
đ Bosh sahifa
đĻ Katalog
đ Kiyimlar
đ Formalar
đ Shimlar
đ Galistuklar
đ Aksessuarlar
â¤ī¸ Sevimlilar
đ Savatcha
đ¤ Profil
`;
document.body.prepend(mobileNav);
// Hamburger toggle
const hamburger = document.getElementById('hamburger');
hamburger.addEventListener('click', () => {
hamburger.classList.toggle('active');
mobileNav.classList.toggle('active');
});
// Profile button â login modal or profile page
document.getElementById('navProfileBtn')?.addEventListener('click', (e) => {
e.preventDefault();
if (isLoggedIn()) { window.location.href = 'profile.html'; }
else { openLoginModal(); }
});
// Scroll effect
window.addEventListener('scroll', () => {
nav.classList.toggle('scrolled', window.scrollY > 50);
});
document.getElementById('navSearchBtn')?.addEventListener('click', openSearchModal);
// Initialize Search Modal
renderSearchModal();
updateCartBadge();
}
function updateCartBadge() {
const badge = document.getElementById('cartBadge');
if (!badge) return;
const count = getCartCount();
badge.textContent = count;
badge.style.display = count > 0 ? 'flex' : 'none';
}
// ========================================
// LIVE SEARCH
// ========================================
function showSearchResults(query) {
const dropdown = document.getElementById('searchDropdown');
if (!dropdown) return;
const q = query.trim();
if (q.length < 2) { dropdown.classList.remove('active'); return; }
const results = searchProducts(q).slice(0, 6);
if (results.length === 0) {
dropdown.innerHTML = 'Mahsulot topilmadi
';
} else {
dropdown.innerHTML = results.map(p => `
${p.name}
${formatPrice(p.price)}
`).join('') + `Barcha natijalar â`;
}
dropdown.classList.add('active');
}
// ========================================
// ADVANCED SEARCH MODAL
// ========================================
function renderSearchModal() {
if (document.getElementById('searchModalOverlay')) return;
const modalHTML = `
Ko'p qidiriladiganlar:
Ko'ylak
Kostyum
Kurtka
Shim
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
document.getElementById('searchModalOverlay').addEventListener('click', (e) => {
if (e.target.id === 'searchModalOverlay') closeSearchModal();
});
const input = document.getElementById('modalSearchInput');
let debounceTimeout;
input.addEventListener('input', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => handleModalSearch(input.value), 250);
});
}
function openSearchModal() {
const overlay = document.getElementById('searchModalOverlay');
if (!overlay) return;
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
setTimeout(() => document.getElementById('modalSearchInput').focus(), 100);
}
function closeSearchModal() {
const overlay = document.getElementById('searchModalOverlay');
if (overlay) {
overlay.classList.remove('active');
document.body.style.overflow = '';
}
}
function setSearchQuery(q) {
const input = document.getElementById('modalSearchInput');
input.value = q;
handleModalSearch(q);
}
function highlightText(text, query) {
if (!query) return text;
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '$1');
}
function handleModalSearch(query) {
const resultsContainer = document.getElementById('searchModalResults');
const suggestions = document.getElementById('searchModalSuggestions');
const q = query.trim().toLowerCase();
if (q.length < 2) {
resultsContainer.style.display = 'none';
suggestions.style.display = 'block';
return;
}
suggestions.style.display = 'none';
resultsContainer.style.display = 'grid';
const results = searchProducts(q).slice(0, 8);
if (results.length === 0) {
resultsContainer.innerHTML = 'Hech narsa topilmadi
';
} else {
resultsContainer.innerHTML = results.map(p => `
${highlightText(p.name, q)}
${highlightText(p.category, q)}
${formatPrice(p.price)}
`).join('');
}
}
// ========================================
// FOOTER
// ========================================
function renderFooter() {
const settings = JSON.parse(localStorage.getItem('mtextile_settings')) || {};
const globalAddress = settings.address || "Toshkent, O'zbekiston";
const footer = document.createElement('footer');
footer.className = 'footer';
footer.innerHTML = `
`;
document.body.appendChild(footer);
}
// ========================================
// TOAST NOTIFICATIONS
// ========================================
function initToastContainer() {
if (!document.getElementById('toastContainer')) {
const container = document.createElement('div');
container.className = 'toast-container';
container.id = 'toastContainer';
document.body.appendChild(container);
}
}
function showToast(title, message, type = 'success', duration = 3000) {
initToastContainer();
const container = document.getElementById('toastContainer');
const icons = { success: 'â
', error: 'â', info: 'âšī¸' };
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
${icons[type] || 'đĸ'}
${title}
${message ? `
${message}
` : ''}
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('toast-exit');
setTimeout(() => toast.remove(), 300);
}, duration);
}
// ========================================
// PRODUCT CARD RENDERER
// ========================================
function renderProductCard(product) {
const wishlisted = isInWishlist(product.id);
const card = document.createElement('div');
card.className = 'product-card';
card.innerHTML = `
${product.isNew ? 'YANGI' : ''}
${product.discount > 0 ? `-${product.discount}%` : ''}
${CATEGORIES[product.category]?.name || product.category}
${product.name}
${getStarsHTML(product.rating)}
(${product.reviewCount})
${formatPrice(product.price)}
${product.oldPrice ? `${formatPrice(product.oldPrice)}` : ''}
`;
card.addEventListener('click', () => {
window.location.href = `product.html?id=${product.id}`;
});
return card;
}
function handleWishlist(id, btn) {
toggleWishlist(id);
const wishlisted = isInWishlist(id);
btn.classList.toggle('wishlisted', wishlisted);
btn.innerHTML = wishlisted ? ICONS.heartFill : ICONS.heart;
showToast(wishlisted ? 'Sevimlilarga qo\'shildi' : 'Sevimlilardan olib tashlandi', '', wishlisted ? 'success' : 'info');
}
function quickAddToCart(id, event) {
id = parseInt(id);
const product = getProductById(id);
if (!product) return;
addToCart(id, product.sizes[0], product.colors[0], 1);
updateCartBadge();
showToast('Savatga qo\'shildi', product.name, 'success');
if (event) {
const btn = event.currentTarget || event.target;
let img = btn.closest('.product-card')?.querySelector('img') || btn.closest('.modal')?.querySelector('img') || document.querySelector('#mainImage');
if (img) flyToCart(img);
}
}
function flyToCart(imgElement) {
if (!imgElement) return;
const cartIcon = document.getElementById('navCartBtn');
if (!cartIcon) return;
const imgClone = imgElement.cloneNode(true);
const rect = imgElement.getBoundingClientRect();
const cartRect = cartIcon.getBoundingClientRect();
imgClone.className = 'flying-img';
imgClone.style.top = rect.top + 'px';
imgClone.style.left = rect.left + 'px';
imgClone.style.width = rect.width + 'px';
imgClone.style.height = rect.height + 'px';
document.body.appendChild(imgClone);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
imgClone.style.top = cartRect.top + 'px';
imgClone.style.left = cartRect.left + 'px';
imgClone.style.width = '20px';
imgClone.style.height = '20px';
imgClone.style.opacity = '0';
imgClone.style.transform = 'scale(0.5)';
});
});
setTimeout(() => {
imgClone.remove();
cartIcon.classList.add('bump');
setTimeout(() => cartIcon.classList.remove('bump'), 300);
}, 700);
}
// ========================================
// INTERSECTION OBSERVER FOR ANIMATIONS
// ========================================
function initRevealAnimations() {
// Convert old manual reveals to AOS targets
document.querySelectorAll('.reveal').forEach((el, index) => {
if (!el.hasAttribute('data-aos')) {
el.setAttribute('data-aos', 'fade-up');
el.setAttribute('data-aos-delay', (index % 4) * 100);
el.classList.remove('reveal');
}
});
// Automatically add stagger effects for product grids
document.querySelectorAll('.stagger').forEach(grid => {
Array.from(grid.children).forEach((child, idx) => {
if (!child.hasAttribute('data-aos')) {
child.setAttribute('data-aos', 'fade-up');
child.setAttribute('data-aos-delay', (idx % 8) * 100);
}
});
});
if (typeof AOS !== 'undefined') {
AOS.refreshHard();
}
}
// ========================================
// QUICK VIEW MODAL
// ========================================
function openQuickView(id) {
const product = getProductById(id);
if (!product) return;
trackRecentlyViewed(id);
let overlay = document.getElementById('quickViewOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'quickViewOverlay';
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeQuickView(); });
document.body.appendChild(overlay);
}
const wishlisted = isInWishlist(product.id);
overlay.innerHTML = `
${CATEGORIES[product.category]?.name}
${product.name}
${getStarsHTML(product.rating)} (${product.reviewCount})
${formatPrice(product.price)}
${product.oldPrice ? `${formatPrice(product.oldPrice)}-${product.discount}%` : ''}
${product.description}
O'lchamlar: ${product.sizes.join(', ')}
Ranglar: ${product.colors.map(c => COLORS_MAP[c]?.name || c).join(', ')}
`;
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeQuickView() {
const overlay = document.getElementById('quickViewOverlay');
if (overlay) { overlay.classList.remove('active'); document.body.style.overflow = ''; }
}
function handleWishlistQV(id, btn) {
toggleWishlist(id);
const w = isInWishlist(id);
btn.innerHTML = w ? ICONS.heartFill : ICONS.heart;
btn.style.color = w ? 'var(--clr-error)' : '';
showToast(w ? 'Sevimlilarga qo\'shildi' : 'Olib tashlandi', '', w ? 'success' : 'info');
}
// ========================================
// BACK TO TOP BUTTON
// ========================================
function initBackToTop() {
const btn = document.createElement('button');
btn.className = 'back-to-top';
btn.id = 'backToTop';
btn.innerHTML = '';
btn.title = 'Yuqoriga';
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
document.body.appendChild(btn);
window.addEventListener('scroll', () => {
btn.classList.toggle('visible', window.scrollY > 400);
});
}
// ========================================
// RECENTLY VIEWED PRODUCTS
// ========================================
function trackRecentlyViewed(id) {
let rv = JSON.parse(localStorage.getItem('mtextile_recent') || '[]');
rv = rv.filter(i => i !== id);
rv.unshift(id);
if (rv.length > 10) rv = rv.slice(0, 10);
localStorage.setItem('mtextile_recent', JSON.stringify(rv));
}
function getRecentlyViewed() {
return JSON.parse(localStorage.getItem('mtextile_recent') || '[]');
}
function renderRecentlyViewed(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
const ids = getRecentlyViewed();
if (ids.length === 0) { container.closest('.recently-viewed-section')?.remove(); return; }
ids.slice(0, 4).forEach(id => {
const p = getProductById(id);
if (p) container.appendChild(renderProductCard(p));
});
}
// ========================================
// ANIMATED STAT COUNTERS
// ========================================
function animateCounters() {
document.querySelectorAll('.hero-stat-num[data-count]').forEach(el => {
const target = parseInt(el.dataset.count);
const suffix = el.dataset.suffix || '';
let current = 0;
const step = Math.max(1, Math.floor(target / 40));
const timer = setInterval(() => {
current += step;
if (current >= target) { current = target; clearInterval(timer); }
el.textContent = current + suffix;
}, 30);
});
}
// ========================================
// SIZE GUIDE MODAL
// ========================================
function openSizeGuide() {
let overlay = document.getElementById('sizeGuideOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.id = 'sizeGuideOverlay';
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSizeGuide(); });
document.body.appendChild(overlay);
}
overlay.innerHTML = `
| O'lcham |
Ko'krak (sm) |
Bel (sm) |
Bo'y (sm) |
| XS | 82-86 | 62-66 | 155-162 |
| S | 86-90 | 66-70 | 162-170 |
| M | 90-96 | 70-76 | 170-176 |
| L | 96-102 | 76-82 | 176-182 |
| XL | 102-108 | 82-88 | 182-188 |
| XXL | 108-114 | 88-94 | 188-194 |
* O'lchamlar taxminiy. Aniq o'lchamlar uchun mahsulot tavsifiga qarang.
`;
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeSizeGuide() {
const overlay = document.getElementById('sizeGuideOverlay');
if (overlay) { overlay.classList.remove('active'); document.body.style.overflow = ''; }
}
// ========================================
// THEME TOGGLE (Dark / Light)
// ========================================
function initThemeToggle() {
document.getElementById('themeToggle')?.addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-theme') || 'dark';
const next = cur === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('mtextile_theme', next);
});
}
function applyTheme(theme) {
if (theme === 'light') document.documentElement.setAttribute('data-theme', 'light');
else document.documentElement.removeAttribute('data-theme');
const icon = document.getElementById('themeIcon');
if (icon) icon.textContent = theme === 'light' ? 'âī¸' : 'đ';
}
// ========================================
// HERO SLIDER
// ========================================
function initHeroSlider() {
const slider = document.getElementById('heroSlider');
if (!slider) return;
const slides = [
{ tag: '⨠Yangi kolleksiya â 2026 Bahor', title: 'Stilingizni kashf eting', desc: 'Eng sifatli kiyim-kechak mahsulotlarini uyingizdan turib xarid qiling.' },
{ tag: 'đĨ Maxsus chegirma', title: '30% gacha tejang', desc: 'Tanlangan mahsulotlarga katta chegirmalar!' },
{ tag: 'đ Maktab formasi', title: 'Sifatli formalar', desc: 'Maktab va ish uchun eng sifatli formal kiyimlar.' }
];
let cur = 0, iv;
function render(i) {
const s = slides[i];
slider.innerHTML = '' + s.tag + '
' + s.title + '
' + s.desc + '
';
document.getElementById('heroDots')?.querySelectorAll('.hero-dot').forEach(function (d, j) { d.classList.toggle('active', j === i); });
}
var dots = document.getElementById('heroDots');
if (dots) slides.forEach(function (_, i) {
var d = document.createElement('button');
d.className = 'hero-dot' + (i === 0 ? ' active' : '');
d.addEventListener('click', function () { cur = i; render(i); clearInterval(iv); iv = setInterval(next, 5000); });
dots.appendChild(d);
});
function next() { cur = (cur + 1) % slides.length; render(cur); }
render(0);
iv = setInterval(next, 5000);
}
// ========================================
// LOGIN / REGISTER MODAL
// ========================================
function openLoginModal() {
var ov = document.getElementById('loginOverlay');
if (!ov) { ov = document.createElement('div'); ov.className = 'modal-overlay'; ov.id = 'loginOverlay'; ov.addEventListener('click', function (e) { if (e.target === ov) closeLoginModal(); }); document.body.appendChild(ov); }
ov.innerHTML = '' +
'
' +
'
' +
'
Kirish
Ro\'yxatdan o\'tish
' +
'
' +
'
';
ov.classList.add('active'); document.body.style.overflow = 'hidden';
document.getElementById('loginCloseBtn').addEventListener('click', closeLoginModal);
document.getElementById('tabLogin').addEventListener('click', function () { switchLoginTab('login'); });
document.getElementById('tabRegister').addEventListener('click', function () { switchLoginTab('register'); });
document.getElementById('loginSubmitBtn').addEventListener('click', doLogin);
document.getElementById('regSubmitBtn').addEventListener('click', doRegister);
document.getElementById('googleSignInBtn').addEventListener('click', async function () {
this.disabled = true; this.style.opacity = '0.7';
await signInWithGoogle();
this.disabled = false; this.style.opacity = '1';
if (isLoggedIn()) { closeLoginModal(); if (typeof updateAuthUI === 'function') updateAuthUI(); }
});
}
function closeLoginModal() { var ov = document.getElementById('loginOverlay'); if (ov) { ov.classList.remove('active'); document.body.style.overflow = ''; } }
function switchLoginTab(tab) { document.querySelectorAll('.login-tab').forEach(function (t) { t.classList.toggle('active', t.dataset.tab === tab); }); document.getElementById('loginForm').style.display = tab === 'login' ? 'block' : 'none'; document.getElementById('registerForm').style.display = tab === 'register' ? 'block' : 'none'; }
async function doLogin() {
var id = document.getElementById('loginId').value.trim(), pass = document.getElementById('loginPass').value;
if (!id) { showToast('Xatolik', 'Telefon yoki email kiriting', 'error'); return; }
if (!pass || pass.length < 4) { showToast('Xatolik', 'Parol kamida 4 belgi', 'error'); return; }
try {
var result = await API.login(id, pass);
if (!result.success) { showToast('Xatolik', result.message, 'error'); return; }
localStorage.setItem('mtextile_session', JSON.stringify(result.user));
closeLoginModal(); showToast('Xush kelibsiz!', result.user.name + ', tizimga kirdingiz', 'success');
if (typeof updateAuthUI === 'function') updateAuthUI();
} catch (e) { showToast('Xatolik', "Serverga ulanib bo'lmadi", 'error'); }
}
async function doRegister() {
var name = document.getElementById('regName').value.trim(), phone = document.getElementById('regPhone').value.trim(), email = document.getElementById('regEmail').value.trim(), pass = document.getElementById('regPass').value;
if (!name || name.length < 2) { showToast('Xatolik', 'Ismingizni kiriting', 'error'); return; }
if (!phone || phone.length < 9) { showToast('Xatolik', 'Telefon raqam kiriting', 'error'); return; }
if (!pass || pass.length < 6) { showToast('Xatolik', 'Parol kamida 6 belgi', 'error'); return; }
try {
var result = await API.register(name, phone, pass, email);
if (!result.success) { showToast('Xatolik', result.message, 'error'); return; }
localStorage.setItem('mtextile_session', JSON.stringify(result.user));
closeLoginModal(); showToast('Muvaffaqiyat!', "Ro'yxatdan o'tdingiz!", 'success');
if (typeof updateAuthUI === 'function') updateAuthUI();
} catch (e) { showToast('Xatolik', "Serverga ulanib bo'lmadi", 'error'); }
}
// ========================================
// PRODUCT IMAGE ZOOM
// ========================================
function initImageZoom() {
var img = document.getElementById('mainImage');
if (!img) return;
var c = img.parentElement; c.style.overflow = 'hidden'; c.style.cursor = 'zoom-in';
img.addEventListener('mousemove', function (e) {
var r = c.getBoundingClientRect();
img.style.transformOrigin = ((e.clientX - r.left) / r.width * 100) + '% ' + ((e.clientY - r.top) / r.height * 100) + '%';
img.style.transform = 'scale(1.8)';
});
img.addEventListener('mouseleave', function () { img.style.transform = 'scale(1)'; img.style.transformOrigin = 'center'; });
}
// ========================================
// MICRO-INTERACTIONS
// ========================================
function initRippleEffect() {
document.addEventListener('click', function (e) {
const btn = e.target.closest('.btn');
if (!btn) return;
const circle = document.createElement('span');
const d = Math.max(btn.clientWidth, btn.clientHeight);
const rect = btn.getBoundingClientRect();
circle.style.width = circle.style.height = d + 'px';
circle.style.left = e.clientX - rect.left - d / 2 + 'px';
circle.style.top = e.clientY - rect.top - d / 2 + 'px';
circle.classList.add('ripple');
const existing = btn.querySelector('.ripple');
if (existing) existing.remove();
btn.appendChild(circle);
setTimeout(() => circle.remove(), 600);
});
}
function initTiltEffect() {
const cards = document.querySelectorAll('.product-card:not(.tilt-bound)');
cards.forEach(card => {
card.classList.add('tilt-bound');
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const xPct = (e.clientX - rect.left) / rect.width;
const yPct = (e.clientY - rect.top) / rect.height;
card.style.transform = `perspective(1000px) rotateX(${(yPct - 0.5) * -12}deg) rotateY(${(xPct - 0.5) * 12}deg) scale3d(1.02, 1.02, 1.02)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.transition = 'transform 0.4s ease';
});
card.addEventListener('mouseenter', () => {
card.style.transition = 'none';
});
});
}
// ========================================
// INIT
// ========================================
document.addEventListener('DOMContentLoaded', () => {
// Preloader
const pl = document.getElementById('global-preloader');
if (pl) {
setTimeout(() => { pl.classList.add('hidden'); setTimeout(() => pl.style.display = 'none', 600); }, 300);
}
// AOS
if (typeof AOS !== 'undefined') AOS.init({ duration: 800, once: true, offset: 50 });
document.body.classList.add('page-fade-enter');
setTimeout(() => document.body.classList.remove('page-fade-enter'), 450);
var saved = localStorage.getItem('mtextile_theme') || 'dark';
applyTheme(saved);
renderNavbar();
renderFooter();
initRevealAnimations();
initBackToTop();
initThemeToggle();
initRippleEffect();
document.addEventListener('cartUpdated', updateCartBadge);
if (getCurrentPage() === 'home') { setTimeout(animateCounters, 600); initHeroSlider(); }
if (getCurrentPage() === 'product') {
var id = new URLSearchParams(window.location.search).get('id');
if (id) trackRecentlyViewed(parseInt(id));
setTimeout(initImageZoom, 500);
}
// Try initializing tilt on load, and setup observer for dynamic changes
setTimeout(initTiltEffect, 500);
const observer = new MutationObserver(initTiltEffect);
observer.observe(document.body, { childList: true, subtree: true });
});