10 KiB
Страница профиля (/profile)
Личный кабинет пользователя: показывает данные аккаунта, баланс, адреса кошельков
и даёт доступ к мнемонике. Маршрут ROUTES.PROFILE (/profile), защищён
ProtectedRoute (только для авторизованных).
Главная идея
Страница — тонкая композиция виджетов. В ProfilePage.tsx нет ни хуков данных,
ни стейта, ни дериваций — только разметка. Вся логика и презентация живут в слайсе
widgets/profile и в нижних слоях (features/auth, features/wallet, shared).
Это сознательный результат рефакторинга под FSD (app → pages → widgets → features → entities → shared; нижние слои не импортируют верхние).
Ключевой приём: каждый под-виджет сам тянет свои данные через хуки. Прокидывать
data пропсами со страницы не нужно — useMe использует queryKey: ['me'] со
staleTime/gcTime: Infinity, поэтому вызов useMe() в нескольких компонентах
дедуплицируется в один сетевой запрос. Та же логика, что в WalletPage.
Карта файлов
pages/profile/
ui/ProfilePage.tsx ← композиция (без логики)
ui/ProfilePage.module.css ← только внешний каркас: .page .main .sections
index.ts ← export { ProfilePage }
README.md ← этот файл
widgets/profile/ ← ВСЁ содержимое страницы живёт здесь
model/usePhoneField.ts ← логика редактирования телефона + стейт нотификации
ui/
ProfileSummary.tsx ← аватар + имя + баланс (self-fetch: useMe, usePortfolio)
ProfileDetails.tsx ← выбирает ветку individual/legal (self-fetch: useMe)
IndividualFields.tsx ← поля физлица; зовёт usePhoneField + рендерит Notification
LegalEntityFields.tsx ← поля юрлица (read-only, из data.legal_entity)
SecuritySection.tsx ← адреса кошельков (self-fetch: useWalletAddresses)
MnemonicSection.tsx ← кнопка → ROUTES.SEED_PHRASE
KycBanner.tsx ← баннер, если !kyc_verified (self-fetch: useMe)
ProfileAvatar.tsx ← загрузка/кроп аватара (был раньше)
AvatarCropModal.tsx ← модалка кропа (был раньше)
ProfileSection.tsx ← обёртка-карточка: title + children + actions (был раньше)
fieldsGrid.module.css ← общие .grid2/.grid1 (для Individual/Legal/Security)
*.module.css ← по одному на компонент с собственными стилями
index.ts ← барель: экспортит верхнеуровневые компоненты
features/auth/lib/profile.ts ← getFullName / getDisplayName / isLegalEntity (над MeResponse)
shared/lib/utils/formatUsd.ts← formatUsd(value) → "$1,234.56" | "$—"
IndividualFields и LegalEntityFields — внутренние для виджета: в барель
(widgets/profile/index.ts) не выносятся, их использует только ProfileDetails
относительным импортом.
Поток данных
| Источник | Откуда | Что даёт |
|---|---|---|
useMe() |
@features/auth |
MeResponse: ФИО, email, паспорт, ИНН, account_type, kyc_verified, legal_entity |
usePortfolio() |
@features/wallet |
portfolio.totalUsd для баланса |
useWalletAddresses() |
@features/wallet |
массив { chain, address } |
useUpdatePhone() |
@features/auth |
мутация сохранения телефона (внутри usePhoneField) |
MeResponse определён в features/auth/api/profileApi.ts. Намеренно не перенесён
в пустой entities/user — это была бы отдельная миграция (затрагивает все импорты
типа). Если когда-нибудь будете наполнять entities/user, тип и хелперы из
features/auth/lib/profile.ts — первые кандидаты на переезд вниз.
Развилка individual / legal
ProfileDetails решает по isLegalEntity(data) (account_type !== 'individual'):
- individual →
IndividualFields: ФИО/email/паспорт/телефон + ИНН/ID. Телефон редактируемый. - legal →
LegalEntityFields: данные организации/адреса/контакты изdata.legal_entity(еслиlegal_entityпуст → рендеритnull). Всё read-only — данные юрлица управляются на стороне админа.
Логика телефона (usePhoneField)
Единственная нетривиальная логика на странице, живёт в widgets/profile/model/usePhoneField.ts
(по образцу widgets/login-form/model/useLoginForm.ts). Хук владеет:
- локальным
phone, синхронизируемым с серверным значением черезuseEffect; - санитизацией ввода (
onPhoneChange— regex оставляет только\d + пробел () -); - сохранением по blur (
onPhoneBlur) с guard'ами: не сохраняет, если значение не менялось или мутация уже в полёте; - стейтом нотификации (успех/ошибка) — она побочный эффект мутации, вне хука смысла нет.
Рендер <Notification> — в IndividualFields (хук владеет стейтом, компонент —
рендером). Это тост из @shared/ui, позиционируется фиксированно.
CSS
ProfilePage.module.css— только внешний каркас страницы (flex-колонка, центрирование.main, отступы, брейкпоинты 1023/639px для паддингов).- Стили секций — рядом со своими компонентами в
widgets/profile/ui/*.module.css. fieldsGrid.module.css— единственный осознанно общий стиль (.grid22 колонки → 1 колонка на <640px;.grid1), импортится из Individual/Legal/Security. Лежит внутри виджета, поэтому ни один компонент не лезет в стиль страницы.- Брейкпоинты по проекту: 1023 (десктоп→планшет), 649/639 (телефон), 549 (узкий телефон).
Правила при доработке
- Страница остаётся тонкой. Новый блок профиля = новый компонент в
widgets/profile/ui/+ экспорт в барель + вставка вProfilePage.tsx. Не тащите хуки/стейт обратно в страницу. - Данные — через self-fetch хуки, не через пропсы со страницы (дедуп по queryKey).
- Деривации над
MeResponse→features/auth/lib/profile.ts(там же тип). Не дублируйте форматирование имени/isLegalв компонентах. - Форматирование
$-сумм →formatUsdиз@shared/lib/utils/formatUsd. - Соблюдайте порядок слоёв: виджеты импортируют из features/shared, не наоборот.
Известные точки расширения / TODO
ProfileSummary: закомментирован рублёвый баланс (.userBalanceRub, «≈ … ₽») — ждёт реального источника курса.SecuritySection: кнопки «⚠️ Посмотреть приватный ключ» и «СОХРАНИТЬ» сейчас без обработчиков (заглушки из исходной вёрстки). При реализации навесить логику.formatUsdдублируется инлайном ещё в 5 местах (BalanceCard, WalletHeader, TokenTable, useChainTokenRows, PoolsTable) — отдельная задача на дедупликацию. Внимание: часть из них использует вариант с 4–6 знаками, а не фиксированные 2 — при миграции, возможно, понадобится расширитьformatUsdопциями точности.
Проверка
npm run build # tsc -b + vite build (тестов в проекте нет)
npm run lint
npm run dev # ручная проверка /profile
Ручной чек-лист /profile:
- individual: поля видны; ввод телефона санитизируется; по blur сохраняется и появляется тост «Номер телефона обновлён» (и тост ошибки при сбое);
- legal: секции организации/адресов/контактов; телефон read-only;
- баланс через
formatUsd(и$—при загрузке); - KYC-баннер виден только при
!kyc_verified; - «Мнемоника» ведёт на
ROUTES.SEED_PHRASE; - адаптив на брейкпоинтах 1023/649/639/549 не сломан.