/** * اسکریپت اصلی تم مطیب * * @package Motayeb * @version 1.0.1 * * تغییرات نسخه ۱.۰.۱: * - persist شدن سبد خرید و علاقه‌مندی‌ها در localStorage * - persist شدن دارک مود در کوکی (هماهنگ با PHP body_class) * - جلوگیری از XSS با escapeHtml روی همه ورودی‌های داینامیک * - guard کردن motayeb_ajax * - بهبود مدیریت خطای fetch * - رفع toggleWishlist و setRating * - استفاده از کلاس‌های Tailwind در renderStars */ // ============================================ // Guard متغیرهای سراسری // ============================================ const MOTAYEB_AJAX = (typeof motayeb_ajax !== 'undefined') ? motayeb_ajax : { ajax_url: '/wp-admin/admin-ajax.php', nonce: '', home_url: '/', checkout_url: '/checkout/', cart_url: '/cart/' }; // ============================================ // مدیریت State با localStorage // ============================================ let cart = loadFromStorage('motayeb_cart', []); let wishlist = loadFromStorage('motayeb_wishlist', []); let selectedWeightPrice = 295000; function loadFromStorage(key, fallback) { try { const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : fallback; } catch (e) { return fallback; } } function saveToStorage(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { // localStorage ممکنه private mode باشه — نادیده می‌گیریم } } // ============================================ // ابزارهای کمکی // ============================================ /** * Escape کردن HTML برای جلوگیری از XSS */ function escapeHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * فرمت قیمت — اعداد فارسی + نمایش رایگان برای قیمت صفر */ function formatPrice(n) { n = Number(n) || 0; if (n === 0) return 'رایگان'; return n.toLocaleString('fa-IR'); } /** * درخواست Ajax با مدیریت خطای کامل */ async function motayebFetch(action, params = {}) { const url = new URL(MOTAYEB_AJAX.ajax_url, window.location.origin); url.searchParams.set('action', action); url.searchParams.set('nonce', MOTAYEB_AJAX.nonce); Object.keys(params).forEach(k => url.searchParams.set(k, params[k])); const response = await fetch(url.toString()); if (!response.ok) { throw new Error('HTTP ' + response.status); } const data = await response.json(); return data; } // ============================================ // توابع سبد خرید // ============================================ async function quickAddToCart(id) { try { const data = await motayebFetch('get_product_data', { id: id }); if (data.success) { const p = data.data; const existing = cart.find(x => x.id === p.id); if (existing) { existing.qty++; } else { cart.push({ ...p, qty: 1 }); } saveToStorage('motayeb_cart', cart); updateCartUI(); showToast(escapeHtml(p.name) + ' به سبد خرید اضافه شد', 'success'); } else { showToast('محصول یافت نشد', 'error'); } } catch (err) { console.error('quickAddToCart:', err); showToast('خطا در ارتباط با سرور', 'error'); } } async function addToCartDetail(productId) { const qtyEl = document.getElementById('product-qty'); const qty = parseInt(qtyEl ? qtyEl.textContent : '1') || 1; try { const data = await motayebFetch('get_product_data', { id: productId }); if (data.success) { const p = data.data; const existing = cart.find(x => x.id === p.id); if (existing) { existing.qty += qty; } else { cart.push({ ...p, qty: qty }); } saveToStorage('motayeb_cart', cart); updateCartUI(); showToast(escapeHtml(p.name) + ' به سبد خرید اضافه شد', 'success'); } else { showToast('محصول یافت نشد', 'error'); } } catch (err) { console.error('addToCartDetail:', err); showToast('خطا در ارتباط با سرور', 'error'); } } function removeFromCart(id) { cart = cart.filter(x => x.id !== id); saveToStorage('motayeb_cart', cart); updateCartUI(); } function updateCartQty(id, delta) { const item = cart.find(x => x.id === id); if (!item) return; item.qty += delta; if (item.qty <= 0) { removeFromCart(id); return; } saveToStorage('motayeb_cart', cart); updateCartUI(); } function updateCartUI() { const count = cart.reduce((s, i) => s + i.qty, 0); const total = cart.reduce((s, i) => s + (Number(i.price) || 0) * i.qty, 0); // Badge روی آیکون سبد ['cart-count', 'mobile-cart-count', 'cart-drawer-count'].forEach(id => { const el = document.getElementById(id); if (!el) return; if (id === 'cart-drawer-count') { el.textContent = count; } else { if (count > 0) { el.textContent = count; el.classList.remove('hidden'); } else { el.classList.add('hidden'); } } }); // محتویات drawer const listEl = document.getElementById('cart-items-list'); const emptyEl = document.getElementById('cart-empty'); const footerEl = document.getElementById('cart-drawer-footer'); if (!listEl) return; if (cart.length === 0) { listEl.classList.add('hidden'); if (emptyEl) emptyEl.classList.remove('hidden'); if (footerEl) footerEl.classList.add('hidden'); } else { listEl.classList.remove('hidden'); if (emptyEl) emptyEl.classList.add('hidden'); if (footerEl) footerEl.classList.remove('hidden'); const totalEl = document.getElementById('cart-drawer-total'); if (totalEl) totalEl.textContent = formatPrice(total) + ' تومان'; listEl.innerHTML = cart.map(i => `
${escapeHtml(i.name)}

${escapeHtml(i.name)}

${formatPrice(i.price)} تومان
${i.qty}
`).join(''); } // صفحه checkout const checkoutItems = document.getElementById('checkout-items'); if (checkoutItems) { if (cart.length === 0) { checkoutItems.innerHTML = '

سبد خرید خالی است

'; } else { checkoutItems.innerHTML = cart.map(i => `
${escapeHtml(i.name)}
${escapeHtml(i.name)}
${i.qty} عدد
${formatPrice(Number(i.price) * i.qty)}
`).join(''); } const subtotalEl = document.getElementById('checkout-subtotal'); const totalEl = document.getElementById('checkout-total'); if (subtotalEl) subtotalEl.textContent = formatPrice(total) + ' تومان'; if (totalEl) totalEl.textContent = formatPrice(total) + ' تومان'; } } // ============================================ // توابع UI — جستجو، سبد، منو // ============================================ function openSearch() { const overlay = document.getElementById('search-overlay'); if (!overlay) return; overlay.classList.remove('hidden'); setTimeout(() => { const input = document.getElementById('search-input'); if (input) input.focus(); }, 100); } function closeSearch() { const overlay = document.getElementById('search-overlay'); if (overlay) overlay.classList.add('hidden'); const input = document.getElementById('search-input'); if (input) input.value = ''; const results = document.getElementById('search-results'); if (results) results.innerHTML = ''; const suggestions = document.getElementById('search-suggestions'); if (suggestions) suggestions.classList.remove('hidden'); } function openCart() { const drawer = document.getElementById('cart-drawer'); if (drawer) drawer.classList.remove('hidden'); } function closeCart() { const drawer = document.getElementById('cart-drawer'); if (drawer) drawer.classList.add('hidden'); } function openMobileMenu() { const menu = document.getElementById('mobile-menu'); if (menu) menu.classList.remove('hidden'); } function closeMobileMenu() { const menu = document.getElementById('mobile-menu'); if (menu) menu.classList.add('hidden'); } function toggleLang() { showToast('نسخه انگلیسی به زودی اضافه می‌شود', 'info'); } function goToCheckout() { closeCart(); if (MOTAYEB_AJAX.checkout_url) { window.location.href = MOTAYEB_AJAX.checkout_url; } else { window.location.href = '/checkout/'; } } // ============================================ // دارک مود — هماهنگ با PHP body_class // ============================================ /** * تغییر حالت تاریک. * - کلاس dark روی (برای Tailwind dark:) * - کلاس dark-mode روی (برای CSS variables قدیمی) * - ذخیره در کوکی (برای خواندن سمت سرور در functions.php) */ function toggleDark() { const html = document.documentElement; const body = document.body; const isDark = html.classList.toggle('dark'); body.classList.toggle('dark-mode', isDark); // ذخیره در کوکی — ۱ سال اعتبار const value = isDark ? 'on' : 'off'; document.cookie = 'motayeb_darkmode=' + value + ';path=/;max-age=31536000;SameSite=Lax'; } // ============================================ // جستجوی زنده // ============================================ async function handleSearch(val) { const results = document.getElementById('search-results'); const suggestions = document.getElementById('search-suggestions'); if (!results) return; if (val.length < 2) { results.innerHTML = ''; if (suggestions) suggestions.classList.remove('hidden'); return; } if (suggestions) suggestions.classList.add('hidden'); try { const data = await motayebFetch('search_products', { term: val }); if (data.success && data.data.length > 0) { results.innerHTML = data.data.map(p => { const imgSrc = p.image || p.img || ('https://picsum.photos/seed/' + p.id + '/60/60.jpg'); const permalink = p.permalink || '#'; return ` ${escapeHtml(p.name)}
${escapeHtml(p.name)}
${escapeHtml(p.category || 'محصول')}
${formatPrice(p.price)}
`; }).join(''); } else { results.innerHTML = '

محصولی یافت نشد

'; } } catch (err) { console.error('handleSearch:', err); results.innerHTML = '

خطا در جستجو. لطفاً دوباره تلاش کنید.

'; } } // ============================================ // گالری محصول — زوم و تغییر تصویر // ============================================ function changeImage(src, btn) { const mainImg = document.getElementById('main-product-img'); if (!mainImg) return; mainImg.src = src; mainImg.setAttribute('data-large_image', src); mainImg.setAttribute('data-src', src); const parentLink = mainImg.closest('a'); if (parentLink) parentLink.href = src; document.querySelectorAll('.gallery-thumb').forEach(t => t.classList.remove('active')); if (btn) btn.classList.add('active'); // بازسازی زوم ووکامرس (اگه jQuery لود شده باشه) if (typeof jQuery !== 'undefined' && typeof jQuery.fn.zoom !== 'undefined') { const $mainImg = jQuery(mainImg); const $wrapper = $mainImg.parent(); $wrapper.trigger('zoom.destroy'); $wrapper.zoom({ url: src, touch: false }); } } // ============================================ // صفحه محصول — انتخاب وزن و تعداد // ============================================ function selectWeight(btn, price) { document.querySelectorAll('.weight-btn').forEach(b => { b.classList.remove('active', 'border-forest-500', 'dark:border-forest-300', 'bg-forest-50', 'dark:bg-forest-900/30', 'text-forest-500', 'dark:text-forest-300'); b.classList.add('border-cream-200', 'dark:border-dark-border'); }); btn.classList.add('active', 'border-forest-500', 'dark:border-forest-300', 'bg-forest-50', 'dark:bg-forest-900/30', 'text-forest-500', 'dark:text-forest-300'); btn.classList.remove('border-cream-200', 'dark:border-dark-border'); selectedWeightPrice = price; const priceEl = document.getElementById('product-price'); if (priceEl) priceEl.textContent = formatPrice(price); } function changeQty(delta) { const el = document.getElementById('product-qty'); if (!el) return; let val = parseInt(el.textContent) + delta; if (val < 1) val = 1; if (val > 10) val = 10; el.textContent = val; } function switchTab(tabId, btn) { document.querySelectorAll('.tab-content').forEach(t => t.classList.add('hidden')); document.querySelectorAll('.tab-btn').forEach(b => { b.classList.remove('tab-active'); b.classList.add('text-cream-500', 'dark:text-dark-muted'); }); const target = document.getElementById('tab-' + tabId); if (target) target.classList.remove('hidden'); if (btn) { btn.classList.add('tab-active'); btn.classList.remove('text-cream-500', 'dark:text-dark-muted'); } } // ============================================ // علاقه‌مندی‌ها — با persist در localStorage // ============================================ function toggleWishlist(btn, productId) { if (!btn) return; const icon = btn.querySelector('iconify-icon'); if (!icon) return; productId = productId || parseInt(btn.getAttribute('data-product-id')) || 0; const index = wishlist.indexOf(productId); if (index === -1) { // افزودن به علاقه‌مندی‌ها wishlist.push(productId); icon.setAttribute('icon', 'lucide:heart'); icon.style.color = '#ef4444'; icon.style.fill = '#ef4444'; showToast('به علاقه‌مندی‌ها اضافه شد', 'success'); } else { // حذف از علاقه‌مندی‌ها wishlist.splice(index, 1); icon.setAttribute('icon', 'lucide:heart'); icon.style.color = ''; icon.style.fill = ''; showToast('از علاقه‌مندی‌ها حذف شد', 'info'); } saveToStorage('motayeb_wishlist', wishlist); } // ============================================ // Toast نوتیفیکیشن // ============================================ function showToast(msg, type) { type = type || 'info'; const container = document.getElementById('toast-container'); if (!container) return; const colors = { success: 'bg-forest-500', info: 'bg-earth-400', error: 'bg-red-500' }; const icons = { success: 'lucide:check-circle', info: 'lucide:info', error: 'lucide:alert-circle' }; const toast = document.createElement('div'); toast.className = 'toast flex items-center gap-3 px-5 py-3 ' + (colors[type] || 'bg-earth-400') + ' text-white rounded-2xl shadow-xl text-sm font-medium'; toast.innerHTML = '' + escapeHtml(msg) + ''; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, 3000); } // ============================================ // فرم‌ها // ============================================ function handleNewsletter(e) { e.preventDefault(); const email = document.getElementById('newsletter-email'); if (email && email.value) { showToast(email.value + ' با موفقیت ثبت شد!', 'success'); email.value = ''; } return false; } function handleContact(e) { e.preventDefault(); showToast('پیام شما با موفقیت ارسال شد. به زودی پاسخ می‌دهیم.', 'success'); e.target.reset(); return false; } // ============================================ // سیستم امتیازدهی ستاره‌ای // ============================================ let currentRating = 0; /** * تعیین امتیاز با کلیک روی ستاره */ function setRating(rating) { currentRating = rating; renderStars(rating); // تنظیم فیلد مخفی برای ارسال با فرم const hiddenInput = document.getElementById('rating-input'); if (hiddenInput) hiddenInput.value = rating; } /** * نمایش ستاره‌ها با امتیاز داده‌شده * @param {number} rating - امتیاز از ۱ تا ۵ * @param {string} containerId - شناسه ظرف (پیش‌فرض: star-rating) */ function renderStars(rating, containerId) { containerId = containerId || 'star-rating'; const container = document.getElementById(containerId); if (!container) return; container.innerHTML = ''; for (let i = 1; i <= 5; i++) { const star = document.createElement('i'); if (i <= rating) { // ستاره پر — طلایی (کلاس‌های Tailwind/FontAwesome) star.className = 'fa-solid fa-star text-honey-400 cursor-pointer transition'; } else { // ستاره خالی — خاکستری star.className = 'fa-regular fa-star text-cream-300 dark:text-dark-muted cursor-pointer transition'; } star.dataset.rating = i; star.setAttribute('role', 'button'); star.setAttribute('aria-label', 'امتیاز ' + i + ' از ۵'); star.addEventListener('click', function() { setRating(parseInt(this.dataset.rating)); }); star.addEventListener('mouseenter', function() { renderStars(parseInt(this.dataset.rating), containerId); }); container.appendChild(star); } // وقتی موس از ظرف خارج شد، به امتیاز فعلی برگرد container.onmouseleave = function() { renderStars(currentRating, containerId); }; } // ============================================ // رویدادهای صفحه کلید // ============================================ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeSearch(); closeCart(); closeMobileMenu(); } // Ctrl/Cmd + K برای باز کردن جستجو if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); } }); // ============================================ // تغییر هدر هنگام اسکرول // ============================================ window.addEventListener('scroll', function() { const header = document.getElementById('header'); if (!header) return; if (window.scrollY > 100) { header.classList.add('shadow-lg'); } else { header.classList.remove('shadow-lg'); } }, { passive: true }); // ============================================ // مقداردهی اولیه — همه در یک listener // ============================================ document.addEventListener('DOMContentLoaded', function() { // ۱. به‌روزرسانی UI سبد خرید از localStorage updateCartUI(); // ۲. تنظیم گالری محصول (اگه در صفحه محصول هستیم) const mainImg = document.getElementById('main-product-img'); if (mainImg) { const firstThumb = document.querySelector('.gallery-thumb'); if (firstThumb) firstThumb.classList.add('active'); } // ۳. رندر ستاره‌های امتیازدهی (اگه ظرف وجود داشته باشه) const starContainer = document.getElementById('star-rating'); if (starContainer) { // خواندن امتیاز فعلی از فیلد مخفی (اگه ویرایش نظر باشه) const hiddenInput = document.getElementById('rating-input'); currentRating = hiddenInput ? parseInt(hiddenInput.value) || 0 : 0; renderStars(currentRating); } // ۴. علامت‌گذاری آیکون‌های علاقه‌مندی که قبلاً انتخاب شده‌ان document.querySelectorAll('[data-wishlist-btn]').forEach(btn => { const productId = parseInt(btn.getAttribute('data-product-id')) || 0; if (wishlist.indexOf(productId) !== -1) { const icon = btn.querySelector('iconify-icon'); if (icon) { icon.style.color = '#ef4444'; icon.style.fill = '#ef4444'; } } }); }); // AJAX Add to Cart برای صفحه محصول document.addEventListener('DOMContentLoaded', function() { var form = document.querySelector('form.cart'); if (!form) return; form.addEventListener('submit', function(e) { e.preventDefault(); var formData = new FormData(form); var productId = formData.get('add-to-cart'); var quantity = formData.get('quantity') || 1; fetch(MOTAYEB_AJAX.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'action=woocommerce_add_to_cart&product_id=' + productId + '&quantity=' + quantity }) .then(r => r.json()) .then(data => { if (data.fragments) { // به‌روزرسانی fragment های ووکامرس (مثل شمارنده سبد) Object.keys(data.fragments).forEach(function(key) { var el = document.querySelector(key); if (el) el.innerHTML = data.fragments[key]; }); showToast('به سبد خرید اضافه شد', 'success'); } }) .catch(err => showToast('خطا در افزودن', 'error')); }); });