Недели 11-14 · Async Race · ФИНАЛ

🧭 ← HTML Builder · ← Roadmap · 🏁 ФИНАЛ bootcamp — следующего таска нет

⚠️ Это финал bootcamp. Здесь сходится всё что ты делал 10 недель: вёрстка из Slider, Figma из Shelter, JS-логика из Shelter P3, типизация и архитектура из последующих тасок. Без этого compound — задача неподъёмная. С compound — закроешь за 4 недели на 600/600.

🎯 Что строим

Single Page Application для дрэг-рейсинга радиоуправляемых тачек. Два View — Garage (CRUD тачек + анимация гонки) и Winners (таблица победителей с сортировкой). Backend — готовый mock-сервер (клонируется отдельно), фронт — твой, без единого фреймворка.

Идея: клиент хочет устроить соревнование между своими радиоуправляемыми машинами. Каждый «контроллер» — это HTTP API: start engine, stop engine, drive. Твоя задача — обернуть это в UI: создавать машины, выбирать им цвет, генерить пачками по 100 штук, запускать гонку, показывать победителя, вести таблицу winners с пагинацией и сортировкой.

Стек жёстко зафиксирован:

  • TypeScript (без any, без as, без ! — -100 при нарушении)
  • Vanilla SPA — только нативный DOM API (Bootstrap CSS опционально, любые JS-фреймворки = -600)
  • Vite / Webpack — обязателен бандлер
  • ESLint unicorn config — обязателен
  • Функции ≤ 40 строк — обязательно

📄 Полное задание на GitHub → · 🎥 Demo video → · 🔧 Server mock →

🏷 Required Skills (как заявлено в задании)

TypeScript · Single Page Application · vanilla DOM rendering · Fetch API · async/await · REST · CSS animations · requestAnimationFrame · pagination · sorting · state management · Vite/Webpack

🚫 Запреты (нарушишь — баллы улетают)

Что нельзя Что вместо этого Штраф
🔴 React / Vue / Angular / jQuery / Lodash / Material UI / любая UI-либа vanilla DOM API + свои модули -600 (всё обнуляется)
🔴 any (явный или неявный) точные типы, unknown + narrowing -100
🔴 Type assertions (foo as Bar) type guards, valid types -100
🔴 Non-null assertions (y!) проверка на null/undefined -100
🔴 Функции > 40 строк декомпозиция -40
🔴 Не проходит eslint-unicorn пройти линтер -40
🟡 Magic numbers / strings вынеси в const по линтеру
🟡 Смешать API, UI и state в одном файле модульная архитектура по ревью
🟢 Bootstrap CSS — разрешён для стилизации
🟢 CSS Modules / Sass / Less / PostCSS / Tailwind / clsx — разрешены

💡 «Flaky requests» — когда юзер спамит Start/Stop кнопки, mock-сервер иногда отвечает 404 / 429. По заданию это не баг. Не тратьте время на обработку.

💡 Bug penalties: major bug (фича работает но ломается после манипуляций + ошибки в консоли) = -30. Minor bug (ломается без ошибок в консоли) = -10. Кросс-чек смотрит ВСЁ.


⛰ Compound из всего bootcamp

Это последний таск — он не учит новому в одиночку, он синтезирует. Каждая прошлая неделя дала кусок, который тут собирается:

Откуда Что переиспользуешь
Slider transform: translateX, transition, относительные единицы, mobile-адаптив 500px
Shelter P1 Семантика, BEM, Figma-разбор макета (если будешь делать кастомный UI)
Shelter P2 Mobile-first, breakpoints, responsive layout для гонки на 500px
Shelter P3 DOM API, события, рендеринг карточек, pagination логика
Codejam / последующие Командная работа, code review (понадобится для cross-check)
HTML Builder (прошлая неделя) Декларативный API для построения DOM-деревьев, factory-функции, type-safe тегирование

Без compound тут невозможно. Если на любой из прошлых задач ты остановился — вернись и закрой. Async Race раскроется только если в голове сложилась полная картина «как vanilla-фронт превращается в работающее SPA».


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

⚠️ Финал — иди жёстко по порядку. Это не Slider где можно угадать. Здесь 12 блоков, каждый закрывает один из 600 баллов задачи. Пропустишь типизацию — словишь -100. Пропустишь модульную архитектуру — словишь штрафы за длинные функции и magic numbers.

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

Если хоть один блок ниже не пройден — назад к Slider/Shelter:

Не открыл хоть одно — закрой. Иначе застрянешь.


1 · Compound: HTML Builder → Async Race

Зачем: в HTML Builder ты сделал декларативный API для построения DOM. В Async Race он становится твоим «маленьким React» — ты будешь рендерить карточку машины через builder, а не через копипасту document.createElement.

Перечитай свой HTML Builder. Спроси себя:

  • Как мой builder типизирует тег и атрибуты?
  • Что он возвращает — HTMLElement или generic?
  • Могу ли я переиспользовать его 1:1 в Async Race, или нужно расширить?

Если HTML Builder был на JS — тут переписываешь на TS. Generics и type guards пригодятся.

Self-check: объясни на пальцах разницу между «императивный код создаёт DOM» и «декларативный API описывает DOM». Почему второе ремонтопригоднее?


2 · TypeScript обязателен ⭐⭐⭐

Зачем: ВСЁ задание держится на типах. any / as / ! = -100. Это не страховка, это требование. Учись писать так, чтобы TS-компилятор не давал ошибок без читерства.

База:

Практика:

Как обойти запрет на as:

  • response.json возвращает Promise<unknown> → пиши type guard: function isCar(x: unknown): x is Car { ... }
  • Атрибуты DOM возвращают string | null → проверяй if (value === null) явно
  • document.querySelector возвращает Element | null → не пиши !, пиши if (el === null) return

Self-check: напиши type guard для Car { name: string; color: string; id: number }. Объясни в чём разница interface и type — когда какой выбираешь? Что вернёт JSON.parse('{}') в TS strict mode и как с этим жить без as?


3 · SPA-архитектура без фреймворков ⭐

Зачем: ты собираешь приложение которое выглядит и ведёт себя как React-app, но без React. Это значит — сам решаешь как хранить state, как ре-рендерить, как роутить.

Решения которые надо принять до того как кодить:

  1. State — один глобальный объект? class State? Pub/sub? — выбери одно и держись.
  2. Render — полная перерисовка View или точечные мутации DOM? (Точечные эффективнее но сложнее).
  3. RoutingHistory API или hash-роутинг? (/garage vs #garage).
  4. Communication — как кнопка «start race» сообщает таблице winners о новом победителе? (Event bus, callback, прямая ссылка).

Self-check: опиши за 3 минуты архитектуру своего приложения: где state, где API, где UI, как они общаются. Если не можешь — ты ещё не готов писать код.


4 · REST + CRUD + Fetch с типами ⭐

Зачем: mock-сервер — REST. GET /garage, POST /garage, PATCH /garage/:id, DELETE /garage/:id, и три endpoint'а для engine: PATCH /engine?id=X&status=started/stopped/drive. Без чёткого API-слоя у тебя будет хаос из fetch по всему коду.

Архитектурное правило: в коде должен быть один модуль api/ (или services/api.ts) который единственный делает fetch. Все View вызывают функции из него: getCars, createCar(car), startEngine(id). Не разбрасывай fetch по компонентам.

Self-check: напиши типизированную функцию getCars(page: number, limit: number): Promise<{ cars: Car; total: number }>. Где она достаёт total? (Подсказка: header X-Total-Count). Что вернёт твоя функция если сеть упадёт?


5 · Анимация: requestAnimationFrame + CSS transforms ⭐⭐

Зачем: машина должна доехать от старта до финиша. Анимация ДОЛЖНА быть плавной на 500px и держать кадр при 7 одновременных машинах. CSS-only (transition) не подойдёт — тебе нужно прерывать анимацию в произвольной точке (на 500 от drive), а это умеет только rAF.

Архитектура engine flow:

  1. Юзер нажимает Start → UI отправляет PATCH /engine?status=started → получает { velocity, distance }.
  2. Считаешь время: time = distance / velocity (миллисекунды).
  3. Запускаешь rAF-цикл: каждый кадр сдвигаешь transform: translateX(progress * trackWidth).
  4. Параллельно отправляешь PATCH /engine?status=drive. Если он вернёт 500 — твой rAF-цикл должен остановиться в текущей позиции.
  5. Если выиграл — отправляешь сообщение в state.

Подводный камень: trackWidth — это ширина дорожки, которая зависит от viewport. На resize пересчитывай.

Self-check: напиши псевдокод rAF-анимации. Где сохранишь requestId чтобы можно было отменить? Что произойдёт если юзер кликнет Stop в середине анимации?


6 · Routing — History API ⭐

Зачем: Garage и Winners должны быть на разных URL (/garage, /winners или #garage/#winners). При обновлении страницы юзер должен попасть на тот же View. State (страница, фильтры) сохраняется при навигации.

Что должно быть у тебя:

  • Функция navigate(path) — единственная, кто вызывает history.pushState.
  • Слушатель popstate — реагирует на back/forward кнопки браузера.
  • Маппинг path → View — простая table-driven логика.

Self-check: что произойдёт если юзер на /winners?page=3 нажмёт F5? Куда он попадёт? Чем history.pushState отличается от location.href = ...?


7 · State management в vanilla TS

Зачем: «при переключении между Garage и Winners состояние сохраняется — номер страницы, инпуты, выбранный цвет». Это твой главный архитектурный челлендж. Без state-слоя у тебя будет 100500 локальных переменных и баги.

Концептуально:

  • Один источник истиныclass AppState или const state: State.
  • View не хранят свой state — они читают из глобального state и подписываются на изменения.
  • Изменения state идут через методы/функции (state.setGaragePage(2)), не через прямую мутацию полей.

Полезные паттерны (без библиотек):

  • Pub/sub через EventTarget (нативный, типизируется).
  • Observable через Proxy (продвинутый вариант).
  • Просто class State с методами + событие state-changed на котором View подписаны.

Связи с твоей базой:

Self-check: опиши на одной странице свой state: какие поля? кто меняет? кто читает? Если переключусь на Winners и вернусь на Garage — какие поля сохранятся, какие сбросятся?


8 · Pagination + Sorting

Зачем: Garage = 7 машин на страницу. Winners = 10 на страницу + сортировка по wins / best time, asc / desc. Это не «фича сверху», это 30 + 30 + 25 баллов.

Сервер уже умеет:

  • Pagination: GET /garage?_page=1&_limit=7 (json-server convention)
  • Sorting: GET /winners?_sort=wins&_order=DESC
  • Total: header X-Total-Count

Что делаешь ты:

  • UI-контролы (Prev/Next/номер страницы) → меняют state → перезапрос данных.
  • Сортировочные клики по колонкам → меняют state → перезапрос с другими query-параметрами.
  • Disabled-состояния кнопок (Prev на первой странице, Next на последней).

Связи:

Self-check: общее число машин = 23, страница 4, лимит 7. Сколько машин на странице? Что вернёт сервер? Какие кнопки активны?


9 · Сборка: Vite

Зачем: обязателен бандлер. Vite быстрее и проще Webpack — бери его, если не любишь страдать.

Минимум:

  • npm create vite@latest async-race -- --template vanilla-ts
  • Настроить base под gh-pages (base: '/async-race/' в vite.config.ts)
  • npm run build + gh-pages -d dist для деплоя

Связи:

Self-check: что произойдёт с относительными путями в index.html если ты не настроишь base? Что Vite делает с TS-файлами на dev и на build?


10 · ESLint unicorn + код-стандарты

Зачем: требование задания. Если линтер не проходит — -40. Unicorn — очень строгий, многое будет ругаться на привычные паттерны.

Что особенно бесит unicorn:

  • for (let i = 0; i < arr.length; i++) → используй for...of или методы массива
  • arr[0]arr.at(0) (или check if (arr.length > 0))
  • Сокращения в именах (btn, el, idx) → пиши полное (button, element, index)
  • null vs undefined — будет требовать единообразия
  • Magic numbers — выноси в const FINISH_TIMEOUT_MS = 500

Связи:

  • ESLint (если файл есть в твоей базе)

Self-check: что напишет unicorn если ты вызовешь array.forEach? Что предложит вместо arr[arr.length - 1]?


11 · Архитектура: модули API / UI / State ⭐⭐

Зачем: требование «модульная архитектура — чёткое разделение API, UI, state». Это самое размытое требование, но именно по нему ревьюер ставит или не ставит крупные баллы.

Минимальная структура проекта:

src/
├── api/
│   ├── garage.ts         // getCars, createCar, updateCar, deleteCar
│   ├── engine.ts         // startEngine, stopEngine, drive
│   └── winners.ts        // getWinners, createWinner, updateWinner
├── state/
│   ├── app-state.ts      // глобальный state + pub/sub
│   └── types.ts          // Car, Winner, RaceResult interfaces
├── views/
│   ├── garage/
│   │   ├── garage-view.ts
│   │   ├── car-card.ts
│   │   └── pagination.ts
│   └── winners/
│       └── winners-view.ts
├── ui/
│   ├── html-builder.ts   // твой builder из прошлой задачи
│   ├── button.ts
│   └── input.ts
├── animation/
│   └── race-animation.ts // rAF-логика
├── router/
│   └── router.ts         // History API обёртка
├── constants.ts          // магические числа в одном месте
└── main.ts               // entry point

Главное правило: API не знает про UI, State не знает про DOM, UI читает из State и вызывает API. Зависимости только сверху вниз.

Self-check: если завтра решишь заменить REST на WebSocket — сколько файлов изменишь? Если только api/ — ты молодец. Если 20 файлов — у тебя нет архитектуры.


12 · Workflow (последний раз)

Подводный камень: mock-сервер юзер кросс-чека запустит у себя локально. Если твой фронт жёстко вшит на localhost:3000 — он не сможет его прогнать на проде. Сделай переменную окружения / config-файл с URL сервера.


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

Basic structure · 240

View configuration · 80

  • Два View: Garage и Winners (+25)
  • Garage показывает имя, номер страницы, общее число тачек (+15)
  • Winners показывает имя, номер страницы, общее число записей (+15)
  • State сохраняется при переключении View — page, input, color (+25)

Garage view · 160

  • CRUD тачек: create / update / delete / list. Delete удаляет из обеих таблиц (+60)
  • Цвет выбирается из RGB-палитры, отражается на изображении (+25)
  • Кнопки update/delete возле каждой тачки (+15)
  • Pagination 7 тачек на страницу (+30)
  • Кнопка генерит 100 случайных тачек: имена из 2 частей × ≥10 опций, цвет рандом (+30)

Car animation · 140

  • Кнопки start/stop возле каждой тачки (+25)
  • Start engine: ждём velocity → анимация → drive request. 500 на drive → стоп в текущей позиции (+60)
  • Stop engine: ждём stop response → тачка возвращается на старт (+25)
  • Кнопки disabled-логика: start disabled во время drive, stop disabled на старте (+15)
  • Анимация плавная на 500px viewport (+15)

Race animation · 100

  • Start race запускает гонку всех тачек на текущей странице (+40)
  • Reset race возвращает всех на старт (+30)
  • После финиша первой — сообщение с именем победителя (+30)

Winners view · 120

  • После гонки победитель попадает в Winners (+40)
  • Pagination 10 на страницу (+30)
  • Колонки: №, image, name, wins, best time. Wins инкрементятся, best time обновляется только если лучше (+25)
  • Сортировка по wins и best time, asc и desc (+25)

Без штрафов

  • Нет UI-фреймворков (иначе -600)
  • TypeScript, нет any / as / ! (иначе -100)
  • Функции ≤ 40 строк, eslint-unicorn проходит (иначе -40)
  • Нет major-багов (-30 каждый) и minor-багов (-10 каждый)

🧠 Self-check перед финальным коммитом

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

  1. Архитектура. Если попрошу нарисовать схему: модули, зависимости, поток данных — нарисую за 5 минут?
  2. Типизация. Прогнал tsc --noEmit --strict — 0 ошибок? grep -rn ' as \| any \| !' src/ — пусто (кроме комментариев)?
  3. State. Если переключаюсь Garage → Winners → Garage — номер страницы и инпуты сохранены? Какой объект это хранит?
  4. Engine flow. Что произойдёт если: (а) start → быстро stop, (б) start → drive вернёт 500, (в) start → юзер уходит на Winners?
  5. Race. Если 7 тачек стартуют одновременно — какая из них окажется в winners? Что если две финишируют в одном кадре?
  6. Winners table. Если та же тачка выиграла второй раз с худшим временем — что изменится в её строке? А с лучшим?
  7. Linter. npm run lint — 0 ошибок? Прогнал на чистом репо без .eslintcache?
  8. Bundle. npm run build собирается без warning? Размер dist разумный (не 5 МБ)?
  9. Deploy. Открыл gh-pages в инкогнито — работает? Все пути относительные?
  10. Cross-check. Reviewer склонирует mock-сервер на свой localhost:3000 — мой URL сервера это localhost:3000 или вынесен в config?

➡️ Compound forward — после bootcamp начинается карьера

Это финал bootcamp. Дальше — реальная работа. Что переходит:

В реальные проекты

  • TypeScript — будет в каждой вакансии middle/senior. Async Race — твой первый serious TS-проект в портфолио.
  • Архитектура vanilla SPA — даёт понимание что React/Vue делают под капотом. Junior без этого = «знает React, не знает frontend».
  • REST + типизированный API-слой — паттерн, который ты переиспользуешь во ВСЕХ проектах: меняется фреймворк, не меняется идея «один файл = один источник правды для бэкенда».
  • rAF + transform — анимации в любом проекте. Knowledge transfer 1:1.
  • State management из первых принципов — когда дойдёшь до Redux / Zustand / Pinia, поймёшь зачем они существуют, а не «потому что в туториале так сказали».

Карьерный roadmap

  • _MOC Процессы — там разделы про собеседования, поиск работы, soft skills. Открой и спланируй ближайшие 3 месяца.
  • CV. Async Race + Shelter + Codejam в портфолио — это уже сильное резюме. Async Race — главный pet-project, описывай его подробнее остальных.
  • Собесы. Async Race — отличный материал. На вопросы «расскажи как ты делал state» / «расскажи как делал routing без React» / «как типизировал API» — у тебя готовые ответы из твоего же кода.
  • Технические собесы. Из Async Race вытаскиваются темы: event loop (rAF, async/await), TypeScript narrowing, REST, кэширование, race conditions (Start spam → 404/429), архитектурные паттерны.

Углубление (по интересу, не обязательно для junior)

  • Frameworks. Возьми Async Race и перепиши на React/Vue/Solid — почувствуешь чем они помогают и где мешают.
  • Тесты. Vitest + Playwright на Async Race — закроет вопрос «есть ли у тебя опыт с тестами».
  • Backend. Перепиши mock-сервер на свой NestJS — закроешь полный fullstack-цикл.
  • Performance. Профилируй Race на 100 машинах в Chrome DevTools — узнаешь про reflow, paint, composite layers вживую.

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