Неделя 4 · Shelter Part 3 — JavaScript Functionality

🧭 ← P2 · Следующая → JS30 ·

🎯 Что строим

Берёшь свой Shelter из P2 и оживляешь на чистом vanilla JS (никаких фреймворков). Четыре фичи:

  1. Бургер-меню на mobile/tablet (<768px) — открывается, оверлей, scroll-lock, transform-иконка
  2. Бесконечный карусель в Our Friends на Main (3/2/1 карточки по breakpoint'ам, без повторов между группами, рандом внутри)
  3. Пагинация в Our Friends на Pets — 48 карточек (6×8 / 8×6 / 16×3), без соседних дублей, контролы first/prev/next/last
  4. Popup с деталями pet'а — клик по карточке → modal с dark overlay + scroll-lock

Данные карточек берутся из pets.json (8 питомцев), готовый файл лежит в task folder.

📄 Полное задание Part 3 →

🏷 Required Skills

vanilla JavaScript · DOM manipulation · event handling · event delegation · JSON · fetch · infinite carousel · pagination · modal/popup · CSS animations

🚫 Запреты + штрафы

Что нельзя Штраф
🔴 Любой фреймворк или библиотека (jQuery, React, Vue, Bootstrap, Swiper и т.п.) -120
🔴 TypeScript -120
🔴 Данные карточек захардкожены в HTML/JS вместо pets.json -30

💡 Разрешено: только нативные браузерные API, твой собственный JS. Если хочется удобный селектор — пиши свой однострочник, не тащи jQuery.

⛰ Что унаследовано из P1+P2 (compound)

Из Shelter P1 ты уже умеешь:

  • Семантический HTML + BEM-разметка
  • Pixel-perfect Figma → код
  • Flexbox + Grid layout

Из Shelter P2:

  • Mobile-first CSS + media queries на 3 breakpoint'ах (320/768/1280)
  • Бургер-иконка (без логики) уже стоит в шапке на <768px
  • CSS transitions/animations (готовые keyframes)

Эта задача добавляет → JS интерактив: DOM, события, fetch JSON, делегирование, scroll-lock, алгоритм рандомизации без повторов.


📚 Что изучить (по порядку)

⚠️ Идти строго по порядку. Сначала фундамент языка → DOM → события → fetch → потом каждую фичу.

📥 Что должен знать ДО старта

Если JavaScript вообще не трогал — сначала пройди:


1 · Переменные, область видимости, hoisting

Зачем: базовый словарь. Без let/const/scope не поймёшь почему обработчик внутри цикла «видит не то».

Self-check: что выведет for (var i = 0; i < 3; i++) setTimeout( => console.log(i), 0)? Почему? Как починить через let?


2 · Функции и контекст

Зачем: обработчик клика по карточке должен достать id именно той карточки. Стрелочная или обычная функция? — от этого зависит this.

Self-check: почему element.addEventListener('click', () => this.close()) иногда работает, а function{ this.close() } — нет?


3 · Объекты, массивы, итерация

Зачем: pets.json — это массив объектов. Будешь его фильтровать, мапить, рандомизировать, доставать по name.

Self-check: напиши однострочник: «вернуть случайный элемент массива». Чем arr.includes(x) отличается от arr.indexOf(x) !== -1?


4 · DOM — поиск и навигация ⭐

Зачем: базовая операция — «найти кнопку бургера», «найти все карточки», «найти ближайшего родителя карточки от клика».

Self-check: чем querySelectorAll отличается от getElementsByClassName (живая vs статическая)? Зачем closest('.card') в popup?


5 · DOM — манипуляция

Зачем: создавать карточки, обновлять текст popup'а, добавлять/убирать классы.

Self-check: textContent vs innerHTML — что безопаснее и почему? Когда нужен DocumentFragment?


6 · События ⭐ (особенно делегирование)

Зачем: ставить 48 обработчиков на 48 карточек — антипаттерн. Один обработчик на контейнер + event.target.closest('.card') = делегирование.

Self-check: почему event.targetevent.currentTarget? Как одним обработчиком на <ul> обработать клик по любому <li>?


7 · Асинхронность и загрузка JSON

Зачем: требование -30: данные ТОЛЬКО из pets.json. Значит fetch + await response.json.

Шаблон загрузки:

async function loadPets() {
  const res = await fetch('./pets.json');
  if (!res.ok) throw new Error('pets.json not loaded');
  return res.json();
}

Подводный камень: на gh-pages путь к pets.json относительный — ./pets.json, не /pets.json (иначе на subpath 404).

Self-check: что вернёт fetch если файл не найден — reject или resolve со статусом 404? Почему нужен res.ok?


8 · Фича 1 — Бургер-меню

Зачем: самая простая. Тренируешь связку «событие → классы → animation».

Что соберёшь сам:

  • Клик на бургер → body.classList.add('menu-open') (всё стилирование — через CSS, JS только переключает класс)
  • Клик на overlay / close / любая <a> внутри меню → убрать класс
  • Scroll-lock: body.style.overflow = 'hidden' при открытии, очистить при закрытии

Нюанс scroll-lock: простой overflow: hidden на iOS Safari не работает идеально. Минимальный приемлемый вариант — overflow + сохранить позицию. Глубокий разбор — в body-scroll-lock (НЕ тащить либу, понять идею).

Self-check: что произойдёт если убрать overflow: hidden с body но оставить меню открытым на mobile?


9 · Фича 2 — Бесконечный карусель ⭐ (самая сложная)

Зачем: правила «никакой питомец из текущей группы не появляется в следующей» + «внутри следующей группы все уникальны» + «рандом» = маленький, но настоящий алгоритм.

Алгоритм next-group (sketch):

  1. groupSize = 3 / 2 / 1 в зависимости от viewport (определяешь через matchMedia или window.innerWidth)
  2. available = allPets.filter(p => !currentGroup.includes(p))
  3. shuffle available, взять первые groupSize
  4. показать → запустить CSS-transition (translateX)
  5. lock во время анимации (флаг isAnimating = true), на конец transitionend снять lock

Wikilinks:

Подводный камень — повторные клики: если просто игнорить клик пока isAnimating, юзер не сможет «накликать вперёд». Решение из задачи — именно игнорить (+5 баллов). Не очередь.

Self-check: Fisher-Yates shuffle — как работает, почему for (let i = n-1; i > 0; i--) а не наоборот?


10 · Фича 3 — Пагинация (Pets)

Зачем: 48 карточек, равное число повторов (по 6 каждого из 8 питомцев), без соседних дублей. Это задача распределения.

Алгоритм генерации 48 (sketch):

  1. Сделать массив [pet0×6, pet1×6, …, pet7×6] = 48 элементов
  2. Перемешать с условием «соседи не совпадают» (Fisher-Yates + проверка, retry при коллизии)
  3. Сохранить порядок на всю сессию (генерировать один раз)
  4. На клик «следующая страница» — показать срез slice(pageStart, pageEnd)
  5. Контролы: first / prev / current / next / last
  6. Disabled-состояния: на первой странице prev+first disabled, на последней — next+last
  7. Анимация переключения страницы — fade или slide через CSS-классы

Wikilinks:

Подводный камень — disabled: не забудь визуально + логически. aria-disabled="true" + pointer-events: none + проверка в обработчике (if (btn.dataset.disabled) return).

Self-check: на 6×8 (mobile) показываешь 3 карточки на страницу — почему именно 16 страниц, а не 15 или 17?


11 · Фича 4 — Popup

Зачем: клик по карточке → модалка с деталями. Делегирование + dataset + scroll-lock — всё что уже знаешь.

Структура:

  • Один <dialog> или <div class="popup"> в DOM, hidden по умолчанию
  • Клик по карточке → найти data-pet-id через closest('.card') → найти питомца в массиве → заполнить popup → показать
  • Закрытие: клик на close, клик на overlay (НЕ на сам popup — event.target === overlay), Escape

Wikilinks:

Self-check: почему клик внутри popup не должен пробрасываться на overlay? Что лучше — event.target === overlay или stopPropagation на popup?


12 · Архитектура vanilla-JS кода

Зачем: без фреймворка легко скатиться в спагетти. Простое правило — одна фича = один файл-модуль (или одна IIFE/функция-инициализатор).

Рекомендуемая структура:

shelter-part3/
  pets.json
  index.html, pets.html
  styles/...
  scripts/
    main.js          ← entry, dispatch по странице
    burger.js
    carousel.js      ← только на Main
    pagination.js    ← только на Pets
    popup.js         ← общий
    api.js           ← fetch('./pets.json')
    utils.js         ← shuffle, randomFrom

Self-check: зачем defer у <script>? Что произойдёт если поставить <script src="main.js"> без defer в <head>?


13 · Workflow

⚠️ Branching: Part 3 идёт от ветки shelter (куда смержена P2), а не от main. PR делаешь обратно в shelter и мержишь.


✅ Чек-лист критериев (120 баллов)

Burger menu · 25

  • Меню открывается по клику на бургер (+5)
  • Открытие со smooth animation (+5)
  • Бургер-иконка превращается в close-иконку (+5)
  • Меню закрывается: клик на close / клик на overlay / клик на любую ссылку (+5)
  • Page behind меню не скроллится пока меню открыто (+5)
  • Правильное число карточек на breakpoint: 3 / 2 / 1 (+5)
  • Работающие left/right arrows (+5)
  • Следующая группа НЕ содержит ни одного pet из текущей (+10)
  • Все pets в новой группе уникальны (+5)
  • Порядок в новой группе — случайный (в рамках правил) (+5)
  • Переключение анимировано (slide) (+5)
  • Повторные клики во время анимации игнорируются — не стакаются (+5)

Pagination (Pets) · 40

  • 48 карточек: 6×8 desktop / 8×6 tablet / 16×3 mobile (+10)
  • Сгенерированы из pets.json, все pets встречаются равное число раз (+5)
  • Никакие два соседних card (в линейном порядке) не показывают одного pet (+5)
  • Контролы: first / prev / current / next / last — все есть (+5)
  • Disabled-контролы визуально неактивны и не реагируют на клик (+5)
  • Переключение страниц анимировано (+10)
  • Клик по карточке открывает popup с деталями из pets.json (+5)
  • Dark backdrop поверх страницы, закрытие по close или клику на backdrop (+5)
  • Page behind popup не скроллится (+5)

Штрафы (помни)

  • Фреймворк/либа → -120
  • TypeScript → -120
  • Hardcode данных вместо pets.json-30

🧠 Self-check перед коммитом

Не нажимай git push, пока не сможешь ответить:

  1. Зачем closest('.card') вместо event.target в обработчике клика по карточке?
  2. Где у меня делегирование, а где per-element listener? Почему именно так?
  3. Что произойдёт если юзер кликнет «next» во время анимации карусели? Игнорируется или копится?
  4. Загружаю ли я pets.json через fetch (а не вшил его в <script>)? Путь относительный (./pets.json)?
  5. На пагинации (Pets) могут ли два соседних card быть одинаковыми? Если да — алгоритм поломан.
  6. Scroll-lock работает на открытом меню и popup? Когда закрываю — body снова скроллится?
  7. У меня PR из shelter-part3 в shelter (НЕ в main), и я его смержил?
  8. Я тестировал на 320 / 768 / 1280? Карусель показывает 1 / 2 / 3 карточки соответственно?

➡️ Что переходит в следующие задачи (compound forward)

Шаблоны и паттерны отсюда живут долго:

  • В JS30 — там 30 мини-проектов в чистом JS, прямой апгрейд DOM/событий из этой задачи. Делегирование и dataset будут везде.
  • В Podcast Playerfetch + JSON + <audio> events. Архитектура vanilla-модулей переиспользуется.
  • В NFC App — Web NFC API, поверх той же event-driven архитектуры.
  • В Async Raceкарусель → гонка машинок. requestAnimationFrame + анимация transform: translateX + лок во время анимации — те же концепты.
  • В любом React/Angular проекте дальше — ты уже понимаешь что фреймворк делает за тебя (диффинг DOM, делегирование, scoped state).

📚 Внешние ресурсы

🎓 Видео-разборы (опционально)

Для Shelter P3 в индексах AsForJS прямых лекций «как делать карусель» нет — это инженерия. Но фундамент языка можно подкрепить: