refactor
This commit is contained in:
137
src/pages/profile/README.md
Normal file
137
src/pages/profile/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Страница профиля (`/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` — единственный осознанно общий стиль (`.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 не сломан.
|
||||
Reference in New Issue
Block a user