Files
2026-06-29 19:11:45 +03:00

11 KiB
Raw Permalink Blame History

Context

Сегодня в RouterProvider есть два независимых маршрута под ProtectedRoute:

  • /profileProfilePage, которая сама рендерит WalletHeader + KycBanner + ProfileSummary + блоки ProfileDetails / SecuritySection / MnemonicSection одним столбцом (ProfilePage.module.css, .main — flex с max-width: 1024px).
  • /transactionsTransactionsPage внутри обёртки 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/profileProfileMenu (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

  1. Добавить вложенные маршруты профиля и редирект /transactions в RouterProvider + константы в routes.ts.
  2. Создать ProfileMenu и компоненты-пункты, переработать ProfilePage в layout.
  3. Вынести содержимое TransactionsPage в переиспользуемый блок, подключить как пункт.
  4. Добавить состояние загрузки (Spinner) в пунктах.
  5. 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.)
  • Делать ли блок ProfileSummary sticky при скролле длинного контента (например списка транзакций)? (По умолчанию: обычное позиционирование, без sticky.)