profile refactor

This commit is contained in:
2026-06-29 17:41:56 +03:00
parent 2eaa11f790
commit 237112c302
33 changed files with 7722 additions and 232 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-29

View File

@@ -0,0 +1,96 @@
## 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.)

View File

@@ -0,0 +1,35 @@
## Why
Сейчас профиль и транзакции — это две отдельные страницы (`/profile` и `/transactions`), между которыми нужно переключаться через общую навигацию. Профиль показывает все свои блоки (данные, безопасность, мнемоника) одним длинным скроллом. Объединение их в единый раздел с боковым меню даёт пользователю одну точку входа в «личный кабинет», убирает длинный скролл и делает транзакции равноправным пунктом наряду с настройками профиля.
## What Changes
- Страница профиля превращается в layout «личного кабинета»: слева колонка ~25% (меню), справа колонка ~75% (контент активного пункта).
- В левой колонке сверху всегда закреплён блок `ProfileSummary` (аватар + сводка), под ним — список пунктов меню.
- Существующие блоки профиля становятся пунктами меню и показываются в правой колонке по клику:
- **Данные профиля** (`ProfileDetails`) — пункт по умолчанию.
- **Безопасность** (`SecuritySection`).
- **Мнемоническая фраза** (`MnemonicSection`).
- **Транзакции** добавляются как ещё один пункт меню; в правой колонке рендерится содержимое бывшей `TransactionsPage` (список транзакций + табы «Транзакции / Заявки» для юр. лиц).
- Навигация между пунктами — через отдельные вложенные URL (`/profile/details`, `/profile/security`, `/profile/mnemonic`, `/profile/transactions`). Работают кнопка «Назад» и прямые ссылки.
- `/profile` редиректит на пункт по умолчанию (`/profile/details`).
- **BREAKING**: старый маршрут `/transactions` редиректит на `/profile/transactions`; отдельная страница транзакций в навигации больше не используется как самостоятельная.
- `KycBanner` отображается над контентом в правой колонке.
- Пока данные активного пункта загружаются, в правой колонке показывается индикатор загрузки (переиспользуется `Spinner` из `@shared/ui/Spinner`); меню остаётся доступным.
- Адаптив: на узких экранах (<1024px) меню и контент схлопываются в одну колонку, сохраняя текущее мобильное поведение профиля.
## Capabilities
### New Capabilities
- `profile-dashboard`: единый раздел личного кабинета с боковым меню (25/75), вложенной маршрутизацией по пунктам, закреплённой сводкой профиля, интеграцией транзакций как пункта меню и состоянием загрузки контента.
### Modified Capabilities
<!-- Нет существующих спеков, требующих изменения требований. -->
## Impact
- **pages**: `pages/profile` (переработка `ProfilePage` в layout с `<Outlet/>` или переключателем пунктов), `pages/transactions` (контент переезжает в пункт меню; маршрут редиректится).
- **widgets**: `widgets/profile` (новый виджет бокового меню / навигации; `ProfileSummary`, `ProfileDetails`, `SecuritySection`, `MnemonicSection`, `KycBanner` переиспользуются), `widgets/transactions-list`, `widgets/purchase-requests-list` (переиспользуются как контент пункта).
- **shared**: `shared/config/routes.ts` — новые вложенные маршруты профиля и редирект со старого `/transactions`; `shared/ui/Spinner` — переиспользуется.
- **app**: `app/providers/RouterProvider.tsx` — описание вложенных маршрутов под `ProtectedRoute`, согласование с `WalletLayout` (профиль сейчас рендерит собственный `WalletHeader`, а транзакции живут внутри `WalletLayout` — нужно унифицировать обёртку).
- Тестового раннера нет — проверка через `npm run build` (tsc) и ручной прогон.

View File

@@ -0,0 +1,77 @@
## ADDED Requirements
### Requirement: Two-column dashboard layout
The profile section SHALL be presented as a two-column layout: a left menu column occupying approximately 25% of the available width and a right content column occupying approximately 75%. On viewports narrower than 1024px the two columns SHALL collapse into a single stacked column.
#### Scenario: Wide viewport shows two columns
- **WHEN** an authenticated user opens the profile section on a viewport ≥ 1024px wide
- **THEN** the left menu column (~25%) and the right content column (~75%) are displayed side by side
#### Scenario: Narrow viewport stacks columns
- **WHEN** an authenticated user opens the profile section on a viewport < 1024px wide
- **THEN** the menu and the content are stacked vertically in a single column
### Requirement: Pinned profile summary
The left menu column SHALL display the profile summary (`ProfileSummary` — avatar and identity) pinned at the top, above the list of menu items, and it SHALL remain visible regardless of which menu item is active.
#### Scenario: Summary visible on every item
- **WHEN** the user switches between any menu items
- **THEN** the profile summary remains displayed at the top of the left column
### Requirement: Menu items for profile sections and transactions
The left menu SHALL list selectable items for: Данные профиля (`ProfileDetails`), Безопасность (`SecuritySection`), Мнемоническая фраза (`MnemonicSection`), and Транзакции (transactions content). Selecting an item SHALL render its content in the right column, and the currently active item SHALL be visually highlighted.
#### Scenario: Selecting a menu item shows its content
- **WHEN** the user clicks a menu item
- **THEN** the right column renders that item's content and the clicked item is marked as active
#### Scenario: Transactions appears as a menu item
- **WHEN** the user views the profile menu
- **THEN** a «Транзакции» item is present alongside the profile section items, and selecting it renders the transactions content (list plus the «Транзакции / Заявки» tabs for legal-entity accounts)
### Requirement: Per-item routing with default redirect
Each menu item SHALL have its own URL under `/profile` (`/profile/details`, `/profile/security`, `/profile/mnemonic`, `/profile/transactions`) so that the browser Back button and direct links select the corresponding item. Navigating to `/profile` SHALL redirect to the default item `/profile/details`.
#### Scenario: Direct link opens the matching item
- **WHEN** the user navigates directly to `/profile/transactions`
- **THEN** the transactions item is active and its content is shown in the right column
#### Scenario: Bare profile path redirects to default
- **WHEN** the user navigates to `/profile`
- **THEN** the app redirects to `/profile/details` and shows the profile data item
#### Scenario: Back button restores previous item
- **WHEN** the user selects one item and then another, and then presses the browser Back button
- **THEN** the previously active item is restored
### Requirement: Legacy transactions route redirect
The legacy route `/transactions` SHALL redirect to `/profile/transactions` so existing links and bookmarks continue to work.
#### Scenario: Old transactions URL redirects
- **WHEN** the user navigates to `/transactions`
- **THEN** the app redirects to `/profile/transactions`
### Requirement: KYC banner placement
The KYC banner (`KycBanner`) SHALL be displayed above the content in the right column.
#### Scenario: KYC banner shown above content
- **WHEN** the profile section is displayed and the KYC banner is applicable
- **THEN** the banner appears above the active item's content in the right column
### Requirement: Loading state for content
While the data for the active menu item is loading, the right column SHALL show a loading indicator (the shared `Spinner`) while the left menu remains interactive.
#### Scenario: Spinner while data loads
- **WHEN** the active item's data is still loading
- **THEN** a loading indicator is shown in the right column and the user can still click other menu items
#### Scenario: Content replaces spinner when ready
- **WHEN** the active item's data finishes loading
- **THEN** the loading indicator is replaced by the item's content

View File

@@ -0,0 +1,38 @@
## 1. Маршруты и конфиг
- [x] 1.1 Добавить в `shared/config/routes.ts` константы вложенных путей профиля (`PROFILE_DETAILS = '/profile/details'`, `PROFILE_SECURITY`, `PROFILE_MNEMONIC`, `PROFILE_TRANSACTIONS`); сохранить `TRANSACTIONS` для обратного редиректа.
- [x] 1.2 В `app/providers/RouterProvider.tsx` обернуть `ProfilePage` в `<WalletLayout footer />` и описать вложенные маршруты: `index``<Navigate to="details" replace />`, `details`, `security`, `mnemonic`, `transactions`.
- [x] 1.3 Заменить маршрут `/transactions` на `<Navigate to="/profile/transactions" replace />`; убрать прямой рендер `TransactionsPage` по `/transactions`.
- [x] 1.4 Добавить поясняющие комментарии о двух уровнях `<Outlet/>` (WalletLayout → ProfilePage).
## 2. Layout страницы профиля
- [x] 2.1 Переработать `pages/profile/ui/ProfilePage.tsx` в layout: убрать собственный `WalletHeader`; двухколоночная разметка — слева `ProfileMenu`, справа `KycBanner` над `<Outlet/>`.
- [x] 2.2 Обновить `pages/profile/ui/ProfilePage.module.css`: колонки ~25% / ~75% + `gap`; перенести медиа-брейкпоинты (< 1024px — стек в одну колонку, < 640px — паддинги).
## 3. Боковое меню (widget)
- [x] 3.1 Создать `widgets/profile/ui/ProfileMenu.tsx`: сверху закреплённый `ProfileSummary`, ниже список пунктов через `NavLink` (Данные профиля, Безопасность, Мнемоническая фраза, Транзакции) с подсветкой активного (`aria-current`/класс).
- [x] 3.2 Создать `widgets/profile/ui/ProfileMenu.module.css` (список, активное состояние, отступы).
- [x] 3.3 Экспортировать `ProfileMenu` из `widgets/profile/index.ts`.
## 4. Контент-пункты
- [x] 4.1 Создать компонент-пункт «Данные профиля» (рендерит `<ProfileDetails/>`); показывать `Spinner`, пока `useMe` грузится.
- [x] 4.2 Создать компонент-пункт «Безопасность» (`<SecuritySection/>`) с состоянием загрузки.
- [x] 4.3 Создать компонент-пункт «Мнемоническая фраза» (`<MnemonicSection/>`) с состоянием загрузки.
- [x] 4.4 Вынести содержимое `pages/transactions/ui/TransactionsPage.tsx` (список + табы «Транзакции/Заявки» для юр. лиц через `useMe`) в переиспользуемый блок и подключить как компонент-пункт «Транзакции»; показывать `Spinner` во время загрузки.
## 5. Состояние загрузки
- [x] 5.1 Заменить `if (!data) return null` в блоках профиля на отображение `<Spinner label="Загрузка" />` в области контента (меню остаётся доступным).
- [x] 5.2 Убедиться, что `Spinner` центрируется в правой колонке (использовать `fullscreen`/класс в пределах контента).
## 6. Проверка
- [x] 6.1 `npm run build` (tsc) проходит без ошибок типов и сборки.
- [ ] 6.2 Ручная проверка deep-link: прямой переход на `/profile/details`, `/profile/security`, `/profile/mnemonic`, `/profile/transactions` открывает нужный пункт.
- [ ] 6.3 `/profile` редиректит на `/profile/details`; `/transactions` редиректит на `/profile/transactions`.
- [ ] 6.4 Кнопка «Назад» переключает между ранее открытыми пунктами; активный пункт меню подсвечен.
- [ ] 6.5 Транзакции: для юр. лица видны табы «Транзакции/Заявки», для физлица — только список.
- [ ] 6.6 Мобильный вид (< 1024px): меню и контент схлопываются в одну колонку; `KycBanner` и `Spinner` отображаются корректно.