profile refactor
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user