Files
frontend/openspec/changes/merge-profile-transactions/design.md
2026-06-29 17:41:56 +03:00

97 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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
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.)