/** * Fridger Frontend Application * Handles dual view/manage modes, live barcode scanning, grams adjustment, and item management. */ // Application State const state = { currentMode: 'view', // 'view' or 'manage' viewGrouping: 'shelf', // 'shelf', 'category', 'grid' items: [], meta: { categories: [], locations: [], units: [] }, filters: { search: '', category: 'All', status: 'all', location: 'All' }, activeGramsItem: null, activeGramsValue: 0, html5QrCode: null, isScanning: false, openedFromModal: false }; // Initialize Application document.addEventListener('DOMContentLoaded', async () => { await loadMetadata(); await loadStats(); await loadItems(); initLucide(); }); function initLucide() { if (window.lucide) { window.lucide.createIcons(); } } // ---------------------------------------------------- // API Calls // ---------------------------------------------------- async function loadMetadata() { try { const res = await fetch('/api/meta'); if (res.ok) { state.meta = await res.json(); renderCategoryFilters(); populateModalSelects(); } } catch (err) { console.error('Failed to load metadata:', err); } } async function loadStats() { try { const res = await fetch('/api/stats'); if (res.ok) { const stats = await res.json(); document.getElementById('stat-total').textContent = stats.total || 0; document.getElementById('stat-fresh').textContent = stats.fresh || 0; document.getElementById('stat-expiring').textContent = stats.expiringSoon || 0; document.getElementById('stat-opened').textContent = stats.opened || 0; } } catch (err) { console.error('Failed to load stats:', err); } } async function loadItems() { try { const params = new URLSearchParams(); if (state.filters.search) params.append('search', state.filters.search); if (state.filters.category && state.filters.category !== 'All') params.append('category', state.filters.category); if (state.filters.location && state.filters.location !== 'All') params.append('location', state.filters.location); if (state.filters.status && state.filters.status !== 'all') params.append('status', state.filters.status); const res = await fetch(`/api/items?${params.toString()}`); if (res.ok) { state.items = await res.json(); renderApp(); } } catch (err) { console.error('Failed to load items:', err); showToast('Failed to load fridge items', 'error'); } } // ---------------------------------------------------- // Mode Switching & Filtering // ---------------------------------------------------- function switchMode(mode) { state.currentMode = mode; const viewBtn = document.getElementById('mode-view-btn'); const manageBtn = document.getElementById('mode-manage-btn'); const viewContainer = document.getElementById('view-mode-container'); const manageContainer = document.getElementById('manage-mode-container'); if (mode === 'view') { viewBtn.className = 'flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-teal-500 text-white shadow-md'; manageBtn.className = 'flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-slate-300 hover:text-white'; viewContainer.classList.remove('hidden'); manageContainer.classList.add('hidden'); } else { manageBtn.className = 'flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition-all duration-200 bg-teal-500 text-white shadow-md'; viewBtn.className = 'flex items-center gap-2 px-3 sm:px-5 py-2 rounded-lg text-sm font-semibold transition-all duration-200 text-slate-300 hover:text-white'; manageContainer.classList.remove('hidden'); viewContainer.classList.add('hidden'); } renderApp(); } function setViewGrouping(grouping) { state.viewGrouping = grouping; const btnShelf = document.getElementById('grp-shelf-btn'); const btnCat = document.getElementById('grp-cat-btn'); const btnGrid = document.getElementById('grp-grid-btn'); const activeClass = 'px-2.5 py-1 rounded bg-teal-500/20 text-teal-300 border border-teal-500/30 font-medium'; const inactiveClass = 'px-2.5 py-1 rounded bg-slate-800 text-slate-400 border border-slate-700 font-medium'; btnShelf.className = grouping === 'shelf' ? activeClass : inactiveClass; btnCat.className = grouping === 'category' ? activeClass : inactiveClass; btnGrid.className = grouping === 'grid' ? activeClass : inactiveClass; renderFridgeView(); } let searchDebounceTimer = null; function handleSearch(val) { state.filters.search = val.trim(); const clearBtn = document.getElementById('clear-search'); if (state.filters.search) { clearBtn.classList.remove('hidden'); } else { clearBtn.classList.add('hidden'); } clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { loadItems(); }, 250); } function clearSearch() { document.getElementById('search-input').value = ''; document.getElementById('clear-search').classList.add('hidden'); state.filters.search = ''; loadItems(); } function setStatusFilter(status) { state.filters.status = status; document.querySelectorAll('[data-status-btn]').forEach(btn => { if (btn.getAttribute('data-status-btn') === status) { btn.className = 'status-chip active px-3 py-2 rounded-lg bg-slate-800 border border-slate-700 text-white whitespace-nowrap transition font-semibold'; } else { btn.className = 'status-chip px-3 py-2 rounded-lg bg-slate-900 border border-slate-800 text-slate-400 hover:text-slate-200 whitespace-nowrap transition'; } }); loadItems(); } function setCategoryFilter(category) { state.filters.category = category; renderCategoryFilters(); loadItems(); } function renderCategoryFilters() { const container = document.getElementById('category-filter-bar'); if (!container) return; let html = ` `; state.meta.categories.forEach(cat => { const isSelected = state.filters.category === cat.id; html += ` `; }); container.innerHTML = html; } function populateModalSelects() { const catSelect = document.getElementById('modal-category'); const locSelect = document.getElementById('modal-location'); if (catSelect && state.meta.categories) { catSelect.innerHTML = state.meta.categories.map(c => ` `).join(''); } if (locSelect && state.meta.locations) { locSelect.innerHTML = state.meta.locations.map(l => ` `).join(''); } } // ---------------------------------------------------- // Rendering // ---------------------------------------------------- function renderApp() { const emptyState = document.getElementById('empty-state'); const countLabel = document.getElementById('item-count-label'); if (countLabel) { countLabel.textContent = `Showing ${state.items.length} item${state.items.length === 1 ? '' : 's'}`; } if (state.items.length === 0) { emptyState.classList.remove('hidden'); document.getElementById('fridge-view-content').innerHTML = ''; document.getElementById('manage-items-list').innerHTML = ''; return; } emptyState.classList.add('hidden'); if (state.currentMode === 'view') { renderFridgeView(); } else { renderManageView(); } initLucide(); } function renderFridgeView() { const container = document.getElementById('fridge-view-content'); if (!container) return; if (state.viewGrouping === 'grid') { container.innerHTML = `
${state.items.map(item => renderFridgeItemCard(item)).join('')}
`; return; } // Group by Shelf or Category const isShelf = state.viewGrouping === 'shelf'; const groups = {}; if (isShelf) { state.meta.locations.forEach(loc => groups[loc] = []); groups['Other Location'] = []; } else { state.meta.categories.forEach(cat => groups[cat.id] = []); groups['Other'] = []; } state.items.forEach(item => { const key = isShelf ? (item.location || 'Other Location') : (item.category || 'Other'); if (!groups[key]) groups[key] = []; groups[key].push(item); }); let html = ''; for (const [groupName, groupItems] of Object.entries(groups)) { if (groupItems.length === 0) continue; const iconName = isShelf ? 'refrigerator' : getCategoryIcon(groupName); html += `

${escapeHtml(groupName)}

${groupItems.length} item${groupItems.length === 1 ? '' : 's'}
${groupItems.map(item => renderFridgeItemCard(item)).join('')}
`; } container.innerHTML = html; } function renderFridgeItemCard(item) { const expiryInfo = getExpiryStatus(item.expiry_date); const percent = item.initial_quantity > 0 ? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100))) : 100; const isWeight = item.unit === 'g' || item.unit === 'ml' || item.unit === 'kg' || item.unit === 'l'; const isLow = percent <= 25; let progressBarColor = 'bg-teal-500'; if (percent <= 20) progressBarColor = 'bg-rose-500'; else if (percent <= 50) progressBarColor = 'bg-amber-500'; return `
${escapeHtml(item.brand || item.category)} ${expiryInfo.label}
${item.image_url ? ` ${escapeHtml(item.name)} ` : `
`}

${escapeHtml(item.name)}

${item.notes ? `

${escapeHtml(item.notes)}

` : ''}
${item.quantity} ${item.unit} of ${item.initial_quantity} ${item.unit} (${percent}%)
${isWeight ? ` ` : ` `}
`; } function renderManageView() { const container = document.getElementById('manage-items-list'); if (!container) return; let html = ''; state.items.forEach(item => { const expiryInfo = getExpiryStatus(item.expiry_date); const percent = item.initial_quantity > 0 ? Math.min(100, Math.max(0, Math.round((item.quantity / item.initial_quantity) * 100))) : 100; const isWeight = item.unit === 'g' || item.unit === 'ml' || item.unit === 'kg' || item.unit === 'l'; html += `
${item.image_url ? ` ${escapeHtml(item.name)} ` : `
`}

${escapeHtml(item.name)}

${item.brand ? `${escapeHtml(item.brand)}` : ''} ${item.is_opened ? `Opened` : ''}
📍 ${escapeHtml(item.location || 'Fridge')} • ${expiryInfo.label} ${item.barcode ? `•${escapeHtml(item.barcode)}` : ''}
Remaining:
${item.quantity} ${item.unit} (${percent}%)
${isWeight ? ` ` : ` `}
`; }); container.innerHTML = html; } // ---------------------------------------------------- // Grams & Quantity Adjustments // ---------------------------------------------------- function openGramsModal(itemId) { const item = state.items.find(i => i.id === itemId); if (!item) return; state.activeGramsItem = item; state.activeGramsValue = item.quantity; document.getElementById('grams-modal-title').textContent = item.name; document.getElementById('grams-modal-sub').textContent = `Package Size: ${item.initial_quantity} ${item.unit} • Location: ${item.location}`; document.getElementById('grams-modal-unit').textContent = item.unit; document.getElementById('grams-current-val').textContent = item.quantity; document.getElementById('grams-exact-input').value = item.quantity; const slider = document.getElementById('grams-slider'); slider.max = item.initial_quantity || 1000; slider.step = (item.unit === 'g' || item.unit === 'ml') ? '5' : '1'; slider.value = item.quantity; updateGramsGauge(); document.getElementById('grams-modal').classList.remove('hidden'); initLucide(); } function closeGramsModal() { document.getElementById('grams-modal').classList.add('hidden'); state.activeGramsItem = null; } function onGramsSliderChange(val) { state.activeGramsValue = parseFloat(val); document.getElementById('grams-exact-input').value = state.activeGramsValue; updateGramsGauge(); } function onGramsExactInput(val) { const num = parseFloat(val); if (!isNaN(num) && num >= 0) { state.activeGramsValue = num; document.getElementById('grams-slider').value = num; updateGramsGauge(); } } function applyGramsDelta(delta) { state.activeGramsValue = Math.max(0, state.activeGramsValue + delta); document.getElementById('grams-slider').value = state.activeGramsValue; document.getElementById('grams-exact-input').value = state.activeGramsValue; updateGramsGauge(); } function setGramsPercent(ratio) { if (!state.activeGramsItem) return; state.activeGramsValue = Math.round(state.activeGramsItem.initial_quantity * ratio); document.getElementById('grams-slider').value = state.activeGramsValue; document.getElementById('grams-exact-input').value = state.activeGramsValue; updateGramsGauge(); } function updateGramsGauge() { document.getElementById('grams-current-val').textContent = state.activeGramsValue; if (state.activeGramsItem && state.activeGramsItem.initial_quantity > 0) { const pct = Math.round((state.activeGramsValue / state.activeGramsItem.initial_quantity) * 100); document.getElementById('grams-percent-text').textContent = `${pct}% remaining of original ${state.activeGramsItem.initial_quantity}${state.activeGramsItem.unit}`; } } async function saveGramsAdjustment() { if (!state.activeGramsItem) return; const itemId = state.activeGramsItem.id; const newQty = state.activeGramsValue; try { const res = await fetch(`/api/items/${itemId}/quantity`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ newQuantity: newQty, markOpened: true }) }); if (res.ok) { showToast(`Updated to ${newQty} ${state.activeGramsItem.unit}`, 'success'); closeGramsModal(); await loadStats(); await loadItems(); } else { showToast('Failed to update quantity', 'error'); } } catch (err) { console.error('Save grams error:', err); showToast('Network error while updating quantity', 'error'); } } async function consumeAllAndRemove() { if (!state.activeGramsItem) return; const itemId = state.activeGramsItem.id; if (confirm(`Remove "${state.activeGramsItem.name}" from fridge?`)) { try { const res = await fetch(`/api/items/${itemId}`, { method: 'DELETE' }); if (res.ok) { showToast('Item finished & removed', 'info'); closeGramsModal(); await loadStats(); await loadItems(); } } catch (err) { console.error(err); } } } async function quickDelta(itemId, delta) { try { const res = await fetch(`/api/items/${itemId}/quantity`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ delta, markOpened: true }) }); if (res.ok) { const updated = await res.json(); showToast(`${updated.name}: ${delta > 0 ? '+' : ''}${delta} ${updated.unit} (Now: ${updated.quantity} ${updated.unit})`, 'success'); await loadStats(); await loadItems(); } } catch (err) { console.error(err); showToast('Failed to adjust amount', 'error'); } } async function onManageSliderCommit(itemId, newQty) { try { const res = await fetch(`/api/items/${itemId}/quantity`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ newQuantity: parseFloat(newQty), markOpened: true }) }); if (res.ok) { showToast(`Updated quantity`, 'success'); await loadStats(); await loadItems(); } } catch (err) { console.error(err); } } // ---------------------------------------------------- // Item Add / Edit Modal // ---------------------------------------------------- function openItemModal(itemId = null) { const form = document.getElementById('item-form'); form.reset(); document.getElementById('modal-image-container').classList.add('hidden'); document.getElementById('modal-image-url').value = ''; if (itemId) { const item = state.items.find(i => i.id === itemId); if (!item) return; document.getElementById('modal-item-id').value = item.id; document.getElementById('item-modal-title').textContent = 'Edit Fridge Item'; document.getElementById('modal-barcode').value = item.barcode || ''; document.getElementById('modal-name').value = item.name || ''; document.getElementById('modal-brand').value = item.brand || ''; document.getElementById('modal-quantity').value = item.quantity; document.getElementById('modal-initial-quantity').value = item.initial_quantity || item.quantity; document.getElementById('modal-unit').value = item.unit || 'g'; document.getElementById('modal-category').value = item.category || 'General'; document.getElementById('modal-location').value = item.location || 'Fridge - Middle Shelf'; document.getElementById('modal-expiry').value = item.expiry_date || ''; document.getElementById('modal-opened').checked = !!item.is_opened; document.getElementById('modal-notes').value = item.notes || ''; if (item.image_url) { document.getElementById('modal-image-url').value = item.image_url; document.getElementById('modal-image-preview').src = item.image_url; document.getElementById('modal-image-label').textContent = item.name; document.getElementById('modal-image-container').classList.remove('hidden'); } } else { document.getElementById('modal-item-id').value = ''; document.getElementById('item-modal-title').textContent = 'Add New Fridge Item'; document.getElementById('modal-unit').value = 'g'; document.getElementById('modal-quantity').value = ''; document.getElementById('modal-initial-quantity').value = ''; document.getElementById('modal-location').value = 'Fridge - Middle Shelf'; } document.getElementById('item-modal').classList.remove('hidden'); initLucide(); } function closeItemModal() { document.getElementById('item-modal').classList.add('hidden'); } async function lookupCurrentBarcode() { const code = document.getElementById('modal-barcode').value.trim(); if (!code) { showToast('Please enter a barcode number first', 'error'); return; } const btn = document.getElementById('modal-barcode-lookup-btn'); btn.textContent = 'Searching...'; btn.disabled = true; try { const res = await fetch(`/api/barcode/${encodeURIComponent(code)}`); if (res.ok) { const data = await res.json(); applyProductDataToForm(data); showToast(`Found: ${data.name}`, 'success'); } else { showToast('Product not found in Open Food Facts', 'info'); } } catch (err) { console.error(err); showToast('Barcode lookup failed', 'error'); } finally { btn.textContent = 'Lookup'; btn.disabled = false; } } function applyProductDataToForm(data) { if (data.name) document.getElementById('modal-name').value = data.name; if (data.brand) document.getElementById('modal-brand').value = data.brand; if (data.quantity) { document.getElementById('modal-quantity').value = data.quantity; document.getElementById('modal-initial-quantity').value = data.quantity; } if (data.unit) document.getElementById('modal-unit').value = data.unit; if (data.category) document.getElementById('modal-category').value = data.category; if (data.location) document.getElementById('modal-location').value = data.location; if (data.barcode) document.getElementById('modal-barcode').value = data.barcode; if (data.image_url) { document.getElementById('modal-image-url').value = data.image_url; document.getElementById('modal-image-preview').src = data.image_url; document.getElementById('modal-image-label').textContent = data.name; document.getElementById('modal-image-container').classList.remove('hidden'); } } function setModalExpiryDays(days) { const date = new Date(Date.now() + days * 24 * 60 * 60 * 1000); document.getElementById('modal-expiry').value = date.toISOString().slice(0, 10); } async function handleItemFormSubmit(event) { event.preventDefault(); const itemId = document.getElementById('modal-item-id').value; const qty = parseFloat(document.getElementById('modal-quantity').value); const initQty = parseFloat(document.getElementById('modal-initial-quantity').value) || qty; const payload = { name: document.getElementById('modal-name').value.trim(), brand: document.getElementById('modal-brand').value.trim(), barcode: document.getElementById('modal-barcode').value.trim(), quantity: qty, initial_quantity: initQty, unit: document.getElementById('modal-unit').value, category: document.getElementById('modal-category').value, location: document.getElementById('modal-location').value, expiry_date: document.getElementById('modal-expiry').value || null, is_opened: document.getElementById('modal-opened').checked ? 1 : 0, notes: document.getElementById('modal-notes').value.trim(), image_url: document.getElementById('modal-image-url').value || null }; try { const url = itemId ? `/api/items/${itemId}` : '/api/items'; const method = itemId ? 'PUT' : 'POST'; const res = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { showToast(itemId ? 'Item updated' : 'Item added to fridge', 'success'); closeItemModal(); await loadStats(); await loadItems(); } else { const err = await res.json(); showToast(err.error || 'Failed to save item', 'error'); } } catch (err) { console.error(err); showToast('Network error while saving item', 'error'); } } async function confirmDeleteItem(itemId, name) { if (confirm(`Are you sure you want to remove "${name}" from your fridge?`)) { try { const res = await fetch(`/api/items/${itemId}`, { method: 'DELETE' }); if (res.ok) { showToast(`Removed "${name}"`, 'info'); await loadStats(); await loadItems(); } } catch (err) { console.error(err); showToast('Failed to delete item', 'error'); } } } // ---------------------------------------------------- // Live Camera Barcode Scanner // ---------------------------------------------------- function openScannerModal(fromModal = false) { state.openedFromModal = fromModal; document.getElementById('scanner-modal').classList.remove('hidden'); document.getElementById('scanner-manual-input').value = ''; document.getElementById('scanner-status').textContent = 'Initializing camera...'; startCameraScanner(); initLucide(); } function closeScannerModal() { stopCameraScanner(); document.getElementById('scanner-modal').classList.add('hidden'); } async function startCameraScanner() { if (!window.Html5Qrcode) { document.getElementById('scanner-status').textContent = 'Scanner library loading...'; return; } try { if (state.html5QrCode) { await state.html5QrCode.stop().catch(() => {}); } state.html5QrCode = new Html5Qrcode("barcode-reader"); const config = { fps: 10, qrbox: { width: 250, height: 160 }, aspectRatio: 1.333 }; await state.html5QrCode.start( { facingMode: "environment" }, config, onBarcodeScannedSuccess, onBarcodeScanError ); state.isScanning = true; document.getElementById('scanner-status').textContent = 'Camera active. Point at barcode.'; } catch (err) { console.warn('Camera start error:', err); document.getElementById('scanner-status').textContent = 'Camera unavailable. Please enter barcode number below.'; } } async function stopCameraScanner() { if (state.html5QrCode && state.isScanning) { try { await state.html5QrCode.stop(); state.isScanning = false; } catch (e) { console.warn(e); } } } async function onBarcodeScannedSuccess(decodedText) { // Beep sound feedback playBeepSound(); stopCameraScanner(); document.getElementById('scanner-status').textContent = `Scanned ${decodedText}! Fetching info...`; try { const res = await fetch(`/api/barcode/${encodeURIComponent(decodedText)}`); closeScannerModal(); if (res.ok) { const data = await res.json(); openItemModal(); applyProductDataToForm(data); showToast(`Scanned: ${data.name}`, 'success'); } else { openItemModal(); document.getElementById('modal-barcode').value = decodedText; showToast(`Scanned barcode: ${decodedText}. Please enter details.`, 'info'); } } catch (err) { console.error(err); closeScannerModal(); openItemModal(); document.getElementById('modal-barcode').value = decodedText; } } function onBarcodeScanError(errorMessage) { // Continuous scanning errors are expected until barcode is in frame } function handleManualBarcodeSubmit() { const code = document.getElementById('scanner-manual-input').value.trim(); if (!code) return; onBarcodeScannedSuccess(code); } function playBeepSound() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(880, ctx.currentTime); gain.gain.setValueAtTime(0.15, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15); osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.15); } catch (e) {} } // ---------------------------------------------------- // Utilities & Helpers // ---------------------------------------------------- function getExpiryStatus(expiryDateStr) { if (!expiryDateStr) { return { label: 'No Expiry Set', badgeClass: 'bg-slate-800 text-slate-400', icon: 'calendar' }; } const today = new Date(); today.setHours(0, 0, 0, 0); const expDate = new Date(expiryDateStr); expDate.setHours(0, 0, 0, 0); const diffDays = Math.round((expDate - today) / (1000 * 60 * 60 * 24)); if (diffDays < 0) { return { label: `Expired ${Math.abs(diffDays)}d ago`, badgeClass: 'bg-rose-500/20 text-rose-300 border border-rose-500/30', icon: 'alert-triangle' }; } if (diffDays === 0) { return { label: 'Expires Today!', badgeClass: 'bg-rose-500/20 text-rose-300 border border-rose-500/30 font-bold', icon: 'alert-circle' }; } if (diffDays === 1) { return { label: 'Expires Tomorrow', badgeClass: 'bg-amber-500/20 text-amber-300 border border-amber-500/30', icon: 'clock' }; } if (diffDays <= 3) { return { label: `${diffDays} days left`, badgeClass: 'bg-amber-500/20 text-amber-300 border border-amber-500/30', icon: 'clock' }; } return { label: `${diffDays} days left`, badgeClass: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20', icon: 'calendar-check' }; } function getCategoryIcon(category) { const cat = state.meta.categories.find(c => c.id === category); return cat ? cat.icon : 'package'; } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); const colors = { success: 'bg-teal-600 text-white border-teal-500', error: 'bg-rose-600 text-white border-rose-500', info: 'bg-slate-800 text-slate-100 border-slate-700' }; toast.className = `p-3.5 rounded-xl border shadow-xl text-xs font-medium flex items-center gap-2 transform transition-all duration-300 translate-y-2 opacity-0 pointer-events-auto ${colors[type] || colors.info}`; toast.innerHTML = `${escapeHtml(message)}`; container.appendChild(toast); requestAnimationFrame(() => { toast.classList.remove('translate-y-2', 'opacity-0'); }); setTimeout(() => { toast.classList.add('translate-y-2', 'opacity-0'); setTimeout(() => toast.remove(), 300); }, 3500); } async function seedDemoData() { try { const res = await fetch('/api/seed?force=true', { method: 'POST' }); if (res.ok) { showToast('Loaded sample fridge items!', 'success'); await loadStats(); await loadItems(); } } catch (err) { console.error(err); } }