Adjust recipe detail view
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m18s

This commit is contained in:
2026-04-14 22:41:41 +02:00
parent 71b91b50b4
commit d642fbd687

View File

@@ -1,5 +1,4 @@
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) {
@@ -10,7 +9,6 @@ function escapeHtml(s) {
.replace(/"/g, '"');
}
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
const RD_THEME = Object.freeze({
surface: '#393937',
surfaceSoft: '#2f2f2d',
@@ -31,72 +29,42 @@ function forceBgBorder(bg, border) {
return `${forceBg(bg)} border:1px solid ${border} !important;`;
}
function renderTagChip(label) {
return `<span class="px-3 py-1 rounded-full text-[11px] font-semibold inline-flex items-center" style="${forceBgBorder(RD_THEME.surfaceSoft, RD_THEME.border)} color:${RD_THEME.textSecondary} !important;">${escapeHtml(label)}</span>`;
}
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 `
<style>
#rd-tab-bar::after {
content: '';
position: absolute;
left: 0; right: 0; bottom: -8px;
height: 8px;
background: linear-gradient(to bottom, rgba(0,0,0,0.25), transparent);
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
#rd-tab-bar.rd-scrolled::after {
opacity: 1;
}
</style>
<div id="recipe-detail-view" class="absolute inset-0 bg-[#2d2e2b] z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none flex flex-col overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
<div id="recipe-detail-view" class="absolute inset-0 bg-[#2d2e2b] z-30 transition-all duration-300 ease-in-out translate-x-full opacity-0 pointer-events-none overflow-hidden" style="background:#2d2e2b !important; background-image:none !important;">
<div class="absolute top-0 w-full p-3.5 flex justify-between z-40 mt-3">
<button onclick="closeRecipeDetail()" class="w-9 h-9 rounded-full border flex items-center justify-center transition-opacity opacity-95 hover:opacity-100" style="background:rgba(47,47,45,0.92) !important; backdrop-filter:none !important; border-color:#444442 !important; color:#ddd6ca !important;">
<button id="rd-back-btn" onclick="closeRecipeDetail()" class="w-9 h-9 rounded-full flex items-center justify-center transition-opacity opacity-95 hover:opacity-100" style="background:rgba(57,57,55,0.93) !important; backdrop-filter:none !important; box-shadow:0 4px 9px rgba(0,0,0,0.33) !important; color:#ddd6ca !important; transition:box-shadow 180ms ease, background-color 180ms ease, opacity 180ms ease;">
<i class="fas fa-arrow-left text-[13px]"></i>
</button>
<button id="rd-add-to-planner-btn" class="h-9 px-3 rounded-full border flex items-center justify-center gap-1.5 transition-opacity opacity-95 hover:opacity-100 text-[12px] font-semibold" style="background:rgba(47,47,45,0.92) !important; backdrop-filter:none !important; border-color:#444442 !important; color:#ddd6ca !important;">
<button id="rd-add-to-planner-btn" class="h-9 px-3 rounded-full flex items-center justify-center gap-1.5 transition-opacity opacity-95 hover:opacity-100 text-[12px] font-semibold" style="background:rgba(57,57,55,0.93) !important; backdrop-filter:none !important; box-shadow:0 3px 8px rgba(0,0,0,0.28) !important; color:#ddd6ca !important; transition:box-shadow 180ms ease, background-color 180ms ease, opacity 180ms ease;">
<i class="fas fa-calendar-plus text-[11px]"></i> Zaplanuj
</button>
</div>
<div id="rd-hero" class="h-[236px] shrink-0 w-full relative overflow-hidden" style="background:linear-gradient(180deg, #3a3937 0%, #23221e 100%) !important;">
<img id="rd-hero-img" src="" alt="" class="w-full h-full object-cover hidden">
<div class="absolute inset-0 pointer-events-none" style="background:linear-gradient(to bottom, rgba(45,46,43,0.1), rgba(45,46,43,0.4), rgba(45,46,43,0.92));"></div>
<span id="rd-hero-label" class="absolute inset-0 z-10 flex items-center justify-center font-medium text-[15px]" style="color:#ddd6ca;"></span>
</div>
<div id="rd-scroll-container" class="absolute inset-0 z-10 overflow-y-auto no-scrollbar" style="overscroll-behavior-y:none;">
<div id="rd-hero" class="h-[236px] w-full relative overflow-hidden" style="background:linear-gradient(180deg, #3a3937 0%, #23221e 100%) !important; will-change:height,opacity;">
<img id="rd-hero-img" src="" alt="" class="w-full h-full object-cover hidden" style="will-change:transform;">
<div class="absolute inset-0 pointer-events-none" style="background:linear-gradient(to bottom, rgba(45,46,43,0.1), rgba(45,46,43,0.4), rgba(45,46,43,0.92));"></div>
<span id="rd-hero-label" class="absolute inset-0 z-10 flex items-center justify-center font-medium text-[15px]" style="color:#ddd6ca;"></span>
</div>
<div class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden" style="background:#2d2e2b !important; background-image:none !important; box-shadow:0 -8px 20px rgba(0,0,0,0.35) !important;">
<div class="mb-3 px-5 shrink-0">
<div class="flex justify-between items-start mb-2.5">
<h1 id="rd-title" class="text-xl font-bold leading-tight" style="color:#ddd6ca;"></h1>
</div>
<div id="rd-tags" class="flex flex-wrap gap-1.5 mb-3"></div>
<div class="flex flex-wrap gap-2 text-[13px] font-medium">
<div class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border" style="background:#2f2f2d; border-color:#444442; color:#d7d2c8;">
<i class="fas fa-clock text-[10px]" style="color:#9b978f;"></i>
<span id="rd-time"></span>
<div id="rd-content-body" class="bg-[#2d2e2b] rounded-t-3xl -mt-6 relative z-30 pt-6 min-h-screen" style="background:#2d2e2b !important; background-image:none !important; box-shadow:0 -8px 20px rgba(0,0,0,0.35) !important;">
<div class="mb-3 px-5">
<div class="flex justify-between items-start mb-2.5">
<h1 id="rd-title" class="text-xl font-bold leading-tight" style="color:#ddd6ca;"></h1>
</div>
</div>
</div>
<div id="rd-tab-bar" class="flex border-b mb-2 px-5 shrink-0 relative z-10" style="border-color:#444442;">
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="ingredients" style="color:#ddd6ca; border-bottom-color:#787876; font-weight:600;">Składniki</button>
<button class="flex-1 pb-2.5 text-[13px] border-b-2 rd-tab-btn" data-rd-tab="steps" style="color:#9b978f; border-bottom-color:transparent; font-weight:500;">Kroki</button>
</div>
<div class="px-5 pt-2">
<h2 class="text-[11px] font-bold uppercase tracking-wider mb-3" style="color:${RD_THEME.textMuted};">Składniki</h2>
<div id="rd-tab-ingredients"></div>
</div>
<div class="flex-1 overflow-y-auto px-5 pt-2 pb-10 no-scrollbar relative" style="background:#2d2e2b !important; background-image:none !important; padding-bottom:calc(2.5rem + env(safe-area-inset-bottom));">
<div id="rd-tab-ingredients" class="rd-tab-content block animate-fade-in"></div>
<div id="rd-tab-steps" class="rd-tab-content hidden animate-fade-in"></div>
<div class="px-5 pt-2" style="padding-bottom:calc(2.5rem + env(safe-area-inset-bottom));">
<div class="border-t my-4" style="border-color:${RD_THEME.border};"></div>
<h2 class="text-[11px] font-bold uppercase tracking-wider mb-3" style="color:${RD_THEME.textMuted};">Kroki</h2>
<div id="rd-tab-steps"></div>
</div>
</div>
</div>
</div>
@@ -116,6 +84,7 @@ let currentAddedIngredients = [];
let currentProductSelections = {};
let expandedAlternatives = new Set();
let ingredientCard = null;
let resetScrollState = null;
function isPlannedMode() {
return currentMode === 'planned';
@@ -225,31 +194,11 @@ function populateDetail(recipeId, options = {}) {
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');
resetScrollState?.();
}
/* ── helpers ───────────────────────────────────────────── */
@@ -523,9 +472,9 @@ function renderSteps(recipe) {
container.innerHTML = `
<div class="space-y-0.5 pb-5">
${steps.map((step, i) => `
<div class="rounded-xl px-3 py-2 flex gap-3" style="background:transparent !important; background-image:none !important; box-shadow:none !important; border:none !important;">
<div class="w-6 h-6 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0" style="background:transparent !important; border:none !important; box-shadow:none !important; color:${RD_THEME.textSecondary} !important;">${i + 1}</div>
<div class="pt-0.5"><p class="text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p></div>
<div class="relative rounded-xl py-2 pl-3 pr-3" style="background:transparent !important; background-image:none !important; box-shadow:none !important; border:none !important;">
<div class="absolute top-2 left-0 w-3 h-6 flex items-center justify-start text-[11px] font-bold shrink-0 tabular-nums" style="background:transparent !important; border:none !important; box-shadow:none !important; color:${RD_THEME.textSecondary} !important;">${i + 1}.</div>
<p class="pl-4 text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p>
</div>`).join('')}
</div>`;
}
@@ -536,37 +485,81 @@ 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;
}
/* ── collapsing hero on scroll ─────────────── */
document.querySelectorAll('.rd-tab-btn').forEach((b) => {
setTabButtonState(b, false);
});
setTabButtonState(btn, true);
});
});
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);
/* ── 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);
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', () => {