| |
|
|
| let menuData = null; |
| let editingItemId = null; |
|
|
| document.addEventListener('DOMContentLoaded', () => checkAuth()); |
|
|
| |
| async function checkAuth() { |
| try { |
| const res = await fetch('/api/auth'); |
| const data = await res.json(); |
| data.authenticated ? showDashboard() : showLogin(); |
| } catch { showLogin(); } |
| } |
|
|
| function showLogin() { |
| document.getElementById('loginScreen').style.display = ''; |
| document.getElementById('dashboard').style.display = 'none'; |
| document.getElementById('loginForm').addEventListener('submit', handleLogin); |
| } |
|
|
| function showDashboard() { |
| document.getElementById('loginScreen').style.display = 'none'; |
| document.getElementById('dashboard').style.display = ''; |
| setupDashboard(); |
| } |
|
|
| async function handleLogin(e) { |
| e.preventDefault(); |
| const pw = document.getElementById('password').value; |
| const errEl = document.getElementById('loginError'); |
| try { |
| const res = await fetch('/api/login', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ password: pw }) |
| }); |
| const data = await res.json(); |
| if (data.success) { errEl.textContent = ''; showDashboard(); } |
| else { errEl.textContent = data.error || 'Invalid password'; } |
| } catch { errEl.textContent = 'Connection error. Server running?'; } |
| } |
|
|
| |
| async function setupDashboard() { |
| await loadMenuData(); |
| setupNav(); |
| setupModal(); |
| setupForms(); |
| showSection('menu'); |
|
|
| document.getElementById('logoutBtn').addEventListener('click', async () => { |
| await fetch('/api/logout', { method: 'POST' }); |
| location.reload(); |
| }); |
| document.getElementById('addItemBtn').addEventListener('click', () => openModal()); |
| } |
|
|
| async function loadMenuData() { |
| const res = await fetch('/api/menu'); |
| menuData = await res.json(); |
| } |
|
|
| |
| function setupNav() { |
| document.getElementById('topbarNav').addEventListener('click', (e) => { |
| const btn = e.target.closest('.tb-nav-item'); |
| if (!btn) return; |
| document.querySelectorAll('.tb-nav-item').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| showSection(btn.dataset.section); |
| }); |
| } |
|
|
| function showSection(section) { |
| document.querySelectorAll('.panel').forEach(p => p.style.display = 'none'); |
|
|
| switch (section) { |
| case 'menu': |
| document.getElementById('menuSection').style.display = ''; |
| renderTable(); |
| break; |
| case 'categories': |
| document.getElementById('categoriesSection').style.display = ''; |
| renderCategories(); |
| break; |
| case 'info': |
| document.getElementById('infoSection').style.display = ''; |
| renderInfo(); |
| break; |
| case 'tables': |
| document.getElementById('tablesSection').style.display = ''; |
| setupTablesSection(); |
| break; |
| case 'settings': |
| document.getElementById('settingsSection').style.display = ''; |
| break; |
| } |
| } |
|
|
| |
| function renderTable() { |
| const tbody = document.getElementById('menuTableBody'); |
| const empty = document.getElementById('menuEmpty'); |
| const items = menuData?.items || []; |
|
|
| if (items.length === 0) { tbody.innerHTML = ''; empty.style.display = ''; return; } |
| empty.style.display = 'none'; |
|
|
| const cats = {}; |
| (menuData.categories || []).forEach(c => { cats[c.id] = c.name; }); |
|
|
| tbody.innerHTML = items.map(item => { |
| const hasImg = item.image && item.image.startsWith('http'); |
| const thumbImg = hasImg |
| ? `<img src="${esc(item.image)}" alt="" loading="lazy" onerror="this.style.display='none'">` |
| : ''; |
| const thumbSvg = adminSvgIcon(item.category); |
|
|
| return ` |
| <tr> |
| <td> |
| <div class="item-cell"> |
| <span class="item-thumb">${thumbSvg}${thumbImg}</span> |
| <div> |
| <div class="item-name">${esc(item.name)}</div> |
| <div class="item-desc">${esc((item.description || '').slice(0, 50))}</div> |
| </div> |
| </div> |
| </td> |
| <td>${cats[item.category] || item.category}</td> |
| <td>₹${item.price.toFixed(0)}</td> |
| <td> |
| <span class="badge ${item.available ? 'badge-available' : 'badge-soldout'}"> |
| ${item.available ? 'Available' : 'Sold out'} |
| </span> |
| </td> |
| <td> |
| <div class="actions"> |
| <button class="act edit" data-id="${item.id}" title="Edit">✎</button> |
| <button class="act toggle" data-id="${item.id}" title="${item.available ? 'Mark sold out' : 'Mark available'}">${item.available ? '⊘' : '✓'}</button> |
| <button class="act danger" data-id="${item.id}" title="Delete">✕</button> |
| </div> |
| </td> |
| </tr> |
| `}).join(''); |
|
|
| tbody.addEventListener('click', (e) => { |
| const btn = e.target.closest('.act'); |
| if (!btn) return; |
| const id = btn.dataset.id; |
| const item = items.find(i => i.id === id); |
| if (!item) return; |
| if (btn.classList.contains('edit')) openModal(item); |
| else if (btn.classList.contains('toggle')) toggleItem(item); |
| else if (btn.classList.contains('danger')) deleteItem(id); |
| }); |
| } |
|
|
| |
| function setupModal() { |
| const modal = document.getElementById('itemModal'); |
| document.getElementById('modalClose').addEventListener('click', closeModal); |
| document.getElementById('modalCancel').addEventListener('click', closeModal); |
| modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); |
| document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); }); |
| document.getElementById('itemForm').addEventListener('submit', saveItem); |
| } |
|
|
| function openModal(item = null) { |
| editingItemId = item?.id || null; |
| document.getElementById('modalTitle').textContent = item ? 'Edit item' : 'Add item'; |
| document.getElementById('modalSave').textContent = item ? 'Update' : 'Add'; |
|
|
| const select = document.getElementById('itemCategory'); |
| select.innerHTML = (menuData.categories || []) |
| .filter(c => c.id !== 'all') |
| .map(c => `<option value="${c.id}">${c.name}</option>`) |
| .join(''); |
|
|
| document.getElementById('itemId').value = item?.id || ''; |
| document.getElementById('itemName').value = item?.name || ''; |
| document.getElementById('itemPrice').value = item?.price || ''; |
| document.getElementById('itemCategory').value = item?.category || ''; |
| document.getElementById('itemDescription').value = item?.description || ''; |
| document.getElementById('itemImage').value = (item?.image && item.image.startsWith('http')) ? item.image : ''; |
| document.getElementById('itemAvailable').checked = item ? item.available : true; |
| document.getElementById('itemFeatured').checked = item ? item.featured : false; |
|
|
| document.getElementById('itemModal').classList.add('open'); |
| setTimeout(() => document.getElementById('itemName').focus(), 150); |
| } |
|
|
| function closeModal() { |
| document.getElementById('itemModal').classList.remove('open'); |
| editingItemId = null; |
| document.getElementById('itemForm').reset(); |
| } |
|
|
| async function saveItem(e) { |
| e.preventDefault(); |
| const id = document.getElementById('itemId').value; |
|
|
| const item = { |
| name: document.getElementById('itemName').value.trim(), |
| price: parseFloat(document.getElementById('itemPrice').value), |
| category: document.getElementById('itemCategory').value, |
| description: document.getElementById('itemDescription').value.trim(), |
| image: document.getElementById('itemImage').value.trim() || null, |
| available: document.getElementById('itemAvailable').checked, |
| featured: document.getElementById('itemFeatured').checked |
| }; |
|
|
| if (!item.name || isNaN(item.price) || !item.category) { |
| return showToast('Fill in name, price, and category.', 'error'); |
| } |
|
|
| try { |
| const url = id ? `/api/menu/${id}` : '/api/menu'; |
| const method = id ? 'PUT' : 'POST'; |
| const res = await fetch(url, { |
| method, |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(item) |
| }); |
| if (!res.ok) throw new Error((await res.json()).error || 'Failed'); |
| await loadMenuData(); |
| renderTable(); |
| closeModal(); |
| showToast(id ? 'Item updated.' : 'Item added.', 'success'); |
| } catch (err) { |
| showToast(err.message || 'Error saving', 'error'); |
| } |
| } |
|
|
| async function toggleItem(item) { |
| try { |
| const res = await fetch(`/api/menu/${item.id}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ available: !item.available }) |
| }); |
| if (!res.ok) throw new Error('Failed'); |
| await loadMenuData(); |
| renderTable(); |
| showToast(`Marked as ${!item.available ? 'available' : 'sold out'}.`, 'success'); |
| } catch { showToast('Error updating', 'error'); } |
| } |
|
|
| async function deleteItem(id) { |
| if (!confirm('Delete this item? This cannot be undone.')) return; |
| try { |
| const res = await fetch(`/api/menu/${id}`, { method: 'DELETE' }); |
| if (!res.ok) throw new Error('Failed'); |
| await loadMenuData(); |
| renderTable(); |
| showToast('Item deleted.', 'success'); |
| } catch { showToast('Error deleting', 'error'); } |
| } |
|
|
| |
| function renderCategories() { |
| const container = document.getElementById('categoriesEditor'); |
| const cats = menuData?.categories || []; |
| container.innerHTML = cats.map((cat, i) => ` |
| <div class="cat-row"> |
| <input type="text" class="cat-icon" value="${esc(cat.icon || '')}" data-index="${i}" data-field="icon" placeholder="Icon"> |
| <input type="text" class="cat-name" value="${esc(cat.name || '')}" data-index="${i}" data-field="name" placeholder="Name"> |
| <button type="button" class="cat-remove" data-index="${i}">✕</button> |
| </div> |
| `).join(''); |
|
|
| container.querySelectorAll('.cat-remove').forEach(btn => { |
| btn.addEventListener('click', () => { |
| menuData.categories.splice(parseInt(btn.dataset.index), 1); |
| renderCategories(); |
| }); |
| }); |
|
|
| document.getElementById('addCategoryBtn').onclick = () => { |
| menuData.categories.push({ id: '', name: '', icon: '📌' }); |
| renderCategories(); |
| }; |
|
|
| document.getElementById('saveCategoriesBtn').onclick = async () => { |
| const rows = container.querySelectorAll('.cat-row'); |
| const categories = []; |
| rows.forEach(row => { |
| const icon = row.querySelector('.cat-icon').value.trim(); |
| const name = row.querySelector('.cat-name').value.trim(); |
| if (name) categories.push({ id: name.toLowerCase().replace(/\s+/g, '-'), name, icon: icon || '📌' }); |
| }); |
| try { |
| const res = await fetch('/api/categories', { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ categories }) |
| }); |
| if (!res.ok) throw new Error('Failed'); |
| await loadMenuData(); |
| renderCategories(); |
| showToast('Categories saved.', 'success'); |
| } catch { showToast('Error saving', 'error'); } |
| }; |
| } |
|
|
| |
| function renderInfo() { |
| const info = menuData?.info || {}; |
| document.getElementById('infoName').value = info.name || ''; |
| document.getElementById('infoTagline').value = info.tagline || ''; |
| document.getElementById('infoAddress').value = info.address || ''; |
| document.getElementById('infoPhone').value = info.phone || ''; |
| document.getElementById('infoEmail').value = info.email || ''; |
|
|
| const hours = info.hours || []; |
| const editor = document.getElementById('hoursEditor'); |
| editor.innerHTML = hours.map((h, i) => ` |
| <div class="hours-row"> |
| <input type="text" class="hours-days" value="${esc(h.days || '')}" data-index="${i}" data-field="days" placeholder="e.g. Mon—Fri"> |
| <input type="text" class="hours-time" value="${esc(h.time || '')}" data-index="${i}" data-field="time" placeholder="7:00 AM — 7:00 PM"> |
| <button type="button" class="cat-remove hours-del" data-index="${i}">✕</button> |
| </div> |
| `).join(''); |
|
|
| editor.querySelectorAll('.hours-del').forEach(btn => { |
| btn.addEventListener('click', () => { |
| menuData.info.hours.splice(parseInt(btn.dataset.index), 1); |
| renderInfo(); |
| }); |
| }); |
|
|
| document.getElementById('addHoursBtn').onclick = () => { |
| if (!menuData.info.hours) menuData.info.hours = []; |
| menuData.info.hours.push({ days: '', time: '' }); |
| renderInfo(); |
| }; |
| } |
|
|
| |
| function setupForms() { |
| document.getElementById('infoForm').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const rows = document.querySelectorAll('#hoursEditor .hours-row'); |
| const hours = []; |
| rows.forEach(row => { |
| const days = row.querySelector('.hours-days').value.trim(); |
| const time = row.querySelector('.hours-time').value.trim(); |
| if (days) hours.push({ days, time }); |
| }); |
|
|
| const info = { |
| name: document.getElementById('infoName').value.trim(), |
| tagline: document.getElementById('infoTagline').value.trim(), |
| address: document.getElementById('infoAddress').value.trim(), |
| phone: document.getElementById('infoPhone').value.trim(), |
| email: document.getElementById('infoEmail').value.trim(), |
| hours |
| }; |
|
|
| try { |
| const res = await fetch('/api/info', { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(info) |
| }); |
| if (!res.ok) throw new Error('Failed'); |
| await loadMenuData(); |
| showToast('Cafe info saved.', 'success'); |
| } catch { showToast('Error saving', 'error'); } |
| }); |
|
|
| document.getElementById('passwordForm').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const errEl = document.getElementById('passwordError'); |
| const okEl = document.getElementById('passwordSuccess'); |
| errEl.textContent = ''; |
| okEl.textContent = ''; |
|
|
| const current = document.getElementById('currentPassword').value; |
| const newPw = document.getElementById('newPassword').value; |
| const confirm = document.getElementById('confirmPassword').value; |
|
|
| if (newPw !== confirm) { errEl.textContent = 'Passwords do not match.'; return; } |
| if (newPw.length < 6) { errEl.textContent = 'At least 6 characters.'; return; } |
|
|
| try { |
| const check = await fetch('/api/login', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ password: current }) |
| }); |
| if (!(await check.json()).success) { |
| errEl.textContent = 'Current password is incorrect.'; |
| return; |
| } |
|
|
| const res = await fetch('/api/change-password', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ newPassword: newPw }) |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| okEl.textContent = 'Password updated.'; |
| document.getElementById('passwordForm').reset(); |
| } else { |
| errEl.textContent = data.error || 'Error updating.'; |
| } |
| } catch { errEl.textContent = 'Connection error.'; } |
| }); |
| } |
|
|
| |
| let tablesSetup = false; |
|
|
| function setupTablesSection() { |
| if (tablesSetup) { renderOrders(); return; } |
| tablesSetup = true; |
|
|
| document.getElementById('saveTablesBtn').addEventListener('click', saveTableCount); |
| document.getElementById('loadQrBtn').addEventListener('click', loadQrCodes); |
| document.getElementById('refreshOrdersBtn').addEventListener('click', () => { renderOrders(); }); |
|
|
| |
| document.getElementById('tableCount').value = menuData.tables || 0; |
| renderOrders(); |
| } |
|
|
| async function saveTableCount() { |
| const count = parseInt(document.getElementById('tableCount').value); |
| if (isNaN(count) || count < 0 || count > 100) { |
| return showToast('Enter 0–100', 'error'); |
| } |
| try { |
| const res = await fetch('/api/tables', { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ count }) |
| }); |
| if (!res.ok) throw new Error('Failed'); |
| menuData.tables = count; |
| showToast(`Saved: ${count} tables`, 'success'); |
| } catch { showToast('Error saving', 'error'); } |
| } |
|
|
| async function loadQrCodes() { |
| const grid = document.getElementById('qrGrid'); |
| grid.innerHTML = '<p style="color:var(--parchment);font-size:0.82rem;padding:20px;">Generating QR codes…</p>'; |
| try { |
| const res = await fetch('/api/qr'); |
| if (!res.ok) throw new Error('Failed'); |
| const qrs = await res.json(); |
|
|
| if (qrs.length === 0) { |
| grid.innerHTML = '<p style="color:var(--parchment);font-size:0.82rem;padding:32px;">No tables configured. Set the number above first.</p>'; |
| return; |
| } |
|
|
| grid.innerHTML = qrs.map(q => ` |
| <div class="qr-card"> |
| <img src="${q.dataUrl}" alt="QR for Table ${q.table}" width="150" height="150"> |
| <div class="qr-label">Table ${q.table}</div> |
| <div class="qr-url">${esc(q.url)}</div> |
| </div> |
| `).join(''); |
|
|
| showToast(`${qrs.length} QR codes ready for print`, 'success'); |
| } catch (err) { |
| console.error(err); |
| grid.innerHTML = '<p style="color:var(--cherry);font-size:0.82rem;padding:32px;">Failed to generate QR codes.</p>'; |
| } |
| } |
|
|
| async function renderOrders() { |
| const container = document.getElementById('ordersContainer'); |
| try { |
| const res = await fetch('/api/orders'); |
| if (!res.ok) throw new Error('Failed'); |
| const orders = await res.json(); |
| |
| const activeOrders = orders.filter(o => o.status === 'pending' || o.status === 'served'); |
|
|
| if (activeOrders.length === 0) { |
| container.innerHTML = '<p style="color:var(--parchment);font-size:0.82rem;padding:16px 0;">No active orders.</p>'; |
| return; |
| } |
|
|
| const statusLabel = { pending: 'New', served: 'Served', paid: 'Paid', cancelled: 'Cancelled' }; |
| const statusClass = { pending: 'badge-order-new', served: 'badge-order-served', paid: 'badge-order-paid', cancelled: 'badge-order-cancelled' }; |
|
|
| container.innerHTML = activeOrders.map(o => ` |
| <div class="order-card"> |
| <div class="order-card-head"> |
| <div> |
| <span class="order-tbl">Table ${o.table}</span> |
| <span class="order-time">${timeAgo(o.createdAt)}</span> |
| </div> |
| <span class="badge ${statusClass[o.status] || ''}">${statusLabel[o.status] || o.status}</span> |
| </div> |
| <div class="order-card-body"> |
| <ul class="order-items"> |
| ${o.items.map(i => i.mid |
| ? `<li>${i.qty}× ${esc(i.name)} <span>₹${(i.price * i.qty).toFixed(0)}</span></li>` |
| : `<li class="order-item-custom">"${esc(i.text)}"</li>` |
| ).join('')} |
| </ul> |
| ${o.note ? `<p class="order-note">📝 ${esc(o.note)}</p>` : ''} |
| <p class="order-total">Total: ₹${o.total.toFixed(0)}</p> |
| </div> |
| ${` |
| <div class="order-card-actions"> |
| ${o.status === 'pending' ? `<button class="btn act-order serve" data-id="${o.id}" data-status="served">✓ Served</button>` : ''} |
| ${o.status === 'served' ? `<button class="btn act-order paid" data-id="${o.id}" data-status="paid">$ Paid</button>` : ''} |
| ${o.status !== 'paid' ? `<button class="btn btn-ghost act-order cancel" data-id="${o.id}" data-status="cancelled">✕ Cancel</button>` : ''} |
| </div> |
| `} |
| </div> |
| `).join(''); |
|
|
| |
| container.querySelectorAll('.act-order').forEach(btn => { |
| btn.addEventListener('click', async () => { |
| const id = btn.dataset.id; |
| const status = btn.dataset.status; |
| try { |
| const res = await fetch(`/api/orders/${id}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ status }) |
| }); |
| if (!res.ok) throw new Error('Failed'); |
| showToast(`Order ${status}`, 'success'); |
| renderOrders(); |
| } catch { showToast('Error updating', 'error'); } |
| }); |
| }); |
| } catch (err) { |
| console.error(err); |
| container.innerHTML = '<p style="color:var(--cherry);">Failed to load orders.</p>'; |
| } |
| } |
|
|
| function timeAgo(iso) { |
| const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000); |
| if (diff < 60) return 'just now'; |
| if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; |
| if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; |
| const d = new Date(iso); |
| return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); |
| } |
|
|
| |
| function showToast(msg, type = 'success') { |
| const container = document.getElementById('toastContainer'); |
| if (!container) return; |
|
|
| const toast = document.createElement('div'); |
| toast.className = `toast ${type}`; |
| toast.innerHTML = `<span>${type === 'success' ? '✓' : '✗'}</span> ${msg}`; |
| container.appendChild(toast); |
|
|
| setTimeout(() => { |
| toast.style.opacity = '0'; |
| toast.style.transition = 'opacity 0.25s'; |
| setTimeout(() => toast.remove(), 250); |
| }, 2800); |
| } |
|
|
| |
| function esc(str) { |
| const div = document.createElement('div'); |
| div.textContent = str || ''; |
| return div.innerHTML; |
| } |
|
|
| function adminSvgIcon(category) { |
| const svgs = { |
| 'coffee': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h14v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"/><path d="M18 9h2a2 2 0 012 2v1a2 2 0 01-2 2h-2"/><ellipse cx="8" cy="14" rx="2" ry="1.5"/><path d="M8 1v2" opacity="0.4"/><path d="M6 0v2" opacity="0.3"/><path d="M10 0v2" opacity="0.3"/></svg>', |
| 'tea': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h10a3 3 0 013 3v3a3 3 0 01-3 3H5a3 3 0 01-3-3v-3a3 3 0 013-3z"/><path d="M2 11l-2 3h2l1-3"/><path d="M7 8V6a1 1 0 011-1h2a1 1 0 011 1v2"/><circle cx="10" cy="3.5" r="1.2"/><path d="M8 1v2" opacity="0.4"/><path d="M10 0v2" opacity="0.3"/><path d="M12 1v2" opacity="0.3"/></svg>', |
| 'pastries': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 16C4 16 6 10 12 8s10 0 10 0c-4 2-6 8-6 8"/><path d="M5 14c1-3 4-6 7-7"/><path d="M19 14c-1-3-4-6-7-7"/><path d="M8 10h8" opacity="0.3"/></svg>', |
| 'food': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="18" rx="10" ry="3"/><rect x="8" y="8" width="8" height="7" rx="1"/><line x1="10" y1="9.5" x2="14" y2="9.5" opacity="0.4"/><line x1="10" y1="11.5" x2="14" y2="11.5" opacity="0.3"/><ellipse cx="12" cy="16" rx="3" ry="2" fill="currentColor" opacity="0.15"/></svg>' |
| }; |
| return svgs[category] || svgs['coffee']; |
| } |
|
|