diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-09 01:19:08 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-09 01:19:08 -0700 |
| commit | 75ce40635736260ce5a19b7a33856305ee516ccc (patch) | |
| tree | 9db3931eae3dbd9308bab0e240e7c75950bcbd4c /static | |
| parent | 3de7c5eed5ba262abf0d746211e33800db6d66df (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'static')
| -rw-r--r-- | static/app.js | 124 | ||||
| -rw-r--r-- | static/index.html | 17 | ||||
| -rw-r--r-- | static/style.css | 9 |
3 files changed, 51 insertions, 99 deletions
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 ` - <tr class="${rowClass}" data-id="${ing.id}"> + <tr data-id="${ing.id}"> <td><strong>${ing.name}</strong></td> - <td>${ing.quantity}</td> - <td>${ing.unit}</td> <td>${ing.category || '—'}</td> - <td>${expiryText}</td> <td> - <button class="btn btn-sm" onclick="editIngredient(${ing.id})">Edit</button> <button class="btn btn-sm btn-danger" onclick="deleteIngredient(${ing.id})">Delete</button> </td> </tr> @@ -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 = ` - <strong>⚠️ Expiring Soon:</strong> ${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() { <div class="grocery-section-title">${titleCased}</div> ${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 ` - <div class="grocery-item"> + <div class="grocery-item ${checkedClass}"> <input type="checkbox" + ${isChecked} data-name="${item.name.replace(/"/g, '"')}" data-quantity="${item.quantity || 0}" data-unit="${(item.unit || '').replace(/"/g, '"')}" @@ -692,22 +663,35 @@ function renderGroceryList() { } async function checkGroceryItem(checkbox) { - if (!checkbox.checked) return; - checkbox.disabled = true; + const checked = checkbox.checked; + const row = checkbox.closest('.grocery-item'); - try { - await api('POST', '/pantry', { - name: checkbox.dataset.name, - quantity: parseFloat(checkbox.dataset.quantity) || 1, - unit: checkbox.dataset.unit, - category: checkbox.dataset.category || null, - }); - checkbox.closest('.grocery-item').classList.add('grocery-item-checked'); - } catch (err) { - showToast('Failed to add to pantry: ' + err.message, 'error'); - checkbox.checked = false; - checkbox.disabled = false; + if (checked) { + checkbox.disabled = true; + try { + await api('POST', '/pantry', { + name: checkbox.dataset.name, + quantity: parseFloat(checkbox.dataset.quantity) || 1, + unit: checkbox.dataset.unit, + category: checkbox.dataset.category || null, + }); + row.classList.add('grocery-item-checked'); + } catch (err) { + showToast('Failed to add to pantry: ' + err.message, 'error'); + checkbox.checked = false; + checkbox.disabled = false; + return; + } finally { + checkbox.disabled = false; + } + } else { + row.classList.remove('grocery-item-checked'); } + + api('PATCH', `/grocery/${state.currentGroceryId}/check-item`, { + name: checkbox.dataset.name, + checked, + }).catch(err => 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'); diff --git a/static/index.html b/static/index.html index d0e0243..ae36a4f 100644 --- a/static/index.html +++ b/static/index.html @@ -33,29 +33,14 @@ <h3>Add Ingredient</h3> <div class="form-row"> <div class="form-group"><label>Name</label><input type="text" id="ing-name" placeholder="e.g. Chicken Breast"></div> - <div class="form-group"><label>Quantity</label><input type="number" id="ing-qty" step="0.1" placeholder="500"></div> - <div class="form-group"><label>Unit</label><input type="text" id="ing-unit" placeholder="grams"></div> - <div class="form-group"><label>Category</label> - <select id="ing-category"> - <option value="">None</option> - <option value="produce">Produce</option> - <option value="protein">Protein</option> - <option value="dairy">Dairy</option> - <option value="pantry">Pantry</option> - <option value="frozen">Frozen</option> - <option value="bakery">Bakery</option> - </select> - </div> - <div class="form-group"><label>Expiry Date</label><input type="date" id="ing-expiry"></div> </div> <div class="form-actions"> <button class="btn btn-primary" id="btn-save-ingredient">Save</button> <button class="btn" id="btn-cancel-ingredient">Cancel</button> </div> </div> - <div id="expiry-warning-banner" class="hidden"></div> <table id="pantry-table"> - <thead><tr><th>Name</th><th>Quantity</th><th>Unit</th><th>Category</th><th>Expires</th><th>Actions</th></tr></thead> + <thead><tr><th>Name</th><th>Category</th><th>Actions</th></tr></thead> <tbody id="pantry-tbody"></tbody> </table> <div id="pantry-empty" class="empty-state hidden">No ingredients yet. Add some to get started.</div> diff --git a/static/style.css b/static/style.css index 4cc033b..c530839 100644 --- a/static/style.css +++ b/static/style.css @@ -324,15 +324,6 @@ tbody tr:hover { background-color: rgba(108, 99, 255, 0.1); } -tbody tr.expiry-danger { - background-color: rgba(239, 68, 68, 0.1); - border-left: 3px solid var(--danger); -} - -tbody tr.expiry-warn { - background-color: rgba(245, 158, 11, 0.1); - border-left: 3px solid var(--warning); -} /* ── Cards ──────────────────────────────────────────────────────────────── */ .card { |
