Implement planner recipe detail interactions and refine dock styling
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m14s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m14s
This commit is contained in:
@@ -408,7 +408,8 @@
|
|||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
#recipe-search-input::placeholder,
|
#recipe-search-input::placeholder,
|
||||||
#planner-picker-search::placeholder {
|
#planner-picker-search::placeholder,
|
||||||
|
#pantry-search::placeholder {
|
||||||
color: #beb8ae !important;
|
color: #beb8ae !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -458,7 +459,7 @@
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 0.85rem calc(1.12rem + env(safe-area-inset-bottom));
|
padding: 0 0.85rem calc(1.58rem + env(safe-area-inset-bottom));
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#app-bottom-nav .bottom-dock {
|
#app-bottom-nav .bottom-dock {
|
||||||
|
|||||||
@@ -335,6 +335,9 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
|
|
||||||
if (hasProducts && !product) {
|
if (hasProducts && !product) {
|
||||||
const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length;
|
const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length;
|
||||||
|
const helperChip = state.allowProductSelection
|
||||||
|
? '<span class="inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold shrink-0" style="background:#2f2f2d; color:#d7d2c8;">Wybierz produkt</span>'
|
||||||
|
: '';
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
||||||
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||||
@@ -344,7 +347,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
<p class="text-[16px] font-bold tabular-nums mt-1" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p>
|
<p class="text-[16px] font-bold tabular-nums mt-1" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p>
|
||||||
<p class="text-[11px] mt-1 leading-snug" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma zapas</p>
|
<p class="text-[11px] mt-1 leading-snug" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma zapas</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold shrink-0" style="background:#2f2f2d; color:#d7d2c8;">Wybierz produkt</span>
|
${helperChip}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function syncTodayButton(mode, weekStart, monthAnchor, selected) {
|
|||||||
|
|
||||||
export function getMealPlannerHTML() {
|
export function getMealPlannerHTML() {
|
||||||
return `
|
return `
|
||||||
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10 pb-24">
|
<div id="planner-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-[#2d2e2b] z-10">
|
||||||
<div id="planner-cal-bar" class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3 relative z-10">
|
<div id="planner-cal-bar" class="shrink-0 bg-[#2d2e2b] border-b border-[#444442] mt-3 relative z-10">
|
||||||
${createCalendarTopbarHTML({
|
${createCalendarTopbarHTML({
|
||||||
titleId: 'cal-period-label',
|
titleId: 'cal-period-label',
|
||||||
@@ -95,7 +95,7 @@ export function getMealPlannerHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planner-scroll" class="flex-1 overflow-y-auto px-4 pt-3 pb-4 bg-[#2d2e2b]">
|
<div id="planner-scroll" class="flex-1 overflow-y-auto px-4 pt-3 pb-24 bg-[#2d2e2b]">
|
||||||
<div id="planner-summary-card" class="mb-3">
|
<div id="planner-summary-card" class="mb-3">
|
||||||
<div class="h-full flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
<div class="h-full flex flex-col" style="background:#2d2e2b !important; background-image:none !important; box-shadow:none !important;">
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-2">Wartości odżywcze</p>
|
||||||
@@ -523,9 +523,9 @@ function renderDayContent(state, onMealRemoved = null) {
|
|||||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-end px-4" style="${backgroundStyle}">
|
<div class="pointer-events-none absolute inset-0 flex items-center justify-end px-4" style="${backgroundStyle}">
|
||||||
${backgroundLabel}
|
${backgroundLabel}
|
||||||
</div>
|
</div>
|
||||||
<div class="relative z-[1] rounded-lg p-2" style="background:${isPendingDelete ? 'rgba(45,45,43,0.76)' : '#2d2e2b'}; box-shadow:inset 0 1px 3px rgba(0,0,0,0.3); transform:${isPendingDelete ? 'translateX(0) scale(0.988)' : 'translateX(0)'}; transition:transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease, background-color 180ms ease; touch-action:pan-y; opacity:1;" data-planner-swipe-card data-slot-id="${slot.id}" data-entry-id="${eid}">
|
<div class="relative z-[1] rounded-lg p-2 planner-open-recipe cursor-pointer" style="background:${isPendingDelete ? 'rgba(45,45,43,0.76)' : '#2d2e2b'}; box-shadow:inset 0 1px 3px rgba(0,0,0,0.3); transform:${isPendingDelete ? 'translateX(0) scale(0.988)' : 'translateX(0)'}; transition:transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease, background-color 180ms ease; touch-action:pan-y; opacity:1;" data-planner-swipe-card data-slot-id="${slot.id}" data-entry-id="${eid}" data-recipe-id="${escapeHtml(recipe.id)}">
|
||||||
<div class="relative flex items-start justify-between gap-2">
|
<div class="relative flex items-start justify-between gap-2">
|
||||||
<div class="flex items-center gap-2 min-w-0 cursor-pointer planner-open-recipe" style="${contentToneStyle}" data-recipe-id="${escapeHtml(recipe.id)}">
|
<div class="flex items-center gap-2 min-w-0" style="${contentToneStyle}">
|
||||||
<div class="w-8 h-8 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
|
<div class="w-8 h-8 rounded-lg bg-[#3a3a37] overflow-hidden shrink-0">
|
||||||
${recipe.image
|
${recipe.image
|
||||||
? `<img src="${escapeHtml(recipe.image)}" alt="" class="w-full h-full object-cover">`
|
? `<img src="${escapeHtml(recipe.image)}" alt="" class="w-full h-full object-cover">`
|
||||||
@@ -1258,14 +1258,6 @@ export function setupMealPlanner() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openRecipe = e.target.closest('.planner-open-recipe');
|
|
||||||
if (openRecipe) {
|
|
||||||
const recipeId = openRecipe.getAttribute('data-recipe-id');
|
|
||||||
if (recipeId && window.openRecipeDetail) {
|
|
||||||
window.openRecipeDetail(recipeId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const addBtn = e.target.closest('.planner-add-meal');
|
const addBtn = e.target.closest('.planner-add-meal');
|
||||||
if (addBtn) {
|
if (addBtn) {
|
||||||
const slotId = addBtn.getAttribute('data-slot-id');
|
const slotId = addBtn.getAttribute('data-slot-id');
|
||||||
@@ -1299,6 +1291,19 @@ export function setupMealPlanner() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const openRecipe = e.target.closest('.planner-open-recipe');
|
||||||
|
if (openRecipe) {
|
||||||
|
const recipeId = openRecipe.getAttribute('data-recipe-id');
|
||||||
|
if (recipeId && window.openRecipeDetail) {
|
||||||
|
const slotId = openRecipe.closest('[data-slot-id]')?.getAttribute('data-slot-id');
|
||||||
|
const entryId = openRecipe.closest('[data-entry-id]')?.getAttribute('data-entry-id');
|
||||||
|
const key = dateKey(state.selected);
|
||||||
|
const entries = slotId ? state.plans[key]?.[slotId] : null;
|
||||||
|
const entry = Array.isArray(entries) ? entries.find((item) => item && item.id === entryId) : null;
|
||||||
|
window.openRecipeDetail(recipeId, entry ? { plannedEntry: entry } : {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const closePicker = () => {
|
const closePicker = () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RECIPES, INGREDIENTS } from '../data/catalog.js?v=8';
|
import { RECIPES, INGREDIENTS, PRODUCTS } from '../data/catalog.js?v=8';
|
||||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||||
|
import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107';
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
@@ -99,30 +100,117 @@ export function getRecipeDetailHTML() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${getIngredientCardHTML({ idBase: 'rd-ing-card' })}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── state ─────────────────────────────────────────────── */
|
/* ── state ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
let currentRecipeId = null;
|
let currentRecipeId = null;
|
||||||
|
let currentMode = 'catalog';
|
||||||
let currentServings = 1;
|
let currentServings = 1;
|
||||||
let currentSubstitutions = {};
|
let currentSubstitutions = {};
|
||||||
|
let currentExcludedIngredients = new Set();
|
||||||
|
let currentAmountOverrides = {};
|
||||||
|
let currentAddedIngredients = [];
|
||||||
|
let currentProductSelections = {};
|
||||||
let expandedAlternatives = new Set();
|
let expandedAlternatives = new Set();
|
||||||
|
let ingredientCard = null;
|
||||||
|
|
||||||
|
function isPlannedMode() {
|
||||||
|
return currentMode === 'planned';
|
||||||
|
}
|
||||||
|
|
||||||
function getEffectiveIngredientId(originalId) {
|
function getEffectiveIngredientId(originalId) {
|
||||||
return currentSubstitutions[originalId] || 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 ──────────────────────────────────────────── */
|
/* ── populate ──────────────────────────────────────────── */
|
||||||
|
|
||||||
function populateDetail(recipeId) {
|
function populateDetail(recipeId, options = {}) {
|
||||||
const recipe = RECIPES[recipeId];
|
const recipe = RECIPES[recipeId];
|
||||||
if (!recipe) return;
|
if (!recipe) return;
|
||||||
|
|
||||||
currentRecipeId = recipeId;
|
currentRecipeId = recipeId;
|
||||||
currentServings = 1;
|
currentMode = options.plannedEntry ? 'planned' : 'catalog';
|
||||||
currentSubstitutions = {};
|
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();
|
expandedAlternatives.clear();
|
||||||
|
ingredientCard?.close();
|
||||||
|
|
||||||
const heroImg = document.getElementById('rd-hero-img');
|
const heroImg = document.getElementById('rd-hero-img');
|
||||||
const heroLabel = document.getElementById('rd-hero-label');
|
const heroLabel = document.getElementById('rd-hero-label');
|
||||||
@@ -138,6 +226,7 @@ function populateDetail(recipeId) {
|
|||||||
}
|
}
|
||||||
document.getElementById('rd-title').textContent = recipe.title;
|
document.getElementById('rd-title').textContent = recipe.title;
|
||||||
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
|
||||||
|
document.getElementById('rd-add-to-planner-btn')?.classList.toggle('hidden', isPlannedMode());
|
||||||
|
|
||||||
const tagsHtml = [];
|
const tagsHtml = [];
|
||||||
for (const slotId of recipe.allowedSlots) {
|
for (const slotId of recipe.allowedSlots) {
|
||||||
@@ -165,19 +254,22 @@ function populateDetail(recipeId) {
|
|||||||
|
|
||||||
/* ── helpers ───────────────────────────────────────────── */
|
/* ── helpers ───────────────────────────────────────────── */
|
||||||
|
|
||||||
function nutritionForAmount(ingredientId, amount, unit) {
|
function nutritionForAmount(ingredientId, amount, unit, productIdOverride = null) {
|
||||||
const def = INGREDIENTS[ingredientId];
|
const def = INGREDIENTS[ingredientId];
|
||||||
if (!def?.nutritionPer100g) return null;
|
const productId = productIdOverride || getProductSelectionForIngredient(ingredientId);
|
||||||
|
const product = getSelectedProduct(productId);
|
||||||
|
const nutrition = product?.nutritionPer100g || def?.nutritionPer100g;
|
||||||
|
if (!def || !nutrition) return null;
|
||||||
let grams = amount;
|
let grams = amount;
|
||||||
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) {
|
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) {
|
||||||
grams = amount * def.weightPerPiece;
|
grams = amount * def.weightPerPiece;
|
||||||
}
|
}
|
||||||
const f = grams / 100;
|
const f = grams / 100;
|
||||||
return {
|
return {
|
||||||
kcal: Math.round(def.nutritionPer100g.kcal * f),
|
kcal: Math.round(nutrition.kcal * f),
|
||||||
protein: Math.round(def.nutritionPer100g.protein * f * 10) / 10,
|
protein: Math.round(nutrition.protein * f * 10) / 10,
|
||||||
fat: Math.round(def.nutritionPer100g.fat * f * 10) / 10,
|
fat: Math.round(nutrition.fat * f * 10) / 10,
|
||||||
carbs: Math.round(def.nutritionPer100g.carbs * f * 10) / 10,
|
carbs: Math.round(nutrition.carbs * f * 10) / 10,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +281,8 @@ function fmtAmt(n) {
|
|||||||
|
|
||||||
function computeEffectiveNutritionTotals(recipe) {
|
function computeEffectiveNutritionTotals(recipe) {
|
||||||
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
let kcal = 0, protein = 0, fat = 0, carbs = 0;
|
||||||
for (const ing of recipe.ingredients) {
|
for (const ing of buildVisibleIngredients(recipe)) {
|
||||||
const effectiveId = getEffectiveIngredientId(ing.ingredientId);
|
const n = nutritionForAmount(ing.ingredientId, ing.amount, ing.unit, ing.productId);
|
||||||
const n = nutritionForAmount(effectiveId, ing.amount * currentServings, ing.unit);
|
|
||||||
if (n) {
|
if (n) {
|
||||||
kcal += n.kcal;
|
kcal += n.kcal;
|
||||||
protein += n.protein;
|
protein += n.protein;
|
||||||
@@ -209,6 +300,25 @@ function computeEffectiveNutritionTotals(recipe) {
|
|||||||
|
|
||||||
function renderNutritionSummary(recipe) {
|
function renderNutritionSummary(recipe) {
|
||||||
const total = computeEffectiveNutritionTotals(recipe);
|
const total = computeEffectiveNutritionTotals(recipe);
|
||||||
|
const servingsHtml = isPlannedMode()
|
||||||
|
? `
|
||||||
|
<div class="mt-3 flex items-center justify-between gap-3">
|
||||||
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Porcje</p>
|
||||||
|
<p class="pr-1 text-[13px] font-semibold leading-none text-[#d7d2c8] tabular-nums">${currentServings}</p>
|
||||||
|
</div>`
|
||||||
|
: `
|
||||||
|
<div class="mt-3 flex items-center justify-between gap-3">
|
||||||
|
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Porcje</p>
|
||||||
|
<div class="flex h-[2rem] w-[5.25rem] shrink-0 items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
||||||
|
<button type="button" id="rd-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
||||||
|
<i class="fas fa-minus text-[10px]"></i>
|
||||||
|
</button>
|
||||||
|
<span id="rd-servings" class="flex-1 h-full inline-flex items-center justify-center px-0.5 text-[12px] font-semibold leading-none text-[#d7d2c8] tabular-nums">${currentServings}</span>
|
||||||
|
<button type="button" id="rd-serv-plus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zwiększ liczbę porcji">
|
||||||
|
<i class="fas fa-plus text-[10px]"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -235,18 +345,7 @@ function renderNutritionSummary(recipe) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex items-center justify-between gap-3">
|
${servingsHtml}
|
||||||
<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider">Porcje</p>
|
|
||||||
<div class="flex h-[2rem] w-[5.25rem] shrink-0 items-center gap-0.5 rounded-full border px-0.5" style="background:#2f2f2d;border-color:#444442;box-shadow:0 2px 8px rgba(0,0,0,0.25);">
|
|
||||||
<button type="button" id="rd-serv-minus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zmniejsz liczbę porcji">
|
|
||||||
<i class="fas fa-minus text-[10px]"></i>
|
|
||||||
</button>
|
|
||||||
<span id="rd-servings" class="flex-1 h-full inline-flex items-center justify-center px-0.5 text-[12px] font-semibold leading-none text-[#d7d2c8] tabular-nums">${currentServings}</span>
|
|
||||||
<button type="button" id="rd-serv-plus" class="shrink-0 w-7 h-full flex items-center justify-center rounded-full border-0 bg-transparent text-[#d7d2c8] transition-colors" aria-label="Zwiększ liczbę porcji">
|
|
||||||
<i class="fas fa-plus text-[10px]"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +353,59 @@ function renderIngredients(recipe) {
|
|||||||
const container = document.getElementById('rd-tab-ingredients');
|
const container = document.getElementById('rd-tab-ingredients');
|
||||||
if (!container) return;
|
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
|
||||||
|
? `<div class="flex items-center gap-1 mt-0.5"><span class="text-[10px] text-emerald-400 truncate">${escapeHtml(item.productName)}</span></div>`
|
||||||
|
: '';
|
||||||
|
const addedMark = item.added
|
||||||
|
? '<span class="shrink-0 inline-flex items-center justify-center text-[#8f8b84]" title="Dodany składnik" aria-label="Dodany składnik"><i class="fas fa-plus text-[8px]"></i></span>'
|
||||||
|
: '';
|
||||||
|
return `<li>
|
||||||
|
<button type="button" class="${rowClass}" style="${rowStyle}" data-rd-open-ingredient data-rd-ingredient-id="${escapeHtml(item.ingredientId)}" data-rd-product-id="${escapeHtml(item.productId || '')}">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-[12px] font-semibold text-gray-900 truncate block">${escapeHtml(item.name)}</span>
|
||||||
|
${addedMark}
|
||||||
|
</div>
|
||||||
|
${productBadge}
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg">
|
||||||
|
<span class="text-[12px] font-semibold text-gray-900 tabular-nums">${fmtAmt(item.amount)}</span>
|
||||||
|
<span class="text-[11px] text-gray-500">${escapeHtml(item.unit)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</li>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
${renderNutritionSummary(recipe)}
|
||||||
|
${rows
|
||||||
|
? `<ul class="space-y-1.5" id="rd-ingredient-list">${rows}</ul>`
|
||||||
|
: `<p class="text-sm text-center py-8" style="color:${RD_THEME.textMuted};">Brak składników w tej wersji przepisu.</p>`}`;
|
||||||
|
|
||||||
|
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 rows = recipe.ingredients.map((ing) => {
|
||||||
const origId = ing.ingredientId;
|
const origId = ing.ingredientId;
|
||||||
const hasAlts = ing.alternatives && ing.alternatives.length > 0;
|
const hasAlts = ing.alternatives && ing.alternatives.length > 0;
|
||||||
@@ -369,9 +521,9 @@ function renderSteps(recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="space-y-2 pb-5">
|
<div class="space-y-0.5 pb-5">
|
||||||
${steps.map((step, i) => `
|
${steps.map((step, i) => `
|
||||||
<div class="rounded-xl p-3 flex gap-3" style="background:transparent !important; background-image:none !important; box-shadow:none !important; border:none !important;">
|
<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="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="pt-0.5"><p class="text-[13px] leading-relaxed" style="color:${RD_THEME.textSecondary};">${escapeHtml(step)}</p></div>
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
@@ -381,6 +533,9 @@ function renderSteps(recipe) {
|
|||||||
/* ── setup ─────────────────────────────────────────────── */
|
/* ── setup ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
export function setupRecipeDetail() {
|
export function setupRecipeDetail() {
|
||||||
|
ingredientCard = createIngredientCardController({ idBase: 'rd-ing-card', defaultSourceNote: 'Z planera' });
|
||||||
|
ingredientCard.bind();
|
||||||
|
|
||||||
document.querySelectorAll('.rd-tab-btn').forEach((btn) => {
|
document.querySelectorAll('.rd-tab-btn').forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const tabId = btn.dataset.rdTab;
|
const tabId = btn.dataset.rdTab;
|
||||||
@@ -415,7 +570,7 @@ export function setupRecipeDetail() {
|
|||||||
/* ── planner — delegate to MealPlanEditor ─────── */
|
/* ── planner — delegate to MealPlanEditor ─────── */
|
||||||
|
|
||||||
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {
|
document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => {
|
||||||
if (!currentRecipeId) return;
|
if (!currentRecipeId || isPlannedMode()) return;
|
||||||
window.openMealPlanEditor?.({
|
window.openMealPlanEditor?.({
|
||||||
mode: 'add',
|
mode: 'add',
|
||||||
recipeId: currentRecipeId,
|
recipeId: currentRecipeId,
|
||||||
@@ -424,15 +579,16 @@ export function setupRecipeDetail() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.openRecipeDetail = (recipeId) => {
|
window.openRecipeDetail = (recipeId, options = {}) => {
|
||||||
if (!recipeId || !RECIPES[recipeId]) return;
|
if (!recipeId || !RECIPES[recipeId]) return;
|
||||||
populateDetail(recipeId);
|
populateDetail(recipeId, options);
|
||||||
const view = document.getElementById('recipe-detail-view');
|
const view = document.getElementById('recipe-detail-view');
|
||||||
view.classList.remove('translate-x-full', 'opacity-0', 'pointer-events-none');
|
view.classList.remove('translate-x-full', 'opacity-0', 'pointer-events-none');
|
||||||
view.classList.add('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
view.classList.add('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.closeRecipeDetail = () => {
|
window.closeRecipeDetail = () => {
|
||||||
|
ingredientCard?.close();
|
||||||
window.closeMealPlanEditor?.();
|
window.closeMealPlanEditor?.();
|
||||||
const view = document.getElementById('recipe-detail-view');
|
const view = document.getElementById('recipe-detail-view');
|
||||||
view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto');
|
||||||
|
|||||||
Reference in New Issue
Block a user