Add more example recipes
All checks were successful
Build and Deploy / build-and-push (push) Successful in 27s

This commit is contained in:
2026-03-27 22:52:14 +01:00
parent 58ff081bc8
commit 7944ad2dbf
13 changed files with 458 additions and 603 deletions

View File

@@ -523,8 +523,10 @@ function renderDayContent(state) {
<div class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" data-recipe-id="${escapeHtml(recipe.id)}">
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>
<div class="w-8 h-8 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="min-w-0">
<p class="text-[13px] font-bold text-gray-900 truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>
@@ -629,8 +631,10 @@ function getRecentRecipeIds(plans, limit = 5) {
function recipeCardHtml(r) {
return `
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-gray-200 bg-gray-50/80 hover:border-gray-900 hover:bg-white text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] flex items-center justify-center shrink-0">
<span class="text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>
<div class="w-11 h-11 rounded-lg bg-[#d4d4d4] overflow-hidden shrink-0">
${r.image
? `<img src="${escapeHtml(r.image)}" alt="" class="w-full h-full object-cover">`
: `<span class="w-full h-full flex items-center justify-center text-white text-[9px] font-medium">${escapeHtml(r.thumbLabel)}</span>`}
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="text-[13px] font-bold text-gray-900 line-clamp-2">${escapeHtml(r.title)}</p>
@@ -872,9 +876,9 @@ function seedDemoIfEmpty(plans) {
return {
...plans,
[todayKey]: {
sniadanie: [{ id: newPlanEntryId(), recipeId: 'owsianka', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'salatka', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'makaron', servings: 1 }],
sniadanie: [{ id: newPlanEntryId(), recipeId: 'jajecznica', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'makaron_ricotta', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'kanapka_losos', servings: 1 }],
},
};
}

View File

@@ -67,8 +67,9 @@ export function getRecipeDetailHTML() {
</div>
</div>
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] flex items-center justify-center relative">
<span id="rd-hero-label" class="text-white font-medium text-[15px]"></span>
<div id="rd-hero" class="h-[220px] shrink-0 w-full bg-[#d4d4d4] relative overflow-hidden">
<img id="rd-hero-img" src="" alt="" class="w-full h-full object-cover hidden">
<span id="rd-hero-label" class="absolute inset-0 flex items-center justify-center text-white font-medium text-[15px]"></span>
</div>
<div class="bg-white rounded-t-3xl -mt-6 relative z-30 pt-6 flex flex-col flex-1 overflow-hidden">
@@ -128,7 +129,18 @@ function populateDetail(recipeId) {
currentSubstitutions = {};
expandedAlternatives.clear();
document.getElementById('rd-hero-label').textContent = `Zdjęcie: ${recipe.title}`;
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`;
updateKcalDisplay();
@@ -190,10 +202,11 @@ function renderNutritionLine(nutrition) {
/* ── ingredients tab with inline nutrition + summary ───── */
function computeIngredientNutritionTotals(recipe) {
function computeEffectiveNutritionTotals(recipe) {
let kcal = 0, protein = 0, fat = 0, carbs = 0;
for (const ing of recipe.ingredients) {
const n = nutritionForAmount(ing.ingredientId, ing.amount * currentServings);
const effectiveId = getEffectiveIngredientId(ing.ingredientId);
const n = nutritionForAmount(effectiveId, ing.amount * currentServings);
if (n) {
kcal += n.kcal;
protein += n.protein;
@@ -210,14 +223,8 @@ function computeIngredientNutritionTotals(recipe) {
}
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,
};
const total = computeEffectiveNutritionTotals(recipe);
return `
<div class="mb-4 pt-1 pb-3 border-b border-gray-100">
@@ -255,7 +262,7 @@ function renderIngredientCard(name, amount, unit, nutrition, extra) {
: '';
return `
<div class="${extra?.cls || 'bg-white'} rounded-xl p-2.5 border ${extra?.border || 'border-gray-200'} flex items-center gap-2.5">
<div class="${extra?.cls || 'bg-white'} rounded-xl p-2.5 border ${extra?.border || 'border-gray-200'} flex items-center gap-2.5" ${extra?.dataAttrs || ''}>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
${extra?.prefix || ''}
@@ -277,35 +284,52 @@ function renderIngredients(recipe) {
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 origId = ing.ingredientId;
const hasAlts = ing.alternatives && ing.alternatives.length > 0;
const isExpanded = expandedAlternatives.has(ing.ingredientId);
const effectiveId = hasAlts ? getEffectiveIngredientId(origId) : origId;
const effectiveDef = INGREDIENTS[effectiveId];
const effectiveName = effectiveDef?.name || effectiveId;
const scaledAmount = ing.amount * currentServings;
const nutrition = nutritionForAmount(effectiveId, scaledAmount);
const isSwapped = effectiveId !== origId;
const isExpanded = expandedAlternatives.has(origId);
const toggleBtn = hasAlts
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 rounded-full ${isExpanded ? 'bg-amber-50 text-amber-500' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'} flex items-center justify-center transition-colors" data-original-id="${escapeHtml(ing.ingredientId)}"><i class="fas fa-shuffle text-[8px]"></i></button>`
? `<button type="button" class="rd-alt-toggle shrink-0 w-5 h-5 rounded-full ${isExpanded ? 'bg-amber-50 text-amber-500' : isSwapped ? 'bg-amber-50 text-amber-500' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'} flex items-center justify-center transition-colors" data-original-id="${escapeHtml(origId)}"><i class="fas fa-shuffle text-[8px]"></i></button>`
: '';
const cardHtml = renderIngredientCard(name, scaledAmount, ing.unit, nutrition, { suffix: toggleBtn });
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 altListHtml = '';
if (hasAlts) {
const altCards = ing.alternatives.map((altId) => {
const altDef = INGREDIENTS[altId];
const altName = altDef?.name || altId;
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 isOriginal = altId === origId;
const altNutrition = nutritionForAmount(altId, scaledAmount);
const radioDot = `<div class="w-3.5 h-3.5 rounded-full border-2 shrink-0 ${isSelected ? 'border-gray-900' : 'border-gray-300'} flex items-center justify-center">${isSelected ? '<div class="w-1.5 h-1.5 rounded-full bg-gray-900"></div>' : ''}</div>`;
const defaultTag = isOriginal ? `<span class="text-[9px] px-1.5 py-0.5 rounded ${isSelected ? 'bg-gray-200 text-gray-600' : 'bg-gray-100 text-gray-400'} font-medium ml-1">Domyślny</span>` : '';
return renderIngredientCard(altName, scaledAmount, ing.unit, altNutrition, {
cls: 'bg-gray-50',
border: 'border-gray-100',
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)}"`,
});
});
altListHtml = `
<div class="rd-alt-list ${isExpanded ? '' : 'hidden'} mt-1.5 space-y-1.5" data-original-id="${escapeHtml(ing.ingredientId)}">
<p class="text-[10px] text-gray-400 font-medium mb-1 pl-1">Można zamienić na:</p>
${altCards.join('')}
<div class="mt-1.5 space-y-1.5 rd-alt-options" data-original-id="${escapeHtml(origId)}">
${optionCards.join('')}
</div>`;
}
@@ -324,12 +348,23 @@ function renderIngredients(recipe) {
} 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');
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);
});
});
});
}
@@ -625,7 +660,6 @@ export function setupRecipeDetail() {
if (!recipe) return;
document.getElementById('rd-planner-recipe-name').textContent = recipe.title;
currentSubstitutions = {};
expandedVariantGroups.clear();
const today = startOfDay(new Date());

View File

@@ -55,8 +55,10 @@ function renderRecipeCard(recipe) {
const labels = slotLabelsFor(recipe);
return `
<div onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="border border-gray-200 rounded-xl overflow-hidden shadow-sm flex flex-col bg-white cursor-pointer hover:shadow-md transition-shadow">
<div class="h-32 bg-[#d4d4d4] relative flex items-center justify-center">
<span class="text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>
<div class="h-32 bg-[#d4d4d4] relative overflow-hidden">
${recipe.image
? `<img src="${escapeHtml(recipe.image)}" alt="${escapeHtml(recipe.title)}" class="w-full h-full object-cover">`
: `<span class="absolute inset-0 flex items-center justify-center text-white font-medium text-xs">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="p-3 flex flex-col flex-1">
<h3 class="text-sm font-medium underline decoration-1 underline-offset-2 text-black mb-1 line-clamp-1">${escapeHtml(recipe.title)}</h3>