From b44b985e76725e4ffc4c6a25671a63c68d4a446f Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Sat, 4 Apr 2026 22:26:19 +0200 Subject: [PATCH] Redesign recipe details --- .idea/workspace.xml | 37 +- VIEWS_AND_SCENARIOS.md | 3 - js/app.js | 2 +- js/ui/mealPlanEditor.js | 46 ++- js/views/RecipeDetail.js | 667 ------------------------------------- js/views/RecipeDetailV2.js | 266 +++++++-------- 6 files changed, 206 insertions(+), 815 deletions(-) delete mode 100644 js/views/RecipeDetail.js diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 3278371..bc7e56a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,17 +4,24 @@ + + + + \ No newline at end of file diff --git a/VIEWS_AND_SCENARIOS.md b/VIEWS_AND_SCENARIOS.md index 2c48bcd..43c0cc0 100644 --- a/VIEWS_AND_SCENARIOS.md +++ b/VIEWS_AND_SCENARIOS.md @@ -47,7 +47,6 @@ js/ RecipeList.js ← lista przepisów Filter.js ← overlay filtrów RecipeDetailV2.js ← detal przepisu (aktywna wersja) - RecipeDetail.js ← detal przepisu (oryginał, nieużywany — 3-zakładkowy) MealPlanner.js ← planer posiłków + kalendarz Pantry.js ← spiżarnia Shopping.js ← listy zakupów @@ -88,8 +87,6 @@ Slide-in overlay z detalami przepisu. Dwie zakładki: **Składniki** i **Kroki** 3. Wymienne składniki — **wstępnie ustawione z wyborów na liście składników**, z możliwością dalszej zmiany 4. Przycisk "Dodaj" → zapis do `planStore` (z opcjonalnym obiektem `substitutions`) -> **Uwaga:** Istnieje starsza wersja (`RecipeDetail.js`) z 3 zakładkami (Składniki, Kroki, Wartości) i read-only alternatywami. Aktualnie nieużywana — import w `app.js` wskazuje na `RecipeDetailV2.js`. - **Model danych — wymienne składniki:** - W `RECIPES`, składnik może mieć pole `alternatives: ['id1', 'id2', ...]` - Wybrane zamienniki zapisywane jako `substitutions: { originalId: chosenAltId }` w `planStore` diff --git a/js/app.js b/js/app.js index 4af1188..7f416d5 100644 --- a/js/app.js +++ b/js/app.js @@ -3,7 +3,7 @@ import { getFilterHTML, setupFilter } from './views/Filter.js?v=2'; import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2'; import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=4'; import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2'; -import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=5'; +import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=7'; function getAppToastHTML() { return ` diff --git a/js/ui/mealPlanEditor.js b/js/ui/mealPlanEditor.js index ea6bb06..6d24895 100644 --- a/js/ui/mealPlanEditor.js +++ b/js/ui/mealPlanEditor.js @@ -70,7 +70,7 @@ export function getMealPlanEditorHTML() {
-
+
@@ -103,6 +103,8 @@ export function setupMealPlanEditor() { recipeId: null, date: null, slotId: null, + originalDate: null, + originalSlotId: null, servings: 1, subs: {}, excluded: new Set(), @@ -436,6 +438,8 @@ export function setupMealPlanEditor() { S.mode = opts.mode || 'add'; S.recipeId = opts.recipeId; + S.originalDate = opts.date ? startOfDay(new Date(opts.date)) : null; + S.originalSlotId = opts.slotId || null; S.servings = opts.servings || opts.entry?.servings || 1; S.subs = { ...(opts.substitutions || opts.entry?.substitutions || {}) }; S.excluded = new Set(opts.entry?.excludedIngredients || []); @@ -447,9 +451,11 @@ export function setupMealPlanEditor() { S.addQuery = ''; if (opts.date && opts.slotId) { - S.date = opts.date; + S.date = startOfDay(new Date(opts.date)); + S.calDate = new Date(S.date); + S.calExpanded = false; S.slotId = opts.slotId; - S.showCal = false; + S.showCal = S.mode === 'edit'; } else { const today = startOfDay(new Date()); S.date = today; @@ -500,18 +506,38 @@ export function setupMealPlanEditor() { function handleConfirm() { if (!S.recipeId || !S.date || !S.slotId) return; const plans = loadPlans(); - const key = dateKey(S.date); + const targetKey = dateKey(S.date); + const nextEntry = buildEntry(); if (S.mode === 'edit' && S.entryId) { - const arr = plans[key]?.[S.slotId]; - if (Array.isArray(arr)) { + const sourceDate = S.originalDate || S.date; + const sourceSlotId = S.originalSlotId || S.slotId; + const sourceKey = dateKey(sourceDate); + const sameTarget = sourceKey === targetKey && sourceSlotId === S.slotId; + + if (sameTarget) { + if (!plans[targetKey]) plans[targetKey] = {}; + if (!plans[targetKey][S.slotId]) plans[targetKey][S.slotId] = []; + const arr = plans[targetKey][S.slotId]; const idx = arr.findIndex((e) => e.id === S.entryId); - if (idx >= 0) arr[idx] = buildEntry(); + if (idx >= 0) arr[idx] = nextEntry; + else arr.push(nextEntry); + } else { + const sourceArr = plans[sourceKey]?.[sourceSlotId]; + if (Array.isArray(sourceArr)) { + const idx = sourceArr.findIndex((e) => e.id === S.entryId); + if (idx >= 0) sourceArr.splice(idx, 1); + if (sourceArr.length === 0 && plans[sourceKey]) delete plans[sourceKey][sourceSlotId]; + if (plans[sourceKey] && Object.keys(plans[sourceKey]).length === 0) delete plans[sourceKey]; + } + if (!plans[targetKey]) plans[targetKey] = {}; + if (!plans[targetKey][S.slotId]) plans[targetKey][S.slotId] = []; + plans[targetKey][S.slotId].push(nextEntry); } } else { - if (!plans[key]) plans[key] = {}; - if (!plans[key][S.slotId]) plans[key][S.slotId] = []; - plans[key][S.slotId].push(buildEntry()); + if (!plans[targetKey]) plans[targetKey] = {}; + if (!plans[targetKey][S.slotId]) plans[targetKey][S.slotId] = []; + plans[targetKey][S.slotId].push(nextEntry); } savePlans(plans); diff --git a/js/views/RecipeDetail.js b/js/views/RecipeDetail.js deleted file mode 100644 index cdd6fcb..0000000 --- a/js/views/RecipeDetail.js +++ /dev/null @@ -1,667 +0,0 @@ -import { RECIPES, INGREDIENTS } from '../data/catalog.js?v=2'; -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); - renderNutrition(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 recipe = RECIPES[currentRecipeId]; - if (!recipe) return; - const kcal = Math.round(recipe.nutritionPerServing.kcal * currentServings); - document.getElementById('rd-kcal').textContent = `${kcal} kcal`; -} - -function nutritionForAmount(ingredientId, amount, unit) { - const def = INGREDIENTS[ingredientId]; - if (!def?.nutritionPer100g) return null; - let grams = amount; - if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) { - grams = amount * def.weightPerPiece; - } - const f = grams / 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 (read-only) ───────────────────────── */ - -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 displayAmount = Number.isInteger(scaledAmount) ? scaledAmount : parseFloat(scaledAmount.toFixed(1)); - - const hasAlts = ing.alternatives && ing.alternatives.length > 0; - const isExpanded = expandedAlternatives.has(ing.ingredientId); - - const toggleBtn = hasAlts - ? `` - : ''; - - let altListHtml = ''; - if (hasAlts) { - const altCards = ing.alternatives.map((altId) => { - const altDef = INGREDIENTS[altId]; - const altName = escapeHtml(altDef?.name || altId); - const nutrition = nutritionForAmount(altId, scaledAmount, ing.unit); - return `
-

${altName}

- ${renderNutritionLine(nutrition)} -
`; - }); - altListHtml = ` -
-

Można zamienić na:

- ${altCards.join('')} -
`; - } - - return ` -
  • -
    - ${escapeHtml(name)} - ${toggleBtn} - ${displayAmount} ${escapeHtml(ing.unit)} -
    ${altListHtml} -
  • `; - }).join(''); - - container.innerHTML = ` - `; - - 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('')} -
    `; -} - -/* ── nutrition tab ─────────────────────────────────────── */ - -function renderNutrition(recipe) { - const container = document.getElementById('rd-tab-nutrition'); - if (!container) return; - - const n = recipe.nutritionPerServing; - const s = currentServings; - - container.innerHTML = ` -
    -

    ${s > 1 ? `Wartości dla ${s} porcji` : 'Wartości na 1 porcję'}

    - -
    `; -} - -/* ── 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); - renderNutrition(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); - renderNutrition(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, ing.unit); - - 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'); - }; -} diff --git a/js/views/RecipeDetailV2.js b/js/views/RecipeDetailV2.js index 521f86a..12caea4 100644 --- a/js/views/RecipeDetailV2.js +++ b/js/views/RecipeDetailV2.js @@ -10,51 +10,79 @@ function escapeHtml(s) { } 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 ` -
    +
    - -
    -
    +
    - +
    +
    -
    +
    -

    +

    -
    -
    -
    +
    +
    + +
    -
    - -
    - 1 - -
    - +
    + +
    -
    - - +
    + +
    -
    +
    @@ -104,13 +132,12 @@ function populateDetail(recipeId) { const tagsHtml = []; for (const slotId of recipe.allowedSlots) { const label = slotLabelMap[slotId]; - if (label) tagsHtml.push(`${escapeHtml(label)}`); + if (label) tagsHtml.push(renderTagChip(label)); } for (const tag of (recipe.tags || [])) { - tagsHtml.push(`${escapeHtml(tag)}`); + tagsHtml.push(renderTagChip(tag)); } document.getElementById('rd-tags').innerHTML = tagsHtml.join(''); - document.getElementById('rd-servings').textContent = '1'; renderIngredients(recipe); renderSteps(recipe); @@ -118,11 +145,9 @@ function populateDetail(recipeId) { 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'); + setTabButtonState(b, false); }); - tabBtns[0]?.classList.remove('text-gray-500', 'border-transparent', 'font-medium'); - tabBtns[0]?.classList.add('text-gray-900', 'border-gray-900', 'font-semibold'); + 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'); @@ -155,6 +180,10 @@ function nutritionForAmount(ingredientId, amount, unit) { }; } +function fmtAmt(n) { + return Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(1))); +} + /* ── ingredients tab with inline nutrition + summary ───── */ function computeEffectiveNutritionTotals(recipe) { @@ -178,58 +207,42 @@ function computeEffectiveNutritionTotals(recipe) { } function renderNutritionSummary(recipe) { - const s = currentServings; const total = computeEffectiveNutritionTotals(recipe); return ` -
    - ${s > 1 ? `

    Wartości dla ${s} porcji

    ` : ''} -
    -
    - ${total.kcal} - Kalorie +
    +
    +
    +
    +

    Wartości odżywcze

    +
    +
    +
    +
    ${total.kcal}kcal
    +
    ${total.protein}gBiałko
    +
    ${total.carbs}gWęgle
    +
    ${total.fat}gTłuszcze
    +
    +
    +
    +
    -
    - ${total.protein} g - Białko +
    +
    +

    Porcje

    +
    +
    + + ${currentServings} + +
    +
    +
    -
    - ${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}
    `; } @@ -245,21 +258,24 @@ function renderIngredients(recipe) { const effectiveDef = INGREDIENTS[effectiveId]; const effectiveName = effectiveDef?.name || effectiveId; const scaledAmount = ing.amount * currentServings; - const nutrition = nutritionForAmount(effectiveId, scaledAmount, ing.unit); - const isSwapped = effectiveId !== origId; const isExpanded = expandedAlternatives.has(origId); + const rowStyle = 'background:#393937 !important; background-image:none !important; box-shadow:none !important; border:none !important;'; const toggleBtn = hasAlts - ? `` + ? `` : ''; - const cardBorder = isSwapped ? 'border-amber-200' : 'border-gray-200'; - const cardCls = isSwapped ? 'bg-amber-50/30' : 'bg-white'; - const cardHtml = renderIngredientCard(effectiveName, scaledAmount, ing.unit, nutrition, { - suffix: toggleBtn, - border: cardBorder, - cls: cardCls, - }); + let rowHtml = `
    `; + rowHtml += '
    '; + rowHtml += `
    ${escapeHtml(effectiveName)}
    `; + rowHtml += '
    '; + rowHtml += toggleBtn; + rowHtml += `
    + ${fmtAmt(scaledAmount)} + ${escapeHtml(ing.unit)} +
    `; + rowHtml += '
    '; + rowHtml += '
    '; let altListHtml = ''; if (hasAlts && isExpanded) { @@ -268,32 +284,46 @@ function renderIngredients(recipe) { const def = INGREDIENTS[altId]; const altName = def?.name || altId; const isSelected = effectiveId === altId; - const isOriginal = altId === origId; 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
    ` + : ''; - const radioDot = `
    ${isSelected ? '
    ' : ''}
    `; - const defaultTag = isOriginal ? `Domyślny` : ''; - - return renderIngredientCard(altName, scaledAmount, ing.unit, altNutrition, { - cls: isSelected ? 'bg-gray-50' : 'bg-white hover:bg-gray-50 cursor-pointer', - border: isSelected ? 'border-gray-900 ring-1 ring-gray-900' : 'border-gray-100', - prefix: radioDot, - badge: defaultTag, - dataAttrs: `data-original-id="${escapeHtml(origId)}" data-alt-id="${escapeHtml(altId)}"`, - }); + return ``; }); altListHtml = ` -
    +
    ${optionCards.join('')}
    `; } - return `
  • ${cardHtml}${altListHtml}
  • `; + rowHtml += altListHtml; + rowHtml += '
    '; + return `
  • ${rowHtml}
  • `; }).join(''); container.innerHTML = ` ${renderNutritionSummary(recipe)} -
      ${rows}
    `; +
      ${rows}
    `; + + container.querySelector('#rd-serv-minus')?.addEventListener('click', () => { + if (currentServings <= 1) return; + currentServings--; + renderIngredients(recipe); + updateKcalDisplay(); + }); + + container.querySelector('#rd-serv-plus')?.addEventListener('click', () => { + if (currentServings >= 12) return; + currentServings++; + renderIngredients(recipe); + updateKcalDisplay(); + }); container.querySelectorAll('.rd-alt-toggle').forEach((btn) => { btn.addEventListener('click', () => { @@ -332,16 +362,16 @@ function renderSteps(recipe) { const steps = recipe.steps || []; if (steps.length === 0) { - container.innerHTML = '

    Brak kroków przygotowania.

    '; + container.innerHTML = `

    Brak kroków przygotowania.

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

    ${escapeHtml(step)}

    +
    +
    ${i + 1}
    +

    ${escapeHtml(step)}

    `).join('')}
    `; } @@ -364,36 +394,12 @@ export function setupRecipeDetail() { } 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'); + setTabButtonState(b, false); }); - btn.classList.remove('text-gray-500', 'border-transparent', 'font-medium'); - btn.classList.add('text-gray-900', 'border-gray-900', 'font-semibold'); + setTabButtonState(btn, true); }); }); - 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 — delegate to MealPlanEditor ─────── */ document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {