Lucy bind paths + LAN host pins replaced with env defaults. Repository URLs → git.sulkta.com. Audit-changelog scaffolding stripped from inline comments (technical reasoning preserved). README sheds marketing scaffolding. AI-speak in load-bearing prompts/SOULs left alone — that IS the product.
80 lines
3.5 KiB
HTML
80 lines
3.5 KiB
HTML
{% extends "_base.html" %}
|
|
{% block title %}Picks · Cauldron{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="page-head">
|
|
<div class="crumb">// picks · household pool</div>
|
|
<h1>the <span class="accent">pool</span></h1>
|
|
<div class="lede">
|
|
{% if picks %}
|
|
{{ picks|length }} {% if picks|length == 1 %}recipe{% else %}recipes{% endif %} pinned by your household ({{ household_size }} {% if household_size == 1 %}member{% else %}members{% endif %}). next ai meal-plan run uses all of these.
|
|
{% else %}
|
|
empty pool. tap the mushroom 🍄 on any recipe in <a href="/recipes">the grimoire</a> to pin it for the next meal-plan run.
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if picks %}
|
|
<section class="panel purple">
|
|
<div class="panel-head">
|
|
<h2>pinned</h2>
|
|
<span class="ctx">{{ picks|length }} {% if picks|length == 1 %}recipe{% else %}recipes{% endif %}</span>
|
|
</div>
|
|
<ul id="picks-list" style="list-style: none; padding: 0; margin: 0;">
|
|
{% for p in picks %}
|
|
<li data-slug="{{ p.slug }}" style="padding: 12px 0; border-bottom: 1px solid var(--line-soft);">
|
|
<div style="display: flex; justify-content: space-between; align-items: baseline; gap: 14px;">
|
|
<a href="/recipes/{{ p.slug }}" style="flex: 1; color: var(--bone); font-family: var(--serif); font-size: 1.05em; border: none;">{{ p.name }}</a>
|
|
{% if p.mine %}
|
|
<button class="btn js-unpin" type="button" style="font-size: 11px; padding: .35em .9em;">unpin</button>
|
|
{% endif %}
|
|
</div>
|
|
<div style="margin-top: 4px; color: var(--muted); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; font-family: var(--mono);">
|
|
🍄 pinned by
|
|
{% for picker in p.pickers %}<span style="color: var(--green-bright);">{{ picker }}</span>{% if not loop.last %} · {% endif %}{% endfor %}
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="panel green">
|
|
<div class="panel-head">
|
|
<h2>next</h2>
|
|
</div>
|
|
<p>once the meal plan generator lands in <code>v0.3</code>, this whole pool seeds the week — guaranteed slots for pinned dishes, ai fills around them.</p>
|
|
<p class="muted">picks are shared across your household. you can only unpin your own; ask whoever pinned the others to drop them.</p>
|
|
</section>
|
|
{% else %}
|
|
<section class="panel">
|
|
<p>head to <a href="/recipes">the grimoire</a>, tap the mushroom 🍄 on any recipe to pin it.</p>
|
|
<p class="muted">picks are shared. abby's pins, your pins, bay's pins — all combine.</p>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<script>
|
|
// Delegated unpin listener — slug is read from the parent <li>'s
|
|
// data-slug attribute (HTML-attribute context, autoescaped by Jinja),
|
|
// never interpolated into a JS string literal inside HTML. The prior
|
|
// `onclick="removePick('{{ slug }}',...)"` pattern was a stored-XSS
|
|
// surface because HTML attribute decoding returns the bare `'` to the
|
|
// JS engine.
|
|
document.getElementById('picks-list')?.addEventListener('click', async (ev) => {
|
|
const btn = ev.target.closest('.js-unpin');
|
|
if (!btn) return;
|
|
const li = btn.closest('li');
|
|
const slug = li?.dataset?.slug;
|
|
if (!slug) return;
|
|
btn.disabled = true; btn.textContent = '…';
|
|
try {
|
|
const r = await fetch(`/api/picks/${encodeURIComponent(slug)}`, { method: 'DELETE' });
|
|
if (!r.ok) throw new Error(r.status);
|
|
li.remove();
|
|
if (!document.querySelectorAll('#picks-list li').length) location.reload();
|
|
} catch (e) {
|
|
btn.disabled = false; btn.textContent = 'unpin';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|