Extract ingredientCard
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { INGREDIENTS, RECIPES, PRODUCTS, CATEGORY_LABELS, getProductsForIngredient } from '../data/catalog.js?v=8';
|
||||
import { INGREDIENTS, RECIPES, PRODUCTS, getProductsForIngredient } from '../data/catalog.js?v=8';
|
||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||
import {
|
||||
addDays,
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
savePlans,
|
||||
} from '../services/planStore.js?v=2';
|
||||
import { dayHasAnyMeal, autoSelectProducts, saveLastProductSelection } from '../services/planIngredients.js?v=4';
|
||||
import { loadPantry, getPantryTotal, getPantryProducts, setPantryQty, setPantryProductQty, addOrMergeShoppingLines } from '../services/pantryShopping.js?v=2';
|
||||
import { showAppToast } from './toast.js';
|
||||
import { loadPantry } from '../services/pantryShopping.js?v=2';
|
||||
import {
|
||||
bindCalendarDayClicks,
|
||||
createCalendarTopbarHTML,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
renderCalendarGrid,
|
||||
syncCalendarTodayButton,
|
||||
} from './mealCalendar.js?v=1';
|
||||
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-99';
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
@@ -34,207 +34,6 @@ function esc(s) {
|
||||
|
||||
const slotLabel = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
|
||||
|
||||
/* ── Product Card Popup ────────────────────────────── */
|
||||
|
||||
function getProductCardHTML() {
|
||||
return `
|
||||
<div id="mpe-product-card-overlay" class="fixed inset-0 z-[70] bg-black/50 hidden flex items-center justify-center p-6" style="pointer-events:none">
|
||||
<div id="mpe-product-card" class="relative w-full max-w-xs bg-[#2d2e2b] rounded-2xl shadow-2xl overflow-hidden" style="pointer-events:auto; max-height:80vh; overflow-y:auto;">
|
||||
<div id="mpe-pc-hero" class="relative w-full h-[180px] bg-gray-800 overflow-hidden">
|
||||
<img id="mpe-pc-img" class="w-full h-full object-cover hidden" alt="" />
|
||||
<div id="mpe-pc-fallback" class="w-full h-full flex items-center justify-center">
|
||||
<i class="fas fa-box-open text-3xl text-gray-600"></i>
|
||||
</div>
|
||||
<button type="button" id="mpe-pc-close" class="absolute top-3 right-3 w-8 h-8 rounded-full bg-black/50 text-white flex items-center justify-center hover:bg-black/70 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-4 pt-3 pb-4 space-y-3">
|
||||
<div>
|
||||
<p id="mpe-pc-brand" class="text-[10px] font-semibold uppercase tracking-wider text-emerald-400"></p>
|
||||
<h3 id="mpe-pc-name" class="text-[15px] font-bold text-gray-100 leading-snug mt-0.5"></h3>
|
||||
<p id="mpe-pc-category" class="text-[11px] text-gray-500 mt-0.5"></p>
|
||||
</div>
|
||||
<div id="mpe-pc-pack" class="hidden">
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wider text-gray-500">Opakowanie</span>
|
||||
<p id="mpe-pc-pack-val" class="text-[13px] font-semibold text-gray-300 mt-0.5"></p>
|
||||
</div>
|
||||
<div>
|
||||
<span id="mpe-pc-nut-label" class="text-[10px] font-semibold uppercase tracking-wider text-gray-500">Wartości odżywcze na 100 g</span>
|
||||
<div class="grid grid-cols-4 gap-2 mt-1.5">
|
||||
<div class="bg-[#393937] rounded-xl px-2.5 py-2 text-center">
|
||||
<p id="mpe-pc-kcal" class="text-[15px] font-bold text-gray-100 tabular-nums"></p>
|
||||
<p class="text-[9px] text-gray-500 font-medium mt-0.5">kcal</p>
|
||||
</div>
|
||||
<div class="bg-[#393937] rounded-xl px-2.5 py-2 text-center">
|
||||
<p id="mpe-pc-protein" class="text-[15px] font-bold text-blue-400 tabular-nums"></p>
|
||||
<p class="text-[9px] text-gray-500 font-medium mt-0.5">białko</p>
|
||||
</div>
|
||||
<div class="bg-[#393937] rounded-xl px-2.5 py-2 text-center">
|
||||
<p id="mpe-pc-fat" class="text-[15px] font-bold text-amber-400 tabular-nums"></p>
|
||||
<p class="text-[9px] text-gray-500 font-medium mt-0.5">tłuszcz</p>
|
||||
</div>
|
||||
<div class="bg-[#393937] rounded-xl px-2.5 py-2 text-center">
|
||||
<p id="mpe-pc-carbs" class="text-[15px] font-bold text-orange-400 tabular-nums"></p>
|
||||
<p class="text-[9px] text-gray-500 font-medium mt-0.5">węgl.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mpe-pc-stock" class="space-y-1.5"></div>
|
||||
<div id="mpe-pc-shop"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function openProductCard(ingredientId, productId) {
|
||||
const overlay = document.getElementById('mpe-product-card-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
const def = INGREDIENTS[ingredientId];
|
||||
const product = productId ? PRODUCTS[productId] : null;
|
||||
const name = product?.name || def?.name || ingredientId;
|
||||
const brand = product?.brand || '';
|
||||
const category = CATEGORY_LABELS[def?.category] || '';
|
||||
const nutrition = product?.nutritionPer100g || def?.nutritionPer100g;
|
||||
const image = product?.image || def?.image;
|
||||
const packLabel = product?.packLabel || def?.purchasePack?.label || '';
|
||||
const nutUnit = def?.pantryUnit === 'ml' ? '100 ml' : '100 g';
|
||||
|
||||
document.getElementById('mpe-pc-name').textContent = name;
|
||||
document.getElementById('mpe-pc-brand').textContent = brand;
|
||||
document.getElementById('mpe-pc-category').textContent = category;
|
||||
document.getElementById('mpe-pc-nut-label').textContent = `Wartości odżywcze na ${nutUnit}`;
|
||||
|
||||
const img = document.getElementById('mpe-pc-img');
|
||||
const fallback = document.getElementById('mpe-pc-fallback');
|
||||
if (image) {
|
||||
img.src = image;
|
||||
img.alt = name;
|
||||
img.classList.remove('hidden');
|
||||
fallback.classList.add('hidden');
|
||||
} else {
|
||||
img.classList.add('hidden');
|
||||
fallback.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const packWrap = document.getElementById('mpe-pc-pack');
|
||||
if (packLabel) {
|
||||
packWrap.classList.remove('hidden');
|
||||
document.getElementById('mpe-pc-pack-val').textContent = packLabel;
|
||||
} else {
|
||||
packWrap.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (nutrition) {
|
||||
document.getElementById('mpe-pc-kcal').textContent = nutrition.kcal;
|
||||
document.getElementById('mpe-pc-protein').textContent = nutrition.protein + 'g';
|
||||
document.getElementById('mpe-pc-fat').textContent = nutrition.fat + 'g';
|
||||
document.getElementById('mpe-pc-carbs').textContent = nutrition.carbs + 'g';
|
||||
}
|
||||
|
||||
// Stock section
|
||||
renderPlannerCardStock(ingredientId, productId);
|
||||
|
||||
// Shop section
|
||||
renderPlannerCardShop(ingredientId, productId);
|
||||
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
}
|
||||
|
||||
function renderPlannerCardStock(ingredientId, productId) {
|
||||
const wrap = document.getElementById('mpe-pc-stock');
|
||||
if (!wrap) return;
|
||||
const def = INGREDIENTS[ingredientId];
|
||||
if (!def) return;
|
||||
const u = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit;
|
||||
const pantry = loadPantry();
|
||||
const product = productId ? PRODUCTS[productId] : null;
|
||||
|
||||
let qty, step, pid;
|
||||
if (product) {
|
||||
const items = getPantryProducts(ingredientId, pantry);
|
||||
qty = items.find(i => i.productId === productId)?.qty || 0;
|
||||
step = product.packSize || 1;
|
||||
pid = productId;
|
||||
} else {
|
||||
qty = getPantryTotal(ingredientId, pantry);
|
||||
step = def.purchasePack?.amount || (def.pantryUnit === 'szt' ? 1 : 10);
|
||||
pid = '_generic';
|
||||
}
|
||||
|
||||
wrap.innerHTML = `
|
||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5 text-gray-500">Zapas</p>
|
||||
<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2 bg-[#393937]">
|
||||
<button type="button" class="mpe-pc-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95 bg-[#2f2f2d] text-[#d7d2c8]" data-pid="${esc(pid)}" data-step="${step}" data-dir="-1"><i class="fas fa-minus text-xs"></i></button>
|
||||
<span class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${Math.round(qty)} ${esc(u)}</span>
|
||||
<button type="button" class="mpe-pc-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95 bg-[#2f2f2d] text-[#d7d2c8]" data-pid="${esc(pid)}" data-step="${step}" data-dir="1"><i class="fas fa-plus text-xs"></i></button>
|
||||
</div>`;
|
||||
|
||||
wrap.querySelectorAll('.mpe-pc-stock-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const bpid = btn.dataset.pid;
|
||||
const bstep = Number(btn.dataset.step) || 1;
|
||||
const dir = Number(btn.dataset.dir);
|
||||
const p = loadPantry();
|
||||
if (bpid === '_generic') {
|
||||
const cur = getPantryTotal(ingredientId, p);
|
||||
setPantryQty(ingredientId, Math.max(0, cur + bstep * dir));
|
||||
} else {
|
||||
const items = getPantryProducts(ingredientId, p);
|
||||
const cur = items.find(i => i.productId === bpid)?.qty || 0;
|
||||
setPantryProductQty(ingredientId, bpid, Math.max(0, cur + bstep * dir));
|
||||
}
|
||||
renderPlannerCardStock(ingredientId, productId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlannerCardShop(ingredientId, productId) {
|
||||
const wrap = document.getElementById('mpe-pc-shop');
|
||||
if (!wrap) return;
|
||||
const def = INGREDIENTS[ingredientId];
|
||||
if (!def) return;
|
||||
const u = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit;
|
||||
const product = productId ? PRODUCTS[productId] : null;
|
||||
const packSize = product?.packSize || def.purchasePack?.amount;
|
||||
const packLabel = product?.packLabel || def.purchasePack?.label;
|
||||
const usesPacks = Boolean(packSize && packSize > 0);
|
||||
const btnLabel = usesPacks ? `Dodaj na listę (${packLabel || `${packSize} ${u}`})` : 'Dodaj na listę';
|
||||
|
||||
wrap.innerHTML = `
|
||||
<button type="button" id="mpe-pc-add-list" class="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[13px] font-semibold transition-colors active:scale-[0.98]" style="background:#ddd6ca; color:#2d2e2b;">
|
||||
<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}
|
||||
</button>`;
|
||||
|
||||
document.getElementById('mpe-pc-add-list')?.addEventListener('click', () => {
|
||||
const amt = usesPacks ? packSize : (def.pantryUnit === 'szt' ? 1 : 10);
|
||||
const line = {
|
||||
ingredientId,
|
||||
amount: amt,
|
||||
unit: u,
|
||||
name: product?.name || def.name,
|
||||
category: def.category,
|
||||
sourceNote: packLabel || 'Z planera',
|
||||
};
|
||||
if (productId) line.productId = productId;
|
||||
addOrMergeShoppingLines([line]);
|
||||
// Quick visual feedback
|
||||
const btn = document.getElementById('mpe-pc-add-list');
|
||||
if (btn) { btn.textContent = '✓ Dodano'; setTimeout(() => { btn.innerHTML = `<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}`; }, 1200); }
|
||||
window.refreshShopping?.();
|
||||
});
|
||||
}
|
||||
|
||||
function closeProductCard() {
|
||||
const overlay = document.getElementById('mpe-product-card-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.add('hidden');
|
||||
overlay.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/* ── HTML template ──────────────────────────────────── */
|
||||
|
||||
export function getMealPlanEditorHTML() {
|
||||
@@ -286,7 +85,7 @@ export function getMealPlanEditorHTML() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${getProductCardHTML()}`;
|
||||
${getIngredientCardHTML({ idBase: 'mpe-pc' })}`;
|
||||
}
|
||||
|
||||
/* ── Setup ──────────────────────────────────────────── */
|
||||
@@ -295,6 +94,8 @@ export function setupMealPlanEditor() {
|
||||
const overlay = document.getElementById('mpe-overlay');
|
||||
const sheet = document.getElementById('mpe-sheet');
|
||||
if (!overlay || !sheet) return;
|
||||
const ingredientCard = createIngredientCardController({ idBase: 'mpe-pc', defaultSourceNote: 'Z planera' });
|
||||
ingredientCard.bind();
|
||||
|
||||
const S = {
|
||||
mode: null,
|
||||
@@ -715,6 +516,7 @@ export function setupMealPlanEditor() {
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
ingredientCard.close();
|
||||
sheet.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => { overlay.classList.add('hidden'); overlay.style.pointerEvents = 'none'; }, 300);
|
||||
}
|
||||
@@ -856,13 +658,6 @@ export function setupMealPlanEditor() {
|
||||
renderNutrition();
|
||||
});
|
||||
|
||||
/* ── Product card ────────────────────────────── */
|
||||
|
||||
document.getElementById('mpe-pc-close')?.addEventListener('click', closeProductCard);
|
||||
document.getElementById('mpe-product-card-overlay')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'mpe-product-card-overlay') closeProductCard();
|
||||
});
|
||||
|
||||
/* ── Ingredient section delegation ────────────── */
|
||||
|
||||
const ingSec = document.getElementById('mpe-ing-section');
|
||||
@@ -873,7 +668,23 @@ export function setupMealPlanEditor() {
|
||||
if (!changeProdEarly) {
|
||||
const cardBtn = e.target.closest('.mpe-open-product-card');
|
||||
if (cardBtn) {
|
||||
openProductCard(cardBtn.dataset.eid, cardBtn.dataset.pid || null);
|
||||
const eid = cardBtn.dataset.eid;
|
||||
if (!eid) return;
|
||||
ingredientCard.open({
|
||||
ingredientId: eid,
|
||||
productId: cardBtn.dataset.pid || null,
|
||||
selectedProductId: cardBtn.dataset.pid || null,
|
||||
sourceNote: 'Z planera',
|
||||
onProductChange: (nextProductId) => {
|
||||
if (!nextProductId) return;
|
||||
S.productSelections[eid] = nextProductId;
|
||||
saveLastProductSelection(eid, nextProductId);
|
||||
},
|
||||
onAfterChange: () => {
|
||||
renderIngList();
|
||||
renderNutrition();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user