ibrohm's picture
Initial deploy via assistant API
7b3aac2 verified
// ========================================
// APP.JS — Shared Components & Logic
// ========================================
// SVG Icons
const ICONS = {
search: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>',
cart: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>',
heart: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>',
heartFill: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>',
user: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
eye: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>',
minus: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/></svg>',
plus: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5v14"/></svg>',
trash: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
x: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
star: '★',
starHalf: '★',
filter: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>',
grid: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>',
list: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>',
check: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>',
arrowRight: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
truck: '🚚', shield: '🛡️', clock: '⏰', refresh: '🔄',
instagram: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="2" width="20" height="20" rx="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><path d="M17.5 6.5h.01"/></svg>',
telegram: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 1 0 24 12.056A12.013 12.013 0 0 0 11.944 0Zm5.654 8.22l-1.8 8.49c-.136.612-.492.764-.998.476l-2.756-2.032l-1.33 1.278c-.147.147-.27.27-.554.27l.198-2.8l5.1-4.608c.222-.198-.048-.308-.344-.11l-6.306 3.972l-2.716-.848c-.59-.184-.602-.59.124-.874l10.62-4.094c.49-.176.92.12.762.87Z"/></svg>',
facebook: '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
};
function getStarsHTML(rating) {
let html = '';
for (let i = 1; i <= 5; i++) {
html += i <= Math.round(rating) ? '<span style="color:var(--clr-warning)">★</span>' : '<span style="color:var(--clr-gray)">★</span>';
}
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 = `
<div class="navbar-inner">
<a href="index.html" class="navbar-logo">M-TEXTILE</a>
<div class="navbar-nav" id="navLinks">
<a href="index.html" class="${page === 'home' ? 'active' : ''}">Bosh sahifa</a>
<a href="catalog.html" class="${page === 'catalog' ? 'active' : ''}">Katalog</a>
<a href="catalog.html?category=kiyimlar">Kiyimlar</a>
<a href="catalog.html?category=shimlar">Shimlar</a>
<a href="catalog.html?category=galistuklar">Galistuklar</a>
</div>
<div class="navbar-actions">
<button class="btn btn-icon btn-ghost" id="navSearchBtn" title="Qidirish">
${ICONS.search}
</button>
<button class="btn btn-icon btn-ghost" id="themeToggle" title="Rejimni o'zgartirish">
<span id="themeIcon">🌙</span>
</button>
<a href="wishlist.html" class="btn btn-icon btn-ghost" title="Sevimlilar">${ICONS.heart}</a>
<a href="cart.html" class="btn btn-icon btn-ghost cart-badge" title="Savatcha" id="navCartBtn">
${ICONS.cart}
<span class="badge-count" id="cartBadge" style="display:none">0</span>
</a>
<a href="#" class="btn btn-icon btn-ghost" title="Profil" id="navProfileBtn">${ICONS.user}</a>
<div class="hamburger" id="hamburger">
<span></span><span></span><span></span>
</div>
</div>
</div>
`;
document.body.prepend(nav);
// Mobile nav
const mobileNav = document.createElement('div');
mobileNav.className = 'mobile-nav';
mobileNav.id = 'mobileNav';
mobileNav.innerHTML = `
<a href="index.html" class="${page === 'home' ? 'active' : ''}">🏠 Bosh sahifa</a>
<a href="catalog.html" class="${page === 'catalog' ? 'active' : ''}">📦 Katalog</a>
<a href="catalog.html?category=kiyimlar">👔 Kiyimlar</a>
<a href="catalog.html?category=formalar">🎓 Formalar</a>
<a href="catalog.html?category=shimlar">👖 Shimlar</a>
<a href="catalog.html?category=galistuklar">🎀 Galistuklar</a>
<a href="catalog.html?category=aksessuarlar">👜 Aksessuarlar</a>
<a href="wishlist.html">❤️ Sevimlilar</a>
<a href="cart.html">🛒 Savatcha</a>
<a href="profile.html">👤 Profil</a>
`;
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 = '<div class="search-dropdown-empty">Mahsulot topilmadi</div>';
} else {
dropdown.innerHTML = results.map(p => `
<a href="product.html?id=${p.id}" class="search-dropdown-item">
<img src="${p.images[0]}" alt="${p.name}">
<div class="search-dropdown-info">
<div class="search-dropdown-name">${p.name}</div>
<div class="search-dropdown-price">${formatPrice(p.price)}</div>
</div>
</a>
`).join('') + `<a href="catalog.html?search=${encodeURIComponent(q)}" class="search-dropdown-all">Barcha natijalar →</a>`;
}
dropdown.classList.add('active');
}
// ========================================
// ADVANCED SEARCH MODAL
// ========================================
function renderSearchModal() {
if (document.getElementById('searchModalOverlay')) return;
const modalHTML = `
<div class="search-modal-overlay" id="searchModalOverlay">
<div class="search-modal">
<div class="search-modal-header">
<div class="search-modal-input-wrapper">
${ICONS.search}
<input type="text" placeholder="Mahsulot yoki kategoriya nomi..." id="modalSearchInput" autocomplete="off">
</div>
<button class="btn btn-icon btn-ghost" onclick="closeSearchModal()">${ICONS.x}</button>
</div>
<div class="search-modal-body">
<div id="searchModalSuggestions" class="search-suggestions">
<h4 style="margin-bottom:1rem;font-size:0.9rem;color:var(--clr-text-secondary);">Ko'p qidiriladiganlar:</h4>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<span class="badge" style="cursor:pointer;" onclick="setSearchQuery('Ko\\'ylak')">Ko'ylak</span>
<span class="badge" style="cursor:pointer;" onclick="setSearchQuery('Kostyum')">Kostyum</span>
<span class="badge" style="cursor:pointer;" onclick="setSearchQuery('Kurtka')">Kurtka</span>
<span class="badge" style="cursor:pointer;" onclick="setSearchQuery('Shim')">Shim</span>
</div>
</div>
<div id="searchModalResults" class="search-results-grid" style="display:none;margin-top:1.5rem;display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:1rem;"></div>
</div>
</div>
</div>
`;
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, '<mark style="background:var(--clr-accent);color:var(--clr-bg);padding:0 2px;border-radius:2px;">$1</mark>');
}
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 = '<div style="grid-column:1/-1;text-align:center;padding:2rem;color:var(--clr-text-secondary);">Hech narsa topilmadi</div>';
} else {
resultsContainer.innerHTML = results.map(p => `
<a href="product.html?id=${p.id}" style="display:flex;gap:1rem;background:var(--clr-surface);padding:0.75rem;border-radius:var(--radius-md);border:1px solid var(--clr-border);text-decoration:none;transition:0.2s ease;">
<img src="${p.images[0]}" alt="${p.name}" style="width:60px;height:60px;object-fit:cover;border-radius:var(--radius-sm);">
<div>
<div style="font-weight:600;color:var(--clr-text-primary);margin-bottom:0.25rem;">${highlightText(p.name, q)}</div>
<div style="color:var(--clr-text-secondary);font-size:0.85rem;margin-bottom:0.25rem;">${highlightText(p.category, q)}</div>
<div style="color:var(--clr-accent);font-weight:600;">${formatPrice(p.price)}</div>
</div>
</a>
`).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 = `
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<div class="footer-brand-name">M-TEXTILE</div>
<p class="footer-brand-desc">Eng sifatli kiyim-kechak mahsulotlarini uyingizdan turib oling. Barcha buyurtmalar do'kondan olib ketilishi kerak.</p>
<div class="footer-social" style="margin-top: 1.5rem;">
<a href="#" title="Instagram">${ICONS.instagram}</a>
<a href="#" title="Telegram">${ICONS.telegram}</a>
<a href="#" title="Facebook">${ICONS.facebook}</a>
</div>
</div>
<div>
<h4 class="footer-heading">Kategoriyalar</h4>
<div class="footer-links">
<a href="catalog.html?category=kiyimlar">Kiyimlar</a>
<a href="catalog.html?category=formalar">Formalar</a>
<a href="catalog.html?category=shimlar">Shimlar</a>
<a href="catalog.html?category=galistuklar">Galistuklar</a>
<a href="catalog.html?category=aksessuarlar">Aksessuarlar</a>
</div>
</div>
<div>
<h4 class="footer-heading">Yordam</h4>
<div class="footer-links">
<a href="#">Do'kondan olib ketish usuli</a>
<a href="#">Qaytarish siyosati</a>
<a href="#">To'lov usullari</a>
<a href="#">Ko'p so'raladigan savollar</a>
</div>
</div>
<div>
<h4 class="footer-heading">Aloqa</h4>
<div class="footer-links">
<a href="tel:+998996083712">+998 99 608 37 12</a>
<a href="mailto:info@m-textile.uz">info@m-textile.uz</a>
<a href="#">${globalAddress}</a>
<a href="#">Dushanba-Juma: 9:00-18:00</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>© 2026 M-TEXTILE. Barcha huquqlar himoyalangan.</p>
<div style="display:flex; gap: 1.5rem;">
<a href="#">Maxfiylik siyosati</a>
<a href="#">Foydalanish shartlari</a>
</div>
</div>
</div>
`;
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 = `
<span class="toast-icon">${icons[type] || '📢'}</span>
<div class="toast-content">
<div class="toast-title">${title}</div>
${message ? `<div class="toast-message">${message}</div>` : ''}
</div>
<button class="toast-close" onclick="this.closest('.toast').remove()">${ICONS.x}</button>
`;
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 = `
<div class="product-card-image">
<img src="${product.images[0]}" alt="${product.name}" loading="lazy">
<div class="product-card-badges">
${product.isNew ? '<span class="badge badge-new">YANGI</span>' : ''}
${product.discount > 0 ? `<span class="badge badge-discount">-${product.discount}%</span>` : ''}
</div>
<div class="product-card-actions">
<button class="product-card-action-btn ${wishlisted ? 'wishlisted' : ''}" onclick="event.stopPropagation();handleWishlist(${product.id},this)" title="Sevimlilar">
${wishlisted ? ICONS.heartFill : ICONS.heart}
</button>
<button class="product-card-action-btn" onclick="event.stopPropagation();openQuickView(${product.id})" title="Tezkor ko'rish">${ICONS.eye}</button>
<button class="product-card-action-btn" onclick="event.stopPropagation();quickAddToCart(${product.id}, event)" title="Savatga qo'shish">${ICONS.cart}</button>
</div>
</div>
<div class="product-card-body">
<div class="product-card-category">${CATEGORIES[product.category]?.name || product.category}</div>
<h3 class="product-card-name">${product.name}</h3>
<div class="product-card-rating">
<span class="stars">${getStarsHTML(product.rating)}</span>
<span class="count">(${product.reviewCount})</span>
</div>
<div class="product-card-price">
<span class="price-current">${formatPrice(product.price)}</span>
${product.oldPrice ? `<span class="price-old">${formatPrice(product.oldPrice)}</span>` : ''}
</div>
</div>
`;
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 = `
<div class="modal" style="max-width:800px;">
<div class="modal-header">
<h2 class="modal-title">Tezkor ko'rish</h2>
<button class="modal-close" onclick="closeQuickView()">${ICONS.x}</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;">
<div style="aspect-ratio:3/4;border-radius:var(--radius-lg);overflow:hidden;background:var(--clr-surface);">
<img src="${product.images[0]}" alt="${product.name}" style="width:100%;height:100%;object-fit:cover;">
</div>
<div>
<div style="font-size:0.75rem;color:var(--clr-text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.5rem;">${CATEGORIES[product.category]?.name}</div>
<h3 style="font-size:1.35rem;margin-bottom:0.75rem;">${product.name}</h3>
<div style="margin-bottom:0.75rem;">${getStarsHTML(product.rating)} <span style="color:var(--clr-text-muted);font-size:0.85rem;">(${product.reviewCount})</span></div>
<div style="display:flex;align-items:baseline;gap:0.75rem;margin-bottom:1rem;">
<span style="font-size:1.5rem;font-weight:700;">${formatPrice(product.price)}</span>
${product.oldPrice ? `<span style="text-decoration:line-through;color:var(--clr-text-muted);">${formatPrice(product.oldPrice)}</span><span class="badge badge-discount">-${product.discount}%</span>` : ''}
</div>
<p style="color:var(--clr-text-secondary);font-size:0.9rem;line-height:1.7;margin-bottom:1.25rem;">${product.description}</p>
<div style="margin-bottom:0.75rem;font-size:0.85rem;">O'lchamlar: <strong>${product.sizes.join(', ')}</strong></div>
<div style="margin-bottom:1.25rem;font-size:0.85rem;">Ranglar: <strong>${product.colors.map(c => COLORS_MAP[c]?.name || c).join(', ')}</strong></div>
<div style="display:flex;gap:0.75rem;">
<button class="btn btn-primary" style="flex:1;" onclick="quickAddToCart(${product.id}, event);closeQuickView()">🛒 Savatga</button>
<button class="btn btn-outline" onclick="window.location.href='product.html?id=${product.id}'">${ICONS.eye} Batafsil</button>
<button class="btn btn-outline btn-icon" onclick="handleWishlistQV(${product.id},this)" style="${wishlisted ? 'color:var(--clr-error)' : ''}">${wishlisted ? ICONS.heartFill : ICONS.heart}</button>
</div>
</div>
</div>
</div>
`;
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 = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 15l-6-6-6 6"/></svg>';
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 = `
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">📏 O'lcham yo'riqnomasi</h2>
<button class="modal-close" onclick="closeSizeGuide()">${ICONS.x}</button>
</div>
<table style="width:100%;border-collapse:collapse;font-size:0.9rem;">
<thead><tr style="border-bottom:2px solid var(--clr-border);">
<th style="padding:0.75rem;text-align:left;">O'lcham</th>
<th style="padding:0.75rem;text-align:center;">Ko'krak (sm)</th>
<th style="padding:0.75rem;text-align:center;">Bel (sm)</th>
<th style="padding:0.75rem;text-align:center;">Bo'y (sm)</th>
</tr></thead>
<tbody>
<tr style="border-bottom:1px solid var(--clr-border);"><td style="padding:0.75rem;">XS</td><td style="text-align:center;">82-86</td><td style="text-align:center;">62-66</td><td style="text-align:center;">155-162</td></tr>
<tr style="border-bottom:1px solid var(--clr-border);"><td style="padding:0.75rem;">S</td><td style="text-align:center;">86-90</td><td style="text-align:center;">66-70</td><td style="text-align:center;">162-170</td></tr>
<tr style="border-bottom:1px solid var(--clr-border);"><td style="padding:0.75rem;">M</td><td style="text-align:center;">90-96</td><td style="text-align:center;">70-76</td><td style="text-align:center;">170-176</td></tr>
<tr style="border-bottom:1px solid var(--clr-border);"><td style="padding:0.75rem;">L</td><td style="text-align:center;">96-102</td><td style="text-align:center;">76-82</td><td style="text-align:center;">176-182</td></tr>
<tr style="border-bottom:1px solid var(--clr-border);"><td style="padding:0.75rem;">XL</td><td style="text-align:center;">102-108</td><td style="text-align:center;">82-88</td><td style="text-align:center;">182-188</td></tr>
<tr><td style="padding:0.75rem;">XXL</td><td style="text-align:center;">108-114</td><td style="text-align:center;">88-94</td><td style="text-align:center;">188-194</td></tr>
</tbody>
</table>
<p style="margin-top:1rem;font-size:0.8rem;color:var(--clr-text-muted);">* O'lchamlar taxminiy. Aniq o'lchamlar uchun mahsulot tavsifiga qarang.</p>
</div >
`;
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 <span>kashf eting</span>', desc: 'Eng sifatli kiyim-kechak mahsulotlarini uyingizdan turib xarid qiling.' },
{ tag: '🔥 Maxsus chegirma', title: '<span>30%</span> gacha tejang', desc: 'Tanlangan mahsulotlarga katta chegirmalar!' },
{ tag: '🎓 Maktab formasi', title: 'Sifatli <span>formalar</span>', desc: 'Maktab va ish uchun eng sifatli formal kiyimlar.' }
];
let cur = 0, iv;
function render(i) {
const s = slides[i];
slider.innerHTML = '<div class="hero-slide animate-fade-in"><div class="hero-tag">' + s.tag + '</div><h1 class="hero-title">' + s.title + '</h1><p class="hero-desc">' + s.desc + '</p><div class="hero-actions"><a href="catalog.html" class="btn btn-primary btn-lg">Xarid qilish →</a><a href="catalog.html?filter=new" class="btn btn-outline btn-lg">Yangi kelganlar</a></div></div>';
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 = '<div class="modal" style="max-width:440px;"><div class="modal-header"><h2 class="modal-title">Kirish</h2><button class="modal-close" id="loginCloseBtn">' + ICONS.x + '</button></div>' +
'<div style="padding: 0 var(--space-lg);"><button id="googleSignInBtn" class="btn" style="width:100%; display:flex; align-items:center; justify-content:center; gap:12px; padding:14px; background:#fff; color:#333; border:1px solid #ddd; border-radius:var(--radius-md); font-size:var(--fs-base); font-weight:500; cursor:pointer;"><svg width="20" height="20" viewBox="0 0 48 48"><path fill="#FFC107" d="M43.6 20.1H42V20H24v8h11.3C33.9 33.1 29.4 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3 0 5.8 1.1 7.9 3l5.7-5.7C34 5.9 29.3 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.2-2.6-.4-3.9z"/><path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.5 15.5 18.8 12 24 12c3 0 5.8 1.1 7.9 3l5.7-5.7C34 5.9 29.3 4 24 4 16.3 4 9.7 8.3 6.3 14.7z"/><path fill="#4CAF50" d="M24 44c5.2 0 9.9-1.8 13.4-5l-6.2-5.2c-2 1.5-4.5 2.4-7.2 2.4-5.3 0-9.8-3.6-11.4-8.5l-6.5 5C9.5 39.6 16.2 44 24 44z"/><path fill="#1976D2" d="M43.6 20.1H42V20H24v8h11.3c-.8 2.2-2.2 4.1-4.1 5.5l6.2 5.2C37 39.1 44 34 44 24c0-1.3-.2-2.6-.4-3.9z"/></svg>Google orqali kirish</button></div>' +
'<div style="display:flex; align-items:center; gap:12px; padding:var(--space-md) var(--space-lg);"><div style="flex:1; height:1px; background:var(--clr-border);"></div><span style="color:var(--clr-text-muted); font-size:var(--fs-sm);">yoki</span><div style="flex:1; height:1px; background:var(--clr-border);"></div></div>' +
'<div class="login-tabs"><div class="login-tab active" data-tab="login" id="tabLogin">Kirish</div><div class="login-tab" data-tab="register" id="tabRegister">Ro\'yxatdan o\'tish</div></div>' +
'<div id="loginForm"><div class="form-group"><label class="form-label">Telefon yoki Email</label><input type="text" class="form-input" id="loginId" placeholder="+998 90 123 45 67"></div><div class="form-group"><label class="form-label">Parol</label><input type="password" class="form-input" id="loginPass" placeholder="Parolingiz"></div><button class="btn btn-primary btn-lg" style="width:100%" id="loginSubmitBtn">Kirish</button></div>' +
'<div id="registerForm" style="display:none;"><div class="form-group"><label class="form-label">Ismingiz</label><input type="text" class="form-input" id="regName" placeholder="To\'liq ism"></div><div class="form-group"><label class="form-label">Telefon</label><input type="tel" class="form-input" id="regPhone" placeholder="+998 90 123 45 67"></div><div class="form-group"><label class="form-label">Email</label><input type="email" class="form-input" id="regEmail" placeholder="email@example.com"></div><div class="form-group"><label class="form-label">Parol</label><input type="password" class="form-input" id="regPass" placeholder="Kamida 6 belgi"></div><button class="btn btn-primary btn-lg" style="width:100%" id="regSubmitBtn">Ro\'yxatdan o\'tish</button></div></div>';
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 });
});