// Main application logic
class App {
constructor() {
this.dbHelper = dbHelper;
this.cart = null; // will initialize after DB is ready
this.products = [];
this.categories = []; // will be loaded from categories.json
this.currentCategory = 'all';
this.searchTerm = '';
// Initialize modals
this.cartModal = new Modal('cartModal');
this.checkoutModal = new Modal('checkoutModal');
this.orderConfirmationModal = new Modal('orderConfirmationModal');
// Initialize event listeners
this.initEventListeners();
}
async init() {
try {
// Initialize database
await this.dbHelper.init();
// Initialize with mock data if needed
await this.dbHelper.initializeWithMockData();
// Create cart now that DB is available
this.cart = new Cart();
// Load categories
await this.loadCategories();
// Load products
await this.loadProducts();
// Load best sellers
await this.loadBestSellers();
// Update cart UI
this.cart.updateCartUI();
console.log('App initialized successfully');
} catch (error) {
console.error('Error initializing app:', error);
}
}
initEventListeners() {
// Cart icon click
const cartIcon = document.querySelector('.cart-icon');
if (cartIcon) {
cartIcon.addEventListener('click', () => {
this.cartModal.open();
});
}
// Continue shopping button
const continueShoppingBtn = document.getElementById('continueShopping');
if (continueShoppingBtn) {
continueShoppingBtn.addEventListener('click', () => {
this.cartModal.close();
});
}
// Checkout button
const checkoutBtn = document.getElementById('checkoutBtn');
if (checkoutBtn) {
checkoutBtn.addEventListener('click', async () => {
if (this.cart.items.length === 0) {
toast.show('ตะกร้าสินค้าว่าง', 'warning');
return;
}
this.cartModal.close();
this.checkoutModal.open();
this.updateCheckoutSummary();
await this.updatePaymentDetails();
});
}
// Back to cart button
const backToCartBtn = document.getElementById('backToCart');
if (backToCartBtn) {
backToCartBtn.addEventListener('click', () => {
this.checkoutModal.close();
this.cartModal.open();
});
}
// Place order button
const placeOrderBtn = document.getElementById('placeOrderBtn');
if (placeOrderBtn) {
placeOrderBtn.addEventListener('click', () => {
this.placeOrder();
});
}
// Continue shopping button in order confirmation
const continueShoppingBtn2 = document.getElementById('continueShoppingBtn');
if (continueShoppingBtn2) {
continueShoppingBtn2.addEventListener('click', () => {
this.orderConfirmationModal.close();
this.cart.clearCart();
});
}
// Delivery method change
const deliveryMethods = document.querySelectorAll('input[name="deliveryMethod"]');
deliveryMethods.forEach(method => {
method.addEventListener('change', () => {
this.updateCheckoutSummary();
this.updatePaymentDetails();
// show/hide address fields based on selection
const delivery = document.querySelector('input[name="deliveryMethod"]:checked').value;
const addressSection = document.getElementById('addressSection');
if (addressSection) {
if (delivery === 'pickup') {
addressSection.classList.add('hidden');
} else {
addressSection.classList.remove('hidden');
}
}
});
});
// Payment method change
const paymentMethods = document.querySelectorAll('input[name="paymentMethod"]');
paymentMethods.forEach(method => {
method.addEventListener('change', () => {
this.updatePaymentDetails();
});
});
// Listen for cart updates to refresh checkout summary and payment details (real-time QR update)
window.addEventListener('cart:updated', async () => {
this.updateCheckoutSummary();
await this.updatePaymentDetails();
});
// Category filter
const categoryItems = document.querySelectorAll('.category-item');
categoryItems.forEach(item => {
item.addEventListener('click', () => {
// Remove active class from all items
categoryItems.forEach(i => i.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Filter products
this.currentCategory = item.getAttribute('data-category');
this.filterProducts();
});
});
// Search functionality
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
this.searchTerm = e.target.value.toLowerCase();
this.filterProducts();
});
}
// Contact form
const contactForm = document.getElementById('contactForm');
if (contactForm) {
contactForm.addEventListener('submit', (e) => {
e.preventDefault();
this.handleContactForm();
});
}
// Mobile menu toggle
const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
if (mobileMenuToggle) {
mobileMenuToggle.addEventListener('click', () => {
const mainNav = document.querySelector('.main-nav');
mainNav.classList.toggle('active');
});
}
}
async loadCategories() {
try {
// Prefer categories from IndexedDB (keeps admin edits persistent)
const cats = await this.dbHelper.getAll('categories');
if (cats && cats.length > 0) {
this.categories = cats;
} else {
// fallback to mock file
try {
const response = await fetch('mock/categories.json');
if (response.ok) {
this.categories = await response.json();
} else {
this.categories = [];
}
} catch (err) {
console.warn('Could not fetch mock categories', err);
this.categories = [];
}
}
// If still empty, show only default 'ทั้งหมด'
if (!this.categories || this.categories.length === 0) {
this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}];
}
this.renderCategories();
} catch (error) {
console.error('Error loading categories:', error);
this.categories = [{id: 'all', name: 'ทั้งหมด', icon: 'bi-grid-3x3-gap'}];
this.renderCategories();
}
}
renderCategories() {
const categoryList = document.querySelector('.category-list');
if (categoryList && this.categories.length > 0) {
// Prepend "ทั้งหมด" category dynamically
const allCategory = `
<div class="category-item active" data-category="all">
<i class="bi bi-grid-3x3-gap"></i>
<span>ทั้งหมด</span>
</div>
`;
const categoryHTML = this.categories.map((cat) => `
<div class="category-item" data-category="${cat.id}">
<i class="bi ${cat.icon}"></i>
<span>${cat.name}</span>
</div>
`).join('');
categoryList.innerHTML = allCategory + categoryHTML;
// Re-attach event listeners for new category items
const categoryItems = document.querySelectorAll('.category-item');
categoryItems.forEach(item => {
item.addEventListener('click', () => {
// Remove active class from all items
categoryItems.forEach(i => i.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Filter products
this.currentCategory = item.getAttribute('data-category');
this.filterProducts();
});
});
}
}
async loadProducts() {
try {
const productsData = await this.dbHelper.getAll('products');
this.products = productsData.map(product => new Product(product));
this.renderProducts();
} catch (error) {
console.error('Error loading products:', error);
}
}
async loadBestSellers() {
try {
// Get some products to display as best sellers
const productsData = await this.dbHelper.getAll('products');
// Take first 4 products as best sellers (in a real app, this would be based on sales data)
const bestSellers = productsData.slice(0, 4).map(product => new Product(product));
const bestSellerGrid = document.getElementById('bestSellerGrid');
if (bestSellerGrid) {
bestSellerGrid.innerHTML = bestSellers.map(product => `
<div class="best-seller-card">
<div class="best-seller-image">
<img src="${product.image}" alt="${product.name}">
</div>
<div class="best-seller-name">${product.name}</div>
<div class="best-seller-price">฿${product.price}</div>
</div>
`).join('');
}
} catch (error) {
console.error('Error loading best sellers:', error);
}
}
renderProducts() {
const productGrid = document.getElementById('productGrid');
if (productGrid) {
productGrid.innerHTML = this.products.map(product => product.renderCard()).join('');
// Add event listeners to product cards
this.attachProductEventListeners();
}
}
filterProducts() {
let filteredProducts = this.products;
// Filter by category
if (this.currentCategory !== 'all') {
filteredProducts = filteredProducts.filter(product => product.category === this.currentCategory);
}
// Filter by search term
if (this.searchTerm) {
filteredProducts = filteredProducts.filter(product =>
product.name.toLowerCase().includes(this.searchTerm) ||
product.description.toLowerCase().includes(this.searchTerm)
);
}
// Render filtered products
const productGrid = document.getElementById('productGrid');
if (productGrid) {
if (filteredProducts.length === 0) {
productGrid.innerHTML = '<p>ไม่พบสินค้าที่ค้นหา</p>';
} else {
productGrid.innerHTML = filteredProducts.map(product => product.renderCard()).join('');
// Add event listeners to product cards
this.attachProductEventListeners();
}
}
}
attachProductEventListeners() {
// Decrease quantity buttons
document.querySelectorAll('.product-card .decrease-quantity').forEach(button => {
button.addEventListener('click', (e) => {
const input = e.target.nextElementSibling;
const currentValue = parseInt(input.value);
if (currentValue > 1) {
input.value = currentValue - 1;
}
});
});
// Increase quantity buttons
document.querySelectorAll('.product-card .increase-quantity').forEach(button => {
button.addEventListener('click', (e) => {
const input = e.target.previousElementSibling;
const currentValue = parseInt(input.value);
input.value = currentValue + 1;
});
});
// Add to cart buttons
document.querySelectorAll('.add-to-cart').forEach(button => {
button.addEventListener('click', async (e) => {
const productId = e.target.getAttribute('data-id');
const productCard = e.target.closest('.product-card');
const quantityInput = productCard.querySelector('.quantity-input');
const quantity = parseInt(quantityInput.value);
// Find product
const product = this.products.find(p => p.id === productId);
if (product) {
// Add to cart
const success = await this.cart.addItem(product, quantity);
if (success) {
toast.show('สินค้าถูกเพิ่มในตะกร้าแล้ว');
// Reset quantity input
quantityInput.value = 1;
} else {
toast.show('เกิดข้อผิดพลาดในการเพิ่มสินค้า', 'error');
}
}
});
});
}
updateCheckoutSummary() {
const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked');
const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50;
const subtotalElement = document.getElementById('checkoutSubtotal');
const shippingElement = document.getElementById('checkoutShipping');
const totalElement = document.getElementById('checkoutTotal');
if (subtotalElement) {
subtotalElement.textContent = `฿${this.cart.getSubtotal()}`;
}
if (shippingElement) {
shippingElement.textContent = `฿${shippingCost}`;
}
if (totalElement) {
totalElement.textContent = `฿${this.cart.getTotal(shippingCost)}`;
}
}
async updatePaymentDetails() {
const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value;
// Hide all payment details
const promptpayDetails = document.getElementById('promptpayDetails');
const bankDetails = document.getElementById('bankDetails');
if (promptpayDetails) promptpayDetails.classList.add('hidden');
if (bankDetails) bankDetails.classList.add('hidden');
// Show selected payment details
if (paymentMethod === 'promptpay') {
if (promptpayDetails) {
// populate QR dynamically using shop phone and amount
try {
const shopPhoneEntry = await this.dbHelper.getById('settings', 'shopPhone');
const shopPhone = shopPhoneEntry && shopPhoneEntry.value ? String(shopPhoneEntry.value) : null;
let idForPromptpay = '';
if (shopPhone) {
// normalize phone: digits only, convert leading 0 to 66
let digits = shopPhone.replace(/\D/g, '');
if (digits.length === 10 && digits.startsWith('0')) {
digits = '66' + digits.slice(1);
}
idForPromptpay = digits;
}
// compute amount including delivery method
const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked');
const shippingCost = deliveryMethod && deliveryMethod.value === 'pickup' ? 0 : 50;
const amount = this.cart.getTotal(shippingCost).toFixed(2);
// set QR image src (using promptpay.io service)
const img = promptpayDetails.querySelector('img');
const infoP = promptpayDetails.querySelector('p');
function isValidPromptpayId(id) {
// id should be digits only; valid if 11-digit starting with 66 (mobile) or 13-digit (national id)
if (!id) return false;
if (!/^\d+$/.test(id)) return false;
if (id.length === 11 && id.startsWith('66')) return true; // mobile normalized
if (id.length === 13) return true; // national id
return false;
}
if (img && idForPromptpay) {
// try a list of candidate IDs (normalized and some variants) and retry on error
const candidates = [];
if (isValidPromptpayId(idForPromptpay)) candidates.push(idForPromptpay);
// also try without 66 prefix (raw digits) and with leading 0 if orig provided
const raw = shopPhone ? shopPhone.replace(/\D/g, '') : '';
if (raw && !candidates.includes(raw)) candidates.push(raw);
if (raw && raw.length === 10 && raw.startsWith('0')) {
const normalized = '66' + raw.slice(1);
if (!candidates.includes(normalized)) candidates.unshift(normalized);
}
let attempt = 0;
function tryNext() {
if (attempt >= candidates.length) {
// all failed
img.removeAttribute('src');
if (infoP) infoP.textContent = 'ไม่สามารถโหลด PromptPay QR ได้ — โปรดตรวจสอบค่า PromptPay ID ใน settings (shopPhone)';
return;
}
const id = candidates[attempt];
const url = `https://promptpay.io/${id}/${encodeURIComponent(amount)}.png`;
img.onload = () => {
img.alt = `PromptPay QR - ฿${amount}`;
if (infoP) infoP.textContent = `สแกน QR Code เพื่อชำระเงิน ฿${amount}`;
};
img.onerror = () => {
attempt += 1;
tryNext();
};
img.src = url;
}
tryNext();
} else {
if (img) img.removeAttribute('src');
if (infoP) infoP.textContent = 'PromptPay ID ไม่ถูกต้องหรือยังไม่ตั้งค่าในระบบ (settings.shopPhone)';
}
promptpayDetails.classList.remove('hidden');
} catch (err) {
console.error('Error preparing PromptPay details:', err);
// still show the block so user sees instructions
promptpayDetails.classList.remove('hidden');
}
}
} else if (paymentMethod === 'bank') {
if (bankDetails) bankDetails.classList.remove('hidden');
}
}
async placeOrder() {
try {
// Get form values
const customerName = document.getElementById('customerName').value;
const customerPhone = document.getElementById('customerPhone').value;
const customerAddress = document.getElementById('customerAddress').value;
const customerProvince = document.getElementById('customerProvince').value;
const customerZipcode = document.getElementById('customerZipcode').value;
const deliveryMethod = document.querySelector('input[name="deliveryMethod"]:checked').value;
const paymentMethod = document.querySelector('input[name="paymentMethod"]:checked').value;
// Validate form: address fields only required if delivery selected (not pickup)
if (!customerName || !customerPhone) {
toast.show('กรุณากรอกชื่อและเบอร์โทรศัพท์', 'warning');
return;
}
if (deliveryMethod !== 'pickup') {
if (!customerAddress || !customerProvince || !customerZipcode) {
toast.show('กรุณาระบุที่อยู่สำหรับการจัดส่ง', 'warning');
return;
}
}
// Create order object
const order = {
customerName,
customerPhone,
// email removed intentionally
customerAddress: `${customerAddress}${customerAddress ? ', ' : ''}${customerProvince ? customerProvince + ' ' : ''}${customerZipcode ? customerZipcode : ''}`,
deliveryMethod,
paymentMethod,
items: this.cart.items.map(item => ({
id: item.id,
name: item.name,
price: item.price,
quantity: item.quantity
})),
subtotal: this.cart.getSubtotal(),
shipping: deliveryMethod === 'pickup' ? 0 : 50,
total: this.cart.getTotal(deliveryMethod === 'pickup' ? 0 : 50),
status: 'pending',
date: new Date().toISOString(),
type: 'online'
};
// Ensure order has an id (orders objectStore uses keyPath 'id')
if (!order.id) {
order.id = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
// Save order to IndexedDB
await this.dbHelper.add('orders', order);
// Close checkout modal
this.checkoutModal.close();
// Show order confirmation
this.showOrderConfirmation(order);
// Clear cart
await this.cart.clearCart();
console.log('Order placed successfully:', order);
} catch (error) {
console.error('Error placing order:', error);
toast.show('เกิดข้อผิดพลาดในการสั่งซื้อ', 'error');
}
}
showOrderConfirmation(order) {
// Update order confirmation modal
document.getElementById('orderNumber').textContent = `#${order.id}`;
document.getElementById('orderSubtotal').textContent = `฿${order.subtotal}`;
document.getElementById('orderShipping').textContent = `฿${order.shipping}`;
document.getElementById('orderTotal').textContent = `฿${order.total}`;
document.getElementById('orderCustomerName').textContent = order.customerName;
document.getElementById('orderCustomerPhone').textContent = order.customerPhone;
document.getElementById('orderCustomerAddress').textContent = order.customerAddress;
document.getElementById('orderDeliveryMethod').textContent = order.deliveryMethod === 'pickup' ? 'รับที่ร้าน' : 'จัดส่งถึงบ้าน';
document.getElementById('orderPaymentMethod').textContent = this.getPaymentMethodText(order.paymentMethod);
// Update order items
const orderSummaryItems = document.getElementById('orderSummaryItems');
orderSummaryItems.innerHTML = order.items.map(item => `
<div class="order-summary-item">
<span>${item.name} x ${item.quantity}</span>
<span>฿${item.price * item.quantity}</span>
</div>
`).join('');
// Open modal
this.orderConfirmationModal.open();
}
getPaymentMethodText(method) {
const methodMap = {
'promptpay': 'PromptPay',
'bank': 'โอนเงินผ่านธนาคาร',
'cod': 'เก็บเงินปลายทาง'
};
return methodMap[method] || method;
}
handleContactForm() {
// Get form values
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const message = document.getElementById('message').value;
// In a real app, this would send the form data to a server
// For now, we'll just show a success message
toast.show('ข้อความของคุณถูกส่งเรียบร้อยแล้ว');
// Reset form
document.getElementById('contactForm').reset();
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const app = new App();
app.init();
});