11 KiB
Context
Сегодня в RouterProvider есть два независимых маршрута под ProtectedRoute:
/profile→ProfilePage, которая сама рендеритWalletHeader+KycBanner+ProfileSummary+ блокиProfileDetails/SecuritySection/MnemonicSectionодним столбцом (ProfilePage.module.css,.main— flex сmax-width: 1024px)./transactions→TransactionsPageвнутри обёрткиWalletLayout(footer), которая сама даётWalletHeader+<Outlet/>+Footer.TransactionsPageхранит таб «Транзакции/Заявки» в локальномuseState(заявки только для юр. лиц черезuseMe()).
Несогласованность обёрток (профиль рисует свой WalletHeader, транзакции — через WalletLayout) — главный технический момент, который нужно унифицировать при слиянии.
Существующие переиспользуемые части: виджеты @widgets/profile (баррель уже экспортирует ProfileSummary, ProfileDetails, SecuritySection, MnemonicSection, KycBanner, ProfileSection), @widgets/transactions-list, @widgets/purchase-requests-list, и Spinner из @shared/ui/Spinner (поддерживает size, label, fullscreen).
Goals / Non-Goals
Goals:
- Единый раздел
/profileс боковым меню 25% / контентом 75% и вложенными URL на каждый пункт. - Переиспользовать существующие блоки профиля и виджеты транзакций без переписывания их логики.
- Сохранить текущее поведение транзакций (табы «Транзакции/Заявки» для юр. лиц) внутри пункта меню.
- Индикатор загрузки контента через существующий
Spinner; меню остаётся доступным. - Сохранить рабочими старые ссылки (
/transactions→/profile/transactions). - Сохранить мобильное поведение (стек в одну колонку < 1024px).
Non-Goals:
- Не меняем бизнес-логику профиля, авторизации, загрузки данных (
useMe, профильные хуки) и API транзакций. - Не добавляем новые пункты меню сверх перечисленных.
- Не трогаем дизайн-токены и глобальные стили, кроме новых модульных CSS для layout/меню.
- Не вводим менеджер состояния — навигация через URL (React Router).
Decisions
D1. Маршрутизация — вложенные routes с <Outlet/>, а не локальный useState
Использовать nested routes React Router: родитель /profile рендерит layout профиля (меню + <Outlet/>), дети — details / security / mnemonic / transactions. index-маршрут редиректит на details (<Navigate to="details" replace />).
- Почему: пользователь выбрал отдельные URL с поддержкой кнопки «Назад» и deep-link.
<Outlet/>— идиоматичный для проекта способ (так уже сделанWalletLayout). - Альтернатива (отклонена): один компонент с
useStateдля активного пункта — нет deep-link и истории; противоречит выбранному варианту. - Альтернатива (отклонена):
?tab=query — менее явный URL, чем сегментный путь.
D2. Структура компонентов
pages/profile/ui/ProfilePage.tsx→ становится layout-страницей: рендерит обёртку (см. D3), левое меню и<Outlet/>в правой колонке сKycBannerнад<Outlet/>.- Новый виджет навигации
@widgets/profile→ProfileMenu(ProfileSummaryсверху + список ссылок-пунктов черезNavLink, активный пункт подсвечивается черезaria-current/класс). Добавить в баррельwidgets/profile/index.ts. - Контент-пункты — тонкие компоненты-страницы в
pages/profile/ui/(или существующие виджеты напрямую):details→<ProfileDetails/>security→<SecuritySection/>mnemonic→<MnemonicSection/>transactions→ содержимое бывшейTransactionsPage(список + табы юр. лиц).
pages/transactionsсохраняется как тонкая обёртка/переиспользуемый блок, переносимый в пункт меню; маршрут/transactionsзаменяется на редирект.
D3. Унификация обёртки (header/footer)
Завести профиль под общий WalletLayout (как транзакции), убрав собственный WalletHeader из ProfilePage. В RouterProvider:
<Route element={<WalletLayout footer />}>
<Route path={ROUTES.PROFILE} element={<ProfilePage />}>
<Route index element={<Navigate to="details" replace />} />
<Route path="details" element={<ProfileDetailsItem />} />
<Route path="security" element={<SecurityItem />} />
<Route path="mnemonic" element={<MnemonicItem />} />
<Route path="transactions" element={<TransactionsItem />} />
</Route>
</Route>
<Route path={ROUTES.TRANSACTIONS} element={<Navigate to="/profile/transactions" replace />} />
ProfilePage рендерит свой <Outlet/> для пунктов внутри двухколоночного layout — то есть здесь два уровня Outlet (внешний от WalletLayout, внутренний от ProfilePage). Это допустимо в React Router.
- Почему: единый
WalletHeader/Footerустраняет рассинхрон обёрток и убирает дублирование. - Альтернатива (отклонена): оставить профилю собственный header — расходится с остальными разделами и усложняет поддержку.
D4. Маршруты в конфиге
Добавить в shared/config/routes.ts константы для вложенных путей профиля (например PROFILE_DETAILS, PROFILE_SECURITY, PROFILE_MNEMONIC, PROFILE_TRANSACTIONS) либо хелпер; TRANSACTIONS сохранить для обратного редиректа. Меню (ProfileMenu) строит ссылки из этих констант.
D5. Состояние загрузки
В правой колонке каждый пункт сам отвечает за свою загрузку: пока соответствующий React Query запрос в состоянии загрузки — рендерить <Spinner label="Загрузка" /> (по центру области контента, fullscreen в пределах колонки). Меню вне <Outlet/>, поэтому остаётся интерактивным. Сейчас профильные блоки делают if (!data) return null — заменить на показ Spinner, пока useMe грузится.
D6. Стили
Новый ProfilePage.module.css: grid/flex 25% / 75%, gap, и @media (max-width: 1023px) — стек в одну колонку (перенести существующие брейкпоинты профиля). Меню — отдельный ProfileMenu.module.css (список, активное состояние, sticky сводка по желанию). Всё на CSS Modules, без новых зависимостей.
Risks / Trade-offs
- Двойной
<Outlet/>(WalletLayout → ProfilePage) → может запутать при чтении роутера. Mitigation: явные комментарии вRouterProviderи вProfilePage. - Профиль внутри
WalletLayout footerменяет наличиеFooter/центрирование на странице профиля по сравнению с текущим видом → Mitigation: сверить визуально; при необходимости подобрать пропсыWalletLayout(footer/center). - Дублирование табов транзакций при переносе в пункт меню → Mitigation: вынести содержимое
TransactionsPageв переиспользуемый компонент, не копировать логику. - Ломающий редирект
/transactions→ внешние ссылки продолжают работать через<Navigate replace/>; зафиксировано как BREAKING в proposal. - Регрессия мобильного вида профиля → Mitigation: перенести существующие медиа-брейкпоинты профиля.
Migration Plan
- Добавить вложенные маршруты профиля и редирект
/transactionsвRouterProvider+ константы вroutes.ts. - Создать
ProfileMenuи компоненты-пункты, переработатьProfilePageв layout. - Вынести содержимое
TransactionsPageв переиспользуемый блок, подключить как пункт. - Добавить состояние загрузки (
Spinner) в пунктах. npm run build(tsc) + ручная проверка: deep-link каждого пункта, кнопка «Назад», редиректы/profileи/transactions, мобильный вид, юр.лицо vs физлицо (табы).
Откат: вернуть прежние отдельные маршруты /profile и /transactions (изменения изолированы в pages/profile, pages/transactions, widgets/profile, routes.ts, RouterProvider).
Open Questions
- Нужно ли подсвечивать пункт «Транзакции» в верхней навигации (
WalletHeader) как раздел профиля, или ссылка на транзакции из шапки ведёт прямо на/profile/transactions? (По умолчанию: шапка ведёт на/profile/transactions.) - Делать ли блок
ProfileSummarysticky при скролле длинного контента (например списка транзакций)? (По умолчанию: обычное позиционирование, без sticky.)