Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Аукционы грузоперевозок

SPA для работы перевозчика с грузовыми аукционами по предоставленному OpenAPI-контракту. Проект последовательно строится на React, TypeScript, Vite, TanStack Router и Feature-Sliced Design.

Демо

Опубликованный стенд: https://maker27.github.io/freight-auctions/ — собирается и публикуется автоматически из main после прохождения всех гейтов, содержит MSW и закрыт от индексации. Пока репозиторий закрытый, адрес недоступен извне.

Посмотреть локально можно без установки зависимостей:

docker compose up --build     # http://localhost:8080

или той же сборкой стенда через Node:

npm ci && npm run build:stand && npm run preview

Интерфейс

Публичная витрина и каталог аукционов в светлой схеме, кабинет — в тёмной. Скриншоты сняты Playwright’ом с production-сборки стенда при 1440 × 940.

Публичная витрина Каталог с фильтрами
Публичная витрина в светлой схеме Каталог аукционов с панелью фильтров
Детали аукциона История ставок Форма ставки
Детали аукциона в тёмной схеме История ставок с победителем и отменённой ставкой Форма ставки с подставленной доступной ценой

Сделано сверх минимума задания

  • каждый ответ API проверяется в рантайме Zod-схемами, сгенерированными из OpenAPI; отдельный гейт падает при дрейфе генерации;
  • защищённые контакты, адреса, координаты и цена груза удаляются до попадания в TanStack Query cache, а не только скрываются в разметке; это закреплено тестами;
  • 514 тестов в 70 файлах и 5 браузерных сценариев Playwright; порог покрытия по слоям lib и model — гейт CI, а не справка;
  • доступность и валидность разметки проверяются автоматически: vitest-axe и html-validate на отрендеренном DOM, отдельный тест контраста 13 утверждённых пар токенов в обеих схемах;
  • нефункциональные бюджеты считаются из Vite manifest и падают при превышении: 209,05 из 250 КиБ начального JS gzip, CSS 5,36 из 20 КиБ, шрифты 65,46 из 72 КиБ;
  • матрица 9 × 9 статусов аукциона и участия с безопасным поведением для неизвестных сочетаний; время подаётся через внедряемые часы, а не Date.now();
  • конкурентная ставка: принудительное перечитывание условий перед POST, mutex двойной отправки, отсутствие ложного успеха при неопределённом сетевом исходе;
  • единый источник визуальных значений tokens.ts → tokens.generated.css с немутирующей проверкой дрейфа и запретом визуальных литералов в коде;
  • SPA работает и в корне домена, и в подкаталоге Pages: base path один для роутера, API и Service Worker;
  • Docker с непривилегированным nginx, cache policy и healthcheck, публикация Pages и GHCR из того же workflow только после успешных гейтов.

Исходные материалы

Стек

  • React, TypeScript в строгом режиме и Vite;
  • TanStack Router и TanStack Query;
  • React Hook Form и Zod;
  • MSW и Zustand для точечного клиентского UI-state;
  • Tailwind CSS, собственный shared/ui на Radix, Tabler Icons и Sonner;
  • Vitest, React Testing Library, vitest-axe и html-validate.

Требования и запуск

  • Node.js >=22.17.0 <23;
  • npm 10.9.8.
npm ci
npm run dev

Локальный сервер по умолчанию открывается на http://localhost:5177. Порт для dev и preview можно переопределить в локальном .env (пример есть в .env.example):

DEV_SERVER_PORT=5180

Допустимо целое значение от 1 до 65535. Некорректное значение или занятый порт останавливают запуск вместо тихого выбора другого адреса.

Production-сборка и локальный preview:

npm run build
npm run preview

Сборка для GitHub Pages под именем этого репозитория и её локальный preview:

npm run build:pages
npm run preview:pages

build:pages задаёт base path /freight-auctions/, включает MSW и режим публичного демо, создаёт dist/404.html из того же app shell и закрывает весь артефакт от индексации. Поэтому ассеты, Router, mock API, worker и прямое открытие SPA-маршрутов используют единый подкаталог GitHub Pages.

Контейнерный стенд собирается и запускается одной командой:

docker compose up --build

После healthcheck приложение доступно на http://localhost:8080. Порт можно переопределить, например FREIGHT_AUCTIONS_PORT=8090 docker compose up --build. Образ содержит root-сборку build:stand с MSW и запретом индексации, nginx работает непривилегированным пользователем и отдаёт SPA-маршруты через fallback на index.html. HTML, CSS и JavaScript передаются с gzip; хешированные assets кешируются на год как immutable, а app shell, Service Worker и SEO-файлы ревалидируются через ETag и Last-Modified без сохранения устаревшей версии. Остановка и удаление контейнера: docker compose down.

Реализованная функциональность

  • публичная витрина с маршрутным SEO-слоем, JSON-LD и парной hero-иллюстрацией для светлой и тёмной схемы;
  • локальная демо-сессия, guard закрытого контура и безопасное восстановление исходного URL вместе с query и hash;
  • каталог аукционов: URL-фильтры с Zod-разбором, сортировка, размер страницы, пагинация router-ссылками, вид «список / сетка» и все async-состояния;
  • детальная страница: все контрактные секции, четыре permission-флага, prefetch по фокусу и наведению, опрос активного аукциона раз в 15 секунд;
  • история ставок: адаптивные таблица и мобильный список, сортировка с aria-sort, победитель, отмена с причиной, скрытая история без запроса;
  • форма ставки: денежные ограничения в минорных единицах, помощники шага и доступной цены, перечитывание условий перед POST, обработка 422 и 401;
  • переключатели цветовой схемы, режима отображения НДС и вида каталога;
  • транспорт с рантайм-валидацией ответов, доменными ошибками, таймаутом и повторами только для читающих операций;
  • доменный слой: брендированные идентификаторы, матрица статусов, состояния поля, внедряемые часы и московская трактовка дат без смещения;
  • stateful mock API на MSW для четырёх операций с детерминированными сценариями прав, пустоты и ошибок;
  • собственный UI-кит на нативных элементах и Radix, локальный шрифт Inter, единый источник токенов;
  • Docker, Compose, nginx и единый CI-workflow с публикацией Pages и GHCR.

Подробный перечень по слоям — в docs/features.md.

После успешной ставки форма остаётся на своём прямом URL: активный аукцион допускает изменение ставки, поэтому пользователь сразу видит обновлённые ограничения и может осознанно изменить значение. Переход к деталям выполняется явной ссылкой и сохраняет точный URL исходного списка.

Демо-вход

Вход полностью локальный: он не обращается к серверу, не проверяет реальные учётные данные и не предоставляет настоящую авторизацию. Подходит любой корректный email и пароль длиной не менее восьми символов. Значения формы не сохраняются; в sessionStorage записывается только признак активной демо-сессии.

Неавторизованный переход на любой URL ветки /auctions перенаправляет на /login. После успешной отправки формы пользователь возвращается на исходный внутренний URL. Внешние и некорректные redirect-значения заменяются безопасным /auctions.

Публичная витрина и SEO

Корневой маршрут описывает возможности демонстрационного кабинета и не загружает данные аукционов. Canonical строится из текущего origin и Vite base path, поэтому одинаково работает в корне домена и в подкаталоге Pages.

Обычная production-сборка разрешает индексирование витрины и /login; оба маршрута получают canonical, social metadata и JSON-LD, а вход включён в sitemap как потенциальный sitelink «Личный кабинет». Ветка /auctions закрыта в robots.txt и маршрутных метаданных. Режим build:pages задаёт VITE_PUBLIC_DEMO=true: весь публичный контур получает noindex, nofollow, а итоговый артефакт содержит статический robots meta и robots.txt с Disallow: /. sitemap.xml содержит / и /login, но не включает кабинет; полный запрет индексации стенда задаётся robots и route metadata.

Hero-иллюстрация имеет отдельные светлую и тёмную версии и переключается по фактически разрешённой теме, включая ручной выбор, отличный от системного. Изображение дублирует контекст соседнего текста, поэтому остаётся декоративным с пустым alt; браузер загружает только URL активной версии.

Mock API

MSW включается автоматически до первого рендера React в development, в build:stand и в build:pages демонстрационного стенда. Под корневым или настроенным базовым путём он перехватывает четыре контрактных адреса api/v1:

  • POST /auctions/list;
  • GET /auctions/{auctionUuid};
  • GET /auctions/{auctionUuid}/bets;
  • POST /auctions/{auctionUuid}/bets.

Обычная production-сборка по умолчанию ожидает настоящий API. Режим можно задать явно через VITE_API_MODE=mock|remote; любое другое значение останавливает приложение с ошибкой конфигурации. build:stand и build:pages используют отдельный Vite mode из .env.pages и всегда собирают демонстрацию с MSW. В Vitest тот же набор хендлеров подключён через Node server; состояние сбрасывается после каждого теста. Фильтр списка cargo_num: "MOCK-503" воспроизводимо возвращает 503, а несуществующее значение обычного фильтра — пустой список. Остальные сценарии прав и данных представлены отдельными аукционами со стабильными UUID в src/mocks/data/auctions.fixture.ts.

public/mockServiceWorker.js — сгенерированный MSW CLI браузерный worker. Он раздаётся отдельным файлом, потому что Service Worker должен иметь стабильный URL и scope приложения. Файл совпадает с поставляемой версией MSW и вручную не редактируется; директивы отключения линтеров являются частью upstream-файла.

Маршруты

URL Назначение Индексация
/ публичная демо-витрина зависит от режима
/login локальный демо-вход зависит от режима
/auctions список аукционов noindex, nofollow
/auctions/$auctionUuid детали аукциона noindex, nofollow
/auctions/$auctionUuid/bets история ставок noindex, nofollow
/auctions/$auctionUuid/bet создание или изменение ставки noindex, nofollow

Публикация стенда

При push в main единый workflow сначала выполняет все гейты и собирает Pages-артефакт. Только после успешного job Verify этот артефакт публикуется в GitHub Pages, а Dockerfile собирается и образ отправляется в GHCR с тегами latest и полным SHA коммита. Для pull request выполняются гейты и Pages-сборка, но внешняя публикация не запускается.

В настройках репозитория источником Pages должен быть выбран GitHub Actions. Workflow использует только штатный GITHUB_TOKEN: секреты приложения для демо-стенда не нужны. Опубликованный Pages-артефакт и контейнерная сборка всегда включают MSW и noindex, nofollow; обычный npm run build по-прежнему ожидает настоящий API и не подменяется демонстрационным режимом.

Проверки

npm run env:check
npm run api:validate
npm run api:types:check
npm run typecheck
npm run lint
npm run format:check
npm run dead-code:check
npm run names:check
npm run tokens:check
npm run test:run
npm run test:coverage
npm run test:e2e
npm run build
npm run build:budget
npm run build:stand
npm run build:pages
docker compose config
nginx -t -c "$PWD/deploy/nginx.conf"

test:e2e запускает браузерный смоук Playwright поверх production-сборки build:stand с MSW: каталог с фильтром и сортировкой, форма ставки и проход клавиатурой без указателя. Перед первым запуском нужен браузер: npx playwright install chromium. Полное покрытие остаётся за интеграционными тестами Vitest, смоук их не дублирует.

test:coverage прогоняет те же тесты и дополнительно требует порогов покрытия по слоям lib и model: 85 % строк и операторов, 90 % функций, 75 % ветвлений. Этот же скрипт выполняется в CI вместо test:run, поэтому порог является гейтом, а не справкой.

Локальные хуки ставит husky через prepare. pre-commit запускает lint-staged (eslint --fix и prettier --write по отобранным файлам) и npm run typecheck; pre-push запускает npm run test:run. Хук не изменяет индекс за пределами файлов, уже отобранных разработчиком. Полный набор гейтов остаётся за CI и ручной проверкой: дублировать его в хуке нельзя, иначе хук начнут обходить через --no-verify.

tokens:check сравнивает генерируемый CSS с tokens.ts, не изменяя рабочее дерево. visual:check, входящий в lint, запрещает визуальные литералы, произвольные значения Tailwind, inline-стили и динамически составленные классы. format:check проверяет единое форматирование Prettier, а dead-code:check ищет неиспользуемые файлы, зависимости и публичные экспорты. Для сгенерированных OpenAPI-файлов и динамически загружаемого источника токенов действуют точечные исключения только для неприменимых видов export-проверок.

Обычная remote production-сборка создаёт Vite manifest и автоматически запускает build:budget: начальный JavaScript ограничен 250 КиБ gzip, дополнительные чанки каждого lazy-маршрута — 120 КиБ gzip, весь CSS — 20 КиБ gzip, локальные файлы шрифта — 72 КиБ gzip. Отдельная команда полезна для повторной проверки уже собранного dist. Browser-only MSW из demo-сборок измеряется отдельно: это инфраструктура демонстрационного backend, а не код remote production-приложения.

Генерация TypeScript-типов и Zod-схем из OpenAPI выполняется отдельно; проверка дрейфа повторяет генерацию и требует чистого результата:

npm run api:types
npm run api:types:check

Полный состав покрытого тестами поведения, проверки SEO и конфигурации поставки, а также матрица соответствия первичному заданию — в docs/verification.md.

Фактический прогон проверок

Последний полный прогон — 03.08.2026: OpenAPI и generated drift, typecheck, lint с visual gate, format:check, 514 тестов в 70 файлах, test:coverage (строки 94,55 %, операторы 94,54 %, функции 98,93 %, ветвления 85,52 %), 5 браузерных сценариев Playwright, dead-code:check, names:check, tokens:check, обычная production-сборка, root-сборка стенда, Pages-сборка и бюджетный гейт. env:check на машине разработчика падает из-за Node 24.18.1 и npm 11.16.0 вместо требуемых 22.17.0 и 10.9.8; сам репозиторий согласован, CI берёт версию из .nvmrc.

Remote production укладывается в бюджеты: транзитивный начальный JavaScript — 209,05 КиБ gzip из 250 КиБ, дополнительные чанки lazy-маршрутов — от 2,55 до 5,51 КиБ из 120 КиБ, весь CSS — 5,36 КиБ из 20 КиБ, локальные шрифты — 65,46 КиБ из 72 КиБ. Browser-only MSW demo-сборок занимает 163,32 КиБ gzip и раскрывается отдельно от бюджета production-приложения.

Ручная браузерная матрица пройдена 01.08.2026 в headless Chromium на сборке с полным набором аукционных экранов:

  • вход с клавиатуры, фокус первого невалидного поля и возврат на исходный URL;
  • список с данными, пустым результатом, безопасным fallback URL, 503 после завершения retry, пагинацией и мобильной панелью фильтров;
  • карточки и деталь со всеми секциями, not-found и вариантами скрытых контактов, адресов и цены груза;
  • история с обычной, победившей и отменённой ставками, пустое и скрытое состояния; при скрытой истории запрос /bets не выполнялся;
  • форма ставки: пустое значение, успешная ставка 139 000 ₽, обновлённая доступная цена, единственный toast, сохранённый ввод и запрещённое состояние;
  • ширины 360, 768 и 1280 px, светлая и тёмная схемы, отсутствие горизонтального переполнения, Escape и возврат фокуса мобильной панели;
  • отсутствие скрытых контактов, адресов, координат и цены груза в DOM и TanStack Query cache.

В консоли страниц неожиданных warning/error и ошибок приложения не было. При намеренном воспроизведении not-found и 503 Chromium ожидаемо зарегистрировал сетевые ответы 404/503; UI обработал их предусмотренными состояниями. Процесс Snap Chromium отдельно вывел инфраструктурные сообщения GCM и software-GPU: они не относятся к приложению и в консоль страниц не попали. Проверка реальным скринридером и работа с настоящим backend не выполнялись.

Витрина и SEO проверены 02.08.2026 в headless Chromium на 360, 768 и 1280 px: критичный текст и действия не обрезаны по горизонтали, маркировка демо заметна, title, robots и canonical обновляются, неожиданных ошибок страницы нет. В Pages-артефакте проверены статический noindex, nofollow, robots.txt с полным Disallow: /, sitemap с двумя публичными URL и скомпилированный demo-режим метаданных.

Поставка проверена 02.08.2026: docker compose config и синтаксис deploy/nginx.conf через nginx -t прошли, для обоих закреплённых base images подтверждён multi-platform manifest, root-артефакт проверен на MSW, noindex, nofollow, 404.html и полный Disallow: /. Docker-образ собран, контейнер достиг состояния healthy; прямой SPA-маршрут, заголовки noindex и Cache-Control и ответ 304 на условный запрос проверены через nginx, после чего контейнер и Compose-сеть удалены. Переопределение порта проверено реальными запусками dev на 5197 и preview на 5198.

Не выполнялись: ручной браузерный проход после последних изменений интерфейса, проверка реальным скринридером, работа с настоящим backend и GitHub-hosted deploy jobs.

Архитектура и состояние

Направление зависимостей:

app -> pages -> widgets -> features -> entities -> shared
  • TanStack Router владеет маршрутами, фильтрами и пагинацией в search params.
  • React Hook Form владеет значениями форм входа, фильтров и ставки до их применения.
  • Zustand хранит только локальную демо-сессию, UI-предпочтения и состояние мобильной панели фильтров.
  • TanStack Query владеет серверным списком и деталями; кэш полностью очищается при выходе из демо-сессии.
  • Нормализованный MSW store владеет только состоянием демонстрационного бэкенда; наружу возвращаются клоны контрактных DTO.
  • shared/ui содержит доменно-независимые компоненты с публичным API каждого сегмента.

Ограничения

  • Демо-сессия не является механизмом безопасности и не заменяет серверную авторизацию.
  • GitHub-hosted jobs публикации и фактические URL стенда/GHCR станут проверяемы только после коммита и push в main; эта внешняя операция не выполнялась.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages