File manager - Edit - /home/webapp69.cm.in.th/u69319090006/shop/catalog.js
Back
function getCatName(name) { return name; } // ========================================================================== // 5. PRODUCT CATALOG & MARKETPLACE HOME // ========================================================================== function initFilters() { const filterCatDropdown = document.getElementById("filterCategory"); const adminStockCatDropdown = document.getElementById("adminStockFilterCategory"); const sellerStockCatDropdown = document.getElementById("sellerStockFilterCategory"); const newProdCatSelect = document.getElementById("newProdCategory"); const editProdCatSelect = document.getElementById("editProdCategory"); if (filterCatDropdown) { filterCatDropdown.innerHTML = `<option value="all">หมวดหมู่ทั้งหมด</option>`; DEFAULT_CATEGORIES.forEach(c => { filterCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (adminStockCatDropdown) { adminStockCatDropdown.innerHTML = `<option value="all">ทุกหมวดหมู่</option>`; DEFAULT_CATEGORIES.forEach(c => { adminStockCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (sellerStockCatDropdown) { sellerStockCatDropdown.innerHTML = `<option value="all">ทุกหมวดหมู่</option>`; DEFAULT_CATEGORIES.forEach(c => { sellerStockCatDropdown.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (newProdCatSelect) { newProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { newProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } if (editProdCatSelect) { editProdCatSelect.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { editProdCatSelect.innerHTML += `<option value="${c.name}">${c.name}</option>`; }); } // Accordion Collapsible Filter Groups Listener (Default closed, click to open/close) document.querySelectorAll(".filter-group-header").forEach(header => { header.addEventListener("click", () => { const group = header.closest(".filter-group"); if (group) { group.classList.toggle("open"); } }); }); // Rating Search Mode Toggle Listener (แบบขั้นต่ำ vs เจาะจง) document.querySelectorAll("input[name='ratingMode']").forEach(radio => { radio.addEventListener("change", (e) => { state.activeFilters.ratingMode = e.target.value; state.activeFilters.ratingMin = 0; state.activeFilters.ratings = []; document.querySelectorAll(".rating-mode-btn").forEach(btn => { const input = btn.querySelector("input"); btn.classList.toggle("active", !!(input && input.checked)); }); updateRatingChipsUI(); renderCatalog(); }); }); // Rating Chips Click Listener document.querySelectorAll(".rating-chip").forEach(chip => { chip.addEventListener("click", (e) => { e.preventDefault(); const val = parseInt(chip.getAttribute("data-rating")); const mode = state.activeFilters.ratingMode; if (mode === "min") { state.activeFilters.ratingMin = val; } else { if (val === 0) { state.activeFilters.ratings = []; } else { const idx = state.activeFilters.ratings.indexOf(val); if (idx > -1) { state.activeFilters.ratings.splice(idx, 1); } else { state.activeFilters.ratings.push(val); } } } updateRatingChipsUI(); renderCatalog(); }); }); // Populate Seller Filter Dropdown const filterSellerDropdown = document.getElementById("filterSeller"); if (filterSellerDropdown) { filterSellerDropdown.innerHTML = `<option value="all">ร้านค้าทั้งหมด</option>`; const users = db.getUsers(); const sellers = users.filter(u => u.role === "seller"); sellers.forEach(s => { filterSellerDropdown.innerHTML += `<option value="${s.username}">${s.name || s.username}</option>`; }); filterSellerDropdown.addEventListener("change", (e) => { state.activeFilters.sellerId = e.target.value; renderCatalog(); }); } // Populate Marketplace horizontal icon navigator const marketCatNav = document.getElementById("marketCategoryNav"); if (marketCatNav) { marketCatNav.innerHTML = ""; DEFAULT_CATEGORIES.forEach(c => { const div = document.createElement("div"); div.className = "market-category-item"; div.setAttribute("data-cat", c.name); div.innerHTML = ` <div class="market-category-icon"><i class="fa-solid ${c.icon}"></i></div> <span class="market-category-label">${c.name}</span> `; div.addEventListener("click", () => { const isActive = div.classList.contains("active"); document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); if (isActive) { state.activeFilters.category = "all"; if (filterCatDropdown) filterCatDropdown.value = "all"; } else { div.classList.add("active"); state.activeFilters.category = c.name; if (filterCatDropdown) filterCatDropdown.value = c.name; } renderCatalog(); }); marketCatNav.appendChild(div); }); } // Event hooks if (filterCatDropdown) { filterCatDropdown.addEventListener("change", (e) => { state.activeFilters.category = e.target.value; // Match horizontal icon selected state document.querySelectorAll(".market-category-item").forEach(item => { if (item.getAttribute("data-cat") === e.target.value) { item.classList.add("active"); } else { item.classList.remove("active"); } }); renderCatalog(); }); } // Accordion Collapsible Headers Toggle Handler document.querySelectorAll(".filter-group-header").forEach(header => { header.addEventListener("click", () => { const group = header.closest(".filter-group"); if (group) group.classList.toggle("collapsed"); }); }); // Global search inputs const globalSearchInput = document.getElementById("globalSearchBar"); const globalSearchBtn = document.getElementById("globalSearchBtn"); if (globalSearchInput && globalSearchBtn) { const handleGlobalSearch = () => { state.activeFilters.search = globalSearchInput.value.toLowerCase().trim(); renderCatalog(); }; globalSearchBtn.addEventListener("click", handleGlobalSearch); globalSearchInput.addEventListener("keypress", (e) => { if (e.key === "Enter") handleGlobalSearch(); }); } const filterMin = document.getElementById("filterPriceMin"); if (filterMin) { filterMin.addEventListener("input", (e) => { state.activeFilters.priceMin = e.target.value ? parseFloat(e.target.value) : null; updateQuickPriceChips(); renderCatalog(); }); } const filterMax = document.getElementById("filterPriceMax"); if (filterMax) { filterMax.addEventListener("input", (e) => { state.activeFilters.priceMax = e.target.value ? parseFloat(e.target.value) : null; updateQuickPriceChips(); renderCatalog(); }); } // Quick Price Presets Chips document.querySelectorAll(".quick-price-chip").forEach(btn => { btn.addEventListener("click", () => { const min = btn.getAttribute("data-min") !== "" ? parseFloat(btn.getAttribute("data-min")) : null; const max = btn.getAttribute("data-max") !== "" ? parseFloat(btn.getAttribute("data-max")) : null; if (state.activeFilters.priceMin === min && state.activeFilters.priceMax === max && (min !== null || max !== null)) { state.activeFilters.priceMin = null; state.activeFilters.priceMax = null; if (filterMin) filterMin.value = ""; if (filterMax) filterMax.value = ""; } else { state.activeFilters.priceMin = min; state.activeFilters.priceMax = max; if (filterMin) filterMin.value = min !== null ? min : ""; if (filterMax) filterMax.value = max !== null ? max : ""; } updateQuickPriceChips(); renderCatalog(); }); }); // Promotions Checkboxes (Free Shipping / Has Coupon) const filterFreeShipping = document.getElementById("filterFreeShipping"); if (filterFreeShipping) { filterFreeShipping.addEventListener("change", (e) => { state.activeFilters.freeShipping = e.target.checked; const chip = document.getElementById("chipFreeShipping"); if (chip) chip.classList.toggle("active", e.target.checked); renderCatalog(); }); } const filterHasCoupon = document.getElementById("filterHasCoupon"); if (filterHasCoupon) { filterHasCoupon.addEventListener("change", (e) => { state.activeFilters.hasCoupon = e.target.checked; const chip = document.getElementById("chipHasCoupon"); if (chip) chip.classList.toggle("active", e.target.checked); renderCatalog(); }); } const resetAllFilters = () => { if (globalSearchInput) globalSearchInput.value = ""; if (filterCatDropdown) filterCatDropdown.value = "all"; if (filterSellerDropdown) filterSellerDropdown.value = "all"; if (filterMin) filterMin.value = ""; if (filterMax) filterMax.value = ""; if (filterFreeShipping) filterFreeShipping.checked = false; const chipShip = document.getElementById("chipFreeShipping"); if (chipShip) chipShip.classList.remove("active"); if (filterHasCoupon) filterHasCoupon.checked = false; const chipCoup = document.getElementById("chipHasCoupon"); if (chipCoup) chipCoup.classList.remove("active"); const modeMinRadio = document.querySelector("input[name='ratingMode'][value='min']"); if (modeMinRadio) modeMinRadio.checked = true; document.querySelectorAll(".rating-mode-btn").forEach(btn => { const input = btn.querySelector("input"); btn.classList.toggle("active", !!(input && input.checked)); }); document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); updateQuickPriceChips(); state.activeFilters = { search: "", category: "all", sellerId: "all", priceMin: null, priceMax: null, ratingMode: "min", ratingMin: 0, ratings: [], freeShipping: false, hasCoupon: false, colors: [], sizes: [] }; updateRatingChipsUI(); renderCatalog(); }; const clearFiltersBtn = document.getElementById("clearFiltersBtn"); if (clearFiltersBtn) { clearFiltersBtn.addEventListener("click", resetAllFilters); } const clearAllTagsBtn = document.getElementById("clearAllTagsBtn"); if (clearAllTagsBtn) { clearAllTagsBtn.addEventListener("click", resetAllFilters); } const sortBySelect = document.getElementById("sortBy"); if (sortBySelect) { sortBySelect.addEventListener("change", (e) => { state.sortBy = e.target.value; renderCatalog(); }); } } function updateRatingChipsUI() { const mode = state.activeFilters.ratingMode; document.querySelectorAll(".rating-chip").forEach(chip => { const val = parseInt(chip.getAttribute("data-rating")); if (mode === "min") { chip.classList.toggle("active", val === state.activeFilters.ratingMin); } else { if (state.activeFilters.ratings.length === 0) { chip.classList.toggle("active", val === 0); } else { chip.classList.toggle("active", val !== 0 && state.activeFilters.ratings.includes(val)); } } }); } function updateQuickPriceChips() { const currentMin = state.activeFilters.priceMin; const currentMax = state.activeFilters.priceMax; document.querySelectorAll(".quick-price-chip").forEach(btn => { const min = btn.getAttribute("data-min") !== "" ? parseFloat(btn.getAttribute("data-min")) : null; const max = btn.getAttribute("data-max") !== "" ? parseFloat(btn.getAttribute("data-max")) : null; if (currentMin === min && currentMax === max && (min !== null || max !== null)) { btn.classList.add("active"); } else { btn.classList.remove("active"); } }); } function renderActiveFilterTags() { const wrapper = document.getElementById("activeFilterTagsWrapper"); const container = document.getElementById("activeFilterTagsContainer"); if (!wrapper || !container) return; let tagsHtml = ""; const f = state.activeFilters; if (f.search) { tagsHtml += `<span class="active-tag" data-filter="search">ค้นหา: "${f.search}" <i class="fa-solid fa-xmark remove-tag-btn" data-filter="search"></i></span>`; } if (f.category !== "all") { tagsHtml += `<span class="active-tag" data-filter="category">หมวดหมู่หลัก: ${f.category} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="category"></i></span>`; } if (f.sellerId !== "all") { const sellerObj = db.getUsers().find(u => u.username === f.sellerId); const sellerName = sellerObj ? sellerObj.name : f.sellerId; tagsHtml += `<span class="active-tag" data-filter="sellerId">ร้าน: ${sellerName} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="sellerId"></i></span>`; } const minP = (f.priceMin !== undefined && f.priceMin !== null) ? f.priceMin : ((f.minPrice !== undefined && f.minPrice !== null) ? f.minPrice : null); const maxP = (f.priceMax !== undefined && f.priceMax !== null) ? f.priceMax : ((f.maxPrice !== undefined && f.maxPrice !== null) ? f.maxPrice : null); if (minP !== null || maxP !== null) { let priceTxt = ""; if (minP !== null && maxP !== null) priceTxt = `฿${minP.toLocaleString()} - ฿${maxP.toLocaleString()}`; else if (minP !== null) priceTxt = `>= ฿${minP.toLocaleString()}`; else if (maxP !== null) priceTxt = `<= ฿${maxP.toLocaleString()}`; tagsHtml += `<span class="active-tag" data-filter="price">ราคา: ${priceTxt} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="price"></i></span>`; } if (f.ratingMode === "min" && f.ratingMin > 0) { tagsHtml += `<span class="active-tag" data-filter="rating">⭐ ${f.ratingMin} ดาวขึ้นไป <i class="fa-solid fa-xmark remove-tag-btn" data-filter="rating"></i></span>`; } else if (f.ratingMode === "exact" && Array.isArray(f.ratings) && f.ratings.length > 0) { const sorted = f.ratings.slice().sort((a, b) => b - a).map(r => r + '★').join(', '); tagsHtml += `<span class="active-tag" data-filter="rating">⭐ เจาะจง: ${sorted} <i class="fa-solid fa-xmark remove-tag-btn" data-filter="rating"></i></span>`; } if (f.freeShipping) { tagsHtml += `<span class="active-tag" data-filter="freeShipping">🚚 ส่งฟรี <i class="fa-solid fa-xmark remove-tag-btn" data-filter="freeShipping"></i></span>`; } if (f.hasCoupon) { tagsHtml += `<span class="active-tag" data-filter="hasCoupon">🎟️ มีคูปอง <i class="fa-solid fa-xmark remove-tag-btn" data-filter="hasCoupon"></i></span>`; } if (tagsHtml) { container.innerHTML = tagsHtml; wrapper.style.display = "flex"; container.querySelectorAll(".remove-tag-btn").forEach(btn => { btn.addEventListener("click", (e) => { e.stopPropagation(); const filterKey = btn.getAttribute("data-filter"); removeSpecificFilter(filterKey); }); }); } else { container.innerHTML = ""; wrapper.style.display = "none"; } } function removeSpecificFilter(key) { if (key === "search") { state.activeFilters.search = ""; const input = document.getElementById("globalSearchBar"); if (input) input.value = ""; } else if (key === "category") { state.activeFilters.category = "all"; const catSelect = document.getElementById("filterCategory"); if (catSelect) catSelect.value = "all"; document.querySelectorAll(".market-category-item").forEach(item => item.classList.remove("active")); } else if (key === "sellerId") { state.activeFilters.sellerId = "all"; const sellerSelect = document.getElementById("filterSeller"); if (sellerSelect) sellerSelect.value = "all"; } else if (key === "price") { state.activeFilters.priceMin = null; state.activeFilters.priceMax = null; const pMin = document.getElementById("filterPriceMin"); const pMax = document.getElementById("filterPriceMax"); if (pMin) pMin.value = ""; if (pMax) pMax.value = ""; updateQuickPriceChips(); } else if (key === "rating") { state.activeFilters.ratingMin = 0; state.activeFilters.ratings = []; updateRatingChipsUI(); } else if (key === "freeShipping") { state.activeFilters.freeShipping = false; const cb = document.getElementById("filterFreeShipping"); if (cb) cb.checked = false; const chip = document.getElementById("chipFreeShipping"); if (chip) chip.classList.remove("active"); } else if (key === "hasCoupon") { state.activeFilters.hasCoupon = false; const cb = document.getElementById("filterHasCoupon"); if (cb) cb.checked = false; const chip = document.getElementById("chipHasCoupon"); if (chip) chip.classList.remove("active"); } renderCatalog(); } function renderCatalog() { const productsGrid = document.getElementById("productsGrid"); const resultsCount = document.getElementById("resultsCount"); if (!productsGrid) return; const filterCatDropdown = document.getElementById("filterCategory"); if (filterCatDropdown && filterCatDropdown.options.length > 0) { filterCatDropdown.options[0].text = "หมวดหมู่ทั้งหมด"; Array.from(filterCatDropdown.options).slice(1).forEach(opt => { opt.text = opt.value; }); } document.querySelectorAll(".market-category-item").forEach(item => { const cat = item.getAttribute("data-cat"); const labelEl = item.querySelector(".market-category-label"); if (cat && labelEl) { labelEl.textContent = cat; } }); state.products = db.getProducts(); // Render Seller Store Header Banner const sellerContainer = document.getElementById("sellerStoreHeaderContainer"); const sellerId = state.activeFilters.sellerId; if (sellerContainer) { if (sellerId === "all") { sellerContainer.innerHTML = ""; sellerContainer.style.display = "none"; } else { const sellerObj = db.getUsers().find(u => u.username === sellerId); const sellerName = sellerObj ? sellerObj.name : "ร้านค้าผู้ขาย"; const sellerProds = state.products.filter(p => p.sellerId === sellerId && p.active); const avgRating = sellerProds.length > 0 ? (sellerProds.reduce((sum, p) => sum + p.rating, 0) / sellerProds.length).toFixed(1) : "5.0"; const sellerProdCount = sellerProds.length; const followed = getFollowedSellers(); const isFollowing = followed.includes(sellerId); const followBtnText = isFollowing ? "<i class='fa-solid fa-check'></i> ติดตามแล้ว" : "<i class='fa-solid fa-plus'></i> ติดตามร้านค้า"; const followBtnClass = isFollowing ? "btn btn-sky btn-sm" : "btn btn-sky-outline btn-sm"; const companySub = (sellerObj && sellerObj.companyName) ? `<div style="font-size:12px; opacity:0.85; margin-top:2px;"><i class="fa-solid fa-building"></i> ${sellerObj.companyName}</div>` : ''; const sloganHtml = (sellerObj && sellerObj.slogan) ? `<div style="font-size:12px; opacity:0.95; margin-top:4px;"><i class="fa-solid fa-quote-left"></i> ${sellerObj.slogan}</div>` : ''; const avatarBoxHtml = (sellerObj && sellerObj.avatar) ? `<img src="${sellerObj.avatar}" style="width:100%; height:100%; object-fit:cover; border-radius:50%;">` : '<i class="fa-solid fa-store"></i>'; const hasCover = !!(sellerObj && sellerObj.coverBanner); const bannerBgStyle = hasCover ? `background: linear-gradient(rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.85)), url(${sellerObj.coverBanner}) center/cover no-repeat;` : ''; const hasCoverClass = hasCover ? 'has-cover' : ''; const contactBadges = sellerObj ? ` <div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:8px;"> ${sellerObj.storePhone ? `<span class="badge badge-dark" style="font-size:11px; background:rgba(255,255,255,0.15);"><i class="fa-solid fa-phone"></i> ${sellerObj.storePhone}</span>` : ''} ${sellerObj.cutoffTime ? `<span class="badge badge-sky" style="font-size:11px;"><i class="fa-solid fa-clock"></i> ตัดรอบ ${sellerObj.cutoffTime}</span>` : ''} ${sellerObj.primaryCarrier ? `<span class="badge badge-dark" style="font-size:11px; background:#0d9488;"><i class="fa-solid fa-truck-fast"></i> ${sellerObj.primaryCarrier}</span>` : ''} ${sellerObj.warrantyPolicy ? `<span class="badge badge-warning" style="font-size:11px;"><i class="fa-solid fa-shield-halved"></i> รับประกันสินค้า</span>` : ''} </div> ` : ''; const vacationBannerHtml = (sellerObj && sellerObj.isVacationMode) ? ` <div class="vacation-alert-banner" style="margin-top:14px; width:100%; grid-column: 1 / -1;"> <i class="fa-solid fa-umbrella-beach" style="font-size:1.4rem;"></i> <div> <strong>ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว (Vacation Mode)</strong> <div style="font-size:12px;">ทางร้านระงับการสั่งซื้อชั่วคราวเพื่อจัดเตรียมสินค้า ขออภัยในความไม่สะดวกค่ะ</div> </div> </div> ` : ''; sellerContainer.style.display = "block"; sellerContainer.innerHTML = ` <div class="seller-store-banner ${hasCoverClass}" style="${bannerBgStyle}"> <div class="seller-store-left"> <div class="seller-store-avatar"> ${avatarBoxHtml} </div> <div class="seller-store-details"> <h3 class="seller-store-title">${sellerName}${getOfficialBadgeHtml(sellerId)}</h3> ${companySub} ${sloganHtml} ${contactBadges} <div class="seller-store-rating" style="margin-top:6px;"> <span><i class="fa-solid fa-star"></i> ${avgRating} / 5.0</span> <span>•</span> <span>สินค้าทั้งหมด ${sellerProdCount} ชิ้น</span> </div> </div> </div> <div class="seller-store-actions"> <button class="${followBtnClass}" id="btnFollowSeller" data-id="${sellerId}">${followBtnText}</button> </div> ${vacationBannerHtml} </div> `; const followBtn = sellerContainer.querySelector("#btnFollowSeller"); if (followBtn) { followBtn.addEventListener("click", () => { toggleFollowSeller(sellerId); renderCatalog(); }); } } } let filtered = state.products.filter(p => { if (!p.active) return false; const nameMatch = p.name.toLowerCase().includes(state.activeFilters.search) || (p.sellerName && p.sellerName.toLowerCase().includes(state.activeFilters.search)); const catMatch = state.activeFilters.category === "all" || p.category === state.activeFilters.category; const sellerMatch = state.activeFilters.sellerId === "all" || p.sellerId === state.activeFilters.sellerId; const minPriceMatch = state.activeFilters.priceMin == null || p.price >= state.activeFilters.priceMin; const maxPriceMatch = state.activeFilters.priceMax == null || p.price <= state.activeFilters.priceMax; let ratingMatch = true; if (state.activeFilters.ratingMode === "min") { ratingMatch = p.rating >= state.activeFilters.ratingMin; } else { ratingMatch = state.activeFilters.ratings.length === 0 || state.activeFilters.ratings.includes(p.rating); } const freeShippingMatch = !state.activeFilters.freeShipping || p.freeShipping === true || p.isFreeShipping === true || p.price >= 500; const hasCouponMatch = !state.activeFilters.hasCoupon || p.hasCoupon === true || p.couponCode || (p.discount && p.discount > 0) || p.price > 1000; return nameMatch && catMatch && sellerMatch && minPriceMatch && maxPriceMatch && ratingMatch && freeShippingMatch && hasCouponMatch; }); renderActiveFilterTags(); if (state.sortBy === "price-asc") { filtered.sort((a, b) => a.price - b.price); } else if (state.sortBy === "price-desc") { filtered.sort((a, b) => b.price - a.price); } else if (state.sortBy === "rating-desc") { filtered.sort((a, b) => b.rating - a.rating); } if (resultsCount) resultsCount.innerText = `พบสินค้าทั้งหมด ${filtered.length} รายการ`; // Render Skeleton Loading Cards const skeletonCount = Math.min(8, Math.max(4, filtered.length)); const skeletonHtml = Array(skeletonCount).fill(0).map(() => ` <div class="skeleton-card"> <div class="skeleton-box skeleton-img"></div> <div class="skeleton-box skeleton-title"></div> <div class="skeleton-box skeleton-price"></div> </div> `).join(""); productsGrid.innerHTML = skeletonHtml; setTimeout(() => { productsGrid.innerHTML = ""; if (filtered.length === 0) { productsGrid.innerHTML = `<div class="text-center w-100 py-5" style="grid-column: 1/-1;"><p style="color: var(--color-sky-slate)">ไม่พบสินค้าที่คุณค้นหา</p></div>`; return; } filtered.forEach(p => { const totalStock = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : 0; const card = document.createElement("div"); card.className = "product-card"; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const sellerObj = db.getUsers().find(u => u.username === p.sellerId); const isVacation = sellerObj && sellerObj.isVacationMode; const isLiked = state.wishlist.includes(p.id); const vacationText = 'ร้านพักร้อน'; const outOfStockText = 'หมดชั่วคราว'; const viewDetailTooltip = 'ดูรายละเอียดสินค้า'; const likeTitle = isLiked ? 'ยกเลิกถูกใจ' : 'ถูกใจสินค้า'; card.innerHTML = ` <button class="product-wishlist-btn ${isLiked ? 'active' : ''}" data-id="${p.id}" title="${likeTitle}"> <i class="fa-solid fa-heart"></i> </button> <div class="product-image-wrapper"> <div class="product-badge-overlay"> ${isVacation ? `<span class="badge badge-warning" style="background:#f59e0b; color:white;"><i class="fa-solid fa-umbrella-beach"></i> ${vacationText}</span>` : ''} ${totalStock === 0 ? `<span class="badge badge-danger">${outOfStockText}</span>` : ''} </div> <img src="${imgSrc}" class="product-img" alt="${p.name}"> </div> <div class="product-details"> <span class="product-category">${getCatName(p.category)}</span> <h4 class="product-title">${p.name}</h4> <div class="product-meta-row"> <span class="product-seller-name"><i class="fa-solid fa-store"></i> <strong>${p.sellerName || 'SkyMall'}</strong>${getOfficialBadgeHtml(p.sellerId)}</span> <span class="product-rating-inline"><i class="fa-solid fa-star text-warning"></i> ${p.rating.toFixed(1)}</span> </div> <div class="product-bottom"> <span class="product-price">฿${p.price.toLocaleString()}</span> <button class="btn-card-action view-detail-btn" data-id="${p.id}" title="${viewDetailTooltip}"> <i class="fa-solid fa-eye"></i> </button> </div> </div> `; // Whole Card Click Event Handler card.addEventListener("click", () => { openProductDetail(p.id); }); // Detail Button Click Event const detailBtn = card.querySelector(".view-detail-btn"); if (detailBtn) { detailBtn.addEventListener("click", (e) => { e.stopPropagation(); openProductDetail(p.id); }); } // Wishlist Toggle Event with stopPropagation const wishBtn = card.querySelector(".product-wishlist-btn"); if (wishBtn) { wishBtn.addEventListener("click", (e) => { e.stopPropagation(); if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนดำเนินการค่ะ", "danger"); document.querySelectorAll(".modal-overlay").forEach(m => m.style.display = "none"); state.postLoginRedirect = "wishlist"; navigateTo("auth"); return; } toggleWishlist(p.id); }); } productsGrid.appendChild(card); }); }, 1000); } // ========================================================================== // 6. WISHLIST MANAGEMENT // ========================================================================== function updateWishlistBadge() { const countLabel = document.getElementById("wishlistCountLabel"); if (countLabel) { const user = db.getCurrentUser(); if (!user) { countLabel.innerText = "0"; state.wishlist = []; } else { const wish = db.getWishlist(); state.wishlist = wish; countLabel.innerText = wish.length; } } } function toggleWishlist(productId) { if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนดำเนินการค่ะ", "danger"); document.querySelectorAll(".modal-overlay").forEach(m => m.style.display = "none"); state.postLoginRedirect = "wishlist"; navigateTo("auth"); return; } let wish = db.getWishlist(); const idx = wish.indexOf(productId); const isAdded = idx === -1; if (idx > -1) { wish.splice(idx, 1); } else { wish.push(productId); } db.saveWishlist(wish); state.wishlist = wish; updateWishlistBadge(); const wishlistBtns = document.querySelectorAll(`.product-wishlist-btn[data-id="${productId}"]`); wishlistBtns.forEach(btn => { if (isAdded) { btn.classList.add("active"); } else { btn.classList.remove("active"); } }); const wishlistSection = document.getElementById("page-wishlist"); if (wishlistSection && wishlistSection.style.display === "block") { renderWishlist(); } } // ========================================================================== // 7. CLIENT PRODUCT DETAILS MODAL (Options selector & Specifications) // ========================================================================== const MOCK_REVIEWS = { "prod-1": [ { username: "@somchai_r", rating: 5, date: "2026-07-15", text: "จัดส่งรวดเร็วมากครับ สินค้าแพ็คมาอย่างดี ของแท้แน่นอน แนะนำร้านนี้เลยครับ!" }, { username: "@kitti_k", rating: 4, date: "2026-07-10", text: "กล้องซูมได้ชัดมากครับ ชิป AI เร็วใช้ได้ แต่แบตเตอรี่หมดเร็วกว่าที่คิดนิดหน่อย โดยรวมยังดีครับ" }, { username: "@nattapong_m", rating: 5, date: "2026-07-08", text: "ประทับใจความไหลลื่นของหน้าจอมาก สีไทเทเนียมสวยหรูหรามาก คุ้มราคาที่สุด" }, { username: "@patty_s", rating: 5, date: "2026-07-01", text: "ดีไซน์กล้องสวยมาก ขนส่งสุภาพ บริการประทับใจค่ะ" }, { username: "@anuchit_t", rating: 3, date: "2026-06-25", text: "สินค้าดี แต่แถมหัวชาร์จช้าไปนิดนึง ลำโพงเสียงยังไม่แน่นเท่าไหร่" }, { username: "@wipha_j", rating: 2, date: "2026-06-18", text: "ขนส่งช้าไปหน่อย กล่องบุบ แต่ตัวเครื่องไม่มีรอยเสียหายค่ะ" } ], "prod-2": [ { username: "@somchai_r", rating: 5, date: "2026-07-14", text: "ตัดเสียงรบกวนได้เงียบดีมากครับ เบสแน่นสะใจ ใส่สบายหูไม่เจ็บเลย" }, { username: "@kitti_k", rating: 4, date: "2026-07-13", text: "คุณภาพดีมากคุ้มราคา ขนาดตรงตามรายละเอียดที่ลงไว้ครับ" }, { username: "@wilai_p", rating: 5, date: "2026-07-05", text: "เสียงดีมากค่ะ คุยโทรศัพท์ไมค์ชัดเจน ไม่มีเสียงแทรกคุ้มค่ามาก" }, { username: "@tewan_s", rating: 3, date: "2026-06-30", text: "ฟีเจอร์ตัดเสียงรบกวนใช้ได้ แต่เชื่อมต่อกับมือถือบางแบรนด์ยังช้าอยู่นะครับ" }, { username: "@chalit_a", rating: 1, date: "2026-06-20", text: "ได้รับของแล้วหูข้างขวาไม่มีเสียงเชื่อมต่อไม่ได้เลยครับ ต้องส่งเปลี่ยน เสียเวลามาก" } ], "prod-3": [ { username: "@nat_h", rating: 5, date: "2026-07-16", text: "ผ้านุ่มใส่สบายมาก ลายสกรีนเมฆสีฟ้าน่ารักตรงปกสุดๆ แนะนำค่ะ" }, { username: "@piti_s", rating: 5, date: "2026-07-12", text: "เนื้อผ้าหนากำลังดี ตัดเย็บเรียบร้อย ทรงสวยตรงปกส่งไวมากครับ" }, { username: "@somsri_t", rating: 4, date: "2026-07-06", text: "เสื้อสวยดีค่ะ แต่ไซส์ XL แอบใหญ่กว่าปกตินิดนึง" }, { username: "@chalong_c", rating: 3, date: "2026-06-28", text: "ผ้าสีฟ้าสวยงาม แต่ซักครั้งแรกแอบมีสีตกเล็กน้อย ควรซักแยกนะครับ" } ], "prod-4": [ { username: "@anong_k", rating: 5, date: "2026-07-17", text: "ใช้งานง่ายมาก หน้าจอสัมผัสลื่นไหล ทอดเฟรนช์ฟรายส์กรอบอร่อยโดยไม่ต้องใช้น้ำมันเลย ชอบมากค่ะ" }, { username: "@pricha_t", rating: 5, date: "2026-07-14", text: "ขนาดใหญ่จุใจ 5.5 ลิตร ทำไก่ทอดได้เป็นตัวๆ เลย ล้างทำความสะอาดง่ายด้วยครับ" }, { username: "@mana_s", rating: 4, date: "2026-07-10", text: "ทอดอาหารกรอบดีมากครับ แต่เสียงพัดลมเครื่องแอบดังไปนิดหน่อยตอนทำงาน" }, { username: "@sudaporn_c", rating: 5, date: "2026-07-01", text: "ใช้ดีจริงค่ะ ซื้อให้คุณแม่ใช้ ทำอาหารเช้าง่ายขึ้นเยอะเลย" } ], "prod-5": [ { username: "@suda_m", rating: 5, date: "2026-07-15", text: "ซึมไวไม่เหนียวเหนอะหนะ ใช้แล้วผิวใสขึ้นอย่างเห็นได้ชัดในหนึ่งสัปดาห์ ไม่แพ้เลยค่ะ" }, { username: "@pim_p", rating: 4, date: "2026-07-09", text: "เนื้อเซรั่มใสดี ไม่มีกลิ่นน้ำหอมฉุน จุดด่างดำจางลงนิดหน่อย ต้องใช้ระยะยาวค่ะ" }, { username: "@warat_y", rating: 5, date: "2026-07-02", text: "คุ้มค่าราคาประหยัด ใช้ดีมาก ผิวแพ้ง่ายใช้ได้สบายๆ ครับ แนะนำ" }, { username: "@urai_l", rating: 3, date: "2026-06-20", text: "ชุ่มชื้นปานกลาง ไม่ค่อยเห็นผลเรื่องหน้าใสเท่าไหร่ แต่ผิวดูเรียบเนียนขึ้นบ้าง" } ], "prod-6": [ { username: "@sayan_t", rating: 4, date: "2026-07-12", text: "สภาพเครื่อง 90% ตามที่แจ้งเลยครับ ล้างสะอาดมาก ประหยัดเวลาล้างจานไปได้เยอะ" }, { username: "@jirapon_p", rating: 3, date: "2026-07-05", text: "ใช้งานล้างได้สะอาดดี แต่เมนูเป็นภาษาญี่ปุ่นทั้งหมด ต้องงมหาคู่มือเอาครับ" }, { username: "@metha_n", rating: 2, date: "2026-06-25", text: "ตัวเครื่องล้างสะอาด แต่สายระบายน้ำทิ้งสั้นเกินไป ต้องซื้อสายต่อเพิ่มเองครับ" } ] }; function getProductReviews(productId) { if (MOCK_REVIEWS[productId]) { return MOCK_REVIEWS[productId]; } const product = db.getProducts().find(p => p.id === productId); const rating = product ? product.rating : 5; return [ { username: "@customer_a", rating: rating, date: "2026-07-16", text: "ได้รับสินค้าเรียบร้อยดีครับ ตรงปกและจัดส่งรวดเร็วมาก" }, { username: "@customer_b", rating: Math.max(1, rating - 1), date: "2026-07-15", text: "คุณภาพเหมาะสมกับราคา ใช้งานได้ปกติไม่มีปัญหาอะไรครับ" } ]; } function openProductDetail(productId) { const modal = document.getElementById("productDetailModal"); const content = document.getElementById("modalProductDetailContent"); const product = db.getProducts().find(p => p.id === productId); if (!product || !modal || !content) return; const placeholderImg = (typeof DEFAULT_IMAGE_PLACEHOLDERS !== 'undefined' && DEFAULT_IMAGE_PLACEHOLDERS[product.id]) ? DEFAULT_IMAGE_PLACEHOLDERS[product.id] : ((typeof DEFAULT_IMAGE_PLACEHOLDERS !== 'undefined') ? DEFAULT_IMAGE_PLACEHOLDERS["default"] : ""); const imgSrc = product.image || placeholderImg; const imagesArray = (product.images && product.images.length > 0) ? product.images : [imgSrc]; const hasColors = product.colors && product.colors.length > 0; const hasSizes = product.variants && product.variants.some(v => v.size !== "Default" && v.size !== ""); const sellerObj = db.getUsers().find(u => u.username === product.sellerId); const isSellerOnVacation = sellerObj && sellerObj.isVacationMode; const vacationAlertModalHtml = isSellerOnVacation ? ` <div class="vacation-alert-banner" style="margin-top: 12px; font-size: 0.85rem;"> <i class="fa-solid fa-umbrella-beach" style="font-size:1.3rem;"></i> <div> <strong>ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว (Vacation Mode)</strong> <div style="font-size:11px;">ผู้ขายปิดรับคำสั่งซื้อชั่วคราว ไม่สามารถสั่งซื้อสินค้าได้ในขณะนี้</div> </div> </div> ` : ''; const thumbnailsHtml = imagesArray.map((img, idx) => ` <img class="gallery-thumb-item detail-thumb-img ${idx === 0 ? 'active' : ''}" src="${img}" alt="Thumbnail ${idx + 1}" data-src="${img}"> `).join(""); const initialStock = (product.stock !== undefined && product.stock !== null) ? product.stock : 0; content.innerHTML = ` <div class="detail-img-wrapper"> <div class="detail-img-box"> <img id="detailMainImg" src="${imgSrc}" alt="${product.name}"> </div> <div class="product-gallery-thumbnails detail-thumbnails-row"> ${thumbnailsHtml} </div> </div> <div class="detail-info-box"> <div> <span class="badge badge-sky">${getCatName(product.category)}</span> <span class="badge badge-dark" style="background:#0f766e; color:#ffffff; margin-left: 5px;"><i class="fa-solid fa-store"></i> ร้านค้า: ${product.sellerName || 'SkyMall'}</span>${getOfficialBadgeHtml(product.sellerId)} <button class="btn btn-sky-outline btn-xs" id="btnGoToSellerShop" style="margin-left: 8px; font-size: 11px; padding: 2px 6px; border-radius: 4px;"><i class="fa-solid fa-arrow-right"></i> ไปที่ร้านค้า</button> <h2 class="mt-3">${product.name}</h2> <div style="display: flex; align-items: center; gap: 10px; margin-top: 8px; margin-bottom: 12px;"> <span style="color: #f59e0b; font-size: 1.1rem;">${"★".repeat(product.rating || 5)}${"☆".repeat(5 - (product.rating || 5))}</span> <span style="color: var(--color-sky-slate); font-size: 0.85rem;">ขายแล้ว ${Math.floor((product.price % 37) + 12)} ชิ้น</span> </div> <div class="detail-price">฿${product.price.toLocaleString()}</div> <!-- Trust Badges --> <div style="display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap;"> <span class="badge badge-sky-outline" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; border: 1px solid #0d9488; color: #0d9488; background: #f0fdfa;"><i class="fa-solid fa-truck-fast"></i> ฟรีค่าจัดส่ง Standard Delivery</span> <span class="badge badge-sky-outline" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; border: 1px solid #0d9488; color: #0d9488; background: #f0fdfa;"><i class="fa-solid fa-shield-halved"></i> รับประกันของแท้ 100%</span> </div> ${vacationAlertModalHtml} </div> <p class="detail-desc" style="margin-top: 15px;">${product.description}</p> <!-- Color Selection Group --> ${hasColors ? ` <div class="option-group"> <span class="option-title">เลือกรูปแบบ / ตัวเลือกสินค้า:</span> <div class="option-selector" id="modalColorSelector"> ${product.colors.map(col => `<button class="filter-chip btn-select-color" data-color="${col}">${col}</button>`).join("")} </div> </div> ` : ''} <!-- Size Selection Group --> ${hasSizes ? ` <div class="option-group"> <div class="row align-items-center"> <span class="option-title">เลือกขนาด / คุณลักษณะย่อย:</span> <a href="#" class="size-chart-link" id="toggleSizeChartBtn" style="display: ${product.category === 'แฟชั่น/เสื้อผ้า' || product.category === 'แฟชั่น & เสื้อผ้า' ? 'block' : 'none'};"><i class="fa-solid fa-table"></i> ตารางไซส์เสื้อผ้า</a> </div> <div class="option-selector" id="modalSizeSelector"> <span style="font-size:0.85rem; color:var(--color-sky-slate)">โปรดเลือกตัวเลือกหลักก่อนทำการระบุคุณลักษณะย่อย</span> </div> </div> ` : ''} <!-- Apparel size chart details --> <div class="size-chart-box" id="sizeChartBox" style="display: none; margin-top: 10px; padding: 10px; background: var(--color-bg-gray); border: 1px solid var(--color-border); border-radius: 6px; font-size: 0.8rem;"> <strong>ตารางขนาดไซส์มาตรฐาน (เสื้อผ้า):</strong> <ul style="margin: 5px 0 0 15px; padding: 0;"> <li>S: รอบอก 34-36 นิ้ว | ความยาว 26 นิ้ว</li> <li>M: รอบอก 38-40 นิ้ว | ความยาว 28 นิ้ว</li> <li>L: รอบอก 42-44 นิ้ว | ความยาว 30 นิ้ว</li> <li>XL: รอบอก 46-48 นิ้ว | ความยาว 32 นิ้ว</li> </ul> </div> <div class="detail-actions" style="margin-top: 20px;"> <div class="detail-qty" style="display: flex; align-items: center; gap: 12px; margin-bottom: 15px;"> <div class="qty-selector-wrapper"> <button type="button" class="qty-btn" id="btnDetailQtyMinus">-</button> <input type="number" class="qty-input" id="detailQtyInput" value="1" min="1" max="${initialStock}"> <button type="button" class="qty-btn" id="btnDetailQtyPlus">+</button> </div> <span class="stock-info" id="spanMaxStockHint" style="font-size: 0.85rem; color: var(--color-sky-slate);">(มีสินค้าทั้งหมด ${initialStock} ชิ้น)</span> </div> <div style="display: flex; gap: 10px; width: 100%; margin-top: 15px;"> <button class="btn btn-sky-outline w-100" id="addToCartBtn" ${isSellerOnVacation || initialStock <= 0 ? 'disabled style="opacity:0.5; cursor:not-allowed;"' : ''}> <i class="fa-solid fa-cart-plus"></i> เพิ่มลงตะกร้า </button> <button class="btn btn-sky w-100" id="buyNowBtn" ${isSellerOnVacation || initialStock <= 0 ? 'disabled style="opacity:0.5; cursor:not-allowed;"' : ''}> <i class="fa-solid fa-bolt"></i> ซื้อทันที </button> </div> </div> <!-- PRODUCT REVIEWS SECTION --> <div class="product-reviews-container mt-4 pt-3" style="border-top: 1px solid var(--color-border);"> <h4 style="font-size: 1rem; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between;"> <span><i class="fa-solid fa-comments text-sky"></i> รีวิวจากผู้ใช้ (${product.reviewsCount || 0})</span> <span style="font-size: 0.85rem; color: #f59e0b;"><i class="fa-solid fa-star"></i> ${product.rating || 5.0} / 5.0</span> </h4> <div class="reviews-list" style="display: flex; flex-direction: column; gap: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;"> ${getProductReviews(product.id).map(r => ` <div class="review-item" style="background: var(--color-bg-gray); border-radius: 8px; padding: 10px; font-size: 0.82rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;"> <strong style="color: var(--color-sky-dark);">${r.username || r.user || '@customer'}</strong> <span style="color: #f59e0b;">${"★".repeat(r.rating)}${"☆".repeat(5 - r.rating)}</span> </div> <div style="color: var(--color-sky-slate); font-size: 0.78rem; margin-bottom: 4px;">${r.date} · ตัวเลือก: ${r.variant || 'มาตรฐาน'}</div> <div style="color: var(--color-sky-dark);">${r.text || r.comment || ''}</div> </div> `).join('')} </div> </div> </div> `; // Add Image Gallery Thumbnail Click Listeners const mainImg = content.querySelector("#detailMainImg"); const thumbs = content.querySelectorAll(".gallery-thumb-item, .detail-thumb-img"); thumbs.forEach(thumb => { thumb.addEventListener("click", () => { thumbs.forEach(t => t.classList.remove("active")); thumb.classList.add("active"); if (mainImg) { mainImg.src = thumb.getAttribute("data-src"); } }); }); const btnGoToSellerShop = content.querySelector("#btnGoToSellerShop"); if (btnGoToSellerShop) { btnGoToSellerShop.addEventListener("click", () => { modal.style.display = "none"; openStoreProfile(product.sellerId); }); } const toggleSizeChartBtn = content.querySelector("#toggleSizeChartBtn"); const sizeChartBox = content.querySelector("#sizeChartBox"); if (toggleSizeChartBtn && sizeChartBox) { toggleSizeChartBtn.addEventListener("click", (e) => { e.preventDefault(); sizeChartBox.style.display = sizeChartBox.style.display === "none" ? "block" : "none"; }); } let selectedColor = hasColors ? product.colors[0] : null; let selectedSize = null; function getSelectedMaxStock() { let stockVal = (product.stock !== undefined && product.stock !== null) ? product.stock : 0; if (product.variants && product.variants.length > 0) { const sColor = selectedColor ? selectedColor.trim() : null; const sSize = selectedSize ? selectedSize.trim() : null; let variantMatch = null; if (hasColors && hasSizes && sColor && sSize) { variantMatch = product.variants.find(v => { const vc = v.color ? v.color.trim() : ""; const vs = v.size ? v.size.trim() : ""; const vn = v.name ? v.name.trim() : ""; return (vc === sColor && vs === sSize) || (vn.includes(sColor) && vn.includes(sSize)); }); } else if (hasColors && sColor) { variantMatch = product.variants.find(v => { const vc = v.color ? v.color.trim() : ""; const vn = v.name ? v.name.trim() : ""; return vc === sColor || vn.includes(sColor); }); } else if (hasSizes && sSize) { variantMatch = product.variants.find(v => { const vs = v.size ? v.size.trim() : ""; const vn = v.name ? v.name.trim() : ""; return vs === sSize || vn.includes(sSize); }); } if (variantMatch && variantMatch.stock !== undefined && variantMatch.stock !== null) { stockVal = variantMatch.stock; } } return (isNaN(stockVal) || stockVal < 0) ? 0 : stockVal; } function updateQtySelector(maxStock) { const hint = content.querySelector("#spanMaxStockHint"); const input = content.querySelector("#detailQtyInput"); const bMinus = content.querySelector("#btnDetailQtyMinus"); const bPlus = content.querySelector("#btnDetailQtyPlus"); const validMax = (maxStock === undefined || maxStock === null || isNaN(maxStock)) ? 0 : maxStock; if (hint) { hint.innerText = `(มีสินค้าทั้งหมด ${validMax} ชิ้น)`; } if (input) { input.max = validMax; let currentVal = parseInt(input.value, 10) || 1; if (validMax === 0) { input.value = 0; } else if (currentVal > validMax) { input.value = validMax; } else if (currentVal < 1) { input.value = 1; } } if (bMinus && input) { bMinus.disabled = (parseInt(input.value, 10) <= 1 || validMax === 0); } if (bPlus && input) { bPlus.disabled = (parseInt(input.value, 10) >= validMax || validMax === 0); } } function updateAddToCartButton() { const cartBtn = content.querySelector("#addToCartBtn") || content.querySelector("#btnAddToCartModal"); const buyBtn = content.querySelector("#buyNowBtn") || content.querySelector("#btnBuyNowModal"); const priceDisplay = content.querySelector(".detail-price"); const stockHint = content.querySelector("#spanMaxStockHint"); let stock = (product.stock !== undefined && product.stock !== null) ? product.stock : 0; let price = product.price; if (product.variants && product.variants.length > 0) { const sColor = selectedColor ? selectedColor.trim() : null; const sSize = selectedSize ? selectedSize.trim() : null; let variantMatch = null; if (hasColors && hasSizes && sColor && sSize) { variantMatch = product.variants.find(v => { const vc = v.color ? v.color.trim() : ""; const vs = v.size ? v.size.trim() : ""; const vn = v.name ? v.name.trim() : ""; return (vc === sColor && vs === sSize) || (vn.includes(sColor) && vn.includes(sSize)); }); } else if (hasColors && sColor) { variantMatch = product.variants.find(v => { const vc = v.color ? v.color.trim() : ""; const vn = v.name ? v.name.trim() : ""; return vc === sColor || vn.includes(sColor); }); } else if (hasSizes && sSize) { variantMatch = product.variants.find(v => { const vs = v.size ? v.size.trim() : ""; const vn = v.name ? v.name.trim() : ""; return vs === sSize || vn.includes(sSize); }); } if (variantMatch) { if (variantMatch.stock !== undefined && variantMatch.stock !== null) { stock = variantMatch.stock; } if (variantMatch.price !== undefined && variantMatch.price !== null) { price = variantMatch.price; } } } if (isNaN(stock) || stock < 0) stock = 0; if (isNaN(price)) price = product.price || 0; if (priceDisplay) { priceDisplay.innerText = `฿${price.toLocaleString()}`; } if (stockHint) { stockHint.innerText = `(มีสินค้าทั้งหมด ${stock || 0} ชิ้น)`; } updateQtySelector(stock); const isSelectionComplete = (!hasColors || selectedColor) && (!hasSizes || selectedSize); const canPurchase = !isSellerOnVacation && isSelectionComplete && stock > 0; if (cartBtn) { cartBtn.disabled = !canPurchase; cartBtn.style.opacity = canPurchase ? "1" : "0.5"; cartBtn.style.cursor = canPurchase ? "pointer" : "not-allowed"; } if (buyBtn) { buyBtn.disabled = !canPurchase; buyBtn.style.opacity = canPurchase ? "1" : "0.5"; buyBtn.style.cursor = canPurchase ? "pointer" : "not-allowed"; } } const modalColorSelector = content.querySelector("#modalColorSelector"); const modalSizeSelector = content.querySelector("#modalSizeSelector"); if (hasColors && modalColorSelector) { const colorBtns = modalColorSelector.querySelectorAll(".btn-select-color"); colorBtns.forEach(btn => { if (btn.getAttribute("data-color") === selectedColor) { btn.classList.add("active"); } btn.addEventListener("click", () => { colorBtns.forEach(b => b.classList.remove("active")); btn.classList.add("active"); selectedColor = btn.getAttribute("data-color"); if (hasSizes && modalSizeSelector) { renderModalSizes(product, selectedColor, (size) => { selectedSize = size; updateAddToCartButton(); }); } updateAddToCartButton(); }); }); } if (hasSizes && modalSizeSelector) { renderModalSizes(product, selectedColor, (size) => { selectedSize = size; updateAddToCartButton(); }); } const qtyInput = content.querySelector("#detailQtyInput"); const btnMinus = content.querySelector("#btnDetailQtyMinus"); const btnPlus = content.querySelector("#btnDetailQtyPlus"); if (btnMinus && qtyInput) { btnMinus.addEventListener("click", () => { let current = parseInt(qtyInput.value, 10) || 1; if (current > 1) { qtyInput.value = current - 1; updateQtySelector(getSelectedMaxStock()); } }); } if (btnPlus && qtyInput) { btnPlus.addEventListener("click", () => { let current = parseInt(qtyInput.value, 10) || 1; let maxStock = getSelectedMaxStock(); if (current < maxStock) { qtyInput.value = current + 1; updateQtySelector(maxStock); } }); } if (qtyInput) { qtyInput.addEventListener("input", () => { updateQtySelector(getSelectedMaxStock()); }); } updateAddToCartButton(); const btnAddToCart = content.querySelector("#addToCartBtn") || content.querySelector("#btnAddToCartModal"); const btnBuyNow = content.querySelector("#buyNowBtn") || content.querySelector("#btnBuyNowModal"); if (btnAddToCart) { btnAddToCart.addEventListener("click", (e) => { if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนดำเนินการค่ะ", "danger"); modal.style.display = "none"; state.postLoginRedirect = "cart"; navigateTo("auth"); return; } if (isSellerOnVacation) return; if (hasColors && !selectedColor) { showToast("กรุณาเลือกสี/ตัวเลือกสินค้า", "danger"); return; } if (hasSizes && !selectedSize) { showToast("กรุณาเลือกไซส์/ขนาดสินค้า", "danger"); return; } const currentQty = parseInt(qtyInput ? qtyInput.value : "1", 10) || 1; const added = addToCart(product.id, selectedColor, selectedSize, currentQty); if (added) { if (typeof createFlyingCartAnimation === 'function') { const imgToFly = mainImg ? mainImg.src : imgSrc; createFlyingCartAnimation(e.clientX, e.clientY, imgToFly); } modal.style.display = "none"; } }); } if (btnBuyNow) { btnBuyNow.addEventListener("click", () => { if (!state.currentUser) { showToast("กรุณาเข้าสู่ระบบก่อนดำเนินการค่ะ", "danger"); modal.style.display = "none"; state.postLoginRedirect = "checkout"; navigateTo("auth"); return; } if (isSellerOnVacation) { showToast("ร้านค้านี้กำลังอยู่ในช่วงพักร้อนชั่วคราว ไม่สามารถสั่งซื้อสินค้าได้", "danger"); return; } if ((hasColors && !selectedColor) || (hasSizes && !selectedSize)) { showToast("กรุณาเลือกตัวเลือกสินค้าให้ครบถ้วน", "danger"); return; } const currentQty = parseInt(qtyInput ? qtyInput.value : "1", 10) || 1; // 1. เพิ่มสินค้าลงในตะกร้า (state.cart) const added = addToCart(product.id, selectedColor, selectedSize, currentQty); // 2. ถ้าเพิ่มสำเร็จจึงปิด Modal และพาย้ายไปหน้า Checkout ทันที if (added) { modal.style.display = "none"; if (typeof renderCheckout === "function") { renderCheckout(); } navigateTo("checkout"); } }); } modal.style.display = "flex"; } function renderModalSizes(product, color, onSelectSize) { const sizeContainer = document.getElementById("modalSizeSelector"); if (!sizeContainer) return; sizeContainer.innerHTML = ""; const sColor = color ? color.trim() : ""; const colorVariants = product.variants ? product.variants.filter(v => { const vc = v.color ? v.color.trim() : ""; const vn = v.name ? v.name.trim() : ""; return vc === sColor || vn.includes(sColor); }) : []; if (colorVariants.length === 0) { sizeContainer.innerHTML = `<span style="font-size:0.85rem; color:var(--color-sky-slate)">ไม่มีไซส์สำหรับตัวเลือกนี้</span>`; return; } colorVariants.forEach(variant => { const btn = document.createElement("button"); btn.className = "filter-chip"; const vSize = variant.size ? variant.size.trim() : "Default"; const vStock = (variant.stock !== undefined && variant.stock !== null) ? variant.stock : 0; btn.innerHTML = `${vSize} <small>(${vStock})</small>`; if (vStock <= 0) { btn.classList.add("disabled"); btn.style.opacity = "0.5"; btn.style.cursor = "not-allowed"; } else { btn.addEventListener("click", () => { sizeContainer.querySelectorAll(".filter-chip").forEach(b => b.classList.remove("active")); btn.classList.add("active"); onSelectSize(vSize); }); } sizeContainer.appendChild(btn); }); } function openStoreProfile(sellerId) { const modal = document.getElementById("storeProfileModal"); const content = document.getElementById("storeProfileModalContent"); if (!modal || !content) return; const users = db.getUsers(); const sellerObj = users.find(u => u.username === sellerId); const sellerName = sellerObj ? sellerObj.name : sellerId; const companyName = (sellerObj && sellerObj.companyName) ? sellerObj.companyName : ''; const slogan = (sellerObj && sellerObj.slogan) ? sellerObj.slogan : 'ร้านค้าพรีเมียมบน SkyMall บริการด้วยใจ จัดส่งรวดเร็ว'; const storePhone = (sellerObj && sellerObj.storePhone) ? sellerObj.storePhone : '02-123-4567'; const cutoffTime = (sellerObj && sellerObj.cutoffTime) ? sellerObj.cutoffTime : '14:00 น.'; const primaryCarrier = (sellerObj && sellerObj.primaryCarrier) ? sellerObj.primaryCarrier : 'Standard Express'; const warrantyPolicy = (sellerObj && sellerObj.warrantyPolicy) ? sellerObj.warrantyPolicy : 'รับประกันสินค้า 7 วันเปลี่ยนใหม่'; const avatarHtml = (sellerObj && sellerObj.avatar) ? `<img src="${sellerObj.avatar}" alt="${sellerName}">` : '<i class="fa-solid fa-store"></i>'; const hasCover = !!(sellerObj && sellerObj.coverBanner); const coverStyle = hasCover ? `background: linear-gradient(rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.75)), url('${sellerObj.coverBanner}') center/cover no-repeat;` : ''; const allProds = db.getProducts(); const storeProds = allProds.filter(p => p.sellerId === sellerId && p.active); const avgRating = storeProds.length > 0 ? (storeProds.reduce((sum, p) => sum + p.rating, 0) / storeProds.length).toFixed(1) : "5.0"; const totalProdCount = storeProds.length; const isVacation = sellerObj && sellerObj.isVacationMode; content.innerHTML = ` <div class="store-cover-banner-modal" style="${coverStyle}"> ${isVacation ? '<span class="badge badge-warning store-vacation-badge"><i class="fa-solid fa-umbrella-beach"></i> ร้านพักร้อนชั่วคราว</span>' : ''} </div> <div class="store-profile-header-modal"> <div class="store-avatar-modal"> ${avatarHtml} </div> <div class="store-info-modal"> <h3 class="store-title-modal">${sellerName}${getOfficialBadgeHtml(sellerId)}</h3> ${companyName ? `<div class="store-company-modal"><i class="fa-solid fa-building"></i> ${companyName}</div>` : ''} <div class="store-slogan-modal"><i class="fa-solid fa-quote-left"></i> ${slogan}</div> </div> </div> <div class="store-badges-grid-modal"> <div class="store-badge-item"> <i class="fa-solid fa-phone text-sky"></i> <div> <span class="badge-title">ติดต่อร้านค้า</span> <span class="badge-val">${storePhone}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-clock text-sky"></i> <div> <span class="badge-title">เวลาตัดรอบ</span> <span class="badge-val">${cutoffTime}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-truck-fast text-sky"></i> <div> <span class="badge-title">ขนส่งหลัก</span> <span class="badge-val">${primaryCarrier}</span> </div> </div> <div class="store-badge-item"> <i class="fa-solid fa-shield-halved text-warning"></i> <div> <span class="badge-title">การรับประกัน</span> <span class="badge-val">${warrantyPolicy}</span> </div> </div> </div> <div class="store-stats-row-modal"> <div class="store-stat-box"> <span class="stat-num"><i class="fa-solid fa-star text-warning"></i> ${avgRating}</span> <span class="stat-lbl">คะแนนร้านค้า</span> </div> <div class="store-stat-box"> <span class="stat-num">${totalProdCount}</span> <span class="stat-lbl">รายการสินค้า</span> </div> <div class="store-stat-box"> <span class="stat-num">98%</span> <span class="stat-lbl">อัตราตอบกลับ</span> </div> </div> <div class="store-actions-modal mt-3"> <button class="btn btn-sky w-100 btn-lg" id="btnViewAllStoreProducts" data-seller="${sellerId}"> <i class="fa-solid fa-store"></i> ดูสินค้าทั้งหมดของร้านนี้ (${totalProdCount} ชิ้น) </button> </div> `; const viewAllBtn = content.querySelector("#btnViewAllStoreProducts"); if (viewAllBtn) { viewAllBtn.addEventListener("click", () => { state.activeFilters.sellerId = sellerId; const filterSellerDropdown = document.getElementById("filterSeller"); if (filterSellerDropdown) { filterSellerDropdown.value = sellerId; } modal.style.display = "none"; navigateTo("home"); renderCatalog(); const catalogElem = document.getElementById("catalog-start"); if (catalogElem) { catalogElem.scrollIntoView({ behavior: "smooth" }); } }); } modal.style.display = "flex"; } const closeDetailBtn = document.getElementById("closeProductDetailModalBtn"); if (closeDetailBtn) { closeDetailBtn.addEventListener("click", () => { const modal = document.getElementById("productDetailModal"); if (modal) modal.style.display = "none"; }); } const closeStoreProfileBtn = document.getElementById("closeStoreProfileModalBtn"); if (closeStoreProfileBtn) { closeStoreProfileBtn.addEventListener("click", () => { const modal = document.getElementById("storeProfileModal"); if (modal) modal.style.display = "none"; }); } // Global Backdrop Click to Close for All Modals (.modal-overlay) document.addEventListener("click", (e) => { if (e.target && e.target.classList.contains("modal-overlay")) { e.target.style.display = "none"; } }); function renderWishlist() { const grid = document.getElementById("wishlistGrid"); if (!grid) return; grid.innerHTML = ""; state.wishlist = db.getWishlist(); if (state.wishlist.length === 0) { grid.innerHTML = ` <div class="text-center w-100 py-5" style="grid-column: 1/-1;"> <i class="fa-solid fa-heart" style="font-size:3.5rem; color:var(--color-sky-light)"></i> <h3 class="mt-3">ยังไม่มีสินค้าที่ถูกใจ</h3> <p style="color:var(--color-sky-slate)">ลองกลับไปเลือกช้อปสินค้าชิ้นที่คุณสนใจและกดรูปหัวใจได้เลย</p> <button class="btn btn-sky mt-3" onclick="navigateTo('home')">กลับสู่ร้านค้า</button> </div> `; return; } const wishProducts = db.getProducts().filter(p => state.wishlist.includes(p.id) && p.active); wishProducts.forEach(p => { const totalStock = p.variants ? p.variants.reduce((sum, v) => sum + v.stock, 0) : 0; const card = document.createElement("div"); card.className = "product-card"; const placeholderImg = DEFAULT_IMAGE_PLACEHOLDERS[p.id] || DEFAULT_IMAGE_PLACEHOLDERS["default"]; const imgSrc = p.image || placeholderImg; const starHtml = "★".repeat(p.rating) + "☆".repeat(5 - p.rating); card.innerHTML = ` <button class="product-wishlist-btn active" data-id="${p.id}"> <i class="fa-solid fa-heart"></i> </button> <div class="product-image-wrapper"> <div class="product-badge-overlay"> ${totalStock === 0 ? '<span class="badge badge-danger">หมดชั่วคราว</span>' : ''} </div> <img src="${imgSrc}" class="product-img" alt="${p.name}"> </div> <div class="product-details"> <span class="product-category">${getCatName(p.category)}</span> <h4 class="product-title">${p.name}</h4> <div class="product-rating">${starHtml}</div> <div class="product-bottom"> <span class="product-price">฿${p.price.toLocaleString()}</span> <button class="btn btn-sky btn-sm view-detail-btn" data-id="${p.id}"> ดูรายละเอียด </button> </div> </div> `; card.querySelector(".view-detail-btn").addEventListener("click", () => { openProductDetail(p.id); }); card.querySelector(".product-wishlist-btn").addEventListener("click", () => { toggleWishlist(p.id); }); grid.appendChild(card); }); } // ========================================================================== // 11. ADMIN DASHBOARD OPERATIONS // ==========================================================================
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings