profile refactor

This commit is contained in:
2026-06-29 18:13:56 +03:00
parent 237112c302
commit d247e41d25
12 changed files with 272 additions and 200 deletions

161
dist/assets/index-19UfAL9p.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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
<script type="module" crossorigin src="/assets/index-B1RucWiR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B3CuZijI.css">
<script type="module" crossorigin src="/assets/index-19UfAL9p.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DsuaJlzX.css">
</head>
<body>
<div id="root"></div>

View File

@@ -104,12 +104,16 @@ export function useCreateWallet() {
return useMutation({ mutationFn: createWallet })
}
export function useRevealMnemonic() {
// `enabled` gates the request so callers can require an explicit user action
// (e.g. a "Показать" button) before the mnemonic is fetched. Defaults to true
// to preserve the standalone /seed-phrase page behavior.
export function useRevealMnemonic(enabled = true) {
return useQuery({
queryKey: ['wallet', 'mnemonic'],
queryFn: revealMnemonic,
staleTime: Infinity,
retry: false,
enabled,
})
}

View File

@@ -0,0 +1,6 @@
.hint {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
}

View File

@@ -1,10 +1,32 @@
import { useMe } from '@features/auth'
import { MnemonicSection } from '@widgets/profile'
import { Spinner } from '@shared/ui'
import { useState } from 'react'
import { useRevealMnemonic } from '@features/wallet'
import { SeedPhraseWidget } from '@widgets/seed-phrase'
import { ProfileSection } from '@widgets/profile'
import { Button, Spinner } from '@shared/ui'
import styles from './MnemonicItem.module.css'
// «Мнемоническая фраза» dashboard item.
// «Мнемоническая фраза» dashboard item. The mnemonic is fetched only after the
// user explicitly clicks "Показать мнемонику" (the query stays disabled until
// then), then the seed-phrase content is shown inline.
export function MnemonicItem() {
const { isLoading } = useMe()
const [revealed, setRevealed] = useState(false)
const { data: mnemonic, isLoading } = useRevealMnemonic(revealed)
if (!revealed) {
return (
<ProfileSection
title="Мнемоническая фраза"
actions={<Button variant="danger" onClick={() => setRevealed(true)}>Показать мнемонику</Button>}
>
<p className={styles.hint}>
Сид-фраза из 12 слов для восстановления кошелька. Никогда не передавайте её третьим лицам.
</p>
</ProfileSection>
)
}
if (isLoading) return <Spinner fullscreen label="Загрузка" />
return <MnemonicSection />
const words = mnemonic ? mnemonic.split(' ') : []
return <SeedPhraseWidget words={words} showProfileLink={false} />
}

View File

@@ -1,5 +1,5 @@
.col {
width: 200px;
width: 100%;
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -8,8 +8,9 @@
}
.avatar {
width: 150px;
height: 150px;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
border-radius: 50%;
background: linear-gradient(135deg, var(--grad-edge), var(--grad-center));
border: 2px solid rgba(255, 255, 255, 0.1);
@@ -73,6 +74,17 @@
.addPhoto {
display: none;
}
/* On mobile the avatar sits in a row next to the user info — keep it compact */
.col {
width: auto;
}
.avatar {
width: 150px;
height: 150px;
aspect-ratio: auto;
}
}
@media (max-width: 549px) {

View File

@@ -38,16 +38,29 @@
color: var(--interactive, #4a6dff);
}
/* < 1024px: menu becomes a horizontal, scrollable row above the content */
/* Mobile-only section selector — hidden on desktop. */
.select {
display: none;
width: 100%;
height: 44px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: var(--bg-deep);
color: var(--text-primary);
font-size: 15px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
}
/* < 1024px: replace the list with the select. */
@media (max-width: 1023px) {
.nav {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
display: none;
}
.item {
padding: 8px 14px;
font-size: 14px;
.select {
display: block;
}
}

View File

@@ -1,4 +1,4 @@
import { NavLink } from 'react-router-dom'
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
import { ROUTES } from '@shared/config/routes'
import { ProfileSummary } from './ProfileSummary'
import styles from './ProfileMenu.module.css'
@@ -6,18 +6,37 @@ 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: 'Транзакции' },
{ to: ROUTES.PROFILE_MNEMONIC, label: 'Мнемоническая фраза' },
]
// Left column of the profile dashboard: pinned profile summary on top, then
// the navigable list of sections. Active item is highlighted via NavLink.
// the section navigation. On desktop the navigation is a vertical list; on
// mobile it collapses into a <select> so only the chosen section is shown.
export function ProfileMenu() {
const navigate = useNavigate()
const { pathname } = useLocation()
const current = ITEMS.find((i) => pathname.startsWith(i.to))?.to ?? ITEMS[0].to
return (
<aside className={styles.menu}>
<div className={styles.summary}>
<ProfileSummary />
</div>
<select
className={styles.select}
value={current}
onChange={(e) => navigate(e.target.value)}
aria-label="Раздел профиля"
>
{ITEMS.map(({ to, label }) => (
<option key={to} value={to}>
{label}
</option>
))}
</select>
<nav className={styles.nav}>
{ITEMS.map(({ to, label }) => (
<NavLink

View File

@@ -1,5 +1,5 @@
import { useWalletAddresses } from '@features/wallet'
import { Button, FormField } from '@shared/ui'
import { FormField } from '@shared/ui'
import { ProfileSection } from './ProfileSection'
import styles from './fieldsGrid.module.css'
@@ -7,15 +7,7 @@ export function SecuritySection() {
const { data: walletAddresses } = useWalletAddresses()
return (
<ProfileSection
title="Безопасность"
actions={
<>
<Button variant="danger"> Посмотреть приватный ключ</Button>
<Button variant="primary">СОХРАНИТЬ</Button>
</>
}
>
<ProfileSection title="Безопасность">
<div className={styles.grid1}>
{walletAddresses?.map(({ chain, address }) => (
<FormField

View File

@@ -6,9 +6,11 @@ import styles from './SeedPhraseWidget.module.css'
interface Props {
words: string[]
/** Show the "Перейти в профиль" link. Hidden when embedded inside the profile. */
showProfileLink?: boolean
}
export function SeedPhraseWidget({ words }: Props) {
export function SeedPhraseWidget({ words, showProfileLink = true }: Props) {
const { hidden, countdown, copied, handleHide, handleCopy } = useSeedPhrase(words)
return (
@@ -54,11 +56,13 @@ export function SeedPhraseWidget({ words }: Props) {
</p>
</div>
<div className={styles.profileLinkRow}>
<Link to={ROUTES.PROFILE} className={styles.profileLink}>
Перейти в профиль
</Link>
</div>
{showProfileLink && (
<div className={styles.profileLinkRow}>
<Link to={ROUTES.PROFILE} className={styles.profileLink}>
Перейти в профиль
</Link>
</div>
)}
</div>
)
}