Recipes: tick off ingredients and steps while cooking (remembered per device), ingredients beside steps on wide screens
build-container / build (push) Successful in 6m11s
build-container / build (push) Successful in 6m11s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efd17f4263
commit
a31fdcdabd
3 files changed
+20
-4
No files matched your search
@@ -148,7 +148,7 @@ playing an episode or pressing the button still works. Settings -> Health shows
|
||||
|
||||
- **Podcast discovery**: *Add podcast* searches Apple's free directory and, when `PODCASTINDEX_KEY` and `PODCASTINDEX_SECRET` are set (a free key from podcastindex.org), Podcast Index too, with topic buttons (News, History, Science...) that list trending shows. A feed in both appears once.
|
||||
- **Search what's said** (Podcast screen): finds a word or phrase in every transcript on the server and plays that episode from that moment. Only podcasts on your own list (and audiobooks) are searched; older transcripts are matched to their episode the first time.
|
||||
- **Recipes** (needs `MEALIE_TOKEN`; `MEALIE_URL` defaults to `https://mealie.necloud.us`): browse and search your Mealie recipes, read one (ingredients, steps, times), import a recipe from a web link, and add its ingredients to the Mealie shopping list (Mealie adds up repeats). The shopping list is also kept in **Tasks** as a "Shopping" list: ticking, adding or deleting an item in either place is carried over when either screen opens or an item changes. Turn the tab on per person in the admin screen.
|
||||
- **Recipes** (needs `MEALIE_TOKEN`; `MEALIE_URL` defaults to `https://mealie.necloud.us`): browse and search your Mealie recipes, read one (ingredients, steps, times), import a recipe from a web link, and add its ingredients to the Mealie shopping list (Mealie adds up repeats). The shopping list is also kept in **Tasks** as a "Shopping" list: ticking, adding or deleting an item in either place is carried over when either screen opens or an item changes. In a recipe, tick ingredients (and tap steps) as you go; this is remembered on the device, with *Uncheck all* to start over. On wide screens the ingredients sit beside the steps. Turn the tab on per person in the admin screen.
|
||||
- **Contacts**: an address book per person (name, company, emails, phones, address, birthday, notes). Mail suggests contacts while you type a recipient, a message you are reading can be saved as a contact (or opens the one you have), birthdays appear in the Calendar, and .vcf files from iCloud, Google or a phone can be imported and exported.
|
||||
|
||||
## Store
|
||||
|
||||
+15
-2
@@ -60,9 +60,22 @@ async function rcOne(slug) {
|
||||
${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><a class="btn ghost" href="#/recipes/shop">Shopping list</a></div>
|
||||
<h2>Ingredients</h2><div class="list">${r.ingredients.map(i => i.title && !i.text ? `<p class="sub" style="margin:10px 0 2px"><b>${esc(i.title)}</b></p>` : `<div class="row"><span class="grow">${esc(i.text)}</span></div>`).join('') || '<p class="empty">No ingredients.</p>'}</div>
|
||||
<h2>Steps</h2><ol class="rcsteps">${r.steps.map(s => `<li>${s.title ? `<b>${esc(s.title)}</b> ` : ''}${esc(s.text).replace(/\n/g, '<br>')}</li>`).join('') || '<li class="empty">No steps.</li>'}</ol>
|
||||
<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>
|
||||
${r.notes.length ? `<h2>Notes</h2>${r.notes.map(n => `<p>${n.title ? `<b>${esc(n.title)}</b><br>` : ''}${esc(n.text).replace(/\n/g, '<br>')}</p>`).join('')}` : ''}</div>`;
|
||||
// What you have used and done is remembered on this device for each recipe, so you can leave and come back while cooking.
|
||||
const sk = 'rc_done:' + r.id; let done = store.get(sk, { i: [], s: [] }); if (!done || !Array.isArray(done.i)) done = { i: [], s: [] };
|
||||
const paint = () => {
|
||||
document.querySelectorAll('.rcing input').forEach(c => { const on = done.i.includes(+c.dataset.n); c.checked = on; c.nextElementSibling.style.cssText = on ? 'text-decoration:line-through;color:var(--muted)' : ''; });
|
||||
document.querySelectorAll('.rcsteps li[data-s]').forEach(li => li.classList.toggle('done', done.s.includes(+li.dataset.s)));
|
||||
$('#rcreset').hidden = !done.i.length && !done.s.length;
|
||||
};
|
||||
const flip = (list, n) => { const k = done[list].indexOf(n); if (k < 0) done[list].push(n); else done[list].splice(k, 1); if (done.i.length || done.s.length) store.set(sk, done); else store.del(sk); paint(); };
|
||||
document.querySelectorAll('.rcing input').forEach(c => c.onchange = () => flip('i', +c.dataset.n));
|
||||
document.querySelectorAll('.rcsteps li[data-s]').forEach(li => li.onclick = () => flip('s', +li.dataset.s));
|
||||
$('#rcreset').onclick = () => { done = { i: [], s: [] }; store.del(sk); paint(); };
|
||||
paint();
|
||||
$('#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; }
|
||||
|
||||
+4
-1
@@ -954,7 +954,10 @@ body.vizopen { overflow: hidden; }
|
||||
/* Recipes and Contacts */
|
||||
.rchero { display: flex; gap: 16px; align-items: flex-start; margin-bottom: 12px; }
|
||||
@media (max-width: 560px) { .rchero { flex-direction: column; } .rchero .vimg { width: 100% !important; } }
|
||||
.rcsteps { padding-left: 22px; line-height: 1.5; } .rcsteps li { margin: 0 0 10px; }
|
||||
.rcsteps { padding-left: 22px; line-height: 1.5; } .rcsteps li { margin: 0 0 12px; cursor: pointer; } .rcsteps li.done { color: var(--muted); text-decoration: line-through; }
|
||||
.rchead { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .rchead h2 { margin: 16px 0 8px; }
|
||||
.rccols { display: block; }
|
||||
@media (min-width: 900px) { .rccols { display: grid; grid-template-columns: minmax(280px, 2fr) 3fr; gap: 28px; align-items: start; } .rccols > section:first-child { position: sticky; top: 8px; } }
|
||||
.rcitem input { width: 22px; height: 22px; flex: none; }
|
||||
.ctgrp { margin: 14px 0 4px; font-size: 13px; font-weight: 600; color: var(--muted); }
|
||||
.ctav { width: 40px; height: 40px; border-radius: 50%; flex: none; display: grid; place-items: center; color: #fff; font-weight: 600; }
|
||||
|
||||
Reference in new issue
Block a user