import { RECIPES } from '../data/catalog.js?v=6'; import { MEAL_SLOTS } from '../planner/mealSlots.js'; import { applyFilters, getFilterState } from './RecipeList.js'; const FILTER_PANEL_TRANSITION = 'opacity 180ms ease, transform 180ms ease'; const FILTER_SURFACE = '#2d2e2b'; const FILTER_SURFACE_SOFT = '#2f2f2d'; const FILTER_BORDER = '#444442'; const FILTER_BORDER_ACTIVE = '#787876'; const FILTER_CHIP_ACTIVE = '#23221e'; const FILTER_TEXT_PRIMARY = '#ddd6ca'; const FILTER_TEXT_SECONDARY = '#d7d2c8'; const FILTER_TEXT_MUTED = '#9b978f'; const FILTER_TEXT_DIM = '#7d7a74'; const FILTER_TEXT_ACTIVE = '#f2efe8'; const FILTER_TRACK = '#393937'; const FILTER_TRACK_FILL = '#56534f'; const PREP_TIME_MIN = 5; 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)'; function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function collectAllTags() { const tags = new Set(); for (const r of Object.values(RECIPES)) { for (const t of (r.tags || [])) tags.add(t); } return [...tags].sort((a, b) => a.localeCompare(b, 'pl')); } export function getFilterHTML() { return `
`; } let localSlots = []; let localTags = []; let localMinMinutes = PREP_TIME_MIN; let localMaxMinutes = PREP_TIME_MAX; let closeTimer = null; function normalizeTimeRange(minMinutes, maxMinutes) { let nextMin = snapTimeValue(minMinutes); let nextMax = snapTimeValue(maxMinutes); nextMin = Math.min(Math.max(nextMin, PREP_TIME_MIN), PREP_TIME_MAX - PREP_TIME_MIN_GAP); nextMax = Math.max(Math.min(nextMax, PREP_TIME_MAX), PREP_TIME_MIN + PREP_TIME_MIN_GAP); if (nextMax - nextMin < PREP_TIME_MIN_GAP) { if (nextMin + PREP_TIME_MIN_GAP <= PREP_TIME_MAX) nextMax = nextMin + PREP_TIME_MIN_GAP; else nextMin = nextMax - PREP_TIME_MIN_GAP; } return { minMinutes: nextMin, maxMinutes: nextMax }; } function formatTimeValue(minutes) { return `${minutes} min`; } function formatTimeRangeSummary(minMinutes, maxMinutes) { return `${formatTimeValue(minMinutes)} - ${formatTimeValue(maxMinutes)}`; } function getChipStyle(active) { const background = active ? FILTER_CHIP_ACTIVE : FILTER_SURFACE_SOFT; const border = active ? FILTER_BORDER_ACTIVE : FILTER_BORDER; const color = active ? FILTER_TEXT_ACTIVE : FILTER_TEXT_SECONDARY; return `background:${background} !important; background-image:none !important; box-shadow:none !important; border-color:${border} !important; color:${color} !important;`; } function clampTimeValue(value) { return Math.min(Math.max(value, PREP_TIME_MIN), PREP_TIME_MAX); } function snapTimeValue(value) { const steps = Math.round((clampTimeValue(value) - PREP_TIME_MIN) / PREP_TIME_STEP); return PREP_TIME_MIN + steps * PREP_TIME_STEP; } function getSliderPercent(value) { return ((value - PREP_TIME_MIN) / (PREP_TIME_MAX - PREP_TIME_MIN)) * 100; } function setActiveTimeHandle(activeHandle) { const minHandle = document.getElementById('prep-time-min-handle'); const maxHandle = document.getElementById('prep-time-max-handle'); if (!minHandle || !maxHandle) return; minHandle.style.zIndex = activeHandle === 'min' ? '4' : '2'; maxHandle.style.zIndex = activeHandle === 'max' ? '4' : '3'; } function syncTimeRangeUI() { const minHandle = document.getElementById('prep-time-min-handle'); const maxHandle = document.getElementById('prep-time-max-handle'); const rangeDisplay = document.getElementById('time-display-range'); const rangeFill = document.getElementById('prep-time-range-fill'); const start = getSliderPercent(localMinMinutes); const end = getSliderPercent(localMaxMinutes); if (minHandle) { minHandle.style.left = `${start}%`; minHandle.setAttribute('aria-valuenow', String(localMinMinutes)); minHandle.setAttribute('aria-valuetext', formatTimeValue(localMinMinutes)); } if (maxHandle) { maxHandle.style.left = `${end}%`; maxHandle.setAttribute('aria-valuenow', String(localMaxMinutes)); maxHandle.setAttribute('aria-valuetext', formatTimeValue(localMaxMinutes)); } if (rangeDisplay) rangeDisplay.textContent = formatTimeRangeSummary(localMinMinutes, localMaxMinutes); if (rangeFill) { rangeFill.style.left = `${start}%`; rangeFill.style.width = `${Math.max(end - start, 0)}%`; } } 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 viewRect = view.getBoundingClientRect(); const anchorRect = (searchShell || button).getBoundingClientRect(); const gap = 8; const margin = 12; const width = Math.min(anchorRect.width, viewRect.width - margin * 2); const top = Math.max(margin, anchorRect.bottom - viewRect.top + gap); const left = Math.max( margin, Math.min(anchorRect.left - viewRect.left, viewRect.width - width - margin), ); const maxHeight = Math.max(220, viewRect.height - top - margin); panel.style.width = `${width}px`; panel.style.left = `${left}px`; panel.style.top = `${top}px`; panel.style.maxHeight = `${maxHeight}px`; if (body) body.style.maxHeight = `${maxHeight - 56}px`; } function setRecipeAreaBlur(visible) { const recipeScroll = document.getElementById('recipe-scroll'); if (!recipeScroll) return; recipeScroll.style.transition = 'filter 180ms ease, opacity 180ms ease'; recipeScroll.style.willChange = 'filter, opacity'; recipeScroll.style.filter = visible ? FILTER_RECIPE_BLUR : 'none'; recipeScroll.style.opacity = visible ? '0.78' : '1'; } function showFilterPanel() { const view = document.getElementById('filter-view'); const panel = document.getElementById('filter-panel'); if (!view || !panel) return; clearTimeout(closeTimer); view.classList.remove('hidden'); view.classList.add('filter-open'); view.style.pointerEvents = 'auto'; view.setAttribute('aria-hidden', 'false'); positionFilterPanel(); setRecipeAreaBlur(true); requestAnimationFrame(() => { view.classList.add('opacity-100'); panel.style.opacity = '1'; panel.style.transform = 'translateY(0) scale(1)'; }); } function hideFilterPanel() { const view = document.getElementById('filter-view'); const panel = document.getElementById('filter-panel'); if (!view || !panel) return; view.classList.remove('opacity-100', 'filter-open'); view.style.pointerEvents = 'none'; view.setAttribute('aria-hidden', 'true'); panel.style.opacity = '0'; panel.style.transform = 'translateY(-0.5rem) scale(0.98)'; setRecipeAreaBlur(false); closeTimer = setTimeout(() => { view.classList.add('hidden'); }, 180); } function isFilterPanelOpen() { return document.getElementById('filter-view')?.classList.contains('filter-open'); } function renderSlotChips() { const wrap = document.getElementById('filter-slot-chips'); if (!wrap) return; wrap.innerHTML = MEAL_SLOTS.map((slot) => { const active = localSlots.includes(slot.id); return ``; }).join(''); wrap.querySelectorAll('[data-filter-slot]').forEach((btn) => { btn.addEventListener('click', () => { const id = btn.dataset.filterSlot; const idx = localSlots.indexOf(id); if (idx >= 0) localSlots.splice(idx, 1); else localSlots.push(id); renderSlotChips(); syncLiveFilters(); }); }); } function renderTagChips() { const wrap = document.getElementById('filter-tag-chips'); if (!wrap) return; const allTags = collectAllTags(); wrap.innerHTML = allTags.map((tag) => { const active = localTags.includes(tag.toLowerCase()); return ``; }).join(''); wrap.querySelectorAll('[data-filter-tag]').forEach((btn) => { btn.addEventListener('click', () => { const tag = btn.dataset.filterTag.toLowerCase(); const idx = localTags.indexOf(tag); if (idx >= 0) localTags.splice(idx, 1); else localTags.push(tag); renderTagChips(); syncLiveFilters(); }); }); } function syncLiveFilters() { applyFilters({ slots: localSlots, tags: localTags, minMinutes: localMinMinutes, maxMinutes: localMaxMinutes, }); } export function setupFilter() { const rangeTrack = document.getElementById('prep-time-range-fill')?.parentElement; const minHandle = document.getElementById('prep-time-min-handle'); const maxHandle = document.getElementById('prep-time-max-handle'); const filterView = document.getElementById('filter-view'); let activeTimeHandle = null; setRecipeAreaBlur(false); function valueFromPointer(clientX) { if (!rangeTrack) return PREP_TIME_MIN; const rect = rangeTrack.getBoundingClientRect(); if (rect.width <= 0) return PREP_TIME_MIN; const ratio = Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1); return snapTimeValue(PREP_TIME_MIN + ratio * (PREP_TIME_MAX - PREP_TIME_MIN)); } function applyDraggedTimeValue(handleName, nextValue) { const value = snapTimeValue(nextValue); if (handleName === 'min') { localMinMinutes = Math.min(value, localMaxMinutes - PREP_TIME_MIN_GAP); localMinMinutes = Math.max(localMinMinutes, PREP_TIME_MIN); } else { localMaxMinutes = Math.max(value, localMinMinutes + PREP_TIME_MIN_GAP); localMaxMinutes = Math.min(localMaxMinutes, PREP_TIME_MAX); } syncTimeRangeUI(); syncLiveFilters(); } function stopTimeHandleDrag() { activeTimeHandle = null; window.removeEventListener('pointermove', onWindowPointerMove); window.removeEventListener('pointerup', stopTimeHandleDrag); window.removeEventListener('pointercancel', stopTimeHandleDrag); } function onWindowPointerMove(e) { if (!activeTimeHandle) return; e.preventDefault(); applyDraggedTimeValue(activeTimeHandle, valueFromPointer(e.clientX)); } function startTimeHandleDrag(handleName, e) { activeTimeHandle = handleName; setActiveTimeHandle(handleName); applyDraggedTimeValue(handleName, valueFromPointer(e.clientX)); window.addEventListener('pointermove', onWindowPointerMove); window.addEventListener('pointerup', stopTimeHandleDrag); window.addEventListener('pointercancel', stopTimeHandleDrag); } function handleTimeHandleKeydown(handleName, e) { let nextValue = handleName === 'min' ? localMinMinutes : localMaxMinutes; if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') nextValue -= PREP_TIME_STEP; else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') nextValue += PREP_TIME_STEP; else if (e.key === 'Home') nextValue = PREP_TIME_MIN; else if (e.key === 'End') nextValue = PREP_TIME_MAX; else return; e.preventDefault(); setActiveTimeHandle(handleName); applyDraggedTimeValue(handleName, nextValue); } syncTimeRangeUI(); setActiveTimeHandle('max'); if (minHandle) { minHandle.addEventListener('pointerdown', (e) => { e.preventDefault(); startTimeHandleDrag('min', e); }); minHandle.addEventListener('focus', () => { setActiveTimeHandle('min'); }); minHandle.addEventListener('keydown', (e) => { handleTimeHandleKeydown('min', e); }); } if (maxHandle) { maxHandle.addEventListener('pointerdown', (e) => { e.preventDefault(); startTimeHandleDrag('max', e); }); maxHandle.addEventListener('focus', () => { setActiveTimeHandle('max'); }); maxHandle.addEventListener('keydown', (e) => { handleTimeHandleKeydown('max', e); }); } rangeTrack?.addEventListener('pointerdown', (e) => { if (e.target.closest('[data-time-handle]')) return; const clickedValue = valueFromPointer(e.clientX); const distToMin = Math.abs(clickedValue - localMinMinutes); const distToMax = Math.abs(clickedValue - localMaxMinutes); const handleName = distToMin <= distToMax ? 'min' : 'max'; e.preventDefault(); startTimeHandleDrag(handleName, e); }); filterView?.addEventListener('click', (e) => { if (e.composedPath().includes(document.getElementById('filter-panel'))) return; window.closeFilters(); }); window.addEventListener('resize', () => { if (isFilterPanelOpen()) positionFilterPanel(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isFilterPanelOpen()) window.closeFilters(); }); document.getElementById('filter-clear-btn')?.addEventListener('click', () => { localSlots = []; localTags = []; localMinMinutes = PREP_TIME_MIN; localMaxMinutes = PREP_TIME_MAX; syncTimeRangeUI(); renderSlotChips(); renderTagChips(); syncLiveFilters(); }); window.openFilters = () => { if (isFilterPanelOpen()) { window.closeFilters(); return; } const state = getFilterState(); 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, ); localMinMinutes = normalized.minMinutes; localMaxMinutes = normalized.maxMinutes; syncTimeRangeUI(); renderSlotChips(); renderTagChips(); showFilterPanel(); }; window.closeFilters = () => { stopTimeHandleDrag(); hideFilterPanel(); }; }