import { RECIPES } from '../data/catalog.js?v=9'; 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 = 'rgb(var(--sunken-rgb))'; const FILTER_SURFACE_SOFT = 'rgb(var(--app-bg-rgb))'; const FILTER_BORDER = 'rgb(var(--border-input-rgb))'; const FILTER_CHIP_ACTIVE_BG = 'rgb(var(--card-rgb))'; const FILTER_TEXT_SECONDARY = 'rgb(var(--text-body-soft-rgb))'; const FILTER_TEXT_MUTED = 'rgb(var(--text-muted-rgb))'; const FILTER_TEXT_ACTIVE = 'rgb(var(--text-emphasis-rgb))'; const FILTER_TRACK = 'rgb(var(--card-rgb))'; const FILTER_TRACK_FILL = 'rgba(var(--border-input-rgb), 0.58)'; 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_CONTEXTS = { recipes: { anchorShellId: 'recipe-bottom-controls', 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) .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; let activeFilterContext = 'recipes'; function getActiveFilterConfig() { return FILTER_CONTEXTS[activeFilterContext] || FILTER_CONTEXTS.recipes; } 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_BG : FILTER_SURFACE_SOFT; const color = active ? FILTER_TEXT_ACTIVE : FILTER_TEXT_SECONDARY; const borderRule = active ? `border:1px solid ${FILTER_BORDER};` : 'border:none;'; const shadow = active ? 'box-shadow:inset 0 1px 0 rgba(255,255,255,0.04), 0 0 0 1px rgba(var(--overlay-rgb),0.08);' : ''; return `background:${background}; ${borderRule} color:${color}; ${shadow}`; } 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 { 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(); const gap = 8; const margin = 12; const width = Math.min(anchorRect.width, viewRect.width - margin * 2); const spaceBelow = viewRect.bottom - anchorRect.bottom - margin; const preferredMaxHeight = Math.min(420, viewRect.height - margin * 2); const spaceAbove = anchorRect.top - viewRect.top - gap - margin; const opensUpward = spaceBelow < 260 && anchorRect.top - viewRect.top > spaceBelow; const maxHeight = opensUpward ? Math.max(220, Math.min(preferredMaxHeight, spaceAbove)) : Math.max(220, viewRect.height - Math.max(margin, anchorRect.bottom - viewRect.top + gap) - margin); const top = opensUpward ? Math.max(margin, anchorRect.top - viewRect.top - gap - maxHeight) : Math.max(margin, anchorRect.bottom - viewRect.top + gap); const left = Math.max( margin, Math.min(anchorRect.left - viewRect.left, viewRect.width - width - 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) { // Dark overlay on filter-view handles the dimming now } 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); syncPanelCount(); 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); syncPanelCount(); 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 syncFilterSections() { const slotSection = document.getElementById('filter-slot-section'); if (!slotSection) return; slotSection.classList.toggle('hidden', !getActiveFilterConfig().showSlots); } function getActiveFilterCount() { const config = getActiveFilterConfig(); let count = localTags.length; if (config.showSlots) count += localSlots.length; if (localMinMinutes > PREP_TIME_MIN || localMaxMinutes < PREP_TIME_MAX) count += 1; return count; } function syncPanelCount() { const count = getActiveFilterCount(); const { buttonId } = getActiveFilterConfig(); const button = buttonId ? document.getElementById(buttonId) : null; if (button) { const highlight = isFilterPanelOpen() || count > 0; const isRecipeGlassButton = buttonId === 'recipe-filter-btn'; if (isRecipeGlassButton && !highlight) { button.style.removeProperty('background'); button.style.removeProperty('border-color'); button.style.removeProperty('color'); } else { button.style.setProperty('background', highlight ? 'rgba(var(--overlay-rgb), 0.12)' : 'rgb(var(--card-rgb))', 'important'); button.style.setProperty('border-color', highlight ? 'rgb(var(--border-input-rgb))' : 'rgb(var(--border-card-rgb))', 'important'); button.style.setProperty('color', highlight ? 'rgb(var(--text-emphasis-rgb))' : 'rgb(var(--text-body-rgb))', 'important'); } } const badge = button?.querySelector('[id$="-filter-count"]'); if (!badge) return; badge.textContent = String(count); badge.classList.toggle('hidden', count === 0); badge.classList.toggle('flex', count > 0); } function syncLiveFilters() { const config = getActiveFilterConfig(); config.applyState?.({ slots: config.showSlots ? localSlots : [], tags: localTags, minMinutes: localMinMinutes, maxMinutes: localMaxMinutes, }); syncPanelCount(); } 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 = (contextName = 'recipes') => { if (isFilterPanelOpen() && activeFilterContext === contextName) { window.closeFilters(); return; } 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, ); localMinMinutes = normalized.minMinutes; localMaxMinutes = normalized.maxMinutes; syncTimeRangeUI(); renderSlotChips(); renderTagChips(); syncFilterSections(); syncPanelCount(); showFilterPanel(); }; window.closeFilters = () => { stopTimeHandleDrag(); hideFilterPanel(); }; }