This commit is contained in:
2026-03-25 23:57:47 +01:00
parent 2ff7e6f8ce
commit 6e76977ace
4 changed files with 492 additions and 273 deletions

View File

@@ -1,4 +1,4 @@
import { RECIPES } from '../data/catalog.js';
import { INGREDIENTS, RECIPES } from '../data/catalog.js';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
addDays,
@@ -12,11 +12,12 @@ import {
weekContains,
} from '../services/dateUtils.js';
import {
aggregateDayIngredientsBySlot,
computeFullForecast,
countDayShortfalls,
dayHasAnyMeal,
sumDayNutrition,
} from '../services/planIngredients.js';
import { addOrMergeShoppingLines } from '../services/pantryShopping.js';
import { addOrMergeShoppingLines, loadPantry } from '../services/pantryShopping.js';
import {
dateKey,
getDayPlan,
@@ -72,44 +73,39 @@ export function getMealPlannerHTML() {
<i class="fas fa-chevron-right text-xs" aria-hidden="true"></i>
</button>
</div>
<div class="px-3 pb-2 flex items-center justify-between gap-3">
<div class="px-3 pb-2 flex items-center justify-center">
<button type="button" id="cal-go-today" title="Dziś" aria-label="Przejdź do dzisiejszego dnia"
class="h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2.5 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors">
<i class="fas fa-calendar-day text-[9px] opacity-70" aria-hidden="true"></i>
Dziś
</button>
<div id="planner-cal-mode" class="inline-flex items-center shrink-0 rounded-md border border-gray-200 bg-gray-50 p-px gap-px shadow-sm" role="tablist" aria-label="Skala kalendarza">
<button type="button" data-cal-mode="week" id="planner-mode-week" title="Tydzień" aria-label="Widok tygodnia"
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors bg-white text-gray-900 shadow-sm">
<i class="fas fa-calendar-week text-[10px]" aria-hidden="true"></i>
</button>
<button type="button" data-cal-mode="month" id="planner-mode-month" title="Miesiąc" aria-label="Widok miesiąca"
class="planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors text-gray-400 hover:text-gray-600">
<i class="fas fa-calendar-days text-[10px]" aria-hidden="true"></i>
</button>
</div>
</div>
<div id="calendar-week-wrap" class="px-3 pb-2.5">
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
<div id="calendar-swipe-zone" style="touch-action: pan-x">
<div id="calendar-week-wrap" class="px-3 pb-1" style="overflow: hidden; max-height: 10rem; opacity: 1">
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
</div>
<div id="calendar-week-grid" class="grid grid-cols-7 gap-0.5"></div>
</div>
<div id="calendar-week-grid" class="grid grid-cols-7 gap-0.5"></div>
</div>
<div id="calendar-month-wrap" class="hidden px-3 pb-2.5">
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
<div id="calendar-month-wrap" class="px-3 pb-1" style="overflow: hidden; max-height: 0; opacity: 0">
<div class="grid grid-cols-7 gap-0.5 text-center text-[9px] font-medium text-gray-400 uppercase tracking-wide mb-0.5 leading-none">
${WEEKDAYS_SHORT.map((d) => `<div>${d}</div>`).join('')}
</div>
<div id="calendar-month-grid" class="grid grid-cols-7 gap-0.5"></div>
</div>
<div id="calendar-drag-handle" class="flex items-center justify-center pb-2 pt-0.5">
<i id="calendar-handle-icon" class="fas fa-chevron-down text-[8px] text-gray-300" aria-hidden="true"></i>
</div>
<div id="calendar-month-grid" class="grid grid-cols-7 gap-0.5"></div>
</div>
</div>
<div id="planner-scroll" class="flex-1 overflow-y-auto px-4 pt-3 pb-4">
<p id="planner-day-heading" class="text-sm font-semibold text-gray-900 tabular-nums mb-2"></p>
<div id="planner-summary-card" class="rounded-xl border border-amber-200/80 bg-gradient-to-br from-amber-50 to-white p-3 shadow-sm mb-3">
<p id="planner-day-heading" class="text-[13px] font-semibold text-gray-900 tabular-nums mb-2"></p>
<div id="planner-summary-card" class="rounded-xl border border-amber-200/80 bg-gradient-to-br from-amber-50 to-white p-2.5 shadow-sm mb-3">
<div class="flex items-start justify-between gap-2 mb-2">
<div>
<p class="text-[10px] font-semibold uppercase tracking-wide text-amber-900/70">Dziś — podsumowanie</p>
<p id="planner-summary-kcal" class="text-2xl font-bold text-gray-900 tabular-nums leading-tight mt-0.5">0 <span class="text-sm font-semibold text-gray-500">kcal</span></p>
<p id="planner-summary-kcal" class="text-xl font-bold text-gray-900 tabular-nums leading-tight mt-0.5">0 <span class="text-[13px] font-semibold text-gray-500">kcal</span></p>
</div>
<button type="button" id="planner-toggle-nutrition" class="shrink-0 flex items-center gap-1 text-[11px] font-semibold text-amber-900/80 hover:text-gray-900 py-1 px-2 rounded-lg hover:bg-amber-100/50 transition-colors" aria-expanded="false">
Szczegóły
@@ -140,8 +136,8 @@ export function getMealPlannerHTML() {
<p id="planner-summary-hint" class="text-[11px] text-gray-500 mt-2">Suma z zaplanowanych posiłków (porcje × wartości z przepisu).</p>
</div>
</div>
<button type="button" id="planner-open-ingredients" class="w-full mb-4 flex items-center justify-center gap-2 py-3 rounded-xl border border-dashed border-gray-300 bg-white text-sm font-semibold text-gray-800 hover:border-gray-400 hover:bg-gray-50 transition-colors">
<i class="fas fa-shopping-basket text-gray-500" aria-hidden="true"></i>
<button type="button" id="planner-open-ingredients" class="w-full mb-3 flex items-center justify-center gap-2 py-2.5 rounded-xl border border-dashed border-gray-300 bg-white text-[13px] font-semibold text-gray-800 hover:border-gray-400 hover:bg-gray-50 transition-colors">
<i class="fas fa-shopping-basket text-gray-500 text-xs" aria-hidden="true"></i>
Składniki na ten dzień
</button>
<div id="planner-meal-slots" class="space-y-3 pb-2"></div>
@@ -149,30 +145,30 @@ export function getMealPlannerHTML() {
<div id="planner-picker-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-picker-sheet" class="absolute left-0 right-0 z-[50] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col min-h-0 will-change-transform" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}; max-height: calc(100% - ${PLANNER_SHEET_BOTTOM_INSET}); transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-labelledby="planner-picker-title" aria-modal="true">
<div class="shrink-0 p-4 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-3" aria-hidden="true"></div>
<h2 id="planner-picker-title" class="text-lg font-bold text-gray-900 leading-tight pr-2">Wybierz przepis</h2>
<p id="planner-picker-sub" class="text-xs text-gray-500 mt-1"></p>
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-picker-title" class="text-[15px] font-bold text-gray-900 leading-tight pr-2">Wybierz przepis</h2>
<p id="planner-picker-sub" class="text-[11px] text-gray-500 mt-1"></p>
</div>
<div id="planner-picker-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-3 pb-8 space-y-2"></div>
<div id="planner-picker-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2.5 pb-8 space-y-2"></div>
</div>
<div id="planner-ing-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-ing-sheet" class="absolute left-0 right-0 z-[50] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col min-h-0 will-change-transform" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}; max-height: calc(100% - ${PLANNER_SHEET_BOTTOM_INSET}); transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
<div class="shrink-0 p-4 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-3" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-lg font-bold text-gray-900 leading-tight pr-2">Składniki na dziś</h2>
<p class="text-xs text-gray-500 mt-1">Dodaj wszystko lub zaznacz wiersze. Zamknij: przeciągnij w dół lub dotknij tła.</p>
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-gray-100 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-[15px] font-bold text-gray-900 leading-tight pr-2">Składniki i spiżarnia</h2>
<p id="planner-ing-sub" class="text-[11px] text-gray-500 mt-1">Porównanie potrzeb z zapasami.</p>
</div>
<div id="planner-ing-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2 pb-2"></div>
<div class="shrink-0 p-4 pt-2 pb-5 border-t border-gray-100 bg-white space-y-2">
<button type="button" id="planner-ing-add-all" 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">
<i class="fas fa-cart-plus" aria-hidden="true"></i>
Dodaj wszystkie do listy zakupów
<div id="planner-ing-footer" class="shrink-0 p-4 pt-2 pb-5 border-t border-gray-100 bg-white space-y-2">
<button type="button" id="planner-ing-add-all" 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">
<i class="fas fa-cart-plus text-xs" aria-hidden="true"></i>
Dodaj braki na dziś do listy
</button>
<button type="button" id="planner-ing-add-btn" class="w-full border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 py-3 rounded-xl font-semibold text-sm flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-check-square text-gray-500 text-xs" aria-hidden="true"></i>
Dodaj tylko zaznaczone
<button type="button" id="planner-ing-add-btn" class="hidden w-full border border-gray-200 bg-white text-gray-800 hover:bg-gray-50 py-2.5 rounded-xl font-semibold text-[13px] flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-calendar-week text-gray-500 text-[11px]" aria-hidden="true"></i>
Dodaj braki na cały tydzień
</button>
</div>
</div>
@@ -257,27 +253,22 @@ function updatePeriodLabel(mode, weekStart, monthAnchor) {
}
function syncModeToggle(mode) {
const w = document.getElementById('planner-mode-week');
const m = document.getElementById('planner-mode-month');
const weekWrap = document.getElementById('calendar-week-wrap');
const monthWrap = document.getElementById('calendar-month-wrap');
const handleIcon = document.getElementById('calendar-handle-icon');
const base = 'planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors';
const active = `${base} bg-white text-gray-900 shadow-sm`;
const idle = `${base} text-gray-400 hover:text-gray-600`;
if (w && m) {
if (mode === 'week') {
w.className = active;
m.className = idle;
} else {
w.className = idle;
m.className = active;
}
if (weekWrap) {
weekWrap.style.maxHeight = mode === 'week' ? '10rem' : '0';
weekWrap.style.opacity = mode === 'week' ? '1' : '0';
}
if (weekWrap && monthWrap) {
weekWrap.classList.toggle('hidden', mode !== 'week');
monthWrap.classList.toggle('hidden', mode !== 'month');
if (monthWrap) {
monthWrap.style.maxHeight = mode === 'month' ? '25rem' : '0';
monthWrap.style.opacity = mode === 'month' ? '1' : '0';
}
if (handleIcon) {
handleIcon.className = mode === 'week'
? 'fas fa-chevron-down text-[8px] text-gray-300'
: 'fas fa-chevron-up text-[8px] text-gray-300';
}
}
@@ -291,6 +282,59 @@ function bindDayClicks(container, state, rerender) {
});
}
function bindCalendarSwipeGesture(state, rerender) {
const zone = document.getElementById('calendar-swipe-zone');
if (!zone) return;
let startY = 0;
let ptrId = null;
let moved = false;
zone.addEventListener('pointerdown', (e) => {
if (ptrId !== null) return;
startY = e.clientY;
ptrId = e.pointerId;
moved = false;
});
zone.addEventListener('pointermove', (e) => {
if (e.pointerId !== ptrId) return;
if (Math.abs(e.clientY - startY) > 10) moved = true;
});
zone.addEventListener('pointerup', (e) => {
if (e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
ptrId = null;
if (!moved || Math.abs(dy) < 30) return;
let switched = false;
if (state.mode === 'week' && dy > 30) {
state.mode = 'month';
state.monthAnchor = startOfMonth(state.selected);
switched = true;
} else if (state.mode === 'month' && dy < -30) {
state.mode = 'week';
state.weekStart = startOfWeekMonday(state.selected);
switched = true;
}
if (switched) {
zone.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
}, { capture: true, once: true });
rerender();
}
});
zone.addEventListener('pointercancel', () => {
ptrId = null;
moved = false;
});
}
function showPlannerToast(message) {
const wrap = document.getElementById('planner-toast');
const text = document.getElementById('planner-toast-text');
@@ -405,9 +449,24 @@ function renderDayContent(state) {
const ingBtn = document.getElementById('planner-open-ingredients');
if (ingBtn) {
ingBtn.disabled = totals.mealCount === 0;
ingBtn.classList.toggle('opacity-50', totals.mealCount === 0);
ingBtn.classList.toggle('cursor-not-allowed', totals.mealCount === 0);
const noMeals = totals.mealCount === 0;
ingBtn.disabled = noMeals;
ingBtn.classList.toggle('opacity-50', noMeals);
ingBtn.classList.toggle('cursor-not-allowed', noMeals);
if (!noMeals) {
const shortCount = countDayShortfalls(dayPlan, loadPantry());
if (shortCount > 0) {
ingBtn.innerHTML = `<i class="fas fa-shopping-basket text-xs" aria-hidden="true"></i>
Składniki na ten dzień
<span class="ml-auto bg-red-500 text-white text-[10px] font-bold rounded-full w-5 h-5 inline-flex items-center justify-center">${shortCount}</span>`;
} else {
ingBtn.innerHTML = `<i class="fas fa-check-circle text-emerald-500 text-xs" aria-hidden="true"></i>
Składniki na ten dzień
<span class="ml-auto text-[10px] font-semibold text-emerald-600">OK</span>`;
}
} else {
ingBtn.innerHTML = `<i class="fas fa-shopping-basket text-gray-500 text-xs" aria-hidden="true"></i> Składniki na ten dzień`;
}
}
const slotsRoot = document.getElementById('planner-meal-slots');
@@ -427,14 +486,14 @@ function renderDayContent(state) {
const kcal = Math.round(n.kcal * servings);
const eid = escapeHtml(entry.id);
return `
<div class="rounded-lg border border-gray-200 bg-white p-2.5 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<div class="w-9 h-9 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[9px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>
</div>
<div class="min-w-0">
<p class="text-sm font-bold text-gray-900 truncate">${escapeHtml(recipe.title)}</p>
<p class="text-[13px] font-bold text-gray-900 truncate">${escapeHtml(recipe.title)}</p>
<p class="text-[11px] text-gray-500 mt-0.5 tabular-nums">
<i class="fas fa-clock text-gray-400 mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-gray-300">·</span>
@@ -442,11 +501,11 @@ function renderDayContent(state) {
</p>
</div>
</div>
<button type="button" class="planner-clear-meal w-7 h-7 shrink-0 rounded-full border border-gray-200 text-gray-400 hover:text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[10px]" aria-hidden="true"></i>
<button type="button" class="planner-clear-meal w-6 h-6 shrink-0 rounded-full border border-gray-200 text-gray-400 hover:text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
</button>
</div>
<div class="flex items-center justify-between gap-2 mt-2 pt-2 border-t border-gray-100">
<div class="flex items-center justify-between gap-2 mt-1.5 pt-1.5 border-t border-gray-100">
<span class="text-[11px] font-medium text-gray-500">Porcje</span>
<div class="flex items-center gap-0.5 bg-gray-100 p-0.5 rounded-lg">
<button type="button" class="planner-serv-minus w-6 h-6 bg-white rounded-md shadow-sm flex items-center justify-center text-gray-600 hover:text-black" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Mniej porcji"><i class="fas fa-minus text-[9px]"></i></button>
@@ -459,19 +518,19 @@ function renderDayContent(state) {
const addLabel = entries.length === 0 ? 'Dodaj przepis' : 'Dodaj kolejny przepis';
const addClasses = entries.length === 0
? 'planner-add-meal w-full py-2.5 rounded-lg border border-dashed border-gray-200 text-sm font-semibold text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-colors'
: 'planner-add-meal w-full py-2 rounded-lg border border-dashed border-gray-200 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors';
? 'planner-add-meal w-full py-2 rounded-lg border border-dashed border-gray-200 text-[13px] font-semibold text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-colors'
: 'planner-add-meal w-full py-1.5 rounded-lg border border-dashed border-gray-200 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors';
return `
<div class="rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden" data-slot-id="${slot.id}">
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-gray-100 bg-gray-50/90">
<span class="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500 shrink-0">
<i class="fas ${slot.icon} text-sm" aria-hidden="true"></i>
<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50/90">
<span class="w-7 h-7 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500 shrink-0">
<i class="fas ${slot.icon} text-[13px]" aria-hidden="true"></i>
</span>
<span class="text-sm font-semibold text-gray-900 truncate min-w-0 flex-1">${slot.label}</span>
<span class="text-[13px] font-semibold text-gray-900 truncate min-w-0 flex-1">${slot.label}</span>
${countLabel}
</div>
<div class="p-3 space-y-2">
<div class="p-2.5 space-y-2">
${entryCards}
<button type="button" class="${addClasses}" data-slot-id="${slot.id}">
<i class="fas fa-plus text-[10px] mr-1 opacity-70" aria-hidden="true"></i>
@@ -507,12 +566,12 @@ function renderPickerList(slotId) {
}
list.innerHTML = recipes.map((r) => `
<button type="button" class="planner-pick-recipe w-full flex gap-3 p-3 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white text-left transition-all" data-recipe-id="${r.id}">
<div class="w-14 h-14 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[10px] font-medium">${escapeHtml(r.thumbLabel)}</span>
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="text-sm font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
<p class="text-[13px] font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
<p class="text-[11px] text-gray-500 mt-1 tabular-nums">
<i class="fas fa-fire text-gray-400 mr-0.5" aria-hidden="true"></i>${r.nutritionPerServing.kcal} kcal
<span class="mx-1 text-gray-300">·</span>
@@ -523,43 +582,175 @@ function renderPickerList(slotId) {
`).join('');
}
function plIngredientWord(n) {
if (n === 1) return 'składnik';
const m10 = n % 10;
const m100 = n % 100;
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'składniki';
return 'składników';
}
function updateIngButtons(state) {
const btn1 = document.getElementById('planner-ing-add-all');
const btn2 = document.getElementById('planner-ing-add-btn');
const todayCount = (state._todayShortfalls || []).length;
const allCount = (state._allForecastShortfalls || []).length;
if (btn1) {
if (todayCount > 0) {
btn1.classList.remove('hidden');
btn1.disabled = false;
btn1.innerHTML = `<i class="fas fa-cart-plus text-xs" aria-hidden="true"></i> Dodaj braki na dziś do listy`;
} else {
btn1.classList.add('hidden');
}
}
if (btn2) {
if (allCount > todayCount) {
btn2.classList.remove('hidden');
btn2.innerHTML = `<i class="fas fa-calendar-week text-gray-500 text-[11px]" aria-hidden="true"></i> Dodaj braki na cały tydzień`;
} else {
btn2.classList.add('hidden');
}
}
}
function renderIngredientsSheet(state) {
const body = document.getElementById('planner-ing-body');
const titleEl = document.getElementById('planner-ing-title');
const subEl = document.getElementById('planner-ing-sub');
if (!body) return;
const dayPlan = getDayPlan(state.plans, state.selected);
const blocks = aggregateDayIngredientsBySlot(dayPlan);
const pantry = loadPantry();
const forecast = computeFullForecast(state.plans, pantry, state.selected);
if (blocks.length === 0) {
const today = forecast.length > 0 && forecast[0].dayIndex === 0 ? forecast[0] : null;
const upcoming = forecast.filter((d) => d.dayIndex > 0 && d.hasShortfall);
state._todayShortfalls = today ? today.items.filter((it) => !it.enough) : [];
state._allForecastShortfalls = [];
for (const d of forecast) {
for (const it of d.items) {
if (!it.enough) state._allForecastShortfalls.push(it);
}
}
if (titleEl) {
const wd = WEEKDAYS_LONG[state.selected.getDay()];
titleEl.textContent = `${wd}, ${state.selected.getDate()} ${MONTHS_SHORT[state.selected.getMonth()]} — składniki`;
}
if (subEl) subEl.textContent = 'Porównanie potrzeb z zapasami w spiżarni.';
if (!today || today.items.length === 0) {
body.innerHTML = '<p class="text-sm text-gray-500 text-center py-8">Najpierw zaplanuj posiłki.</p>';
updateIngButtons(state);
return;
}
body.innerHTML = blocks.map((block) => `
<div class="mb-6 last:mb-2">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wide mb-2">${escapeHtml(block.mealLabel)}</p>
<div class="space-y-3">
${block.recipes.map((rec) => `
<div>
<p class="text-sm font-semibold text-gray-800 mb-1.5">${escapeHtml(rec.recipeTitle)}</p>
<ul class="space-y-0 border border-gray-100 rounded-xl overflow-hidden bg-white">
${rec.items.map((ing) => `
<li class="flex items-center gap-3 py-3 px-3 border-b border-gray-100 last:border-b-0 cursor-pointer hover:bg-gray-50 transition-colors planner-ing-row"
data-ingredient-id="${escapeHtml(ing.ingredientId)}"
data-amount="${escapeHtml(String(ing.amount))}"
data-unit="${escapeHtml(ing.unit)}"
data-category="${escapeHtml(ing.category)}">
<div class="w-5 h-5 rounded border border-gray-300 flex items-center justify-center text-white check-box transition-colors shrink-0"><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">${escapeHtml(ing.name)}</span>
<span class="font-medium text-gray-900 text-sm tabular-nums">${formatAmount(ing.amount)} ${escapeHtml(ing.unit)}</span>
</li>
`).join('')}
</ul>
</div>
`).join('')}
const shortItems = today.items.filter((it) => !it.enough);
const okItems = today.items.filter((it) => it.enough);
let html = '';
if (shortItems.length === 0) {
html += `<div class="rounded-xl bg-emerald-50 border border-emerald-200/80 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-emerald-100 flex items-center justify-center shrink-0">
<i class="fas fa-check text-emerald-600 text-sm"></i>
</div>
</div>
`).join('');
<div>
<p class="text-[13px] font-semibold text-emerald-800">Wszystko masz w spiżarni</p>
<p class="text-[11px] text-emerald-600/80">${today.items.length} ${plIngredientWord(today.items.length)} — zapasy wystarczą</p>
</div>
</div>`;
} else {
html += `<div class="rounded-xl bg-red-50 border border-red-200/80 p-3 mb-4 flex items-center gap-2.5">
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center shrink-0">
<i class="fas fa-exclamation text-red-500 text-sm"></i>
</div>
<div>
<p class="text-[13px] font-semibold text-red-800">${shortItems.length} ${plIngredientWord(shortItems.length)} do kupienia</p>
<p class="text-[11px] text-red-600/80">Brakuje składników na zaplanowane posiłki</p>
</div>
</div>`;
}
if (shortItems.length > 0) {
html += `<div class="mb-5">
<p class="text-[10px] font-bold text-red-400 uppercase tracking-wider mb-2 px-0.5">
<i class="fas fa-cart-shopping text-[9px] mr-1"></i>Do kupienia
</p>
<ul class="border border-red-100/80 rounded-xl overflow-hidden bg-white divide-y divide-red-50">
${shortItems.map((ing) => `
<li class="flex items-start gap-3 py-3 px-3">
<div class="w-2 h-2 rounded-full bg-red-400 mt-1.5 shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-semibold text-gray-900">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-gray-500 mt-0.5">
potrzeba <span class="font-medium text-gray-700">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-gray-300">&middot;</span>
w spiżarni <span class="font-medium ${ing.pantryQty > 0 ? 'text-amber-600' : 'text-gray-400'}">${ing.pantryQty > 0 ? formatAmount(ing.pantryQty) + ' ' + escapeHtml(ing.pantryUnit) : 'brak'}</span>
</p>
</div>
<div class="text-right shrink-0 pt-0.5">
<p class="text-[13px] font-bold text-red-600 tabular-nums leading-tight">&minus;${formatAmount(ing.shortfall)}</p>
<p class="text-[9px] text-red-400 font-medium">${escapeHtml(ing.pantryUnit)}</p>
</div>
</li>`).join('')}
</ul>
</div>`;
}
if (okItems.length > 0) {
html += `<div class="mb-5">
<p class="text-[10px] font-bold text-emerald-500 uppercase tracking-wider mb-2 px-0.5">
<i class="fas fa-check text-[9px] mr-1"></i>W spiżarni
</p>
<ul class="border border-gray-100 rounded-xl overflow-hidden bg-white divide-y divide-gray-50">
${okItems.map((ing) => `
<li class="flex items-start gap-3 py-2.5 px-3">
<div class="w-2 h-2 rounded-full bg-emerald-400 mt-1.5 shrink-0"></div>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-medium text-gray-700">${escapeHtml(ing.name)}</p>
<p class="text-[11px] text-gray-400 mt-0.5">
potrzeba <span class="font-medium">${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}</span>
<span class="mx-1 text-gray-300">&middot;</span>
masz <span class="font-medium text-emerald-600">${formatAmount(ing.pantryQty)} ${escapeHtml(ing.pantryUnit)}</span>
</p>
</div>
</li>`).join('')}
</ul>
</div>`;
}
if (upcoming.length > 0) {
html += `<div class="mb-2">
<p class="text-[10px] font-bold text-amber-500 uppercase tracking-wider mb-2 px-0.5">
<i class="fas fa-calendar-alt text-[9px] mr-1"></i>Nadchodzące braki
</p>
<div class="space-y-2">
${upcoming.map((day) => {
const wd = WEEKDAYS_LONG[day.date.getDay()];
const label = `${wd}, ${day.date.getDate()} ${MONTHS_SHORT[day.date.getMonth()]}`;
const shorts = day.items.filter((it) => !it.enough);
return `<div class="rounded-xl border border-amber-200/80 bg-amber-50/50 p-3">
<p class="text-[12px] font-semibold text-amber-900">
<i class="fas fa-calendar-day text-[10px] mr-1.5 text-amber-500"></i>${escapeHtml(label)}
</p>
<ul class="mt-2 space-y-1.5">
${shorts.map((it) => `
<li class="flex items-center justify-between text-[11px]">
<span class="font-medium text-amber-900">${escapeHtml(it.name)}</span>
<span class="font-semibold text-red-600 tabular-nums">&minus;${formatAmount(it.shortfall)} ${escapeHtml(it.pantryUnit)}</span>
</li>`).join('')}
</ul>
</div>`;
}).join('')}
</div>
</div>`;
}
body.innerHTML = html;
updateIngButtons(state);
}
function formatAmount(n) {
@@ -606,11 +797,8 @@ export function setupMealPlanner() {
syncModeToggle(state.mode);
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor);
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
if (state.mode === 'week') {
renderWeekGrid(state.weekStart, state.selected, state.plans);
} else {
renderMonthGrid(state.monthAnchor, state.selected, state.plans);
}
renderWeekGrid(state.weekStart, state.selected, state.plans);
renderMonthGrid(state.monthAnchor, state.selected, state.plans);
renderDayContent(state);
};
@@ -622,20 +810,6 @@ export function setupMealPlanner() {
bindDayClicks(weekGrid?.parentElement, state, rerender);
bindDayClicks(monthGrid?.parentElement, state, rerender);
document.getElementById('planner-cal-mode')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-cal-mode]');
if (!btn) return;
const mode = btn.getAttribute('data-cal-mode');
if (mode !== 'week' && mode !== 'month') return;
state.mode = mode;
if (mode === 'week') {
state.weekStart = startOfWeekMonday(state.selected);
} else {
state.monthAnchor = startOfMonth(state.selected);
}
rerender();
});
document.getElementById('cal-prev')?.addEventListener('click', () => {
if (state.mode === 'week') {
state.weekStart = addWeeks(state.weekStart, -1);
@@ -766,59 +940,56 @@ export function setupMealPlanner() {
});
document.getElementById('planner-ing-add-all')?.addEventListener('click', () => {
const body = document.getElementById('planner-ing-body');
const rows = body?.querySelectorAll('.planner-ing-row');
const n = rows?.length ?? 0;
if (n === 0) return;
const lines = [];
rows.forEach((row) => {
const id = row.getAttribute('data-ingredient-id');
const amount = parseFloat(row.getAttribute('data-amount') || '');
const unit = row.getAttribute('data-unit') || '';
const category = row.getAttribute('data-category') || '';
if (!id || !Number.isFinite(amount)) return;
lines.push({
ingredientId: id,
amount,
unit,
category,
sourceNote: 'Z planu dnia',
});
});
const items = state._todayShortfalls || [];
if (items.length === 0) return;
const lines = items.map((it) => ({
ingredientId: it.ingredientId,
amount: it.shortfall,
unit: it.pantryUnit,
category: it.category,
sourceNote: 'Braki z planu dnia',
}));
addOrMergeShoppingLines(lines);
showPlannerToast(`Dodano ${lines.length} składników na listę.`);
showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
document.getElementById('planner-ing-add-btn')?.addEventListener('click', () => {
const body = document.getElementById('planner-ing-body');
const selected = body?.querySelectorAll('.planner-ing-row.ingredient-active');
const n = selected?.length ?? 0;
if (n === 0) {
showPlannerToast('Zaznacz składniki na liście albo użyj „Dodaj wszystkie”.');
return;
const items = state._allForecastShortfalls || [];
if (items.length === 0) return;
const map = new Map();
for (const it of items) {
const key = it.ingredientId;
if (map.has(key)) {
const cur = map.get(key);
cur.amount = Math.round((cur.amount + it.shortfall) * 10) / 10;
} else {
map.set(key, {
ingredientId: it.ingredientId,
amount: it.shortfall,
unit: it.pantryUnit,
category: it.category,
sourceNote: 'Braki z planu tygodnia',
});
}
}
const lines = [];
selected.forEach((row) => {
const id = row.getAttribute('data-ingredient-id');
const amount = parseFloat(row.getAttribute('data-amount') || '');
const unit = row.getAttribute('data-unit') || '';
const category = row.getAttribute('data-category') || '';
if (!id || !Number.isFinite(amount)) return;
lines.push({
ingredientId: id,
amount,
unit,
category,
sourceNote: 'Z planu dnia',
});
});
const lines = [...map.values()];
addOrMergeShoppingLines(lines);
showPlannerToast(`Dodano ${lines.length} pozycji na listę.`);
showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
rerender();
bindCalendarSwipeGesture(state, rerender);
requestAnimationFrame(() => {
const ww = document.getElementById('calendar-week-wrap');
const mw = document.getElementById('calendar-month-wrap');
const t = 'max-height 300ms ease, opacity 200ms ease';
if (ww) ww.style.transition = t;
if (mw) mw.style.transition = t;
});
}

View File

@@ -55,10 +55,10 @@ export function getPantryHTML() {
<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-1.5 overflow-x-auto no-scrollbar -mx-1 px-1 pb-0.5"></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-[11px] font-medium text-gray-500">Tylko na stanie</span>
<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>
@@ -68,7 +68,7 @@ export function getPantryHTML() {
</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-1"></div>
<div id="pantry-board" class="px-4 pt-3 pb-4 space-y-2"></div>
</div>
<!-- ── product sheet ── -->
@@ -142,9 +142,9 @@ function renderCategoryChips() {
const active = selectedCategories.has(k);
const icon = CATEGORY_ICONS[k] || 'fa-jar';
const cls = active
? 'shrink-0 inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-[11px] font-semibold bg-gray-900 text-white transition-colors'
: 'shrink-0 inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-[11px] 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-[9px]"></i>${esc(categoryLabel(k))}</button>`;
? '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 => {
@@ -176,15 +176,15 @@ function chipHtml(id, pantry) {
const u = unitLabel(def.pantryUnit);
if (qty > 0) {
return `<button type="button" class="pv2-chip inline-flex flex-col items-start px-3 py-2 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-[12px] font-semibold text-gray-900 leading-tight whitespace-nowrap">${esc(def.name)}</span>
<span class="text-[10px] text-emerald-600 font-semibold tabular-nums leading-tight mt-0.5">${Math.round(qty)} ${esc(u)}</span>
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 py-2 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-[12px] font-medium text-gray-400 group-hover:text-gray-600 whitespace-nowrap transition-colors">${esc(def.name)}</span>
<i class="fas fa-plus text-[7px] text-gray-300 group-hover:text-gray-500 ml-1.5 transition-colors"></i>
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>`;
}
@@ -230,11 +230,11 @@ function renderBoard() {
for (const { cat, ids } of groups) {
const icon = CATEGORY_ICONS[cat] || 'fa-jar';
html += `
<div class="mb-3 last:mb-0">
<p class="text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5 px-0.5">
<i class="fas ${icon} text-[9px] mr-1"></i>${esc(categoryLabel(cat))}
<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-1.5">${ids.map(id => chipHtml(id, pantry)).join('')}</div>
<div class="flex flex-wrap gap-2">${ids.map(id => chipHtml(id, pantry)).join('')}</div>
</div>`;
}

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('');