Этап 3: Фильтрация и поиск

Free Preview
Продолжительность:

Карточки загружаются и отображаются. Теперь добавим возможность искать и фильтровать — пользователь должен быстро найти то, что ищет.

Требования

Фильтрация по категории

  • Клик по кнопке категории показывает только карточки с соответствующей категорией.
  • Кнопка «Все» сбрасывает фильтр и показывает все карточки.
  • Активная кнопка визуально выделена (класс, который вы уже стилизовали в предыдущем этапе).
  • В один момент времени активна только одна кнопка категории.

Как переключать активную кнопку. Снимите класс у всех кнопок, затем добавьте нужной:

document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
clickedButton.classList.add('active');
js

Поиск по тексту

  • Ввод текста в строку поиска фильтрует карточки по полям name и description.
  • Поиск нечувствителен к регистру.
  • Поиск работает одновременно с фильтром категории: если выбрана категория «Музей» и введён текст «эрм», показываются только музеи, в названии или описании которых есть «эрм».
  • При очистке поля поиска карточки возвращаются к предыдущему состоянию (с учётом активного фильтра категории).

Состояние «ничего не найдено»

  • Если ни одна карточка не подходит под текущие фильтры, в сетке должно отображаться сообщение «Ничего не найдено» с подсказкой сбросить поиск.
  • Рядом с сообщением — кнопка «Сбросить», которая очищает поле поиска и снимает фильтр категории.

Архитектура

  • Не удаляйте и не добавляйте карточки при каждом изменении фильтра. Лучший подход — держать все карточки в DOM и управлять их видимостью через CSS. Это быстрее и проще.
  • Вся логика фильтрации — одна функция, которая вызывается и при смене категории, и при вводе в поиск. Не дублируйте код.

Как связать всё вместе. Центральная идея — одна функция applyFilters, которая читает текущее состояние и решает, какие карточки показать:

function applyFilters() {
  const query = state.searchQuery.toLowerCase();
  const category = state.activeCategory;

  document.querySelectorAll('.card').forEach(card => {
    const id = Number(card.dataset.id);
    const attraction = state.attractions.find(a => a.id === id);

    const matchesCategory = category === 'Все' || attraction.category === category;
    const matchesSearch = !query
      || attraction.name.toLowerCase().includes(query)
      || attraction.description.toLowerCase().includes(query);

    card.hidden = !(matchesCategory && matchesSearch);
  });

  // показать/скрыть состояние "ничего не найдено"
  const anyVisible = [...document.querySelectorAll('.card')].some(c => !c.hidden);
  emptyState.hidden = anyVisible;
}
js

Обработчики событий только обновляют state и вызывают applyFilters():

searchInput.addEventListener('input', (e) => {
  state.searchQuery = e.target.value;
  applyFilters();
});

filtersContainer.addEventListener('click', (e) => {
  const btn = e.target.closest('.filter-btn');
  if (!btn) return;
  state.activeCategory = btn.dataset.category;
  // обновить активную кнопку
  applyFilters();
});
js

Обратите внимание на btn.dataset.category — при создании кнопок фильтрации добавляйте атрибут data-category="Музей", чтобы потом легко его читать.

Продвинутое задание — debounce

Поиск запускается при каждом нажатии клавиши. Если данных много, это может тормозить браузер. Реальное решение — debounce: функция-обёртка, которая откладывает выполнение до тех пор, пока пользователь не перестанет печатать (обычно 300–400 мс).

Реализуйте debounce самостоятельно — это небольшая функция, которую полезно уметь писать:

Подсказка. debounce принимает функцию и задержку, возвращает новую функцию. Каждый вызов новой функции сбрасывает предыдущий таймер и запускает новый. Выполнение происходит только когда таймер «добежал» до конца.

function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Использование:
searchInput.addEventListener('input', debounce((e) => {
  state.searchQuery = e.target.value;
  applyFilters();
}, 300));
js

Ожидаемый результат

  • клик по категории показывает только карточки этой категории
  • поиск работает по имени и описанию, нечувствителен к регистру
  • поиск и фильтр по категории работают одновременно
  • при отсутствии результатов видно сообщение и кнопка сброса
  • активная кнопка категории визуально выделена