Added meal planner, pantry and shopping list

This commit is contained in:
2026-03-26 17:10:16 +01:00
parent 7b9db1c5e6
commit c1b3ee5ce6
22 changed files with 2888 additions and 223 deletions

File diff suppressed because it is too large Load Diff

464
js/views/Pantry.js Normal file
View File

@@ -0,0 +1,464 @@
import {
INGREDIENTS,
CATEGORY_LABELS,
pantryQtyStep,
} from '../data/catalog.js';
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty } from '../services/pantryShopping.js';
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 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 = Number(pantry[id]) || 0;
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 => (Number(pantry[id]) || 0) > 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 = Number(pantry[ingredientId]) || 0;
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 = '';
}
}
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 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;
}

View File

@@ -2,144 +2,142 @@ export function getRecipeDetailHTML() {
return `
<div id="recipe-detail-view" class="absolute inset-0 bg-white z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden">
<div class="absolute top-0 w-full p-4 flex justify-between z-40 mt-4">
<button onclick="closeRecipeDetail()" class="w-10 h-10 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-800 hover:bg-white transition-colors">
<i class="fas fa-arrow-left"></i>
<div class="absolute top-0 w-full p-3.5 flex justify-between z-40 mt-3">
<button onclick="closeRecipeDetail()" class="w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-800 hover:bg-white transition-colors">
<i class="fas fa-arrow-left text-[13px]"></i>
</button>
<button class="w-10 h-10 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-400 hover:text-red-500 transition-colors">
<i class="far fa-heart"></i>
<button class="w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center shadow-sm text-gray-400 hover:text-red-500 transition-colors">
<i class="far fa-heart text-[13px]"></i>
</button>
</div>
<div class="h-[260px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
<span class="text-white font-medium text-lg">Zdjęcie: Serek z owocami</span>
<div class="h-[220px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
<span class="text-white font-medium text-[15px]">Zdjęcie: Serek z owocami</span>
</div>
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-8 flex flex-col flex-1 overflow-hidden">
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden">
<div class="mb-4 px-6 shrink-0">
<div class="flex justify-between items-start mb-3">
<h1 class="text-2xl font-bold text-gray-900">Serek wiejski z orzechami i owocami</h1>
<div class="mb-3 px-5 shrink-0">
<div class="flex justify-between items-start mb-2.5">
<h1 class="text-xl font-bold text-gray-900">Serek wiejski z orzechami i owocami</h1>
</div>
<div class="flex flex-wrap gap-2 mb-4">
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Śniadanie</span>
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Wegetariańskie</span>
<span class="px-3 py-1 bg-gray-100 text-gray-700 text-xs rounded-md font-medium">Słodkie</span>
<div class="flex flex-wrap gap-1.5 mb-3">
<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-[11px] rounded-md font-medium">Śniadanie</span>
<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-[11px] rounded-md font-medium">Wegetariańskie</span>
<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-[11px] rounded-md font-medium">Słodkie</span>
</div>
<div class="flex justify-between items-center text-sm text-gray-600 font-medium">
<div class="flex gap-4">
<div class="flex items-center gap-1.5"><i class="fas fa-clock text-gray-400"></i><span>5 min</span></div>
<div class="flex items-center gap-1.5"><i class="fas fa-fire text-gray-400"></i><span>642 kcal</span></div>
<div class="flex justify-between items-center text-[13px] text-gray-600 font-medium">
<div class="flex gap-3.5">
<div class="flex items-center gap-1.5"><i class="fas fa-clock text-gray-400 text-xs"></i><span>5 min</span></div>
<div class="flex items-center gap-1.5"><i class="fas fa-fire text-gray-400 text-xs"></i><span>642 kcal</span></div>
</div>
<div class="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
<button onclick="changeServings(-1)" class="w-6 h-6 bg-white rounded shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-minus text-[10px]"></i></button>
<div class="flex items-center gap-1 px-2">
<span id="servings-count" class="font-bold text-gray-900 text-sm w-3 text-center">1</span>
<span class="text-xs text-gray-500"><i class="fas fa-user-friends"></i></span>
<div class="flex items-center gap-0.5 bg-gray-100 p-0.5 rounded-lg">
<button onclick="changeServings(-1)" class="w-6 h-6 bg-white rounded-md shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-minus text-[9px]"></i></button>
<div class="flex items-center gap-1 px-1.5">
<span id="servings-count" class="font-bold text-gray-900 text-[13px] w-3 text-center tabular-nums">1</span>
<span class="text-[11px] text-gray-500"><i class="fas fa-user-friends"></i></span>
</div>
<button onclick="changeServings(1)" class="w-6 h-6 bg-white rounded shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-plus text-[10px]"></i></button>
<button onclick="changeServings(1)" class="w-6 h-6 bg-white rounded-md shadow-sm flex items-center justify-center text-gray-600 hover:text-black hover:bg-gray-50"><i class="fas fa-plus text-[9px]"></i></button>
</div>
</div>
</div>
<div class="flex border-b border-gray-200 mb-2 px-6 shrink-0">
<button class="flex-1 pb-3 text-sm font-semibold text-gray-900 border-b-2 border-gray-900 tab-btn" onclick="switchTab('ingredients', this)">Składniki</button>
<button class="flex-1 pb-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('steps', this)">Kroki</button>
<button class="flex-1 pb-3 text-sm font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('nutrition', this)">Wartości</button>
<div class="flex border-b border-gray-200 mb-2 px-5 shrink-0">
<button class="flex-1 pb-2.5 text-[13px] font-semibold text-gray-900 border-b-2 border-gray-900 tab-btn" onclick="switchTab('ingredients', this)">Składniki</button>
<button class="flex-1 pb-2.5 text-[13px] font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('steps', this)">Kroki</button>
<button class="flex-1 pb-2.5 text-[13px] font-medium text-gray-500 border-b-2 border-transparent hover:text-gray-700 tab-btn" onclick="switchTab('nutrition', this)">Wartości</button>
</div>
<div class="flex-1 overflow-y-auto px-6 pt-2 pb-10 no-scrollbar relative">
<div class="flex-1 overflow-y-auto px-5 pt-2 pb-10 no-scrollbar relative">
<div id="tab-ingredients" class="tab-content block animate-fade-in">
<div class="flex justify-between items-end mb-4">
<span class="text-xs text-gray-500 font-medium">Zaznacz, by dodać do listy zakupów</span>
<div class="flex justify-between items-end mb-3">
<span class="text-[11px] text-gray-500 font-medium">Zaznacz, by dodać do listy zakupów</span>
</div>
<ul class="space-y-0 mb-6" id="ingredient-list">
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<ul class="space-y-0 mb-5" id="ingredient-list">
<li class="flex items-center gap-2.5 py-2.5 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors">Serek wiejski</span>
<span class="font-medium text-gray-900 text-sm ingredient-amount" data-base-amount="200" data-unit="g">200 g</span>
<span class="text-gray-700 text-[13px] flex-1 ingredient-text transition-colors">Serek wiejski</span>
<span class="font-medium text-gray-900 text-[13px] ingredient-amount tabular-nums" data-base-amount="200" data-unit="g">200 g</span>
</li>
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<li class="flex items-center gap-2.5 py-2.5 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors">Miód</span>
<span class="font-medium text-gray-900 text-sm ingredient-amount" data-base-amount="10" data-unit="g">10 g</span>
<span class="text-gray-700 text-[13px] flex-1 ingredient-text transition-colors">Miód</span>
<span class="font-medium text-gray-900 text-[13px] ingredient-amount tabular-nums" data-base-amount="10" data-unit="g">10 g</span>
</li>
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<li class="flex items-center gap-2.5 py-2.5 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-orzechy">Orzechy włoskie</span>
<div class="flex items-center gap-3">
<button onclick="event.stopPropagation(); openSwapModal('orzechy')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[10px]"></i>
<span class="text-gray-700 text-[13px] flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-orzechy">Orzechy włoskie</span>
<div class="flex items-center gap-2.5">
<button onclick="event.stopPropagation(); openSwapModal('orzechy')" class="w-6 h-6 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[9px]"></i>
</button>
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="50" data-unit="g">50 g</span>
<span class="font-medium text-gray-900 text-[13px] ingredient-amount w-10 text-right tabular-nums" data-base-amount="50" data-unit="g">50 g</span>
</div>
</li>
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<li class="flex items-center gap-2.5 py-2.5 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce1">Truskawki</span>
<div class="flex items-center gap-3">
<button onclick="event.stopPropagation(); openSwapModal('owoce1')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[10px]"></i>
<span class="text-gray-700 text-[13px] flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce1">Truskawki</span>
<div class="flex items-center gap-2.5">
<button onclick="event.stopPropagation(); openSwapModal('owoce1')" class="w-6 h-6 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[9px]"></i>
</button>
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="100" data-unit="g">100 g</span>
<span class="font-medium text-gray-900 text-[13px] ingredient-amount w-10 text-right tabular-nums" data-base-amount="100" data-unit="g">100 g</span>
</div>
</li>
<li class="flex items-center gap-3 py-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<li class="flex items-center gap-2.5 py-2.5 border-b border-gray-100 cursor-pointer hover:bg-gray-50 px-1 -mx-1 transition-colors" onclick="toggleIngredient(this)">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors"><i class="fas fa-check text-[10px] hidden check-icon"></i></div>
<span class="text-gray-700 text-sm flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce2">Borówki ameryk.</span>
<div class="flex items-center gap-3">
<button onclick="event.stopPropagation(); openSwapModal('owoce2')" class="w-7 h-7 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[10px]"></i>
<span class="text-gray-700 text-[13px] flex-1 ingredient-text transition-colors font-medium text-gray-900" id="ingredient-owoce2">Borówki ameryk.</span>
<div class="flex items-center gap-2.5">
<button onclick="event.stopPropagation(); openSwapModal('owoce2')" class="w-6 h-6 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-colors shadow-sm" title="Zamień">
<i class="fas fa-exchange-alt text-[9px]"></i>
</button>
<span class="font-medium text-gray-900 text-sm ingredient-amount w-10 text-right" data-base-amount="100" data-unit="g">100 g</span>
<span class="font-medium text-gray-900 text-[13px] ingredient-amount w-10 text-right tabular-nums" data-base-amount="100" data-unit="g">100 g</span>
</div>
</li>
</ul>
<button class="w-full bg-gray-900 hover:bg-black text-white py-3.5 rounded-xl font-semibold shadow-sm transition-colors text-sm flex items-center justify-center gap-2 mb-6">
<i class="fas fa-plus"></i> Dodaj do listy zakupów
<button class="w-full bg-gray-900 hover:bg-black text-white py-3 rounded-xl font-semibold shadow-sm transition-colors text-[13px] flex items-center justify-center gap-2 mb-5">
<i class="fas fa-plus text-xs"></i> Dodaj do listy zakupów
</button>
</div>
<div id="tab-steps" class="tab-content hidden animate-fade-in">
<div class="space-y-6 pb-6">
<div class="flex gap-4">
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">1</div>
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Przełóż serek wiejski do miseczki.</p></div>
<div class="space-y-5 pb-5">
<div class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-gray-900 text-white flex items-center justify-center text-[11px] font-bold shrink-0 shadow-sm">1</div>
<div class="pt-0.5"><p class="text-[13px] text-gray-600 leading-relaxed">Przełóż serek wiejski do miseczki.</p></div>
</div>
<div class="flex gap-4">
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">2</div>
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Dodaj miód i delikatnie wymieszaj.</p></div>
<div class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-gray-900 text-white flex items-center justify-center text-[11px] font-bold shrink-0 shadow-sm">2</div>
<div class="pt-0.5"><p class="text-[13px] text-gray-600 leading-relaxed">Dodaj miód i delikatnie wymieszaj.</p></div>
</div>
<div class="flex gap-4">
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">3</div>
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Orzechy posiekaj na mniejsze kawałki i posyp nimi serek z miodem.</p></div>
<div class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-gray-900 text-white flex items-center justify-center text-[11px] font-bold shrink-0 shadow-sm">3</div>
<div class="pt-0.5"><p class="text-[13px] text-gray-600 leading-relaxed">Orzechy posiekaj na mniejsze kawałki i posyp nimi serek z miodem.</p></div>
</div>
<div class="flex gap-4">
<div class="w-7 h-7 rounded-full bg-gray-900 text-white flex items-center justify-center text-sm font-bold shrink-0 shadow-sm">4</div>
<div class="pt-0.5"><p class="text-sm text-gray-600 leading-relaxed">Umyj owoce (ew. pokrój na połówki) i ułóż na wierzchu. Gotowe!</p></div>
<div class="flex gap-3">
<div class="w-6 h-6 rounded-full bg-gray-900 text-white flex items-center justify-center text-[11px] font-bold shrink-0 shadow-sm">4</div>
<div class="pt-0.5"><p class="text-[13px] text-gray-600 leading-relaxed">Umyj owoce (ew. pokrój na połówki) i ułóż na wierzchu. Gotowe!</p></div>
</div>
</div>
</div>
<div id="tab-nutrition" class="tab-content hidden animate-fade-in">
<div class="bg-gray-50 rounded-xl p-5 border border-gray-100 mb-6">
<h3 class="font-bold text-gray-900 border-b border-gray-200 pb-2 mb-2 text-lg">Wartości odżywcze</h3>
<p class="text-xs text-gray-500 mb-4">Dla bazowej porcji (1 porcja)</p>
<div class="bg-gray-50 rounded-xl p-4 border border-gray-100 mb-5">
<ul class="space-y-0 divide-y divide-gray-200">
<li class="flex justify-between py-2.5 font-bold"><span class="text-gray-900 text-sm">Kalorie</span><span class="text-gray-900 text-sm">642 kcal</span></li>
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Białko</span><span class="font-medium text-gray-900 text-sm">32 g</span></li>
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Tłuszcze</span><span class="font-medium text-gray-900 text-sm">43 g</span></li>
<li class="flex justify-between py-2.5"><span class="text-gray-800 text-sm font-medium">Węglowodany</span><span class="font-medium text-gray-900 text-sm">41 g</span></li>
<li class="flex justify-between py-2 font-bold"><span class="text-gray-900 text-[13px]">Kalorie</span><span class="text-gray-900 text-[13px] tabular-nums">642 kcal</span></li>
<li class="flex justify-between py-2"><span class="text-gray-800 text-[13px] font-medium">Białko</span><span class="font-medium text-gray-900 text-[13px] tabular-nums">32 g</span></li>
<li class="flex justify-between py-2"><span class="text-gray-800 text-[13px] font-medium">Tłuszcze</span><span class="font-medium text-gray-900 text-[13px] tabular-nums">43 g</span></li>
<li class="flex justify-between py-2"><span class="text-gray-800 text-[13px] font-medium">Węglowodany</span><span class="font-medium text-gray-900 text-[13px] tabular-nums">41 g</span></li>
</ul>
</div>
</div>
@@ -149,11 +147,11 @@ export function getRecipeDetailHTML() {
<div id="swap-backdrop" onclick="closeSwapModal()" class="absolute inset-0 bg-black/40 z-40 hidden opacity-0 transition-opacity duration-300"></div>
<div id="swap-modal" class="absolute inset-x-0 bottom-0 bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.1)] z-50 transform translate-y-full transition-transform duration-300 ease-in-out p-6 flex flex-col max-h-[60%]">
<div class="flex justify-between items-center mb-5 shrink-0">
<h3 class="text-xl font-bold text-gray-900">Zmień <span id="swap-title-target" class="text-blue-600">składnik</span></h3>
<button onclick="closeSwapModal()" class="w-8 h-8 flex items-center justify-center bg-gray-100 rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-900 transition-colors">
<i class="fas fa-times text-sm"></i>
<div id="swap-modal" class="absolute inset-x-0 bottom-0 bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.1)] z-50 transform translate-y-full transition-transform duration-300 ease-in-out p-5 flex flex-col max-h-[60%]">
<div class="flex justify-between items-center mb-4 shrink-0">
<h3 class="text-[15px] font-bold text-gray-900">Zmień <span id="swap-title-target" class="text-blue-600">składnik</span></h3>
<button onclick="closeSwapModal()" class="w-7 h-7 flex items-center justify-center bg-gray-100 rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-900 transition-colors">
<i class="fas fa-times text-xs"></i>
</button>
</div>
@@ -253,9 +251,9 @@ export function setupRecipeDetail() {
if (opt.color === 'green') badgeClass = 'text-green-600 bg-green-100';
return `
<button onclick="confirmSwap('${opt.name}')" class="w-full flex justify-between items-center p-4 border border-gray-200 rounded-xl hover:border-gray-900 hover:shadow-sm transition-all bg-gray-50 hover:bg-white text-left">
<span class="font-medium text-gray-800">${opt.name}</span>
<span class="text-[11px] px-2 py-1 rounded-md font-semibold ${badgeClass}">${opt.hint}</span>
<button onclick="confirmSwap('${opt.name}')" class="w-full flex justify-between items-center p-3 border border-gray-200 rounded-xl hover:border-gray-900 hover:shadow-sm transition-all bg-gray-50 hover:bg-white text-left">
<span class="font-medium text-[13px] text-gray-800">${opt.name}</span>
<span class="text-[10px] px-2 py-0.5 rounded-md font-semibold ${badgeClass}">${opt.hint}</span>
</button>
`;
}).join('');

237
js/views/Shopping.js Normal file
View File

@@ -0,0 +1,237 @@
import {
addFreeformLine,
addFreeformList,
categoryLabel,
deleteList,
getActiveList,
getListSummaries,
KITCHEN_LIST_ID,
removeItemFromList,
setActiveListId,
toggleItemInList,
} from '../services/pantryShopping.js';
import { showAppToast } from '../ui/toast.js';
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
export function getShoppingHTML() {
return `
<div id="shopping-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-200 mt-3 px-4 pt-2 pb-3 space-y-3">
<div class="flex gap-2 items-center">
<label class="sr-only" for="shopping-list-select">Aktywna lista</label>
<select id="shopping-list-select" class="flex-1 min-w-0 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2.5 text-sm font-medium text-gray-900 outline-none focus:border-gray-400"></select>
<button type="button" id="shopping-new-list" class="shrink-0 w-10 h-10 rounded-xl border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 flex items-center justify-center" title="Nowa lista dowolna" aria-label="Nowa lista dowolna">
<i class="fas fa-plus text-sm"></i>
</button>
</div>
<button type="button" id="shopping-delete-list" class="hidden w-full py-2 rounded-lg text-xs font-medium text-red-600 hover:bg-red-50 transition-colors">
Usuń tę listę (nie dotyczy listy kuchennej)
</button>
</div>
<div id="shopping-freeform-add" class="hidden shrink-0 px-4 pt-3 pb-2 space-y-2 border-b border-gray-100">
<div class="flex gap-2">
<input type="text" id="shopping-freeform-input" class="flex-1 rounded-xl border border-gray-200 px-3 py-2.5 text-sm outline-none focus:border-gray-400" placeholder="Co kupić?" maxlength="200" />
<button type="button" id="shopping-freeform-submit" class="shrink-0 px-4 py-2.5 rounded-xl bg-gray-900 text-white text-xs font-semibold hover:bg-black">Dodaj</button>
</div>
<input type="text" id="shopping-freeform-note" class="w-full rounded-lg border border-gray-100 px-3 py-2 text-xs text-gray-600 outline-none focus:border-gray-300" placeholder="Opcjonalna notatka (ilość, sklep…)" maxlength="120" />
</div>
<div id="shopping-scroll" class="flex-1 overflow-y-auto px-4 py-3 pb-4 no-scrollbar">
<div id="shopping-list-root" class="space-y-4"></div>
</div>
</div>
`;
}
function syncListSelect() {
const sel = document.getElementById('shopping-list-select');
if (!sel) return;
const { lists, activeListId } = getListSummaries();
sel.innerHTML = lists.map((l) => {
const suffix = l.openCount ? ` (${l.openCount})` : '';
const label = `${l.name}${suffix}`;
return `<option value="${escapeHtml(l.id)}">${escapeHtml(label)}</option>`;
}).join('');
sel.value = activeListId;
}
function syncChromeForList() {
const list = getActiveList();
const isKitchen = list.type === 'kitchen';
const delBtn = document.getElementById('shopping-delete-list');
const ffAdd = document.getElementById('shopping-freeform-add');
if (ffAdd) ffAdd.classList.toggle('hidden', isKitchen);
if (delBtn) {
delBtn.classList.toggle('hidden', isKitchen);
}
}
function renderKitchenItems() {
const root = document.getElementById('shopping-list-root');
if (!root) return;
const list = getActiveList();
if (list.type !== 'kitchen') return;
const items = list.items;
if (items.length === 0) {
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Brak pozycji.</p>';
return;
}
const groups = {};
for (const it of items) {
const c = it.category || 'inne';
if (!groups[c]) groups[c] = [];
groups[c].push(it);
}
root.innerHTML = Object.keys(groups)
.sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)))
.map((cat) => `
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wide px-3 py-2 bg-gray-50 border-b border-gray-100">${escapeHtml(categoryLabel(cat))}</p>
<ul class="divide-y divide-gray-100">
${groups[cat].map((it) => `
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}" aria-label="Kupione">
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
</button>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.name)}</p>
<p class="text-xs text-gray-600 tabular-nums mt-0.5">${escapeHtml(String(it.amount))} ${escapeHtml(it.unit)}</p>
${it.sourceNote ? `<p class="text-[10px] text-gray-400 mt-1">${escapeHtml(it.sourceNote)}</p>` : ''}
</div>
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
<i class="fas fa-times text-xs"></i>
</button>
</li>`).join('')}
</ul>
</div>
`)
.join('');
bindItemButtons(list.id);
}
function renderFreeformItems() {
const root = document.getElementById('shopping-list-root');
if (!root) return;
const list = getActiveList();
if (list.type !== 'freeform') return;
const items = list.items;
if (items.length === 0) {
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Dodaj dowolny tekst powyżej — bez powiązania z katalogiem składników.</p>';
return;
}
root.innerHTML = `
<div class="rounded-xl border border-gray-200 bg-white overflow-hidden shadow-sm">
<ul class="divide-y divide-gray-100">
${items.map((it) => `
<li class="flex items-start gap-3 px-3 py-3 ${it.checked ? 'opacity-60' : ''}">
<button type="button" data-shop-toggle="${escapeHtml(it.id)}" class="mt-0.5 w-5 h-5 rounded border shrink-0 flex items-center justify-center transition-colors ${it.checked ? 'bg-gray-900 border-gray-900 text-white' : 'border-gray-300 bg-white'}">
${it.checked ? '<i class="fas fa-check text-[10px]"></i>' : ''}
</button>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 ${it.checked ? 'line-through text-gray-500' : ''}">${escapeHtml(it.text)}</p>
${it.note ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(it.note)}</p>` : ''}
</div>
<button type="button" data-shop-remove="${escapeHtml(it.id)}" class="shrink-0 w-8 h-8 rounded-full text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors" aria-label="Usuń">
<i class="fas fa-times text-xs"></i>
</button>
</li>`).join('')}
</ul>
</div>`;
bindItemButtons(list.id);
}
function bindItemButtons(listId) {
const root = document.getElementById('shopping-list-root');
if (!root) return;
root.querySelectorAll('[data-shop-toggle]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-shop-toggle');
if (id) toggleItemInList(listId, id);
refreshShopping();
});
});
root.querySelectorAll('[data-shop-remove]').forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-shop-remove');
if (id) removeItemFromList(listId, id);
refreshShopping();
});
});
}
export function refreshShopping() {
syncListSelect();
syncChromeForList();
const list = getActiveList();
if (list.type === 'kitchen') renderKitchenItems();
else renderFreeformItems();
}
export function setupShopping() {
const sel = document.getElementById('shopping-list-select');
sel?.addEventListener('change', () => {
const v = sel.value;
if (v) setActiveListId(v);
refreshShopping();
});
document.getElementById('shopping-new-list')?.addEventListener('click', () => {
const name = window.prompt('Nazwa nowej listy (dowolne zakupy):', 'Nowa lista');
if (name === null) return;
addFreeformList(name);
showAppToast('Utworzono listę.');
refreshShopping();
});
document.getElementById('shopping-delete-list')?.addEventListener('click', () => {
const list = getActiveList();
if (list.id === KITCHEN_LIST_ID) return;
if (!window.confirm(`Usunąć listę „${list.name}”?`)) return;
deleteList(list.id);
showAppToast('Lista usunięta.');
refreshShopping();
});
const submitFreeform = () => {
const list = getActiveList();
if (list.type !== 'freeform') return;
const input = document.getElementById('shopping-freeform-input');
const note = document.getElementById('shopping-freeform-note');
const text = input?.value || '';
addFreeformLine(list.id, text, note?.value || '');
if (input) input.value = '';
if (note) note.value = '';
refreshShopping();
};
document.getElementById('shopping-freeform-submit')?.addEventListener('click', submitFreeform);
document.getElementById('shopping-freeform-input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submitFreeform();
}
});
refreshShopping();
window.refreshShopping = refreshShopping;
}