Update planner search and planner editor

This commit is contained in:
2026-04-08 22:16:20 +02:00
parent 4706430316
commit 165f39d0b7
7 changed files with 693 additions and 301 deletions

View File

@@ -20,6 +20,27 @@ const PREP_TIME_MAX = 120;
const PREP_TIME_STEP = 5;
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
const FILTER_RECIPE_BLUR = 'blur(3px) saturate(0.94)';
const FILTER_CONTEXTS = {
recipes: {
anchorShellId: 'recipe-search-shell',
buttonId: 'recipe-filter-btn',
getState: () => getFilterState(),
applyState: (nextState) => applyFilters(nextState),
showSlots: true,
},
plannerPicker: {
anchorShellId: 'planner-picker-search-shell',
buttonId: 'planner-picker-filter-btn',
getState: () => window.getPlannerPickerFilterState?.() || ({
slots: [],
tags: [],
minMinutes: PREP_TIME_MIN,
maxMinutes: PREP_TIME_MAX,
}),
applyState: (nextState) => window.applyPlannerPickerFilters?.(nextState),
showSlots: false,
},
};
function escapeHtml(s) {
return String(s)
@@ -61,7 +82,7 @@ export function getFilterHTML() {
outline: none;
}
</style>
<div id="filter-view" class="absolute inset-0 z-[55] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:rgba(0,0,0,0.5) !important; background-image:none !important;" aria-hidden="true">
<div id="filter-view" class="absolute inset-0 z-[70] hidden opacity-0 transition-opacity duration-150" style="pointer-events:none; background:rgba(0,0,0,0.5) !important; background-image:none !important;" aria-hidden="true">
<div id="filter-panel" class="absolute flex flex-col overflow-hidden rounded-[1.5rem] border" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important; opacity:0; transform:translateY(-0.5rem) scale(0.98); transform-origin:top center; transition:${FILTER_PANEL_TRANSITION}; box-shadow:0 18px 40px rgba(0,0,0,0.34), 0 4px 12px rgba(0,0,0,0.18); width:min(calc(100% - 1.5rem), 22rem);">
<div class="pointer-events-none absolute inset-x-0 top-0 h-px" style="background:rgba(242,239,232,0.12);" aria-hidden="true"></div>
<div class="shrink-0 px-3.5 pt-3 pb-2 flex justify-end" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
@@ -71,7 +92,7 @@ export function getFilterHTML() {
</div>
<div id="filter-panel-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 pb-4 space-y-2.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<section class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<section id="filter-slot-section" class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
<p class="text-[10px] font-bold uppercase tracking-wider mb-3" style="color:${FILTER_TEXT_MUTED};">Pora posiłku</p>
<div id="filter-slot-chips" class="flex flex-wrap gap-2"></div>
</section>
@@ -132,6 +153,11 @@ let localTags = [];
let localMinMinutes = PREP_TIME_MIN;
let localMaxMinutes = PREP_TIME_MAX;
let closeTimer = null;
let activeFilterContext = 'recipes';
function getActiveFilterConfig() {
return FILTER_CONTEXTS[activeFilterContext] || FILTER_CONTEXTS.recipes;
}
function normalizeTimeRange(minMinutes, maxMinutes) {
let nextMin = snapTimeValue(minMinutes);
@@ -216,9 +242,10 @@ function positionFilterPanel() {
const view = document.getElementById('filter-view');
const panel = document.getElementById('filter-panel');
const body = document.getElementById('filter-panel-body');
const searchShell = document.getElementById('recipe-search-shell');
const button = document.getElementById('recipe-filter-btn');
if (!view || !panel || !button) return;
const { anchorShellId, buttonId } = getActiveFilterConfig();
const searchShell = document.getElementById(anchorShellId);
const button = document.getElementById(buttonId);
if (!view || !panel || (!searchShell && !button)) return;
const viewRect = view.getBoundingClientRect();
const anchorRect = (searchShell || button).getBoundingClientRect();
@@ -327,9 +354,16 @@ function renderTagChips() {
});
}
function syncFilterSections() {
const slotSection = document.getElementById('filter-slot-section');
if (!slotSection) return;
slotSection.classList.toggle('hidden', !getActiveFilterConfig().showSlots);
}
function syncLiveFilters() {
applyFilters({
slots: localSlots,
const config = getActiveFilterConfig();
config.applyState?.({
slots: config.showSlots ? localSlots : [],
tags: localTags,
minMinutes: localMinMinutes,
maxMinutes: localMaxMinutes,
@@ -465,15 +499,17 @@ export function setupFilter() {
syncLiveFilters();
});
window.openFilters = () => {
if (isFilterPanelOpen()) {
window.openFilters = (contextName = 'recipes') => {
if (isFilterPanelOpen() && activeFilterContext === contextName) {
window.closeFilters();
return;
}
const state = getFilterState();
localSlots = [...state.slots];
localTags = [...state.tags];
activeFilterContext = FILTER_CONTEXTS[contextName] ? contextName : 'recipes';
const config = getActiveFilterConfig();
const state = config.getState?.() || {};
localSlots = [...(state.slots || [])];
localTags = [...(state.tags || [])];
const normalized = normalizeTimeRange(
Number.isFinite(state.minMinutes) ? state.minMinutes : PREP_TIME_MIN,
Number.isFinite(state.maxMinutes) ? state.maxMinutes : PREP_TIME_MAX,
@@ -485,6 +521,7 @@ export function setupFilter() {
renderSlotChips();
renderTagChips();
syncFilterSections();
showFilterPanel();
};

View File

@@ -37,6 +37,11 @@ import {
syncCalendarTodayButton,
syncCollapsibleCalendarMode,
} from '../ui/mealCalendar.js?v=1';
import {
filterRecipesByQuery,
renderRecipeGrid,
} from '../ui/recipeGrid.js';
import { getRecipeSearchFieldHTML } from '../ui/recipeSearchField.js';
const WEEKDAYS_LONG = [
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
@@ -45,6 +50,11 @@ const WEEKDAYS_LONG = [
const PLANNER_SHEET_BOTTOM_INSET = '5.25rem';
const PLANNER_SHEET_MAX_HEIGHT = '70vh';
const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`;
const PLANNER_PICKER_OFF_TRANSFORM = 'translateY(100%)';
const PLANNER_MEAL_SWIPE_MAX = 132;
const PLANNER_MEAL_SWIPE_DELETE_THRESHOLD = 96;
const PICKER_FILTER_MIN_MINUTES = 5;
const PICKER_FILTER_MAX_MINUTES = 120;
function recipesForSlot(slotId) {
return Object.values(RECIPES).filter((r) => r.allowedSlots.includes(slotId));
@@ -115,44 +125,61 @@ export function getMealPlannerHTML() {
</button>
<div id="planner-meal-slots" class="space-y-3 pb-2 bg-[#2d2e2b]"></div>
</div>
</div>
<div id="planner-picker-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-picker-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-picker-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-picker-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Wybierz przepis</h2>
<p id="planner-picker-sub" class="text-[11px] text-[#9b978f] mt-1"></p>
<div id="planner-picker-backdrop" class="absolute inset-0 z-[55] bg-black/45 hidden opacity-0 transition-opacity duration-200" aria-hidden="true"></div>
<div id="planner-picker-sheet" data-off-transform="${PLANNER_PICKER_OFF_TRANSFORM}" class="absolute inset-x-0 bottom-0 z-[60] flex flex-col will-change-transform rounded-t-[1.85rem] overflow-hidden" style="top:calc(env(safe-area-inset-top) + 0.35rem); visibility: hidden; transform: ${PLANNER_PICKER_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-label="Wybierz przepis" aria-modal="true">
<div class="pointer-events-none absolute inset-x-0 top-0 z-[2] px-4 pt-3">
<div class="pointer-events-auto pb-4 touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto" aria-hidden="true"></div>
</div>
<div class="shrink-0 px-4 pt-2 pb-2">
<input type="text" id="planner-picker-search" class="w-full rounded-xl border border-[#444442] bg-[#2f2f2d] px-3 py-2 text-sm text-[#ddd6ca] outline-none focus:border-[#6d6c67] placeholder:text-[#7d7a74]" placeholder="Szukaj przepisu…" />
</div>
<div id="planner-picker-list" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2.5 pb-8 space-y-2"></div>
</div>
<div id="planner-ing-backdrop" class="absolute left-0 right-0 top-0 z-[45] bg-black/45 hidden opacity-0 transition-opacity duration-200" style="bottom: ${PLANNER_SHEET_BOTTOM_INSET}" aria-hidden="true"></div>
<div id="planner-ing-sheet" class="absolute left-0 right-0 z-[50] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; bottom: ${PLANNER_SHEET_BOTTOM_INSET}; height: auto; max-height: ${PLANNER_SHEET_MAX_HEIGHT}; transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Składniki i spiżarnia</h2>
<p id="planner-ing-sub" class="text-[11px] text-[#9b978f] mt-1">Porównanie potrzeb z zapasami.</p>
</div>
<div id="planner-ing-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2 pb-2"></div>
<div id="planner-ing-footer" class="shrink-0 p-4 pt-2 pb-5 border-t border-[#444442] bg-[#2d2e2b] space-y-2">
<button type="button" id="planner-ing-add-all" class="w-full bg-gray-900 hover:bg-black text-white py-3 rounded-xl font-semibold shadow-sm transition-colors text-[13px] flex items-center justify-center gap-2">
<i class="fas fa-cart-plus text-xs" aria-hidden="true"></i>
Dodaj braki na dziś do listy
</button>
<button type="button" id="planner-ing-add-btn" class="hidden w-full border border-[#444442] bg-[#2d2e2b] text-[#d7d2c8] hover:bg-[#3a3a37] py-2.5 rounded-xl font-semibold text-[13px] flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-calendar-week text-[#9b978f] text-[11px]" aria-hidden="true"></i>
Dodaj braki na cały tydzień
</button>
<div class="pointer-events-auto">
${getRecipeSearchFieldHTML({
shellId: 'planner-picker-search-shell',
inputId: 'planner-picker-search',
placeholder: 'Wybierz przepis',
inputAriaLabel: 'Wybierz przepis',
filterButtonId: 'planner-picker-filter-btn',
filterButtonAction: 'openFilters(\'plannerPicker\')',
filterButtonLabel: 'Otwórz filtry',
})}
</div>
</div>
<div id="planner-toast" class="pointer-events-none absolute left-4 right-4 bottom-28 z-[55] opacity-0 translate-y-2 transition-all duration-300" role="status">
<div class="rounded-xl bg-gray-900 text-white text-sm font-medium px-4 py-3 shadow-lg text-center" id="planner-toast-text"></div>
<div id="planner-picker-scroll" class="relative min-h-0 flex-1 overflow-y-auto px-4 pt-28 pb-8 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
<div id="planner-picker-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
<div id="planner-picker-empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<i class="fas fa-search text-2xl text-gray-300" aria-hidden="true"></i>
</div>
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Spróbuj innej nazwy przepisu</p>
</div>
</div>
</div>
<div id="planner-ing-backdrop" class="absolute inset-0 z-[55] bg-black/45 hidden opacity-0 transition-opacity duration-200" aria-hidden="true"></div>
<div id="planner-ing-sheet" class="absolute left-0 right-0 bottom-0 z-[60] rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] flex flex-col will-change-transform" style="visibility: hidden; height: auto; max-height: calc(100vh - env(safe-area-inset-top) - 1rem); transform: ${PLANNER_SHEET_OFF_TRANSFORM}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1); background:#2d2e2b !important; background-image:none !important;" role="dialog" aria-labelledby="planner-ing-title" aria-modal="true">
<div class="shrink-0 px-4 pt-3 pb-2 border-b border-[#444442] touch-none cursor-grab active:cursor-grabbing select-none" data-planner-sheet-drag-zone aria-label="Przeciągnij w dół, by zamknąć">
<div class="w-10 h-1 bg-[#6d6c67]/75 rounded-full mx-auto mb-2.5" aria-hidden="true"></div>
<h2 id="planner-ing-title" class="text-[15px] font-bold text-[#ddd6ca] leading-tight pr-2">Składniki i spiżarnia</h2>
<p id="planner-ing-sub" class="text-[11px] text-[#9b978f] mt-1">Porównanie potrzeb z zapasami.</p>
</div>
<div id="planner-ing-body" class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-2 pb-2"></div>
<div id="planner-ing-footer" class="shrink-0 p-4 pt-2 pb-5 border-t border-[#444442] bg-[#2d2e2b] space-y-2">
<button type="button" id="planner-ing-add-all" class="w-full bg-gray-900 hover:bg-black text-white py-3 rounded-xl font-semibold shadow-sm transition-colors text-[13px] flex items-center justify-center gap-2">
<i class="fas fa-cart-plus text-xs" aria-hidden="true"></i>
Dodaj braki na dziś do listy
</button>
<button type="button" id="planner-ing-add-btn" class="hidden w-full border border-[#444442] bg-[#2d2e2b] text-[#d7d2c8] hover:bg-[#3a3a37] py-2.5 rounded-xl font-semibold text-[13px] flex items-center justify-center gap-2 transition-colors">
<i class="fas fa-calendar-week text-[#9b978f] text-[11px]" aria-hidden="true"></i>
Dodaj braki na cały tydzień
</button>
</div>
</div>
<div id="planner-toast" class="pointer-events-none absolute left-4 right-4 bottom-28 z-[65] opacity-0 translate-y-2 transition-all duration-300" role="status">
<div class="rounded-xl bg-gray-900 text-white text-sm font-medium px-4 py-3 shadow-lg text-center" id="planner-toast-text"></div>
</div>
`;
}
@@ -254,10 +281,14 @@ function openSheet(backdrop, sheet) {
});
}
function getSheetOffTransform(sheet) {
return sheet?.dataset.offTransform || PLANNER_SHEET_OFF_TRANSFORM;
}
function closeSheet(backdrop, sheet) {
if (!backdrop || !sheet) return;
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = PLANNER_SHEET_OFF_TRANSFORM;
sheet.style.transform = getSheetOffTransform(sheet);
backdrop.classList.add('opacity-0');
setTimeout(() => {
backdrop.classList.add('hidden');
@@ -265,60 +296,117 @@ function closeSheet(backdrop, sheet) {
}, 300);
}
/** Zamykanie panelu: przeciągnięcie nagłówka w dół (pointer). */
function bindPlannerSheetDragClose(sheet, closeFn) {
const zone = sheet.querySelector('[data-planner-sheet-drag-zone]');
/** Zamykanie panelu: przeciągnięcie w dół (pointer). */
function bindPlannerSheetDragClose(sheet, closeFn, options = {}) {
const zone = options.dragSurface || sheet.querySelector('[data-planner-sheet-drag-zone]');
const scrollEl = options.scrollEl || null;
if (!zone || !sheet) return;
let startX = 0;
let startY = 0;
let pulling = false;
let ptrId = null;
let engaged = false;
let startFromHandle = false;
let suppressClickUntil = 0;
const resetVisual = () => {
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = 'translateY(0)';
};
const stopTracking = () => {
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerCancel);
ptrId = null;
engaged = false;
startFromHandle = false;
};
const shouldIgnoreTarget = (target) => target instanceof Element
&& Boolean(target.closest('input, textarea, select, [contenteditable="true"]'));
zone.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return;
pulling = true;
ptrId = e.pointerId;
startY = e.clientY;
sheet.style.transition = 'none';
zone.setPointerCapture(e.pointerId);
});
if (shouldIgnoreTarget(e.target)) return;
zone.addEventListener('pointermove', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = Math.max(0, e.clientY - startY);
sheet.style.transform = `translateY(${dy}px)`;
});
zone.addEventListener('pointerup', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
pulling = false;
ptrId = null;
try {
zone.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
if (scrollEl && scrollEl.scrollTop > 0 && !(e.target instanceof Element && e.target.closest('[data-planner-sheet-drag-zone]'))) {
return;
}
if (dy > 56) {
ptrId = e.pointerId;
startX = e.clientX;
startY = e.clientY;
engaged = false;
startFromHandle = e.target instanceof Element && Boolean(e.target.closest('[data-planner-sheet-drag-zone]'));
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerCancel);
});
const onPointerMove = (e) => {
if (e.pointerId !== ptrId) return;
const dx = e.clientX - startX;
const rawDy = e.clientY - startY;
if (!engaged) {
if (Math.abs(dx) < 8 && Math.abs(rawDy) < 8) return;
if (rawDy <= 0 || Math.abs(dx) > Math.abs(rawDy)) {
stopTracking();
resetVisual();
return;
}
if (!startFromHandle && scrollEl && scrollEl.scrollTop > 0) {
stopTracking();
resetVisual();
return;
}
engaged = true;
suppressClickUntil = Date.now() + 250;
sheet.style.transition = 'none';
}
const dy = Math.max(0, rawDy);
sheet.style.transform = `translateY(${dy}px)`;
if (e.cancelable) e.preventDefault();
};
const onPointerUp = (e) => {
if (e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
const shouldClose = engaged && dy > 56;
stopTracking();
if (shouldClose) {
closeFn();
return;
}
resetVisual();
});
};
zone.addEventListener('pointercancel', () => {
pulling = false;
ptrId = null;
const onPointerCancel = (e) => {
if (ptrId !== null && e.pointerId !== ptrId) return;
stopTracking();
resetVisual();
};
zone.addEventListener('click', (e) => {
if (Date.now() < suppressClickUntil) {
e.preventDefault();
e.stopPropagation();
}
}, true);
zone.addEventListener('pointercancel', (e) => {
onPointerCancel(e);
});
}
function renderDayContent(state) {
function renderDayContent(state, onMealRemoved = null) {
const sel = state.selected;
const dayPlan = getDayPlan(state.plans, sel);
const totals = sumDayNutrition(dayPlan);
@@ -385,34 +473,39 @@ function renderDayContent(state) {
(entry.amountOverrides && Object.keys(entry.amountOverrides).length > 0) ||
(entry.addedIngredients?.length > 0) ||
(entry.substitutions && Object.keys(entry.substitutions).length > 0);
const customDot = hasCustom ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
const customDot = hasCustom ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
const servLabel = servings > 1 ? `<span class="mx-1.5 text-[#6d6c67]">·</span>×${servings}` : '';
return `
<div class="rounded-lg bg-[#2d2e2b] p-2" style="box-shadow:inset 0 1px 3px rgba(0,0,0,0.3);" 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-[#3a3a37] 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 class="relative overflow-hidden rounded-lg" data-planner-swipe-row style="--planner-swipe-progress:0;" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="pointer-events-none absolute inset-0 flex items-center justify-end px-4" style="background:rgba(203,74,72, calc(0.18 + var(--planner-swipe-progress) * 0.5));">
<span class="inline-flex items-center gap-1.5 text-[11px] font-semibold tracking-wide uppercase" style="color:rgba(250,234,234, calc(0.55 + var(--planner-swipe-progress) * 0.45));">
<i class="fas fa-trash text-[10px]" aria-hidden="true"></i>
Usuń
</span>
</div>
<div class="relative z-[1] rounded-lg bg-[#2d2e2b] p-2" style="box-shadow:inset 0 1px 3px rgba(0,0,0,0.3); transform:translateX(0); transition:transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease; touch-action:pan-y;" data-planner-swipe-card 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-[#3a3a37] 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">
<div class="flex items-center"><p class="text-[13px] font-bold text-[#ddd6ca] truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
<p class="text-[11px] text-[#9b978f] mt-0.5 tabular-nums">
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-[#6d6c67]">·</span>
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${entryN.kcal} kcal${servLabel}
</p>
</div>
</div>
<div class="min-w-0">
<div class="flex items-center"><p class="text-[13px] font-bold text-[#ddd6ca] truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
<p class="text-[11px] text-[#9b978f] mt-0.5 tabular-nums">
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-[#6d6c67]">·</span>
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${entryN.kcal} kcal${servLabel}
</p>
<div class="flex items-center gap-1 shrink-0 self-center">
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-[#ddd6ca] hover:border-[#6d6c67] hover:bg-[#3a3a37] flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-[#ddd6ca] hover:border-[#6d6c67] hover:bg-[#3a3a37] flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
</button>
<button type="button" class="planner-clear-meal w-6 h-6 rounded-full border border-[#444442] text-[#9b978f] hover:text-red-400 hover:border-red-300/60 hover:bg-[#3a2326] transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
</button>
</div>
</div>
</div>`;
}).join('');
@@ -450,6 +543,147 @@ function renderDayContent(state) {
</div>
</div>`;
}).join('');
bindMealEntrySwipe(state, onMealRemoved);
}
function removeMealEntry(state, slotId, entryId) {
const key = dateKey(state.selected);
const day = state.plans[key];
const arr = day?.[slotId];
if (!Array.isArray(arr) || !entryId) return false;
const next = arr.filter((x) => x && x.id !== entryId);
if (next.length === arr.length) return false;
if (next.length === 0) delete day[slotId];
else day[slotId] = next;
if (Object.keys(day).length === 0) delete state.plans[key];
return true;
}
function bindMealEntrySwipe(state, onMealRemoved = null) {
const cards = document.querySelectorAll('[data-planner-swipe-card]');
cards.forEach((card) => {
const row = card.closest('[data-planner-swipe-row]');
if (!row) return;
let pointerId = null;
let startX = 0;
let startY = 0;
let dx = 0;
let engaged = false;
let suppressClickUntil = 0;
const resetVisual = () => {
row.classList.remove('planner-meal-row-swiping');
row.style.setProperty('--planner-swipe-progress', '0');
row.style.pointerEvents = '';
card.style.willChange = '';
card.style.transition = 'transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease';
card.style.transform = 'translateX(0)';
card.style.opacity = '1';
};
const stopTracking = () => {
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerCancel);
pointerId = null;
};
const onPointerMove = (e) => {
if (e.pointerId !== pointerId) return;
dx = e.clientX - startX;
const dy = e.clientY - startY;
if (!engaged) {
if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return;
suppressClickUntil = Date.now() + 250;
if (Math.abs(dy) > Math.abs(dx) || dx >= 0) {
stopTracking();
resetVisual();
return;
}
engaged = true;
row.classList.add('planner-meal-row-swiping');
card.style.transition = 'none';
card.style.willChange = 'transform';
}
const translateX = Math.max(-PLANNER_MEAL_SWIPE_MAX, Math.min(0, dx));
const progress = Math.min(1, Math.abs(translateX) / PLANNER_MEAL_SWIPE_DELETE_THRESHOLD);
row.style.setProperty('--planner-swipe-progress', String(progress));
card.style.transform = `translateX(${translateX}px)`;
if (e.cancelable) e.preventDefault();
};
const onPointerUp = (e) => {
if (e.pointerId !== pointerId) return;
const shouldDelete = engaged && dx <= -PLANNER_MEAL_SWIPE_DELETE_THRESHOLD;
stopTracking();
if (shouldDelete) {
const slotId = card.getAttribute('data-slot-id');
const entryId = card.getAttribute('data-entry-id');
row.style.pointerEvents = 'none';
row.style.setProperty('--planner-swipe-progress', '1');
card.style.willChange = '';
card.style.transition = 'transform 160ms cubic-bezier(0.32, 0.72, 0, 1), opacity 160ms ease';
card.style.transform = 'translateX(-120%)';
card.style.opacity = '0';
window.setTimeout(() => {
if (removeMealEntry(state, slotId, entryId)) {
if (typeof onMealRemoved === 'function') onMealRemoved();
else {
savePlans(state.plans);
renderDayContent(state);
}
showPlannerToast('Usunięto posiłek z planu dnia');
}
}, 160);
return;
}
engaged = false;
dx = 0;
resetVisual();
};
const onPointerCancel = (e) => {
if (pointerId !== null && e.pointerId !== pointerId) return;
stopTracking();
engaged = false;
dx = 0;
resetVisual();
};
card.addEventListener('click', (e) => {
if (Date.now() < suppressClickUntil) {
e.preventDefault();
e.stopPropagation();
}
}, true);
card.addEventListener('pointerdown', (e) => {
if (pointerId !== null) return;
if (e.pointerType === 'mouse' && e.button !== 0) return;
pointerId = e.pointerId;
startX = e.clientX;
startY = e.clientY;
dx = 0;
engaged = false;
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp);
window.addEventListener('pointercancel', onPointerCancel);
});
});
}
function escapeHtml(s) {
@@ -481,71 +715,65 @@ function getRecentRecipeIds(plans, limit = 5) {
return [...seen.keys()];
}
function recipeCardHtml(r) {
return `
<button type="button" class="planner-pick-recipe w-full flex gap-2.5 p-2.5 rounded-xl border border-[#444442] bg-[#2d2e2b] hover:border-[#6d6c67] hover:bg-[#3a3a37] text-left transition-all" data-recipe-id="${r.id}">
<div class="w-11 h-11 rounded-lg bg-[#3a3a37] 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-[#ddd6ca] line-clamp-2">${escapeHtml(r.title)}</p>
<p class="text-[11px] text-[#9b978f] mt-1 tabular-nums">
<i class="fas fa-fire text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.nutritionPerServing.kcal} kcal
<span class="mx-1 text-[#6d6c67]">·</span>
<i class="fas fa-clock text-[#7d7a74] mr-0.5" aria-hidden="true"></i>${r.minutes} min
</p>
</div>
</button>`;
function sortPickerRecipes(recipes, plans) {
const recentIndex = new Map(
getRecentRecipeIds(plans, 12).map((recipeId, index) => [recipeId, index]),
);
return [...recipes].sort((a, b) => {
const aRecent = recentIndex.has(a.id) ? recentIndex.get(a.id) : Number.POSITIVE_INFINITY;
const bRecent = recentIndex.has(b.id) ? recentIndex.get(b.id) : Number.POSITIVE_INFINITY;
if (aRecent !== bRecent) return aRecent - bRecent;
return a.title.localeCompare(b.title, 'pl');
});
}
let _pickerSlotRecipes = [];
let _pickerPlans = {};
function matchesPickerFilters(recipe, filterState) {
const slots = Array.isArray(filterState?.slots) ? filterState.slots : [];
const tags = Array.isArray(filterState?.tags) ? filterState.tags : [];
const minMinutes = Number.isFinite(filterState?.minMinutes)
? filterState.minMinutes
: PICKER_FILTER_MIN_MINUTES;
const maxMinutes = Number.isFinite(filterState?.maxMinutes)
? filterState.maxMinutes
: PICKER_FILTER_MAX_MINUTES;
function renderPickerList(slotId, plans, query = '') {
const slot = MEAL_SLOTS.find((s) => s.id === slotId);
const list = document.getElementById('planner-picker-list');
const title = document.getElementById('planner-picker-title');
const sub = document.getElementById('planner-picker-sub');
if (!list || !title || !sub) return;
if (slots.length > 0 && !recipe.allowedSlots.some((slotId) => slots.includes(slotId))) return false;
title.textContent = 'Wybierz przepis';
sub.textContent = slot ? `Dla: ${slot.label}` : '';
const allRecipes = recipesForSlot(slotId);
_pickerSlotRecipes = allRecipes;
_pickerPlans = plans;
const q = query.trim().toLowerCase();
const filtered = q
? allRecipes.filter((r) => r.title.toLowerCase().includes(q) || (r.tags || []).some((t) => t.toLowerCase().includes(q)))
: allRecipes;
if (filtered.length === 0 && q) {
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak wyników.</p>';
return;
}
if (filtered.length === 0) {
list.innerHTML = '<p class="text-sm text-[#9b978f] text-center py-6">Brak dopasowanych przepisów.</p>';
return;
if (tags.length > 0) {
const recipeTags = (recipe.tags || []).map((tag) => tag.toLowerCase());
if (!tags.some((tag) => recipeTags.includes(tag.toLowerCase()))) return false;
}
let html = '';
if (minMinutes > PICKER_FILTER_MIN_MINUTES && recipe.minutes < minMinutes) return false;
if (maxMinutes < PICKER_FILTER_MAX_MINUTES && recipe.minutes > maxMinutes) return false;
if (!q) {
const recentIds = getRecentRecipeIds(plans);
const recentInSlot = recentIds.map((id) => RECIPES[id]).filter((r) => r && r.allowedSlots.includes(slotId));
if (recentInSlot.length > 0) {
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1"><i class="fas fa-history text-[9px] mr-1"></i>Ostatnio używane</p>`;
html += recentInSlot.map(recipeCardHtml).join('');
html += `<div class="border-t border-[#444442] my-2"></div>`;
html += `<p class="text-[10px] font-bold text-gray-400 uppercase tracking-wider px-0.5 pt-1 pb-1">Wszystkie</p>`;
}
}
return true;
}
html += filtered.map(recipeCardHtml).join('');
list.innerHTML = html;
function renderPickerGrid(slotId, plans, query = '') {
const grid = document.getElementById('planner-picker-grid');
const emptyState = document.getElementById('planner-picker-empty-state');
if (!grid || !emptyState) return;
const pickerFilters = window.getPlannerPickerFilterState?.() || {
slots: [],
tags: [],
minMinutes: PICKER_FILTER_MIN_MINUTES,
maxMinutes: PICKER_FILTER_MAX_MINUTES,
};
const filtered = filterRecipesByQuery(recipesForSlot(slotId), query)
.filter((recipe) => matchesPickerFilters(recipe, pickerFilters));
const sorted = sortPickerRecipes(filtered, plans);
renderRecipeGrid({
gridEl: grid,
emptyStateEl: emptyState,
recipes: sorted,
showSlotLabels: false,
cardClassName: 'planner-picker-recipe-card',
});
}
function plIngredientWord(n) {
@@ -755,8 +983,15 @@ export function setupMealPlanner() {
const pickerBackdrop = document.getElementById('planner-picker-backdrop');
const pickerSheet = document.getElementById('planner-picker-sheet');
const pickerScroll = document.getElementById('planner-picker-scroll');
const ingBackdrop = document.getElementById('planner-ing-backdrop');
const ingSheet = document.getElementById('planner-ing-sheet');
const pickerFilterState = {
slots: [],
tags: [],
minMinutes: PICKER_FILTER_MIN_MINUTES,
maxMinutes: PICKER_FILTER_MAX_MINUTES,
};
const rerender = () => {
syncModeToggle(state.mode);
@@ -775,7 +1010,7 @@ export function setupMealPlanner() {
: dayHasAnyMeal(state.plans, day),
}),
});
renderDayContent(state);
renderDayContent(state, persist);
};
const persist = () => {
@@ -881,8 +1116,13 @@ export function setupMealPlanner() {
state.pickerSlot = slotId;
const searchInput = document.getElementById('planner-picker-search');
if (searchInput) searchInput.value = '';
renderPickerList(slotId, state.plans);
const pickerScroll = document.getElementById('planner-picker-scroll');
if (pickerScroll) pickerScroll.scrollTop = 0;
renderPickerGrid(slotId, state.plans);
openSheet(pickerBackdrop, pickerSheet);
window.setTimeout(() => {
if (pickerScroll) pickerScroll.scrollTop = 0;
}, 40);
return;
}
const editBtn = e.target.closest('.planner-edit-meal');
@@ -903,54 +1143,66 @@ export function setupMealPlanner() {
});
return;
}
const clearBtn = e.target.closest('.planner-clear-meal');
if (clearBtn) {
const slotId = clearBtn.getAttribute('data-slot-id');
const entryId = clearBtn.getAttribute('data-entry-id');
const key = dateKey(state.selected);
const arr = state.plans[key]?.[slotId];
if (!Array.isArray(arr) || !entryId) return;
const next = arr.filter((x) => x && x.id !== entryId);
if (!state.plans[key]) state.plans[key] = {};
if (next.length === 0) delete state.plans[key][slotId];
else state.plans[key][slotId] = next;
if (Object.keys(state.plans[key]).length === 0) delete state.plans[key];
persist();
return;
}
});
const closePicker = () => {
state.pickerSlot = null;
const searchInput = document.getElementById('planner-picker-search');
if (searchInput) searchInput.value = '';
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
closeSheet(pickerBackdrop, pickerSheet);
};
window.getPlannerPickerFilterState = () => ({
...pickerFilterState,
slots: [...pickerFilterState.slots],
tags: [...pickerFilterState.tags],
});
window.applyPlannerPickerFilters = (nextState = {}) => {
Object.assign(pickerFilterState, nextState);
pickerFilterState.slots = Array.isArray(nextState.slots)
? [...nextState.slots]
: [...pickerFilterState.slots];
pickerFilterState.tags = Array.isArray(nextState.tags)
? [...nextState.tags]
: [...pickerFilterState.tags];
if (state.pickerSlot) {
const searchValue = document.getElementById('planner-picker-search')?.value || '';
renderPickerGrid(state.pickerSlot, state.plans, searchValue);
}
};
document.getElementById('planner-picker-search')?.addEventListener('input', (e) => {
if (state.pickerSlot) {
renderPickerList(state.pickerSlot, state.plans, e.target.value);
renderPickerGrid(state.pickerSlot, state.plans, e.target.value);
}
});
bindPlannerSheetDragClose(pickerSheet, closePicker);
bindPlannerSheetDragClose(pickerSheet, closePicker, {
dragSurface: pickerSheet,
scrollEl: pickerScroll,
});
bindPlannerSheetDragClose(ingSheet, () => closeSheet(ingBackdrop, ingSheet));
pickerBackdrop?.addEventListener('click', closePicker);
document.getElementById('planner-picker-list')?.addEventListener('click', (e) => {
const pick = e.target.closest('.planner-pick-recipe');
document.getElementById('planner-picker-grid')?.addEventListener('click', (e) => {
const pick = e.target.closest('.recipe-browser-card');
if (!pick || !state.pickerSlot) return;
const recipeId = pick.getAttribute('data-recipe-id');
if (!recipeId || !RECIPES[recipeId]) return;
const slotId = state.pickerSlot;
closePicker();
setTimeout(() => {
window.requestAnimationFrame(() => {
window.openMealPlanEditor?.({
mode: 'add',
recipeId,
date: state.selected,
slotId,
});
}, 320);
});
});
document.getElementById('planner-open-ingredients')?.addEventListener('click', () => {

View File

@@ -1,25 +1,12 @@
import { RECIPES } from '../data/catalog.js?v=8';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import { getRecipeGridSectionHTML, renderRecipeGrid } from '../ui/recipeGrid.js';
import {
getRecipeSearchFieldHTML,
syncRecipeSearchShellShadow,
} from '../ui/recipeSearchField.js';
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label]));
const DEFAULT_MIN_MINUTES = 5;
const DEFAULT_MAX_MINUTES = 120;
const SEARCH_SHELL_BASE_SHADOW =
'0 5px 10px rgba(0,0,0,0.16), 0 14px 22px rgba(0,0,0,0.24), 0 22px 34px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.04)';
function slotLabelsFor(recipe) {
return (recipe.allowedSlots || [])
.map((id) => slotLabelMap[id])
.filter(Boolean);
}
let filterState = {
query: '',
@@ -57,107 +44,49 @@ function getFilteredRecipes() {
return Object.values(RECIPES).filter(matchesFilters);
}
function renderRecipeCard(recipe) {
const labels = slotLabelsFor(recipe);
return `
<div data-recipe-id="${escapeHtml(recipe.id)}" onclick="openRecipeDetail('${escapeHtml(recipe.id)}')" class="recipe-card rounded-xl overflow-hidden flex flex-col bg-[#393937] cursor-pointer transition-shadow" style="background:#393937 !important; border:none !important; box-shadow:0 2px 8px rgba(0,0,0,0.28) !important;">
<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-[#f1ede4] mb-3 line-clamp-2">${escapeHtml(recipe.title)}</h3>
<div class="mt-auto">
<div class="flex items-center justify-between text-[11px] text-[#c2bcb2] font-medium mb-2">
<div class="flex items-center gap-1"><i class="fas fa-clock text-[#8f8b84]"></i><span>${recipe.minutes} min</span></div>
<div class="flex items-center gap-1"><i class="fas fa-fire text-[#8f8b84]"></i><span>${recipe.nutritionPerServing.kcal} kcal</span></div>
</div>
<div class="flex flex-wrap gap-1">
${labels.map((l) => `<span class="px-2 py-0.5 bg-[#2f2f2d] text-[#d7d2c8] text-[10px] rounded-md font-medium">${escapeHtml(l)}</span>`).join('')}
</div>
</div>
</div>
</div>`;
}
function getEmptyStateHTML() {
return `
<div id="recipe-empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center">
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
<i class="fas fa-search text-2xl text-gray-300"></i>
</div>
<p class="text-sm font-semibold text-gray-700">Brak wyników</p>
<p class="text-xs text-gray-500 mt-1 max-w-[220px] leading-relaxed">Zmień kryteria wyszukiwania lub filtry</p>
</div>`;
}
function syncRecipeScrollShadow() {
const scroll = document.getElementById('recipe-scroll');
const searchShell = document.getElementById('recipe-search-shell');
if (!searchShell) return;
if (!scroll) {
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
return;
}
searchShell.style.boxShadow = SEARCH_SHELL_BASE_SHADOW;
syncRecipeSearchShellShadow(searchShell);
}
function renderAllRecipeCards() {
const grid = document.getElementById('recipe-grid');
if (!grid) return;
grid.innerHTML = Object.values(RECIPES).map(renderRecipeCard).join('');
}
function syncVisibleRecipeCards() {
function renderGrid() {
const grid = document.getElementById('recipe-grid');
const emptyState = document.getElementById('recipe-empty-state');
if (!grid || !emptyState) return;
let visibleCount = 0;
grid.querySelectorAll('[data-recipe-id]').forEach((card) => {
const recipeId = card.getAttribute('data-recipe-id');
const recipe = recipeId ? RECIPES[recipeId] : null;
const isVisible = Boolean(recipe && matchesFilters(recipe));
card.classList.toggle('hidden', !isVisible);
if (isVisible) visibleCount += 1;
});
grid.classList.toggle('hidden', visibleCount === 0);
emptyState.classList.toggle('hidden', visibleCount !== 0);
requestAnimationFrame(syncRecipeScrollShadow);
}
function renderGrid({ rebuild = false } = {}) {
const grid = document.getElementById('recipe-grid');
if (!grid) return;
if (rebuild || !grid.querySelector('[data-recipe-id]')) {
renderAllRecipeCards();
}
syncVisibleRecipeCards();
renderRecipeGrid({
gridEl: grid,
emptyStateEl: emptyState,
recipes: getFilteredRecipes(),
});
requestAnimationFrame(syncRecipeScrollShadow);
}
export function getRecipeListHTML() {
return `
<div id="main-view" class="flex flex-col h-full absolute inset-0 bg-[#2d2e2b] z-10" style="background:#2d2e2b !important;">
<div id="recipe-top-bar" class="pointer-events-none absolute inset-x-0 top-0 z-[12] px-4 pt-4" style="background:transparent !important; border:none !important;">
<div id="recipe-search-shell" class="pointer-events-auto relative z-[1] mx-auto flex items-center w-full overflow-hidden" style="width:min(calc(100% - 0.5rem), 22.4rem); background:#393937 !important; border:1px solid #41423f !important; border-radius:999px !important; box-shadow:${SEARCH_SHELL_BASE_SHADOW} !important; transition:box-shadow 180ms ease;">
<input type="text" id="recipe-search-input" placeholder="Szukaj przepisów..." class="w-full bg-transparent outline-none text-[15px] text-center py-[12px] pl-8 pr-14" style="background:transparent !important; border:none !important; box-shadow:none !important; backdrop-filter:none !important;">
<button id="recipe-filter-btn" onclick="openFilters()" class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 text-[#c9c3b8] hover:text-[#f0e8dc] flex items-center justify-center transition-colors" style="background:transparent !important; border:none !important; box-shadow:none !important;" aria-label="Otwórz filtry">
<i class="fas fa-sliders-h"></i>
</button>
<div class="pointer-events-auto">
${getRecipeSearchFieldHTML({
shellId: 'recipe-search-shell',
inputId: 'recipe-search-input',
placeholder: 'Szukaj przepisów...',
filterButtonId: 'recipe-filter-btn',
filterButtonAction: 'openFilters()',
filterButtonLabel: 'Otwórz filtry',
})}
</div>
</div>
<div id="recipe-scroll" class="relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]" style="background:#2d2e2b !important;">
<div id="recipe-grid" class="grid grid-cols-2 gap-3 bg-[#2d2e2b]" style="background:#2d2e2b !important;"></div>
${getEmptyStateHTML()}
</div>
${getRecipeGridSectionHTML({
scrollId: 'recipe-scroll',
gridId: 'recipe-grid',
emptyStateId: 'recipe-empty-state',
scrollClassName: 'relative flex-1 overflow-y-auto px-4 pt-20 pb-24 bg-[#2d2e2b]',
gridClassName: 'grid grid-cols-2 gap-3 bg-[#2d2e2b]',
emptyTitle: 'Brak wyników',
emptyMessage: 'Zmień kryteria wyszukiwania lub filtry',
})}
</div>
`;
}
@@ -176,17 +105,24 @@ export function getFilteredCount() {
}
export function refreshRecipeList() {
renderGrid({ rebuild: true });
renderGrid();
}
export function setupRecipeList() {
renderGrid({ rebuild: true });
renderGrid();
document.getElementById('recipe-search-input')?.addEventListener('input', (e) => {
filterState.query = e.target.value.trim();
renderGrid();
});
document.getElementById('recipe-grid')?.addEventListener('click', (e) => {
const card = e.target.closest('.recipe-browser-card');
if (!card) return;
const recipeId = card.getAttribute('data-recipe-id');
if (recipeId) window.openRecipeDetail?.(recipeId);
});
document.getElementById('recipe-scroll')?.addEventListener('scroll', syncRecipeScrollShadow);
syncRecipeScrollShadow();
}