From 75ce40635736260ce5a19b7a33856305ee516ccc Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sat, 9 May 2026 01:19:08 -0700 Subject: Simplify pantry UX and persist grocery check state server-side - Remove quantity, unit, expiry, and category fields from pantry form; auto-detect category via keyword map - Persist grocery item checked/unchecked state in DB via PATCH /api/grocery/{id}/check-item - Fix unchecking: previously bailed early and never removed the checked class - Fix NOT NULL constraint on ingredients.unit by defaulting to empty string Co-Authored-By: Claude Sonnet 4.6 --- static/app.js | 124 +++++++++++++++++++++++----------------------------------- 1 file changed, 50 insertions(+), 74 deletions(-) (limited to 'static/app.js') diff --git a/static/app.js b/static/app.js index 8f39fb8..724e565 100644 --- a/static/app.js +++ b/static/app.js @@ -86,6 +86,17 @@ function daysUntilExpiry(expiryDateStr) { return days; } +// ── Pantry Category Auto-Detection ────────────────────────────────────── +function autoCategory(name) { + const n = name.toLowerCase(); + if (/\b(apple|banana|berry|berries|broccoli|carrot|celery|corn|cucumber|garlic|grape|greens|kale|lemon|lettuce|lime|mango|mushroom|onion|orange|pepper|potato|spinach|tomato|zucchini|avocado|cabbage|cauliflower|beet|pea|herb|basil|cilantro|parsley|thyme|rosemary|ginger|scallion|leek|squash|radish|arugula|asparagus|artichoke|eggplant)\b/.test(n)) return 'produce'; + if (/\b(milk|cheese|butter|cream|yogurt|egg|eggs|cheddar|mozzarella|parmesan|ricotta|sour cream|half.and.half|heavy cream|kefir)\b/.test(n)) return 'dairy'; + if (/\b(chicken|beef|pork|turkey|fish|salmon|tuna|shrimp|lamb|bacon|sausage|ham|steak|ground|tofu|tempeh|lentil|bean|beans|chickpea)\b/.test(n)) return 'protein'; + if (/\b(bread|bun|baguette|tortilla|roll|bagel|muffin|croissant|pita)\b/.test(n)) return 'bakery'; + if (/\b(ice cream|frozen|edamame)\b/.test(n)) return 'frozen'; + return 'pantry'; +} + // ── Tab Switching ────────────────────────────────────────────────────────── function switchTab(tabName) { state.activeTab = tabName; @@ -114,7 +125,6 @@ async function loadPantry() { try { state.pantry = await api('GET', '/pantry'); renderPantryTable(); - updateExpiryWarning(); } catch (err) { showToast(err.message, 'error'); } @@ -136,22 +146,11 @@ function renderPantryTable() { emptyState.classList.add('hidden'); tbody.innerHTML = state.pantry.map(ing => { - const days = daysUntilExpiry(ing.expiry_date); - let rowClass = ''; - if (days !== null && days < 2) rowClass = 'expiry-danger'; - else if (days !== null && days < 7) rowClass = 'expiry-warn'; - - const expiryText = ing.expiry_date ? formatDate(ing.expiry_date) : '—'; - return ` - + ${ing.name} - ${ing.quantity} - ${ing.unit} ${ing.category || '—'} - ${expiryText} - @@ -159,30 +158,8 @@ function renderPantryTable() { }).join(''); } -function updateExpiryWarning() { - const banner = document.getElementById('expiry-warning-banner'); - const expiring = state.pantry.filter(ing => { - const days = daysUntilExpiry(ing.expiry_date); - return days !== null && days < 3 && days >= 0; - }); - - if (expiring.length === 0) { - banner.classList.add('hidden'); - return; - } - - banner.classList.remove('hidden'); - banner.innerHTML = ` - ⚠️ Expiring Soon: ${expiring.map(ing => `${ing.name} (${formatDate(ing.expiry_date)})`).join(', ')} - `; -} - async function addIngredient() { const name = document.getElementById('ing-name').value.trim(); - const qty = parseFloat(document.getElementById('ing-qty').value); - const unit = document.getElementById('ing-unit').value.trim(); - const category = document.getElementById('ing-category').value; - const expiry = document.getElementById('ing-expiry').value; if (!name) { showToast('Please enter ingredient name', 'error'); @@ -190,20 +167,15 @@ async function addIngredient() { } try { + const category = autoCategory(name); await api('POST', '/pantry', { name, - quantity: qty || 0, - unit, - category: category || null, - expiry_date: expiry || null, + quantity: 1, + category, }); document.getElementById('add-ingredient-form').classList.add('hidden'); document.getElementById('ing-name').value = ''; - document.getElementById('ing-qty').value = ''; - document.getElementById('ing-unit').value = ''; - document.getElementById('ing-category').value = ''; - document.getElementById('ing-expiry').value = ''; await loadPantry(); showToast('Ingredient added!'); @@ -218,10 +190,6 @@ function editIngredient(id) { state.editingIngredientId = id; document.getElementById('ing-name').value = ing.name; - document.getElementById('ing-qty').value = ing.quantity || ''; - document.getElementById('ing-unit').value = ing.unit || ''; - document.getElementById('ing-category').value = ing.category || ''; - document.getElementById('ing-expiry').value = ing.expiry_date || ''; document.getElementById('add-ingredient-form').classList.remove('hidden'); document.getElementById('btn-save-ingredient').textContent = 'Update'; } @@ -656,9 +624,12 @@ function renderGroceryList() {
${titleCased}
${grouped[section].map(item => { const usedIn = item.used_in ? item.used_in.join(', ') : ''; + const isChecked = item.checked ? 'checked' : ''; + const checkedClass = item.checked ? 'grocery-item-checked' : ''; return ` -
+
console.error('Failed to update check state:', err)); } async function generateGrocery() { @@ -720,6 +704,7 @@ async function generateGrocery() { try { const userNotes = document.getElementById('grocery-user-notes').value.trim(); const res = await api('POST', '/grocery/generate', userNotes ? { user_notes: userNotes } : {}); + state.currentGrocery = res; state.currentGroceryId = res.grocery_list.id; document.getElementById('grocery-empty').classList.add('hidden'); @@ -748,6 +733,7 @@ async function generateGrocery() { async function clearGrocery() { try { await api('DELETE', '/grocery/current', null); + state.currentGrocery = null; state.currentGroceryId = null; document.getElementById('grocery-sections').innerHTML = ''; @@ -866,10 +852,6 @@ async function init() { document.getElementById('btn-add-ingredient').addEventListener('click', () => { state.editingIngredientId = null; document.getElementById('ing-name').value = ''; - document.getElementById('ing-qty').value = ''; - document.getElementById('ing-unit').value = ''; - document.getElementById('ing-category').value = ''; - document.getElementById('ing-expiry').value = ''; document.getElementById('btn-save-ingredient').textContent = 'Save'; document.getElementById('add-ingredient-form').classList.remove('hidden'); }); @@ -884,10 +866,6 @@ async function init() { if (!ing) return; const name = document.getElementById('ing-name').value.trim(); - const qty = parseFloat(document.getElementById('ing-qty').value); - const unit = document.getElementById('ing-unit').value.trim(); - const category = document.getElementById('ing-category').value; - const expiry = document.getElementById('ing-expiry').value; if (!name) { showToast('Please enter ingredient name', 'error'); @@ -895,12 +873,10 @@ async function init() { } try { + const category = autoCategory(name); await api('PUT', `/pantry/${state.editingIngredientId}`, { name, - quantity: qty || 0, - unit, - category: category || null, - expiry_date: expiry || null, + category, }); document.getElementById('add-ingredient-form').classList.add('hidden'); -- cgit v1.3-2-g0d8e