'use strict'; // ---- Recipes: your Mealie recipes and shopping list, through the hub (the server holds the Mealie token). A recipe's ingredients go on one shopping list, which is // also kept in Tasks (a "Shopping" list there), so it can be ticked off from either place. const rcApi = (p, opts) => jfetch('/api/recipes' + p, opts); const rcOpt = (m, o) => ({ method: m, headers: { 'content-type': 'application/json' }, body: o ? JSON.stringify(o) : undefined }); const rcTime = m => { m = +m || 0; if (!m) return ''; const h = Math.floor(m / 60); return h ? h + ' h' + (m % 60 ? ' ' + (m % 60) + ' min' : '') : m + ' min'; }; const rcTabs = on => `
${icon('chef')}Recipes${icon('check')}Shopping list
`; let rcQuery = '', rcPage = 1; async function viewRecipes(args = []) { const v = $('#view'); v.innerHTML = '

Recipes

Loading…

'; try { if (args[0] === 'r' && args[1]) return await rcOne(decodeURIComponent(args[1])); if (args[0] === 'shop') return await rcShop(); return await rcHome(); } catch (e) { if (!location.hash.startsWith('#/recipes')) return; v.innerHTML = `

Recipes

${esc(e.message)}

`; $('#rcretry').onclick = () => route(); } } async function rcHome() { const draw = (items, more, page) => { const g = $('#rcgrid'); if (!g) return; const html = items.map(r => `${r.image ? `` : ''} ${esc(r.name)}${esc([rcTime(r.minutes), r.rating ? '★'.repeat(Math.round(r.rating)) : ''].filter(Boolean).join(' · '))}`).join(''); if (page > 1) g.insertAdjacentHTML('beforeend', html); else g.innerHTML = html || '

No recipes found.

'; const mb = $('#rcmore'); if (mb) mb.hidden = !more; }; const first = await rcApi('/?page=1&q=' + encodeURIComponent(rcQuery)); if (!location.hash.startsWith('#/recipes')) return; $('#view').innerHTML = `

Recipes

${rcTabs('all')}
`; rcPage = 1; draw(first.items, first.more, 1); let timer, seq = 0; const load = async page => { const my = ++seq; try { const j = await rcApi('/?page=' + page + '&q=' + encodeURIComponent(rcQuery)); if (my === seq) { rcPage = page; draw(j.items, j.more, page); } } catch (e) { toast(e.message); } }; $('#rcq').oninput = e => { clearTimeout(timer); timer = setTimeout(() => { rcQuery = e.target.value.trim(); load(1); }, 350); }; $('#rcmore').onclick = () => load(rcPage + 1); $('#rcimp').onclick = () => { const el = openSheet('Import a recipe', 'Mealie reads the recipe from the web page', `
`); el.querySelector('#rcx').onclick = closeSheet; const go = el.querySelector('#rcgo'), err = el.querySelector('#rce'), inp = el.querySelector('#rcurl'); inp.focus(); const run = async () => { err.textContent = ''; go.disabled = true; go.textContent = 'Importing…'; try { const r = await rcApi('/import', rcOpt('POST', { url: inp.value.trim() })); closeSheet(); toast('Imported'); location.hash = '#/recipes/r/' + encodeURIComponent(r.slug); } catch (e) { err.textContent = e.message; go.disabled = false; go.textContent = 'Import'; } }; go.onclick = run; inp.onkeydown = e => { if (e.key === 'Enter') run(); }; }; } async function rcOne(slug) { const r = await rcApi('/r/' + encodeURIComponent(slug)); if (!location.hash.startsWith('#/recipes')) return; const facts = [['Prep', rcTime(r.prep)], ['Cook', rcTime(r.cook)], ['Total', rcTime(r.total)], ['Serves', r.servings]].filter(f => f[1]); $('#view').innerHTML = `
${icon('back')}Recipes
${r.image ? `` : ''}

${esc(r.name)}

${r.description ? `

${esc(r.description)}

` : ''} ${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. '}
${r.notes.length ? `

Notes

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

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

`).join('')}` : ''}
`; $('#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; } catch (err) { toast(err.message); b.disabled = false; } }; } async function rcShop() { const draw = async l => { if (!location.hash.startsWith('#/recipes/shop')) return; const open = l.items.filter(i => !i.checked), done = l.items.filter(i => i.checked), row = i => ``; $('#view').innerHTML = `

Shopping list

${rcTabs('shop')}
${open.map(row).join('') || '

Nothing to buy. Open a recipe and add its ingredients.

'}
${done.length ? `

Got it

${done.map(row).join('')}
` : ''}

Also in Tasks as the “Shopping” list: ticking an item there ticks it here.

`; document.querySelectorAll('.rcitem input').forEach(c => c.onchange = async () => { c.disabled = true; try { await rcApi('/shopping/check', rcOpt('POST', { id: c.dataset.id, checked: c.checked })); draw(await rcApi('/shopping')); } catch (e) { toast(e.message); c.checked = !c.checked; c.disabled = false; } }); $('#rcnew').onsubmit = async e => { e.preventDefault(); const t = $('#rctext').value.trim(); if (!t) return; try { await rcApi('/shopping/add', rcOpt('POST', { text: t })); draw(await rcApi('/shopping')); } catch (err) { toast(err.message); } }; if ($('#rcclear')) $('#rcclear').onclick = async () => { try { await rcApi('/shopping/clear', rcOpt('POST', {})); draw(await rcApi('/shopping')); } catch (e) { toast(e.message); } }; }; try { await rcApi('/shopping/sync', rcOpt('POST', {})); } catch {} // (opening the list also brings Tasks up to date) await draw(await rcApi('/shopping')); }