# Страница профиля (`/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'ами: не сохраняет, если значение не менялось или мутация уже в полёте; - стейтом нотификации (успех/ошибка) — она побочный эффект мутации, вне хука смысла нет. Рендер `` — в `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. **Деривации над `MeResponse`** → `features/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) — отдельная задача на дедупликацию. Внимание: часть из них использует вариант с 4–6 знаками, а не фиксированные 2 — при миграции, возможно, понадобится расширить `formatUsd` опциями точности. ## Проверка ```bash 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 не сломан.