Redesign recipe list
This commit is contained in:
@@ -2,6 +2,24 @@ import { RECIPES } from '../data/catalog.js?v=2';
|
||||
import { MEAL_SLOTS } from '../planner/mealSlots.js';
|
||||
import { applyFilters, getFilterState, getFilteredCount, refreshRecipeList } from './RecipeList.js';
|
||||
|
||||
const FILTER_SHEET_TRANSITION = 'transform 300ms cubic-bezier(0.32,0.72,0,1)';
|
||||
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 = '#444442';
|
||||
const FILTER_TRACK_FILL = '#9b978f';
|
||||
const PREP_TIME_MIN = 5;
|
||||
const PREP_TIME_MAX = 120;
|
||||
const PREP_TIME_STEP = 5;
|
||||
const PREP_TIME_MIN_GAP = PREP_TIME_STEP;
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
@@ -20,38 +38,104 @@ function collectAllTags() {
|
||||
|
||||
export function getFilterHTML() {
|
||||
return `
|
||||
<div id="filter-view" class="absolute inset-0 bg-white z-50 hidden flex-col">
|
||||
<div class="p-4 border-b border-gray-200 flex items-center justify-between mt-4">
|
||||
<button id="filter-close-btn" class="w-10 h-10 flex items-center justify-center text-gray-600 hover:bg-gray-100 rounded-full transition-colors"><i class="fas fa-arrow-left text-lg"></i></button>
|
||||
<h2 class="text-lg font-semibold text-black">Filtry</h2>
|
||||
<button id="filter-clear-btn" class="px-2 text-sm font-medium text-gray-500 hover:text-black transition-colors">Wyczyść</button>
|
||||
</div>
|
||||
<style id="filter-view-styles">
|
||||
#filter-view.filter-open {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-black mb-4">Pora posiłku</h3>
|
||||
<div id="filter-slot-chips" class="flex flex-wrap gap-2.5"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-black mb-4">Dieta i tagi</h3>
|
||||
<div id="filter-tag-chips" class="flex flex-wrap gap-2.5"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-base font-semibold text-black">Maks. czas przygotowania</h3>
|
||||
<span id="time-display" class="text-sm font-medium text-gray-600">120 min</span>
|
||||
</div>
|
||||
<div class="px-1">
|
||||
<input type="range" id="prep-time-slider" min="5" max="120" step="5" value="120" class="w-full appearance-none bg-transparent">
|
||||
<div class="flex justify-between text-xs text-gray-400 mt-3 font-medium">
|
||||
<span>5 min</span><span>30 min</span><span>1 godz.</span><span>2 godz.+</span>
|
||||
#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: 2px solid ${FILTER_SURFACE};
|
||||
background: ${FILTER_TEXT_ACTIVE};
|
||||
box-shadow: 0 0 0 1px ${FILTER_BORDER_ACTIVE};
|
||||
touch-action: none;
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
<div id="filter-view" class="absolute inset-0 z-[55] hidden items-end opacity-0 transition-opacity duration-200" style="pointer-events:none; background:transparent !important; background-image:none !important;" aria-hidden="true">
|
||||
<div id="filter-sheet" class="relative w-full flex flex-col overflow-hidden rounded-t-3xl" style="background:${FILTER_SURFACE} !important; background-image:none !important; max-height:calc(100dvh - 4.5rem); transform:translateY(100%); transition:${FILTER_SHEET_TRANSITION}; box-shadow:0 -26px 46px rgba(0,0,0,0.28), 0 -6px 16px rgba(0,0,0,0.22);">
|
||||
<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-4 pt-3 pb-3 border-b" style="background:${FILTER_SURFACE} !important; background-image:none !important; border-color:${FILTER_BORDER} !important;">
|
||||
<div class="w-10 h-1 rounded-full mx-auto mb-3" style="background:rgba(109,108,103,0.75);" aria-hidden="true"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="filter-close-btn" type="button" class="shrink-0 w-9 h-9 rounded-full border flex items-center justify-center transition-colors" style="background:${FILTER_SURFACE_SOFT} !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_SECONDARY} !important;" aria-label="Zamknij filtry">
|
||||
<i class="fas fa-arrow-left text-[12px]"></i>
|
||||
</button>
|
||||
<div class="min-w-0 flex-1 pr-2">
|
||||
<h2 class="text-[15px] font-bold leading-tight" style="color:${FILTER_TEXT_PRIMARY};">Filtry</h2>
|
||||
<p class="text-[11px] mt-1" style="color:${FILTER_TEXT_MUTED};">Dopasuj listę przepisów do swoich potrzeb.</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="p-4 border-t border-gray-200 bg-white mt-auto">
|
||||
<button id="filter-apply-btn" class="w-full bg-gray-900 hover:bg-black text-white py-3.5 rounded-xl font-semibold shadow-sm transition-colors text-sm"></button>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto no-scrollbar px-4 py-4 space-y-4" style="background:${FILTER_SURFACE} !important; background-image:none !important;">
|
||||
<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 class="shrink-0 px-4 pt-2 pb-4" style="background:${FILTER_SURFACE} !important; background-image:none !important; padding-bottom:calc(1rem + env(safe-area-inset-bottom));">
|
||||
<button id="filter-apply-btn" type="button" class="w-full py-3 rounded-xl border font-semibold text-[13px] transition-colors flex items-center justify-center gap-2" style="background:${FILTER_SURFACE} !important; background-image:none !important; box-shadow:none !important; border-color:${FILTER_BORDER} !important; color:${FILTER_TEXT_ACTIVE} !important;">
|
||||
<i class="fas fa-check text-[11px]" style="color:${FILTER_TEXT_MUTED};" aria-hidden="true"></i>
|
||||
<span id="filter-apply-label">Pokaż wyniki</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -59,7 +143,121 @@ export function getFilterHTML() {
|
||||
|
||||
let localSlots = [];
|
||||
let localTags = [];
|
||||
let localMaxMinutes = 120;
|
||||
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 showFilterSheet() {
|
||||
const view = document.getElementById('filter-view');
|
||||
const sheet = document.getElementById('filter-sheet');
|
||||
if (!view || !sheet) return;
|
||||
|
||||
clearTimeout(closeTimer);
|
||||
view.classList.remove('hidden');
|
||||
view.classList.add('flex', 'filter-open');
|
||||
view.style.pointerEvents = 'auto';
|
||||
view.setAttribute('aria-hidden', 'false');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
view.classList.add('opacity-100');
|
||||
sheet.style.transform = 'translateY(0)';
|
||||
});
|
||||
}
|
||||
|
||||
function hideFilterSheet() {
|
||||
const view = document.getElementById('filter-view');
|
||||
const sheet = document.getElementById('filter-sheet');
|
||||
if (!view || !sheet) return;
|
||||
|
||||
view.classList.remove('opacity-100', 'filter-open');
|
||||
view.style.pointerEvents = 'none';
|
||||
view.setAttribute('aria-hidden', 'true');
|
||||
sheet.style.transform = 'translateY(100%)';
|
||||
|
||||
closeTimer = setTimeout(() => {
|
||||
view.classList.add('hidden');
|
||||
view.classList.remove('flex');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function renderSlotChips() {
|
||||
const wrap = document.getElementById('filter-slot-chips');
|
||||
@@ -67,10 +265,7 @@ function renderSlotChips() {
|
||||
|
||||
wrap.innerHTML = MEAL_SLOTS.map((slot) => {
|
||||
const active = localSlots.includes(slot.id);
|
||||
const cls = active
|
||||
? 'px-4 py-2 bg-gray-900 text-white rounded-full text-sm font-medium transition-colors'
|
||||
: 'px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors';
|
||||
return `<button type="button" data-filter-slot="${escapeHtml(slot.id)}" class="${cls}">${escapeHtml(slot.label)}</button>`;
|
||||
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) => {
|
||||
@@ -92,10 +287,7 @@ function renderTagChips() {
|
||||
const allTags = collectAllTags();
|
||||
wrap.innerHTML = allTags.map((tag) => {
|
||||
const active = localTags.includes(tag.toLowerCase());
|
||||
const cls = active
|
||||
? 'px-4 py-2 bg-gray-900 text-white rounded-full text-sm font-medium transition-colors'
|
||||
: 'px-4 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-full text-sm font-medium transition-colors';
|
||||
return `<button type="button" data-filter-tag="${escapeHtml(tag)}" class="${cls}">${escapeHtml(tag)}</button>`;
|
||||
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) => {
|
||||
@@ -111,31 +303,135 @@ function renderTagChips() {
|
||||
}
|
||||
|
||||
function updateResultCount() {
|
||||
applyFilters({ slots: localSlots, tags: localTags, maxMinutes: localMaxMinutes });
|
||||
applyFilters({
|
||||
slots: localSlots,
|
||||
tags: localTags,
|
||||
minMinutes: localMinMinutes,
|
||||
maxMinutes: localMaxMinutes,
|
||||
});
|
||||
const count = getFilteredCount();
|
||||
const applyBtn = document.getElementById('filter-apply-btn');
|
||||
if (applyBtn) applyBtn.textContent = `Pokaż ${count} wyników`;
|
||||
const applyBtnLabel = document.getElementById('filter-apply-label');
|
||||
if (applyBtnLabel) applyBtnLabel.textContent = `Pokaż ${count} wyników`;
|
||||
}
|
||||
|
||||
export function setupFilter() {
|
||||
const timeSlider = document.getElementById('prep-time-slider');
|
||||
const timeDisplay = document.getElementById('time-display');
|
||||
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;
|
||||
|
||||
if (timeSlider) {
|
||||
timeSlider.addEventListener('input', (e) => {
|
||||
const val = Number(e.target.value);
|
||||
localMaxMinutes = val;
|
||||
timeDisplay.textContent = val >= 120 ? 'ponad 120 min' : `${val} min`;
|
||||
updateResultCount();
|
||||
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();
|
||||
updateResultCount();
|
||||
}
|
||||
|
||||
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.target === filterView) window.closeFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-close-btn')?.addEventListener('click', () => {
|
||||
window.closeFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-apply-btn')?.addEventListener('click', () => {
|
||||
applyFilters({ slots: localSlots, tags: localTags, maxMinutes: localMaxMinutes });
|
||||
applyFilters({
|
||||
slots: localSlots,
|
||||
tags: localTags,
|
||||
minMinutes: localMinMinutes,
|
||||
maxMinutes: localMaxMinutes,
|
||||
});
|
||||
refreshRecipeList();
|
||||
window.closeFilters();
|
||||
});
|
||||
@@ -143,12 +439,17 @@ export function setupFilter() {
|
||||
document.getElementById('filter-clear-btn')?.addEventListener('click', () => {
|
||||
localSlots = [];
|
||||
localTags = [];
|
||||
localMaxMinutes = 120;
|
||||
if (timeSlider) timeSlider.value = '120';
|
||||
if (timeDisplay) timeDisplay.textContent = '120 min';
|
||||
localMinMinutes = PREP_TIME_MIN;
|
||||
localMaxMinutes = PREP_TIME_MAX;
|
||||
syncTimeRangeUI();
|
||||
renderSlotChips();
|
||||
renderTagChips();
|
||||
applyFilters({ slots: [], tags: [], maxMinutes: 120 });
|
||||
applyFilters({
|
||||
slots: [],
|
||||
tags: [],
|
||||
minMinutes: PREP_TIME_MIN,
|
||||
maxMinutes: PREP_TIME_MAX,
|
||||
});
|
||||
updateResultCount();
|
||||
});
|
||||
|
||||
@@ -156,23 +457,24 @@ export function setupFilter() {
|
||||
const state = getFilterState();
|
||||
localSlots = [...state.slots];
|
||||
localTags = [...state.tags];
|
||||
localMaxMinutes = state.maxMinutes;
|
||||
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;
|
||||
|
||||
if (timeSlider) timeSlider.value = String(localMaxMinutes);
|
||||
if (timeDisplay) timeDisplay.textContent = localMaxMinutes >= 120 ? 'ponad 120 min' : `${localMaxMinutes} min`;
|
||||
syncTimeRangeUI();
|
||||
|
||||
renderSlotChips();
|
||||
renderTagChips();
|
||||
updateResultCount();
|
||||
|
||||
const fv = document.getElementById('filter-view');
|
||||
fv.classList.remove('hidden');
|
||||
fv.classList.add('flex');
|
||||
showFilterSheet();
|
||||
};
|
||||
|
||||
window.closeFilters = () => {
|
||||
const fv = document.getElementById('filter-view');
|
||||
fv.classList.add('hidden');
|
||||
fv.classList.remove('flex');
|
||||
stopTimeHandleDrag();
|
||||
hideFilterSheet();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user