Files
recipe-mockup/js/views/Pantry.js
ulfrxdev 868862d031
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m20s
Add ingredients' products
2026-04-07 22:51:30 +02:00

557 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
INGREDIENTS,
CATEGORY_LABELS,
PRODUCTS,
pantryQtyStep,
getProductsForIngredient,
ingredientHasProducts,
} from '../data/catalog.js?v=6';
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts, getPantryGeneric } from '../services/pantryShopping.js?v=2';
import { showAppToast } from '../ui/toast.js';
/* ── helpers ── */
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function unitLabel(u) {
return u === 'szt' ? 'szt.' : u;
}
function normalizeSearch(q) {
return String(q).trim().toLowerCase();
}
const CATEGORY_ICONS = {
pieczywo: 'fa-bread-slice',
nabial: 'fa-cheese',
mieso_ryby: 'fa-drumstick-bite',
warzywa: 'fa-carrot',
owoce: 'fa-apple-whole',
suche: 'fa-wheat-awn',
przyprawy: 'fa-leaf',
inne: 'fa-jar',
};
/* ── state ── */
let showOnlyStock = false;
let editingId = null;
/** @type {Set<string>} */
const selectedCategories = new Set();
let editShopStep = 1;
let editShopUsesPacks = false;
const BOTTOM = '5.25rem';
const HIDDEN_Y = `translateY(calc(100% + ${BOTTOM}))`;
/* ══════════════════════ HTML SHELL ══════════════════════ */
export function getPantryHTML() {
return `
<div id="pantry-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-gray-50 z-10 pb-24">
<div class="shrink-0 bg-white border-b border-gray-100 mt-3 px-4 pt-2 pb-2.5 space-y-2">
<div class="flex items-center gap-2.5 bg-gray-100 rounded-2xl px-3.5 py-2.5 focus-within:ring-2 focus-within:ring-gray-900/10 transition-all">
<i class="fas fa-search text-gray-400 text-xs"></i>
<input type="search" id="pantry-search" autocomplete="off" placeholder="Szukaj produktu…"
class="flex-1 bg-transparent outline-none text-sm text-gray-800 placeholder-gray-400" />
</div>
<div id="pantry-category-chips" class="flex gap-2 overflow-x-auto no-scrollbar -mx-1 px-1 pb-0.5"></div>
<div class="flex items-center justify-end">
<label class="flex items-center gap-2 cursor-pointer select-none">
<span class="text-xs font-medium text-gray-500">Tylko na stanie</span>
<button type="button" id="pantry-stock-toggle" role="switch" aria-checked="false"
class="relative w-10 h-[22px] rounded-full bg-gray-200 transition-colors duration-200 shrink-0">
<span class="absolute left-0.5 top-0.5 w-[18px] h-[18px] bg-white rounded-full shadow-sm transition-transform duration-200"></span>
</button>
</label>
</div>
</div>
<div id="pantry-scroll" class="flex-1 overflow-y-auto no-scrollbar">
<div id="pantry-board" class="px-4 pt-3 pb-4 space-y-2"></div>
</div>
<!-- ── product sheet ── -->
<div id="pv2-edit-bg" class="absolute inset-0 z-[38] bg-black/40 hidden opacity-0 transition-opacity duration-200"></div>
<div id="pv2-edit-sheet" class="absolute left-0 right-0 z-[40] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] px-5 pt-2 pb-4 flex flex-col gap-2.5 max-h-[75%] min-h-0 overflow-y-auto no-scrollbar"
style="bottom:${BOTTOM};transform:${HIDDEN_Y};transition:transform 300ms cubic-bezier(.32,.72,0,1)">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto shrink-0"></div>
<div class="shrink-0">
<h2 id="pv2-edit-name" class="text-[15px] font-bold text-gray-900 leading-snug"></h2>
<p id="pv2-edit-meta" class="text-[11px] text-gray-500"></p>
</div>
<div class="shrink-0 flex items-center gap-2">
<span class="text-[10px] font-semibold uppercase tracking-wider text-gray-400 w-[3.2rem] shrink-0">Zapas</span>
<button type="button" id="pv2-edit-minus" class="w-9 h-9 rounded-xl bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0">
<i class="fas fa-minus text-xs"></i>
</button>
<div class="flex items-baseline gap-0.5">
<input type="number" id="pv2-edit-qty" min="0" step="1" inputmode="decimal"
class="w-14 text-center text-lg font-bold tabular-nums bg-transparent outline-none" value="0" />
<span id="pv2-edit-unit" class="text-xs text-gray-400 font-medium"></span>
</div>
<button type="button" id="pv2-edit-plus" class="w-9 h-9 rounded-xl bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0">
<i class="fas fa-plus text-xs"></i>
</button>
</div>
<div id="pv2-product-breakdown" class="shrink-0"></div>
<div class="border-t border-gray-100 shrink-0"></div>
<div class="shrink-0 space-y-1">
<div class="flex items-center gap-2">
<span class="text-[10px] font-semibold uppercase tracking-wider text-gray-400 w-[3.2rem] shrink-0">Lista</span>
<button type="button" id="pv2-shop-minus" class="w-9 h-9 rounded-xl bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0">
<i class="fas fa-minus text-xs"></i>
</button>
<div class="flex items-baseline gap-0.5">
<input type="number" id="pv2-shop-qty" min="1" step="1" inputmode="numeric"
class="w-14 text-center text-lg font-bold tabular-nums bg-transparent outline-none" value="1" />
<span id="pv2-shop-unit" class="text-xs text-gray-400 font-medium"></span>
</div>
<button type="button" id="pv2-shop-plus" class="w-9 h-9 rounded-xl bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0">
<i class="fas fa-plus text-xs"></i>
</button>
<button type="button" id="pv2-shop-add" class="ml-auto shrink-0 px-3.5 py-2 rounded-xl bg-gray-900 text-white text-[11px] font-semibold hover:bg-black transition-colors active:scale-95">
<i class="fas fa-cart-plus text-[9px] mr-1"></i>Dodaj
</button>
</div>
<p id="pv2-shop-hint" class="text-[10px] text-gray-400 pl-[3.5rem]"></p>
</div>
<div id="pv2-edit-nutrition" class="shrink-0"></div>
</div>
</div>`;
}
/* ══════════════════════ CATEGORY CHIPS (multi-select) ══════════════════════ */
function allCategoryKeys() {
const s = new Set();
Object.values(INGREDIENTS).forEach(d => s.add(d.category));
return [...s].sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)));
}
function renderCategoryChips() {
const wrap = document.getElementById('pantry-category-chips');
if (!wrap) return;
const keys = allCategoryKeys();
wrap.innerHTML = keys.map(k => {
const active = selectedCategories.has(k);
const icon = CATEGORY_ICONS[k] || 'fa-jar';
const cls = active
? 'shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-semibold bg-gray-900 text-white transition-colors'
: 'shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-semibold bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors';
return `<button type="button" data-cat="${esc(k)}" class="pv2-cat-chip ${cls}"><i class="fas ${icon} text-[10px]"></i>${esc(categoryLabel(k))}</button>`;
}).join('');
wrap.querySelectorAll('.pv2-cat-chip').forEach(btn => {
btn.addEventListener('click', () => {
const cat = btn.dataset.cat;
if (selectedCategories.has(cat)) selectedCategories.delete(cat);
else selectedCategories.add(cat);
renderCategoryChips();
renderBoard();
});
});
}
/* ══════════════════════ BOARD RENDERING ══════════════════════ */
function getFilteredIds(searchRaw) {
const q = normalizeSearch(searchRaw);
return Object.keys(INGREDIENTS).filter(id => {
const d = INGREDIENTS[id];
if (selectedCategories.size > 0 && !selectedCategories.has(d.category)) return false;
if (!q) return true;
return d.name.toLowerCase().includes(q) || (CATEGORY_LABELS[d.category] || '').toLowerCase().includes(q);
}).sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl'));
}
function chipHtml(id, pantry) {
const def = INGREDIENTS[id];
const qty = getPantryTotal(id, pantry);
const u = unitLabel(def.pantryUnit);
if (qty > 0) {
return `<button type="button" class="pv2-chip inline-flex flex-col items-start px-3.5 py-2.5 rounded-xl bg-emerald-50 border border-emerald-200/80 text-left hover:bg-emerald-100/80 transition-colors active:scale-[0.96]" data-id="${esc(id)}">
<span class="text-[13px] font-semibold text-gray-900 leading-tight whitespace-nowrap">${esc(def.name)}</span>
<span class="text-[11px] text-emerald-600 font-semibold tabular-nums leading-tight mt-0.5">${Math.round(qty)} ${esc(u)}</span>
</button>`;
}
return `<button type="button" class="pv2-chip inline-flex items-center px-3.5 py-2.5 rounded-xl border border-dashed border-gray-200 text-left hover:border-gray-300 hover:bg-white transition-colors active:scale-[0.96] group" data-id="${esc(id)}">
<span class="text-[13px] font-medium text-gray-400 group-hover:text-gray-600 whitespace-nowrap transition-colors">${esc(def.name)}</span>
<i class="fas fa-plus text-[8px] text-gray-300 group-hover:text-gray-500 ml-1.5 transition-colors"></i>
</button>`;
}
function groupByCategory(ids) {
/** @type {Map<string, string[]>} */
const groups = new Map();
for (const id of ids) {
const cat = INGREDIENTS[id].category;
if (!groups.has(cat)) groups.set(cat, []);
groups.get(cat).push(id);
}
return [...groups.keys()]
.sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)))
.map(cat => ({ cat, ids: groups.get(cat) }));
}
function renderBoard() {
const root = document.getElementById('pantry-board');
if (!root) return;
const q = document.getElementById('pantry-search')?.value || '';
const pantry = loadPantry();
const allFiltered = getFilteredIds(q);
const visible = showOnlyStock
? allFiltered.filter(id => getPantryTotal(id, pantry) > 0)
: allFiltered;
if (visible.length === 0) {
root.innerHTML = showOnlyStock
? `<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<i class="fas fa-box-open text-2xl text-gray-300"></i>
</div>
<p class="text-sm font-semibold text-gray-700">Nic na stanie</p>
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Wyłącz filtr, aby zobaczyć cały katalog produktów</p>
</div>`
: `<p class="text-sm text-gray-500 text-center py-10">Brak wyników — zmień wyszukiwanie lub filtry.</p>`;
return;
}
const groups = groupByCategory(visible);
let html = '';
for (const { cat, ids } of groups) {
const icon = CATEGORY_ICONS[cat] || 'fa-jar';
html += `
<div class="mb-4 last:mb-0">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 px-0.5">
<i class="fas ${icon} text-[10px] mr-1"></i>${esc(categoryLabel(cat))}
</p>
<div class="flex flex-wrap gap-2">${ids.map(id => chipHtml(id, pantry)).join('')}</div>
</div>`;
}
root.innerHTML = html;
root.querySelectorAll('.pv2-chip').forEach(btn => {
btn.addEventListener('click', () => openEditSheet(btn.dataset.id));
});
}
/* ══════════════════════ STOCK TOGGLE ══════════════════════ */
function updateToggleVisuals() {
const btn = document.getElementById('pantry-stock-toggle');
if (!btn) return;
const thumb = btn.querySelector('span');
btn.setAttribute('aria-checked', String(showOnlyStock));
if (showOnlyStock) {
btn.classList.remove('bg-gray-200');
btn.classList.add('bg-emerald-500');
thumb?.classList.add('translate-x-[18px]');
} else {
btn.classList.add('bg-gray-200');
btn.classList.remove('bg-emerald-500');
thumb?.classList.remove('translate-x-[18px]');
}
}
/* ══════════════════════ EDIT BOTTOM SHEET ══════════════════════ */
function openEditSheet(ingredientId) {
const def = INGREDIENTS[ingredientId];
if (!def) return;
editingId = ingredientId;
const pantry = loadPantry();
const qty = getPantryTotal(ingredientId, pantry);
const u = unitLabel(def.pantryUnit);
const step = pantryQtyStep(ingredientId);
const pack = def.purchasePack;
const nameEl = document.getElementById('pv2-edit-name');
if (nameEl) nameEl.textContent = def.name;
const metaEl = document.getElementById('pv2-edit-meta');
if (metaEl) {
let meta = categoryLabel(def.category);
if (pack) meta += ` · ${pack.label || `${pack.amount} ${u}`}`;
metaEl.textContent = meta;
}
const qtyEl = document.getElementById('pv2-edit-qty');
if (qtyEl) qtyEl.value = qty > 0 ? String(Math.round(qty)) : '0';
const unitEl = document.getElementById('pv2-edit-unit');
if (unitEl) unitEl.textContent = u;
editShopUsesPacks = Boolean(pack && pack.amount > 0);
editShopStep = editShopUsesPacks ? 1 : step;
const shopQtyEl = document.getElementById('pv2-shop-qty');
if (shopQtyEl) shopQtyEl.value = String(editShopStep);
const shopUnitEl = document.getElementById('pv2-shop-unit');
if (shopUnitEl) shopUnitEl.textContent = editShopUsesPacks ? 'opak.' : u;
const shopHintEl = document.getElementById('pv2-shop-hint');
if (shopHintEl) {
if (editShopUsesPacks) {
const lab = pack.label || `${pack.amount} ${u}`;
shopHintEl.textContent = `1 opak. = ${lab}`;
} else {
shopHintEl.textContent = '';
}
}
// Hide main +/- when products exist (total is sum of product rows)
const hasProds = ingredientHasProducts(ingredientId);
const mainMinus = document.getElementById('pv2-edit-minus');
const mainPlus = document.getElementById('pv2-edit-plus');
const mainQtyInput = document.getElementById('pv2-edit-qty');
if (mainMinus) mainMinus.classList.toggle('hidden', hasProds);
if (mainPlus) mainPlus.classList.toggle('hidden', hasProds);
if (mainQtyInput) mainQtyInput.readOnly = hasProds;
renderProductBreakdown(ingredientId, pantry);
renderNutritionInSheet(def);
const bg = document.getElementById('pv2-edit-bg');
const sheet = document.getElementById('pv2-edit-sheet');
if (!bg || !sheet) return;
bg.classList.remove('hidden');
sheet.classList.remove('hidden');
requestAnimationFrame(() => {
bg.classList.remove('opacity-0');
sheet.style.transform = 'translateY(0)';
});
}
function nutritionListRow(label, valueHtml) {
return `<li class="flex items-baseline justify-between gap-3 py-0.5 border-b border-gray-100/80 last:border-0">
<span class="text-gray-500 shrink-0">${esc(label)}</span>
<span class="text-right font-semibold tabular-nums text-gray-800">${valueHtml}</span>
</li>`;
}
function renderProductBreakdown(ingredientId, pantry) {
const wrap = document.getElementById('pv2-product-breakdown');
if (!wrap) return;
const products = getProductsForIngredient(ingredientId);
if (products.length === 0) { wrap.innerHTML = ''; return; }
const def = INGREDIENTS[ingredientId];
const u = unitLabel(def.pantryUnit);
const pantryProducts = getPantryProducts(ingredientId, pantry);
const generic = getPantryGeneric(ingredientId, pantry);
const productQty = (pid) => {
const item = pantryProducts.find(i => i.productId === pid);
return item ? item.qty : 0;
};
const rows = products.map(p => {
const q = Math.round(productQty(p.id));
return `<div class="flex items-center gap-1.5 py-1" data-product-row="${esc(p.id)}">
<span class="flex-1 text-[12px] text-gray-700 truncate" title="${esc(p.name)}">${esc(p.name)}</span>
<button type="button" class="pv2-prod-minus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="${esc(p.id)}" data-step="${p.packSize}">
<i class="fas fa-minus text-[9px]"></i>
</button>
<span class="pv2-prod-qty w-12 text-center text-[13px] font-semibold tabular-nums text-gray-800">${q} ${esc(u)}</span>
<button type="button" class="pv2-prod-plus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="${esc(p.id)}" data-step="${p.packSize}">
<i class="fas fa-plus text-[9px]"></i>
</button>
</div>`;
}).join('');
const genericRow = `<div class="flex items-center gap-1.5 py-1" data-product-row="_generic">
<span class="flex-1 text-[12px] text-gray-500 italic truncate">Nieokreślony</span>
<button type="button" class="pv2-prod-minus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}">
<i class="fas fa-minus text-[9px]"></i>
</button>
<span class="pv2-prod-qty w-12 text-center text-[13px] font-semibold tabular-nums text-gray-800">${Math.round(generic)} ${esc(u)}</span>
<button type="button" class="pv2-prod-plus w-7 h-7 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 flex items-center justify-center transition-colors active:scale-95 shrink-0" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}">
<i class="fas fa-plus text-[9px]"></i>
</button>
</div>`;
wrap.innerHTML = `
<div class="space-y-0.5 px-0.5">
<p class="text-[9px] font-semibold uppercase tracking-wide text-gray-400 mb-1">Produkty</p>
${rows}
${genericRow}
</div>`;
// Bind product +/- buttons
wrap.querySelectorAll('.pv2-prod-plus, .pv2-prod-minus').forEach(btn => {
btn.addEventListener('click', () => {
if (!editingId) return;
const pid = btn.dataset.pid;
const step = Number(btn.dataset.step) || 1;
const isPlus = btn.classList.contains('pv2-prod-plus');
const pantry = loadPantry();
if (pid === '_generic') {
const cur = getPantryGeneric(editingId, pantry);
const next = Math.max(0, cur + (isPlus ? step : -step));
setPantryQty(editingId, next);
} else {
const items = getPantryProducts(editingId, pantry);
const cur = items.find(i => i.productId === pid)?.qty || 0;
const next = Math.max(0, cur + (isPlus ? step : -step));
setPantryProductQty(editingId, pid, next);
}
// Re-render breakdown and total
const freshPantry = loadPantry();
renderProductBreakdown(editingId, freshPantry);
const totalQty = getPantryTotal(editingId, freshPantry);
setEditQty(totalQty);
});
});
}
function renderNutritionInSheet(def) {
const wrap = document.getElementById('pv2-edit-nutrition');
if (!wrap) return;
const n = def.nutritionPer100g;
if (!n) { wrap.innerHTML = ''; return; }
const refLabel = def.pantryUnit === 'ml' ? '100 ml produktu' : '100 g produktu';
wrap.innerHTML = `
<div class="text-[10px] leading-snug mt-0.5 pt-2 border-t border-gray-100 space-y-1">
<p class="text-[9px] font-semibold uppercase tracking-wide text-gray-500 px-0.5">${esc(refLabel)}</p>
<ul class="space-y-0 rounded-lg bg-white/70 px-2 py-1 ring-1 ring-gray-100/90">
${nutritionListRow('Energia', `${n.kcal} kcal`)}
${nutritionListRow('Białko', `${n.protein} g`)}
${nutritionListRow('Tłuszcz', `${n.fat} g`)}
${nutritionListRow('Węglowodany', `${n.carbs} g`)}
</ul>
</div>`;
}
function closeEditSheet() {
editingId = null;
const bg = document.getElementById('pv2-edit-bg');
const sheet = document.getElementById('pv2-edit-sheet');
if (sheet) sheet.style.transform = HIDDEN_Y;
if (bg) bg.classList.add('opacity-0');
setTimeout(() => {
bg?.classList.add('hidden');
sheet?.classList.add('hidden');
}, 300);
renderBoard();
}
function getEditQty() {
const el = document.getElementById('pv2-edit-qty');
return Math.max(0, parseFloat(String(el?.value).replace(',', '.')) || 0);
}
function setEditQty(v) {
const el = document.getElementById('pv2-edit-qty');
if (el) el.value = String(Math.max(0, Math.round(v)));
}
function applyEditQty(newQty) {
if (!editingId) return;
const v = Math.max(0, Math.round(Number(newQty) * 1000) / 1000 || 0);
setPantryQty(editingId, v);
setEditQty(v);
}
function readShopQty() {
const el = document.getElementById('pv2-shop-qty');
return Math.max(1, Math.round(parseFloat(String(el?.value).replace(',', '.')) || 0));
}
function setShopQty(v) {
const el = document.getElementById('pv2-shop-qty');
if (el) el.value = String(Math.max(1, Math.round(Number(v))));
}
function bindEditSheet() {
document.getElementById('pv2-edit-bg')?.addEventListener('click', closeEditSheet);
document.getElementById('pv2-edit-minus')?.addEventListener('click', () => {
if (!editingId) return;
applyEditQty(Math.max(0, getEditQty() - pantryQtyStep(editingId)));
});
document.getElementById('pv2-edit-plus')?.addEventListener('click', () => {
if (!editingId) return;
applyEditQty(getEditQty() + pantryQtyStep(editingId));
});
document.getElementById('pv2-edit-qty')?.addEventListener('change', () => {
applyEditQty(getEditQty());
});
document.getElementById('pv2-shop-minus')?.addEventListener('click', () => {
setShopQty(Math.max(1, readShopQty() - editShopStep));
});
document.getElementById('pv2-shop-plus')?.addEventListener('click', () => {
setShopQty(readShopQty() + editShopStep);
});
document.getElementById('pv2-shop-qty')?.addEventListener('change', () => {
setShopQty(readShopQty());
});
document.getElementById('pv2-shop-add')?.addEventListener('click', () => {
if (!editingId) return;
const def = INGREDIENTS[editingId];
if (!def) return;
const count = readShopQty();
const u = unitLabel(def.pantryUnit);
if (editShopUsesPacks && def.purchasePack) {
const packAmt = def.purchasePack.amount;
const total = count * packAmt;
const note = `${count}× ${def.purchasePack.label || `${packAmt} ${u}`}`;
addIngredientToKitchenList(editingId, total, note);
showAppToast(`Dodano ${count} op. (${total} ${u}) na listę.`);
} else {
addIngredientToKitchenList(editingId, count);
showAppToast(`Dodano ${count} ${u} na listę.`);
}
window.refreshShopping?.();
});
}
/* ══════════════════════ PUBLIC API ══════════════════════ */
export function refreshPantry() {
renderCategoryChips();
renderBoard();
}
export function setupPantry() {
renderCategoryChips();
renderBoard();
bindEditSheet();
document.getElementById('pantry-search')?.addEventListener('input', () => renderBoard());
document.getElementById('pantry-stock-toggle')?.addEventListener('click', () => {
showOnlyStock = !showOnlyStock;
updateToggleVisuals();
renderBoard();
});
window.refreshPantry = refreshPantry;
}