import { RECIPES, INGREDIENTS } from '../data/catalog.js'; import { MEAL_SLOTS } from '../planner/mealSlots.js'; import { addDays, addMonths, sameDay, sameMonth, startOfDay, startOfMonth, startOfWeekMonday } from '../services/dateUtils.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, '>') .replace(/"/g, '"'); } const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label])); const MONTHS_LONG = ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień']; const WEEKDAYS_SHORT = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd']; export function getRecipeDetailHTML() { return `

1
`; } /* ── state ─────────────────────────────────────────────── */ let currentRecipeId = null; let currentServings = 1; let currentSubstitutions = {}; let expandedAlternatives = new Set(); function getEffectiveIngredientId(originalId) { return currentSubstitutions[originalId] || originalId; } /* ── populate ──────────────────────────────────────────── */ function populateDetail(recipeId) { const recipe = RECIPES[recipeId]; if (!recipe) return; currentRecipeId = recipeId; currentServings = 1; currentSubstitutions = {}; expandedAlternatives.clear(); 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(); const tagsHtml = []; for (const slotId of recipe.allowedSlots) { const label = slotLabelMap[slotId]; if (label) tagsHtml.push(`${escapeHtml(label)}`); } for (const tag of (recipe.tags || [])) { tagsHtml.push(`${escapeHtml(tag)}`); } document.getElementById('rd-tags').innerHTML = tagsHtml.join(''); document.getElementById('rd-servings').textContent = '1'; renderIngredients(recipe); renderSteps(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'); } /* ── helpers ───────────────────────────────────────────── */ function updateKcalDisplay() { const el = document.getElementById('rd-kcal'); if (!el) return; const recipe = RECIPES[currentRecipeId]; if (!recipe) return; const kcal = Math.round(recipe.nutritionPerServing.kcal * currentServings); el.textContent = `${kcal} kcal`; } function nutritionForAmount(ingredientId, amount) { const def = INGREDIENTS[ingredientId]; if (!def?.nutritionPer100g) return null; const f = amount / 100; return { kcal: Math.round(def.nutritionPer100g.kcal * f), protein: Math.round(def.nutritionPer100g.protein * f * 10) / 10, fat: Math.round(def.nutritionPer100g.fat * f * 10) / 10, carbs: Math.round(def.nutritionPer100g.carbs * f * 10) / 10, }; } function renderNutritionLine(nutrition) { if (!nutrition) return ''; return `
${nutrition.kcal} kcal${nutrition.protein}g białko${nutrition.fat}g tłuszcz${nutrition.carbs}g węgle
`; } /* ── ingredients tab with inline nutrition + summary ───── */ function computeIngredientNutritionTotals(recipe) { let kcal = 0, protein = 0, fat = 0, carbs = 0; for (const ing of recipe.ingredients) { const n = nutritionForAmount(ing.ingredientId, ing.amount * currentServings); if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; } } return { kcal: Math.round(kcal), protein: Math.round(protein * 10) / 10, fat: Math.round(fat * 10) / 10, carbs: Math.round(carbs * 10) / 10, }; } function renderNutritionSummary(recipe) { const n = recipe.nutritionPerServing; const s = currentServings; const total = { kcal: Math.round(n.kcal * s * 10) / 10, protein: Math.round(n.protein * s * 10) / 10, fat: Math.round(n.fat * s * 10) / 10, carbs: Math.round(n.carbs * s * 10) / 10, }; return `
${s > 1 ? `

Wartości dla ${s} porcji

` : ''}
${total.kcal} Kalorie
${total.protein} g Białko
${total.carbs} g Węglo.
${total.fat} g Tłuszcze
`; } function renderIngredientCard(name, amount, unit, nutrition, extra) { const displayAmount = Number.isInteger(amount) ? amount : parseFloat(amount.toFixed(1)); const macroHtml = nutrition ? `
${nutrition.protein}g B${nutrition.fat}g T${nutrition.carbs}g W
` : ''; const kcalHtml = nutrition ? `${nutrition.kcal} kcal` : ''; return `
${extra?.prefix || ''} ${escapeHtml(name)} ${extra?.suffix || ''}
${macroHtml} ${extra?.badge || ''}
${displayAmount} ${escapeHtml(unit)} ${kcalHtml}
`; } function renderIngredients(recipe) { const container = document.getElementById('rd-tab-ingredients'); if (!container) return; const rows = recipe.ingredients.map((ing) => { const def = INGREDIENTS[ing.ingredientId]; const name = def?.name || ing.ingredientId; const scaledAmount = ing.amount * currentServings; const nutrition = nutritionForAmount(ing.ingredientId, scaledAmount); const hasAlts = ing.alternatives && ing.alternatives.length > 0; const isExpanded = expandedAlternatives.has(ing.ingredientId); const toggleBtn = hasAlts ? `` : ''; const cardHtml = renderIngredientCard(name, scaledAmount, ing.unit, nutrition, { suffix: toggleBtn }); let altListHtml = ''; if (hasAlts) { const altCards = ing.alternatives.map((altId) => { const altDef = INGREDIENTS[altId]; const altName = altDef?.name || altId; const altNutrition = nutritionForAmount(altId, scaledAmount); return renderIngredientCard(altName, scaledAmount, ing.unit, altNutrition, { cls: 'bg-gray-50', border: 'border-gray-100', }); }); altListHtml = `

Można zamienić na:

${altCards.join('')}
`; } return `
  • ${cardHtml}${altListHtml}
  • `; }).join(''); container.innerHTML = ` ${renderNutritionSummary(recipe)} `; container.querySelectorAll('.rd-alt-toggle').forEach((btn) => { btn.addEventListener('click', () => { const origId = btn.dataset.originalId; if (expandedAlternatives.has(origId)) { expandedAlternatives.delete(origId); } else { expandedAlternatives.add(origId); } const list = container.querySelector(`.rd-alt-list[data-original-id="${origId}"]`); if (list) list.classList.toggle('hidden'); btn.classList.toggle('bg-gray-100'); btn.classList.toggle('text-gray-400'); btn.classList.toggle('bg-amber-50'); btn.classList.toggle('text-amber-500'); }); }); } /* ── steps tab ─────────────────────────────────────────── */ function renderSteps(recipe) { const container = document.getElementById('rd-tab-steps'); if (!container) return; const steps = recipe.steps || []; if (steps.length === 0) { container.innerHTML = '

    Brak kroków przygotowania.

    '; return; } container.innerHTML = `
    ${steps.map((step, i) => `
    ${i + 1}

    ${escapeHtml(step)}

    `).join('')}
    `; } /* ── setup ─────────────────────────────────────────────── */ 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); 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); updateKcalDisplay(); } }); /* ── planner picker ──────────────────────────────── */ let plannerPickerDay = null; let plannerPickerSlot = null; let calViewDate = null; let calExpanded = false; const plannerOverlay = document.getElementById('rd-planner-picker'); const plannerSheet = document.getElementById('rd-planner-sheet'); function renderCalendarCell(d, today, activeMonth) { const isToday = sameDay(d, today); const isSelected = plannerPickerDay && sameDay(d, plannerPickerDay); const isOtherMonth = d.getMonth() !== activeMonth; const isPast = d.getTime() < today.getTime(); let cls = 'flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors w-full min-h-10 py-1 gap-0.5 leading-tight '; if (isSelected) { cls += 'bg-gray-900 text-white '; } else if (isPast) { cls += 'text-gray-300 '; } else if (isOtherMonth) { cls += 'text-gray-300 '; } else { cls += 'text-gray-800 hover:bg-gray-100 '; } if (isToday && !isSelected && !isPast) { cls += 'ring-1 ring-inset ring-gray-900 '; } const inner = `${d.getDate()}`; if (isPast && !isSelected) { return `
    ${inner}
    `; } return ``; } function renderPlannerCalendar() { const grid = document.getElementById('rd-cal-grid'); const titleEl = document.getElementById('rd-cal-title'); const todayBtn = document.getElementById('rd-cal-today'); const toggleIcon = document.getElementById('rd-cal-toggle-icon'); if (!grid || !titleEl) return; const today = startOfDay(new Date()); if (calExpanded) { const ms = startOfMonth(calViewDate); titleEl.textContent = `${MONTHS_LONG[ms.getMonth()]} ${ms.getFullYear()}`; toggleIcon.className = 'fas fa-chevron-up text-[10px]'; const firstCell = startOfWeekMonday(ms); const cells = []; let d = new Date(firstCell); for (let i = 0; i < 42; i++) { cells.push(new Date(d)); d = addDays(d, 1); } while (cells.length > 7) { const last7 = cells.slice(-7); if (last7.every((c) => c.getMonth() !== ms.getMonth())) cells.splice(-7); else break; } grid.innerHTML = cells.map((c) => renderCalendarCell(c, today, ms.getMonth())).join(''); todayBtn.classList.toggle('hidden', sameMonth(today, calViewDate)); } else { const ws = startOfWeekMonday(calViewDate); titleEl.textContent = `${MONTHS_LONG[calViewDate.getMonth()]} ${calViewDate.getFullYear()}`; toggleIcon.className = 'fas fa-chevron-down text-[10px]'; const cells = []; for (let i = 0; i < 7; i++) cells.push(addDays(ws, i)); grid.innerHTML = cells.map((c) => renderCalendarCell(c, today, calViewDate.getMonth())).join(''); const todayWs = startOfWeekMonday(today); todayBtn.classList.toggle('hidden', sameDay(ws, todayWs)); } grid.querySelectorAll('.rd-cal-day').forEach((btn) => { btn.addEventListener('click', () => { plannerPickerDay = new Date(Number(btn.dataset.ts)); calViewDate = new Date(plannerPickerDay); renderPlannerCalendar(); }); }); } document.getElementById('rd-cal-prev')?.addEventListener('click', () => { calViewDate = calExpanded ? addMonths(calViewDate, -1) : addDays(calViewDate, -7); renderPlannerCalendar(); }); document.getElementById('rd-cal-next')?.addEventListener('click', () => { calViewDate = calExpanded ? addMonths(calViewDate, 1) : addDays(calViewDate, 7); renderPlannerCalendar(); }); document.getElementById('rd-cal-today')?.addEventListener('click', () => { const today = startOfDay(new Date()); plannerPickerDay = today; calViewDate = today; renderPlannerCalendar(); }); document.getElementById('rd-cal-toggle')?.addEventListener('click', () => { calExpanded = !calExpanded; renderPlannerCalendar(); }); /* ── planner variant selection ────────────────────── */ const expandedVariantGroups = new Set(); function renderPlannerVariants(recipe) { const section = document.getElementById('rd-planner-variant'); const container = document.getElementById('rd-planner-variant-options'); if (!section || !container) return; const altsIngredients = recipe.ingredients.filter((i) => i.alternatives?.length > 0); if (altsIngredients.length === 0) { section.classList.add('hidden'); return; } section.classList.remove('hidden'); let html = ''; for (const ing of altsIngredients) { const origId = ing.ingredientId; const scaledAmount = ing.amount * currentServings; const displayAmount = Number.isInteger(scaledAmount) ? scaledAmount : parseFloat(scaledAmount.toFixed(1)); const effectiveId = getEffectiveIngredientId(origId); const effectiveDef = INGREDIENTS[effectiveId]; const effectiveName = effectiveDef?.name || effectiveId; const isExpanded = expandedVariantGroups.has(origId); const isSwapped = effectiveId !== origId; html += `
    `; if (isExpanded) { const allOptions = [origId, ...ing.alternatives]; html += '
    '; for (const altId of allOptions) { const def = INGREDIENTS[altId]; const altName = def?.name || altId; const isSelected = effectiveId === altId; const isOriginal = altId === origId; const nutrition = nutritionForAmount(altId, scaledAmount); const selectedCls = isSelected ? 'border-gray-900 bg-gray-50 ring-1 ring-gray-900' : 'border-gray-200 bg-white hover:border-gray-300'; let tag = ''; if (isOriginal) tag = `Domyślny`; html += ` `; } html += '
    '; } html += '
    '; } container.innerHTML = html; } document.getElementById('rd-planner-variant-options')?.addEventListener('click', (e) => { const toggle = e.target.closest('.rd-variant-toggle'); if (toggle) { const origId = toggle.dataset.originalId; if (expandedVariantGroups.has(origId)) expandedVariantGroups.delete(origId); else expandedVariantGroups.add(origId); const recipe = RECIPES[currentRecipeId]; if (recipe) renderPlannerVariants(recipe); return; } const btn = e.target.closest('.rd-variant-option'); if (!btn) return; const originalId = btn.dataset.originalId; const altId = btn.dataset.altId; if (altId === originalId) { delete currentSubstitutions[originalId]; } else { currentSubstitutions[originalId] = altId; } expandedVariantGroups.delete(originalId); const recipe = RECIPES[currentRecipeId]; if (recipe) renderPlannerVariants(recipe); }); function openPlannerPicker() { const recipe = RECIPES[currentRecipeId]; if (!recipe) return; document.getElementById('rd-planner-recipe-name').textContent = recipe.title; currentSubstitutions = {}; expandedVariantGroups.clear(); const today = startOfDay(new Date()); plannerPickerDay = today; calViewDate = today; calExpanded = false; renderPlannerCalendar(); renderPlannerVariants(recipe); plannerPickerSlot = recipe.allowedSlots[0] || MEAL_SLOTS[0]?.id; 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 ``; }).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-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] = []; const entry = { id: newPlanEntryId(), recipeId: currentRecipeId, servings: currentServings, }; if (Object.keys(currentSubstitutions).length > 0) { entry.substitutions = { ...currentSubstitutions }; } plans[key][plannerPickerSlot].push(entry); 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.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'); }; }