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

BIN
cbm-ui.zip Normal file

Binary file not shown.

22
cbm-ui/LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 DeusData
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

Binary file not shown.

154
cbm-ui/install.ps1 Normal file
View File

@@ -0,0 +1,154 @@
# install.ps1 — One-line installer for codebase-memory-mcp (Windows).
#
# Usage: see README.md for install instructions.
#
# Environment:
# CBM_DOWNLOAD_URL Override base URL for downloads (for testing)
$ErrorActionPreference = "Stop"
# Enforce TLS 1.2+ (older PowerShell defaults to TLS 1.0 which GitHub rejects)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
$Repo = "DeusData/codebase-memory-mcp"
$InstallDir = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp"
$BinName = "codebase-memory-mcp.exe"
$BaseUrl = if ($env:CBM_DOWNLOAD_URL) { $env:CBM_DOWNLOAD_URL } else { "https://github.com/$Repo/releases/latest/download" }
# Security: reject non-HTTPS download URLs (defense-in-depth)
if (-not $BaseUrl.StartsWith("https://") -and -not $BaseUrl.StartsWith("http://localhost") -and -not $BaseUrl.StartsWith("http://127.0.0.1")) {
Write-Host "error: refusing non-HTTPS download URL: $BaseUrl" -ForegroundColor Red
exit 1
}
# Detect variant from args (--ui or --standard)
$Variant = "standard"
$SkipConfig = $false
foreach ($arg in $args) {
if ($arg -eq "--ui") { $Variant = "ui" }
if ($arg -eq "--standard") { $Variant = "standard" }
if ($arg -eq "--skip-config") { $SkipConfig = $true }
if ($arg -like "--dir=*") { $InstallDir = $arg.Substring(6) }
}
Write-Host "codebase-memory-mcp installer (Windows)"
Write-Host " variant: $Variant"
Write-Host " target: $InstallDir\$BinName"
Write-Host ""
# Build download URL
if ($Variant -eq "ui") {
$Archive = "codebase-memory-mcp-ui-windows-amd64.zip"
} else {
$Archive = "codebase-memory-mcp-windows-amd64.zip"
}
$Url = "$BaseUrl/$Archive"
# Download
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-install-$(Get-Random)"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
Write-Host "Downloading $Archive..."
try {
Invoke-WebRequest -Uri $Url -OutFile "$TmpDir\$Archive" -UseBasicParsing
} catch {
Write-Host "error: download failed: $_" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
exit 1
}
# Checksum verification
$ChecksumUrl = "$BaseUrl/checksums.txt"
try {
Invoke-WebRequest -Uri $ChecksumUrl -OutFile "$TmpDir\checksums.txt" -UseBasicParsing
$checksumLine = Get-Content "$TmpDir\checksums.txt" | Where-Object { $_ -like "*$Archive*" }
if ($checksumLine) {
$expected = ($checksumLine -split '\s+')[0]
$actual = (Get-FileHash -Path "$TmpDir\$Archive" -Algorithm SHA256).Hash.ToLower()
if ($expected -ne $actual) {
Write-Host "error: CHECKSUM MISMATCH!" -ForegroundColor Red
Write-Host " expected: $expected"
Write-Host " actual: $actual"
Remove-Item -Recurse -Force $TmpDir
exit 1
}
Write-Host "Checksum verified."
}
} catch {
Write-Host "warning: could not verify checksum (non-fatal)"
}
# Extract
Write-Host "Extracting..."
Expand-Archive -Path "$TmpDir\$Archive" -DestinationPath $TmpDir -Force
$DlBin = Join-Path $TmpDir $BinName
if (-not (Test-Path $DlBin)) {
# UI variant may have different name in zip
$UiBin = Join-Path $TmpDir "codebase-memory-mcp-ui.exe"
if (Test-Path $UiBin) {
Rename-Item $UiBin $BinName
$DlBin = Join-Path $TmpDir $BinName
} else {
Write-Host "error: binary not found after extraction" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
}
# Install
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$Dest = Join-Path $InstallDir $BinName
# Handle replace-if-running (rename-aside)
if (Test-Path $Dest) {
$OldDest = "$Dest.old"
Remove-Item $OldDest -Force -ErrorAction SilentlyContinue
try {
Rename-Item $Dest $OldDest -ErrorAction Stop
} catch {
Write-Host "warning: could not rename existing binary (may be in use)"
}
}
Copy-Item $DlBin $Dest -Force
# Verify
try {
$ver = & $Dest --version 2>&1
Write-Host "Installed: $ver"
} catch {
Write-Host "error: installed binary failed to run" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
# Configure agents
if ($SkipConfig) {
Write-Host ""
Write-Host "Skipping agent configuration (--skip-config)"
} else {
Write-Host ""
Write-Host "Configuring coding agents..."
try {
& $Dest install -y 2>&1 | Write-Host
} catch {
Write-Host "Agent configuration failed (non-fatal)."
Write-Host "Run manually: codebase-memory-mcp install"
}
}
# Add to PATH (user scope, no admin needed)
$UserPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($UserPath -notlike "*$InstallDir*") {
[Environment]::SetEnvironmentVariable("PATH", "$UserPath;$InstallDir", "User")
$env:PATH = "$env:PATH;$InstallDir"
Write-Host "Added $InstallDir to user PATH"
}
# Cleanup
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "Done! Restart your terminal and coding agent to start using codebase-memory-mcp."

161
dist/assets/index-B1RucWiR.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ЭКСА — Ваш мост в мир цифровых активов</title> <title>ЭКСА — Ваш мост в мир цифровых активов</title>
<script type="module" crossorigin src="/assets/index-NvuTFZg0.js"></script> <script type="module" crossorigin src="/assets/index-B1RucWiR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CzU7MdbY.css"> <link rel="stylesheet" crossorigin href="/assets/index-B3CuZijI.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

154
install.ps1 Normal file
View File

@@ -0,0 +1,154 @@
# install.ps1 — One-line installer for codebase-memory-mcp (Windows).
#
# Usage: see README.md for install instructions.
#
# Environment:
# CBM_DOWNLOAD_URL Override base URL for downloads (for testing)
$ErrorActionPreference = "Stop"
# Enforce TLS 1.2+ (older PowerShell defaults to TLS 1.0 which GitHub rejects)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
$Repo = "DeusData/codebase-memory-mcp"
$InstallDir = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp"
$BinName = "codebase-memory-mcp.exe"
$BaseUrl = if ($env:CBM_DOWNLOAD_URL) { $env:CBM_DOWNLOAD_URL } else { "https://github.com/$Repo/releases/latest/download" }
# Security: reject non-HTTPS download URLs (defense-in-depth)
if (-not $BaseUrl.StartsWith("https://") -and -not $BaseUrl.StartsWith("http://localhost") -and -not $BaseUrl.StartsWith("http://127.0.0.1")) {
Write-Host "error: refusing non-HTTPS download URL: $BaseUrl" -ForegroundColor Red
exit 1
}
# Detect variant from args (--ui or --standard)
$Variant = "standard"
$SkipConfig = $false
foreach ($arg in $args) {
if ($arg -eq "--ui") { $Variant = "ui" }
if ($arg -eq "--standard") { $Variant = "standard" }
if ($arg -eq "--skip-config") { $SkipConfig = $true }
if ($arg -like "--dir=*") { $InstallDir = $arg.Substring(6) }
}
Write-Host "codebase-memory-mcp installer (Windows)"
Write-Host " variant: $Variant"
Write-Host " target: $InstallDir\$BinName"
Write-Host ""
# Build download URL
if ($Variant -eq "ui") {
$Archive = "codebase-memory-mcp-ui-windows-amd64.zip"
} else {
$Archive = "codebase-memory-mcp-windows-amd64.zip"
}
$Url = "$BaseUrl/$Archive"
# Download
$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "cbm-install-$(Get-Random)"
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
Write-Host "Downloading $Archive..."
try {
Invoke-WebRequest -Uri $Url -OutFile "$TmpDir\$Archive" -UseBasicParsing
} catch {
Write-Host "error: download failed: $_" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
exit 1
}
# Checksum verification
$ChecksumUrl = "$BaseUrl/checksums.txt"
try {
Invoke-WebRequest -Uri $ChecksumUrl -OutFile "$TmpDir\checksums.txt" -UseBasicParsing
$checksumLine = Get-Content "$TmpDir\checksums.txt" | Where-Object { $_ -like "*$Archive*" }
if ($checksumLine) {
$expected = ($checksumLine -split '\s+')[0]
$actual = (Get-FileHash -Path "$TmpDir\$Archive" -Algorithm SHA256).Hash.ToLower()
if ($expected -ne $actual) {
Write-Host "error: CHECKSUM MISMATCH!" -ForegroundColor Red
Write-Host " expected: $expected"
Write-Host " actual: $actual"
Remove-Item -Recurse -Force $TmpDir
exit 1
}
Write-Host "Checksum verified."
}
} catch {
Write-Host "warning: could not verify checksum (non-fatal)"
}
# Extract
Write-Host "Extracting..."
Expand-Archive -Path "$TmpDir\$Archive" -DestinationPath $TmpDir -Force
$DlBin = Join-Path $TmpDir $BinName
if (-not (Test-Path $DlBin)) {
# UI variant may have different name in zip
$UiBin = Join-Path $TmpDir "codebase-memory-mcp-ui.exe"
if (Test-Path $UiBin) {
Rename-Item $UiBin $BinName
$DlBin = Join-Path $TmpDir $BinName
} else {
Write-Host "error: binary not found after extraction" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
}
# Install
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$Dest = Join-Path $InstallDir $BinName
# Handle replace-if-running (rename-aside)
if (Test-Path $Dest) {
$OldDest = "$Dest.old"
Remove-Item $OldDest -Force -ErrorAction SilentlyContinue
try {
Rename-Item $Dest $OldDest -ErrorAction Stop
} catch {
Write-Host "warning: could not rename existing binary (may be in use)"
}
}
Copy-Item $DlBin $Dest -Force
# Verify
try {
$ver = & $Dest --version 2>&1
Write-Host "Installed: $ver"
} catch {
Write-Host "error: installed binary failed to run" -ForegroundColor Red
Remove-Item -Recurse -Force $TmpDir
exit 1
}
# Configure agents
if ($SkipConfig) {
Write-Host ""
Write-Host "Skipping agent configuration (--skip-config)"
} else {
Write-Host ""
Write-Host "Configuring coding agents..."
try {
& $Dest install -y 2>&1 | Write-Host
} catch {
Write-Host "Agent configuration failed (non-fatal)."
Write-Host "Run manually: codebase-memory-mcp install"
}
}
# Add to PATH (user scope, no admin needed)
$UserPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($UserPath -notlike "*$InstallDir*") {
[Environment]::SetEnvironmentVariable("PATH", "$UserPath;$InstallDir", "User")
$env:PATH = "$env:PATH;$InstallDir"
Write-Host "Added $InstallDir to user PATH"
}
# Cleanup
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "Done! Restart your terminal and coding agent to start using codebase-memory-mcp."

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` отображаются корректно.

View File

@@ -1,9 +1,9 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom' import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { HomePage } from '@pages/home' import { HomePage } from '@pages/home'
import { WalletPage } from '@pages/wallet' import { WalletPage } from '@pages/wallet'
import { SwapPage } from '@pages/swap' import { SwapPage } from '@pages/swap'
import { BridgePage } from '@pages/bridge' import { BridgePage } from '@pages/bridge'
import { ProfilePage } from '@pages/profile' import { ProfilePage, ProfileDetailsItem, SecurityItem, MnemonicItem, TransactionsItem } from '@pages/profile'
import { LoginPage } from '@pages/login' import { LoginPage } from '@pages/login'
import { RegisterPage } from '@pages/register' import { RegisterPage } from '@pages/register'
import { RegisterTestPage } from '@pages/register-test' import { RegisterTestPage } from '@pages/register-test'
@@ -16,7 +16,6 @@ import { PolitikaPage } from '@pages/politika-personalnyh-dannyh'
import { PolitikaCookiePage } from '@pages/politika-cookie' import { PolitikaCookiePage } from '@pages/politika-cookie'
import { SoglasiePage } from '@pages/soglasie-personalnyh-dannyh' import { SoglasiePage } from '@pages/soglasie-personalnyh-dannyh'
import { ReestryPage } from '@pages/reestr-pd-rkn' import { ReestryPage } from '@pages/reestr-pd-rkn'
import { TransactionsPage } from '@pages/transactions'
import { StakingPage } from '@pages/staking' import { StakingPage } from '@pages/staking'
import { PoolsPage } from '@pages/pools' import { PoolsPage } from '@pages/pools'
import { WalletLayout } from '@widgets/wallet-layout' import { WalletLayout } from '@widgets/wallet-layout'
@@ -52,14 +51,25 @@ export function RouterProvider() {
<Route element={<WalletLayout footer />}> <Route element={<WalletLayout footer />}>
<Route path={ROUTES.SWAP} element={<SwapPage />} /> <Route path={ROUTES.SWAP} element={<SwapPage />} />
<Route path={ROUTES.BRIDGE} element={<BridgePage />} /> <Route path={ROUTES.BRIDGE} element={<BridgePage />} />
<Route path={ROUTES.TRANSACTIONS} element={<TransactionsPage />} />
<Route path={ROUTES.STAKING} element={<StakingPage />} /> <Route path={ROUTES.STAKING} element={<StakingPage />} />
<Route path={ROUTES.POOLS} element={<PoolsPage />} /> <Route path={ROUTES.POOLS} element={<PoolsPage />} />
{/* Profile dashboard: WalletLayout gives header/footer, ProfilePage
renders the menu + an inner <Outlet/> for the active item. */}
<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>
{/* Legacy route — transactions now live inside the profile dashboard. */}
<Route path={ROUTES.TRANSACTIONS} element={<Navigate to={ROUTES.PROFILE_TRANSACTIONS} replace />} />
<Route path={ROUTES.WALLET} element={<WalletPage />} /> <Route path={ROUTES.WALLET} element={<WalletPage />} />
<Route path={ROUTES.WALLET_CHAIN} element={<WalletPage />} /> <Route path={ROUTES.WALLET_CHAIN} element={<WalletPage />} />
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
<Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} /> <Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} />
<Route path={ROUTES.KYC} element={<KycPage />} /> <Route path={ROUTES.KYC} element={<KycPage />} />
</Route> </Route>

View File

@@ -1 +1,5 @@
export { ProfilePage } from './ui/ProfilePage' export { ProfilePage } from './ui/ProfilePage'
export { ProfileDetailsItem } from './ui/items/ProfileDetailsItem'
export { SecurityItem } from './ui/items/SecurityItem'
export { MnemonicItem } from './ui/items/MnemonicItem'
export { TransactionsItem } from './ui/items/TransactionsItem'

View File

@@ -1,30 +1,29 @@
.page { .page {
min-height: 100vh; max-width: 1200px;
display: flex;
flex-direction: column;
background: var(--bg-deep);
}
.main {
max-width: 1024px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 40px 32px 60px; padding: 40px 32px 60px;
display: flex; display: grid;
grid-template-columns: 25% 1fr;
gap: 32px; gap: 32px;
align-items: start;
} }
.sections { .menu {
flex: 1; min-width: 0;
}
.content {
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
/* < 1024px: avatar + sections stack into a single column */ /* < 1024px: menu and content stack into a single column */
@media (max-width: 1023px) { @media (max-width: 1023px) {
.main { .page {
flex-direction: column; grid-template-columns: 1fr;
padding: 24px 20px 40px; padding: 24px 20px 40px;
gap: 24px; gap: 24px;
} }
@@ -32,7 +31,7 @@
/* < 640px: tighter page padding */ /* < 640px: tighter page padding */
@media (max-width: 639px) { @media (max-width: 639px) {
.main { .page {
padding: 16px 16px 40px; padding: 16px 16px 40px;
} }
} }

View File

@@ -1,19 +1,19 @@
import { WalletHeader } from '@widgets/wallet-header' import { Outlet } from 'react-router-dom'
import { KycBanner, ProfileSummary, ProfileDetails, SecuritySection, MnemonicSection } from '@widgets/profile' import { KycBanner, ProfileMenu } from '@widgets/profile'
import styles from './ProfilePage.module.css' import styles from './ProfilePage.module.css'
// Profile dashboard layout. The outer WalletLayout (see RouterProvider) renders
// the header/footer; here we split into a ~25% menu column and a ~75% content
// column. The active item is rendered into the inner <Outlet/>.
export function ProfilePage() { export function ProfilePage() {
return ( return (
<div className={styles.page}> <div className={styles.page}>
<WalletHeader /> <div className={styles.menu}>
<KycBanner /> <ProfileMenu />
<main className={styles.main}> </div>
<ProfileSummary /> <main className={styles.content}>
<div className={styles.sections}> <KycBanner />
<ProfileDetails /> <Outlet />
<SecuritySection />
<MnemonicSection />
</div>
</main> </main>
</div> </div>
) )

View File

@@ -0,0 +1,10 @@
import { useMe } from '@features/auth'
import { MnemonicSection } from '@widgets/profile'
import { Spinner } from '@shared/ui'
// «Мнемоническая фраза» dashboard item.
export function MnemonicItem() {
const { isLoading } = useMe()
if (isLoading) return <Spinner fullscreen label="Загрузка" />
return <MnemonicSection />
}

View File

@@ -0,0 +1,11 @@
import { useMe } from '@features/auth'
import { ProfileDetails } from '@widgets/profile'
import { Spinner } from '@shared/ui'
// «Данные профиля» — default dashboard item. Shows a spinner while the profile
// data is still loading, then the details form.
export function ProfileDetailsItem() {
const { isLoading } = useMe()
if (isLoading) return <Spinner fullscreen label="Загрузка" />
return <ProfileDetails />
}

View File

@@ -0,0 +1,10 @@
import { useMe } from '@features/auth'
import { SecuritySection } from '@widgets/profile'
import { Spinner } from '@shared/ui'
// «Безопасность» dashboard item.
export function SecurityItem() {
const { isLoading } = useMe()
if (isLoading) return <Spinner fullscreen label="Загрузка" />
return <SecuritySection />
}

View File

@@ -0,0 +1,7 @@
import { TransactionsPanel } from '@widgets/transactions-panel'
// «Транзакции» dashboard item — the shared transactions panel (list + tabs).
// The panel handles its own loading state.
export function TransactionsItem() {
return <TransactionsPanel />
}

View File

@@ -1,45 +1,15 @@
import { useState } from 'react' import { TransactionsPanel } from '@widgets/transactions-panel'
import { useMe } from '@features/auth'
import { TransactionsList } from '@widgets/transactions-list'
import { PurchaseRequestsList } from '@widgets/purchase-requests-list'
import styles from './TransactionsPage.module.css' import styles from './TransactionsPage.module.css'
type Tab = 'transactions' | 'requests' // The standalone /transactions route now redirects to /profile/transactions
// (see RouterProvider). This page is kept as a thin wrapper around the shared
// TransactionsPanel for any direct/legacy usage.
export function TransactionsPage() { export function TransactionsPage() {
const { data } = useMe()
const [tab, setTab] = useState<Tab>('transactions')
const isLegal = !!data && data.account_type !== 'individual'
const activeTab: Tab = isLegal ? tab : 'transactions'
return ( return (
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.glow} /> <div className={styles.glow} />
<h1 className={styles.title}>Транзакции</h1> <h1 className={styles.title}>Транзакции</h1>
<TransactionsPanel />
{isLegal && (
<div className={styles.tabs}>
<button
type="button"
className={styles.tab}
data-active={activeTab === 'transactions' || undefined}
onClick={() => setTab('transactions')}
>
Транзакции
</button>
<button
type="button"
className={styles.tab}
data-active={activeTab === 'requests' || undefined}
onClick={() => setTab('requests')}
>
Заявки
</button>
</div>
)}
{activeTab === 'transactions' ? <TransactionsList /> : <PurchaseRequestsList />}
</div> </div>
) )
} }

View File

@@ -9,6 +9,10 @@ export const ROUTES = {
REGISTER_TEST: '/register-test', REGISTER_TEST: '/register-test',
CONVERTER_TEST: '/converter-test', CONVERTER_TEST: '/converter-test',
PROFILE: '/profile', PROFILE: '/profile',
PROFILE_DETAILS: '/profile/details',
PROFILE_SECURITY: '/profile/security',
PROFILE_MNEMONIC: '/profile/mnemonic',
PROFILE_TRANSACTIONS: '/profile/transactions',
SEED_PHRASE: '/seed-phrase', SEED_PHRASE: '/seed-phrase',
CONVERTER: '/converter', CONVERTER: '/converter',
KYC: '/kyc', KYC: '/kyc',

View File

@@ -1,6 +1,7 @@
export { ProfileAvatar } from './ui/ProfileAvatar' export { ProfileAvatar } from './ui/ProfileAvatar'
export { ProfileSection } from './ui/ProfileSection' export { ProfileSection } from './ui/ProfileSection'
export { ProfileSummary } from './ui/ProfileSummary' export { ProfileSummary } from './ui/ProfileSummary'
export { ProfileMenu } from './ui/ProfileMenu'
export { ProfileDetails } from './ui/ProfileDetails' export { ProfileDetails } from './ui/ProfileDetails'
export { SecuritySection } from './ui/SecuritySection' export { SecuritySection } from './ui/SecuritySection'
export { MnemonicSection } from './ui/MnemonicSection' export { MnemonicSection } from './ui/MnemonicSection'

View File

@@ -0,0 +1,53 @@
.menu {
display: flex;
flex-direction: column;
gap: 24px;
}
.summary {
flex-shrink: 0;
}
.nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.item {
display: block;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid transparent;
color: var(--text-secondary);
font-size: 15px;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.item:hover {
background: rgba(255, 255, 255, 0.04);
color: var(--text-primary);
}
.itemActive {
background: rgba(74, 109, 255, 0.12);
border-color: rgba(74, 109, 255, 0.5);
color: var(--interactive, #4a6dff);
}
/* < 1024px: menu becomes a horizontal, scrollable row above the content */
@media (max-width: 1023px) {
.nav {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.item {
padding: 8px 14px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,36 @@
import { NavLink } from 'react-router-dom'
import { ROUTES } from '@shared/config/routes'
import { ProfileSummary } from './ProfileSummary'
import styles from './ProfileMenu.module.css'
const ITEMS = [
{ to: ROUTES.PROFILE_DETAILS, label: 'Данные профиля' },
{ to: ROUTES.PROFILE_SECURITY, label: 'Безопасность' },
{ to: ROUTES.PROFILE_MNEMONIC, label: 'Мнемоническая фраза' },
{ to: ROUTES.PROFILE_TRANSACTIONS, label: 'Транзакции' },
]
// Left column of the profile dashboard: pinned profile summary on top, then
// the navigable list of sections. Active item is highlighted via NavLink.
export function ProfileMenu() {
return (
<aside className={styles.menu}>
<div className={styles.summary}>
<ProfileSummary />
</div>
<nav className={styles.nav}>
{ITEMS.map(({ to, label }) => (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
isActive ? `${styles.item} ${styles.itemActive}` : styles.item
}
>
{label}
</NavLink>
))}
</nav>
</aside>
)
}

View File

@@ -0,0 +1 @@
export { TransactionsPanel } from './ui/TransactionsPanel'

View File

@@ -0,0 +1,37 @@
.panel {
position: relative;
z-index: 1;
}
.tabs {
display: flex;
gap: 8px;
margin: 0 0 20px;
}
.tab {
display: inline-flex;
align-items: center;
height: 38px;
padding: 0 20px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: var(--text-secondary);
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.tab:hover {
border-color: rgba(74, 109, 255, 0.4);
color: var(--text-primary);
}
.tab[data-active] {
background: rgba(74, 109, 255, 0.12);
border-color: rgba(74, 109, 255, 0.5);
color: var(--interactive, #4a6dff);
}

View File

@@ -0,0 +1,48 @@
import { useState } from 'react'
import { useMe } from '@features/auth'
import { Spinner } from '@shared/ui'
import { TransactionsList } from '@widgets/transactions-list'
import { PurchaseRequestsList } from '@widgets/purchase-requests-list'
import styles from './TransactionsPanel.module.css'
type Tab = 'transactions' | 'requests'
// Reusable transactions block: list plus the «Транзакции / Заявки» tabs that
// only legal-entity accounts see. Used as the «Транзакции» item of the profile
// dashboard.
export function TransactionsPanel() {
const { data, isLoading } = useMe()
const [tab, setTab] = useState<Tab>('transactions')
if (isLoading) return <Spinner fullscreen label="Загрузка" />
const isLegal = !!data && data.account_type !== 'individual'
const activeTab: Tab = isLegal ? tab : 'transactions'
return (
<div className={styles.panel}>
{isLegal && (
<div className={styles.tabs}>
<button
type="button"
className={styles.tab}
data-active={activeTab === 'transactions' || undefined}
onClick={() => setTab('transactions')}
>
Транзакции
</button>
<button
type="button"
className={styles.tab}
data-active={activeTab === 'requests' || undefined}
onClick={() => setTab('requests')}
>
Заявки
</button>
</div>
)}
{activeTab === 'transactions' ? <TransactionsList /> : <PurchaseRequestsList />}
</div>
)
}

View File

@@ -98,7 +98,7 @@ export function WalletHeader() {
<Link to={ROUTES.POOLS} className={styles.dropdownItem} onClick={() => setOpen(false)}> <Link to={ROUTES.POOLS} className={styles.dropdownItem} onClick={() => setOpen(false)}>
Пулы Пулы
</Link> </Link>
<Link to={ROUTES.TRANSACTIONS} className={styles.dropdownItem} onClick={() => setOpen(false)}> <Link to={ROUTES.PROFILE_TRANSACTIONS} className={styles.dropdownItem} onClick={() => setOpen(false)}>
Транзакции Транзакции
</Link> </Link>
<button className={`${styles.dropdownItem} ${styles.danger}`} onClick={handleLogout}> <button className={`${styles.dropdownItem} ${styles.danger}`} onClick={handleLogout}>

File diff suppressed because one or more lines are too long