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

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>
)
}