const MONTHS_SHORT = [
'sty', 'lut', 'mar', 'kwi', 'maj', 'cze',
'lip', 'sie', 'wrz', 'paź', 'lis', 'gru',
];
const WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd'];
const WEEKDAYS_LONG = [
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
];
const MEAL_SLOTS = [
{ id: 'sniadanie', label: 'Śniadanie', icon: 'fa-sun' },
{ id: 'drugie_sniadanie', label: 'Drugie śniadanie', icon: 'fa-coffee' },
{ id: 'obiad', label: 'Obiad', icon: 'fa-utensils' },
{ id: 'przekaska', label: 'Przekąska', icon: 'fa-apple-alt' },
{ id: 'kolacja', label: 'Kolacja', icon: 'fa-moon' },
];
/** Katalog przepisów (spójny z listą w aplikacji) — porcja bazowa = 1 */
const PLANNER_RECIPES = {
placki: {
id: 'placki',
title: 'Puszyste placki',
minutes: 15,
thumbLabel: 'Placki',
allowedSlots: ['sniadanie', 'drugie_sniadanie'],
nutritionPerServing: { kcal: 320, protein: 12, fat: 8, carbs: 48 },
ingredients: [
{ name: 'Mąka pszenna', amount: 200, unit: 'g' },
{ name: 'Mleko', amount: 250, unit: 'ml' },
{ name: 'Jajka', amount: 2, unit: 'szt.' },
],
},
salatka: {
id: 'salatka',
title: 'Sałatka z kurczakiem',
minutes: 20,
thumbLabel: 'Sałatka',
allowedSlots: ['obiad'],
nutritionPerServing: { kcal: 250, protein: 35, fat: 9, carbs: 12 },
ingredients: [
{ name: 'Pierś z kurczaka', amount: 150, unit: 'g' },
{ name: 'Mix sałat', amount: 100, unit: 'g' },
{ name: 'Pomidor', amount: 1, unit: 'szt.' },
],
},
makaron: {
id: 'makaron',
title: 'Makaron z pomidorami i bazylią',
minutes: 30,
thumbLabel: 'Makaron',
allowedSlots: ['obiad', 'kolacja'],
nutritionPerServing: { kcal: 450, protein: 14, fat: 12, carbs: 72 },
ingredients: [
{ name: 'Makaron', amount: 120, unit: 'g' },
{ name: 'Pomidory krojone', amount: 400, unit: 'g' },
{ name: 'Bazylia świeża', amount: 10, unit: 'g' },
],
},
koktajl: {
id: 'koktajl',
title: 'Koktajl owocowy',
minutes: 5,
thumbLabel: 'Koktajl',
allowedSlots: ['przekaska', 'drugie_sniadanie'],
nutritionPerServing: { kcal: 180, protein: 8, fat: 3, carbs: 32 },
ingredients: [
{ name: 'Jogurt naturalny', amount: 200, unit: 'g' },
{ name: 'Mieszanka jagód', amount: 150, unit: 'g' },
{ name: 'Miód', amount: 15, unit: 'g' },
],
},
tost_awokado: {
id: 'tost_awokado',
title: 'Tost z awokado',
minutes: 10,
thumbLabel: 'Tost',
allowedSlots: ['sniadanie', 'drugie_sniadanie'],
nutritionPerServing: { kcal: 220, protein: 6, fat: 14, carbs: 20 },
ingredients: [
{ name: 'Chleb na zakwasie', amount: 2, unit: 'kromki' },
{ name: 'Awokado', amount: 1, unit: 'szt.' },
{ name: 'Cytryna', amount: 0.5, unit: 'szt.' },
],
},
losos: {
id: 'losos',
title: 'Grillowany łosoś',
minutes: 25,
thumbLabel: 'Łosoś',
allowedSlots: ['kolacja', 'obiad'],
nutritionPerServing: { kcal: 380, protein: 38, fat: 22, carbs: 4 },
ingredients: [
{ name: 'Filet z łososia', amount: 180, unit: 'g' },
{ name: 'Cytryna', amount: 0.5, unit: 'szt.' },
{ name: 'Koper', amount: 5, unit: 'g' },
],
},
tacos: {
id: 'tacos',
title: 'Tacos z wołowiną',
minutes: 20,
thumbLabel: 'Tacos',
allowedSlots: ['kolacja', 'obiad'],
nutritionPerServing: { kcal: 410, protein: 28, fat: 18, carbs: 38 },
ingredients: [
{ name: 'Mięso mielone wołowe', amount: 200, unit: 'g' },
{ name: 'Tortille kukurydziane', amount: 4, unit: 'szt.' },
{ name: 'Salsa pomidorowa', amount: 100, unit: 'g' },
],
},
owsianka: {
id: 'owsianka',
title: 'Miska owsianki',
minutes: 10,
thumbLabel: 'Owsianka',
allowedSlots: ['sniadanie', 'drugie_sniadanie'],
nutritionPerServing: { kcal: 210, protein: 8, fat: 6, carbs: 34 },
ingredients: [
{ name: 'Płatki owsiane', amount: 60, unit: 'g' },
{ name: 'Mleko', amount: 200, unit: 'ml' },
{ name: 'Miód', amount: 20, unit: 'g' },
],
},
serek_owoc: {
id: 'serek_owoc',
title: 'Serek wiejski z orzechami i owocami',
minutes: 5,
thumbLabel: 'Serek',
allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'],
nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 },
ingredients: [
{ name: 'Serek wiejski', amount: 200, unit: 'g' },
{ name: 'Miód', amount: 10, unit: 'g' },
{ name: 'Orzechy włoskie', amount: 50, unit: 'g' },
{ name: 'Truskawki', amount: 100, unit: 'g' },
{ name: 'Borówki ameryk.', amount: 80, unit: 'g' },
],
},
};
const PLANS_STORAGE_KEY = 'recipe-planner-plans-v1';
/** Odstęp od dołu planera = miejsce na dolną nawigację. Ten sam w `bottom`, `max-height` i w `translateY(calc(100% + …))` przy zamknięciu — inaczej zostaje widoczny uchwyt. */
const PLANNER_SHEET_BOTTOM_INSET = '5.25rem';
const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`;
function startOfDay(d) {
const x = new Date(d);
x.setHours(0, 0, 0, 0);
return x;
}
function sameDay(a, b) {
return a.getFullYear() === b.getFullYear()
&& a.getMonth() === b.getMonth()
&& a.getDate() === b.getDate();
}
function addDays(d, n) {
const x = new Date(d);
x.setDate(x.getDate() + n);
return startOfDay(x);
}
/** Poniedziałek jako pierwszy dzień tygodnia (PL) */
function startOfWeekMonday(d) {
const date = startOfDay(d);
const day = date.getDay();
const diff = day === 0 ? -6 : 1 - day;
return addDays(date, diff);
}
function startOfMonth(d) {
const x = new Date(d.getFullYear(), d.getMonth(), 1);
return startOfDay(x);
}
function addMonths(d, n) {
const x = new Date(d);
x.setMonth(x.getMonth() + n);
return startOfDay(x);
}
function addWeeks(d, n) {
return addDays(d, n * 7);
}
function weekContains(weekStart, d) {
const t = startOfDay(d).getTime();
const ws = weekStart.getTime();
const we = addDays(weekStart, 6).getTime();
return t >= ws && t <= we;
}
function sameMonth(a, b) {
return a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
}
function dateKey(d) {
const x = startOfDay(d);
const y = x.getFullYear();
const m = String(x.getMonth() + 1).padStart(2, '0');
const day = String(x.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
function newPlanEntryId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `e${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}
/** Jedna pora dnia = tablica wpisów { id, recipeId, servings } (stary format: jeden obiekt — migrujemy przy wczytaniu). */
function normalizeSlotValue(v) {
if (!v) return [];
if (Array.isArray(v)) {
return v
.filter((x) => x && x.recipeId && PLANNER_RECIPES[x.recipeId])
.map((x) => ({
id: x.id && String(x.id).length ? String(x.id) : newPlanEntryId(),
recipeId: x.recipeId,
servings: Math.max(1, Math.min(12, Number(x.servings) || 1)),
}));
}
if (typeof v === 'object' && v.recipeId && PLANNER_RECIPES[v.recipeId]) {
return [{
id: newPlanEntryId(),
recipeId: v.recipeId,
servings: Math.max(1, Math.min(12, Number(v.servings) || 1)),
}];
}
return [];
}
function normalizeDayPlan(day) {
if (!day || typeof day !== 'object') return {};
const out = {};
MEAL_SLOTS.forEach((s) => {
const arr = normalizeSlotValue(day[s.id]);
if (arr.length > 0) out[s.id] = arr;
});
return out;
}
function normalizeAllPlans(plans) {
if (!plans || typeof plans !== 'object') return {};
const out = {};
Object.keys(plans).forEach((key) => {
const d = normalizeDayPlan(plans[key]);
if (Object.keys(d).length > 0) out[key] = d;
});
return out;
}
function loadPlans() {
try {
const raw = localStorage.getItem(PLANS_STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
if (typeof parsed !== 'object' || parsed === null) return {};
return normalizeAllPlans(parsed);
} catch {
return {};
}
}
function savePlans(plans) {
try {
localStorage.setItem(PLANS_STORAGE_KEY, JSON.stringify(plans));
} catch { /* ignore */ }
}
function getDayPlan(plans, d) {
const key = dateKey(d);
const day = plans[key];
return day && typeof day === 'object' ? day : {};
}
function dayHasAnyMeal(plans, d) {
const p = getDayPlan(plans, d);
return MEAL_SLOTS.some((s) => {
const arr = p[s.id];
return Array.isArray(arr) && arr.length > 0;
});
}
function sumDayNutrition(dayPlan) {
let kcal = 0;
let protein = 0;
let fat = 0;
let carbs = 0;
let mealCount = 0;
MEAL_SLOTS.forEach((slot) => {
const entries = dayPlan[slot.id];
if (!Array.isArray(entries)) return;
entries.forEach((entry) => {
if (!entry || !entry.recipeId) return;
const r = PLANNER_RECIPES[entry.recipeId];
if (!r) return;
const s = Math.max(1, Number(entry.servings) || 1);
mealCount += 1;
kcal += r.nutritionPerServing.kcal * s;
protein += r.nutritionPerServing.protein * s;
fat += r.nutritionPerServing.fat * s;
carbs += r.nutritionPerServing.carbs * s;
});
});
return {
kcal: Math.round(kcal),
protein: Math.round(protein * 10) / 10,
fat: Math.round(fat * 10) / 10,
carbs: Math.round(carbs * 10) / 10,
mealCount,
};
}
/**
* Jedna grupa na porę dnia: nagłówek pory raz, potem bloki przepisów ze składnikami.
* @returns {{ mealLabel: string, recipes: { recipeTitle: string, items: { name: string, amount: number, unit: string }[] }[] }[]}
*/
function aggregateDayIngredientsBySlot(dayPlan) {
/** @type {{ mealLabel: string, recipes: { recipeTitle: string, items: { name: string, amount: number, unit: string }[] }[] }[]} */
const blocks = [];
MEAL_SLOTS.forEach((slot) => {
const entries = dayPlan[slot.id];
if (!Array.isArray(entries) || entries.length === 0) return;
const recipes = [];
entries.forEach((entry) => {
if (!entry || !entry.recipeId) return;
const r = PLANNER_RECIPES[entry.recipeId];
if (!r) return;
const s = Math.max(1, Number(entry.servings) || 1);
const items = r.ingredients.map((ing) => ({
name: ing.name,
amount: Math.round(ing.amount * s * 10) / 10,
unit: ing.unit,
}));
recipes.push({ recipeTitle: r.title, items });
});
if (recipes.length > 0) {
blocks.push({ mealLabel: slot.label, recipes });
}
});
return blocks;
}
function recipesForSlot(slotId) {
return Object.values(PLANNER_RECIPES).filter((r) => r.allowedSlots.includes(slotId));
}
function isCalendarOnToday(mode, weekStart, monthAnchor, selected) {
const today = startOfDay(new Date());
if (!sameDay(selected, today)) return false;
if (mode === 'week') return weekContains(weekStart, today);
return sameMonth(monthAnchor, today);
}
function syncTodayButton(mode, weekStart, monthAnchor, selected) {
const btn = document.getElementById('cal-go-today');
if (!btn) return;
const onToday = isCalendarOnToday(mode, weekStart, monthAnchor, selected);
const active = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors';
const dim = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-100 bg-gray-50 px-2 text-[10px] font-semibold text-gray-400 shadow-none cursor-default transition-colors';
btn.className = onToday ? dim : active;
btn.disabled = onToday;
}
export function getMealPlannerHTML() {
return `
${WEEKDAYS_SHORT.map((d) => `
${d}
`).join('')}
${WEEKDAYS_SHORT.map((d) => `
${d}
`).join('')}
`;
}
function renderWeekGrid(weekStart, selected, plans) {
const grid = document.getElementById('calendar-week-grid');
if (!grid) return;
const cells = [];
for (let i = 0; i < 7; i++) {
const day = addDays(weekStart, i);
const isSel = selected && sameDay(day, selected);
const isToday = sameDay(day, new Date());
const hasMeals = dayHasAnyMeal(plans, day);
cells.push(`
`);
}
grid.innerHTML = cells.join('');
}
function renderMonthGrid(monthAnchor, selected, plans) {
const grid = document.getElementById('calendar-month-grid');
if (!grid) return;
const first = startOfMonth(monthAnchor);
const startGrid = startOfWeekMonday(first);
const cells = [];
for (let i = 0; i < 42; i++) {
const day = addDays(startGrid, i);
const inMonth = day.getMonth() === first.getMonth();
const isSel = selected && sameDay(day, selected);
const isToday = sameDay(day, new Date());
const hasMeals = inMonth && dayHasAnyMeal(plans, day);
cells.push(`
`);
}
grid.innerHTML = cells.join('');
}
function updatePeriodLabel(mode, weekStart, monthAnchor) {
const el = document.getElementById('cal-period-label');
if (!el) return;
if (mode === 'week') {
const end = addDays(weekStart, 6);
const y = weekStart.getFullYear();
if (weekStart.getMonth() === end.getMonth()) {
el.textContent = `${weekStart.getDate()}–${end.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} ${y}`;
} else {
el.textContent = `${weekStart.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} – ${end.getDate()} ${MONTHS_SHORT[end.getMonth()]} ${y}`;
}
} else {
const m = monthAnchor.getMonth();
const y = monthAnchor.getFullYear();
const monthLong = [
'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec',
'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień',
][m];
el.textContent = `${monthLong} ${y}`;
}
}
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 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 && monthWrap) {
weekWrap.classList.toggle('hidden', mode !== 'week');
monthWrap.classList.toggle('hidden', mode !== 'month');
}
}
function bindDayClicks(container, state, rerender) {
container?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-planner-day]');
if (!btn) return;
const ts = Number(btn.getAttribute('data-planner-day'));
state.selected = new Date(ts);
rerender();
});
}
function showPlannerToast(message) {
const wrap = document.getElementById('planner-toast');
const text = document.getElementById('planner-toast-text');
if (!wrap || !text) return;
text.textContent = message;
wrap.classList.remove('opacity-0', 'translate-y-2');
wrap.classList.add('opacity-100', 'translate-y-0');
clearTimeout(showPlannerToast._t);
showPlannerToast._t = setTimeout(() => {
wrap.classList.add('opacity-0', 'translate-y-2');
wrap.classList.remove('opacity-100', 'translate-y-0');
}, 2600);
}
function openSheet(backdrop, sheet) {
if (!backdrop || !sheet) return;
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = 'translateY(0)';
backdrop.classList.remove('hidden');
requestAnimationFrame(() => {
backdrop.classList.remove('opacity-0');
});
}
function closeSheet(backdrop, sheet) {
if (!backdrop || !sheet) return;
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = PLANNER_SHEET_OFF_TRANSFORM;
backdrop.classList.add('opacity-0');
setTimeout(() => backdrop.classList.add('hidden'), 300);
}
/** Zamykanie panelu: przeciągnięcie nagłówka w dół (pointer). */
function bindPlannerSheetDragClose(sheet, closeFn) {
const zone = sheet.querySelector('[data-planner-sheet-drag-zone]');
if (!zone || !sheet) return;
let startY = 0;
let pulling = false;
let ptrId = null;
const resetVisual = () => {
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = 'translateY(0)';
};
zone.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return;
pulling = true;
ptrId = e.pointerId;
startY = e.clientY;
sheet.style.transition = 'none';
zone.setPointerCapture(e.pointerId);
});
zone.addEventListener('pointermove', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = Math.max(0, e.clientY - startY);
sheet.style.transform = `translateY(${dy}px)`;
});
zone.addEventListener('pointerup', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
pulling = false;
ptrId = null;
try {
zone.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
if (dy > 56) {
closeFn();
return;
}
resetVisual();
});
zone.addEventListener('pointercancel', () => {
pulling = false;
ptrId = null;
resetVisual();
});
}
function renderDayContent(state) {
const sel = state.selected;
const heading = document.getElementById('planner-day-heading');
if (heading) {
const wd = WEEKDAYS_LONG[sel.getDay()];
heading.textContent = `${wd}, ${sel.getDate()} ${MONTHS_SHORT[sel.getMonth()]}`;
}
const dayPlan = getDayPlan(state.plans, sel);
const totals = sumDayNutrition(dayPlan);
const kcalEl = document.getElementById('planner-summary-kcal');
if (kcalEl) {
kcalEl.innerHTML = totals.mealCount === 0
? `— kcal`
: `${totals.kcal} kcal`;
}
const fmt = (n) => `${n} g`;
document.getElementById('planner-macro-p').textContent = totals.mealCount ? fmt(totals.protein) : '—';
document.getElementById('planner-macro-f').textContent = totals.mealCount ? fmt(totals.fat) : '—';
document.getElementById('planner-macro-c').textContent = totals.mealCount ? fmt(totals.carbs) : '—';
document.getElementById('planner-detail-kcal').textContent = `${totals.kcal} kcal`;
document.getElementById('planner-detail-p').textContent = fmt(totals.protein);
document.getElementById('planner-detail-f').textContent = fmt(totals.fat);
document.getElementById('planner-detail-c').textContent = fmt(totals.carbs);
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 slotsRoot = document.getElementById('planner-meal-slots');
if (!slotsRoot) return;
slotsRoot.innerHTML = MEAL_SLOTS.map((slot) => {
const entries = Array.isArray(dayPlan[slot.id]) ? dayPlan[slot.id] : [];
const countLabel = entries.length > 1
? `${entries.length} dania`
: '';
const entryCards = entries.map((entry) => {
const recipe = entry && entry.recipeId ? PLANNER_RECIPES[entry.recipeId] : null;
if (!recipe) return '';
const servings = Math.max(1, Number(entry.servings) || 1);
const n = recipe.nutritionPerServing;
const kcal = Math.round(n.kcal * servings);
const eid = escapeHtml(entry.id);
return `
${escapeHtml(recipe.thumbLabel)}
${escapeHtml(recipe.title)}
${recipe.minutes} min
·
${kcal} kcal
`;
}).join('');
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';
return `
${slot.label}
${countLabel}
${entryCards}
`;
}).join('');
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function renderPickerList(slotId) {
const slot = MEAL_SLOTS.find((s) => s.id === slotId);
const list = document.getElementById('planner-picker-list');
const title = document.getElementById('planner-picker-title');
const sub = document.getElementById('planner-picker-sub');
if (!list || !title || !sub) return;
title.textContent = 'Wybierz przepis';
sub.textContent = slot ? `Dla: ${slot.label}. Przeciągnij nagłówek w dół lub dotknij tła, by zamknąć.` : '';
const recipes = recipesForSlot(slotId);
if (recipes.length === 0) {
list.innerHTML = 'Brak dopasowanych przepisów.
';
return;
}
list.innerHTML = recipes.map((r) => `
`).join('');
}
function renderIngredientsSheet(state) {
const body = document.getElementById('planner-ing-body');
if (!body) return;
const dayPlan = getDayPlan(state.plans, state.selected);
const blocks = aggregateDayIngredientsBySlot(dayPlan);
if (blocks.length === 0) {
body.innerHTML = 'Najpierw zaplanuj posiłki.
';
return;
}
body.innerHTML = blocks.map((block) => `
${escapeHtml(block.mealLabel)}
${block.recipes.map((rec) => `
${escapeHtml(rec.recipeTitle)}
${rec.items.map((ing) => `
-
${escapeHtml(ing.name)}
${formatAmount(ing.amount)} ${escapeHtml(ing.unit)}
`).join('')}
`).join('')}
`).join('');
}
function formatAmount(n) {
return Number.isInteger(n) ? String(n) : String(n);
}
function seedDemoIfEmpty(plans) {
const todayKey = dateKey(new Date());
if (Object.keys(plans).length > 0) return plans;
return {
...plans,
[todayKey]: {
sniadanie: [{ id: newPlanEntryId(), recipeId: 'owsianka', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'salatka', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'makaron', servings: 1 }],
},
};
}
export function setupMealPlanner() {
let plans = loadPlans();
plans = seedDemoIfEmpty(plans);
savePlans(plans);
const state = {
mode: 'week',
weekStart: startOfWeekMonday(new Date()),
monthAnchor: startOfDay(new Date()),
selected: startOfDay(new Date()),
plans,
nutritionExpanded: false,
pickerSlot: null,
};
const weekGrid = document.getElementById('calendar-week-grid');
const monthGrid = document.getElementById('calendar-month-grid');
const pickerBackdrop = document.getElementById('planner-picker-backdrop');
const pickerSheet = document.getElementById('planner-picker-sheet');
const ingBackdrop = document.getElementById('planner-ing-backdrop');
const ingSheet = document.getElementById('planner-ing-sheet');
const rerender = () => {
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);
}
renderDayContent(state);
};
const persist = () => {
savePlans(state.plans);
rerender();
};
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);
if (!weekContains(state.weekStart, state.selected)) {
state.selected = new Date(state.weekStart);
}
} else {
state.monthAnchor = addMonths(state.monthAnchor, -1);
if (!sameMonth(state.monthAnchor, state.selected)) {
state.selected = startOfMonth(state.monthAnchor);
}
}
rerender();
});
document.getElementById('cal-next')?.addEventListener('click', () => {
if (state.mode === 'week') {
state.weekStart = addWeeks(state.weekStart, 1);
if (!weekContains(state.weekStart, state.selected)) {
state.selected = new Date(state.weekStart);
}
} else {
state.monthAnchor = addMonths(state.monthAnchor, 1);
if (!sameMonth(state.monthAnchor, state.selected)) {
state.selected = startOfMonth(state.monthAnchor);
}
}
rerender();
});
document.getElementById('cal-go-today')?.addEventListener('click', () => {
const today = startOfDay(new Date());
state.selected = today;
state.weekStart = startOfWeekMonday(today);
state.monthAnchor = startOfMonth(today);
rerender();
});
document.getElementById('planner-toggle-nutrition')?.addEventListener('click', () => {
state.nutritionExpanded = !state.nutritionExpanded;
const details = document.getElementById('planner-nutrition-details');
const chev = document.getElementById('planner-nutrition-chevron');
const btn = document.getElementById('planner-toggle-nutrition');
if (details) details.classList.toggle('hidden', !state.nutritionExpanded);
if (chev) chev.classList.toggle('rotate-180', state.nutritionExpanded);
if (btn) btn.setAttribute('aria-expanded', state.nutritionExpanded ? 'true' : 'false');
});
document.getElementById('planner-meal-slots')?.addEventListener('click', (e) => {
const addBtn = e.target.closest('.planner-add-meal');
if (addBtn) {
const slotId = addBtn.getAttribute('data-slot-id');
state.pickerSlot = slotId;
renderPickerList(slotId);
openSheet(pickerBackdrop, pickerSheet);
return;
}
const clearBtn = e.target.closest('.planner-clear-meal');
if (clearBtn) {
const slotId = clearBtn.getAttribute('data-slot-id');
const entryId = clearBtn.getAttribute('data-entry-id');
const key = dateKey(state.selected);
const arr = state.plans[key]?.[slotId];
if (!Array.isArray(arr) || !entryId) return;
const next = arr.filter((x) => x && x.id !== entryId);
if (!state.plans[key]) state.plans[key] = {};
if (next.length === 0) delete state.plans[key][slotId];
else state.plans[key][slotId] = next;
if (Object.keys(state.plans[key]).length === 0) delete state.plans[key];
persist();
return;
}
const minus = e.target.closest('.planner-serv-minus');
const plus = e.target.closest('.planner-serv-plus');
const slotId = (minus || plus)?.getAttribute('data-slot-id');
const entryId = (minus || plus)?.getAttribute('data-entry-id');
if (!slotId || !entryId) return;
const key = dateKey(state.selected);
const arr = state.plans[key]?.[slotId];
if (!Array.isArray(arr)) return;
const entry = arr.find((x) => x && x.id === entryId);
if (!entry) return;
let s = Math.max(1, Number(entry.servings) || 1);
if (minus) s = Math.max(1, s - 1);
if (plus) s = Math.min(12, s + 1);
entry.servings = s;
persist();
});
const closePicker = () => {
state.pickerSlot = null;
closeSheet(pickerBackdrop, pickerSheet);
};
bindPlannerSheetDragClose(pickerSheet, closePicker);
bindPlannerSheetDragClose(ingSheet, () => closeSheet(ingBackdrop, ingSheet));
pickerBackdrop?.addEventListener('click', closePicker);
document.getElementById('planner-picker-list')?.addEventListener('click', (e) => {
const pick = e.target.closest('.planner-pick-recipe');
if (!pick || !state.pickerSlot) return;
const recipeId = pick.getAttribute('data-recipe-id');
if (!recipeId || !PLANNER_RECIPES[recipeId]) return;
const key = dateKey(state.selected);
if (!state.plans[key]) state.plans[key] = {};
const slotId = state.pickerSlot;
if (!state.plans[key][slotId]) state.plans[key][slotId] = [];
state.plans[key][slotId].push({ id: newPlanEntryId(), recipeId, servings: 1 });
closePicker();
persist();
});
document.getElementById('planner-open-ingredients')?.addEventListener('click', () => {
if (sumDayNutrition(getDayPlan(state.plans, state.selected)).mealCount === 0) return;
renderIngredientsSheet(state);
openSheet(ingBackdrop, ingSheet);
});
ingBackdrop?.addEventListener('click', () => {
closeSheet(ingBackdrop, ingSheet);
});
ingSheet?.addEventListener('click', (e) => {
const row = e.target.closest('.planner-ing-row');
if (!row || !ingSheet.contains(row)) return;
row.classList.toggle('ingredient-active');
});
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;
showPlannerToast(`Dodano ${n} składników do listy (zakładka Zakupy w przygotowaniu).`);
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;
}
showPlannerToast(`Dodano ${n} zaznaczonych pozycji do listy (zakładka Zakupy w przygotowaniu).`);
closeSheet(ingBackdrop, ingSheet);
});
rerender();
}