File manager - Edit - /home/webapp69.cm.in.th/u69319090039/Shop39/public/js/main.js
Back
/** * main.js — Shop39 Global JavaScript * ครอบคลุม: AJAX cart (ทุกหน้า), toast, fly animation, cart badge update */ // ============================================================ // 0. Config — อ่านจาก meta tag ที่ header.php ฝังไว้ // ============================================================ const SHOP39 = window.SHOP39 || {}; // ============================================================ // 1. Utility: อัปเดต Cart Badge บน Navbar // ============================================================ function updateCartBadge(count) { const badge = document.getElementById('cartBadge'); if (!badge) return; badge.textContent = String(count); badge.style.transition = 'transform 0.2s'; badge.style.transform = 'scale(1.7)'; setTimeout(() => { badge.style.transform = 'scale(1)'; }, 250); } // ============================================================ // 2. Utility: Toast Notification (ไม่ต้องใช้ SweetAlert) // ============================================================ let _toastTimer = null; function showCartToast(opts) { let toast = document.getElementById('globalCartToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'globalCartToast'; toast.style.cssText = [ 'position:fixed', 'top:85px', 'right:24px', 'z-index:99999', 'min-width:280px', 'max-width:360px', 'background:#ffffff', 'border-radius:14px', 'box-shadow:0 10px 30px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.08)', 'border:1px solid #e2e8f0', 'overflow:hidden', 'transform:translateX(130%)', 'transition:transform 0.3s cubic-bezier(.4,0,.2,1)' ].join(';'); document.body.appendChild(toast); } const titleStr = opts.productTitle ? opts.productTitle : 'สินค้า'; const variantStr = (opts.variantInfo && opts.variantInfo !== 'มาตรฐาน') ? ` <span style="font-size:12px;color:#64748b;font-weight:normal;">(${opts.variantInfo})</span>` : ''; const imgHtml = opts.imgSrc ? `<img src="${opts.imgSrc}" style="width:42px;height:42px;object-fit:cover;border-radius:8px;border:1px solid #e2e8f0;flex-shrink:0;" onerror="this.style.display='none'">` : `<div style="width:42px;height:42px;border-radius:8px;background:#f1f5f9;flex-shrink:0;display:flex;align-items:center;justify-content:center;"><i class="bi bi-cart-check text-primary fs-5"></i></div>`; toast.innerHTML = ` <div style="padding:12px 16px;display:flex;align-items:center;gap:12px;"> ${imgHtml} <div style="flex:1;min-width:0;"> <div style="color:#0f172a;font-weight:600;font-size:13.5px;line-height:1.3;"> เพิ่ม <span style="color:#0d6efd;">${titleStr}</span>${variantStr} ลงตะกร้าแล้ว <span style="color:#10b981;font-weight:700;">✓</span> </div> <div style="font-size:11.5px;color:#64748b;margin-top:2px;">จำนวน: ${opts.qty || 1} ชิ้น</div> </div> <button onclick="closeCartToast()" style="background:none;border:none;color:#94a3b8;font-size:16px;line-height:1;cursor:pointer;padding:4px;flex-shrink:0;" aria-label="Close">×</button> </div> `; // Show toast requestAnimationFrame(() => { requestAnimationFrame(() => { toast.style.transform = 'translateX(0)'; }); }); clearTimeout(_toastTimer); _toastTimer = setTimeout(closeCartToast, 2200); } function showErrorToast(msg) { let toast = document.getElementById('globalCartToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'globalCartToast'; toast.style.cssText = [ 'position:fixed', 'top:85px', 'right:24px', 'z-index:99999', 'min-width:280px', 'max-width:360px', 'background:#fff', 'border-radius:14px', 'box-shadow:0 10px 30px rgba(0,0,0,0.12)', 'border:1px solid #fee2e2', 'overflow:hidden', 'transform:translateX(130%)', 'transition:transform 0.3s cubic-bezier(.4,0,.2,1)' ].join(';'); document.body.appendChild(toast); } toast.style.border = '1px solid #fca5a5'; toast.innerHTML = ` <div style="padding:12px 16px;display:flex;align-items:center;gap:10px;background:#fef2f2;"> <i class="bi bi-exclamation-circle-fill text-danger fs-4"></i> <div style="flex:1;font-size:13.5px;font-weight:600;color:#991b1b;">${msg}</div> <button onclick="closeCartToast()" style="background:none;border:none;color:#991b1b;font-size:16px;cursor:pointer;padding:4px;">×</button> </div> `; requestAnimationFrame(() => { requestAnimationFrame(() => { toast.style.transform = 'translateX(0)'; }); }); clearTimeout(_toastTimer); _toastTimer = setTimeout(closeCartToast, 3000); } function closeCartToast() { const toast = document.getElementById('globalCartToast'); if (toast) toast.style.transform = 'translateX(130%)'; } window.closeCartToast = closeCartToast; // ============================================================ // 3. Fly-to-Cart Animation (Realistic Floating/Bouncing Effect) // ============================================================ function flyToCart(triggerEl, imgSrc) { const cartBadge = document.getElementById('cartBadge'); const cartLink = document.querySelector('a[href*="/cart"]'); const targetEl = cartBadge || cartLink; if (!targetEl) return; // Check if triggerEl exists, or fallback to main product image or center screen let startEl = triggerEl; const mainImgEl = document.getElementById('mainProductImage'); if (mainImgEl && window.getComputedStyle(mainImgEl).display !== 'none') { startEl = mainImgEl; } else if (triggerEl && triggerEl.closest) { const cardImg = triggerEl.closest('.product-card, .card')?.querySelector('img'); if (cardImg) startEl = cardImg; } const startRect = (startEl && typeof startEl.getBoundingClientRect === 'function') ? startEl.getBoundingClientRect() : { top: window.innerHeight / 2, left: window.innerWidth / 2, width: 80, height: 80 }; const endRect = targetEl.getBoundingClientRect(); // Create or reuse fly element let fly = document.getElementById('globalFlyEl'); if (!fly) { fly = document.createElement('div'); fly.id = 'globalFlyEl'; fly.innerHTML = '<img id="globalFlyImg" style="width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;">'; document.body.appendChild(fly); } const flyImg = document.getElementById('globalFlyImg'); if (flyImg) { flyImg.src = imgSrc || (mainImgEl ? mainImgEl.src : ''); } const startX = startRect.left + (startRect.width / 2) - 35; const startY = startRect.top + (startRect.height / 2) - 35; const endX = endRect.left + (endRect.width / 2) - 14; const endY = endRect.top + (endRect.height / 2) - 14; // Initial state at product image center fly.style.cssText = ` position: fixed; z-index: 99998; width: 70px; height: 70px; border-radius: 50%; pointer-events: none; display: block; border: 3px solid #0d6efd; box-shadow: 0 8px 25px rgba(13,110,253,0.35); background: #ffffff; padding: 2px; top: ${startY}px; left: ${startX}px; transform: scale(1); opacity: 1; transition: none; `; // Trigger smooth flight animation toward navbar cart icon requestAnimationFrame(() => { requestAnimationFrame(() => { fly.style.transition = 'top 0.75s cubic-bezier(0.2, 0.8, 0.2, 1), left 0.75s cubic-bezier(0.4, 0, 0.2, 1), width 0.75s ease, height 0.75s ease, transform 0.75s ease, opacity 0.75s ease'; fly.style.top = endY + 'px'; fly.style.left = endX + 'px'; fly.style.width = '24px'; fly.style.height = '24px'; fly.style.transform = 'scale(0.5)'; fly.style.opacity = '0.3'; }); }); // When arrival complete -> trigger bounce on cart badge and cleanup setTimeout(() => { fly.style.display = 'none'; if (cartBadge) { cartBadge.style.transition = 'transform 0.18s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; cartBadge.style.transform = 'scale(1.8)'; setTimeout(() => { cartBadge.style.transform = 'scale(0.9)'; setTimeout(() => { cartBadge.style.transform = 'scale(1)'; }, 120); }, 180); } }, 750); } window.flyToCart = flyToCart; // ============================================================ // 4. Core AJAX Add-to-Cart (ใช้ทุกหน้า) // ============================================================ function ajaxAddToCart(opts, triggerBtn) { /** * opts: { productId, quantity, variantId, productTitle, productImg, baseUrl } * triggerBtn: DOM element ที่กด (สำหรับ fly animation) */ const baseUrl = opts.baseUrl || SHOP39.baseUrl || ''; const csrfToken = opts.csrfToken || SHOP39.csrfToken || ''; const fd = new FormData(); fd.append('csrf_token', csrfToken); fd.append('product_id', opts.productId); fd.append('quantity', opts.quantity || 1); fd.append('variant_id', opts.variantId || 0); return fetch(baseUrl + '/cart/add-ajax', { method: 'POST', body: fd }) .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); const contentType = r.headers.get('content-type') || ''; if (!contentType.includes('json')) { return r.text().then(t => { console.error('[Cart] Non-JSON response:', t); throw new Error('Server returned non-JSON response'); }); } return r.json(); }) .then(data => { if (data.success) { updateCartBadge(data.cart_count); flyToCart(triggerBtn, opts.productImg || ''); showCartToast({ title: 'เพิ่มลงตะกร้าแล้ว ✓', imgSrc: opts.productImg || '', productTitle: data.product_title || opts.productTitle || '', variantInfo: data.variant_info || '', qty: opts.quantity || 1 }); } else { console.warn('[Cart] Add failed:', data.message); showErrorToast(data.message || 'ไม่สามารถเพิ่มสินค้าลงตะกร้าได้'); } return data; }); } window.ajaxAddToCart = ajaxAddToCart; // ============================================================ // 5. Intercept .ajax-cart-form (หน้า home/products listing) // — ดักจับ form submit แล้วใช้ AJAX แทน // ============================================================ document.addEventListener('DOMContentLoaded', () => { // ── Helper: ตรวจสอบและแจ้งเตือนให้เข้าสู่ระบบ ───────────────── function checkAuthOrPrompt(actionName = 'สั่งซื้อสินค้า', redirectUrl = '') { if (!window.SHOP39 || !window.SHOP39.isLoggedIn) { const baseUrl = window.SHOP39?.baseUrl || ''; const targetUrl = redirectUrl || (window.location.pathname + window.location.search); const encodedRedirect = encodeURIComponent(targetUrl); if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'info', title: 'กรุณาเข้าสู่ระบบก่อน', text: `คุณต้องเข้าสู่ระบบหรือสมัครสมาชิกก่อนดำเนินการ ${actionName}`, showCancelButton: true, confirmButtonColor: '#0d6efd', cancelButtonColor: '#6c757d', confirmButtonText: '<i class="bi bi-box-arrow-in-right me-1"></i> เข้าสู่ระบบ', cancelButtonText: '<i class="bi bi-person-plus me-1"></i> สมัครสมาชิก', showDenyButton: true, denyButtonText: 'ยกเลิก', denyButtonColor: '#adb5bd', customClass: { popup: 'rounded-4 shadow-lg border-0' } }).then((result) => { if (result.isConfirmed) { window.location.href = baseUrl + '/login?redirect=' + encodedRedirect; } else if (result.isDismissed && result.dismiss === Swal.DismissReason.cancel) { window.location.href = baseUrl + '/register'; } }); } else { if (confirm(`กรุณาเข้าสู่ระบบก่อนดำเนินการ ${actionName}\n\nกด 'ตกลง' เพื่อเข้าสู่ระบบ หรือ 'ยกเลิก' เพื่อกลับ`)) { window.location.href = baseUrl + '/login?redirect=' + encodedRedirect; } } return false; } return true; } // ── 5a. ajax-cart-form submit intercept ────────────────── document.addEventListener('submit', function(e) { const form = e.target; if (!form.classList.contains('ajax-cart-form')) return; e.preventDefault(); const productId = form.querySelector('[name="product_id"]')?.value || 0; const qty = parseInt(form.querySelector('[name="quantity"]')?.value || 1); const variantId = form.querySelector('[name="variant_id"]')?.value || 0; const csrfToken = form.querySelector('[name="csrf_token"]')?.value || SHOP39.csrfToken || ''; const submitBtn = form.querySelector('button[type="submit"]'); const baseUrl = SHOP39.baseUrl || ''; // Try to find product image from the card let productImg = ''; let productTitle = ''; const card = form.closest('.product-card, .card'); if (card) { const img = card.querySelector('img'); if (img) productImg = img.src; const titleEl = card.querySelector('.card-title a, h6 a, h5 a'); if (titleEl) productTitle = titleEl.textContent.trim(); } if (submitBtn) { submitBtn.disabled = true; const originalHtml = submitBtn.innerHTML; submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; ajaxAddToCart({ productId, quantity: qty, variantId, productTitle, productImg, baseUrl, csrfToken }, submitBtn) .finally(() => { setTimeout(() => { submitBtn.disabled = false; submitBtn.innerHTML = originalHtml; }, 700); }); } else { ajaxAddToCart({ productId, quantity: qty, variantId, productTitle, productImg, baseUrl, csrfToken }, form); } }); // ── 5b. Quantity +/- on product detail ─────────────────── const qtyInput = document.getElementById('productQty'); const qtyMinus = document.getElementById('qtyMinus'); const qtyPlus = document.getElementById('qtyPlus'); if (qtyMinus && qtyInput) { qtyMinus.addEventListener('click', () => { const v = parseInt(qtyInput.value) || 1; if (v > 1) qtyInput.value = v - 1; }); } if (qtyPlus && qtyInput) { qtyPlus.addEventListener('click', () => { const v = parseInt(qtyInput.value) || 1; const max = parseInt(qtyInput.getAttribute('max')) || 9999; if (v < max) { qtyInput.value = v + 1; } else { showErrorToast('จำนวนสินค้าที่เลือกถึงขีดจำกัดสต็อกแล้ว'); } }); } // ── 5c. btnAddToCart (product detail page) ─────────────── const btnAdd = document.getElementById('btnAddToCart'); let _adding = false; if (btnAdd) { btnAdd.addEventListener('click', function() { // 1. ตรวจสอบการเข้าสู่ระบบ if (!checkAuthOrPrompt('เพิ่มสินค้าลงในตะกร้า')) { return; } const hasOptions = window.SHOP39_HAS_OPTIONS || false; const selectedVId = window.SHOP39_SELECTED_VARIANT || 0; if (hasOptions && !selectedVId) { const notice = document.getElementById('variantSelectionNotice'); if (notice) { notice.className = 'small text-danger mb-3 fw-semibold'; notice.innerHTML = '⚠️ กรุณาเลือกตัวเลือกสินค้าให้ครบถ้วนก่อน'; notice.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } _shakeEl(this); return; } const productId = parseInt(this.dataset.productId || 0); const qty = parseInt(document.getElementById('productQty')?.value || 1); const mainImgEl = document.getElementById('mainProductImage'); const productImg = (mainImgEl && mainImgEl.src) ? mainImgEl.src : (this.dataset.productImg || ''); const productTitle = this.dataset.productTitle || ''; const baseUrl = SHOP39.baseUrl || ''; const csrfToken = SHOP39.csrfToken || ''; if (productId <= 0) { showErrorToast('ไม่พบรหัสสินค้า'); return; } console.log('[SHOP39 Add to Cart Clicked]', { product_id: productId, variant_id: selectedVId, quantity: qty, csrf_token: csrfToken ? (csrfToken.substring(0, 10) + '...') : '' }); _adding = true; this.disabled = true; this.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>กำลังเพิ่ม...'; ajaxAddToCart({ productId, quantity: qty, variantId: selectedVId, productTitle, productImg, baseUrl, csrfToken }, this) .finally(() => { setTimeout(() => { _adding = false; btnAdd.disabled = false; btnAdd.innerHTML = '<i class="bi bi-cart-plus fs-5"></i><span>เพิ่มลงตะกร้า</span>'; }, 700); }); }); } // ── 5d. btnBuyNow (product detail page) ────────────────── const btnBuy = document.getElementById('btnBuyNow'); if (btnBuy) { btnBuy.addEventListener('click', function() { // 1. ตรวจสอบการเข้าสู่ระบบ if (!checkAuthOrPrompt('สั่งซื้อสินค้าทันที', '/checkout')) { return; } const hasOptions = window.SHOP39_HAS_OPTIONS || false; const selectedVId = window.SHOP39_SELECTED_VARIANT || 0; if (hasOptions && !selectedVId) { const notice = document.getElementById('variantSelectionNotice'); if (notice) { notice.className = 'small text-danger mb-3 fw-semibold'; notice.innerHTML = '⚠️ กรุณาเลือกตัวเลือกสินค้าก่อนสั่งซื้อ'; notice.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } _shakeEl(this); return; } const productId = parseInt(this.dataset.productId || 0); const qty = parseInt(document.getElementById('productQty')?.value || 1); const baseUrl = SHOP39.baseUrl || ''; const csrfToken = SHOP39.csrfToken || ''; if (productId <= 0) { showErrorToast('ไม่พบรหัสสินค้า'); return; } console.log('[SHOP39 Buy Now Clicked]', { product_id: productId, variant_id: selectedVId, quantity: qty, csrf_token: csrfToken ? (csrfToken.substring(0, 10) + '...') : '' }); btnBuy.disabled = true; btnBuy.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>กำลังดำเนินการ...'; const fd = new FormData(); fd.append('csrf_token', csrfToken); fd.append('product_id', productId); fd.append('quantity', qty); fd.append('variant_id', selectedVId || 0); fetch(baseUrl + '/cart/add-ajax', { method: 'POST', body: fd }) .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(data => { if (data.success) { updateCartBadge(data.cart_count); // If logged in, go directly to checkout; if not, prompt or redirect to login with redirect=/checkout if (!window.SHOP39 || !window.SHOP39.isLoggedIn) { checkAuthOrPrompt('ชำระเงินและสั่งซื้อสินค้า', '/checkout'); btnBuy.disabled = false; btnBuy.innerHTML = '<i class="bi bi-lightning-charge-fill fs-5"></i><span>สั่งซื้อทันที</span>'; } else { window.location.href = baseUrl + '/checkout'; } } else { showErrorToast(data.message || 'เกิดข้อผิดพลาด กรุณาลองใหม่'); btnBuy.disabled = false; btnBuy.innerHTML = '<i class="bi bi-lightning-charge-fill fs-5"></i><span>สั่งซื้อทันที</span>'; } }) .catch(err => { console.error('[BuyNow]', err); showErrorToast('เกิดข้อผิดพลาดในการเชื่อมต่อ กรุณาลองใหม่อีกครั้ง'); btnBuy.disabled = false; btnBuy.innerHTML = '<i class="bi bi-lightning-charge-fill fs-5"></i><span>สั่งซื้อทันที</span>'; }); }); } // ── 5e. Store Map (existing functionality) ──────────────── document.addEventListener('click', function(e) { if (e.target && e.target.id === 'storeMapBtn') { const address = e.target.getAttribute('data-address'); const name = e.target.getAttribute('data-name'); const savedLat = e.target.getAttribute('data-lat'); const savedLng = e.target.getAttribute('data-lng'); const mapContainer = document.getElementById('inlineStoreMap'); if (!mapContainer) return; if (mapContainer.classList.contains('map-expanded')) { mapContainer.classList.remove('map-expanded'); mapContainer.classList.add('map-collapsed'); e.target.textContent = 'ดูแผนที่'; return; } mapContainer.classList.remove('map-collapsed'); mapContainer.classList.add('map-expanded'); e.target.textContent = 'ซ่อนแผนที่'; setTimeout(() => { if (savedLat && savedLng && savedLat !== '' && savedLng !== '') { initOrUpdateStoreMap(parseFloat(savedLat), parseFloat(savedLng), name, address); } else if (window.storeMapLatLng) { initOrUpdateStoreMap(window.storeMapLatLng.lat, window.storeMapLatLng.lon, name, address); } else { geocodeAddress(address).then(coord => { const c = coord || { lat: 13.7563, lon: 100.5018 }; window.storeMapLatLng = c; initOrUpdateStoreMap(c.lat, c.lon, name, address); }); } }, 200); } }); }); // ============================================================ // 6. Shake helper // ============================================================ function _shakeEl(el) { if (!el) return; el.style.animation = 'none'; el.offsetHeight; el.style.animation = '_shop39ShakeBtn 0.4s ease'; setTimeout(() => { el.style.animation = ''; }, 500); } // Inject shake keyframes once (function() { if (document.getElementById('_shop39ShakeStyle')) return; const s = document.createElement('style'); s.id = '_shop39ShakeStyle'; s.textContent = '@keyframes _shop39ShakeBtn{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}'; document.head.appendChild(s); })(); // ============================================================ // 7. Geocoding / Map helpers (ของเดิม) // ============================================================ window.storeMapInstance = null; window.storeMapLatLng = null; function geocodeAddress(address) { return fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(address)) .then(res => res.json()) .then(data => { if (data && data.length > 0) return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) }; return null; }) .catch(() => null); } function initOrUpdateStoreMap(lat, lon, name, address) { if (typeof L === 'undefined') return; const customIcon = L.divIcon({ className: 'custom-marker', html: `<div class="pin"><i class="bi bi-shop"></i></div>`, iconSize: [30, 42], iconAnchor: [15, 42] }); if (!window.storeMapInstance) { const map = L.map('inlineStoreMap', { zoomControl: false }).setView([lat, lon], 15); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© <a href="https://www.openstreetmap.org/">OpenStreetMap</a>', maxZoom: 19 }).addTo(map); L.marker([lat, lon], { icon: customIcon }).addTo(map) .bindPopup(`<strong>${name}</strong><br>${address}`, { className: 'leaflet-popup-custom' }).openPopup(); L.control.zoom({ position: 'topright' }).addTo(map); window.storeMapInstance = map; } else { window.storeMapInstance.setView([lat, lon], 15); window.storeMapInstance.eachLayer(l => { if (l instanceof L.Marker) window.storeMapInstance.removeLayer(l); }); L.marker([lat, lon], { icon: customIcon }).addTo(window.storeMapInstance) .bindPopup(`<strong>${name}</strong><br>${address}`, { className: 'leaflet-popup-custom' }).openPopup(); } setTimeout(() => window.storeMapInstance.invalidateSize(), 0); }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.23 |
proxy
|
phpinfo
|
Settings