This commit is contained in:
2026-06-19 12:43:29 +03:00
parent 990f7c8544
commit 663f6dc268
54 changed files with 2884 additions and 2207 deletions

View File

@@ -0,0 +1,18 @@
# Рефакторинг: чек-лист
`[x]` — отрефакторено.
## Страницы
- [x] **LegalConverterPage** — вынести бизнес-логику (расчёт комиссии, упаковку имени/контакта в `comment` и парсинг) из страницы в `features`/`model`. → чистая логика в `features/b2b/lib/purchaseRequest.ts`, форма-хук в `widgets/legal-converter/model`, тонкий UI `LegalConverterSection`. Док: `pages/converter/README.md`.
- [x] **Юридические страницы** (`publichnaya-oferta`, `politika-personalnyh-dannyh`, `politika-cookie`, `soglasie-personalnyh-dannyh`, `reestr-pd-rkn`) — статичный текст вынести в данные, рендерить общим шаблоном. → текст в `pages/<slug>/model/content.ts` (`LegalDoc`-объекты), единый шаблон+стиль `entities/legal-doc` (`LegalDoc`), 5 старых `*.module.css` удалены. Док: `entities/legal-doc/README.md`.
## Виджеты
- [ ] **swap-form** / **bridge-form** — бизнес-логику (quote-payload, мутации, инвалидация кэша) вынести в `model`-хуки.
- [ ] **bridge-form** / **token-table** — убрать межвиджетные импорты, общие части опустить в `features`/`entities`/`shared`.
- [ ] **Метаданные сетей** (`CHAIN_ID`, `RELAY_CHAIN_ID`, `NATIVE_ADDR`, `CHAIN_META`, `TICKER_TO_CHAIN`) — свести к единому источнику в `entities`/`shared`.
- [ ] **Утилиты** (`formatUsd`, `toBaseUnits`, разбор ошибок) — убрать локальные дубли, использовать `shared/lib/utils`.
- [ ] **send-modal** — логику (base-units, разбор ошибок) в `model`; инлайн-SVG и дропдауны в отдельные компоненты.
- [ ] **staking/StakeCard**, **pools/LpDepositCard** — финансовые расчёты вынести из UI в `model`-хук.
- [ ] **token-table** — разбить `renderRow`/`renderCard` на компоненты, убрать локальный `formatUsd` и инлайн-SVG.

View File

@@ -0,0 +1,109 @@
# Юридические страницы (`entities/legal-doc`)
Общий механизм для **пяти статичных юридических страниц**. Раньше каждая была
самостоятельным `.tsx` с захардкоженной разметкой (200300 строк JSX) и почти
идентичным собственным `.module.css`. После рефакторинга весь текст вынесен в
**данные** (`LegalDoc`-объекты), а рендерит их **один шаблон** `LegalDoc` с единым
стилем. Результат пункта чек-листа `app/providers/ROUTES_REFACTOR.md` → «Юридические
страницы … статичный текст вынести в данные, рендерить общим шаблоном».
## Какие страницы
| Маршрут (`ROUTES`) | Страница (`pages/*`) | Данные | h1 |
|---|---|---|---|
| `/publichnaya-oferta` | `publichnaya-oferta` | `model/content.ts → publichnayaOfertaDoc` | ПУБЛИЧНЫЙ ДОГОВОР ОФЕРТЫ |
| `/politika-personalnyh-dannyh` | `politika-personalnyh-dannyh` | `politikaPersonalnyhDannyhDoc` | ПОЛИТИКА ОБРАБОТКИ ПД |
| `/politika-cookie` | `politika-cookie` | `politikaCookieDoc` | ПОЛИТИКА COOKIE |
| `/soglasie-personalnyh-dannyh` | `soglasie-personalnyh-dannyh` | `soglasieDoc` | СОГЛАСИЕ НА ОБРАБОТКУ ПД |
| `/reestr-pd-rkn` | `reestr-pd-rkn` | `reestrDoc` | Реестр операторов ПД |
Все пять — **публичные** (вне `ProtectedRoute`/`GuestRoute`), подключены в
`app/providers/RouterProvider.tsx`.
## Архитектура (FSD: `pages → entities`)
```
entities/legal-doc/ ← общий, переиспользуемый механизм (без контента)
model/types.ts ← типы LegalDoc / LegalSection / LegalBlock
ui/LegalDoc.tsx ← единый рендерер (main→container→title→sections→blocks)
ui/LegalDoc.module.css ← единый стиль (слит из 5 старых модулей)
index.ts ← export { LegalDoc }, типы (LegalDocData = тип данных)
README.md ← этот файл
pages/<slug>/ ← каждая страница = контент + тонкая обёртка
model/content.ts ← <slug>Doc: LegalDocData — ВЕСЬ текст страницы
ui/<Name>Page.tsx ← <Header/> <LegalDoc doc={<slug>Doc}/> <Footer/>
index.ts ← export { <Name>Page } (не менялся)
```
> **Почему `entities`, а не `widgets`.** `Header`/`Footer` — это виджеты. Если бы шаблон
> рендерил их сам, он стал бы виджетом, импортирующим другие виджеты (межвиджетный импорт,
> который другой пункт чек-листа просит как раз убирать). Поэтому `LegalDoc` рендерит
> **только тело** документа (`<main>…`), а `Header`/`Footer` добавляет страница-обёртка.
> Слой `entities` ниже `widgets` — нарушения порядка слоёв нет. Структура повторяет
> соседний `entities/commission` (`model/` + `ui/` + barrel).
## Модель данных (`model/types.ts`)
```ts
LegalDoc = { title: string; subtitle?: string; sections: LegalSection[] }
LegalSection = { title?: string; blocks: LegalBlock[] } // title → h3; нет title → секция без заголовка
```
`LegalBlock` — размеченный union (поле `kind`). Порядок блоков в массиве = порядок на
экране (подзаголовки и абзацы чередуются как в исходнике).
| `kind` | Поля | Рендер | Где используется |
|---|---|---|---|
| `text` | `text`, `variant?` | `<p class=text>` | везде |
| `subheading` | `text` | `<h4 class=subheading>` (был `subSectionTitle`) | политики, согласие, cookie |
| `list` | `items: LegalListItem[]` | `<ul class=list>` | везде |
| `definitions` | `items: {term,text}[]` | боксы с левым акцентом | оферта, политика ПД |
| `goals` | `items: {title,items[]}[]` | боксы «заголовок + вложенный список» | политика ПД (раздел «Цели сбора») |
| `infoBox` | `lines: LegalListItem[]` | бокс из `<p>`-строк | реквизиты, контакты, данные оператора |
| `link` | `href`, `text` | центрированная кнопка-ссылка (`target=_blank`) | только реестр РКН |
`variant` у `text`: `'example'` (курсивный акцент-бокс), `'warning'` (жёлтый бокс),
`'emphasis'` (полужирный абзац, был `.confirmation`). Без `variant` — обычный абзац.
`LegalListItem` = либо строка, либо `{ strong, text? }` (жирный зачин + остаток).
Используется в `list` и в `infoBox`.
### Важный нюанс: разделитель (`—`, ``, `:`) хранится в данных
Рендерер `{strong,text}` выводит как `<strong>{strong}</strong> {text}`**без** своего
разделителя. Поэтому тире/двоеточие пишется прямо в данных:
- cookie: `{ strong: 'Добровольность', text: '— согласие дается…' }` → «**Добровольность** — …»
- реестр: `{ strong: 'ИНН:', text: '9810001062' }` → «**ИНН:** 9810001062»
Аналогично у `definitions` тире входит в `text` (`{ term: 'Агент', text: ' юридическое лицо…' }`),
т.к. в оферте `` (en-dash), а в политике ПД `—` (em-dash) — единый разделитель в коде
исказил бы исходник.
## Что изменилось визуально (унификация стилей)
Пять старых `.module.css` были на 90% одинаковы. При слиянии разнобойные боксы
`requisites` / `info` / `contacts` / `operatorInfo` сведены к одному классу `.infoBox`
(фон-стекло + левый акцент). Единственное реальное изменение: у **реквизитов оферты**
раньше была сплошная рамка, теперь — левый акцент, как у остальных боксов.
> Сохранён как было (намеренно, чтобы рефакторинг не менял поведение): `text variant=warning`
> — светло-жёлтый фон `#fff3cd` с белым текстом (низкий контраст). Это исторический баг
> исходного cookie-CSS; чинить — отдельной задачей, не в рамках этого пункта.
## Как добавить / отредактировать страницу
- **Поправить текст** — только `pages/<slug>/model/content.ts`. Шаблон и стиль не трогать.
- **Новая юридическая страница** — добавить `ROUTES`, `<Route>` в `RouterProvider`,
папку `pages/<slug>` с `model/content.ts` (объект `LegalDocData`) и тонкой обёрткой
`<Header/> <LegalDoc doc={…}/> <Footer/>`. Новых CSS-файлов не нужно.
- **Новый тип блока** — добавить вариант в `LegalBlock` (`model/types.ts`), ветку в
`Block` (`ui/LegalDoc.tsx`) и стиль в `ui/LegalDoc.module.css`.
## Проверка
```bash
npm run build # tsc -b + vite build (тестов в проекте нет)
npm run lint
npm run dev # ручная проверка пяти /publichnaya-oferta, /politika-*, /soglasie-*, /reestr-pd-rkn
```

View File

@@ -0,0 +1,2 @@
export { LegalDoc } from './ui/LegalDoc'
export type { LegalDoc as LegalDocData, LegalSection, LegalBlock, LegalListItem } from './model/types'

View File

@@ -0,0 +1,38 @@
/**
* Модель данных для юридических страниц (оферта, политики, согласие, реестр).
* Весь статичный текст описывается этими типами и рендерится одним шаблоном
* `LegalDoc` (см. `../ui/LegalDoc.tsx`). README — в корне `entities/legal-doc`.
*/
/** Элемент списка: простая строка или «жирный зачин + остаток». */
export type LegalListItem = string | { strong: string; text?: string }
/** Блок контента внутри секции. Порядок блоков в секции сохраняется как есть. */
export type LegalBlock =
/** Абзац. variant управляет оформлением (пример / предупреждение / акцент). */
| { kind: 'text'; text: string; variant?: 'example' | 'warning' | 'emphasis' }
/** Подзаголовок внутри секции (был `h4.subSectionTitle`). */
| { kind: 'subheading'; text: string }
/** Маркированный список. */
| { kind: 'list'; items: LegalListItem[] }
/** Список определений (термин + расшифровка) в виде боксов. */
| { kind: 'definitions'; items: { term: string; text: string }[] }
/** Список целей: заголовок + вложенный список — в виде боксов. */
| { kind: 'goals'; items: { title: string; items: string[] }[] }
/** Информационный бокс со строками (реквизиты, контакты, данные оператора). */
| { kind: 'infoBox'; lines: LegalListItem[] }
/** Кнопка-ссылка (внешний ресурс), по центру в отдельном блоке. */
| { kind: 'link'; href: string; text: string }
/** Секция документа: необязательный заголовок (h3) + блоки контента. */
export interface LegalSection {
title?: string
blocks: LegalBlock[]
}
/** Документ целиком: заголовок (h1), необязательный подзаголовок (h2), секции. */
export interface LegalDoc {
title: string
subtitle?: string
sections: LegalSection[]
}

View File

@@ -1,3 +1,6 @@
/* Единый стиль юридических страниц. Слит из пяти почти одинаковых *.module.css.
Боксы info/contacts/operatorInfo/requisites унифицированы в .infoBox. */
.main {
padding: 40px 20px;
max-width: 1200px;
@@ -39,7 +42,7 @@
padding-bottom: 10px;
}
.subSectionTitle {
.subheading {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
@@ -47,10 +50,58 @@
color: var(--text-primary, #ffffff);
}
.text {
line-height: 1.7;
color: var(--text-primary, #ffffff);
margin: 0 0 12px;
}
/* Вариант абзаца: пример (курсивный акцент-бокс). */
.example {
padding: 10px 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 3px solid var(--interactive, #4a6dff);
border-radius: 4px;
font-style: italic;
margin: 10px 0;
}
/* Вариант абзаца: предупреждение.
NB: исторический контраст (светлый фон + белый текст) сохранён как было. */
.warning {
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
margin: 15px 0;
font-weight: 500;
}
/* Вариант абзаца: акцент (был `.confirmation`). */
.emphasis {
font-weight: 500;
}
.list {
list-style: disc;
margin: 0 0 12px 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.list strong {
color: var(--text-primary, #ffffff);
}
.definitions {
display: flex;
flex-direction: column;
gap: 15px;
margin: 0 0 12px;
}
.definition {
@@ -61,21 +112,11 @@
line-height: 1.6;
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.goalsList {
.goals {
display: flex;
flex-direction: column;
gap: 20px;
margin: 0 0 12px;
}
.goal {
@@ -102,15 +143,49 @@
color: var(--text-primary, #ffffff);
}
.contacts {
.infoBox {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
margin: 15px 0;
line-height: 1.8;
}
.infoBox p {
margin: 8px 0;
color: var(--text-primary, #ffffff);
}
.infoBox strong {
color: var(--text-primary, #ffffff);
}
.linkBlock {
text-align: center;
padding: 30px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 8px;
margin: 30px 0;
}
.button {
display: inline-block;
padding: 15px 40px;
background: var(--interactive, #4a6dff);
color: #fff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
border: 2px solid var(--interactive, #4a6dff);
}
.button:hover {
background: transparent;
color: var(--interactive, #4a6dff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
@@ -128,11 +203,24 @@
font-size: 16px;
}
.subSectionTitle {
.subheading {
font-size: 14px;
}
.list {
margin-left: 15px;
}
.infoBox {
padding: 15px;
}
.linkBlock {
padding: 20px;
}
.button {
padding: 12px 30px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,101 @@
import type { ReactNode } from 'react'
import type { LegalBlock, LegalDoc as LegalDocData, LegalListItem } from '../model/types'
import styles from './LegalDoc.module.css'
/** «Жирный зачин + остаток» либо простая строка. Разделитель (—, :) пишется в text. */
function renderItem(item: LegalListItem): ReactNode {
if (typeof item === 'string') return item
return (
<>
<strong>{item.strong}</strong>
{item.text ? ` ${item.text}` : null}
</>
)
}
function Block({ block }: { block: LegalBlock }) {
switch (block.kind) {
case 'text':
return (
<p className={block.variant ? `${styles.text} ${styles[block.variant]}` : styles.text}>
{block.text}
</p>
)
case 'subheading':
return <h4 className={styles.subheading}>{block.text}</h4>
case 'list':
return (
<ul className={styles.list}>
{block.items.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
)
case 'definitions':
return (
<div className={styles.definitions}>
{block.items.map((d, i) => (
<div key={i} className={styles.definition}>
<strong>{d.term}</strong> {d.text}
</div>
))}
</div>
)
case 'goals':
return (
<div className={styles.goals}>
{block.items.map((g, i) => (
<div key={i} className={styles.goal}>
<strong>{g.title}</strong>
<ul>
{g.items.map((t, j) => (
<li key={j}>{t}</li>
))}
</ul>
</div>
))}
</div>
)
case 'infoBox':
return (
<div className={styles.infoBox}>
{block.lines.map((line, i) => (
<p key={i}>{renderItem(line)}</p>
))}
</div>
)
case 'link':
return (
<div className={styles.linkBlock}>
<a href={block.href} target="_blank" rel="noopener noreferrer" className={styles.button}>
{block.text}
</a>
</div>
)
}
}
/**
* Единый шаблон юридической страницы. Получает данные документа (`LegalDoc`) и
* рендерит `main → container → title/subtitle → sections → blocks`.
* Header/Footer добавляет страница-обёртка, не этот компонент (entities < widgets).
*/
export function LegalDoc({ doc }: { doc: LegalDocData }) {
return (
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>{doc.title}</h1>
{doc.subtitle && <h2 className={styles.subtitle}>{doc.subtitle}</h2>}
{doc.sections.map((section, i) => (
<section key={i} className={styles.section}>
{section.title && <h3 className={styles.sectionTitle}>{section.title}</h3>}
{section.blocks.map((block, j) => (
<Block key={j} block={block} />
))}
</section>
))}
</div>
</main>
)
}

View File

@@ -1,6 +1,7 @@
export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi'
export { getMe, uploadAvatar, updatePhone } from './api/profileApi'
export type { MeResponse, UploadAvatarPayload } from './api/profileApi'
export { getFullName, getDisplayName, isLegalEntity } from './lib/profile'
export { useMe } from './hooks/useMe'
export { useUploadAvatar } from './hooks/useUploadAvatar'
export { useUpdatePhone } from './hooks/useUpdatePhone'

View File

@@ -0,0 +1,24 @@
import type { MeResponse } from '../api/profileApi'
const capitalize = (s: string | null) =>
s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : ''
// True for any non-individual account (legal entity). Organization data then
// lives in the nested `legal_entity` object.
export function isLegalEntity(data: MeResponse): boolean {
return data.account_type !== 'individual'
}
// "Фамилия Имя Отчество" with each part capitalized; empty parts are dropped.
export function getFullName(data: MeResponse): string {
return [data.last_name, data.first_name, data.middle_name]
.filter(Boolean)
.map(capitalize)
.join(' ')
}
// Name shown in the profile header: organization name for legal accounts,
// full person name otherwise.
export function getDisplayName(data: MeResponse): string {
return isLegalEntity(data) ? (data.legal_entity?.name ?? '') : getFullName(data)
}

View File

@@ -10,3 +10,15 @@ export type {
OrgWalletBalance,
OrgBalancesResponse,
} from './api/b2bApi'
export {
MIN_ORDER,
TERM_OPTIONS,
formatRub,
dayLabel,
termRate,
calcQuote,
buildPurchaseComment,
parseContactFromComment,
latestPurchaseRequest,
} from './lib/purchaseRequest'
export type { Quote, PurchaseCommentParts } from './lib/purchaseRequest'

View File

@@ -0,0 +1,100 @@
import type { B2bPurchaseRequest } from '../api/b2bApi'
// Минимальный объём заявки, ₽.
export const MIN_ORDER = 500_000
// Чем дольше клиент готов ждать исполнения, тем ниже ставка комиссии сервиса.
// days — срок ожидания операции, rate — доля комиссии (0.05 = 5 %).
export const TERM_OPTIONS = [
{ days: 3, rate: 0.05 },
{ days: 4, rate: 0.04636 },
{ days: 5, rate: 0.04273 },
{ days: 6, rate: 0.03909 },
{ days: 7, rate: 0.03545 },
{ days: 8, rate: 0.03182 },
{ days: 9, rate: 0.02818 },
{ days: 10, rate: 0.02455 },
{ days: 11, rate: 0.02091 },
{ days: 12, rate: 0.01727 },
{ days: 13, rate: 0.01364 },
{ days: 14, rate: 0.01 },
] as const
// Целое число с разделителями групп в русской локали: 500000 → "500 000".
export const formatRub = (n: number) =>
n.toLocaleString('ru-RU', { maximumFractionDigits: 0 })
// Русское склонение слова «день» для срока ожидания.
export function dayLabel(days: number): string {
const mod10 = days % 10
const mod100 = days % 100
if (mod10 === 1 && mod100 !== 11) return `${days} день`
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${days} дня`
return `${days} дней`
}
// Ставка комиссии (доля) для выбранного срока. Фоллбэк — ставка первого срока.
export function termRate(days: number): number {
return TERM_OPTIONS.find((o) => o.days === days)?.rate ?? TERM_OPTIONS[0].rate
}
export interface Quote {
rate: number // доля комиссии (0.05 = 5 %)
commission: number // сумма комиссии, ₽
total: number // сумма к оплате, ₽
}
// Расчёт комиссии и итога по объёму заявки и сроку ожидания.
export function calcQuote(amount: number, days: number): Quote {
const rate = termRate(days)
const commission = amount * rate
return { rate, commission, total: amount + commission }
}
export interface PurchaseCommentParts {
name: string
contact: string
days: number
amount: number
commission: number
total: number
}
// У заявки нет отдельных полей под имя/контакт/срок/суммы в ₽ — всё, что нужно
// менеджеру, упаковывается в человекочитаемый comment. Обратная операция —
// parseContactFromComment.
export function buildPurchaseComment(parts: PurchaseCommentParts): string {
const { name, contact, days, amount, commission, total } = parts
return [
name && `Имя: ${name}`,
contact && `Контакт: ${contact}`,
`Срок ожидания: ${dayLabel(days)}`,
`Сумма: ${formatRub(amount)}`,
`Комиссия: ≈${formatRub(commission)}`,
`Итого: ≈${formatRub(total)}`,
]
.filter(Boolean)
.join('; ')
}
// Достаёт имя и контакт из comment последней заявки для автозаполнения формы.
export function parseContactFromComment(comment: string | null): {
name: string
contact: string
} {
return {
name: comment?.match(/Имя:\s*([^;]+)/)?.[1]?.trim() ?? '',
contact: comment?.match(/Контакт:\s*([^;]+)/)?.[1]?.trim() ?? '',
}
}
// Последняя по времени создания заявка на аккаунте, либо null.
export function latestPurchaseRequest(
items: B2bPurchaseRequest[],
): B2bPurchaseRequest | null {
return items.reduce<B2bPurchaseRequest | null>(
(latest, r) =>
!latest || (r.created_at ?? '') > (latest.created_at ?? '') ? r : latest,
null,
)
}

View File

@@ -0,0 +1,154 @@
# Страница конвертера (`/converter`)
Покупка/конвертация валюты. Маршрут `ROUTES.CONVERTER` (`/converter`), **защищён**
`ProtectedRoute` (только для авторизованных) и обёрнут в `WalletLayout footer center`
(см. `app/providers/RouterProvider.tsx`).
## Главная идея
`ConverterPage`**тонкий роутер по типу аккаунта**. Сам по себе он почти ничего не
рендерит: показывает заглушку / спиннер и выбирает один из двух виджетов-конвертеров.
Вся логика и презентация живут в виджетах и нижних слоях (`features/*`, `shared`).
Это результат рефакторинга под FSD (`app → pages → widgets → features → entities →
shared`; нижние слои не импортируют верхние).
```
ConverterPage
├─ PURCHASE_ENABLED === false → заглушка «Покупка временно недоступна»
├─ useMe() ещё грузится → <Spinner/>
└─ по account_type:
├─ legal (юрлицо) → <LegalConverterSection/> (@widgets/legal-converter)
└─ individual (физлицо)→ <ConverterSection/> (@widgets/converter-page)
```
> ⚠️ Сейчас `PURCHASE_ENABLED = false` в `ConverterPage.tsx` — оба конвертера
> скрыты заглушкой. Поставьте `true`, чтобы вернуть рабочий контент.
Развилка по аккаунту: `isLegal = data.account_type !== 'individual'`. Тот же критерий,
что `isLegalEntity()` в `features/auth/lib/profile.ts` (профиль). Здесь оставлен инлайн,
т.к. `ConverterPage` не импортирует профильные хелперы; при желании можно свести к
одному источнику.
## Карта файлов
```
pages/converter/
ui/ConverterPage.tsx ← роутер по account_type (заглушка/спиннер/виджет)
ui/ConverterPage.module.css ← стили только заглушки
index.ts ← export { ConverterPage }
README.md ← этот файл
widgets/legal-converter/ ← ВЕТКА ЮРЛИЦ: форма B2B-заявки
model/useLegalConverterForm.ts← вся логика формы (стейт, расчёт, submit)
ui/LegalConverterForm.tsx ← разметка (LegalConverterSection), без логики
ui/LegalConverterForm.module.css
index.ts ← export { LegalConverterSection }
widgets/converter-page/ ← ВЕТКА ФИЗЛИЦ: обычный конвертер (не трогался)
model/useConverterSection.ts
ui/ConverterSection.tsx
...
features/b2b/ ← домен B2B-заявок
lib/purchaseRequest.ts ← чистая доменная логика (без React) — см. ниже
hooks/useCreatePurchaseRequest.ts ← мутация POST /purchase-requests
hooks/useMyPurchaseRequests.ts ← список заявок аккаунта (queryKey b2b/purchase-requests)
api/b2bApi.ts ← fetch + типы B2bPurchaseRequest и т.д.
```
`LegalConverterForm.tsx` экспортирует компонент `LegalConverterSection` (имя
по аналогии с `ConverterSection` физлиц). Файл назван `*Form`, компонент — `*Section`.
## Ветка юрлиц: где что лежит
Раньше всё (стейт формы + доменная математика + разметка) жило одним файлом
`pages/converter/ui/LegalConverterPage.tsx`. После рефакторинга:
| Слой | Файл | Ответственность |
|---|---|---|
| `features/b2b/lib` | `purchaseRequest.ts` | **чистая** логика без React: расчёт комиссии, упаковка/парсинг comment, склонения, форматирование ₽ |
| `widgets/legal-converter/model` | `useLegalConverterForm.ts` | стейт полей формы, автозаполнение, submit-мутация, нотификация |
| `widgets/legal-converter/ui` | `LegalConverterForm.tsx` | только разметка, дёргает хук |
### `features/b2b/lib/purchaseRequest.ts`
| Экспорт | Что делает |
|---|---|
| `MIN_ORDER` | минимальный объём заявки (500 000 ₽) |
| `TERM_OPTIONS` | таблица «срок ожидания → ставка комиссии». Чем дольше ждать, тем ниже ставка (3 дня → 5 %, 14 дней → 1 %) |
| `termRate(days)` | ставка (доля) для срока; фоллбэк — первая строка таблицы |
| `calcQuote(amount, days)` | `{ rate, commission, total }`**расчёт комиссии и итога** |
| `buildPurchaseComment(parts)` | **упаковка** имени/контакта/срока/сумм в строку `comment` |
| `parseContactFromComment(comment)` | **парсинг** имени и контакта обратно из `comment` |
| `latestPurchaseRequest(items)` | последняя по `created_at` заявка аккаунта (или `null`) |
| `dayLabel(days)` | русское склонение: `1 день` / `2 дня` / `5 дней` |
| `formatRub(n)` | целое с разделителями групп: `500000 → "500 000"` |
## Ключевой нюанс: имя/контакт упакованы в `comment`
У API B2B-заявки (`B2bPurchaseRequest`, `api/b2bApi.ts`) **нет отдельных полей** под
имя, контакт, срок ожидания и суммы в ₽. Есть только `usdt_amount` и свободный
`comment`. Поэтому всё, что нужно менеджеру, кодируется в человекочитаемый `comment`:
```
Имя: Иван; Контакт: +7 900 000-00-00; Срок ожидания: 3 дня; Сумма: 500 000 ₽; Комиссия: ≈25 000 ₽; Итого: ≈525 000 ₽
```
- **Запись:** `buildPurchaseComment()` собирает строку при отправке.
- **Чтение:** `parseContactFromComment()` достаёт `Имя:`/`Контакт:` регулярками — это
обратная операция к записи. Используется для **автозаполнения** формы из последней
заявки аккаунта (один раз, по флагу `prefilled` в хуке).
> Связка хрупкая по контракту: меняете формат строки в `buildPurchaseComment` —
> синхронно правьте регулярки в `parseContactFromComment`, иначе автозаполнение
> молча перестанет находить поля.
## Поток данных (ветка юрлиц)
| Источник | Откуда | Что даёт |
|---|---|---|
| `useMe()` | `@features/auth` | `account_type` для выбора ветки (в `ConverterPage`) |
| `useMyPurchaseRequests()` | `@features/b2b` | список прошлых заявок → автозаполнение |
| `useCreatePurchaseRequest()` | `@features/b2b` | мутация создания заявки; на успех инвалидирует `['b2b','purchase-requests']` |
Числовая сумма берётся из строки ввода: `Number(amount.replace(/\D/g, ''))`. Поле
форматируется на лету (`onAmountChange``formatRub`), поэтому в стейте лежит
строка с пробелами, а в API уходит число `numAmount`.
## Правила при доработке
1. **Страница остаётся роутером.** Новый вид конвертера = новый виджет +
ветка в `ConverterPage`. Не тащите стейт/хуки данных в `ConverterPage.tsx`.
2. **Чистая доменная логика** (расчёты, упаковка/парсинг comment, форматирование) →
`features/b2b/lib/purchaseRequest.ts`. Без React, чтобы переиспользовать и тестировать.
3. **Стейт формы и мутации**`widgets/legal-converter/model`. UI-компонент только рендерит.
4. **`comment` пишется и читается одной парой функций** (`build*`/`parse*`) — не
разносите формат по разным местам.
5. Соблюдайте порядок слоёв: виджет импортирует из `features`/`shared`, не наоборот.
## Известные точки расширения / TODO
- `PURCHASE_ENABLED = false` — глобальный выключатель в `ConverterPage.tsx`. Пока `false`,
оба конвертера не показываются.
- `formatRub` живёт в `features/b2b/lib` (локально для домена). Если понадобится
форматирование ₽ в других местах — кандидат на переезд в `shared/lib/utils`
(рядом с `formatUsd`).
- Хрупкость `comment`-контракта (см. выше). При появлении в API нормальных полей под
имя/контакт/срок — `build/parseComment` можно выкинуть.
## Проверка
```bash
npm run build # tsc -b + vite build (тестов в проекте нет)
npm run lint
npm run dev # ручная проверка /converter (под legal-аккаунтом, PURCHASE_ENABLED=true)
```
Ручной чек-лист `/converter` (legal-аккаунт, `PURCHASE_ENABLED = true`):
- ввод объёма форматируется (`500000 → 500 000`); подсказка при объёме < 500 000 ₽;
- выбор срока меняет ставку комиссии и пересчитывает «Сумму комиссии» / «Итого»;
- кнопка неактивна при пустой/слишком малой сумме и во время отправки;
- успешная отправка → тост «Заявка отправлена», форма очищается;
- при повторном заходе имя/контакт подставляются из последней заявки;
- individual-аккаунт по тому же адресу видит обычный `ConverterSection`.
```

View File

@@ -1,7 +1,7 @@
import { useMe } from '@features/auth'
import { Spinner } from '@shared/ui'
import { ConverterSection } from '@widgets/converter-page'
import { LegalConverterPage } from './LegalConverterPage'
import { LegalConverterSection } from '@widgets/legal-converter'
import styles from './ConverterPage.module.css'
// Пока покупка валюты недоступна — показываем заглушку вместо конвертера.
@@ -28,5 +28,5 @@ export function ConverterPage() {
}
const isLegal = !!data && data.account_type !== 'individual'
return isLegal ? <LegalConverterPage /> : <ConverterSection />
return isLegal ? <LegalConverterSection /> : <ConverterSection />
}

View File

@@ -1,221 +0,0 @@
import { useEffect, useState } from 'react'
import { useCreatePurchaseRequest, useMyPurchaseRequests } from '@features/b2b'
import type { B2bPurchaseRequest } from '@features/b2b'
import { FormField, Notification, Select } from '@shared/ui'
import styles from './LegalConverterPage.module.css'
const MIN_ORDER = 500_000
// Чем дольше пользователь готов ждать, тем ниже комиссия сервиса.
const TERM_OPTIONS = [
{ days: 3, rate: 0.05 },
{ days: 4, rate: 0.04636 },
{ days: 5, rate: 0.04273 },
{ days: 6, rate: 0.03909 },
{ days: 7, rate: 0.03545 },
{ days: 8, rate: 0.03182 },
{ days: 9, rate: 0.02818 },
{ days: 10, rate: 0.02455 },
{ days: 11, rate: 0.02091 },
{ days: 12, rate: 0.01727 },
{ days: 13, rate: 0.01364 },
{ days: 14, rate: 0.01 },
] as const
const ru = (n: number) => n.toLocaleString('ru-RU', { maximumFractionDigits: 0 })
// Имя и контакт у заявки отдельных полей не имеют — они упакованы в comment
// (см. handleSubmit). Достаём их обратно для автозаполнения формы.
const parseContactFromComment = (comment: string | null) => ({
name: comment?.match(/Имя:\s*([^;]+)/)?.[1]?.trim() ?? '',
contact: comment?.match(/Контакт:\s*([^;]+)/)?.[1]?.trim() ?? '',
})
// Последняя по времени создания заявка на аккаунте, либо null.
const latestRequest = (items: B2bPurchaseRequest[]): B2bPurchaseRequest | null =>
items.reduce<B2bPurchaseRequest | null>(
(latest, r) => (!latest || (r.created_at ?? '') > (latest.created_at ?? '') ? r : latest),
null,
)
const dayLabel = (days: number) => {
const mod10 = days % 10
const mod100 = days % 100
if (mod10 === 1 && mod100 !== 11) return `${days} день`
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${days} дня`
return `${days} дней`
}
export function LegalConverterPage() {
const [amount, setAmount] = useState('')
const [name, setName] = useState('')
const [contact, setContact] = useState('')
const [days, setDays] = useState<number>(TERM_OPTIONS[0].days)
const [notice, setNotice] = useState<'success' | 'error' | null>(null)
const { mutate: submitRequest, isPending } = useCreatePurchaseRequest()
const { data: requests } = useMyPurchaseRequests()
// Автозаполнение имени и контакта из последней заявки аккаунта (один раз).
const [prefilled, setPrefilled] = useState(false)
useEffect(() => {
if (prefilled || !requests) return
const last = latestRequest(requests.items)
if (!last) return
const parsed = parseContactFromComment(last.comment)
if (parsed.name) setName(parsed.name)
if (parsed.contact) setContact(parsed.contact)
setPrefilled(true)
}, [prefilled, requests])
const numAmount = Number(amount.replace(/\D/g, '')) || 0
const belowMin = numAmount > 0 && numAmount < MIN_ORDER
const rate = TERM_OPTIONS.find((o) => o.days === days)?.rate ?? TERM_OPTIONS[0].rate
const commission = numAmount * rate
const total = numAmount + commission
const handleAmountChange = (value: string) => {
const digits = value.replace(/\D/g, '')
setAmount(digits ? ru(Number(digits)) : '')
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (numAmount === 0 || belowMin || isPending) return
// Отдельных полей под имя/контакт/срок/сумму в ₽ у API нет — всё уходит в comment.
const comment = [
name && `Имя: ${name}`,
contact && `Контакт: ${contact}`,
`Срок ожидания: ${dayLabel(days)}`,
`Сумма: ${ru(numAmount)}`,
`Комиссия: ≈${ru(commission)}`,
`Итого: ≈${ru(total)}`,
].filter(Boolean).join('; ')
submitRequest(
{ usdt_amount: numAmount, comment },
{
onSuccess: () => {
setNotice('success')
setAmount('')
setName('')
setContact('')
setDays(TERM_OPTIONS[0].days)
},
onError: () => setNotice('error'),
},
)
}
return (
<form className={styles.wrap} onSubmit={handleSubmit}>
<div className={styles.header}>
<h1 className={styles.title}>Оставить заявку</h1>
<p className={styles.subtitle}>
Конвертация крупных объёмов по индивидуальному курсу. Оставьте заявку
менеджер свяжется с вами, подтвердит актуальный курс и сопроводит сделку.
</p>
</div>
<div className={styles.body}>
<div className={styles.formCol}>
<FormField
label="Объём заявки, ₽"
type="text"
value={amount}
onChange={handleAmountChange}
placeholder="от 500 000"
/>
{belowMin && (
<p className={styles.hint}>
Минимальный объём заявки {ru(MIN_ORDER)}
</p>
)}
<Select
id="term"
label="Срок ожидания операции"
value={days}
onChange={setDays}
options={TERM_OPTIONS.map((o) => ({
value: o.days,
label: `${dayLabel(o.days)} — комиссия ${(o.rate * 100).toFixed(3)} %`,
}))}
/>
<FormField
label="Как к вам обращаться"
type="text"
value={name}
onChange={setName}
placeholder="Имя"
/>
<FormField
label="Email или телефон для связи"
type="text"
value={contact}
onChange={setContact}
placeholder="example@mail.ru / +7 900 000-00-00"
/>
</div>
<div className={styles.infoCol}>
<div className={styles.infoTitle}>УСЛОВИЯ</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Минимальный объём</span>
<span className={styles.infoValue}>{ru(MIN_ORDER)} </span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Срок ожидания</span>
<span className={styles.infoValue}>{dayLabel(days)}</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Ставка комиссии</span>
<span className={styles.infoValue}>{(rate * 100).toFixed(3)} %</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Сумма комиссии</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${ru(commission)}` : '—'}
</span>
</div>
<div className={styles.infoRow} data-accent>
<span className={styles.infoLabel}>Итого к оплате</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${ru(total)}` : '—'}
</span>
</div>
<p className={styles.note}>
Итоговая комиссия рассчитывается индивидуально и зависит от объёма,
валюты и направления сделки.
</p>
</div>
</div>
<button
type="submit"
className={styles.submitBtn}
disabled={belowMin || numAmount === 0 || isPending}
>
{isPending ? 'Отправляем...' : 'Оставить заявку'}
</button>
{notice && (
<Notification
status={notice}
message={notice === 'success' ? 'Заявка отправлена' : 'Не удалось отправить заявку'}
onClose={() => setNotice(null)}
/>
)}
</form>
)
}

View File

@@ -0,0 +1,338 @@
import type { LegalDocData } from '@entities/legal-doc'
export const politikaCookieDoc: LegalDocData = {
title: 'ПОЛИТИКА ИСПОЛЬЗОВАНИЯ ФАЙЛОВ COOKIE',
sections: [
{
title: 'Общие положения и терминология',
blocks: [
{
kind: 'text',
text: 'Настоящая Политика использования файлов cookie устанавливает порядок обработки файлов cookie и содержащихся в них персональных данных ООО «БИТФОРС» при использовании пользователями интернет-ресурса https://bitforce-foundation.ru.',
},
{
kind: 'text',
text: 'Файлы cookie — это текстовые файлы небольшого размера, которые устанавливаются на пользовательское устройство при посещении интернет-ресурса или совершении на нем определенных действий. Файлы cookie остаются сохраненными на устройстве даже после покидания ресурса, что позволяет «узнавать» пользователя при последующих посещениях.',
},
{
kind: 'text',
text: 'К персональным данным относится не сам файл cookie, а его содержимое — уникальные идентификаторы, IP-адреса, информация о предпочтениях пользователя и другие данные, позволяющие прямо или косвенно идентифицировать физическое лицо.',
},
],
},
{
title: 'Оператор персональных данных',
blocks: [
{ kind: 'text', text: 'Оператором персональных данных, содержащихся в файлах cookie, является:' },
{
kind: 'infoBox',
lines: [
'ООО «БИТФОРС»',
'ИНН: 9810001062',
'ОГРН: 1257800060990',
'Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н',
],
},
{
kind: 'text',
text: 'Оператор определяет цели обработки персональных данных, их состав, а также действия с персональными данными, включая случаи использования сторонних файлов cookie.',
},
],
},
{
title: 'Категории файлов cookie и их назначение',
blocks: [
{ kind: 'subheading', text: '1. Строго необходимые (технические) файлы cookie' },
{
kind: 'text',
text: 'Данные файлы обеспечивают работу интернет-ресурса и предоставление необходимого уровня сервиса: авторизацию, навигацию, отображение контента в соответствии с параметрами устройства, обеспечение безопасности.',
},
{
kind: 'text',
text: 'Обработка таких файлов cookie осуществляется на основании п. 5 ч. 1 ст. 6 ФЗ № 152 (заключение и исполнение договора). Согласие на использование строго необходимых файлов cookie не требуется.',
},
{
kind: 'text',
variant: 'example',
text: 'Примеры: файлы сессий (PHPSESSID), настройки безопасности, файлы аутентификации.',
},
{ kind: 'subheading', text: '2. Функциональные файлы cookie' },
{
kind: 'text',
text: 'Используются для запоминания пользовательских предпочтений и персонализации взаимодействия с сайтом: сохранение выбранного языка, региона, настроек отображения, размера шрифта.',
},
{
kind: 'text',
text: 'Обработка осуществляется на основании согласия субъекта персональных данных, поскольку данная обработка не является строго необходимой для функционирования сайта.',
},
{
kind: 'text',
variant: 'example',
text: 'Примеры: настройки языка интерфейса, предпочтения отображения, настройки доступности.',
},
{ kind: 'subheading', text: '3. Аналитические файлы cookie' },
{
kind: 'text',
text: 'Собирают информацию о взаимодействии пользователей с интернет-ресурсом для анализа его использования, выявления популярных разделов, обнаружения ошибок и улучшения пользовательского опыта. Могут содержать персональные данные, включая IP-адреса пользователей.',
},
{ kind: 'text', text: 'Обработка осуществляется на основании согласия субъекта персональных данных.' },
{ kind: 'subheading', text: '4. Маркетинговые файлы cookie' },
{
kind: 'text',
text: 'Используются для отслеживания пользователей в целях персонализированной рекламы, анализа эффективности рекламных кампаний, ретаргетинга.',
},
{
kind: 'text',
text: 'Обработка осуществляется исключительно на основании согласия субъекта персональных данных.',
},
{
kind: 'text',
variant: 'example',
text: 'Примеры: пиксели социальных сетей, рекламные идентификаторы, файлы ретаргетинга.',
},
],
},
{
title: 'Правовые основания обработки персональных данных',
blocks: [
{
kind: 'text',
text: 'Обработка персональных данных, содержащихся в файлах cookie, осуществляется на следующих правовых основаниях:',
},
{
kind: 'list',
items: [
{
strong: 'Согласие субъекта персональных данных',
text: '— для функциональных, аналитических и маркетинговых файлов cookie',
},
{
strong: 'Заключение и исполнение договора',
text: '— для строго необходимых файлов cookie, обеспечивающих работу интернет-ресурса',
},
{
strong: 'Законные интересы оператора',
text: '— в исключительных случаях, когда отсутствуют иные основания',
},
],
},
],
},
{
title: 'Порядок получения согласия',
blocks: [
{ kind: 'subheading', text: 'Принципы получения согласия:' },
{
kind: 'list',
items: [
'Согласие должно быть получено до начала обработки персональных данных',
'Информация об использовании файлов cookie размещается на первом уровне интернет-ресурса',
'Предоставляется возможность выбора категорий файлов cookie',
'Используются активные формулировки вместо пассивных',
],
},
{ kind: 'subheading', text: 'Критерии действительного согласия:' },
{
kind: 'list',
items: [
{ strong: 'Добровольность', text: '— согласие дается по свободной воле субъекта' },
{ strong: 'Конкретность', text: '— четко определены цели обработки' },
{ strong: 'Информированность', text: '— предоставлена полная информация об обработке' },
{ strong: 'Однозначность', text: '— согласие выражено в недвусмысленной форме' },
],
},
],
},
{
title: 'Сторонние файлы cookie',
blocks: [
{ kind: 'subheading', text: 'Использование сторонних сервисов:' },
{ kind: 'text', text: 'Наш интернет-ресурс использует файлы cookie сторонних сервисов, включая:' },
{
kind: 'list',
items: ['Яндекс.Метрика (ООО «ЯНДЕКС», Россия)', 'Социальные сети и сервисы интеграции'],
},
{ kind: 'subheading', text: 'Обеспечение защиты:' },
{
kind: 'list',
items: [
'Получено согласие на передачу',
'Применяются дополнительные меры защиты данных',
'Контролируется соблюдение принципов обработки персональных данных получателями',
],
},
],
},
{
title: 'Сроки обработки и хранения',
blocks: [
{ kind: 'subheading', text: 'Категории по срокам хранения:' },
{
kind: 'list',
items: [
'Сеансовые cookie — удаляются автоматически при закрытии браузера',
'Постоянные cookie — хранятся установленный период или до удаления пользователем',
],
},
{ kind: 'subheading', text: 'Конкретные сроки:' },
{
kind: 'list',
items: [
'Необходимые файлы cookie — до 12 месяцев',
'Функциональные файлы cookie — до 12 месяцев',
'Аналитические файлы cookie — до 24 месяцев',
'Маркетинговые файлы cookie — до 24 месяцев',
],
},
{
kind: 'text',
text: 'По истечении установленных сроков файлы cookie удаляются автоматически. Пользователь может удалить файлы cookie досрочно через настройки браузера или отозвать согласие на их обработку.',
},
],
},
{
title: 'Права субъектов персональных данных',
blocks: [
{ kind: 'subheading', text: 'Право на информацию:' },
{
kind: 'list',
items: [
'Получение информации о обработке персональных данных',
'Сведения о правовых основаниях и целях обработки',
'Информация о сроках обработки и составе данных',
],
},
{ kind: 'subheading', text: 'Право на доступ:' },
{
kind: 'list',
items: [
'Получение подтверждения факта обработки',
'Ознакомление с обрабатываемыми персональными данными',
'Получение информации об источниках персональных данных',
],
},
{ kind: 'subheading', text: 'Право на уточнение, блокирование, удаление:' },
{
kind: 'list',
items: [
'Требование уточнения неточных данных',
'Блокирование недостоверных данных',
'Удаление незаконно полученных данных',
],
},
{ kind: 'subheading', text: 'Право на отзыв согласия:' },
{
kind: 'list',
items: [
'Отзыв согласия в любое время',
'Прекращение обработки после отзыва согласия',
'Сохранение права на обжалование действий оператора',
],
},
],
},
{
title: 'Способы управления файлами cookie',
blocks: [
{ kind: 'subheading', text: 'Управление через настройки сайта:' },
{
kind: 'list',
items: [
'Использование баннера согласия на файлы cookie',
'Изменение настроек в любое время через интерфейс сайта',
'Отзыв согласия на использование отдельных категорий файлов cookie',
],
},
{ kind: 'subheading', text: 'Управление через браузер:' },
{ kind: 'text', text: 'Большинство браузеров позволяют контролировать файлы cookie:' },
{
kind: 'list',
items: [
'Блокировка — запрет установки новых файлов cookie',
'Удаление — очистка существующих файлов cookie',
'Уведомления — получение предупреждений при установке файлов cookie',
'Селективная настройка — разрешение файлов cookie только для определенных сайтов',
],
},
{ kind: 'subheading', text: 'Инструкции для популярных браузеров:' },
{
kind: 'list',
items: [
'Google Chrome: Настройки → Конфиденциальность и безопасность → Файлы cookie',
'Mozilla Firefox: Настройки → Приватность и Защита → Файлы cookie',
'Safari: Настройки → Конфиденциальность → Файлы cookie',
'Microsoft Edge: Настройки → Файлы cookie и разрешения сайтов',
],
},
{
kind: 'text',
variant: 'warning',
text: 'Блокировка необходимых файлов cookie может привести к ограничению функциональности интернет-ресурса.',
},
],
},
{
title: 'Меры безопасности',
blocks: [
{
kind: 'text',
text: 'Оператор применяет правовые, организационные и технические меры для защиты персональных данных:',
},
{ kind: 'subheading', text: 'Правовые меры:' },
{
kind: 'list',
items: [
'Назначение ответственного за организацию обработки персональных данных',
'Ознакомление сотрудников с требованиями законодательства',
'Заключение соглашений о неразглашении персональных данных',
],
},
{ kind: 'subheading', text: 'Организационные меры:' },
{
kind: 'list',
items: [
'Определение перечня лиц, допущенных к обработке персональных данных',
'Установление правил доступа к персональным данным',
'Контроль за соблюдением требований по защите персональных данных',
],
},
{ kind: 'subheading', text: 'Технические меры:' },
{
kind: 'list',
items: [
'Использование средств защиты информации',
'Применение криптографических средств защиты',
'Обеспечение целостности и доступности персональных данных',
'Регулярное обновление систем защиты информации',
],
},
],
},
{
title: 'Контактная информация и обращения',
blocks: [
{ kind: 'text', text: 'Для реализации прав субъекта персональных данных обращайтесь к нам:' },
{
kind: 'infoBox',
lines: [
'ООО «БИТФОРС»',
'ИНН: 9810001062',
'ОГРН: 1257800060990',
'Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н',
'Email компании: company@bitforcefoundation.ru',
],
},
{ kind: 'subheading', text: 'Порядок рассмотрения обращений:' },
{
kind: 'list',
items: [
'Срок рассмотрения обращений — до 30 дней с момента получения',
'Обращения рассматриваются в письменной форме',
'Ответ направляется способом, указанным в обращении',
'При отказе в удовлетворении требований указываются мотивированные основания',
],
},
],
},
],
}

View File

@@ -1,115 +0,0 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 30px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.subSectionTitle {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.list strong {
color: var(--text-primary, #ffffff);
}
.info {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
margin: 15px 0;
line-height: 1.8;
}
.info p {
margin: 5px 0;
color: var(--text-primary, #ffffff);
}
.example {
padding: 10px 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 3px solid var(--interactive, #4a6dff);
border-radius: 4px;
font-style: italic;
color: var(--text-primary, #ffffff);
margin: 10px 0;
}
.warning {
padding: 15px;
background: #fff3cd;
border-left: 4px solid #ffc107;
border-radius: 4px;
color: var(--text-primary, #ffffff);
margin: 15px 0;
font-weight: 500;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.sectionTitle {
font-size: 16px;
}
.subSectionTitle {
font-size: 14px;
}
.list {
margin-left: 15px;
}
.info {
padding: 15px;
}
}

View File

@@ -1,273 +1,13 @@
import { LegalDoc } from '@entities/legal-doc'
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PolitikaCookiePage.module.css'
import { politikaCookieDoc } from '../model/content'
export function PolitikaCookiePage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПОЛИТИКА ИСПОЛЬЗОВАНИЯ ФАЙЛОВ COOKIE</h1>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Общие положения и терминология</h3>
<p>
Настоящая Политика использования файлов cookie устанавливает порядок обработки файлов cookie и содержащихся в них персональных данных ООО «БИТФОРС» при использовании пользователями интернет-ресурса https://bitforce-foundation.ru.
</p>
<p>
Файлы cookie это текстовые файлы небольшого размера, которые устанавливаются на пользовательское устройство при посещении интернет-ресурса или совершении на нем определенных действий. Файлы cookie остаются сохраненными на устройстве даже после покидания ресурса, что позволяет «узнавать» пользователя при последующих посещениях.
</p>
<p>
К персональным данным относится не сам файл cookie, а его содержимое уникальные идентификаторы, IP-адреса, информация о предпочтениях пользователя и другие данные, позволяющие прямо или косвенно идентифицировать физическое лицо.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Оператор персональных данных</h3>
<p>Оператором персональных данных, содержащихся в файлах cookie, является:</p>
<div className={styles.info}>
<p>ООО «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н</p>
</div>
<p>
Оператор определяет цели обработки персональных данных, их состав, а также действия с персональными данными, включая случаи использования сторонних файлов cookie.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Категории файлов cookie и их назначение</h3>
<h4 className={styles.subSectionTitle}>1. Строго необходимые (технические) файлы cookie</h4>
<p>
Данные файлы обеспечивают работу интернет-ресурса и предоставление необходимого уровня сервиса: авторизацию, навигацию, отображение контента в соответствии с параметрами устройства, обеспечение безопасности.
</p>
<p>
Обработка таких файлов cookie осуществляется на основании п. 5 ч. 1 ст. 6 ФЗ 152 (заключение и исполнение договора). Согласие на использование строго необходимых файлов cookie не требуется.
</p>
<p className={styles.example}>Примеры: файлы сессий (PHPSESSID), настройки безопасности, файлы аутентификации.</p>
<h4 className={styles.subSectionTitle}>2. Функциональные файлы cookie</h4>
<p>
Используются для запоминания пользовательских предпочтений и персонализации взаимодействия с сайтом: сохранение выбранного языка, региона, настроек отображения, размера шрифта.
</p>
<p>
Обработка осуществляется на основании согласия субъекта персональных данных, поскольку данная обработка не является строго необходимой для функционирования сайта.
</p>
<p className={styles.example}>Примеры: настройки языка интерфейса, предпочтения отображения, настройки доступности.</p>
<h4 className={styles.subSectionTitle}>3. Аналитические файлы cookie</h4>
<p>
Собирают информацию о взаимодействии пользователей с интернет-ресурсом для анализа его использования, выявления популярных разделов, обнаружения ошибок и улучшения пользовательского опыта. Могут содержать персональные данные, включая IP-адреса пользователей.
</p>
<p>
Обработка осуществляется на основании согласия субъекта персональных данных.
</p>
<h4 className={styles.subSectionTitle}>4. Маркетинговые файлы cookie</h4>
<p>
Используются для отслеживания пользователей в целях персонализированной рекламы, анализа эффективности рекламных кампаний, ретаргетинга.
</p>
<p>
Обработка осуществляется исключительно на основании согласия субъекта персональных данных.
</p>
<p className={styles.example}>Примеры: пиксели социальных сетей, рекламные идентификаторы, файлы ретаргетинга.</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Правовые основания обработки персональных данных</h3>
<p>Обработка персональных данных, содержащихся в файлах cookie, осуществляется на следующих правовых основаниях:</p>
<ul className={styles.list}>
<li>
<strong>Согласие субъекта персональных данных</strong> для функциональных, аналитических и маркетинговых файлов cookie
</li>
<li>
<strong>Заключение и исполнение договора</strong> для строго необходимых файлов cookie, обеспечивающих работу интернет-ресурса
</li>
<li>
<strong>Законные интересы оператора</strong> в исключительных случаях, когда отсутствуют иные основания
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Порядок получения согласия</h3>
<h4 className={styles.subSectionTitle}>Принципы получения согласия:</h4>
<ul className={styles.list}>
<li>Согласие должно быть получено до начала обработки персональных данных</li>
<li>Информация об использовании файлов cookie размещается на первом уровне интернет-ресурса</li>
<li>Предоставляется возможность выбора категорий файлов cookie</li>
<li>Используются активные формулировки вместо пассивных</li>
</ul>
<h4 className={styles.subSectionTitle}>Критерии действительного согласия:</h4>
<ul className={styles.list}>
<li>
<strong>Добровольность</strong> согласие дается по свободной воле субъекта
</li>
<li>
<strong>Конкретность</strong> четко определены цели обработки
</li>
<li>
<strong>Информированность</strong> предоставлена полная информация об обработке
</li>
<li>
<strong>Однозначность</strong> согласие выражено в недвусмысленной форме
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Сторонние файлы cookie</h3>
<h4 className={styles.subSectionTitle}>Использование сторонних сервисов:</h4>
<p>Наш интернет-ресурс использует файлы cookie сторонних сервисов, включая:</p>
<ul className={styles.list}>
<li>Яндекс.Метрика (ООО «ЯНДЕКС», Россия)</li>
<li>Социальные сети и сервисы интеграции</li>
</ul>
<h4 className={styles.subSectionTitle}>Обеспечение защиты:</h4>
<ul className={styles.list}>
<li>Получено согласие на передачу</li>
<li>Применяются дополнительные меры защиты данных</li>
<li>Контролируется соблюдение принципов обработки персональных данных получателями</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Сроки обработки и хранения</h3>
<h4 className={styles.subSectionTitle}>Категории по срокам хранения:</h4>
<ul className={styles.list}>
<li>Сеансовые cookie удаляются автоматически при закрытии браузера</li>
<li>Постоянные cookie хранятся установленный период или до удаления пользователем</li>
</ul>
<h4 className={styles.subSectionTitle}>Конкретные сроки:</h4>
<ul className={styles.list}>
<li>Необходимые файлы cookie до 12 месяцев</li>
<li>Функциональные файлы cookie до 12 месяцев</li>
<li>Аналитические файлы cookie до 24 месяцев</li>
<li>Маркетинговые файлы cookie до 24 месяцев</li>
</ul>
<p>
По истечении установленных сроков файлы cookie удаляются автоматически. Пользователь может удалить файлы cookie досрочно через настройки браузера или отозвать согласие на их обработку.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Права субъектов персональных данных</h3>
<h4 className={styles.subSectionTitle}>Право на информацию:</h4>
<ul className={styles.list}>
<li>Получение информации о обработке персональных данных</li>
<li>Сведения о правовых основаниях и целях обработки</li>
<li>Информация о сроках обработки и составе данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на доступ:</h4>
<ul className={styles.list}>
<li>Получение подтверждения факта обработки</li>
<li>Ознакомление с обрабатываемыми персональными данными</li>
<li>Получение информации об источниках персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на уточнение, блокирование, удаление:</h4>
<ul className={styles.list}>
<li>Требование уточнения неточных данных</li>
<li>Блокирование недостоверных данных</li>
<li>Удаление незаконно полученных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Право на отзыв согласия:</h4>
<ul className={styles.list}>
<li>Отзыв согласия в любое время</li>
<li>Прекращение обработки после отзыва согласия</li>
<li>Сохранение права на обжалование действий оператора</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Способы управления файлами cookie</h3>
<h4 className={styles.subSectionTitle}>Управление через настройки сайта:</h4>
<ul className={styles.list}>
<li>Использование баннера согласия на файлы cookie</li>
<li>Изменение настроек в любое время через интерфейс сайта</li>
<li>Отзыв согласия на использование отдельных категорий файлов cookie</li>
</ul>
<h4 className={styles.subSectionTitle}>Управление через браузер:</h4>
<p>Большинство браузеров позволяют контролировать файлы cookie:</p>
<ul className={styles.list}>
<li>Блокировка запрет установки новых файлов cookie</li>
<li>Удаление очистка существующих файлов cookie</li>
<li>Уведомления получение предупреждений при установке файлов cookie</li>
<li>Селективная настройка разрешение файлов cookie только для определенных сайтов</li>
</ul>
<h4 className={styles.subSectionTitle}>Инструкции для популярных браузеров:</h4>
<ul className={styles.list}>
<li>Google Chrome: Настройки Конфиденциальность и безопасность Файлы cookie</li>
<li>Mozilla Firefox: Настройки Приватность и Защита Файлы cookie</li>
<li>Safari: Настройки Конфиденциальность Файлы cookie</li>
<li>Microsoft Edge: Настройки Файлы cookie и разрешения сайтов</li>
</ul>
<p className={styles.warning}>
Блокировка необходимых файлов cookie может привести к ограничению функциональности интернет-ресурса.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Меры безопасности</h3>
<p>
Оператор применяет правовые, организационные и технические меры для защиты персональных данных:
</p>
<h4 className={styles.subSectionTitle}>Правовые меры:</h4>
<ul className={styles.list}>
<li>Назначение ответственного за организацию обработки персональных данных</li>
<li>Ознакомление сотрудников с требованиями законодательства</li>
<li>Заключение соглашений о неразглашении персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Организационные меры:</h4>
<ul className={styles.list}>
<li>Определение перечня лиц, допущенных к обработке персональных данных</li>
<li>Установление правил доступа к персональным данным</li>
<li>Контроль за соблюдением требований по защите персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>Технические меры:</h4>
<ul className={styles.list}>
<li>Использование средств защиты информации</li>
<li>Применение криптографических средств защиты</li>
<li>Обеспечение целостности и доступности персональных данных</li>
<li>Регулярное обновление систем защиты информации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Контактная информация и обращения</h3>
<p>Для реализации прав субъекта персональных данных обращайтесь к нам:</p>
<div className={styles.info}>
<p>ООО «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское ш, д. 25 к. 1 литера В, помещ. 3-н</p>
<p>Email компании: company@bitforcefoundation.ru</p>
</div>
<h4 className={styles.subSectionTitle}>Порядок рассмотрения обращений:</h4>
<ul className={styles.list}>
<li>Срок рассмотрения обращений до 30 дней с момента получения</li>
<li>Обращения рассматриваются в письменной форме</li>
<li>Ответ направляется способом, указанным в обращении</li>
<li>При отказе в удовлетворении требований указываются мотивированные основания</li>
</ul>
</section>
</div>
</main>
<LegalDoc doc={politikaCookieDoc} />
<Footer />
</>
)

View File

@@ -0,0 +1,361 @@
import type { LegalDocData } from '@entities/legal-doc'
export const politikaPersonalnyhDannyhDoc: LegalDocData = {
title: 'ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ',
subtitle: 'ООО «БИТФОРС»',
sections: [
{
title: '1. Общие положения',
blocks: [
{
kind: 'text',
text: 'Настоящая Политика обработки персональных данных разработана в соответствии с Федеральным законом от 27.07.2006 № 152-ФЗ «О персональных данных» и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые ООО «БИТФОРС».',
},
{
kind: 'text',
text: 'Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты права на неприкосновенность частной жизни, личную и семейную тайну.',
},
{
kind: 'text',
text: 'Настоящая Политика действует в отношении всех персональных данных, которые обрабатываются Оператором с использованием средств автоматизации и без использования таких средств.',
},
{ kind: 'subheading', text: '1.4. Основные понятия' },
{
kind: 'definitions',
items: [
{
term: 'Автоматизированная обработка персональных данных',
text: '— обработка персональных данных с помощью средств вычислительной техники.',
},
{
term: 'Обработка персональных данных',
text: '— любое действие или совокупность действий, совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение, извлечение, использование, передачу, обезличивание, блокирование, удаление, уничтожение.',
},
{
term: 'Оператор',
text: '— юридическое или физическое лицо, организующие и осуществляющие обработку персональных данных.',
},
{
term: 'Персональные данные',
text: '— любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу.',
},
{
term: 'Пользователь',
text: '— любой посетитель веб-сайта https://bitforce-foundation.ru.',
},
],
},
],
},
{
title: '2. Сведения об операторе',
blocks: [
{
kind: 'list',
items: [
'Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»',
'Сокращенное наименование: ООО «БИТФОРС»',
'ИНН: 9810001062',
'ОГРН: 1257800060990',
'Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н',
'Электронная почта: company@bitforcefoundation.ru',
'Веб-сайт: https://bitforce-foundation.ru',
],
},
],
},
{
title: '3. Общие цели обработки персональных данных',
blocks: [
{ kind: 'subheading', text: '3.1.1. Основная деятельность:' },
{
kind: 'list',
items: [
'Предоставление услуг по конвертации иного имущества',
'Осуществление операций на криптовалютных рынках',
'Предоставление услуг в области блокчейн технологий',
'Обеспечение функционирования интернет-платформы и мобильных приложений',
],
},
{ kind: 'subheading', text: '3.1.2. Обеспечение безопасности:' },
{
kind: 'list',
items: [
'Предотвращение мошенничества и отмывания денежных средств',
'Обеспечение безопасности платежных операций',
'Выполнение требований по противодействию легализации доходов',
'Идентификация и верификация клиентов',
],
},
{ kind: 'subheading', text: '3.1.3. Соблюдение законодательства:' },
{
kind: 'list',
items: [
'Исполнение требований российского и международного законодательства',
'Взаимодействие с контролирующими и правоохранительными органами',
'Ведение обязательной отчетности и документооборота',
'Соблюдение налогового законодательства',
],
},
],
},
{
title: '4. Цели сбора персональных данных',
blocks: [
{
kind: 'goals',
items: [
{
title: 'Регистрация и идентификация пользователей:',
items: [
'Создание учетной записи на веб-сайте',
'Верификация личности в соответствии с требованиями законодательства',
'Подтверждение права на осуществление операций',
],
},
{
title: 'Обработка платежей и финансовых операций:',
items: [
'Осуществление операций по конвертации криптовалют',
'Проведение расчетов и переводов денежных средств',
'Ведение учета и истории транзакций',
],
},
{
title: 'Коммуникация с клиентами:',
items: [
'Предоставление технической поддержки',
'Уведомления о состоянии операций и счетов',
'Информирование об изменениях в условиях предоставления услуг',
],
},
],
},
],
},
{
title: '5. Правовые основания обработки персональных данных',
blocks: [
{ kind: 'subheading', text: '5.1.1. Согласие субъекта персональных данных:' },
{
kind: 'list',
items: [
'Обработка персональных данных в маркетинговых целях',
'Использование файлов cookie и метрик',
'Персонализация сервисов и предложений',
],
},
{ kind: 'subheading', text: '5.1.2. Необходимость исполнения договора:' },
{
kind: 'list',
items: [
'Регистрация и ведение учетных записей пользователей',
'Осуществление финансовых операций и переводов',
'Предоставление доступа к платформе и сервисам',
'Техническая поддержка и обслуживание клиентов',
],
},
{ kind: 'subheading', text: '5.1.3. Соблюдение правовой обязанности:' },
{
kind: 'list',
items: [
'Выполнение требований валютного законодательства',
'Противодействие легализации доходов, полученных преступным путем',
'Соблюдение требований по налоговому учету и отчетности',
],
},
],
},
{
title: '6. Объем и категории обрабатываемых персональных данных',
blocks: [
{ kind: 'subheading', text: '6.1.1. Пользователи веб-сайта и мобильного приложения:' },
{
kind: 'list',
items: [
'Зарегистрированные пользователи',
'Посетители сайта без регистрации',
'Потенциальные клиенты',
'Бывшие клиенты',
],
},
{ kind: 'subheading', text: '6.2.1. Идентификационные данные:' },
{ kind: 'list', items: ['Фамилия, имя, отчество', 'Дата рождения', 'Гражданство'] },
{ kind: 'subheading', text: '6.2.3. Контактная информация:' },
{
kind: 'list',
items: ['Номера телефонов (мобильный, домашний, рабочий)', 'Адреса электронной почты'],
},
{ kind: 'subheading', text: '6.2.4. Финансовая информация:' },
{
kind: 'list',
items: [
'Номера банковских счетов и карт',
'Реквизиты кошельков криптовалют',
'История операций и транзакций',
'Данные о доходах и источниках средств',
],
},
{ kind: 'subheading', text: '6.2.5. Техническая информация:' },
{
kind: 'list',
items: [
'IP-адреса устройств',
'Данные о браузере и операционной системе',
'Файлы cookie и локальное хранилище',
'Логи действий на сайте',
],
},
],
},
{
title: '7. Порядок и условия обработки персональных данных',
blocks: [
{ kind: 'subheading', text: '7.1. Принципы обработки персональных данных:' },
{
kind: 'list',
items: [
'Обработка осуществляется на законной и справедливой основе',
'Обработка ограничивается достижением конкретных, заранее определенных целей',
'Содержание и объем данных соответствуют заявленным целям',
'Обрабатываемые персональные данные являются точными и актуальными',
],
},
{ kind: 'subheading', text: '7.4. Сроки обработки персональных данных:' },
{
kind: 'list',
items: [
'Персональные данные обрабатываются в течение времени, необходимого для достижения целей',
'После достижения целей персональные данные подлежат уничтожению или обезличиванию',
'Сроки хранения определяются требованиями законодательства',
],
},
{ kind: 'subheading', text: '7.5. Места обработки персональных данных:' },
{
kind: 'list',
items: [
'Основные серверы и хранилища данных расположены на территории Российской Федерации',
'Резервные копии могут храниться в дата-центрах на территории РФ',
],
},
],
},
{
title: '8. Актуализация, исправление, удаление и уничтожение персональных данных',
blocks: [
{ kind: 'subheading', text: '8.2.2. Процедура исправления:' },
{
kind: 'list',
items: [
'Рассмотрение запроса в течение 30 дней',
'Проверка обоснованности требования об исправлении',
'Внесение изменений во все информационные системы',
'Уведомление субъекта о проведенных исправлениях',
],
},
{ kind: 'subheading', text: '8.3.2. Процедура удаления:' },
{
kind: 'list',
items: [
'Проверка наличия законных оснований для продолжения обработки',
'Удаление из всех информационных систем и баз данных',
'Удаление резервных копий (кроме архивных)',
'Уведомление субъекта о выполненном удалении',
],
},
],
},
{
title: '9. Ответы на запросы субъектов персональных данных',
blocks: [
{ kind: 'subheading', text: '9.1.1. Право на информацию:' },
{
kind: 'list',
items: [
'Подтверждение факта обработки персональных данных',
'Правовые основания и цели обработки',
'Применяемые способы обработки',
'Наименование и местонахождение оператора',
'Лица, имеющие доступ к персональным данным',
],
},
{ kind: 'subheading', text: '9.2.2. Сроки рассмотрения:' },
{
kind: 'list',
items: [
'Срок рассмотрения запроса составляет 30 дней с момента получения',
'Срок может быть продлен на 30 дней при большом объеме информации',
'О продлении срока субъект уведомляется в течение 30 дней',
],
},
{ kind: 'subheading', text: '9.4. Плата за предоставление информации:' },
{
kind: 'list',
items: [
'Первый запрос в течение года обрабатывается бесплатно',
'За повторные запросы может взиматься плата в размере расходов',
'Субъект уведомляется о размере платы до предоставления информации',
],
},
],
},
{
title: '10. Обеспечение безопасности персональных данных',
blocks: [
{ kind: 'subheading', text: '10.1. Правовые меры:' },
{
kind: 'list',
items: [
'Назначение ответственного за организацию обработки персональных данных',
'Принятие локальных актов по вопросам обработки персональных данных',
'Ознакомление работников с требованиями законодательства',
'Применение мер ответственности за нарушение требований',
],
},
{ kind: 'subheading', text: '10.3. Технические меры:' },
{
kind: 'list',
items: [
'Предотвращение несанкционированного доступа к персональным данным',
'Своевременное обнаружение фактов несанкционированного доступа',
'Возможность незамедлительного восстановления персональных данных',
'Постоянный контроль за обеспечением уровня защищенности',
],
},
{ kind: 'subheading', text: '10.4. Конкретные технические решения:' },
{
kind: 'list',
items: [
'Использование сертифицированных средств защиты информации',
'Шифрование персональных данных при передаче и хранении',
'Применение межсетевых экранов и систем обнаружения вторжений',
'Резервное копирование и обеспечение отказоустойчивости',
'Антивирусная защита и обновление программного обеспечения',
],
},
],
},
{
title: '12. Заключительные положения',
blocks: [
{ kind: 'subheading', text: '12.2. Жалобы и обращения:' },
{
kind: 'list',
items: [
'Субъекты персональных данных могут обратиться к Оператору по вопросам обработки',
'Жалобы рассматриваются в установленном законом порядке',
'При неурегулировании разногласий возможно обращение в Роскомнадзор или суд',
],
},
{ kind: 'subheading', text: '12.4. Контактная информация для обращений:' },
{
kind: 'infoBox',
lines: [
'Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н',
'Электронная почта: company@bitforcefoundation.ru',
],
},
],
},
],
}

View File

@@ -1,299 +1,13 @@
import { LegalDoc } from '@entities/legal-doc'
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PolitikaPage.module.css'
import { politikaPersonalnyhDannyhDoc } from '../model/content'
export function PolitikaPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Общие положения</h3>
<p>
Настоящая Политика обработки персональных данных разработана в соответствии с Федеральным законом от 27.07.2006 152-ФЗ «О персональных данных» и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые ООО «БИТФОРС».
</p>
<p>
Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты права на неприкосновенность частной жизни, личную и семейную тайну.
</p>
<p>
Настоящая Политика действует в отношении всех персональных данных, которые обрабатываются Оператором с использованием средств автоматизации и без использования таких средств.
</p>
<h4 className={styles.subSectionTitle}>1.4. Основные понятия</h4>
<div className={styles.definitions}>
<div className={styles.definition}>
<strong>Автоматизированная обработка персональных данных</strong> обработка персональных данных с помощью средств вычислительной техники.
</div>
<div className={styles.definition}>
<strong>Обработка персональных данных</strong> любое действие или совокупность действий, совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными, включая сбор, запись, систематизацию, накопление, хранение, уточнение, извлечение, использование, передачу, обезличивание, блокирование, удаление, уничтожение.
</div>
<div className={styles.definition}>
<strong>Оператор</strong> юридическое или физическое лицо, организующие и осуществляющие обработку персональных данных.
</div>
<div className={styles.definition}>
<strong>Персональные данные</strong> любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу.
</div>
<div className={styles.definition}>
<strong>Пользователь</strong> любой посетитель веб-сайта https://bitforce-foundation.ru.
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Сведения об операторе</h3>
<ul className={styles.list}>
<li>Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»</li>
<li>Сокращенное наименование: ООО «БИТФОРС»</li>
<li>ИНН: 9810001062</li>
<li>ОГРН: 1257800060990</li>
<li>Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н</li>
<li>Электронная почта: company@bitforcefoundation.ru</li>
<li>Веб-сайт: https://bitforce-foundation.ru</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Общие цели обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>3.1.1. Основная деятельность:</h4>
<ul className={styles.list}>
<li>Предоставление услуг по конвертации иного имущества</li>
<li>Осуществление операций на криптовалютных рынках</li>
<li>Предоставление услуг в области блокчейн технологий</li>
<li>Обеспечение функционирования интернет-платформы и мобильных приложений</li>
</ul>
<h4 className={styles.subSectionTitle}>3.1.2. Обеспечение безопасности:</h4>
<ul className={styles.list}>
<li>Предотвращение мошенничества и отмывания денежных средств</li>
<li>Обеспечение безопасности платежных операций</li>
<li>Выполнение требований по противодействию легализации доходов</li>
<li>Идентификация и верификация клиентов</li>
</ul>
<h4 className={styles.subSectionTitle}>3.1.3. Соблюдение законодательства:</h4>
<ul className={styles.list}>
<li>Исполнение требований российского и международного законодательства</li>
<li>Взаимодействие с контролирующими и правоохранительными органами</li>
<li>Ведение обязательной отчетности и документооборота</li>
<li>Соблюдение налогового законодательства</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Цели сбора персональных данных</h3>
<div className={styles.goalsList}>
<div className={styles.goal}>
<strong>Регистрация и идентификация пользователей:</strong>
<ul>
<li>Создание учетной записи на веб-сайте</li>
<li>Верификация личности в соответствии с требованиями законодательства</li>
<li>Подтверждение права на осуществление операций</li>
</ul>
</div>
<div className={styles.goal}>
<strong>Обработка платежей и финансовых операций:</strong>
<ul>
<li>Осуществление операций по конвертации криптовалют</li>
<li>Проведение расчетов и переводов денежных средств</li>
<li>Ведение учета и истории транзакций</li>
</ul>
</div>
<div className={styles.goal}>
<strong>Коммуникация с клиентами:</strong>
<ul>
<li>Предоставление технической поддержки</li>
<li>Уведомления о состоянии операций и счетов</li>
<li>Информирование об изменениях в условиях предоставления услуг</li>
</ul>
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Правовые основания обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>5.1.1. Согласие субъекта персональных данных:</h4>
<ul className={styles.list}>
<li>Обработка персональных данных в маркетинговых целях</li>
<li>Использование файлов cookie и метрик</li>
<li>Персонализация сервисов и предложений</li>
</ul>
<h4 className={styles.subSectionTitle}>5.1.2. Необходимость исполнения договора:</h4>
<ul className={styles.list}>
<li>Регистрация и ведение учетных записей пользователей</li>
<li>Осуществление финансовых операций и переводов</li>
<li>Предоставление доступа к платформе и сервисам</li>
<li>Техническая поддержка и обслуживание клиентов</li>
</ul>
<h4 className={styles.subSectionTitle}>5.1.3. Соблюдение правовой обязанности:</h4>
<ul className={styles.list}>
<li>Выполнение требований валютного законодательства</li>
<li>Противодействие легализации доходов, полученных преступным путем</li>
<li>Соблюдение требований по налоговому учету и отчетности</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Объем и категории обрабатываемых персональных данных</h3>
<h4 className={styles.subSectionTitle}>6.1.1. Пользователи веб-сайта и мобильного приложения:</h4>
<ul className={styles.list}>
<li>Зарегистрированные пользователи</li>
<li>Посетители сайта без регистрации</li>
<li>Потенциальные клиенты</li>
<li>Бывшие клиенты</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.1. Идентификационные данные:</h4>
<ul className={styles.list}>
<li>Фамилия, имя, отчество</li>
<li>Дата рождения</li>
<li>Гражданство</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.3. Контактная информация:</h4>
<ul className={styles.list}>
<li>Номера телефонов (мобильный, домашний, рабочий)</li>
<li>Адреса электронной почты</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.4. Финансовая информация:</h4>
<ul className={styles.list}>
<li>Номера банковских счетов и карт</li>
<li>Реквизиты кошельков криптовалют</li>
<li>История операций и транзакций</li>
<li>Данные о доходах и источниках средств</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2.5. Техническая информация:</h4>
<ul className={styles.list}>
<li>IP-адреса устройств</li>
<li>Данные о браузере и операционной системе</li>
<li>Файлы cookie и локальное хранилище</li>
<li>Логи действий на сайте</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Порядок и условия обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>7.1. Принципы обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Обработка осуществляется на законной и справедливой основе</li>
<li>Обработка ограничивается достижением конкретных, заранее определенных целей</li>
<li>Содержание и объем данных соответствуют заявленным целям</li>
<li>Обрабатываемые персональные данные являются точными и актуальными</li>
</ul>
<h4 className={styles.subSectionTitle}>7.4. Сроки обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Персональные данные обрабатываются в течение времени, необходимого для достижения целей</li>
<li>После достижения целей персональные данные подлежат уничтожению или обезличиванию</li>
<li>Сроки хранения определяются требованиями законодательства</li>
</ul>
<h4 className={styles.subSectionTitle}>7.5. Места обработки персональных данных:</h4>
<ul className={styles.list}>
<li>Основные серверы и хранилища данных расположены на территории Российской Федерации</li>
<li>Резервные копии могут храниться в дата-центрах на территории РФ</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Актуализация, исправление, удаление и уничтожение персональных данных</h3>
<h4 className={styles.subSectionTitle}>8.2.2. Процедура исправления:</h4>
<ul className={styles.list}>
<li>Рассмотрение запроса в течение 30 дней</li>
<li>Проверка обоснованности требования об исправлении</li>
<li>Внесение изменений во все информационные системы</li>
<li>Уведомление субъекта о проведенных исправлениях</li>
</ul>
<h4 className={styles.subSectionTitle}>8.3.2. Процедура удаления:</h4>
<ul className={styles.list}>
<li>Проверка наличия законных оснований для продолжения обработки</li>
<li>Удаление из всех информационных систем и баз данных</li>
<li>Удаление резервных копий (кроме архивных)</li>
<li>Уведомление субъекта о выполненном удалении</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Ответы на запросы субъектов персональных данных</h3>
<h4 className={styles.subSectionTitle}>9.1.1. Право на информацию:</h4>
<ul className={styles.list}>
<li>Подтверждение факта обработки персональных данных</li>
<li>Правовые основания и цели обработки</li>
<li>Применяемые способы обработки</li>
<li>Наименование и местонахождение оператора</li>
<li>Лица, имеющие доступ к персональным данным</li>
</ul>
<h4 className={styles.subSectionTitle}>9.2.2. Сроки рассмотрения:</h4>
<ul className={styles.list}>
<li>Срок рассмотрения запроса составляет 30 дней с момента получения</li>
<li>Срок может быть продлен на 30 дней при большом объеме информации</li>
<li>О продлении срока субъект уведомляется в течение 30 дней</li>
</ul>
<h4 className={styles.subSectionTitle}>9.4. Плата за предоставление информации:</h4>
<ul className={styles.list}>
<li>Первый запрос в течение года обрабатывается бесплатно</li>
<li>За повторные запросы может взиматься плата в размере расходов</li>
<li>Субъект уведомляется о размере платы до предоставления информации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>10. Обеспечение безопасности персональных данных</h3>
<h4 className={styles.subSectionTitle}>10.1. Правовые меры:</h4>
<ul className={styles.list}>
<li>Назначение ответственного за организацию обработки персональных данных</li>
<li>Принятие локальных актов по вопросам обработки персональных данных</li>
<li>Ознакомление работников с требованиями законодательства</li>
<li>Применение мер ответственности за нарушение требований</li>
</ul>
<h4 className={styles.subSectionTitle}>10.3. Технические меры:</h4>
<ul className={styles.list}>
<li>Предотвращение несанкционированного доступа к персональным данным</li>
<li>Своевременное обнаружение фактов несанкционированного доступа</li>
<li>Возможность незамедлительного восстановления персональных данных</li>
<li>Постоянный контроль за обеспечением уровня защищенности</li>
</ul>
<h4 className={styles.subSectionTitle}>10.4. Конкретные технические решения:</h4>
<ul className={styles.list}>
<li>Использование сертифицированных средств защиты информации</li>
<li>Шифрование персональных данных при передаче и хранении</li>
<li>Применение межсетевых экранов и систем обнаружения вторжений</li>
<li>Резервное копирование и обеспечение отказоустойчивости</li>
<li>Антивирусная защита и обновление программного обеспечения</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>12. Заключительные положения</h3>
<h4 className={styles.subSectionTitle}>12.2. Жалобы и обращения:</h4>
<ul className={styles.list}>
<li>Субъекты персональных данных могут обратиться к Оператору по вопросам обработки</li>
<li>Жалобы рассматриваются в установленном законом порядке</li>
<li>При неурегулировании разногласий возможно обращение в Роскомнадзор или суд</li>
</ul>
<h4 className={styles.subSectionTitle}>12.4. Контактная информация для обращений:</h4>
<p className={styles.contacts}>
Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н<br />
Электронная почта: company@bitforcefoundation.ru
</p>
</section>
</div>
</main>
<LegalDoc doc={politikaPersonalnyhDannyhDoc} />
<Footer />
</>
)

137
src/pages/profile/README.md Normal file
View 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) — отдельная задача на дедупликацию.
Внимание: часть из них использует вариант с 46 знаками, а не фиксированные 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 не сломан.

View File

@@ -5,17 +5,6 @@
background: var(--bg-deep);
}
.kycBanner {
width: 100%;
padding: 10px 24px;
background: var(--error, #ff4466);
color: #fff;
font-size: 13px;
font-weight: 500;
text-align: center;
line-height: 1.4;
}
.main {
max-width: 1024px;
width: 100%;
@@ -25,15 +14,6 @@
gap: 32px;
}
/* Desktop: avatar is left column, sections grow right */
.profileTop {
flex-shrink: 0;
}
.userInfo {
display: none;
}
.sections {
flex: 1;
display: flex;
@@ -41,127 +21,18 @@
gap: 16px;
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.grid1 {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.mnemonicRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
}
.mnemonicInfo {
display: flex;
align-items: center;
gap: 12px;
}
.mnemonicIcon {
font-size: 16px;
}
.mnemonicText {
color: var(--text-secondary);
font-size: 14px;
font-weight: 400;
}
/* < 1024px: column layout, avatar + userInfo side by side at top */
/* < 1024px: avatar + sections stack into a single column */
@media (max-width: 1023px) {
.main {
flex-direction: column;
padding: 24px 20px 40px;
gap: 24px;
}
.grid2 {
gap: 12px;
}
.profileTop {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 24px;
}
.userInfo {
display: flex;
flex-direction: column;
gap: 6px;
justify-content: center;
}
.userName {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.userBalance {
font-size: 32px;
font-weight: 800;
color: var(--text-primary);
}
.userBalanceRub {
font-size: 13px;
color: var(--text-secondary);
}
}
/* < 650px: card padding 16px (в ProfileSection.module.css),
кнопка мнемоники — 100% ширины */
@media (max-width: 649px) {
.mnemonicRow {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
}
/* < 640px: инпуты в колонку */
/* < 640px: tighter page padding */
@media (max-width: 639px) {
.main {
padding: 16px 16px 40px;
}
.grid2 {
grid-template-columns: 1fr;
}
}
/* < 550px: меньше текст и аватар */
@media (max-width: 549px) {
.profileTop {
gap: 16px;
}
.userInfo {
padding-top: 8px;
}
.userName {
font-size: 18px;
}
.userBalance {
font-size: 22px;
margin-top: 4px;
}
.userBalanceRub {
font-size: 12px;
}
}

View File

@@ -1,136 +1,20 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useMe, useUpdatePhone } from '@features/auth'
import { usePortfolio, useWalletAddresses } from '@features/wallet'
import { ROUTES } from '@shared/config/routes'
import { Button, FormField, Notification } from '@shared/ui'
import { WalletHeader } from '@widgets/wallet-header'
import { ProfileAvatar, ProfileSection } from '@widgets/profile'
import { IndividualFields } from './IndividualFields'
import { LegalEntityFields } from './LegalEntityFields'
import { KycBanner, ProfileSummary, ProfileDetails, SecuritySection, MnemonicSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
export function ProfilePage() {
const { data } = useMe()
const { data: portfolio, isLoading: isPortfolioLoading } = usePortfolio()
const { data: walletAddresses } = useWalletAddresses()
const updatePhone = useUpdatePhone()
const navigate = useNavigate()
const [phone, setPhone] = useState('')
const [savedPhone, setSavedPhone] = useState('')
const [notification, setNotification] = useState<{ message: string; status: 'success' | 'error' } | null>(null)
useEffect(() => {
if (data?.phone != null) {
setPhone(data.phone)
setSavedPhone(data.phone)
}
}, [data?.phone])
function handlePhoneChange(value: string) {
setPhone(value.replace(/[^\d+\s()-]/g, ''))
}
function handlePhoneBlur() {
const next = phone.trim()
if (next === savedPhone || updatePhone.isPending) return
updatePhone.mutate(next, {
onSuccess: () => {
setSavedPhone(next)
setNotification({ status: 'success', message: 'Номер телефона обновлён' })
},
onError: () => {
setNotification({ status: 'error', message: 'Не удалось обновить номер телефона' })
},
})
}
const capitalize = (s: string | null) => (s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : '')
const fullName = data
? [data.last_name, data.first_name, data.middle_name].filter(Boolean).map(capitalize).join(' ')
: ''
const isLegal = !!data && data.account_type !== 'individual'
const displayName = isLegal ? (data?.legal_entity?.name ?? '') : fullName
const userBalance =
isPortfolioLoading || !portfolio || portfolio.totalUsd == null
? '$—'
: `$${portfolio.totalUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
return (
<div className={styles.page}>
<WalletHeader />
{data && !data.kyc_verified && (
<div className={styles.kycBanner}>
Для полного доступа к платформе необходимо пройти верификацию личности (KYC)
</div>
)}
<KycBanner />
<main className={styles.main}>
<div className={styles.profileTop}>
<ProfileAvatar />
<div className={styles.userInfo}>
<span className={styles.userName}>{displayName}</span>
<span className={styles.userBalance}>{userBalance}</span>
{/* <span className={styles.userBalanceRub}>≈ 22 340,50 ₽</span> */}
</div>
</div>
<ProfileSummary />
<div className={styles.sections}>
{data && (isLegal ? (
<LegalEntityFields data={data} />
) : (
<IndividualFields
data={data}
fullName={fullName}
phone={phone}
onPhoneChange={handlePhoneChange}
onPhoneBlur={handlePhoneBlur}
/>
))}
<ProfileSection
title="Безопасность"
actions={
<>
<Button variant="danger"> Посмотреть приватный ключ</Button>
<Button variant="primary">СОХРАНИТЬ</Button>
</>
}
>
<div className={styles.grid1}>
{walletAddresses?.map(({ chain, address }) => (
<FormField
key={chain}
label={`Адрес ${chain}`}
readOnly
icon="lock"
value={address}
placeholder="—"
/>
))}
</div>
</ProfileSection>
<ProfileSection title="Мнемоника">
<div className={styles.mnemonicRow}>
<div className={styles.mnemonicInfo}>
<span className={styles.mnemonicIcon}>🔑</span>
<span className={styles.mnemonicText}>Сид-фраза из 12 слов для восстановления кошелька</span>
</div>
<Button variant="danger" onClick={() => navigate(ROUTES.SEED_PHRASE)}> Показать мнемонику</Button>
</div>
</ProfileSection>
<ProfileDetails />
<SecuritySection />
<MnemonicSection />
</div>
</main>
{notification && (
<Notification
status={notification.status}
message={notification.message}
onClose={() => setNotification(null)}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,240 @@
import type { LegalDocData } from '@entities/legal-doc'
export const publichnayaOfertaDoc: LegalDocData = {
title: 'ПУБЛИЧНЫЙ ДОГОВОР ОФЕРТЫ',
subtitle: 'ООО БИТФОРС',
sections: [
{
title: 'Агентский договор',
blocks: [
{
kind: 'text',
text: 'Настоящая оферта на заключение агентского договора (далее Оферта, Договор) является публичным предложением Общества с ограниченной ответственностью «БИТФОРС», заключить договор на условиях и в порядке, определенных настоящей Офертой.',
},
{
kind: 'text',
text: 'Акцепт оферты производится в соответствии с пунктом 2 статьи 437 Гражданского кодекса Российской Федерации и равносилен заключению агентского договора в письменной форме.',
},
],
},
{
title: 'Основные понятия и определения действующего договора',
blocks: [
{
kind: 'definitions',
items: [
{
term: 'Агент',
text: ' юридическое лицо или индивидуальный предприниматель, зарегистрированный на территории Российской Федерации, в установленном действующим законодательством порядке.',
},
{
term: 'Принципал',
text: ' сторона агентского договора, по поручению которой агент осуществляет юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала.',
},
{
term: 'Агентский договор',
text: ' соглашение, по которому агент обязуется за вознаграждение совершать по поручению принципала юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала в соответствии с п. 1 ст. 1005 Гражданского Кодекса Российской Федерации.',
},
{
term: 'Личный кабинета Агента',
text: ' ресурс, размещенный на сайте Принципала, предназначенный для взаимодействия Агента и Принципала.',
},
{
term: 'Отчетный период',
text: ' период для взаиморасчетов с Агентом, равный одному календарному кварталу с даты активации любой из услуг, предоставляемой Принципалу.',
},
{
term: 'Отчет о сумме начислений (Отчет)',
text: ' отчет, формируемый в Личном кабинете Агента на основании данных систем учета Принципала.',
},
{
term: 'Оферта (Договор)',
text: ' настоящий документ, который отражает предложение и намерение ООО «БИТФОРС» считать заключенным договор с лицом, которым будет принято предложение на условиях, изложенных ниже.',
},
],
},
],
},
{
title: '1. Акцепт оферты и заключение агентского договора',
blocks: [
{
kind: 'text',
text: 'Акцепт настоящей Оферты и заключение Агентского договора осуществляется Принципалом в процессе регистрации в Личном кабинете Принципала (на сайте Агента), при прочтении текста настоящей Оферты, путем проставления специальной отметки (галочки) напротив фразы «Я ознакомился с Офертой и принимаю ее условия» и нажатия кнопки «Подписать».',
},
{
kind: 'text',
text: 'Особый порядок принятия условий Оферты путем проставления специальной отметки (галочки) определяется интерфейсом Личного кабинета Принципала. Принципал не может зарегистрироваться в Личном кабинете и получить к нему доступ без подтверждения принятия условий Оферты.',
},
{
kind: 'text',
text: 'Принимая Оферту, Принципал подтверждает, что прочел и полностью согласен с документами, размещенными на сайте в разделе, предназначенном для Принципала, которые являются неотъемлемой частью настоящей Оферты (Договора) и обязательны для исполнения Сторонами.',
},
],
},
{
title: '2. Общие положения',
blocks: [
{
kind: 'text',
text: 'Публикуемые на сайте Агента документы (формы, требования, правила и т.п.), устанавливающие порядок и условия выполнения действий, предусмотренных настоящим Договором, являются неотъемлемой частью настоящего Договора и обязательны для исполнения Сторонами. Принципал обязан использовать формы документов, утвержденных Агентом, и не вправе вносить в них какие-либо изменения или дополнения.',
},
{
kind: 'text',
text: 'Агент обязуется уведомлять Принципала обо всех изменениях в документах, связанных с исполнением настоящего Договора, путем направления электронных сообщений (через Личный кабинет или на электронную почту Принципала) или размещением уведомлений об изменениях на сайте Агентов в разделе, предназначенном для размещения объявлений.',
},
],
},
{
title: '3. Предмет договора',
blocks: [
{
kind: 'text',
text: 'По настоящему Договору Принципал поручает, а Агент принимает на себя обязательство совершать от имени и за счет Принципала указанные действия, а Принципал обязуется выплатить Агенту вознаграждение за совершенные действия.',
},
{ kind: 'text', text: 'По настоящему Договору Агент совершает следующие действия:' },
{
kind: 'list',
items: [
'Консультирование Принципала об услугах Агента, включая, помимо прочего, порядок активации и оказания услуг, работу в Личном кабинете Принципала и иные дополнительные услуги, оказываемые Агентом;',
'Совершение сделок и иных юридических действий Агентом от своего имени, но за счёт Принципала.',
],
},
{ kind: 'text', text: 'Настоящий Договор действует на территории Российской Федерации и иного иностранного государства.' },
{
kind: 'text',
text: 'Права и обязанности по сделкам, совершенным Агентом во исполнение настоящего Договора, возникают непосредственно у Принципала.',
},
],
},
{
title: '4. Права и обязанности сторон',
blocks: [
{
kind: 'text',
text: 'Агент обязуется совершать действия, составляющие предмет настоящего Договора, в соответствии с законными интересами Принципала, сообщать Принципалу по его требованию все сведения о ходе исполнения настоящего Договора, передавать Принципалу в течение 7 рабочих дней имущество, полученное по сделкам.',
},
{
kind: 'text',
text: 'Агент несет ответственность за сохранность документов и персональных данных, переданных ему Принципалом для исполнения настоящего Договора.',
},
{
kind: 'text',
text: 'Принципал обязан без промедления принять отчет Агента, все предоставленные им документы, обеспечить Агента документами и материалами, необходимыми для выполнения настоящего Договора, возместить Агенту понесенные расходы и выплатить обусловленное Договором агентское вознаграждение.',
},
],
},
{
title: '5. Агентское вознаграждение и порядок оплаты',
blocks: [
{ kind: 'text', text: 'Сумма вознаграждения Агента по настоящему Договору составляет:' },
{
kind: 'list',
items: [
'8% от 5 000 до 30 000 рублей',
'6% от 30 000 до 100 000 рублей',
'4% от 100 000 до 600 000 рублей',
],
},
{
kind: 'text',
text: 'Вознаграждение выплачивается Агенту с момента подписания настоящего Договора об исполнении поручения Агентом от своего имени, но за счет Принципала.',
},
{
kind: 'text',
text: 'Принципал возмещает следующие расходы Агента в сумме не более 30 000 рублей на оплату банковских услуг и иных комиссий.',
},
],
},
{
title: '6. Ответственность сторон',
blocks: [
{
kind: 'text',
text: 'В случае нарушения Агентом сроков, установленных Договором для передачи Принципалу полученного имущества, Принципал вправе предъявить требование об уплате неустойки в размере 0,1% от непереданной суммы за каждый день просрочки.',
},
{
kind: 'text',
text: 'В случае нарушения Принципалом сроков уплаты вознаграждения или возмещения расходов, Агент вправе предъявить требование об уплате неустойки в размере 0,1% от не уплаченной в срок суммы за каждый день просрочки.',
},
],
},
{
title: '7. Форс-мажор',
blocks: [
{
kind: 'text',
text: 'Стороны освобождаются от ответственности за частичное или полное неисполнение обязательств по настоящему Договору, если это неисполнение явилось следствием возникших после заключения настоящего Договора обстоятельств непреодолимой силы.',
},
{
kind: 'text',
text: 'При наступлении форс-мажорных обстоятельств каждая Сторона должна без промедления известить о них в письменном виде другую Сторону с указанием характера обстоятельств и их влияния на исполнение обязательств.',
},
],
},
{
title: '8. Конфиденциальность',
blocks: [
{
kind: 'text',
text: 'Стороны принимают все необходимые меры для того, чтобы их сотрудники, агенты, правопреемники без предварительного согласия другой Стороны не информировали третьих лиц о конфиденциальной информации и персональных данных Сторон настоящего Договора.',
},
],
},
{
title: '9. Изменение и прекращение договора',
blocks: [
{
kind: 'text',
text: 'Настоящий договор вступает в силу с момента его подписания и действует до момента исполнения сторонами своих обязательств по настоящему договору.',
},
{
kind: 'text',
text: 'Настоящий Договор может быть изменен или прекращен по письменному соглашению Сторон, а также в других случаях, предусмотренных законодательством Российской Федерации.',
},
{
kind: 'text',
text: 'Принципал вправе в любое время отказаться от исполнения настоящего Договора путем направления письменного уведомления Агенту за 3 рабочих дня.',
},
],
},
{
title: '10. Заключительные положения',
blocks: [
{
kind: 'text',
text: 'Ни одна из сторон не вправе передавать свои права и обязанности по настоящему договору третьим лицам без согласия другой стороны.',
},
{
kind: 'text',
text: 'Сообщения Стороны могут направлять по факсу, электронной почте или другим способом связи при условии, что он позволяет достоверно установить, от кого исходило сообщение и кому оно адресовано.',
},
{
kind: 'text',
text: 'Споры, вытекающие из настоящего Договора, разрешаются в досудебном порядке. При неурегулировании возникших разногласий спор разрешается в Арбитражном суде г. Санкт–Петербурга и Ленинградской области с обязательным соблюдением претензионного порядка.',
},
],
},
{
title: 'Реквизиты сторон',
blocks: [
{
kind: 'infoBox',
lines: [
'Общество с ограниченной ответственностью «БИТФОРС»',
'196246, г. Санкт-Петербург, Московский р-н, Московское шоссе, д.25к1 литера в, помещ. 3-Н',
'ИНН / КПП: 9810001062 / 781001001',
'ОГРН: 1257800060990',
'ОКПО / ОКАТО / ОКТМО: 68342261 / 40284000000 / 40377000000',
'Руководитель: Кленин Михаил Васильевич',
'Электронная почта: company@bitforcefoundation.ru',
'Наименование банка: ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"',
'Корреспондентский счет: 30101810600000000786',
'БИК: 044030786',
'Расчетный счет: 40702810632250004861',
],
},
],
},
],
}

View File

@@ -1,89 +0,0 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.definitions {
display: flex;
flex-direction: column;
gap: 15px;
}
.definition {
padding: 15px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.6;
}
.requisites {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 4px;
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08));
line-height: 1.8;
}
.requisites p {
margin: 5px 0;
color: var(--text-primary, #ffffff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.sectionTitle {
font-size: 16px;
}
.requisites {
padding: 15px;
}
}

View File

@@ -1,199 +1,13 @@
import { LegalDoc } from '@entities/legal-doc'
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './PublichnayaOfertaPage.module.css'
import { publichnayaOfertaDoc } from '../model/content'
export function PublichnayaOfertaPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>ПУБЛИЧНЫЙ ДОГОВОР ОФЕРТЫ</h1>
<h2 className={styles.subtitle}>ООО БИТФОРС</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Агентский договор</h3>
<p>
Настоящая оферта на заключение агентского договора (далее Оферта, Договор) является публичным предложением Общества с ограниченной ответственностью «БИТФОРС», заключить договор на условиях и в порядке, определенных настоящей Офертой.
</p>
<p>
Акцепт оферты производится в соответствии с пунктом 2 статьи 437 Гражданского кодекса Российской Федерации и равносилен заключению агентского договора в письменной форме.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Основные понятия и определения действующего договора</h3>
<div className={styles.definitions}>
<div className={styles.definition}>
<strong>Агент</strong> юридическое лицо или индивидуальный предприниматель, зарегистрированный на территории Российской Федерации, в установленном действующим законодательством порядке.
</div>
<div className={styles.definition}>
<strong>Принципал</strong> сторона агентского договора, по поручению которой агент осуществляет юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала.
</div>
<div className={styles.definition}>
<strong>Агентский договор</strong> соглашение, по которому агент обязуется за вознаграждение совершать по поручению принципала юридические и иные действия от своего имени, но за счет принципала либо от имени и за счет принципала в соответствии с п. 1 ст. 1005 Гражданского Кодекса Российской Федерации.
</div>
<div className={styles.definition}>
<strong>Личный кабинета Агента</strong> ресурс, размещенный на сайте Принципала, предназначенный для взаимодействия Агента и Принципала.
</div>
<div className={styles.definition}>
<strong>Отчетный период</strong> период для взаиморасчетов с Агентом, равный одному календарному кварталу с даты активации любой из услуг, предоставляемой Принципалу.
</div>
<div className={styles.definition}>
<strong>Отчет о сумме начислений (Отчет)</strong> отчет, формируемый в Личном кабинете Агента на основании данных систем учета Принципала.
</div>
<div className={styles.definition}>
<strong>Оферта (Договор)</strong> настоящий документ, который отражает предложение и намерение ООО «БИТФОРС» считать заключенным договор с лицом, которым будет принято предложение на условиях, изложенных ниже.
</div>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Акцепт оферты и заключение агентского договора</h3>
<p>
Акцепт настоящей Оферты и заключение Агентского договора осуществляется Принципалом в процессе регистрации в Личном кабинете Принципала (на сайте Агента), при прочтении текста настоящей Оферты, путем проставления специальной отметки (галочки) напротив фразы «Я ознакомился с Офертой и принимаю ее условия» и нажатия кнопки «Подписать».
</p>
<p>
Особый порядок принятия условий Оферты путем проставления специальной отметки (галочки) определяется интерфейсом Личного кабинета Принципала. Принципал не может зарегистрироваться в Личном кабинете и получить к нему доступ без подтверждения принятия условий Оферты.
</p>
<p>
Принимая Оферту, Принципал подтверждает, что прочел и полностью согласен с документами, размещенными на сайте в разделе, предназначенном для Принципала, которые являются неотъемлемой частью настоящей Оферты (Договора) и обязательны для исполнения Сторонами.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Общие положения</h3>
<p>
Публикуемые на сайте Агента документы (формы, требования, правила и т.п.), устанавливающие порядок и условия выполнения действий, предусмотренных настоящим Договором, являются неотъемлемой частью настоящего Договора и обязательны для исполнения Сторонами. Принципал обязан использовать формы документов, утвержденных Агентом, и не вправе вносить в них какие-либо изменения или дополнения.
</p>
<p>
Агент обязуется уведомлять Принципала обо всех изменениях в документах, связанных с исполнением настоящего Договора, путем направления электронных сообщений (через Личный кабинет или на электронную почту Принципала) или размещением уведомлений об изменениях на сайте Агентов в разделе, предназначенном для размещения объявлений.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Предмет договора</h3>
<p>
По настоящему Договору Принципал поручает, а Агент принимает на себя обязательство совершать от имени и за счет Принципала указанные действия, а Принципал обязуется выплатить Агенту вознаграждение за совершенные действия.
</p>
<p>
По настоящему Договору Агент совершает следующие действия:
</p>
<ul>
<li>Консультирование Принципала об услугах Агента, включая, помимо прочего, порядок активации и оказания услуг, работу в Личном кабинете Принципала и иные дополнительные услуги, оказываемые Агентом;</li>
<li>Совершение сделок и иных юридических действий Агентом от своего имени, но за счёт Принципала.</li>
</ul>
<p>
Настоящий Договор действует на территории Российской Федерации и иного иностранного государства.
</p>
<p>
Права и обязанности по сделкам, совершенным Агентом во исполнение настоящего Договора, возникают непосредственно у Принципала.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Права и обязанности сторон</h3>
<p>
Агент обязуется совершать действия, составляющие предмет настоящего Договора, в соответствии с законными интересами Принципала, сообщать Принципалу по его требованию все сведения о ходе исполнения настоящего Договора, передавать Принципалу в течение 7 рабочих дней имущество, полученное по сделкам.
</p>
<p>
Агент несет ответственность за сохранность документов и персональных данных, переданных ему Принципалом для исполнения настоящего Договора.
</p>
<p>
Принципал обязан без промедления принять отчет Агента, все предоставленные им документы, обеспечить Агента документами и материалами, необходимыми для выполнения настоящего Договора, возместить Агенту понесенные расходы и выплатить обусловленное Договором агентское вознаграждение.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Агентское вознаграждение и порядок оплаты</h3>
<p>
Сумма вознаграждения Агента по настоящему Договору составляет:
</p>
<ul>
<li>8% от 5 000 до 30 000 рублей</li>
<li>6% от 30 000 до 100 000 рублей</li>
<li>4% от 100 000 до 600 000 рублей</li>
</ul>
<p>
Вознаграждение выплачивается Агенту с момента подписания настоящего Договора об исполнении поручения Агентом от своего имени, но за счет Принципала.
</p>
<p>
Принципал возмещает следующие расходы Агента в сумме не более 30 000 рублей на оплату банковских услуг и иных комиссий.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Ответственность сторон</h3>
<p>
В случае нарушения Агентом сроков, установленных Договором для передачи Принципалу полученного имущества, Принципал вправе предъявить требование об уплате неустойки в размере 0,1% от непереданной суммы за каждый день просрочки.
</p>
<p>
В случае нарушения Принципалом сроков уплаты вознаграждения или возмещения расходов, Агент вправе предъявить требование об уплате неустойки в размере 0,1% от не уплаченной в срок суммы за каждый день просрочки.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Форс-мажор</h3>
<p>
Стороны освобождаются от ответственности за частичное или полное неисполнение обязательств по настоящему Договору, если это неисполнение явилось следствием возникших после заключения настоящего Договора обстоятельств непреодолимой силы.
</p>
<p>
При наступлении форс-мажорных обстоятельств каждая Сторона должна без промедления известить о них в письменном виде другую Сторону с указанием характера обстоятельств и их влияния на исполнение обязательств.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Конфиденциальность</h3>
<p>
Стороны принимают все необходимые меры для того, чтобы их сотрудники, агенты, правопреемники без предварительного согласия другой Стороны не информировали третьих лиц о конфиденциальной информации и персональных данных Сторон настоящего Договора.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Изменение и прекращение договора</h3>
<p>
Настоящий договор вступает в силу с момента его подписания и действует до момента исполнения сторонами своих обязательств по настоящему договору.
</p>
<p>
Настоящий Договор может быть изменен или прекращен по письменному соглашению Сторон, а также в других случаях, предусмотренных законодательством Российской Федерации.
</p>
<p>
Принципал вправе в любое время отказаться от исполнения настоящего Договора путем направления письменного уведомления Агенту за 3 рабочих дня.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>10. Заключительные положения</h3>
<p>
Ни одна из сторон не вправе передавать свои права и обязанности по настоящему договору третьим лицам без согласия другой стороны.
</p>
<p>
Сообщения Стороны могут направлять по факсу, электронной почте или другим способом связи при условии, что он позволяет достоверно установить, от кого исходило сообщение и кому оно адресовано.
</p>
<p>
Споры, вытекающие из настоящего Договора, разрешаются в досудебном порядке. При неурегулировании возникших разногласий спор разрешается в Арбитражном суде г. СанктПетербурга и Ленинградской области с обязательным соблюдением претензионного порядка.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Реквизиты сторон</h3>
<div className={styles.requisites}>
<p>Общество с ограниченной ответственностью «БИТФОРС»</p>
<p>196246, г. Санкт-Петербург, Московский р-н, Московское шоссе, д.25к1 литера в, помещ. 3-Н</p>
<p>ИНН / КПП: 9810001062 / 781001001</p>
<p>ОГРН: 1257800060990</p>
<p>ОКПО / ОКАТО / ОКТМО: 68342261 / 40284000000 / 40377000000</p>
<p>Руководитель: Кленин Михаил Васильевич</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Наименование банка: ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"</p>
<p>Корреспондентский счет: 30101810600000000786</p>
<p>БИК: 044030786</p>
<p>Расчетный счет: 40702810632250004861</p>
</div>
</section>
</div>
</main>
<LegalDoc doc={publichnayaOfertaDoc} />
<Footer />
</>
)

View File

@@ -0,0 +1,60 @@
import type { LegalDocData } from '@entities/legal-doc'
export const reestrDoc: LegalDocData = {
title: 'Реестр операторов персональных данных',
subtitle: 'ООО «БИТФОРС»',
sections: [
{
blocks: [
{
kind: 'text',
text: 'Информация об операторе персональных данных размещена в реестре операторов персональных данных Федеральной службы по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор).',
},
{
kind: 'text',
text: 'Вы можете просмотреть информацию об операторе в реестре Роскомнадзора, перейдя по ссылке ниже:',
},
{
kind: 'link',
href: 'https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn=',
text: 'Открыть реестр Роскомнадзора',
},
{
kind: 'text',
text: 'Реестр содержит информацию об операторах персональных данных, включая сведения о целях и методах обработки персональных данных, а также меры по обеспечению безопасности персональных данных.',
},
],
},
{
title: 'Информация об операторе',
blocks: [
{
kind: 'infoBox',
lines: [
{ strong: 'Наименование:', text: 'ООО «БИТФОРС»' },
{ strong: 'ИНН:', text: '9810001062' },
{ strong: 'ОГРН:', text: '1257800060990' },
{
strong: 'Юридический адрес:',
text: '196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н',
},
{ strong: 'Контактная информация:', text: 'company@bitforcefoundation.ru' },
],
},
],
},
{
title: 'О Роскомнадзоре',
blocks: [
{
kind: 'text',
text: 'Федеральная служба по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор) — это федеральный орган исполнительной власти, осуществляющий функции по контролю и надзору в области персональных данных.',
},
{
kind: 'text',
text: 'Роскомнадзор ведет реестр операторов персональных данных в соответствии с требованиями Федерального закона «О персональных данных». Реестр является открытой информационной системой и доступен всем заинтересованным лицам.',
},
],
},
],
}

View File

@@ -1,115 +0,0 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.description {
font-size: 16px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
margin-bottom: 20px;
}
.info {
font-size: 16px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
margin: 15px 0;
}
.linkBlock {
text-align: center;
padding: 30px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-radius: 8px;
margin: 30px 0;
}
.button {
display: inline-block;
padding: 15px 40px;
background: var(--interactive, #4a6dff);
color: #fff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
border: 2px solid var(--interactive, #4a6dff);
}
.button:hover {
background: transparent;
color: var(--interactive, #4a6dff);
}
.operatorInfo {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.8;
}
.operatorInfo p {
margin: 10px 0;
color: var(--text-primary, #ffffff);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.linkBlock {
padding: 20px;
}
.button {
padding: 12px 30px;
font-size: 14px;
}
}

View File

@@ -1,73 +1,13 @@
import { LegalDoc } from '@entities/legal-doc'
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './ReestryPage.module.css'
import { reestrDoc } from '../model/content'
export function ReestryPage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>Реестр операторов персональных данных</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<p className={styles.description}>
Информация об операторе персональных данных размещена в реестре операторов персональных данных Федеральной службы по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор).
</p>
<p className={styles.info}>
Вы можете просмотреть информацию об операторе в реестре Роскомнадзора, перейдя по ссылке ниже:
</p>
<div className={styles.linkBlock}>
<a
href="https://pd.rkn.gov.ru/operators-registry/operators-list/?act=search&name_full=%D0%91%D0%B8%D1%82%D1%84%D0%BE%D1%80%D1%81&inn=9810001062&regn="
target="_blank"
rel="noopener noreferrer"
className={styles.button}
>
Открыть реестр Роскомнадзора
</a>
</div>
<p className={styles.info}>
Реестр содержит информацию об операторах персональных данных, включая сведения о целях и методах обработки персональных данных, а также меры по обеспечению безопасности персональных данных.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Информация об операторе</h3>
<div className={styles.operatorInfo}>
<p>
<strong>Наименование:</strong> ООО «БИТФОРС»
</p>
<p>
<strong>ИНН:</strong> 9810001062
</p>
<p>
<strong>ОГРН:</strong> 1257800060990
</p>
<p>
<strong>Юридический адрес:</strong> 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н
</p>
<p>
<strong>Контактная информация:</strong> company@bitforcefoundation.ru
</p>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>О Роскомнадзоре</h3>
<p>
Федеральная служба по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор) это федеральный орган исполнительной власти, осуществляющий функции по контролю и надзору в области персональных данных.
</p>
<p>
Роскомнадзор ведет реестр операторов персональных данных в соответствии с требованиями Федерального закона «О персональных данных». Реестр является открытой информационной системой и доступен всем заинтересованным лицам.
</p>
</section>
</div>
</main>
<LegalDoc doc={reestrDoc} />
<Footer />
</>
)

View File

@@ -0,0 +1,346 @@
import type { LegalDocData } from '@entities/legal-doc'
export const soglasieDoc: LegalDocData = {
title: 'СОГЛАСИЕ НА ОБРАБОТКУ ПЕРСОНАЛЬНЫХ ДАННЫХ',
subtitle: 'ООО «БИТФОРС»',
sections: [
{
title: 'Преамбула',
blocks: [
{
kind: 'text',
text: 'Я, субъект персональных данных, действуя своей волей и в своем интересе, в соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных», предоставляю ООО «БИТФОРС» согласие на обработку моих персональных данных на условиях и для целей, определенных настоящим Согласием.',
},
],
},
{
title: '1. Сведения об операторе',
blocks: [
{
kind: 'infoBox',
lines: [
'Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»',
'ИНН: 9810001062',
'ОГРН: 1257800060990',
'Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н',
'Электронная почта: company@bitforcefoundation.ru',
'Веб-сайт: https://bitforce-foundation.ru',
],
},
],
},
{
title: '2. Правовые основания обработки',
blocks: [
{
kind: 'text',
text: 'Настоящее согласие предоставляется на основании пункта 1 части 1 статьи 6 Федерального закона «О персональных данных» и является правовым основанием для обработки персональных данных Оператором.',
},
{ kind: 'text', text: 'Согласие дается добровольно, своей волей и в своих интересах.' },
{
kind: 'text',
text: 'Субъект персональных данных понимает последствия предоставления согласия, включая возможные риски, связанные с обработкой персональных данных.',
},
],
},
{
title: '3. Цели обработки персональных данных',
blocks: [
{ kind: 'subheading', text: '3.1. Основные цели:' },
{
kind: 'list',
items: [
'Регистрация и ведение учетной записи на веб-сайте и в мобильном приложении',
'Идентификация и верификация личности в соответствии с требованиями законодательства',
'Предоставление услуг по обмену криптовалют и электронных денежных средств',
'Проведение финансовых операций, переводов и расчетов',
'Ведение учета и истории операций',
],
},
{ kind: 'subheading', text: '3.2. Дополнительные цели:' },
{
kind: 'list',
items: [
'Обеспечение безопасности операций и предотвращение мошенничества',
'Выполнение требований по противодействию легализации доходов',
'Соблюдение требований валютного, налогового и иного применимого законодательства',
'Предоставление технической поддержки и клиентского сервиса',
'Рассылка уведомлений о состоянии операций и изменениях в условиях',
],
},
{ kind: 'subheading', text: '3.3. Маркетинговые цели (при дополнительном согласии):' },
{
kind: 'list',
items: [
'Направление информационных и рекламных материалов',
'Проведение маркетинговых исследований и опросов',
'Персонализация предложений и услуг',
'Анализ предпочтений и поведения для улучшения сервисов',
],
},
{ kind: 'subheading', text: '3.4. Аналитические цели:' },
{
kind: 'list',
items: [
'Анализ использования веб-сайта и мобильного приложения',
'Улучшение качества предоставляемых услуг',
'Разработка новых продуктов и сервисов',
'Создание статистических отчетов в обезличенном виде',
],
},
],
},
{
title: '4. Перечень персональных данных',
blocks: [
{ kind: 'subheading', text: '4.1. Идентификационные данные:' },
{ kind: 'list', items: ['Фамилия, имя, отчество', 'Дата рождения', 'Гражданство'] },
{ kind: 'subheading', text: '4.2. Документы, удостоверяющие личность:' },
{
kind: 'list',
items: [
'Серия и номер паспорта гражданина Российской Федерации',
'Дата выдачи и код подразделения',
'Адрес регистрации по месту жительства',
'Цифровые копии (сканы) документов',
],
},
{ kind: 'subheading', text: '4.3. Контактная информация:' },
{
kind: 'list',
items: [
'Номера телефонов (мобильный, домашний, рабочий)',
'Адреса электронной почты',
'Почтовые адреса (фактического проживания, для корреспонденции)',
],
},
{ kind: 'subheading', text: '4.4. Финансовая информация:' },
{
kind: 'list',
items: [
'Номера банковских счетов и реквизиты банковских карт',
'Реквизиты криптовалютных кошельков и адресов',
'Информация о доходах и источниках происхождения денежных средств',
'История финансовых операций и транзакций',
],
},
{ kind: 'subheading', text: '4.5. Техническая информация:' },
{
kind: 'list',
items: [
'IP-адреса устройств, с которых осуществляется доступ к сервисам',
'Информация о браузере, операционной системе и устройстве',
'Файлы cookie и данные локального хранилища',
'Логи действий и история использования сервисов',
],
},
{ kind: 'subheading', text: '4.6. Дополнительная информация:' },
{
kind: 'list',
items: [
'Фотографии для процедур верификации',
'Видеозаписи процедур видеоидентификации',
'Биометрические данные (при использовании соответствующих технологий)',
'Информация о семейном положении и профессиональной деятельности',
],
},
],
},
{
title: '5. Перечень действий с персональными данными',
blocks: [
{
kind: 'text',
text: 'Согласие распространяется на следующие действия (операции) с персональными данными:',
},
{
kind: 'list',
items: [
'Сбор, запись и первичная обработка персональных данных',
'Накопление и систематизация в базах данных',
'Создание резервных копий и архивирование',
'Извлечение, использование и анализ данных',
'Уточнение, обновление и актуализация информации',
'Передача данных третьим лицам',
'Обезличивание и удаление данных',
'Автоматизированная обработка и профилирование',
],
},
],
},
{
title: '6. Лица, которым могут быть переданы персональные данные',
blocks: [
{ kind: 'subheading', text: '6.1. Сотрудники Оператора:' },
{
kind: 'list',
items: [
'Уполномоченные сотрудники, непосредственно участвующие в обработке',
'Сотрудники службы безопасности и комплаенса',
'Сотрудники технической поддержки',
'Руководящий состав в рамках их полномочий',
],
},
{ kind: 'subheading', text: '6.2. Государственные и муниципальные органы:' },
{
kind: 'list',
items: [
'Федеральная служба по финансовому мониторингу',
'Федеральная налоговая служба',
'Правоохранительные органы (при наличии законных требований)',
'Суды и органы исполнения судебных решений',
],
},
{ kind: 'subheading', text: '6.3. Партнеры и контрагенты:' },
{
kind: 'list',
items: [
'Банки и платежные системы',
'Операторы электронных денежных средств',
'Поставщики технологических решений',
'Аудиторские и консалтинговые организации',
],
},
],
},
{
title: '7. Сроки обработки персональных данных',
blocks: [
{ kind: 'subheading', text: '7.1. Общие принципы:' },
{
kind: 'text',
text: 'Персональные данные обрабатываются в течение времени, необходимого для достижения целей обработки. После достижения целей данные подлежат уничтожению или обезличиванию.',
},
{ kind: 'subheading', text: '7.2. Конкретные сроки обработки:' },
{
kind: 'list',
items: [
{
strong: 'Данные активных клиентов:',
text: 'в течение всего периода отношений плюс 5 лет после прекращения',
},
{ strong: 'Данные для идентификации:', text: '5 лет с момента прекращения отношений' },
{ strong: 'Финансовая информация:', text: '5 лет с даты совершения операции' },
{ strong: 'Маркетинговые данные:', text: 'до отзыва согласия, но не более 3 лет' },
{ strong: 'Техническая информация:', text: '1 год для безопасности, 6 месяцев для логов' },
],
},
],
},
{
title: '8. Права субъекта персональных данных',
blocks: [
{ kind: 'subheading', text: '8.1. Право на информацию:' },
{
kind: 'list',
items: [
'Получение подтверждения факта обработки персональных данных',
'Получение информации о целях и способах обработки',
'Информация о сроках обработки и составе данных',
'Сведения о лицах, которым передаются данные',
],
},
{ kind: 'subheading', text: '8.2. Право на доступ:' },
{
kind: 'list',
items: [
'Получение копий обрабатываемых персональных данных',
'Ознакомление с историей обработки и изменений',
'Получение информации об источниках персональных данных',
],
},
{ kind: 'subheading', text: '8.3. Право на исправление и удаление:' },
{
kind: 'list',
items: [
'Требование исправления неточных или неполных данных',
'Требование удаления персональных данных при наличии оснований',
'Удаление данных после отзыва согласия',
],
},
{ kind: 'subheading', text: '8.4. Право на отзыв согласия:' },
{
kind: 'list',
items: [
'Согласие может быть отозвано в любое время',
'Отзыв оформляется в письменной форме',
'После отзыва обработка прекращается в разумные сроки',
],
},
{ kind: 'subheading', text: '8.5. Право на обжалование:' },
{
kind: 'list',
items: [
'Обращение к Оператору с жалобами на действия по обработке данных',
'Обращение в Роскомнадзор или его территориальные органы',
'Обращение в суд для защиты нарушенных прав',
],
},
{ kind: 'subheading', text: '8.6. Порядок реализации прав:' },
{
kind: 'list',
items: [
'Обращения направляются на адрес: company@bitforcefoundation.ru',
'Обращения рассматриваются в течение 30 дней',
'При необходимости срок может быть продлен на 30 дней',
],
},
],
},
{
title: '9. Заключительные положения',
blocks: [
{ kind: 'subheading', text: '9.1. Действие согласия:' },
{
kind: 'list',
items: [
'Согласие действует с момента его предоставления',
'Согласие действует до его отзыва или до достижения целей обработки',
'При существенных изменениях целей требуется новое согласие',
],
},
{ kind: 'subheading', text: '9.2. Форма предоставления согласия:' },
{
kind: 'list',
items: [
'Согласие может быть предоставлено в письменной форме',
'Согласие может быть предоставлено в электронной форме',
'Согласие может выражаться путем совершения конклюдентных действий',
],
},
{ kind: 'subheading', text: '9.3. Последствия непредоставления согласия:' },
{
kind: 'list',
items: [
'Отказ в предоставлении согласия может повлечь невозможность регистрации',
'Отказ может ограничить доступ к отдельным услугам',
'Отказ в согласии на маркетинг не влияет на основные услуги',
'Субъект вправе предоставить частичное согласие',
],
},
{ kind: 'subheading', text: '9.4. Контактная информация:' },
{
kind: 'infoBox',
lines: [
'Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н',
'Электронная почта: company@bitforcefoundation.ru',
'Ответственное лицо: Кленин Михаил Васильевич',
'Официальный сайт: https://bitforce-foundation.ru',
],
},
{ kind: 'subheading', text: '9.5. Подтверждение понимания:' },
{ kind: 'text', variant: 'emphasis', text: 'Предоставляя настоящее согласие, я подтверждаю, что:' },
{
kind: 'list',
items: [
'Ознакомлен с содержанием согласия и понимаю его значение',
'Понимаю цели и способы обработки моих персональных данных',
'Знаю о своих правах и способах их реализации',
'Согласие предоставляется добровольно и осознанно',
'Имею возможность отозвать согласие в любое время',
],
},
],
},
],
}

View File

@@ -1,127 +0,0 @@
.main {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
}
.container {
background: var(--bg-mid, #1b1547);
padding: 40px;
border-radius: 8px;
}
.title {
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
color: var(--text-primary, #ffffff);
}
.subtitle {
font-size: 22px;
font-weight: 600;
margin-bottom: 30px;
text-align: center;
color: var(--text-secondary, #b5b0cc);
}
.section {
margin-bottom: 40px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
color: var(--text-primary, #ffffff);
border-bottom: 2px solid var(--interactive, #4a6dff);
padding-bottom: 10px;
}
.subSectionTitle {
font-size: 16px;
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: var(--text-primary, #ffffff);
}
.list {
list-style: disc;
margin-left: 20px;
line-height: 1.8;
color: var(--text-primary, #ffffff);
}
.list li {
margin-bottom: 8px;
}
.list strong {
color: var(--text-primary, #ffffff);
}
.info {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
margin: 15px 0;
line-height: 1.8;
}
.info p {
margin: 8px 0;
color: var(--text-primary, #ffffff);
}
.contacts {
padding: 20px;
background: var(--glass-bg, rgba(255, 255, 255, 0.04));
border-left: 4px solid var(--interactive, #4a6dff);
border-radius: 4px;
line-height: 1.8;
}
.contacts p {
margin: 8px 0;
color: var(--text-primary, #ffffff);
}
.confirmation {
padding: 10px 0;
color: var(--text-primary, #ffffff);
font-weight: 500;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.title {
font-size: 22px;
}
.subtitle {
font-size: 18px;
}
.sectionTitle {
font-size: 16px;
}
.subSectionTitle {
font-size: 14px;
}
.list {
margin-left: 15px;
}
.info,
.contacts {
padding: 15px;
}
}

View File

@@ -1,299 +1,13 @@
import { LegalDoc } from '@entities/legal-doc'
import { Footer } from '@widgets/footer'
import { Header } from '@widgets/header'
import styles from './SoglasiePage.module.css'
import { soglasieDoc } from '../model/content'
export function SoglasiePage() {
return (
<>
<Header />
<main className={styles.main}>
<div className={styles.container}>
<h1 className={styles.title}>СОГЛАСИЕ НА ОБРАБОТКУ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<h2 className={styles.subtitle}>ООО «БИТФОРС»</h2>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Преамбула</h3>
<p>
Я, субъект персональных данных, действуя своей волей и в своем интересе, в соответствии с требованиями Федерального закона от 27.07.2006 152-ФЗ «О персональных данных», предоставляю ООО «БИТФОРС» согласие на обработку моих персональных данных на условиях и для целей, определенных настоящим Согласием.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>1. Сведения об операторе</h3>
<div className={styles.info}>
<p>Полное наименование: Общество с ограниченной ответственностью «БИТФОРС»</p>
<p>ИНН: 9810001062</p>
<p>ОГРН: 1257800060990</p>
<p>Юридический адрес: 196246, город Санкт-Петербург, Московское шоссе, дом 25, корпус 1, литера В, помещение 3-н</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Веб-сайт: https://bitforce-foundation.ru</p>
</div>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>2. Правовые основания обработки</h3>
<p>
Настоящее согласие предоставляется на основании пункта 1 части 1 статьи 6 Федерального закона «О персональных данных» и является правовым основанием для обработки персональных данных Оператором.
</p>
<p>Согласие дается добровольно, своей волей и в своих интересах.</p>
<p>
Субъект персональных данных понимает последствия предоставления согласия, включая возможные риски, связанные с обработкой персональных данных.
</p>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>3. Цели обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>3.1. Основные цели:</h4>
<ul className={styles.list}>
<li>Регистрация и ведение учетной записи на веб-сайте и в мобильном приложении</li>
<li>Идентификация и верификация личности в соответствии с требованиями законодательства</li>
<li>Предоставление услуг по обмену криптовалют и электронных денежных средств</li>
<li>Проведение финансовых операций, переводов и расчетов</li>
<li>Ведение учета и истории операций</li>
</ul>
<h4 className={styles.subSectionTitle}>3.2. Дополнительные цели:</h4>
<ul className={styles.list}>
<li>Обеспечение безопасности операций и предотвращение мошенничества</li>
<li>Выполнение требований по противодействию легализации доходов</li>
<li>Соблюдение требований валютного, налогового и иного применимого законодательства</li>
<li>Предоставление технической поддержки и клиентского сервиса</li>
<li>Рассылка уведомлений о состоянии операций и изменениях в условиях</li>
</ul>
<h4 className={styles.subSectionTitle}>3.3. Маркетинговые цели (при дополнительном согласии):</h4>
<ul className={styles.list}>
<li>Направление информационных и рекламных материалов</li>
<li>Проведение маркетинговых исследований и опросов</li>
<li>Персонализация предложений и услуг</li>
<li>Анализ предпочтений и поведения для улучшения сервисов</li>
</ul>
<h4 className={styles.subSectionTitle}>3.4. Аналитические цели:</h4>
<ul className={styles.list}>
<li>Анализ использования веб-сайта и мобильного приложения</li>
<li>Улучшение качества предоставляемых услуг</li>
<li>Разработка новых продуктов и сервисов</li>
<li>Создание статистических отчетов в обезличенном виде</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>4. Перечень персональных данных</h3>
<h4 className={styles.subSectionTitle}>4.1. Идентификационные данные:</h4>
<ul className={styles.list}>
<li>Фамилия, имя, отчество</li>
<li>Дата рождения</li>
<li>Гражданство</li>
</ul>
<h4 className={styles.subSectionTitle}>4.2. Документы, удостоверяющие личность:</h4>
<ul className={styles.list}>
<li>Серия и номер паспорта гражданина Российской Федерации</li>
<li>Дата выдачи и код подразделения</li>
<li>Адрес регистрации по месту жительства</li>
<li>Цифровые копии (сканы) документов</li>
</ul>
<h4 className={styles.subSectionTitle}>4.3. Контактная информация:</h4>
<ul className={styles.list}>
<li>Номера телефонов (мобильный, домашний, рабочий)</li>
<li>Адреса электронной почты</li>
<li>Почтовые адреса (фактического проживания, для корреспонденции)</li>
</ul>
<h4 className={styles.subSectionTitle}>4.4. Финансовая информация:</h4>
<ul className={styles.list}>
<li>Номера банковских счетов и реквизиты банковских карт</li>
<li>Реквизиты криптовалютных кошельков и адресов</li>
<li>Информация о доходах и источниках происхождения денежных средств</li>
<li>История финансовых операций и транзакций</li>
</ul>
<h4 className={styles.subSectionTitle}>4.5. Техническая информация:</h4>
<ul className={styles.list}>
<li>IP-адреса устройств, с которых осуществляется доступ к сервисам</li>
<li>Информация о браузере, операционной системе и устройстве</li>
<li>Файлы cookie и данные локального хранилища</li>
<li>Логи действий и история использования сервисов</li>
</ul>
<h4 className={styles.subSectionTitle}>4.6. Дополнительная информация:</h4>
<ul className={styles.list}>
<li>Фотографии для процедур верификации</li>
<li>Видеозаписи процедур видеоидентификации</li>
<li>Биометрические данные (при использовании соответствующих технологий)</li>
<li>Информация о семейном положении и профессиональной деятельности</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>5. Перечень действий с персональными данными</h3>
<p>Согласие распространяется на следующие действия (операции) с персональными данными:</p>
<ul className={styles.list}>
<li>Сбор, запись и первичная обработка персональных данных</li>
<li>Накопление и систематизация в базах данных</li>
<li>Создание резервных копий и архивирование</li>
<li>Извлечение, использование и анализ данных</li>
<li>Уточнение, обновление и актуализация информации</li>
<li>Передача данных третьим лицам</li>
<li>Обезличивание и удаление данных</li>
<li>Автоматизированная обработка и профилирование</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>6. Лица, которым могут быть переданы персональные данные</h3>
<h4 className={styles.subSectionTitle}>6.1. Сотрудники Оператора:</h4>
<ul className={styles.list}>
<li>Уполномоченные сотрудники, непосредственно участвующие в обработке</li>
<li>Сотрудники службы безопасности и комплаенса</li>
<li>Сотрудники технической поддержки</li>
<li>Руководящий состав в рамках их полномочий</li>
</ul>
<h4 className={styles.subSectionTitle}>6.2. Государственные и муниципальные органы:</h4>
<ul className={styles.list}>
<li>Федеральная служба по финансовому мониторингу</li>
<li>Федеральная налоговая служба</li>
<li>Правоохранительные органы (при наличии законных требований)</li>
<li>Суды и органы исполнения судебных решений</li>
</ul>
<h4 className={styles.subSectionTitle}>6.3. Партнеры и контрагенты:</h4>
<ul className={styles.list}>
<li>Банки и платежные системы</li>
<li>Операторы электронных денежных средств</li>
<li>Поставщики технологических решений</li>
<li>Аудиторские и консалтинговые организации</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>7. Сроки обработки персональных данных</h3>
<h4 className={styles.subSectionTitle}>7.1. Общие принципы:</h4>
<p>
Персональные данные обрабатываются в течение времени, необходимого для достижения целей обработки. После достижения целей данные подлежат уничтожению или обезличиванию.
</p>
<h4 className={styles.subSectionTitle}>7.2. Конкретные сроки обработки:</h4>
<ul className={styles.list}>
<li>
<strong>Данные активных клиентов:</strong> в течение всего периода отношений плюс 5 лет после прекращения
</li>
<li>
<strong>Данные для идентификации:</strong> 5 лет с момента прекращения отношений
</li>
<li>
<strong>Финансовая информация:</strong> 5 лет с даты совершения операции
</li>
<li>
<strong>Маркетинговые данные:</strong> до отзыва согласия, но не более 3 лет
</li>
<li>
<strong>Техническая информация:</strong> 1 год для безопасности, 6 месяцев для логов
</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>8. Права субъекта персональных данных</h3>
<h4 className={styles.subSectionTitle}>8.1. Право на информацию:</h4>
<ul className={styles.list}>
<li>Получение подтверждения факта обработки персональных данных</li>
<li>Получение информации о целях и способах обработки</li>
<li>Информация о сроках обработки и составе данных</li>
<li>Сведения о лицах, которым передаются данные</li>
</ul>
<h4 className={styles.subSectionTitle}>8.2. Право на доступ:</h4>
<ul className={styles.list}>
<li>Получение копий обрабатываемых персональных данных</li>
<li>Ознакомление с историей обработки и изменений</li>
<li>Получение информации об источниках персональных данных</li>
</ul>
<h4 className={styles.subSectionTitle}>8.3. Право на исправление и удаление:</h4>
<ul className={styles.list}>
<li>Требование исправления неточных или неполных данных</li>
<li>Требование удаления персональных данных при наличии оснований</li>
<li>Удаление данных после отзыва согласия</li>
</ul>
<h4 className={styles.subSectionTitle}>8.4. Право на отзыв согласия:</h4>
<ul className={styles.list}>
<li>Согласие может быть отозвано в любое время</li>
<li>Отзыв оформляется в письменной форме</li>
<li>После отзыва обработка прекращается в разумные сроки</li>
</ul>
<h4 className={styles.subSectionTitle}>8.5. Право на обжалование:</h4>
<ul className={styles.list}>
<li>Обращение к Оператору с жалобами на действия по обработке данных</li>
<li>Обращение в Роскомнадзор или его территориальные органы</li>
<li>Обращение в суд для защиты нарушенных прав</li>
</ul>
<h4 className={styles.subSectionTitle}>8.6. Порядок реализации прав:</h4>
<ul className={styles.list}>
<li>Обращения направляются на адрес: company@bitforcefoundation.ru</li>
<li>Обращения рассматриваются в течение 30 дней</li>
<li>При необходимости срок может быть продлен на 30 дней</li>
</ul>
</section>
<section className={styles.section}>
<h3 className={styles.sectionTitle}>9. Заключительные положения</h3>
<h4 className={styles.subSectionTitle}>9.1. Действие согласия:</h4>
<ul className={styles.list}>
<li>Согласие действует с момента его предоставления</li>
<li>Согласие действует до его отзыва или до достижения целей обработки</li>
<li>При существенных изменениях целей требуется новое согласие</li>
</ul>
<h4 className={styles.subSectionTitle}>9.2. Форма предоставления согласия:</h4>
<ul className={styles.list}>
<li>Согласие может быть предоставлено в письменной форме</li>
<li>Согласие может быть предоставлено в электронной форме</li>
<li>Согласие может выражаться путем совершения конклюдентных действий</li>
</ul>
<h4 className={styles.subSectionTitle}>9.3. Последствия непредоставления согласия:</h4>
<ul className={styles.list}>
<li>Отказ в предоставлении согласия может повлечь невозможность регистрации</li>
<li>Отказ может ограничить доступ к отдельным услугам</li>
<li>Отказ в согласии на маркетинг не влияет на основные услуги</li>
<li>Субъект вправе предоставить частичное согласие</li>
</ul>
<h4 className={styles.subSectionTitle}>9.4. Контактная информация:</h4>
<div className={styles.contacts}>
<p>Почтовый адрес: 196246, г. Санкт-Петербург, Московское ш., д. 25, к. 1, лит. В, пом. 3-н</p>
<p>Электронная почта: company@bitforcefoundation.ru</p>
<p>Ответственное лицо: Кленин Михаил Васильевич</p>
<p>Официальный сайт: https://bitforce-foundation.ru</p>
</div>
<h4 className={styles.subSectionTitle}>9.5. Подтверждение понимания:</h4>
<p className={styles.confirmation}>
Предоставляя настоящее согласие, я подтверждаю, что:
</p>
<ul className={styles.list}>
<li>Ознакомлен с содержанием согласия и понимаю его значение</li>
<li>Понимаю цели и способы обработки моих персональных данных</li>
<li>Знаю о своих правах и способах их реализации</li>
<li>Согласие предоставляется добровольно и осознанно</li>
<li>Имею возможность отозвать согласие в любое время</li>
</ul>
</section>
</div>
</main>
<LegalDoc doc={soglasieDoc} />
<Footer />
</>
)

View File

@@ -0,0 +1,6 @@
// Formats a numeric USD value as "$1,234.56". Returns "$—" when the value is
// missing (loading / unavailable), so callers don't repeat the fallback.
export function formatUsd(value: number | null | undefined): string {
if (value == null) return '$—'
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}

View File

@@ -0,0 +1 @@
export { LegalConverterSection } from './ui/LegalConverterForm'

View File

@@ -0,0 +1,103 @@
import { useEffect, useState, type FormEvent } from 'react'
import {
useCreatePurchaseRequest,
useMyPurchaseRequests,
TERM_OPTIONS,
MIN_ORDER,
formatRub,
calcQuote,
buildPurchaseComment,
parseContactFromComment,
latestPurchaseRequest,
} from '@features/b2b'
// Вся логика формы B2B-заявки: стейт полей, расчёт комиссии (через calcQuote),
// упаковка comment и отправка. По образцу widgets/converter-page/model.
export function useLegalConverterForm() {
const [amount, setAmount] = useState('')
const [name, setName] = useState('')
const [contact, setContact] = useState('')
const [days, setDays] = useState<number>(TERM_OPTIONS[0].days)
const [notice, setNotice] = useState<'success' | 'error' | null>(null)
const { mutate: submitRequest, isPending } = useCreatePurchaseRequest()
const { data: requests } = useMyPurchaseRequests()
// Автозаполнение имени и контакта из последней заявки аккаунта (один раз).
const [prefilled, setPrefilled] = useState(false)
useEffect(() => {
if (prefilled || !requests) return
const last = latestPurchaseRequest(requests.items)
if (!last) return
const parsed = parseContactFromComment(last.comment)
if (parsed.name) setName(parsed.name)
if (parsed.contact) setContact(parsed.contact)
setPrefilled(true)
}, [prefilled, requests])
const numAmount = Number(amount.replace(/\D/g, '')) || 0
const belowMin = numAmount > 0 && numAmount < MIN_ORDER
const { rate, commission, total } = calcQuote(numAmount, days)
const onAmountChange = (value: string) => {
const digits = value.replace(/\D/g, '')
setAmount(digits ? formatRub(Number(digits)) : '')
}
const canSubmit = numAmount > 0 && !belowMin && !isPending
const onSubmit = (e: FormEvent) => {
e.preventDefault()
if (!canSubmit) return
const comment = buildPurchaseComment({
name,
contact,
days,
amount: numAmount,
commission,
total,
})
submitRequest(
{ usdt_amount: numAmount, comment },
{
onSuccess: () => {
setNotice('success')
setAmount('')
setName('')
setContact('')
setDays(TERM_OPTIONS[0].days)
},
onError: () => setNotice('error'),
},
)
}
return {
// значения полей
amount,
name,
contact,
days,
// сеттеры
setName,
setContact,
setDays,
onAmountChange,
// деривации для панели условий
numAmount,
belowMin,
rate,
commission,
total,
// отправка
onSubmit,
isPending,
canSubmit,
// нотификация
notice,
dismissNotice: () => setNotice(null),
}
}

View File

@@ -0,0 +1,136 @@
import { FormField, Notification, Select } from '@shared/ui'
import { TERM_OPTIONS, MIN_ORDER, formatRub, dayLabel } from '@features/b2b'
import { useLegalConverterForm } from '../model/useLegalConverterForm'
import styles from './LegalConverterForm.module.css'
// Форма B2B-заявки для юрлиц (legal-аккаунты). Близнец ConverterSection из
// widgets/converter-page для физлиц; рендерится из ConverterPage по account_type.
// Вся логика — в useLegalConverterForm, здесь только разметка.
export function LegalConverterSection() {
const {
amount,
name,
contact,
days,
setName,
setContact,
setDays,
onAmountChange,
numAmount,
belowMin,
rate,
commission,
total,
onSubmit,
isPending,
canSubmit,
notice,
dismissNotice,
} = useLegalConverterForm()
return (
<form className={styles.wrap} onSubmit={onSubmit}>
<div className={styles.header}>
<h1 className={styles.title}>Оставить заявку</h1>
<p className={styles.subtitle}>
Конвертация крупных объёмов по индивидуальному курсу. Оставьте заявку
менеджер свяжется с вами, подтвердит актуальный курс и сопроводит сделку.
</p>
</div>
<div className={styles.body}>
<div className={styles.formCol}>
<FormField
label="Объём заявки, ₽"
type="text"
value={amount}
onChange={onAmountChange}
placeholder="от 500 000"
/>
{belowMin && (
<p className={styles.hint}>
Минимальный объём заявки {formatRub(MIN_ORDER)}
</p>
)}
<Select
id="term"
label="Срок ожидания операции"
value={days}
onChange={setDays}
options={TERM_OPTIONS.map((o) => ({
value: o.days,
label: `${dayLabel(o.days)} — комиссия ${(o.rate * 100).toFixed(3)} %`,
}))}
/>
<FormField
label="Как к вам обращаться"
type="text"
value={name}
onChange={setName}
placeholder="Имя"
/>
<FormField
label="Email или телефон для связи"
type="text"
value={contact}
onChange={setContact}
placeholder="example@mail.ru / +7 900 000-00-00"
/>
</div>
<div className={styles.infoCol}>
<div className={styles.infoTitle}>УСЛОВИЯ</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Минимальный объём</span>
<span className={styles.infoValue}>{formatRub(MIN_ORDER)} </span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Срок ожидания</span>
<span className={styles.infoValue}>{dayLabel(days)}</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Ставка комиссии</span>
<span className={styles.infoValue}>{(rate * 100).toFixed(3)} %</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Сумма комиссии</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${formatRub(commission)}` : '—'}
</span>
</div>
<div className={styles.infoRow} data-accent>
<span className={styles.infoLabel}>Итого к оплате</span>
<span className={styles.infoValue}>
{numAmount > 0 ? `${formatRub(total)}` : '—'}
</span>
</div>
<p className={styles.note}>
Итоговая комиссия рассчитывается индивидуально и зависит от объёма,
валюты и направления сделки.
</p>
</div>
</div>
<button type="submit" className={styles.submitBtn} disabled={!canSubmit}>
{isPending ? 'Отправляем...' : 'Оставить заявку'}
</button>
{notice && (
<Notification
status={notice}
message={notice === 'success' ? 'Заявка отправлена' : 'Не удалось отправить заявку'}
onClose={dismissNotice}
/>
)}
</form>
)
}

View File

@@ -1,2 +1,7 @@
export { ProfileAvatar } from './ui/ProfileAvatar'
export { ProfileSection } from './ui/ProfileSection'
export { ProfileSummary } from './ui/ProfileSummary'
export { ProfileDetails } from './ui/ProfileDetails'
export { SecuritySection } from './ui/SecuritySection'
export { MnemonicSection } from './ui/MnemonicSection'
export { KycBanner } from './ui/KycBanner'

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from 'react'
import { useUpdatePhone } from '@features/auth'
type PhoneNotification = { message: string; status: 'success' | 'error' }
// Owns the editable phone field: local value synced from the server, input
// sanitization, save-on-blur, and the resulting success/error notification.
// The notification has no meaning outside this flow, so it lives here too.
export function usePhoneField(serverPhone: string | null) {
const updatePhone = useUpdatePhone()
const [phone, setPhone] = useState('')
const [savedPhone, setSavedPhone] = useState('')
const [notification, setNotification] = useState<PhoneNotification | null>(null)
useEffect(() => {
if (serverPhone != null) {
setPhone(serverPhone)
setSavedPhone(serverPhone)
}
}, [serverPhone])
function onPhoneChange(value: string) {
setPhone(value.replace(/[^\d+\s()-]/g, ''))
}
function onPhoneBlur() {
const next = phone.trim()
if (next === savedPhone || updatePhone.isPending) return
updatePhone.mutate(next, {
onSuccess: () => {
setSavedPhone(next)
setNotification({ status: 'success', message: 'Номер телефона обновлён' })
},
onError: () => {
setNotification({ status: 'error', message: 'Не удалось обновить номер телефона' })
},
})
}
return {
phone,
onPhoneChange,
onPhoneBlur,
notification,
dismissNotification: () => setNotification(null),
}
}

View File

@@ -1,17 +1,17 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
import { type MeResponse, getFullName } from '@features/auth'
import { FormField, Notification } from '@shared/ui'
import { ProfileSection } from './ProfileSection'
import { usePhoneField } from '../model/usePhoneField'
import styles from './fieldsGrid.module.css'
interface Props {
data: MeResponse
fullName: string
phone: string
onPhoneChange: (value: string) => void
onPhoneBlur: () => void
}
export function IndividualFields({ data, fullName, phone, onPhoneChange, onPhoneBlur }: Props) {
export function IndividualFields({ data }: Props) {
const fullName = getFullName(data)
const { phone, onPhoneChange, onPhoneBlur, notification, dismissNotification } = usePhoneField(data.phone)
return (
<>
<ProfileSection title="Личные данные">
@@ -29,6 +29,10 @@ export function IndividualFields({ data, fullName, phone, onPhoneChange, onPhone
<FormField label="ID аккаунта" value={data.id ?? ''} readOnly icon="lock" placeholder="ECSA-00000000" />
</div>
</ProfileSection>
{notification && (
<Notification status={notification.status} message={notification.message} onClose={dismissNotification} />
)}
</>
)
}

View File

@@ -0,0 +1,10 @@
.kycBanner {
width: 100%;
padding: 10px 24px;
background: var(--error, #ff4466);
color: #fff;
font-size: 13px;
font-weight: 500;
text-align: center;
line-height: 1.4;
}

View File

@@ -0,0 +1,14 @@
import { useMe } from '@features/auth'
import styles from './KycBanner.module.css'
// Shown until the account passes identity verification (KYC).
export function KycBanner() {
const { data } = useMe()
if (!data || data.kyc_verified) return null
return (
<div className={styles.kycBanner}>
Для полного доступа к платформе необходимо пройти верификацию личности (KYC)
</div>
)
}

View File

@@ -1,7 +1,7 @@
import type { MeResponse } from '@features/auth'
import { FormField } from '@shared/ui'
import { ProfileSection } from '@widgets/profile'
import styles from './ProfilePage.module.css'
import { ProfileSection } from './ProfileSection'
import styles from './fieldsGrid.module.css'
interface Props {
data: MeResponse

View File

@@ -0,0 +1,31 @@
.mnemonicRow {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
}
.mnemonicInfo {
display: flex;
align-items: center;
gap: 12px;
}
.mnemonicIcon {
font-size: 16px;
}
.mnemonicText {
color: var(--text-secondary);
font-size: 14px;
font-weight: 400;
}
/* < 650px: кнопка мнемоники — 100% ширины */
@media (max-width: 649px) {
.mnemonicRow {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
}

View File

@@ -0,0 +1,21 @@
import { useNavigate } from 'react-router-dom'
import { ROUTES } from '@shared/config/routes'
import { Button } from '@shared/ui'
import { ProfileSection } from './ProfileSection'
import styles from './MnemonicSection.module.css'
export function MnemonicSection() {
const navigate = useNavigate()
return (
<ProfileSection title="Мнемоника">
<div className={styles.mnemonicRow}>
<div className={styles.mnemonicInfo}>
<span className={styles.mnemonicIcon}>🔑</span>
<span className={styles.mnemonicText}>Сид-фраза из 12 слов для восстановления кошелька</span>
</div>
<Button variant="danger" onClick={() => navigate(ROUTES.SEED_PHRASE)}> Показать мнемонику</Button>
</div>
</ProfileSection>
)
}

View File

@@ -0,0 +1,12 @@
import { useMe, isLegalEntity } from '@features/auth'
import { IndividualFields } from './IndividualFields'
import { LegalEntityFields } from './LegalEntityFields'
// Picks the right field set for the account type. Phone editing lives inside
// IndividualFields (the only branch with an editable phone).
export function ProfileDetails() {
const { data } = useMe()
if (!data) return null
return isLegalEntity(data) ? <LegalEntityFields data={data} /> : <IndividualFields data={data} />
}

View File

@@ -0,0 +1,67 @@
/* Desktop: avatar is the left column, sections grow to its right */
.profileTop {
flex-shrink: 0;
}
.userInfo {
display: none;
}
/* < 1024px: avatar + userInfo side by side at the top */
@media (max-width: 1023px) {
.profileTop {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 24px;
}
.userInfo {
display: flex;
flex-direction: column;
gap: 6px;
justify-content: center;
}
.userName {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.userBalance {
font-size: 32px;
font-weight: 800;
color: var(--text-primary);
}
.userBalanceRub {
font-size: 13px;
color: var(--text-secondary);
}
}
/* < 550px: меньше текст и аватар */
@media (max-width: 549px) {
.profileTop {
gap: 16px;
}
.userInfo {
padding-top: 8px;
}
.userName {
font-size: 18px;
}
.userBalance {
font-size: 22px;
margin-top: 4px;
}
.userBalanceRub {
font-size: 12px;
}
}

View File

@@ -0,0 +1,23 @@
import { useMe, getDisplayName } from '@features/auth'
import { usePortfolio } from '@features/wallet'
import { formatUsd } from '@shared/lib/utils/formatUsd'
import { ProfileAvatar } from './ProfileAvatar'
import styles from './ProfileSummary.module.css'
export function ProfileSummary() {
const { data } = useMe()
const { data: portfolio } = usePortfolio()
const displayName = data ? getDisplayName(data) : ''
return (
<div className={styles.profileTop}>
<ProfileAvatar />
<div className={styles.userInfo}>
<span className={styles.userName}>{displayName}</span>
<span className={styles.userBalance}>{formatUsd(portfolio?.totalUsd)}</span>
{/* <span className={styles.userBalanceRub}>≈ 22 340,50 ₽</span> */}
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
import { useWalletAddresses } from '@features/wallet'
import { Button, FormField } from '@shared/ui'
import { ProfileSection } from './ProfileSection'
import styles from './fieldsGrid.module.css'
export function SecuritySection() {
const { data: walletAddresses } = useWalletAddresses()
return (
<ProfileSection
title="Безопасность"
actions={
<>
<Button variant="danger"> Посмотреть приватный ключ</Button>
<Button variant="primary">СОХРАНИТЬ</Button>
</>
}
>
<div className={styles.grid1}>
{walletAddresses?.map(({ chain, address }) => (
<FormField
key={chain}
label={`Адрес ${chain}`}
readOnly
icon="lock"
value={address}
placeholder="—"
/>
))}
</div>
</ProfileSection>
)
}

View File

@@ -0,0 +1,25 @@
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.grid1 {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
/* < 1024px: tighter gap */
@media (max-width: 1023px) {
.grid2 {
gap: 12px;
}
}
/* < 640px: инпуты в колонку */
@media (max-width: 639px) {
.grid2 {
grid-template-columns: 1fr;
}
}