Этап 5: Избранное и финальная полировка
Free PreviewФинальный этап. Добавляем избранное, убеждаемся что приложение работает на любом экране, и полируем детали.
Требования
Избранное
- На каждой карточке — кнопка с иконкой сердечка для добавления в избранное.
- Клик по сердечку добавляет или убирает место из избранного. Кнопка меняет внешний вид в зависимости от состояния.
- Список избранных id сохраняется в
localStorageи восстанавливается при перезагрузке страницы. - В шапке — кнопка «Избранное», которая переключает режим: показывает только избранные карточки или возвращает полный список.
- Когда режим избранного включён и список пуст, показывайте соответствующее сообщение с призывом добавить понравившиеся места.
- В модальном окне тоже должна быть кнопка избранного — и она должна быть синхронизирована с кнопкой на карточке (добавил в модалке → иконка на карточке тоже изменилась).
Как хранить избранное. Держите массив id в
state.favouritesи зеркалируйте его вlocalStorage. Удобно вынести в отдельные функции:function toggleFavourite(id) { const index = state.favourites.indexOf(id); if (index === -1) { state.favourites.push(id); } else { state.favourites.splice(index, 1); } localStorage.setItem('favourites', JSON.stringify(state.favourites)); updateFavouriteButtons(id); } function isFavourite(id) { return state.favourites.includes(id); }jsПри загрузке страницы восстанавливайте из localStorage:
const saved = localStorage.getItem('favourites'); state.favourites = saved ? JSON.parse(saved) : [];js
Синхронизация кнопок на карточке и в модалке. После каждого изменения избранного нужно обновить все кнопки, связанные с этим id. Напишите функцию
updateFavouriteButtons(id), которая находит все элементы с нужнымdata-idи обновляет их внешний вид:function updateFavouriteButtons(id) { const isFav = isFavourite(id); // Кнопка на карточке const card = document.querySelector(`.card[data-id="${id}"]`); if (card) { card.querySelector('.card__fav-btn').classList.toggle('active', isFav); } // Кнопка в модалке (если открыта для этого же места) if (state.currentAttractionId === id) { modalFavBtn.classList.toggle('active', isFav); } }js
Чтобы клик по сердечку не открывал модалку. Обработчик делегирования на сетке откликается на любой клик внутри карточки. Если не остановить всплытие при клике на сердечко, сработают оба обработчика. Добавьте проверку:
grid.addEventListener('click', (e) => { if (e.target.closest('.card__fav-btn')) { const card = e.target.closest('.card'); toggleFavourite(Number(card.dataset.id)); return; // не открываем модалку } const card = e.target.closest('.card'); if (card) openModal(Number(card.dataset.id)); });js
Адаптивность
Проверьте приложение на следующих размерах экрана и исправьте всё, что выглядит плохо:
- 320px — самый узкий мобильный
- 375px — стандартный iPhone
- 768px — планшет
- 1280px и шире — десктоп
Типичные проблемы, на которые стоит обратить внимание: кнопки фильтров выходят за экран, модалка не помещается по высоте, поисковая строка слишком узкая, отступы слишком большие на мобильном.
Модалка на мобильном. На маленьких экранах модальное окно часто не помещается в высоту. Чтобы решить: сделайте
.modal__contentсmax-height: 90vh; overflow-y: auto— тогда при необходимости внутри появится прокрутка. На совсем узких экранах можно отображать модалку на весь экран:width: 100%; max-width: 100%; border-radius: 0.
Анимации
- Карточки появляются с анимацией при первой загрузке — через
@keyframesиanimation-delayна основе порядкового номера карточки. - Переход «добавить в избранное» — анимирован (хотя бы небольшой scale или смена цвета).
Последовательная анимация карточек. При создании карточки через JavaScript задайте задержку через
style:function createCard(attraction, index) { const card = document.createElement('article'); // ... card.style.animationDelay = `${index * 0.08}s`; return card; }jsВ CSS пропишите анимацию появления:
.card { animation: fadeUp 0.4s ease-out both; } @keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }css
Финальная проверка
Пройдитесь по всему приложению и убедитесь:
- в консоли браузера нет ошибок
- все интерактивные элементы работают как ожидается
- при быстром вводе в поиск ничего не ломается
- при медленном интернете (вкладка Network → дросселинг → Slow 3G) виден индикатор загрузки
- при отключённом интернете видно сообщение об ошибке
Продвинутые задания
Web Share API — добавьте кнопку «Поделиться» в модальное окно. Если браузер поддерживает Web Share API (navigator.share), используйте его. Если нет — скопируйте ссылку в буфер обмена (navigator.clipboard.writeText). Узнайте как это работает:
prefers-reduced-motion — некоторые пользователи настраивают систему так, чтобы приложения показывали меньше анимаций. Сделайте так, чтобы все ваши анимации отключались для таких пользователей:
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }css
Ожидаемый результат
- кнопка сердечка на карточке добавляет и убирает из избранного
- состояние избранного сохраняется после перезагрузки страницы
- кнопка «Избранное» в шапке переключает режим отображения
- при пустом избранном виден соответствующий экран
- кнопка избранного в модалке синхронизирована с карточкой
- приложение корректно выглядит на экранах от 320px до 1280px+
- в консоли нет ошибок
- карточки появляются с анимацией
Поздравляем — вы построили полноценное интерактивное веб-приложение с нуля. В нём есть загрузка данных, работа с DOM, обработка событий, фильтрация, модальное окно, сохранение состояния и адаптивный дизайн. Это и есть базовый фронтенд.
Дальше — бесконечное поле для роста: фреймворки (React, Vue), TypeScript, серверный рендеринг, тестирование. Но фундамент теперь у вас есть.