import { RECIPES, INGREDIENTS, PRODUCTS } from '../data/catalog.js?v=8'; import { MEAL_SLOTS } from '../planner/mealSlots.js'; import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107'; 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 RD_THEME = Object.freeze({ surface: '#393937', surfaceSoft: '#2f2f2d', surfaceActive: '#23221e', border: '#444442', borderSoft: '#56534f', borderStrong: '#787876', textPrimary: '#ddd6ca', textSecondary: '#d7d2c8', textMuted: '#9b978f', }); function forceBg(bg) { return `background:${bg} !important; background-image:none !important; box-shadow:none !important;`; } function forceBgBorder(bg, border) { return `${forceBg(bg)} border:1px solid ${border} !important;`; } function renderTagChip(label) { return `${escapeHtml(label)}`; } function setTabButtonState(btn, active) { if (!btn) return; btn.style.color = active ? RD_THEME.textPrimary : RD_THEME.textMuted; btn.style.borderBottomColor = active ? RD_THEME.borderStrong : 'transparent'; btn.style.fontWeight = active ? '600' : '500'; } export function getRecipeDetailHTML() { return `

${getIngredientCardHTML({ idBase: 'rd-ing-card' })} `; } /* ── state ─────────────────────────────────────────────── */ let currentRecipeId = null; let currentMode = 'catalog'; let currentServings = 1; let currentSubstitutions = {}; let currentExcludedIngredients = new Set(); let currentAmountOverrides = {}; let currentAddedIngredients = []; let currentProductSelections = {}; let expandedAlternatives = new Set(); let ingredientCard = null; function isPlannedMode() { return currentMode === 'planned'; } function getEffectiveIngredientId(originalId) { return currentSubstitutions[originalId] || originalId; } function clampServings(value) { return Math.max(1, Math.min(12, Number(value) || 1)); } function cloneAddedIngredients(items) { if (!Array.isArray(items)) return []; return items .filter((item) => item && typeof item.ingredientId === 'string' && typeof item.amount === 'number' && typeof item.unit === 'string') .map((item) => ({ ingredientId: item.ingredientId, amount: item.amount, unit: item.unit })); } function getSelectedProduct(productId) { return productId && PRODUCTS[productId] ? PRODUCTS[productId] : null; } function getProductSelectionForIngredient(...ingredientIds) { for (const ingredientId of ingredientIds) { if (!ingredientId) continue; const productId = currentProductSelections[ingredientId]; if (getSelectedProduct(productId)) return productId; } return null; } function buildVisibleIngredients(recipe) { if (!recipe) return []; const rows = []; for (const ing of recipe.ingredients) { const originalId = ing.ingredientId; if (currentExcludedIngredients.has(originalId)) continue; const effectiveId = getEffectiveIngredientId(originalId); const effectiveDef = INGREDIENTS[effectiveId]; const productId = getProductSelectionForIngredient(effectiveId, originalId); rows.push({ key: `recipe:${originalId}`, originalId, ingredientId: effectiveId, name: effectiveDef?.name || effectiveId, amount: (currentAmountOverrides[originalId] ?? ing.amount) * currentServings, unit: ing.unit, productId, productName: getSelectedProduct(productId)?.name || '', added: false, alternatives: Array.isArray(ing.alternatives) ? ing.alternatives : [], }); } currentAddedIngredients.forEach((item, index) => { const def = INGREDIENTS[item.ingredientId]; const productId = getProductSelectionForIngredient(item.ingredientId); rows.push({ key: `added:${index}:${item.ingredientId}`, originalId: item.ingredientId, ingredientId: item.ingredientId, name: def?.name || item.ingredientId, amount: item.amount * currentServings, unit: item.unit, productId, productName: getSelectedProduct(productId)?.name || '', added: true, alternatives: [], }); }); return rows; } /* ── populate ──────────────────────────────────────────── */ function populateDetail(recipeId, options = {}) { const recipe = RECIPES[recipeId]; if (!recipe) return; currentRecipeId = recipeId; currentMode = options.plannedEntry ? 'planned' : 'catalog'; currentServings = clampServings(options.servings ?? options.plannedEntry?.servings ?? 1); currentSubstitutions = { ...(options.substitutions || options.plannedEntry?.substitutions || {}) }; currentExcludedIngredients = new Set(options.excludedIngredients || options.plannedEntry?.excludedIngredients || []); currentAmountOverrides = { ...(options.amountOverrides || options.plannedEntry?.amountOverrides || {}) }; currentAddedIngredients = cloneAddedIngredients(options.addedIngredients || options.plannedEntry?.addedIngredients); currentProductSelections = { ...(options.productSelections || options.plannedEntry?.productSelections || {}) }; expandedAlternatives.clear(); ingredientCard?.close(); const heroImg = document.getElementById('rd-hero-img'); const heroLabel = document.getElementById('rd-hero-label'); if (recipe.image) { heroImg.src = recipe.image; heroImg.alt = recipe.title; heroImg.classList.remove('hidden'); heroLabel.textContent = ''; } else { heroImg.classList.add('hidden'); heroImg.src = ''; heroLabel.textContent = `Zdjęcie: ${recipe.title}`; } document.getElementById('rd-title').textContent = recipe.title; document.getElementById('rd-time').textContent = `${recipe.minutes} min`; document.getElementById('rd-add-to-planner-btn')?.classList.toggle('hidden', isPlannedMode()); const tagsHtml = []; for (const slotId of recipe.allowedSlots) { const label = slotLabelMap[slotId]; if (label) tagsHtml.push(renderTagChip(label)); } for (const tag of (recipe.tags || [])) { tagsHtml.push(renderTagChip(tag)); } document.getElementById('rd-tags').innerHTML = tagsHtml.join(''); renderIngredients(recipe); renderSteps(recipe); const tabBtns = document.querySelectorAll('.rd-tab-btn'); const tabs = document.querySelectorAll('.rd-tab-content'); tabBtns.forEach((b) => { setTabButtonState(b, false); }); setTabButtonState(tabBtns[0], true); 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 nutritionForAmount(ingredientId, amount, unit, productIdOverride = null) { const def = INGREDIENTS[ingredientId]; const productId = productIdOverride || getProductSelectionForIngredient(ingredientId); const product = getSelectedProduct(productId); const nutrition = product?.nutritionPer100g || def?.nutritionPer100g; if (!def || !nutrition) return null; let grams = amount; if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) { grams = amount * def.weightPerPiece; } const f = grams / 100; return { kcal: Math.round(nutrition.kcal * f), protein: Math.round(nutrition.protein * f * 10) / 10, fat: Math.round(nutrition.fat * f * 10) / 10, carbs: Math.round(nutrition.carbs * f * 10) / 10, }; } function fmtAmt(n) { return Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(1))); } /* ── ingredients tab with inline nutrition + summary ───── */ function computeEffectiveNutritionTotals(recipe) { let kcal = 0, protein = 0, fat = 0, carbs = 0; for (const ing of buildVisibleIngredients(recipe)) { const n = nutritionForAmount(ing.ingredientId, ing.amount, ing.unit, ing.productId); 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 total = computeEffectiveNutritionTotals(recipe); const servingsHtml = isPlannedMode() ? `

Porcje

${currentServings}

` : `

Porcje

${currentServings}
`; return `

Wartości odżywcze

${total.kcal}

kcal

${total.protein}g

białko

${total.fat}g

tłuszcz

${total.carbs}g

węglowodany

${servingsHtml}
`; } function renderIngredients(recipe) { const container = document.getElementById('rd-tab-ingredients'); if (!container) return; if (isPlannedMode()) { const items = buildVisibleIngredients(recipe); const rows = items.map((item) => { const rowClass = 'rd-ing-row rounded-xl px-3 py-3 w-full text-left cursor-pointer transition-colors active:scale-[0.99]'; const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;'; const productBadge = item.productName ? `
${escapeHtml(item.productName)}
` : ''; const addedMark = item.added ? '' : ''; return `
  • `; }).join(''); container.innerHTML = ` ${renderNutritionSummary(recipe)} ${rows ? `` : `

    Brak składników w tej wersji przepisu.

    `}`; container.querySelectorAll('[data-rd-open-ingredient]').forEach((btn) => { btn.addEventListener('click', () => { const ingredientId = btn.dataset.rdIngredientId; if (!ingredientId || !ingredientCard) return; const productId = btn.dataset.rdProductId || null; ingredientCard.open({ ingredientId, productId, selectedProductId: productId, allowProductSelection: false, sourceNote: 'Z planera', }); }); }); return; } const rows = recipe.ingredients.map((ing) => { const origId = ing.ingredientId; const hasAlts = ing.alternatives && ing.alternatives.length > 0; const effectiveId = hasAlts ? getEffectiveIngredientId(origId) : origId; const effectiveDef = INGREDIENTS[effectiveId]; const effectiveName = effectiveDef?.name || effectiveId; const scaledAmount = ing.amount * currentServings; const isExpanded = expandedAlternatives.has(origId); const rowClass = 'rd-ing-row rounded-xl px-3 py-3'; const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.25) !important; border:none !important;'; const toggleBtn = hasAlts ? `` : ''; let rowHtml = `
    `; rowHtml += '
    '; rowHtml += `
    ${escapeHtml(effectiveName)}
    `; rowHtml += '
    '; rowHtml += toggleBtn; rowHtml += `
    ${fmtAmt(scaledAmount)} ${escapeHtml(ing.unit)}
    `; rowHtml += '
    '; rowHtml += '
    '; let altListHtml = ''; if (hasAlts && isExpanded) { const allOptions = [origId, ...ing.alternatives]; const optionCards = allOptions.map((altId) => { const def = INGREDIENTS[altId]; const altName = def?.name || altId; const isSelected = effectiveId === altId; const altNutrition = nutritionForAmount(altId, scaledAmount, ing.unit); const checkbox = ` ${isSelected ? '' : ''} `; const nutritionLine = altNutrition ? `
    ${altNutrition.kcal} kcal · ${altNutrition.protein}g B · ${altNutrition.fat}g T · ${altNutrition.carbs}g W
    ` : ''; return ``; }); altListHtml = `
    ${optionCards.join('')}
    `; } rowHtml += altListHtml; rowHtml += '
    '; return `
  • ${rowHtml}
  • `; }).join(''); container.innerHTML = ` ${renderNutritionSummary(recipe)} `; container.querySelector('#rd-serv-minus')?.addEventListener('click', () => { if (currentServings <= 1) return; currentServings--; renderIngredients(recipe); }); container.querySelector('#rd-serv-plus')?.addEventListener('click', () => { if (currentServings >= 12) return; currentServings++; renderIngredients(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); } renderIngredients(recipe); }); }); container.querySelectorAll('.rd-alt-options').forEach((group) => { group.querySelectorAll('[data-alt-id]').forEach((card) => { card.addEventListener('click', () => { const originalId = card.dataset.originalId; const altId = card.dataset.altId; if (altId === originalId) { delete currentSubstitutions[originalId]; } else { currentSubstitutions[originalId] = altId; } expandedAlternatives.delete(originalId); renderIngredients(recipe); }); }); }); } /* ── 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() { ingredientCard = createIngredientCardController({ idBase: 'rd-ing-card', defaultSourceNote: 'Z planera' }); ingredientCard.bind(); 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) => { setTabButtonState(b, false); }); setTabButtonState(btn, true); }); }); /* ── tab-bar scroll shadow ─────────────────── */ const scrollContainer = document.querySelector('#recipe-detail-view .overflow-y-auto'); const tabBar = document.getElementById('rd-tab-bar'); if (scrollContainer && tabBar) { scrollContainer.addEventListener('scroll', () => { tabBar.classList.toggle('rd-scrolled', scrollContainer.scrollTop > 2); }); } /* ── planner — delegate to MealPlanEditor ─────── */ document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => { if (!currentRecipeId || isPlannedMode()) return; window.openMealPlanEditor?.({ mode: 'add', recipeId: currentRecipeId, servings: currentServings, substitutions: { ...currentSubstitutions }, }); }); window.openRecipeDetail = (recipeId, options = {}) => { if (!recipeId || !RECIPES[recipeId]) return; populateDetail(recipeId, options); 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 = () => { ingredientCard?.close(); window.closeMealPlanEditor?.(); 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'); }; }