Пользовательский интерфейс
Общий подход
Рассматривались два устройства кабинета: единый SPA и микрофронтенды на Module Federation. iframe и web components не рассматривались.
Стартовая точка: один SPA. Один бандл Vite, общая тема, общие Zod-схемы из @hh/contracts. Кандидат и работодатель уже разделены маршрутами и своими областями состояния, каждый клиент получает оптимизированный бандл с помощью разделения кода и ленивой загрузки нужных частей (lazy loading). Данный подход - оптимальная отправная точка, которая позволяет быстро реализовать нужные функции системы, а также возможность в дальнейшем разделить UI на Module Federation приложение, разбив зоны ответственности по доменам. Недостаток SPA для поисковой выдачи смягчается библиотекой @unhead (заголовок, описание, public/og.svg). Экраны подгружаются лениво.
Целевая точка: разделить кодовые базы по доменам. Границы уже заданы каталогами pages/profile, pages/employer и shared. Когда backend режут на сервисы или команд растет, эти части выносятся через Module Federation. Сейчас делить UI при одном бэкенде рано. Это отложенный шаг, по той же логике, что у API: сначала явные модули, затем вынос за сеть.
Не строили свой "фреймворк над React". Сборка: Vite.
Стек
- React и TypeScript
- MobX: реактивное состояние кабинетов
- Mantine: компоненты
- Zod: схемы из
@hh/contracts, те же на сервере - nuqs: состояние в URL
@unhead: SEO-мета
Клиент API генерируется по OpenAPI (pnpm openapi, каталог shared/api/generated, руками не правится). Формы: @mantine/form и schemaResolver со { sync: true }, чтобы Zod не уходил в Promise.
Организация кабинетов
Маршруты описаны в shared/router/routes.ts. ProtectedRoute ждёт восстановления сессии (auth.restoreSession()): access-токен в памяти, refresh в HttpOnly-cookie hh_refresh. CandidateRoute и EmployerRoute пропускают по полю role. Работодатель не попадает в лобби теста, кандидат не попадает в подборку.
- Кабинет кандидата. На главной: готовность анкеты, слайд подготовки, материалы по подтверждённым навыкам, баннер если реестр ФСП нашёл историю по почте.
- Анкета собрана плитками (контакты, образование, опыт, поиск работы); раздел добавляется кнопкой. В образовании автодополнение вузов и колледжей РФ по списку Wikidata (лицензия CC0); свободный ввод сохраняется. Разбор PDF и выгрузка резюме в трёх шаблонах описаны в сценарии. В контактах панель ФСП.
- Тест идёт через лобби секций, вопросы по одному, затем итог. Сборка попытки и чистота: антисписывание.
Кабинет работодателя. Потребность, пул, карточка совпадения: оценка от 0 до 100, блоки WhyMatch, ReasonList, BreakdownGroup. Коды причин приходят с API по-английски (verified_grade_match, skill_missing:python). Русские формулировки лежат в shared/lib/labels.ts. Если пункта нет, в карточке показывается исходный код. Приглашение и PDF резюме кандидата используют то же меню шаблонов, что и профиль.
Состояние и запросы
Собранная модель состояния позволяет сократить "гонки состояния". Пока человек ждёт вход или сохранение (loader: true), в набор активных загрузок кладётся идентификатор, оверлей висит, пока набор не пуст. Несколько запросов не ломают друг друга. Для важных запросов используются retry-запросы с разным числом повторов.
Пока оверлей открыт, вместо стандартного спиннера показывается одна из пяти иллюстраций (RequestLoader: чиби, утка, кот на клавиатуре, кофе и деплой, node_modules). Картинка выбирается на этот показ и не меняется, пока запрос не закончится.
Тема ФСП и фон
Тема Mantine собрана под брендбук ФСП: шрифт JetBrains Mono, primaryColor fsp, оттенок 6. Основной #402FFF, акцент #EC1D35 (fspRed), текст #1B1C21, фон #EDEDED. Для контраста на сером фоне бренда заданы свои приглушённый текст и рамка поля, белая поверхность карточек, цвета статусов приглашения и верификации навыка. Переопределены Input, Combobox, Menu, Popover, PasswordInput: выпадающие списки в портале не серые, лигатуры в URL не склеивают ://. Файл: apps/web/src/shared/theme/index.ts.
На страницах входа и в оболочке кабинета рисуются восемь контурных иллюстраций (кот, утка, кружка, коробка, терминал, баг, призрак, принтер). Набор и места не пересчитываются при каждом рендере: функция artLayouts в shared/decor/model/art-layout.ts берёт seed и строит раскладку детерминированно.
Seed на логине и регистрации: введённая почта, если в строке есть @, иначе значение guest. На подтверждении почты и сбросе пароля: токен из ссылки. Строка хешируется FNV-1a, из хеша запускается генератор mulberry32. Генератор перемешивает пул иллюстраций и раскладывает их по слотам: на auth шесть позиций вокруг формы (боковые скрываются на узком экране), в кабинете четыре угла, реже и с меньшей непрозрачностью. Тем же генератором задаются сдвиг позиции на несколько процентов, ширина, наклон в пределах десяти градусов и прозрачность. Одна и та же почта даёт одну раскладку, другая почта даёт другую.