Неделя 4 · Shelter Part 3 — JavaScript Functionality
🧭 ← P2 · Следующая → JS30 ·
🎯 Что строим
Берёшь свой Shelter из P2 и оживляешь на чистом vanilla JS (никаких фреймворков). Четыре фичи:
- Бургер-меню на mobile/tablet (<768px) — открывается, оверлей, scroll-lock, transform-иконка
- Бесконечный карусель в
Our FriendsнаMain(3/2/1 карточки по breakpoint'ам, без повторов между группами, рандом внутри) - Пагинация в
Our FriendsнаPets— 48 карточек (6×8 / 8×6 / 16×3), без соседних дублей, контролы first/prev/next/last - Popup с деталями pet'а — клик по карточке → modal с dark overlay + scroll-lock
Данные карточек берутся из pets.json (8 питомцев), готовый файл лежит в task folder.
🏷 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 вообще не трогал — сначала пройди:
- Введение в JavaScript · Подключение скриптов -- script тег (
deferобязательно!) - Типы данных · null и undefined · Boolean · Number · String
- Условия · Условные конструкции -- if, else, switch
- Операторы · JS операторы
- Циклы · JS циклы · break и continue
- Отладка -- console, debugger, breakpoints
1 · Переменные, область видимости, hoisting
Зачем: базовый словарь. Без let/const/scope не поймёшь почему обработчик внутри цикла «видит не то».
- Блочная область видимости -- let и const
- Scope · Hoisting -- поднятие объявлений
- Замыкания (Closures) · Замыкание в цикле -- распространённая ошибка ← реально стрельнёт в пагинации
Self-check: что выведет for (var i = 0; i < 3; i++) setTimeout( => console.log(i), 0)? Почему? Как починить через let?
2 · Функции и контекст
Зачем: обработчик клика по карточке должен достать id именно той карточки. Стрелочная или обычная функция? — от этого зависит this.
- JS функции основы · _MOC Функции
- Function Declaration · Function Expression · Arrow Function
- Параметры и аргументы · Значения по умолчанию · Оператор return
- Callback · Higher-Order Functions
- this · this в обычных и стрелочных функциях · Потеря контекста -- типичные случаи
Self-check: почему element.addEventListener('click', () => this.close()) иногда работает, а function{ this.close() } — нет?
3 · Объекты, массивы, итерация
Зачем: pets.json — это массив объектов. Будешь его фильтровать, мапить, рандомизировать, доставать по name.
- JS объекты основы · Объекты · Сравнение объектов -- ссылки vs значения
- JS массивы основы · Массивы
- Методы массивов -- map, filter, reduce
- Методы массивов -- find, some, every, includes ←
includesнужен для проверки «нет в предыдущей группе» - Методы массивов -- push, pop, shift, unshift
- Spread и Rest · Деструктуризация · Деструктуризация объектов · Деструктуризация массивов
- Числа -- Number, Math, parseInt, parseFloat ←
Math.random,Math.floorдля shuffle
Self-check: напиши однострочник: «вернуть случайный элемент массива». Чем arr.includes(x) отличается от arr.indexOf(x) !== -1?
4 · DOM — поиск и навигация ⭐
Зачем: базовая операция — «найти кнопку бургера», «найти все карточки», «найти ближайшего родителя карточки от клика».
- _MOC DOM — карта темы
- DOM vs HTML -- в чём разница · DOM дерево · DOM-дерево -- узлы и типы узлов
- document и window
- Поиск элементов · DOM поиск элементов
- getElementById, getElementsByClassName · querySelectorAll и NodeList
- Навигация по DOM · parentElement, children, siblings · firstChild, lastChild, nextSibling
- closest -- поиск родителя ← критично для popup (клик внутри карточки → найти саму карточку)
- Разница между childNodes и children
Self-check: чем querySelectorAll отличается от getElementsByClassName (живая vs статическая)? Зачем closest('.card') в popup?
5 · DOM — манипуляция
Зачем: создавать карточки, обновлять текст popup'а, добавлять/убирать классы.
- Манипуляция DOM · DOM создание и изменение элементов
- createElement и createTextNode · append, prepend, before, after
- insertAdjacentHTML · textContent, innerHTML, innerText
- classList -- add, remove, toggle, contains ←
toggle('is-open')для бургера - data-атрибуты (dataset) ←
data-pet-idдля popup - Атрибуты -- getAttribute, setAttribute · DOM атрибуты и свойства
- style -- инлайн-стили через JS · DOM стили и размеры
- DocumentFragment -- оптимизация · Batch DOM updates ← для 48 карточек пагинации
- cloneNode -- клонирование
Self-check: textContent vs innerHTML — что безопаснее и почему? Когда нужен DocumentFragment?
6 · События ⭐ (особенно делегирование)
Зачем: ставить 48 обработчиков на 48 карточек — антипаттерн. Один обработчик на контейнер + event.target.closest('.card') = делегирование.
- События · События мыши -- click, mouseover, mouseenter
- События загрузки -- DOMContentLoaded, load
- Всплытие и погружение ← основа делегирования
- Делегирование событий ⭐ · Рецепт -- делегирование событий
- preventDefault и stopPropagation ← клик на ссылке внутри меню =
preventDefaultдля smooth scroll - DOM события клавиатуры и мыши
- Событие scroll
- События клавиатуры -- keydown, keyup ← опционально: закрытие popup по
Escape
Self-check: почему event.target ≠ event.currentTarget? Как одним обработчиком на <ul> обработать клик по любому <li>?
7 · Асинхронность и загрузка JSON
Зачем: требование -30: данные ТОЛЬКО из pets.json. Значит fetch + await response.json.
- Event Loop · setTimeout и setInterval (для debounce анимаций)
- Promise · async-await
- Fetch API · Fetch -- headers, mode, credentials
- JSON · Работа с формами через JS (для popup-управления, опционально)
- Обработка ошибок в async коде
- Рецепт -- Fetch обёртка с обработкой ошибок
Шаблон загрузки:
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 (НЕ тащить либу, понять идею).
- Делегирование событий (один обработчик на
navловит клики по любой ссылке) - classList -- add, remove, toggle, contains
Self-check: что произойдёт если убрать overflow: hidden с body но оставить меню открытым на mobile?
9 · Фича 2 — Бесконечный карусель ⭐ (самая сложная)
Зачем: правила «никакой питомец из текущей группы не появляется в следующей» + «внутри следующей группы все уникальны» + «рандом» = маленький, но настоящий алгоритм.
Алгоритм next-group (sketch):
groupSize= 3 / 2 / 1 в зависимости от viewport (определяешь черезmatchMediaилиwindow.innerWidth)available = allPets.filter(p => !currentGroup.includes(p))- shuffle
available, взять первыеgroupSize - показать → запустить CSS-transition (translateX)
- lock во время анимации (флаг
isAnimating = true), на конецtransitionendснять lock
Wikilinks:
- Методы массивов -- find, some, every, includes · Методы массивов -- map, filter, reduce
- Числа -- Number, Math, parseInt, parseFloat (
Math.randomдля shuffle) - ResizeObserver -- изменение размера /
matchMediaдля перевыбораgroupSize - requestAnimationFrame (если анимируешь руками, а не через CSS)
- Debounce и Throttle для DOM-событий ← опционально для resize
Подводный камень — повторные клики: если просто игнорить клик пока isAnimating, юзер не сможет «накликать вперёд». Решение из задачи — именно игнорить (+5 баллов). Не очередь.
Self-check: Fisher-Yates shuffle — как работает, почему for (let i = n-1; i > 0; i--) а не наоборот?
10 · Фича 3 — Пагинация (Pets)
Зачем: 48 карточек, равное число повторов (по 6 каждого из 8 питомцев), без соседних дублей. Это задача распределения.
Алгоритм генерации 48 (sketch):
- Сделать массив
[pet0×6, pet1×6, …, pet7×6]= 48 элементов - Перемешать с условием «соседи не совпадают» (Fisher-Yates + проверка, retry при коллизии)
- Сохранить порядок на всю сессию (генерировать один раз)
- На клик «следующая страница» — показать срез
slice(pageStart, pageEnd) - Контролы: first / prev / current / next / last
- Disabled-состояния: на первой странице prev+first disabled, на последней — next+last
- Анимация переключения страницы — fade или slide через CSS-классы
Wikilinks:
- Методы массивов -- map, filter, reduce · Методы массивов -- push, pop, shift, unshift
- Делегирование событий (один обработчик на блок пагинации ловит клики по 5 кнопкам)
- DocumentFragment -- оптимизация (рендер 8 карточек за раз — один batch)
- data-атрибуты (dataset) (
data-action="next"/data-action="first")
Подводный камень — 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:
- dialog (HTML5 нативный — простой путь, но имеет нюансы стилизации)
- Popup модальное окно ⭐ ← готовый рецепт
- Делегирование событий · closest -- поиск родителя · data-атрибуты (dataset)
- preventDefault и stopPropagation (клик внутри popup не должен закрывать его —
stopPropagation) - События клавиатуры -- keydown, keyup (Escape)
Self-check: почему клик внутри popup не должен пробрасываться на overlay? Что лучше — event.target === overlay или stopPropagation на popup?
12 · Архитектура vanilla-JS кода
Зачем: без фреймворка легко скатиться в спагетти. Простое правило — одна фича = один файл-модуль (или одна IIFE/функция-инициализатор).
- Подключение скриптов -- script тег —
<script type="module" defer src="...">чтобы можно было импортировать - Module Pattern · Revealing Module Pattern (если без ES-модулей)
- IIFE (изоляция scope, классика vanilla)
- Namespace Pattern (один глобальный объект
App.burger,App.carousel...)
Рекомендуемая структура:
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
- Repo workflow — ветка
shelter-part3←shelter(не от main!) - gh-pages деплой — деплой в подпапку
shelter/ - Commit convention —
feat:/fix:+ timestamp - PR description — список фич + breakpoint'ы, на которых проверял
- Cross-check
⚠️ Branching: Part 3 идёт от ветки
shelter(куда смержена P2), а не отmain. PR делаешь обратно вshelterи мержишь.
✅ Чек-лист критериев (120 баллов)
Burger menu · 25
- Меню открывается по клику на бургер (
+5) - Открытие со smooth animation (
+5) - Бургер-иконка превращается в close-иконку (
+5) - Меню закрывается: клик на close / клик на overlay / клик на любую ссылку (
+5) - Page behind меню не скроллится пока меню открыто (
+5)
Infinite carousel (Main) · 40
- Правильное число карточек на 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 · 15
- Клик по карточке открывает popup с деталями из
pets.json(+5) - Dark backdrop поверх страницы, закрытие по close или клику на backdrop (
+5) - Page behind popup не скроллится (
+5)
Штрафы (помни)
- Фреймворк/либа →
-120 - TypeScript →
-120 - Hardcode данных вместо
pets.json→-30
🧠 Self-check перед коммитом
Не нажимай git push, пока не сможешь ответить:
- Зачем
closest('.card')вместоevent.targetв обработчике клика по карточке? - Где у меня делегирование, а где per-element listener? Почему именно так?
- Что произойдёт если юзер кликнет «next» во время анимации карусели? Игнорируется или копится?
- Загружаю ли я
pets.jsonчерезfetch(а не вшил его в<script>)? Путь относительный (./pets.json)? - На пагинации (Pets) могут ли два соседних card быть одинаковыми? Если да — алгоритм поломан.
- Scroll-lock работает на открытом меню и popup? Когда закрываю —
bodyснова скроллится? - У меня PR из
shelter-part3вshelter(НЕ вmain), и я его смержил? - Я тестировал на 320 / 768 / 1280? Карусель показывает 1 / 2 / 3 карточки соответственно?
➡️ Что переходит в следующие задачи (compound forward)
Шаблоны и паттерны отсюда живут долго:
- В JS30 — там 30 мини-проектов в чистом JS, прямой апгрейд DOM/событий из этой задачи. Делегирование и
datasetбудут везде. - В Podcast Player —
fetch + JSON+<audio>events. Архитектура vanilla-модулей переиспользуется. - В NFC App — Web NFC API, поверх той же event-driven архитектуры.
- В Async Race — карусель → гонка машинок.
requestAnimationFrame+ анимацияtransform: translateX+ лок во время анимации — те же концепты. - В любом React/Angular проекте дальше — ты уже понимаешь что фреймворк делает за тебя (диффинг DOM, делегирование, scoped state).
📚 Внешние ресурсы
- 📄 Полное задание Part 3
- JavaScript DOM — MDN
- Introduction to Events — MDN
- Event delegation — JavaScript.info
- Working with JSON — MDN
fetch— MDN<dialog>— MDN- Building a Modal Dialog — web.dev
- Disabling Body Scroll — CSS-Tricks
- Fisher-Yates Shuffle — Wikipedia
- matchMedia API — MDN
🎓 Видео-разборы (опционально)
Для Shelter P3 в индексах AsForJS прямых лекций «как делать карусель» нет — это инженерия. Но фундамент языка можно подкрепить:
- Hoisting согласно официальной спецификации JS (AsForJS,
spec03) - Let и Const диссиденты в языке JS (AsForJS,
razbor15) - How
thisworks in JavaScript (AsForJS, RU)