Pantry units and recipes: amounts in pounds, cups and so on, Use some, and Cooked it with a preview of what comes out

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-27 12:00:11 -04:00
1 parent ac55520a8c
commit c236120a72
11 files changed
+204 -21

No files matched your search

+18 -8
View File
@@ -1,9 +1,11 @@
'use strict';
// ---- Pantry (Recipes > Pantry): what you keep in the kitchen, how many are left, and the level that counts as running low. A phone can scan a barcode to add one or use one;
// the first time a barcode is seen you say which item it is (so Green Giant corn and the store brand are both "Corn"). Low items go on the shopping list in a tap.
const PA_UNITS = [['', 'each'], ['g', 'g'], ['kg', 'kg'], ['oz', 'oz'], ['lb', 'lb'], ['ml', 'ml'], ['l', 'litres'], ['tsp', 'tsp'], ['tbsp', 'tbsp'], ['cup', 'cups'], ['fl oz', 'fl oz'], ['pint', 'pints'], ['quart', 'quarts'], ['gallon', 'gallons']];
const paStep = u => ({ g: 100, ml: 100 }[u] || 1);
const paApi = (p, o) => jfetch('/api/pantry' + p, o), paOpt = o => ({ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(o) });
const paAgo = t => { const m = Math.round((Date.now() - t) / 60000); return m < 1 ? 'just now' : m < 60 ? m + ' min ago' : m < 1440 ? Math.round(m / 60) + ' h ago' : new Date(t).toLocaleDateString(undefined, { weekday: 'short' }); };
const paRecent = r => r.length ? `<h2>Recently</h2><div class="list">${r.map(x => `<div class="row"><span class="grow"><b>${esc(x.who)}</b> ${x.delta < 0 ? 'took' : 'put back'} ${esc(x.item)}<small>${esc(paAgo(x.t))}</small></span></div>`).join('')}</div>` : '';
const paRecent = r => r.length ? `<h2>Recently</h2><div class="list">${r.map(x => `<div class="row"><span class="grow"><b>${esc(x.who)}</b> ${x.delta < 0 ? 'took' : 'put back'} ${esc(x.item)}<small>${esc(paAgo(x.t))}${x.note ? ' · ' + esc(x.note) : ''}</small></span></div>`).join('')}</div>` : '';
/** The simple way to use things: a big tile for each item, tap to take one (with Undo). Children see only this. */
async function viewPantryTake() {
let st; const v = $('#view'), kid = !!(ME && ME.kid && ME.kid.on);
@@ -12,9 +14,9 @@ async function viewPantryTake() {
const draw = () => {
v.innerHTML = `<div class="wrap"><div class="ptop"><h1>${kid ? 'Pantry' : 'Take one'}</h1><span class="grow"></span>${kid ? '' : `<a class="btn ghost" href="#/recipes/pantry">Manage</a>`}</div>
<p class="sub" style="margin-top:0">Tap what you are taking. It counts one off so the family knows when to buy more.</p>
<div class="paTiles">${st.items.map(x => `<button type="button" class="paTile${x.qty === 0 ? ' out' : ''}${x.isLow && x.qty ? ' low' : ''}" data-u="${esc(x.id)}"${x.qty === 0 ? ' disabled' : ''} aria-label="Take one ${esc(x.name)}${x.qty != null ? ', ' + x.qty + ' left' : ''}"><b>${esc(x.name)}</b><small>${x.qty == null ? '' : x.qty === 0 ? 'none left' : x.qty + ' left'}</small></button>`).join('') || '<p class="empty">Nothing in the pantry yet.</p>'}</div>
<div class="paTiles">${st.items.filter(x => !kid || !x.unit).map(x => `<button type="button" class="paTile${x.qty === 0 ? ' out' : ''}${x.isLow && x.qty ? ' low' : ''}" data-u="${esc(x.id)}"${x.qty === 0 ? ' disabled' : ''} aria-label="${x.unit ? 'Use some ' : 'Take one '}${esc(x.name)}${x.qty != null ? ', ' + x.label + ' left' : ''}"><b>${esc(x.name)}</b><small>${x.qty == null ? '' : x.qty === 0 ? 'none left' : x.label + ' left'}</small></button>`).join('') || '<p class="empty">Nothing in the pantry yet.</p>'}</div>
<div class="bar" style="justify-content:flex-start"><button class="btn ghost" id="pasuse">${icon('plus')}Scan a barcode instead</button></div>${paRecent(st.recent || [])}</div>`;
v.querySelectorAll('[data-u]').forEach(b => b.onclick = async () => { b.disabled = true; try { const r = await paApi('/use', paOpt({ id: b.dataset.u })); st.items = r.items; st.recent = r.recent; draw(); const it = r.item;
v.querySelectorAll('[data-u]').forEach(b => b.onclick = async () => { const own = st.items.find(x => x.id === b.dataset.u); if (own && own.unit) return paUseSome(own, list => { st.items = list.items; st.recent = list.recent; draw(); }); b.disabled = true; try { const r = await paApi('/use', paOpt({ id: b.dataset.u })); st.items = r.items; st.recent = r.recent; draw(); const it = r.item;
toast(`Took one ${it.name}${it.qty != null ? ': ' + it.qty + ' left' : ''}`, { label: 'Undo', run: async () => { try { const u = await paApi('/use', paOpt({ id: it.id, undo: true })); st.items = u.items; st.recent = u.recent; if ($('.paTiles')) draw(); toast('Put back'); } catch (e) { toast(e.message); } } }); } catch (e) { b.disabled = false; toast(e.message); } });
$('#pasuse').onclick = () => pantryScan('use', list => { st.items = list; draw(); }, () => st.items);
};
@@ -28,13 +30,14 @@ async function viewPantry(sub) {
const draw = () => {
v.innerHTML = `<div class="wrap"><div class="ptop"><h1>Recipes</h1></div>${typeof rcTabs === 'function' ? rcTabs('pantry') : ''}
<div class="bar" style="justify-content:flex-start"><a class="btn" href="#/recipes/pantry/take">Take one</a><button class="btn ghost" id="pasadd">${icon('plus')}Scan to add</button><button class="btn ghost" id="pasuse">Scan to use one</button></div>
<form id="paf" class="bar" style="flex-wrap:nowrap;gap:6px"><input type="text" id="pan" placeholder="Add an item" autocomplete="off" required style="flex:2;min-width:0"><input type="number" id="paq" placeholder="How many" min="0" step="any" inputmode="decimal" style="flex:1;min-width:0" aria-label="How many are left"><input type="number" id="paw" placeholder="Low at" min="0" step="any" inputmode="decimal" style="flex:1;min-width:0" aria-label="Counts as running low at"><button class="btn">Add</button></form>
<div class="list">${items.map(x => `<div class="row"><span class="grow" style="white-space:normal"><b>${esc(x.name)}</b>${x.isLow ? ' <small style="color:var(--danger)">running low</small>' : ''}<small>${x.qty == null ? 'not counted' : x.qty + ' left'}${x.low != null ? ' · low at ' + x.low : ''}${x.codes.length ? ' · ' + x.codes.length + ' barcode' + (x.codes.length === 1 ? '' : 's') : ''}</small></span>
${x.isLow && typeof rcApi === 'function' ? `<button class="btn ghost" data-shop="${esc(x.id)}" style="min-height:36px">To shopping</button>` : ''}${x.codes.length ? `<button class="ibtn" data-bc="${esc(x.id)}" aria-label="Barcodes for ${esc(x.name)}" title="Barcodes">${icon('list')}</button>` : ''}${x.qty != null ? `<button class="ibtn" data-d="${esc(x.id)}:-1" aria-label="One less ${esc(x.name)}">−</button><button class="ibtn" data-d="${esc(x.id)}:1" aria-label="One more ${esc(x.name)}">+</button>` : ''}<button class="ibtn" data-x="${esc(x.id)}" aria-label="Remove ${esc(x.name)}">${icon('trash')}</button></div>`).join('') || '<div class="row"><span class="grow"><small>Nothing yet. Add what you keep in the kitchen, or scan a barcode.</small></span></div>'}</div>
<form id="paf" class="bar" style="flex-wrap:nowrap;gap:6px"><input type="text" id="pan" placeholder="Add an item" autocomplete="off" required style="flex:2;min-width:0"><input type="number" id="paq" placeholder="How many" min="0" step="any" inputmode="decimal" style="flex:1;min-width:0" aria-label="How many are left"><select id="pau" aria-label="Unit" style="flex:1;min-width:0">${PA_UNITS.map(u => `<option value="${esc(u[0])}">${esc(u[1])}</option>`).join('')}</select><input type="number" id="paw" placeholder="Low at" min="0" step="any" inputmode="decimal" style="flex:1;min-width:0" aria-label="Counts as running low at"><button class="btn">Add</button></form>
<div class="list">${items.map(x => `<div class="row"><span class="grow" style="white-space:normal"><b>${esc(x.name)}</b>${x.isLow ? ' <small style="color:var(--danger)">running low</small>' : ''}<small>${x.qty == null ? 'not counted' : x.label + ' left'}${x.low != null ? ' · low at ' + x.lowLabel : ''}${x.codes.length ? ' · ' + x.codes.length + ' barcode' + (x.codes.length === 1 ? '' : 's') : ''}</small></span>
${x.isLow && typeof rcApi === 'function' ? `<button class="btn ghost" data-shop="${esc(x.id)}" style="min-height:36px">To shopping</button>` : ''}${x.codes.length ? `<button class="ibtn" data-bc="${esc(x.id)}" aria-label="Barcodes for ${esc(x.name)}" title="Barcodes">${icon('list')}</button>` : ''}${x.qty != null && x.unit ? `<button class="btn ghost" data-some="${esc(x.id)}" style="min-height:36px">Use some</button>` : ''}${x.qty != null ? `<button class="ibtn" data-d="${esc(x.id)}:-${paStep(x.unit)}" aria-label="Less ${esc(x.name)}">−</button><button class="ibtn" data-d="${esc(x.id)}:${paStep(x.unit)}" aria-label="More ${esc(x.name)}">+</button>` : ''}<button class="ibtn" data-x="${esc(x.id)}" aria-label="Remove ${esc(x.name)}">${icon('trash')}</button></div>`).join('') || '<div class="row"><span class="grow"><small>Nothing yet. Add what you keep in the kitchen, or scan a barcode.</small></span></div>'}</div>
<p class="sub" style="margin-top:12px">Counts are kept by hand (and by scanning); they do not go down when you cook. The meal plan points out ingredients you already have.</p>${paRecent(recent)}</div>`;
const go = async (p, o) => { try { items = (await paApi(p, paOpt(o))).items; draw(); } catch (e) { toast(e.message); } };
$('#paf').onsubmit = e => { e.preventDefault(); go('/item', { name: $('#pan').value, qty: $('#paq').value, low: $('#paw').value }); };
v.querySelectorAll('[data-d]').forEach(b => b.onclick = () => { const [id, d] = b.dataset.d.split(':'); go('/adjust', { id, delta: +d }); });
$('#paf').onsubmit = e => { e.preventDefault(); go('/item', { name: $('#pan').value, qty: $('#paq').value, unit: $('#pau').value, low: $('#paw').value }); };
v.querySelectorAll('[data-some]').forEach(b => b.onclick = () => paUseSome(items.find(x => x.id === b.dataset.some), r => { items = r.items; recent = r.recent || recent; draw(); }));
v.querySelectorAll('[data-d]').forEach(b => b.onclick = () => { const i = b.dataset.d.lastIndexOf(':'); go('/adjust', { id: b.dataset.d.slice(0, i), delta: +b.dataset.d.slice(i + 1) }); });
v.querySelectorAll('[data-x]').forEach(b => b.onclick = () => go('/delete', { id: b.dataset.x }));
v.querySelectorAll('[data-shop]').forEach(b => b.onclick = async () => { const it = items.find(x => x.id === b.dataset.shop); try { await rcApi('/shopping/add', rcOpt('POST', { text: it.name })); toast(it.name + ' added to the shopping list'); } catch (e) { toast(e.message); } });
v.querySelectorAll('[data-bc]').forEach(b => b.onclick = () => { const it = items.find(x => x.id === b.dataset.bc), el = openSheet('Barcodes for ' + it.name, 'Scanning any of these counts as ' + it.name + '.', `<div class="list">${it.codes.map(c => `<div class="row"><span class="grow"><b>${esc(c)}</b></span><button class="ibtn" data-u="${esc(c)}" aria-label="Unlink ${esc(c)}">${icon('trash')}</button></div>`).join('')}</div>`); el.querySelectorAll('[data-u]').forEach(u => u.onclick = async () => { closeSheet(); go('/unlink', { id: it.id, code: u.dataset.u }); }); });
@@ -89,3 +92,10 @@ function pantryScan(mode, onChange, getItems) {
})();
new MutationObserver((_, ob) => { if (!el.isConnected) { stop(); ob.disconnect(); } }).observe(document.body, { childList: true });
}
/** Use some of an item: an amount in any unit that can be turned into the item's own (4 cups of the flour that is kept in pounds). */
function paUseSome(it, done) {
const el = openSheet('Use some ' + it.name.toLowerCase(), `${it.label} left. Any unit works if it can be turned into ${it.unit || 'a count'} (cups of flour into pounds, for instance).`, `<form id="pusf"><div class="bar" style="flex-wrap:nowrap;gap:6px"><input type="number" id="pusa" min="0" step="any" inputmode="decimal" placeholder="Amount" required style="flex:1;min-width:0"><select id="pusu" aria-label="Unit" style="flex:1;min-width:0">${PA_UNITS.map(u => `<option value="${esc(u[0])}"${u[0] === it.unit ? ' selected' : ''}>${esc(u[1])}</option>`).join('')}</select></div><div class="err" id="puse"></div><div class="bar"><button class="btn" style="flex:1;justify-content:center">Take it out</button></div></form>`);
setTimeout(() => el.querySelector('#pusa').focus(), 80);
el.querySelector('#pusf').onsubmit = async e => { e.preventDefault(); try { const r = await paApi('/take', paOpt({ takes: [{ id: it.id, amount: +el.querySelector('#pusa').value, unit: el.querySelector('#pusu').value }], note: 'used some' })); if (r.skipped.length) { el.querySelector('#puse').textContent = 'That cannot be turned into ' + (it.unit || 'a count') + ' for ' + it.name.toLowerCase() + '. Pick a matching unit.'; return; } closeSheet(); const d = r.done[0]; toast(`${d.name}: ${d.label || 'none'} left${d.isLow ? ' (running low)' : ''}`); done(r); } catch (er) { el.querySelector('#puse').textContent = er.message; } };
}
+23 -3
View File
@@ -62,7 +62,7 @@ async function rcOne(slug) {
<div style="min-width:0"><h1 style="margin:0">${esc(r.name)}</h1>${r.description ? `<p class="sub" style="margin:6px 0 0;white-space:normal">${esc(r.description)}</p>` : ''}
${facts.length ? `<p class="sub" style="margin:8px 0 0">${facts.map(f => `<b>${esc(f[0])}</b> ${esc(f[1])}`).join(' · ')}</p>` : ''}
${r.orgUrl ? `<p class="sub" style="margin:4px 0 0"><a href="${esc(r.orgUrl)}" target="_blank" rel="noopener noreferrer" style="color:var(--accent)">Original page</a></p>` : ''}</div></div>
<div class="bar" style="justify-content:flex-start"><button class="btn" id="rcadd">${icon('plus')}Add ingredients to shopping list</button><button class="btn ghost" id="rcplan">${icon('cal')}Plan it</button><a class="btn ghost" href="#/recipes/shop">Shopping list</a>${allowed('chat') ? '<button class="btn ghost" id="rcshare">Share to Chat</button>' : ''}</div>
<div class="bar" style="justify-content:flex-start"><button class="btn" id="rcadd">${icon('plus')}Add ingredients to shopping list</button><button class="btn ghost" id="rcplan">${icon('cal')}Plan it</button><button class="btn ghost" id="rccook">${icon('check')}Cooked it</button><a class="btn ghost" href="#/recipes/shop">Shopping list</a>${allowed('chat') ? '<button class="btn ghost" id="rcshare">Share to Chat</button>' : ''}</div>
<div class="rccols"><section><div class="rchead"><h2>Ingredients</h2><button class="btn ghost" id="rcreset" style="min-height:32px">Uncheck all</button></div>
<div class="list">${r.ingredients.map((i, n) => i.title && !i.text ? `<p class="sub" style="margin:10px 0 2px"><b>${esc(i.title)}</b></p>` : `<label class="row rcing" style="cursor:pointer"><input type="checkbox" data-n="${n}"><span class="grow">${esc(i.text)}</span></label>`).join('') || '<p class="empty">No ingredients.</p>'}</div></section>
<section><div class="rchead"><h2>Steps</h2></div><ol class="rcsteps">${r.steps.map((s, n) => `<li data-s="${n}">${s.title ? `<b>${esc(s.title)}</b> ` : ''}${esc(s.text).replace(/\n/g, '<br>')}</li>`).join('') || '<li class="empty">No steps.</li>'}</ol></section></div>
@@ -80,7 +80,7 @@ async function rcOne(slug) {
$('#rcreset').onclick = () => { done = { i: [], s: [] }; store.del(sk); paint(); };
paint();
if ($('#rcshare')) $('#rcshare').onclick = () => shareToChat(r.name + ' ' + location.origin + '/#/recipes/r/' + encodeURIComponent(slug));
$('#rcplan').onclick = () => rcPlanPick(r.slug, r.name, r.id);
$('#rcplan').onclick = () => rcPlanPick(r.slug, r.name, r.id); $('#rccook').onclick = () => rcCook(r.slug, r.name);
$('#rcadd').onclick = async e => {
const b = e.currentTarget; b.disabled = true;
try { const o = await rcApi('/shopping/add-recipe', rcOpt('POST', { slug })); toast('Added to your shopping list'); b.disabled = false; return o; }
@@ -119,10 +119,11 @@ async function rcPlan(start) {
<div class="bar" style="justify-content:space-between;align-items:center"><a class="btn ghost" href="${shift(-7)}" aria-label="Previous week">${icon('back')}</a><b>${days[0].toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} – ${days[6].toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}</b><a class="btn ghost" href="${shift(7)}" aria-label="Next week">${icon('chev')}</a></div>
<div class="list">${days.map(d => { const k = rcYmd(d), es = (plan[k] || []).slice().sort((a, b) => RC_MEALS.findIndex(m => m[0] === a.meal) - RC_MEALS.findIndex(m => m[0] === b.meal));
return `<div class="row" style="align-items:flex-start${k === today ? ';outline:2px solid var(--accent);outline-offset:-2px;border-radius:12px' : ''}"><span style="width:52px;flex:none"><b>${d.toLocaleDateString(undefined, { weekday: 'short' })}</b><small style="display:block">${d.getDate()}</small></span>
<span class="grow">${es.map(e => `<span style="display:flex;align-items:center;gap:6px;margin:2px 0"><a href="#/recipes/r/${encodeURIComponent(e.slug)}" class="grow" style="white-space:normal">${esc(e.name)}${e.meal !== 'dinner' ? ` <small>· ${esc(e.meal)}</small>` : ''}</a><button class="ibtn" data-rm="${e.id}" aria-label="Remove ${esc(e.name)}" title="Remove">${icon('x')}</button></span>`).join('') || '<small>Nothing planned</small>'}</span>
<span class="grow">${es.map(e => `<span style="display:flex;align-items:center;gap:6px;margin:2px 0"><a href="#/recipes/r/${encodeURIComponent(e.slug)}" class="grow" style="white-space:normal">${esc(e.name)}${e.meal !== 'dinner' ? ` <small>· ${esc(e.meal)}</small>` : ''}</a><button class="ibtn" data-cook="${esc(e.slug)}|${esc(e.name)}" aria-label="Cooked ${esc(e.name)}" title="Cooked it: take it out of the pantry">${icon('check')}</button><button class="ibtn" data-rm="${e.id}" aria-label="Remove ${esc(e.name)}" title="Remove">${icon('x')}</button></span>`).join('') || '<small>Nothing planned</small>'}</span>
<button class="ibtn" data-add="${k}" aria-label="Plan a meal on ${d.toLocaleDateString(undefined, { weekday: 'long' })}" title="Add a meal">${icon('plus')}</button></div>`; }).join('')}</div>
<div class="bar"><button class="btn" id="rcpshop"${count ? '' : ' disabled'}>${icon('check')}Add this week's ingredients to the shopping list</button><button class="btn ghost" id="rcpantry">Pantry</button></div><div id="rcphave"></div></div>`;
document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => rcPlanPick(null, '', '', b.dataset.add));
document.querySelectorAll('[data-cook]').forEach(b => b.onclick = () => { const i = b.dataset.cook.indexOf('|'); rcCook(b.dataset.cook.slice(0, i), b.dataset.cook.slice(i + 1)); });
document.querySelectorAll('[data-rm]').forEach(b => b.onclick = async () => { try { await rcApi('/plan/remove', rcOpt('POST', { id: b.dataset.rm })); rcPlan(rcYmd(mon)); } catch (e) { toast(e.message); } });
$('#rcpantry').onclick = () => { location.hash = '#/recipes/pantry'; };
if (count) rcApi(`/plan/pantry?from=${from}&to=${to}`).then(r => { const box = $('#rcphave'); if (!box || !r.items.length) return; const names = [...new Set(r.items.map(i => i.have))]; box.innerHTML = `<p class="sub" style="white-space:normal">You may already have ${esc(names.slice(0, 8).join(', '))}${names.length > 8 ? '…' : ''} (${r.items.length} ingredient${r.items.length === 1 ? '' : 's'} this week). They still go on the shopping list; tick them off there.</p>`; }).catch(() => {});
@@ -139,3 +140,22 @@ function rcPlanPick(slug, name, rid, date) {
let t = 0; const find = async () => { try { const r = await rcApi('/?page=1&q=' + encodeURIComponent(el.querySelector('#rcpq').value)); el.querySelector('#rcpr').innerHTML = r.items.slice(0, 8).map(x => `<a href="#" class="row" data-s="${esc(x.slug)}" data-n="${esc(x.name)}" data-i="${esc(x.id)}"><span class="grow">${esc(x.name)}</span></a>`).join('') || '<div class="row"><span class="grow"><small>No recipes found</small></span></div>'; el.querySelectorAll('#rcpr [data-s]').forEach(a => a.onclick = ev => { ev.preventDefault(); save(a.dataset.s, a.dataset.n, a.dataset.i); }); } catch (e) { el.querySelector('#rcpe').textContent = e.message; } };
el.querySelector('#rcpq').oninput = () => { clearTimeout(t); t = setTimeout(find, 250); }; find();
}
/** "Cooked it": shows what cooking this recipe would take out of the pantry (an estimate), lets you untick lines, and takes the rest out when you confirm. */
async function rcCook(slug, name, scale = 1) {
const el = openSheet('Cooked ' + name, 'What this takes out of the pantry', '<div id="rcck"><p class="sub">Working it out…</p></div>');
const show = async sc => {
let r; try { r = await rcApi(`/cook-plan?slug=${encodeURIComponent(slug)}&scale=${sc}`); } catch (e) { el.querySelector('.sheet').insertAdjacentHTML('beforeend', `<p class="err">${esc(e.message)}</p>`); return; }
const ok = r.rows.filter(x => x.ok), skip = r.rows.filter(x => !x.ok), body = `<div id="rcck"><div class="seg" role="radiogroup" aria-label="How much did you make">${[[0.5, 'Half'], [1, 'As written'], [2, 'Double'], [3, 'Triple']].map(([v, l]) => `<a href="#" role="radio" aria-checked="${sc === v}" data-sc="${v}" class="${sc === v ? 'on' : ''}">${l}</a>`).join('')}</div>
${ok.length ? `<div class="list">${ok.map((x, i) => `<label class="row" style="cursor:pointer"><input type="checkbox" data-i="${i}" checked><span class="grow" style="white-space:normal"><b>${esc(x.item.name)}</b> −${esc(x.takeLabel)}<small>${esc(x.line)} · ${x.left === 0 ? 'none' : esc(x.leftLabel || x.left)} left${x.why ? ' · ' + esc(x.why) : ''}</small></span></label>`).join('')}</div>` : '<p class="empty">Nothing in your pantry matches this recipe\'s ingredients that can be counted off.</p>'}
${skip.length ? `<h3 class="sub" style="margin:12px 0 4px">Left alone</h3><div class="list">${skip.map(x => `<div class="row"><span class="grow" style="white-space:normal"><b>${esc(x.item.name)}</b><small>${esc(x.line)} · ${esc(x.why)}</small></span></div>`).join('')}</div>` : ''}
<p class="sub" style="margin-top:8px">These are estimates (a cup of flour is about 4¼ oz). Untick anything that is not right.</p><div id="rcce" class="err"></div>
<div class="bar"><a class="btn ghost" href="#/recipes/pantry" id="rccp" style="flex:1;justify-content:center">Pantry</a>${ok.length ? '<button class="btn" id="rccgo" style="flex:1;justify-content:center">Take these out</button>' : ''}</div></div>`;
el.querySelector('#rcck').outerHTML = body;
el.querySelectorAll('[data-sc]').forEach(a => a.onclick = e => { e.preventDefault(); show(+a.dataset.sc); });
const go = el.querySelector('#rccgo'); if (go) go.onclick = async () => { const picks = [...el.querySelectorAll('[data-i]:checked')].map(c => ok[+c.dataset.i]); if (!picks.length) return closeSheet();
try { const t = await jfetch('/api/pantry/take', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ takes: picks.map(x => ({ id: x.item.id, amount: x.take, unit: x.unit })), note: 'cooked ' + name }) }); closeSheet(); const low = t.done.filter(d => d.isLow).map(d => d.name); toast(`Took ${t.done.length} item${t.done.length === 1 ? '' : 's'} out of the pantry${low.length ? '. Running low: ' + low.join(', ') : ''}`, low.length ? { label: 'Pantry', run: () => { location.hash = '#/recipes/pantry'; } } : undefined); }
catch (e) { el.querySelector('#rcce').textContent = e.message; } };
};
show(scale);
}
+1 -1
View File
@@ -1,5 +1,5 @@
// App-shell cache only; Seafile/API traffic always goes to the network.
const CACHE = 'hub-v125';
const CACHE = 'hub-v127';
const KEEP = /^hub-(offline|share|who|data-)/; // caches that outlive a new version
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];