From a31fdcdabd525e4fce59a3112e4a42af793023c2 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:30:33 -0400 Subject: [PATCH] Recipes: tick off ingredients and steps while cooking (remembered per device), ingredients beside steps on wide screens Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- public/recipes-view.js | 17 +++++++++++++++-- public/style.css | 5 ++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a3bf0c6..37f1ea4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/public/recipes-view.js b/public/recipes-view.js index 3965524..5aab6bb 100644 --- a/public/recipes-view.js +++ b/public/recipes-view.js @@ -60,9 +60,22 @@ async function rcOne(slug) { ${facts.length ? `

${facts.map(f => `${esc(f[0])} ${esc(f[1])}`).join(' ยท ')}

` : ''} ${r.orgUrl ? `

Original page

` : ''}
Shopping list
-

Ingredients

${r.ingredients.map(i => i.title && !i.text ? `

${esc(i.title)}

` : `
${esc(i.text)}
`).join('') || '

No ingredients.

'}
-

Steps

    ${r.steps.map(s => `
  1. ${s.title ? `${esc(s.title)} ` : ''}${esc(s.text).replace(/\n/g, '
    ')}
  2. `).join('') || '
  3. No steps.
  4. '}
+

Ingredients

+
${r.ingredients.map((i, n) => i.title && !i.text ? `

${esc(i.title)}

` : ``).join('') || '

No ingredients.

'}
+

Steps

    ${r.steps.map((s, n) => `
  1. ${s.title ? `${esc(s.title)} ` : ''}${esc(s.text).replace(/\n/g, '
    ')}
  2. `).join('') || '
  3. No steps.
  4. '}
${r.notes.length ? `

Notes

${r.notes.map(n => `

${n.title ? `${esc(n.title)}
` : ''}${esc(n.text).replace(/\n/g, '
')}

`).join('')}` : ''}`; + // 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; } diff --git a/public/style.css b/public/style.css index 76e5637..0fd90fc 100644 --- a/public/style.css +++ b/public/style.css @@ -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; }