tailwind.config = { darkMode: 'class', theme: { extend: { colors: { emerald: { 50: '#ecfdf5', 100: '#d1fae5', 200: '#a7f3d0', 300: '#6ee7b7', 400: '#34d399', 500: '#10b981', 600: '#059669', 700: '#047857', 800: '#065f46', 900: '#064e3b', } }, fontFamily: { sans: ['Outfit', 'sans-serif'], display: ['Plus Jakarta Sans', 'sans-serif'], } } } } if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js') .then(reg => console.log('Service Worker registered successfully with scope: ', reg.scope)) .catch(err => console.error('Service Worker registration failed: ', err)); }); } // --- Mock Data with Options & Variations --- const products = [ // Restaurant Items { id: 'r1', name: 'Truffle Burger Deluxe', category: 'restaurant', subcategory: 'Mains', price: 14.99, description: 'Juicy Wagyu beef patty, black truffle aioli, caramelized onions, Swiss cheese, brioche bun.', image: 'https://images.unsplash.com/photo-1568901346375-23c9450c58cd?auto=format&fit=crop&w=600&q=80', badge: 'Chef Special', options: [ { name: 'Doneness', choices: [ { name: 'Medium Rare', priceModifier: 0 }, { name: 'Medium', priceModifier: 0 }, { name: 'Well Done', priceModifier: 0 } ] } ], extras: [ { name: 'Extra Cheese', price: 1.50 }, { name: 'Crispy Bacon', price: 2.00 }, { name: 'Gluten-Free Bun', price: 1.00 } ] }, { id: 'r2', name: 'Avocado Toast Deluxe', category: 'restaurant', subcategory: 'Breakfast', price: 11.50, description: 'Artisanal sourdough, smashed organic avocado, heirloom cherry tomatoes, feta cheese, poached egg.', image: 'https://images.unsplash.com/photo-1541532713592-79a0317b6b77?auto=format&fit=crop&w=600&q=80', badge: 'Healthy', options: [ { name: 'Egg Style', choices: [ { name: 'Poached', priceModifier: 0 }, { name: 'Fried', priceModifier: 0 }, { name: 'Scrambled', priceModifier: 1.00 } ] } ], extras: [ { name: 'Extra Avocado', price: 2.50 }, { name: 'Smoked Salmon', price: 4.00 } ] }, { id: 'r3', name: 'Spicy Tonkotsu Ramen', category: 'restaurant', subcategory: 'Mains', price: 16.00, description: 'Rich pork broth, chashu pork, soft-boiled egg, bamboo shoots, fresh scallions, house chili oil.', image: 'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=600&q=80', badge: 'Spicy', options: [ { name: 'Spice Level', choices: [ { name: 'Mild', priceModifier: 0 }, { name: 'Medium', priceModifier: 0 }, { name: 'Extra Hot', priceModifier: 1.00 } ] } ], extras: [ { name: 'Extra Chashu Pork', price: 3.00 }, { name: 'Soft Boiled Egg', price: 1.50 }, { name: 'Bamboo Shoots', price: 1.00 } ] }, { id: 'r4', name: 'Matcha Green Tea Latte', category: 'restaurant', subcategory: 'Drinks', price: 5.50, description: 'Ceremonial grade Japanese matcha, steamed oat milk, touched with organic raw honey.', image: 'https://images.unsplash.com/photo-1536256263959-770b48d82b0a?auto=format&fit=crop&w=600&q=80', badge: 'Popular', options: [ { name: 'Milk Choice', choices: [ { name: 'Oat Milk', priceModifier: 0.50 }, { name: 'Almond Milk', priceModifier: 0.50 }, { name: 'Whole Milk', priceModifier: 0 } ] }, { name: 'Sweetness', choices: [ { name: 'Regular', priceModifier: 0 }, { name: 'Less Sweet', priceModifier: 0 }, { name: 'Unsweetened', priceModifier: 0 } ] } ], extras: [ { name: 'Extra Matcha Shot', price: 1.50 }, { name: 'Add Boba Pearls', price: 1.00 } ] }, { id: 'r5', name: 'Crispy Caesar Salad', category: 'restaurant', subcategory: 'Salads', price: 12.90, description: 'Crisp romaine lettuce, garlic herb croutons, shaved parmesan, creamy house Caesar dressing.', image: 'https://images.unsplash.com/photo-1550304943-4f24f54ddde9?auto=format&fit=crop&w=600&q=80', badge: 'Fresh', options: [ { name: 'Dressing', choices: [ { name: 'Caesar', priceModifier: 0 }, { name: 'Vinaigrette', priceModifier: 0 }, { name: 'On the Side', priceModifier: 0 } ] } ], extras: [ { name: 'Grilled Chicken', price: 3.50 }, { name: 'Grilled Shrimp', price: 5.00 } ] }, { id: 'r6', name: 'Molten Chocolate Lava Cake', category: 'restaurant', subcategory: 'Desserts', price: 8.50, description: 'Warm chocolate cake with a luscious molten center, served with premium vanilla bean gelato.', image: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?auto=format&fit=crop&w=600&q=80', badge: 'Sweet', extras: [ { name: 'Extra Gelato Scoop', price: 2.00 }, { name: 'Fresh Berries', price: 1.50 } ] }, // Grocery Items { id: 'g1', name: 'Organic Strawberries', category: 'grocery', subcategory: 'Produce', price: 4.99, description: 'Sweet, juicy, locally sourced organic strawberries. Perfect for snacking or desserts.', image: 'https://images.unsplash.com/photo-1464965911861-746a04b4bca6?auto=format&fit=crop&w=600&q=80', badge: 'Organic' }, { id: 'g2', name: 'Unsweetened Almond Milk', category: 'grocery', subcategory: 'Dairy & Alternatives', price: 3.80, description: 'Creamy, plant-based, dairy-free milk alternative. Rich in calcium and vitamins.', image: 'https://images.unsplash.com/photo-1553456558-aff63285bdd1?auto=format&fit=crop&w=600&q=80', badge: 'Vegan' }, { id: 'g3', name: 'Fresh Hass Avocados', category: 'grocery', subcategory: 'Produce', price: 5.00, description: 'Pack of 3 perfectly ripe, ready-to-eat Hass avocados. Great for guacamole.', image: 'https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?auto=format&fit=crop&w=600&q=80', badge: 'Ripe' }, { id: 'g4', name: 'Artisan Sourdough Bread', category: 'grocery', subcategory: 'Bakery', price: 6.50, description: 'Freshly baked daily, crispy golden crust with a soft, airy, and tangy interior.', image: 'https://images.unsplash.com/photo-1549931319-a545dcf3bc73?auto=format&fit=crop&w=600&q=80', badge: 'Freshly Baked' }, { id: 'g5', name: 'Premium Ribeye Steak', category: 'grocery', subcategory: 'Meat & Seafood', price: 24.90, description: '100% grass-fed, beautifully marbled premium beef ribeye steak. Thick cut.', image: 'https://images.unsplash.com/photo-1603048588665-791ca8aea617?auto=format&fit=crop&w=600&q=80', badge: 'Premium', options: [ { name: 'Cut Thickness', choices: [ { name: 'Standard (1 inch)', priceModifier: 0 }, { name: 'Thick Cut (1.5 inch)', priceModifier: 5.00 } ] } ] } ]; // --- Application State --- let state = { currentTab: 'restaurant', activeSubcategory: 'All', searchQuery: '', cart: [], // Array of cart items: { cartId, product, quantity, selectedOptions, selectedExtras, unitPrice } location: 'Table #01', orderType: 'dine-in', // 'dine-in', 'takeaway', 'delivery' whatsappNumber: '1234567890', // Default fallback darkMode: false }; // --- Customization Modal State --- let currentCustomizingProduct = null; let customSelectedOptions = {}; let customSelectedExtras = []; let customQty = 1; // --- Initialization --- window.addEventListener('DOMContentLoaded', () => { parseUrlParameters(); loadSettings(); initTheme(); loadCartFromStorage(); renderSubcategories(); renderProducts(); updateCartUI(); setupPwaInstall(); }); // --- Theme Management --- function initTheme() { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { document.documentElement.classList.add('dark'); state.darkMode = true; } else { document.documentElement.classList.remove('dark'); state.darkMode = false; } document.getElementById('theme-toggle').addEventListener('click', () => { state.darkMode = !state.darkMode; if (state.darkMode) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); } }); } // --- URL Parameter Parsing --- function parseUrlParameters() { const params = new URLSearchParams(window.location.search); const table = params.get('table'); const address = params.get('address'); if (table) { state.location = `Table #${table}`; setOrderType('dine-in'); } else if (address) { state.location = decodeURIComponent(address); setOrderType('delivery'); } else { state.location = 'Table #01'; // Default setOrderType('dine-in'); } // Update UI Badges document.getElementById('location-badge-desktop').innerText = state.location; document.getElementById('location-badge-mobile').innerText = state.location; document.getElementById('cart-location-text').innerText = state.location; } // --- Settings Management --- function loadSettings() { const savedPhone = localStorage.getItem('whatsappNumber'); if (savedPhone) { state.whatsappNumber = savedPhone; } document.getElementById('merchant-phone').value = state.whatsappNumber; } // --- Settings Modal Actions --- function openSettingsModal() { document.getElementById('settings-modal').classList.remove('hidden'); } function closeSettingsModal() { document.getElementById('settings-modal').classList.add('hidden'); document.getElementById('merchant-phone-error').classList.add('hidden'); } function saveSettings() { const phoneInput = document.getElementById('merchant-phone').value.trim(); const phoneRegex = /^\+?[0-9]{7,15}$/; if (phoneRegex.test(phoneInput)) { state.whatsappNumber = phoneInput; localStorage.setItem('whatsappNumber', phoneInput); document.getElementById('merchant-phone-error').classList.add('hidden'); closeSettingsModal(); } else { document.getElementById('merchant-phone-error').classList.remove('hidden'); } } // --- Cart Persistence --- function saveCartToStorage() { localStorage.setItem('fresh_bites_cart', JSON.stringify(state.cart)); } // --- Load Cart --- function loadCartFromStorage() { const savedCart = localStorage.getItem('fresh_bites_cart'); if (savedCart) { try { state.cart = JSON.parse(savedCart); } catch (e) { console.error("Error parsing saved cart", e); state.cart = []; } } } // --- Tab Switching (Restaurant vs Grocery) --- function switchTab(tab) { state.currentTab = tab; state.activeSubcategory = 'All'; // Update Tab UI const restTab = document.getElementById('tab-restaurant'); const grocTab = document.getElementById('tab-grocery'); const sectionTitle = document.getElementById('section-title'); if (tab === 'restaurant') { restTab.className = "flex-1 flex items-center justify-center space-x-2 py-3 rounded-xl text-sm font-bold transition-all duration-300 bg-white dark:bg-slate-800 text-emerald-600 dark:text-emerald-400 shadow-sm"; grocTab.className = "flex-1 flex items-center justify-center space-x-2 py-3 rounded-xl text-sm font-bold transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"; sectionTitle.innerText = "Restaurant Menu"; } else { grocTab.className = "flex-1 flex items-center justify-center space-x-2 py-3 rounded-xl text-sm font-bold transition-all duration-300 bg-white dark:bg-slate-800 text-emerald-600 dark:text-emerald-400 shadow-sm"; restTab.className = "flex-1 flex items-center justify-center space-x-2 py-3 rounded-xl text-sm font-bold transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"; sectionTitle.innerText = "Grocery Store"; } renderSubcategories(); renderProducts(); } // --- Subcategory Chips --- function getSubcategories() { const filtered = products.filter(p => p.category === state.currentTab); const subs = ['All', ...new Set(filtered.map(p => p.subcategory))]; return subs; } function renderSubcategories() { const container = document.getElementById('subcategory-container'); const subs = getSubcategories(); container.innerHTML = subs.map(sub => { const isActive = state.activeSubcategory === sub; return ` `; }).join(''); } function filterSubcategory(sub) { state.activeSubcategory = sub; renderSubcategories(); renderProducts(); } // --- Search Handling --- function handleSearch(query) { state.searchQuery = query.toLowerCase().trim(); renderProducts(); } // --- Product Rendering --- function renderProducts() { const grid = document.getElementById('product-grid'); const emptyState = document.getElementById('empty-state'); // Filter products const filtered = products.filter(p => { const matchesTab = p.category === state.currentTab; const matchesSub = state.activeSubcategory === 'All' || p.subcategory === state.activeSubcategory; const matchesSearch = p.name.toLowerCase().includes(state.searchQuery) || p.description.toLowerCase().includes(state.searchQuery); return matchesTab && matchesSub && matchesSearch; }); // Update count badge document.getElementById('items-count').innerText = `${filtered.length} item${filtered.length !== 1 ? 's' : ''} available`; if (filtered.length === 0) { grid.classList.add('hidden'); emptyState.classList.remove('hidden'); emptyState.classList.add('flex'); return; } grid.classList.remove('hidden'); emptyState.classList.add('hidden'); emptyState.classList.remove('flex'); grid.innerHTML = filtered.map(product => { // Count total of this product in cart (regardless of options) const totalQtyInCart = state.cart .filter(item => item.product.id === product.id) .reduce((sum, item) => sum + item.quantity, 0); return `
${product.name} ${product.badge ? ` ${product.badge} ` : ''}
${product.subcategory}

${product.name}

${product.description}

${product.price.toFixed(2)}
${totalQtyInCart > 0 ? ` ` : ` `}
`; }).join(''); } // --- Product Click Router (Direct Add vs Customization Modal) --- function handleProductClick(productId) { const product = products.find(p => p.id === productId); if (!product) return; // If product has options or extras, open customization modal if ((product.options && product.options.length > 0) || (product.extras && product.extras.length > 0)) { openCustomizationModal(product); } else { // Direct add to cart addToCartDirect(product); } } // --- Direct Add (No Options) --- function addToCartDirect(product) { const cartId = product.id; // Simple ID as cartId const existingItem = state.cart.find(item => item.cartId === cartId); if (existingItem) { existingItem.quantity += 1; } else { state.cart.push({ cartId, product, quantity: 1, selectedOptions: {}, selectedExtras: [], unitPrice: product.price }); } saveCartToStorage(); updateCartUI(); renderProducts(); } // --- Customization Modal Logic --- function openCustomizationModal(product) { currentCustomizingProduct = product; customQty = 1; customSelectedOptions = {}; customSelectedExtras = []; // Set default options if (product.options) { product.options.forEach(opt => { customSelectedOptions[opt.name] = opt.choices[0]; }); } // Populate static info document.getElementById('custom-modal-image').src = product.image; document.getElementById('custom-modal-image').alt = product.name; document.getElementById('custom-modal-subcategory').innerText = product.subcategory; document.getElementById('custom-modal-title').innerText = product.name; document.getElementById('custom-modal-description').innerText = product.description; renderCustomOptions(); updateCustomModalPrice(); document.getElementById('customization-modal').classList.remove('hidden'); } function closeCustomizationModal() { document.getElementById('customization-modal').classList.add('hidden'); currentCustomizingProduct = null; } function renderCustomOptions() { const container = document.getElementById('custom-options-container'); let html = ''; if (!currentCustomizingProduct) return; // Render Single-Choice Options (Radio Buttons) if (currentCustomizingProduct.options) { currentCustomizingProduct.options.forEach(opt => { html += `

${opt.name}

`; opt.choices.forEach(choice => { const isChecked = customSelectedOptions[opt.name]?.name === choice.name; const priceLabel = choice.priceModifier > 0 ? `(+${choice.priceModifier.toFixed(2)})` : ''; html += ` `; }); html += `
`; }); } // Render Multi-Choice Extras (Checkboxes) if (currentCustomizingProduct.extras) { html += `

Add Extras

`; currentCustomizingProduct.extras.forEach(extra => { const isChecked = customSelectedExtras.some(e => e.name === extra.name); html += ` `; }); html += `
`; } container.innerHTML = html; } function selectOption(optionName, choiceName) { const option = currentCustomizingProduct.options.find(o => o.name === optionName); const choice = option.choices.find(c => c.name === choiceName); customSelectedOptions[optionName] = choice; renderCustomOptions(); updateCustomModalPrice(); } function toggleExtra(extraName) { const extra = currentCustomizingProduct.extras.find(e => e.name === extraName); const index = customSelectedExtras.findIndex(e => e.name === extraName); if (index > -1) { customSelectedExtras.splice(index, 1); } else { customSelectedExtras.push(extra); } renderCustomOptions(); updateCustomModalPrice(); } function adjustCustomQty(change) { customQty = Math.max(1, customQty + change); document.getElementById('custom-qty-display').innerText = customQty; updateCustomModalPrice(); } function calculateCustomUnitPrice() { if (!currentCustomizingProduct) return 0; let price = currentCustomizingProduct.price; // Add option modifiers Object.values(customSelectedOptions).forEach(choice => { price += choice.priceModifier; }); // Add extras customSelectedExtras.forEach(extra => { price += extra.price; }); return price; } function updateCustomModalPrice() { const unitPrice = calculateCustomUnitPrice(); const total = unitPrice * customQty; document.getElementById('custom-total-price').innerText = `${total.toFixed(2)}`; } function confirmCustomization() { if (!currentCustomizingProduct) return; const unitPrice = calculateCustomUnitPrice(); // Generate unique cart ID based on product ID + selected options + selected extras const optKeys = Object.keys(customSelectedOptions).sort(); const optString = optKeys.map(k => `${k}:${customSelectedOptions[k].name}`).join('|'); const extString = customSelectedExtras.map(e => e.name).sort().join('|'); const cartId = `${currentCustomizingProduct.id}_opts[${optString}]_exts[${extString}]`; const existingItem = state.cart.find(item => item.cartId === cartId); if (existingItem) { existingItem.quantity += customQty; } else { state.cart.push({ cartId, product: currentCustomizingProduct, quantity: customQty, selectedOptions: { ...customSelectedOptions }, selectedExtras: [ ...customSelectedExtras ], unitPrice: unitPrice }); } saveCartToStorage(); updateCartUI(); renderProducts(); closeCustomizationModal(); } // --- Cart Actions --- function updateQuantity(cartId, change) { const cartItemIndex = state.cart.findIndex(item => item.cartId === cartId); if (cartItemIndex > -1) { state.cart[cartItemIndex].quantity += change; if (state.cart[cartItemIndex].quantity <= 0) { state.cart.splice(cartItemIndex, 1); } saveCartToStorage(); updateCartUI(); renderProducts(); } } // Overloaded function for direct product card quantity controls function updateQuantityDirect(productId, change) { // Find direct cart item (no options/extras) const cartItemIndex = state.cart.findIndex(item => item.product.id === productId && Object.keys(item.selectedOptions).length === 0 && item.selectedExtras.length === 0); if (cartItemIndex > -1) { state.cart[cartItemIndex].quantity += change; if (state.cart[cartItemIndex].quantity <= 0) { state.cart.splice(cartItemIndex, 1); } saveCartToStorage(); updateCartUI(); renderProducts(); } } function removeFromCart(cartId) { state.cart = state.cart.filter(item => item.cartId !== cartId); saveCartToStorage(); updateCartUI(); renderProducts(); } function toggleCart(open) { const drawer = document.getElementById('cart-drawer'); if (open) { drawer.classList.remove('hidden'); setTimeout(() => { drawer.querySelector('.pointer-events-auto').classList.remove('translate-x-full'); }, 50); } else { drawer.querySelector('.pointer-events-auto').classList.add('translate-x-full'); setTimeout(() => { drawer.classList.add('hidden'); }, 500); } } // --- Cart UI Updates --- function updateCartUI() { const cartCountBadge = document.getElementById('cart-count-badge'); const cartCountBadgeMobile = document.getElementById('cart-count-badge-mobile'); const cartItemsContainer = document.getElementById('cart-items-container'); const checkoutBtn = document.getElementById('checkout-btn'); const totalItems = state.cart.reduce((sum, item) => sum + item.quantity, 0); // Update Badges if (totalItems > 0) { cartCountBadge.innerText = totalItems; cartCountBadge.classList.remove('scale-0'); cartCountBadge.classList.add('scale-100'); cartCountBadgeMobile.innerText = totalItems; checkoutBtn.removeAttribute('disabled'); } else { cartCountBadge.classList.remove('scale-100'); cartCountBadge.classList.add('scale-0'); cartCountBadgeMobile.innerText = 0; checkoutBtn.setAttribute('disabled', 'true'); } // Render Cart Items if (state.cart.length === 0) { cartItemsContainer.innerHTML = `

Your basket is empty

Browse our menu or store and add delicious items to get started.

`; } else { cartItemsContainer.innerHTML = state.cart.map(item => { // Format selected options & extras for display const optionLines = Object.entries(item.selectedOptions).map(([key, choice]) => { return `• ${key}: ${choice.name}`; }).join(''); const extraLines = item.selectedExtras.map(extra => { return `• Extra: ${extra.name} (+${extra.price.toFixed(2)})`; }).join(''); return `
${item.product.name}

${item.product.name}

${(item.unitPrice * item.quantity).toFixed(2)}

${item.unitPrice.toFixed(2)} each

${optionLines} ${extraLines}
${item.quantity}
`; }).join(''); } // Calculate Totals const subtotal = state.cart.reduce((sum, item) => sum + (item.unitPrice * item.quantity), 0); const tax = subtotal * 0.05; const total = subtotal + tax; document.getElementById('cart-subtotal').innerText = `${subtotal.toFixed(2)}`; document.getElementById('cart-tax').innerText = `${tax.toFixed(2)}`; document.getElementById('cart-total').innerText = `${total.toFixed(2)}`; document.getElementById('modal-order-total').innerText = `${total.toFixed(2)}`; } // --- Order Type Selector & Checkout Modal --- function setOrderType(type) { state.orderType = type; // Update Selector Buttons const btnDineIn = document.getElementById('type-dine-in'); const btnTakeaway = document.getElementById('type-takeaway'); const btnDelivery = document.getElementById('type-delivery'); const activeClass = "py-2 text-xs font-bold rounded-lg transition-all bg-white dark:bg-slate-700 text-emerald-600 dark:text-emerald-400 shadow-sm"; const inactiveClass = "py-2 text-xs font-bold rounded-lg transition-all text-slate-600 dark:text-slate-400"; btnDineIn.className = type === 'dine-in' ? activeClass : inactiveClass; btnTakeaway.className = type === 'takeaway' ? activeClass : inactiveClass; btnDelivery.className = type === 'delivery' ? activeClass : inactiveClass; // Update Summary Text document.getElementById('summary-order-type').innerText = type.replace('-', ' '); // Update Location Input Label & Placeholder const locationLabel = document.getElementById('location-label'); const locationInput = document.getElementById('customer-location'); const locationContainer = document.getElementById('location-input-container'); if (type === 'dine-in') { locationContainer.classList.remove('hidden'); locationLabel.innerText = 'Table Number *'; locationInput.placeholder = 'e.g., Table #05'; locationInput.required = true; locationInput.value = state.location.startsWith('Table #') ? state.location : 'Table #01'; } else if (type === 'takeaway') { locationContainer.classList.remove('hidden'); locationLabel.innerText = 'Estimated Pickup Time'; locationInput.placeholder = 'e.g., 15 mins, 6:30 PM'; locationInput.required = false; locationInput.value = ''; } else if (type === 'delivery') { locationContainer.classList.remove('hidden'); locationLabel.innerText = 'Delivery Address *'; locationInput.placeholder = 'e.g., 123 Main St, Apt 4B'; locationInput.required = true; locationInput.value = !state.location.startsWith('Table #') ? state.location : ''; } } function openCheckoutModal() { toggleCart(false); document.getElementById('checkout-modal').classList.remove('hidden'); setOrderType(state.orderType); // Refresh UI states } function closeCheckoutModal() { document.getElementById('checkout-modal').classList.add('hidden'); // Clear validation errors document.getElementById('name-error').classList.add('hidden'); document.getElementById('phone-error').classList.add('hidden'); document.getElementById('location-error').classList.add('hidden'); } // --- Input Validation & WhatsApp Redirect Engine --- function submitOrder(event) { event.preventDefault(); const nameInput = document.getElementById('customer-name'); const phoneInput = document.getElementById('customer-phone'); const locationInput = document.getElementById('customer-location'); const notes = document.getElementById('order-notes').value.trim(); const name = nameInput.value.trim(); const phone = phoneInput.value.trim(); const location = locationInput.value.trim(); let isValid = true; // Validate Name if (name.length < 2) { document.getElementById('name-error').classList.remove('hidden'); nameInput.classList.add('border-rose-500', 'focus:ring-rose-500'); isValid = false; } else { document.getElementById('name-error').classList.add('hidden'); nameInput.classList.remove('border-rose-500', 'focus:ring-rose-500'); } // Validate Phone (7 to 15 digits, optional leading +) const phoneRegex = /^\+?[0-9]{7,15}$/; if (!phoneRegex.test(phone)) { document.getElementById('phone-error').classList.remove('hidden'); phoneInput.classList.add('border-rose-500', 'focus:ring-rose-500'); isValid = false; } else { document.getElementById('phone-error').classList.add('hidden'); phoneInput.classList.remove('border-rose-500', 'focus:ring-rose-500'); } // Validate Location (if required) if (locationInput.required && !location) { document.getElementById('location-error').classList.remove('hidden'); locationInput.classList.add('border-rose-500', 'focus:ring-rose-500'); isValid = false; } else { document.getElementById('location-error').classList.add('hidden'); locationInput.classList.remove('border-rose-500', 'focus:ring-rose-500'); } if (!isValid) return; // Calculate Totals const subtotal = state.cart.reduce((sum, item) => sum + (item.unitPrice * item.quantity), 0); const tax = subtotal * 0.05; const total = subtotal + tax; // Format WhatsApp Message (Markdown) let message = `*Fresh Bites Order* 🍔🥦\n`; message += `-------------------------\n`; message += `*Order Type:* ${state.orderType.toUpperCase().replace('-', ' ')}\n`; message += `*Customer:* ${name}\n`; message += `*Phone:* ${phone}\n`; if (state.orderType !== 'takeaway' || location) { message += `*Location/Time:* ${location}\n`; } if (notes) { message += `*Notes:* ${notes}\n`; } message += `-------------------------\n`; message += `*Items Ordered:*\n`; state.cart.forEach(item => { message += `• ${item.quantity}x *${item.product.name}*\n`; // Include selected options Object.entries(item.selectedOptions).forEach(([key, choice]) => { message += ` _└ ${key}: ${choice.name}_\n`; }); // Include selected extras item.selectedExtras.forEach(extra => { message += ` _└ Extra: ${extra.name} (+${extra.price.toFixed(2)})_\n`; }); message += ` _Price: ${(item.unitPrice * item.quantity).toFixed(2)}_\n`; }); message += `-------------------------\n`; message += `*Subtotal:* ${subtotal.toFixed(2)}\n`; message += `*Tax (5%):* ${tax.toFixed(2)}\n`; message += `*Total Amount:* ${total.toFixed(2)}\n`; message += `-------------------------\n`; message += `Sent via Fresh Bites PWA`; // WhatsApp Redirection Logic const cleanPhone = state.whatsappNumber.replace(/[^0-9]/g, ''); const encodedMessage = encodeURIComponent(message); const whatsappUrl = `https://wa.me/${cleanPhone}?text=${encodedMessage}`; const link = document.createElement('a'); link.href = whatsappUrl; link.target = '_blank'; link.rel = 'noopener noreferrer'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clear Cart & Close Modal state.cart = []; saveCartToStorage(); updateCartUI(); renderProducts(); closeCheckoutModal(); } // --- PWA Setup & Asset Generation --- function togglePwaModal(open) { const modal = document.getElementById('pwa-modal'); if (open) { modal.classList.remove('hidden'); } else { modal.classList.add('hidden'); } } function getManifestContent() { return JSON.stringify({ "name": "Fresh Bites QR Ordering", "short_name": "Fresh Bites", "description": "Order delicious food and fresh groceries directly via WhatsApp.", "start_url": "/?table=Table01", "display": "standalone", "background_color": "#ffffff", "theme_color": "#059669", "icons": [ { "src": "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?auto=format&fit=crop&w=192&h=192&q=80", "sizes": "192x192", "type": "image/jpeg" }, { "src": "https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?auto=format&fit=crop&w=512&h=512&q=80", "sizes": "512x512", "type": "image/jpeg" } ] }, null, 2); } function getServiceWorkerContent() { return `const CACHE_NAME = 'fresh-bites-v1'; const ASSETS = [ '/', '/index.html', '/manifest.json' ]; self.addEventListener('install', (e) => { e.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(ASSETS); }) ); }); self.addEventListener('activate', (e) => { e.waitUntil( caches.keys().then((keys) => { return Promise.all( keys.map((key) => { if (key !== CACHE_NAME) { return caches.delete(key); } }) ); }) ); }); self.addEventListener('fetch', (e) => { e.respondWith( caches.match(e.request).then((cachedResponse) => { return cachedResponse || fetch(e.request); }) ); });`; } function downloadFile(filename, text) { const element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } // --- PWA Installation Prompt --- let deferredPrompt; function setupPwaInstall() { const installBtn = document.getElementById('pwa-install-btn'); window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; installBtn.classList.remove('hidden'); installBtn.classList.add('flex'); }); installBtn.addEventListener('click', () => { if (deferredPrompt) { deferredPrompt.prompt(); deferredPrompt.userChoice.then((choiceResult) => { if (choiceResult.outcome === 'accepted') { console.log('User accepted the install prompt'); } else { console.log('User dismissed the install prompt'); } deferredPrompt = null; installBtn.classList.add('hidden'); }); } }); window.addEventListener('appinstalled', () => { console.log('PWA was installed'); installBtn.classList.add('hidden'); }); }