Reorganise the views and prepare summary
All checks were successful
Build and Deploy / build-and-push (push) Successful in 23s

This commit is contained in:
2026-03-26 22:29:06 +01:00
parent d9cf61ee74
commit f80b115cae
12 changed files with 1394 additions and 484 deletions

View File

@@ -1,290 +1,418 @@
import { RECIPES, INGREDIENTS } from '../data/catalog.js';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import { addDays, startOfDay } from '../services/dateUtils.js';
import { addOrMergeShoppingLines, loadPantry } from '../services/pantryShopping.js';
import { dateKey, loadPlans, newPlanEntryId, savePlans } from '../services/planStore.js';
import { showAppToast } from '../ui/toast.js';
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
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-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-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 id="rd-add-to-planner-btn" class="h-9 px-3 bg-white/90 backdrop-blur rounded-full flex items-center justify-center gap-1.5 shadow-sm text-gray-800 hover:bg-white transition-colors text-[12px] font-semibold">
<i class="fas fa-calendar-plus text-[11px]"></i> Do planera
</button>
</div>
<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 id="rd-planner-picker" class="absolute inset-0 z-50 bg-black/45 hidden flex items-end" style="pointer-events: none">
<div id="rd-planner-sheet" class="w-full bg-white rounded-t-3xl shadow-lg p-5 pb-8 max-h-[70vh] overflow-y-auto" style="pointer-events: auto; transform: translateY(100%); transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-4"></div>
<h3 class="text-[15px] font-bold text-gray-900 mb-1">Dodaj do planera</h3>
<p id="rd-planner-recipe-name" class="text-[11px] text-gray-500 mb-4"></p>
<div id="rd-planner-days" class="space-y-2 mb-4"></div>
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Pora posiłku</p>
<div id="rd-planner-slots" class="flex flex-wrap gap-1.5 mb-5"></div>
<button id="rd-planner-confirm" class="w-full bg-gray-900 hover:bg-black text-white py-3 rounded-xl font-semibold text-[13px] transition-colors flex items-center justify-center gap-2">
<i class="fas fa-check text-xs"></i> Dodaj
</button>
</div>
</div>
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
<span id="rd-hero-label" class="text-white font-medium text-[15px]"></span>
</div>
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden">
<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>
<h1 id="rd-title" class="text-xl font-bold text-gray-900"></h1>
</div>
<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 id="rd-tags" class="flex flex-wrap gap-1.5 mb-3"></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 class="flex items-center gap-1.5"><i class="fas fa-clock text-gray-400 text-xs"></i><span id="rd-time"></span></div>
<div class="flex items-center gap-1.5"><i class="fas fa-fire text-gray-400 text-xs"></i><span id="rd-kcal"></span></div>
</div>
<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>
<button id="rd-serv-minus" 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 id="rd-servings" 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-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>
<button id="rd-serv-plus" 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-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>
<button class="flex-1 pb-2.5 text-[13px] font-semibold text-gray-900 border-b-2 border-gray-900 rd-tab-btn" data-rd-tab="ingredients">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 rd-tab-btn" data-rd-tab="steps">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 rd-tab-btn" data-rd-tab="nutrition">Wartości</button>
</div>
<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-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-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-[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-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-[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-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-[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-[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-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-[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-[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-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-[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-[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 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-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-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-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-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-4 border border-gray-100 mb-5">
<ul class="space-y-0 divide-y divide-gray-200">
<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>
<div id="rd-tab-ingredients" class="rd-tab-content block animate-fade-in"></div>
<div id="rd-tab-steps" class="rd-tab-content hidden animate-fade-in"></div>
<div id="rd-tab-nutrition" class="rd-tab-content hidden animate-fade-in"></div>
</div>
</div>
<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-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>
<div id="swap-options-container" class="space-y-2 overflow-y-auto no-scrollbar pb-2">
</div>
</div>
</div>
`;
}
export function setupRecipeDetail() {
let currentServings = 1; // Domślnie 1 porcja dla tego przepisu
const defaultServings = 1;
let currentlySwapping = null;
let currentRecipeId = null;
let currentServings = 1;
// Dane do dynamicznego modala
const swapOptions = {
'orzechy': [
{ name: 'Orzechy włoskie', hint: 'Bazowe', color: 'gray' },
{ name: 'Migdały', hint: '+ Białko', color: 'blue' },
{ name: 'Orzechy laskowe', hint: 'Klasyk', color: 'gray' },
{ name: 'Orzechy nerkowca', hint: 'Słodsze', color: 'gray' },
{ name: 'Orzechy pekan', hint: '+ Tłuszcz', color: 'green' }
],
'owoce1': [
{ name: 'Truskawki', hint: 'Bazowe', color: 'gray' },
{ name: 'Gruszka konferencja', hint: '+ Węgle', color: 'blue' },
{ name: 'Banany', hint: '+ Kalorie', color: 'green' }
],
'owoce2': [
{ name: 'Borówki ameryk.', hint: 'Bazowe', color: 'gray' },
{ name: 'Jagody leśne', hint: 'Sezonowe', color: 'blue' },
{ name: 'Maliny', hint: '- Kalorie', color: 'green' }
]
};
function populateDetail(recipeId) {
const recipe = RECIPES[recipeId];
if (!recipe) return;
window.switchTab = (tabId, clickedBtn) => {
document.querySelectorAll('.tab-content').forEach(el => {
el.classList.remove('block');
el.classList.add('hidden');
});
const targetTab = document.getElementById(`tab-${tabId}`);
targetTab.classList.remove('hidden');
targetTab.classList.add('block');
targetTab.parentElement.scrollTop = 0;
currentRecipeId = recipeId;
currentServings = 1;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('text-gray-900', 'border-gray-900', 'font-semibold');
btn.classList.add('text-gray-500', 'border-transparent', 'font-medium');
});
document.getElementById('rd-hero-label').textContent = `Zdjęcie: ${recipe.title}`;
document.getElementById('rd-title').textContent = recipe.title;
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
updateKcalDisplay();
clickedBtn.classList.remove('text-gray-500', 'border-transparent', 'font-medium');
clickedBtn.classList.add('text-gray-900', 'border-gray-900', 'font-semibold');
};
const tagsHtml = [];
for (const slotId of recipe.allowedSlots) {
const label = slotLabelMap[slotId];
if (label) tagsHtml.push(`<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-[11px] rounded-md font-medium">${escapeHtml(label)}</span>`);
}
for (const tag of (recipe.tags || [])) {
tagsHtml.push(`<span class="px-2.5 py-0.5 bg-gray-100 text-gray-700 text-[11px] rounded-md font-medium">${escapeHtml(tag)}</span>`);
}
document.getElementById('rd-tags').innerHTML = tagsHtml.join('');
window.toggleIngredient = (element) => {
element.classList.toggle('ingredient-active');
};
document.getElementById('rd-servings').textContent = '1';
window.changeServings = (delta) => {
const newServings = currentServings + delta;
if (newServings < 1) return;
currentServings = newServings;
document.getElementById('servings-count').innerText = currentServings;
const ratio = currentServings / defaultServings;
document.querySelectorAll('.ingredient-amount').forEach(el => {
const baseAmount = parseFloat(el.getAttribute('data-base-amount'));
const unit = el.getAttribute('data-unit');
if (!isNaN(baseAmount)) {
let newAmount = baseAmount * ratio;
newAmount = Number.isInteger(newAmount) ? newAmount : parseFloat(newAmount.toFixed(1));
el.innerText = `${newAmount} ${unit}`;
renderIngredients(recipe);
renderSteps(recipe);
renderNutrition(recipe);
const tabBtns = document.querySelectorAll('.rd-tab-btn');
const tabs = document.querySelectorAll('.rd-tab-content');
tabBtns.forEach((b) => {
b.classList.remove('text-gray-900', 'border-gray-900', 'font-semibold');
b.classList.add('text-gray-500', 'border-transparent', 'font-medium');
});
tabBtns[0]?.classList.remove('text-gray-500', 'border-transparent', 'font-medium');
tabBtns[0]?.classList.add('text-gray-900', 'border-gray-900', 'font-semibold');
tabs.forEach((t) => { t.classList.add('hidden'); t.classList.remove('block'); });
document.getElementById('rd-tab-ingredients')?.classList.remove('hidden');
document.getElementById('rd-tab-ingredients')?.classList.add('block');
}
function updateKcalDisplay() {
const recipe = RECIPES[currentRecipeId];
if (!recipe) return;
const kcal = Math.round(recipe.nutritionPerServing.kcal * currentServings);
document.getElementById('rd-kcal').textContent = `${kcal} kcal`;
}
function renderIngredients(recipe) {
const container = document.getElementById('rd-tab-ingredients');
if (!container) return;
const pantry = loadPantry();
const rows = recipe.ingredients.map((ing) => {
const def = INGREDIENTS[ing.ingredientId];
const name = def?.name || ing.ingredientId;
const scaledAmount = ing.amount * currentServings;
const displayAmount = Number.isInteger(scaledAmount) ? scaledAmount : parseFloat(scaledAmount.toFixed(1));
const pantryQty = Number(pantry[ing.ingredientId]) || 0;
let stockBadge = '';
if (def) {
const u = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit;
if (pantryQty >= scaledAmount) {
stockBadge = `<span class="text-[9px] px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-600 font-semibold whitespace-nowrap">Masz</span>`;
} else if (pantryQty > 0) {
const miss = parseFloat((scaledAmount - pantryQty).toFixed(1));
stockBadge = `<span class="text-[9px] px-1.5 py-0.5 rounded bg-amber-50 text-amber-600 font-semibold whitespace-nowrap">Brak ${miss} ${u}</span>`;
} else {
stockBadge = `<span class="text-[9px] px-1.5 py-0.5 rounded bg-red-50 text-red-500 font-semibold whitespace-nowrap">Brak</span>`;
}
}
return `
<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 rd-ingredient-row" data-ingredient-id="${escapeHtml(ing.ingredientId)}" data-base-amount="${ing.amount}" data-unit="${escapeHtml(ing.unit)}">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white rd-check-box transition-colors"><i class="fas fa-check text-[10px] hidden rd-check-icon"></i></div>
<span class="text-gray-700 text-[13px] flex-1 rd-ing-text transition-colors">${escapeHtml(name)}</span>
${stockBadge}
<span class="font-medium text-gray-900 text-[13px] rd-ing-amount tabular-nums">${displayAmount} ${escapeHtml(ing.unit)}</span>
</li>`;
}).join('');
container.innerHTML = `
<div class="flex justify-between items-end mb-3">
<span class="text-[11px] text-gray-500 font-medium">Zaznacz składniki do kupienia</span>
</div>
<ul class="space-y-0 mb-5" id="rd-ingredient-list">${rows}</ul>
<button id="rd-add-to-shopping" 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>`;
container.querySelectorAll('.rd-ingredient-row').forEach((row) => {
row.addEventListener('click', () => row.classList.toggle('ingredient-active'));
});
document.getElementById('rd-add-to-shopping')?.addEventListener('click', () => {
const recipe = RECIPES[currentRecipeId];
if (!recipe) return;
const checkedRows = container.querySelectorAll('.rd-ingredient-row.ingredient-active');
if (checkedRows.length === 0) {
showAppToast('Zaznacz składniki, które chcesz dodać.');
return;
}
const lines = [];
checkedRows.forEach((row) => {
const ingredientId = row.dataset.ingredientId;
const baseAmount = parseFloat(row.dataset.baseAmount);
const unit = row.dataset.unit;
const def = INGREDIENTS[ingredientId];
lines.push({
ingredientId,
amount: Math.round(baseAmount * currentServings * 100) / 100,
unit,
name: def?.name || ingredientId,
category: def?.category || 'inne',
sourceNote: `Przepis: ${recipe.title}`,
});
});
};
window.openSwapModal = (type) => {
currentlySwapping = type;
let title = '';
if(type === 'orzechy') title = 'Orzechy';
if(type === 'owoce1') title = 'Owoce bazy';
if(type === 'owoce2') title = 'Dodatki owocowe';
document.getElementById('swap-title-target').innerText = title;
// Wygeneruj opcje na podstawie słownika
const container = document.getElementById('swap-options-container');
container.innerHTML = swapOptions[type].map(opt => {
let badgeClass = 'text-gray-600 bg-gray-200'; // Domyślny gray
if (opt.color === 'blue') badgeClass = 'text-blue-600 bg-blue-100';
if (opt.color === 'green') badgeClass = 'text-green-600 bg-green-100';
addOrMergeShoppingLines(lines);
showAppToast(`Dodano ${lines.length} składnik(ów) na listę zakupów.`);
window.refreshShopping?.();
return `
<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>
`;
checkedRows.forEach((row) => row.classList.remove('ingredient-active'));
});
}
function renderSteps(recipe) {
const container = document.getElementById('rd-tab-steps');
if (!container) return;
const steps = recipe.steps || [];
if (steps.length === 0) {
container.innerHTML = '<p class="text-sm text-gray-500 text-center py-8">Brak kroków przygotowania.</p>';
return;
}
container.innerHTML = `
<div class="space-y-5 pb-5">
${steps.map((step, i) => `
<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">${i + 1}</div>
<div class="pt-0.5"><p class="text-[13px] text-gray-600 leading-relaxed">${escapeHtml(step)}</p></div>
</div>`).join('')}
</div>`;
}
function renderNutrition(recipe) {
const container = document.getElementById('rd-tab-nutrition');
if (!container) return;
const n = recipe.nutritionPerServing;
const s = currentServings;
container.innerHTML = `
<div class="bg-gray-50 rounded-xl p-4 border border-gray-100 mb-5">
<p class="text-[11px] text-gray-500 mb-3 font-medium">${s > 1 ? `Wartości dla ${s} porcji` : 'Wartości na 1 porcję'}</p>
<ul class="space-y-0 divide-y divide-gray-200">
<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">${Math.round(n.kcal * s)} 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">${Math.round(n.protein * s * 10) / 10} 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">${Math.round(n.fat * s * 10) / 10} 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">${Math.round(n.carbs * s * 10) / 10} g</span></li>
</ul>
</div>`;
}
export function setupRecipeDetail() {
document.querySelectorAll('.rd-tab-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.rdTab;
document.querySelectorAll('.rd-tab-content').forEach((el) => {
el.classList.add('hidden');
el.classList.remove('block');
});
const target = document.getElementById(`rd-tab-${tabId}`);
if (target) {
target.classList.remove('hidden');
target.classList.add('block');
target.parentElement.scrollTop = 0;
}
document.querySelectorAll('.rd-tab-btn').forEach((b) => {
b.classList.remove('text-gray-900', 'border-gray-900', 'font-semibold');
b.classList.add('text-gray-500', 'border-transparent', 'font-medium');
});
btn.classList.remove('text-gray-500', 'border-transparent', 'font-medium');
btn.classList.add('text-gray-900', 'border-gray-900', 'font-semibold');
});
});
document.getElementById('rd-serv-minus')?.addEventListener('click', () => {
if (currentServings <= 1) return;
currentServings--;
document.getElementById('rd-servings').textContent = currentServings;
const recipe = RECIPES[currentRecipeId];
if (recipe) {
renderIngredients(recipe);
renderNutrition(recipe);
updateKcalDisplay();
}
});
document.getElementById('rd-serv-plus')?.addEventListener('click', () => {
if (currentServings >= 12) return;
currentServings++;
document.getElementById('rd-servings').textContent = currentServings;
const recipe = RECIPES[currentRecipeId];
if (recipe) {
renderIngredients(recipe);
renderNutrition(recipe);
updateKcalDisplay();
}
});
const WEEKDAYS_LONG = ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'];
const MONTHS_SHORT = ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'paź', 'lis', 'gru'];
let plannerPickerDay = null;
let plannerPickerSlot = null;
const plannerOverlay = document.getElementById('rd-planner-picker');
const plannerSheet = document.getElementById('rd-planner-sheet');
function openPlannerPicker() {
const recipe = RECIPES[currentRecipeId];
if (!recipe) return;
document.getElementById('rd-planner-recipe-name').textContent = recipe.title;
const daysContainer = document.getElementById('rd-planner-days');
const today = startOfDay(new Date());
const days = [];
for (let i = 0; i < 7; i++) days.push(addDays(today, i));
plannerPickerDay = today;
plannerPickerSlot = recipe.allowedSlots[0] || MEAL_SLOTS[0]?.id;
daysContainer.innerHTML = days.map((d, idx) => {
const wd = WEEKDAYS_LONG[d.getDay()];
const label = idx === 0 ? `Dziś — ${wd}, ${d.getDate()} ${MONTHS_SHORT[d.getMonth()]}` : `${wd}, ${d.getDate()} ${MONTHS_SHORT[d.getMonth()]}`;
const sel = idx === 0;
return `<button type="button" class="rd-plan-day-btn w-full text-left px-3 py-2.5 rounded-xl border text-[13px] font-semibold transition-all ${sel ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-200 bg-gray-50 text-gray-900 hover:border-gray-400'}" data-day-ts="${d.getTime()}">${escapeHtml(label)}</button>`;
}).join('');
const backdrop = document.getElementById('swap-backdrop');
backdrop.classList.remove('hidden');
setTimeout(() => backdrop.classList.remove('opacity-0'), 10);
const modal = document.getElementById('swap-modal');
modal.classList.remove('translate-y-full');
modal.classList.add('translate-y-0');
const slotsContainer = document.getElementById('rd-planner-slots');
slotsContainer.innerHTML = MEAL_SLOTS.filter((s) => recipe.allowedSlots.includes(s.id)).map((s) => {
const sel = s.id === plannerPickerSlot;
return `<button type="button" class="rd-plan-slot-btn px-3 py-1.5 rounded-lg border text-[12px] font-semibold transition-all ${sel ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-200 bg-gray-50 text-gray-700 hover:border-gray-400'}" data-slot-id="${s.id}">${escapeHtml(s.label)}</button>`;
}).join('');
plannerOverlay.classList.remove('hidden');
plannerOverlay.style.pointerEvents = 'auto';
requestAnimationFrame(() => {
plannerSheet.style.transform = 'translateY(0)';
});
}
function closePlannerPicker() {
plannerSheet.style.transform = 'translateY(100%)';
setTimeout(() => {
plannerOverlay.classList.add('hidden');
plannerOverlay.style.pointerEvents = 'none';
}, 300);
}
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', openPlannerPicker);
plannerOverlay?.addEventListener('click', (e) => {
if (e.target === plannerOverlay) closePlannerPicker();
});
document.getElementById('rd-planner-days')?.addEventListener('click', (e) => {
const btn = e.target.closest('.rd-plan-day-btn');
if (!btn) return;
plannerPickerDay = new Date(Number(btn.getAttribute('data-day-ts')));
document.querySelectorAll('.rd-plan-day-btn').forEach((b) => {
b.classList.remove('border-gray-900', 'bg-gray-900', 'text-white');
b.classList.add('border-gray-200', 'bg-gray-50', 'text-gray-900');
});
btn.classList.remove('border-gray-200', 'bg-gray-50', 'text-gray-900');
btn.classList.add('border-gray-900', 'bg-gray-900', 'text-white');
});
document.getElementById('rd-planner-slots')?.addEventListener('click', (e) => {
const btn = e.target.closest('.rd-plan-slot-btn');
if (!btn) return;
plannerPickerSlot = btn.getAttribute('data-slot-id');
document.querySelectorAll('.rd-plan-slot-btn').forEach((b) => {
b.classList.remove('border-gray-900', 'bg-gray-900', 'text-white');
b.classList.add('border-gray-200', 'bg-gray-50', 'text-gray-700');
});
btn.classList.remove('border-gray-200', 'bg-gray-50', 'text-gray-700');
btn.classList.add('border-gray-900', 'bg-gray-900', 'text-white');
});
document.getElementById('rd-planner-confirm')?.addEventListener('click', () => {
if (!currentRecipeId || !plannerPickerDay || !plannerPickerSlot) return;
const plans = loadPlans();
const key = dateKey(plannerPickerDay);
if (!plans[key]) plans[key] = {};
if (!plans[key][plannerPickerSlot]) plans[key][plannerPickerSlot] = [];
plans[key][plannerPickerSlot].push({
id: newPlanEntryId(),
recipeId: currentRecipeId,
servings: currentServings,
});
savePlans(plans);
closePlannerPicker();
showAppToast('Dodano do planera!');
window.refreshPlanner?.();
});
window.openRecipeDetail = (recipeId) => {
if (!recipeId || !RECIPES[recipeId]) return;
populateDetail(recipeId);
const view = document.getElementById('recipe-detail-view');
view.classList.remove('translate-x-full', 'opacity-0', 'pointer-events-none');
view.classList.add('translate-x-0', 'opacity-100', 'pointer-events-auto');
};
window.closeSwapModal = () => {
const backdrop = document.getElementById('swap-backdrop');
backdrop.classList.add('opacity-0');
setTimeout(() => backdrop.classList.add('hidden'), 300);
const modal = document.getElementById('swap-modal');
modal.classList.remove('translate-y-0');
modal.classList.add('translate-y-full');
window.closeRecipeDetail = () => {
closePlannerPicker();
const view = document.getElementById('recipe-detail-view');
view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto');
view.classList.add('translate-x-full', 'opacity-0', 'pointer-events-none');
};
window.confirmSwap = (newItemName) => {
if (currentlySwapping === 'orzechy') {
document.getElementById('ingredient-orzechy').innerText = newItemName;
} else if (currentlySwapping === 'owoce1') {
document.getElementById('ingredient-owoce1').innerText = newItemName;
} else if (currentlySwapping === 'owoce2') {
document.getElementById('ingredient-owoce2').innerText = newItemName;
}
closeSwapModal();
};
}
}