profile refactor
This commit is contained in:
BIN
cbm-ui.zip
Normal file
BIN
cbm-ui.zip
Normal file
Binary file not shown.
22
cbm-ui/LICENSE
Normal file
22
cbm-ui/LICENSE
Normal 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.
|
||||
|
||||
6711
cbm-ui/THIRD_PARTY_NOTICES.md
Normal file
6711
cbm-ui/THIRD_PARTY_NOTICES.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
cbm-ui/codebase-memory-mcp.exe
Normal file
BIN
cbm-ui/codebase-memory-mcp.exe
Normal file
Binary file not shown.
154
cbm-ui/install.ps1
Normal file
154
cbm-ui/install.ps1
Normal 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
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
161
dist/assets/index-NvuTFZg0.js
vendored
161
dist/assets/index-NvuTFZg0.js
vendored
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
||||
<script type="module" crossorigin src="/assets/index-NvuTFZg0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CzU7MdbY.css">
|
||||
<script type="module" crossorigin src="/assets/index-B1RucWiR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B3CuZijI.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
154
install.ps1
Normal file
154
install.ps1
Normal 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."
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-29
|
||||
96
openspec/changes/merge-profile-transactions/design.md
Normal file
96
openspec/changes/merge-profile-transactions/design.md
Normal 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.)
|
||||
35
openspec/changes/merge-profile-transactions/proposal.md
Normal file
35
openspec/changes/merge-profile-transactions/proposal.md
Normal 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) и ручной прогон.
|
||||
@@ -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
|
||||
38
openspec/changes/merge-profile-transactions/tasks.md
Normal file
38
openspec/changes/merge-profile-transactions/tasks.md
Normal 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` отображаются корректно.
|
||||
@@ -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 { WalletPage } from '@pages/wallet'
|
||||
import { SwapPage } from '@pages/swap'
|
||||
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 { RegisterPage } from '@pages/register'
|
||||
import { RegisterTestPage } from '@pages/register-test'
|
||||
@@ -16,7 +16,6 @@ import { PolitikaPage } from '@pages/politika-personalnyh-dannyh'
|
||||
import { PolitikaCookiePage } from '@pages/politika-cookie'
|
||||
import { SoglasiePage } from '@pages/soglasie-personalnyh-dannyh'
|
||||
import { ReestryPage } from '@pages/reestr-pd-rkn'
|
||||
import { TransactionsPage } from '@pages/transactions'
|
||||
import { StakingPage } from '@pages/staking'
|
||||
import { PoolsPage } from '@pages/pools'
|
||||
import { WalletLayout } from '@widgets/wallet-layout'
|
||||
@@ -52,14 +51,25 @@ export function RouterProvider() {
|
||||
<Route element={<WalletLayout footer />}>
|
||||
<Route path={ROUTES.SWAP} element={<SwapPage />} />
|
||||
<Route path={ROUTES.BRIDGE} element={<BridgePage />} />
|
||||
<Route path={ROUTES.TRANSACTIONS} element={<TransactionsPage />} />
|
||||
<Route path={ROUTES.STAKING} element={<StakingPage />} />
|
||||
<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>
|
||||
|
||||
{/* 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_CHAIN} element={<WalletPage />} />
|
||||
<Route path={ROUTES.PROFILE} element={<ProfilePage />} />
|
||||
<Route path={ROUTES.SEED_PHRASE} element={<SeedPhrasePage />} />
|
||||
<Route path={ROUTES.KYC} element={<KycPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
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'
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-deep);
|
||||
}
|
||||
|
||||
.main {
|
||||
max-width: 1024px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 40px 32px 60px;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 25% 1fr;
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.sections {
|
||||
flex: 1;
|
||||
.menu {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* < 1024px: avatar + sections stack into a single column */
|
||||
/* < 1024px: menu and content stack into a single column */
|
||||
@media (max-width: 1023px) {
|
||||
.main {
|
||||
flex-direction: column;
|
||||
.page {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 24px 20px 40px;
|
||||
gap: 24px;
|
||||
}
|
||||
@@ -32,7 +31,7 @@
|
||||
|
||||
/* < 640px: tighter page padding */
|
||||
@media (max-width: 639px) {
|
||||
.main {
|
||||
.page {
|
||||
padding: 16px 16px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { WalletHeader } from '@widgets/wallet-header'
|
||||
import { KycBanner, ProfileSummary, ProfileDetails, SecuritySection, MnemonicSection } from '@widgets/profile'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { KycBanner, ProfileMenu } from '@widgets/profile'
|
||||
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() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<WalletHeader />
|
||||
<KycBanner />
|
||||
<main className={styles.main}>
|
||||
<ProfileSummary />
|
||||
<div className={styles.sections}>
|
||||
<ProfileDetails />
|
||||
<SecuritySection />
|
||||
<MnemonicSection />
|
||||
</div>
|
||||
<div className={styles.menu}>
|
||||
<ProfileMenu />
|
||||
</div>
|
||||
<main className={styles.content}>
|
||||
<KycBanner />
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
10
src/pages/profile/ui/items/MnemonicItem.tsx
Normal file
10
src/pages/profile/ui/items/MnemonicItem.tsx
Normal 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 />
|
||||
}
|
||||
11
src/pages/profile/ui/items/ProfileDetailsItem.tsx
Normal file
11
src/pages/profile/ui/items/ProfileDetailsItem.tsx
Normal 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 />
|
||||
}
|
||||
10
src/pages/profile/ui/items/SecurityItem.tsx
Normal file
10
src/pages/profile/ui/items/SecurityItem.tsx
Normal 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 />
|
||||
}
|
||||
7
src/pages/profile/ui/items/TransactionsItem.tsx
Normal file
7
src/pages/profile/ui/items/TransactionsItem.tsx
Normal 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 />
|
||||
}
|
||||
@@ -1,45 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
import { useMe } from '@features/auth'
|
||||
import { TransactionsList } from '@widgets/transactions-list'
|
||||
import { PurchaseRequestsList } from '@widgets/purchase-requests-list'
|
||||
import { TransactionsPanel } from '@widgets/transactions-panel'
|
||||
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() {
|
||||
const { data } = useMe()
|
||||
const [tab, setTab] = useState<Tab>('transactions')
|
||||
|
||||
const isLegal = !!data && data.account_type !== 'individual'
|
||||
const activeTab: Tab = isLegal ? tab : 'transactions'
|
||||
|
||||
return (
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.glow} />
|
||||
<h1 className={styles.title}>Транзакции</h1>
|
||||
|
||||
{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 />}
|
||||
<TransactionsPanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ export const ROUTES = {
|
||||
REGISTER_TEST: '/register-test',
|
||||
CONVERTER_TEST: '/converter-test',
|
||||
PROFILE: '/profile',
|
||||
PROFILE_DETAILS: '/profile/details',
|
||||
PROFILE_SECURITY: '/profile/security',
|
||||
PROFILE_MNEMONIC: '/profile/mnemonic',
|
||||
PROFILE_TRANSACTIONS: '/profile/transactions',
|
||||
SEED_PHRASE: '/seed-phrase',
|
||||
CONVERTER: '/converter',
|
||||
KYC: '/kyc',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { ProfileAvatar } from './ui/ProfileAvatar'
|
||||
export { ProfileSection } from './ui/ProfileSection'
|
||||
export { ProfileSummary } from './ui/ProfileSummary'
|
||||
export { ProfileMenu } from './ui/ProfileMenu'
|
||||
export { ProfileDetails } from './ui/ProfileDetails'
|
||||
export { SecuritySection } from './ui/SecuritySection'
|
||||
export { MnemonicSection } from './ui/MnemonicSection'
|
||||
|
||||
53
src/widgets/profile/ui/ProfileMenu.module.css
Normal file
53
src/widgets/profile/ui/ProfileMenu.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
36
src/widgets/profile/ui/ProfileMenu.tsx
Normal file
36
src/widgets/profile/ui/ProfileMenu.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
src/widgets/transactions-panel/index.ts
Normal file
1
src/widgets/transactions-panel/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { TransactionsPanel } from './ui/TransactionsPanel'
|
||||
@@ -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);
|
||||
}
|
||||
48
src/widgets/transactions-panel/ui/TransactionsPanel.tsx
Normal file
48
src/widgets/transactions-panel/ui/TransactionsPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export function WalletHeader() {
|
||||
<Link to={ROUTES.POOLS} className={styles.dropdownItem} onClick={() => setOpen(false)}>
|
||||
Пулы
|
||||
</Link>
|
||||
<Link to={ROUTES.TRANSACTIONS} className={styles.dropdownItem} onClick={() => setOpen(false)}>
|
||||
<Link to={ROUTES.PROFILE_TRANSACTIONS} className={styles.dropdownItem} onClick={() => setOpen(false)}>
|
||||
Транзакции
|
||||
</Link>
|
||||
<button className={`${styles.dropdownItem} ${styles.danger}`} onClick={handleLogout}>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user