import { RECIPES, INGREDIENTS, PRODUCTS } from '../data/catalog.js?v=9'; import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260417-115'; function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } 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;`; } export function getRecipeDetailHTML() { return `

Składniki

Kroki

${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; let resetScrollState = 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-add-to-planner-btn')?.classList.toggle('hidden', isPlannedMode()); renderIngredients(recipe); renderSteps(recipe); resetScrollState?.(); } /* ── 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(); /* ── collapsing hero on scroll ─────────────── */ const HERO_MAX = 236; const scrollContainer = document.getElementById('rd-scroll-container'); const hero = document.getElementById('rd-hero'); const heroImg = document.getElementById('rd-hero-img'); const topActionButtons = [ document.getElementById('rd-back-btn'), document.getElementById('rd-add-to-planner-btn'), ].filter(Boolean); function syncTopActionButtons(progress) { const shadowY = 3 + (progress * 3); const shadowBlur = 8 + (progress * 6); const shadowAlpha = 0.28 + (progress * 0.16); const backgroundAlpha = 0.93 + (progress * 0.05); topActionButtons.forEach((button) => { const isRoundButton = button.id === 'rd-back-btn'; const effectiveShadowY = isRoundButton ? shadowY + 1 : shadowY; const effectiveShadowBlur = isRoundButton ? shadowBlur + 1 : shadowBlur; const effectiveShadowAlpha = isRoundButton ? shadowAlpha + 0.05 : shadowAlpha; button.style.boxShadow = `0 ${effectiveShadowY}px ${effectiveShadowBlur}px rgba(0,0,0,${effectiveShadowAlpha})`; button.style.background = `rgba(57,57,55,${backgroundAlpha})`; }); } if (scrollContainer && hero) { let ticking = false; let lastTouchY = null; scrollContainer.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const scrollY = Math.max(0, scrollContainer.scrollTop); const collapse = Math.min(scrollY, HERO_MAX); const progress = collapse / HERO_MAX; hero.style.height = `${HERO_MAX - collapse}px`; hero.style.opacity = String(Math.max(0, 1 - collapse / HERO_MAX)); syncTopActionButtons(progress); if (heroImg) { heroImg.style.transform = `translateY(${collapse * 0.4}px) scale(${1 + collapse * 0.001})`; } ticking = false; }); }); scrollContainer.addEventListener('touchstart', (e) => { lastTouchY = e.touches[0]?.clientY ?? null; }, { passive: true }); scrollContainer.addEventListener('touchmove', (e) => { const touchY = e.touches[0]?.clientY; if (touchY == null) return; if (scrollContainer.scrollTop <= 0 && lastTouchY != null && touchY > lastTouchY) { e.preventDefault(); } lastTouchY = touchY; }, { passive: false }); scrollContainer.addEventListener('touchend', () => { lastTouchY = null; }, { passive: true }); scrollContainer.addEventListener('touchcancel', () => { lastTouchY = null; }, { passive: true }); } resetScrollState = () => { if (scrollContainer) scrollContainer.scrollTop = 0; if (hero) { hero.style.height = `${HERO_MAX}px`; hero.style.opacity = '1'; } syncTopActionButtons(0); if (heroImg) heroImg.style.transform = ''; }; /* ── 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'); }; }