534 lines
21 KiB
JavaScript
534 lines
21 KiB
JavaScript
import { RECIPES } from '../data/catalog.js?v=8';
|
|
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)';
|
|
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)
|
|
.replace(/&/g, '&')
|
|
.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 `
|
|
<style id="filter-view-styles">
|
|
#filter-view.filter-open {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
#filter-view .prep-time-range-track,
|
|
#filter-view .prep-time-range-fill {
|
|
border-radius: 9999px;
|
|
height: 0.375rem;
|
|
}
|
|
|
|
#filter-view .prep-time-range-handle {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border-radius: 9999px;
|
|
border: 1px solid rgba(242,239,232,0.16);
|
|
background: ${FILTER_TRACK_FILL};
|
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.12);
|
|
touch-action: none;
|
|
outline: none;
|
|
}
|
|
</style>
|
|
<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;">
|
|
<div class="min-w-0 flex items-center justify-end gap-2">
|
|
<button id="filter-clear-btn" type="button" class="shrink-0 h-8 px-3 rounded-full border text-[11px] font-semibold transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;">Wyczyść</button>
|
|
</div>
|
|
</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 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>
|
|
|
|
<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};">Dieta i tagi</p>
|
|
<div id="filter-tag-chips" class="flex flex-wrap gap-2"></div>
|
|
</section>
|
|
|
|
<section class="p-3.5" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
|
<div class="flex items-center justify-between gap-3 mb-3">
|
|
<div class="min-w-0">
|
|
<p class="text-[10px] font-bold uppercase tracking-wider" style="color:${FILTER_TEXT_MUTED};">Czas przygotowania</p>
|
|
</div>
|
|
<span id="time-display-range" class="shrink-0 text-[11px] font-semibold tabular-nums text-right" style="color:${FILTER_TEXT_ACTIVE};">5 min - 120 min</span>
|
|
</div>
|
|
<div class="px-1">
|
|
<div class="relative h-9">
|
|
<div class="prep-time-range-track absolute inset-x-0 top-1/2 -translate-y-1/2" style="background:${FILTER_TRACK};" aria-hidden="true"></div>
|
|
<div id="prep-time-range-fill" class="prep-time-range-fill absolute top-1/2 -translate-y-1/2" style="background:${FILTER_TRACK_FILL}; left:0%; width:100%;" aria-hidden="true"></div>
|
|
<button
|
|
type="button"
|
|
id="prep-time-min-handle"
|
|
class="prep-time-range-handle absolute top-1/2 z-[2] -translate-x-1/2 -translate-y-1/2"
|
|
style="left:0%;"
|
|
data-time-handle="min"
|
|
role="slider"
|
|
aria-label="Minimalny czas przygotowania"
|
|
aria-valuemin="${PREP_TIME_MIN}"
|
|
aria-valuemax="${PREP_TIME_MAX}"
|
|
aria-valuenow="${PREP_TIME_MIN}"
|
|
aria-valuetext="${PREP_TIME_MIN} min"
|
|
></button>
|
|
<button
|
|
type="button"
|
|
id="prep-time-max-handle"
|
|
class="prep-time-range-handle absolute top-1/2 z-[3] -translate-x-1/2 -translate-y-1/2"
|
|
style="left:100%;"
|
|
data-time-handle="max"
|
|
role="slider"
|
|
aria-label="Maksymalny czas przygotowania"
|
|
aria-valuemin="${PREP_TIME_MIN}"
|
|
aria-valuemax="${PREP_TIME_MAX}"
|
|
aria-valuenow="${PREP_TIME_MAX}"
|
|
aria-valuetext="120 min"
|
|
></button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 : 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 { 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 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) {
|
|
// 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);
|
|
|
|
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 `<button type="button" data-filter-slot="${escapeHtml(slot.id)}" class="px-3 py-1.5 rounded-full border text-[12px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(slot.label)}</button>`;
|
|
}).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 `<button type="button" data-filter-tag="${escapeHtml(tag)}" class="px-3 py-1.5 rounded-full border text-[12px] font-semibold transition-colors" style="${getChipStyle(active)}">${escapeHtml(tag)}</button>`;
|
|
}).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 syncLiveFilters() {
|
|
const config = getActiveFilterConfig();
|
|
config.applyState?.({
|
|
slots: config.showSlots ? 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 = (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();
|
|
|
|
showFilterPanel();
|
|
};
|
|
|
|
window.closeFilters = () => {
|
|
stopTimeHandleDrag();
|
|
hideFilterPanel();
|
|
};
|
|
}
|