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.badge ? `
${product.badge}
` : ''}
${product.subcategory}
${product.name}
${product.description}
${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.unitPrice * item.quantity).toFixed(2)}
${item.unitPrice.toFixed(2)} each
${optionLines}
${extraLines}
`;
}).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');
});
}