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

161
dist/assets/index-BVKNc3Mv.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-pOqaElwW.css vendored Normal file

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
<script type="module" crossorigin src="/assets/index-BYdWOI3m.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BtHpZOfG.css">
<script type="module" crossorigin src="/assets/index-BVKNc3Mv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-pOqaElwW.css">
</head>
<body>
<div id="root"></div>

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;
}
}

File diff suppressed because one or more lines are too long