Files
frontend/src/pages/profile/README.md
2026-06-19 12:43:29 +03:00

10 KiB
Raw Blame History

Страница профиля (/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 — первые кандидаты на переезд вниз.

ProfileDetails решает по isLegalEntity(data) (account_type !== 'individual'):

  • individualIndividualFields: ФИО/email/паспорт/телефон + ИНН/ID. Телефон редактируемый.
  • legalLegalEntityFields: данные организации/адреса/контакты из 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 — единственный осознанно общий стиль (.grid2 2 колонки → 1 колонка на <640px; .grid1), импортится из Individual/Legal/Security. Лежит внутри виджета, поэтому ни один компонент не лезет в стиль страницы.
  • Брейкпоинты по проекту: 1023 (десктоп→планшет), 649/639 (телефон), 549 (узкий телефон).

Правила при доработке

  1. Страница остаётся тонкой. Новый блок профиля = новый компонент в widgets/profile/ui/ + экспорт в барель + вставка в ProfilePage.tsx. Не тащите хуки/стейт обратно в страницу.
  2. Данные — через self-fetch хуки, не через пропсы со страницы (дедуп по queryKey).
  3. Деривации над MeResponsefeatures/auth/lib/profile.ts (там же тип). Не дублируйте форматирование имени/isLegal в компонентах.
  4. Форматирование $-суммformatUsd из @shared/lib/utils/formatUsd.
  5. Соблюдайте порядок слоёв: виджеты импортируют из features/shared, не наоборот.

Известные точки расширения / TODO

  • ProfileSummary: закомментирован рублёвый баланс (.userBalanceRub, «≈ … ₽») — ждёт реального источника курса.
  • SecuritySection: кнопки «⚠️ Посмотреть приватный ключ» и «СОХРАНИТЬ» сейчас без обработчиков (заглушки из исходной вёрстки). При реализации навесить логику.
  • formatUsd дублируется инлайном ещё в 5 местах (BalanceCard, WalletHeader, TokenTable, useChainTokenRows, PoolsTable) — отдельная задача на дедупликацию. Внимание: часть из них использует вариант с 46 знаками, а не фиксированные 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 не сломан.