first commit

This commit is contained in:
2026-05-09 00:38:56 +03:00
commit 51a44ef13d
156 changed files with 9832 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
.wrap {
display: flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
text-align: left;
background: none;
border: none;
padding: 0;
}
.box {
width: 20px;
height: 20px;
border-radius: 6px;
border: 2px solid var(--glass-border);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s;
margin-top: 2px;
}
.box svg {
opacity: 0;
transition: opacity 0.2s;
}
.box[data-checked] {
background: var(--grad-center);
border-color: var(--grad-center);
}
.box[data-checked] svg {
opacity: 1;
}
.text {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
max-width: 500px;
}
.link {
color: var(--interactive);
text-decoration: underline;
}
.required {
font-size: 11px;
opacity: 0.6;
}

View File

@@ -0,0 +1,42 @@
import styles from './AgreementCheckbox.module.css'
interface Props {
checked: boolean
onToggle: () => void
}
export function AgreementCheckbox({ checked, onToggle }: Props) {
return (
<button
type="button"
className={styles.wrap}
onClick={onToggle}
aria-pressed={checked}
>
<span className={styles.box} data-checked={checked || undefined}>
<svg width={12} height={12} viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path
d="M2 6l3 3 5-5"
stroke="#fff"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span className={styles.text}>
Я ознакомлен и согласен с{' '}
<a
href="#"
className={styles.link}
onClick={(e) => e.preventDefault()}
>
публичной офертой
</a>
. Вся деятельность компании соответствует законодательству Российской Федерации.
<br />
<span className={styles.required}>ОБЯЗАТЕЛЬНОЕ ПОЛЕ</span>
</span>
</button>
)
}

View File

@@ -0,0 +1,82 @@
.title {
font-size: 16px;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 24px;
}
.table {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid var(--glass-border);
transition: all 0.3s ease;
}
.row[data-active] {
border-color: var(--grad-center);
background: rgba(91, 61, 184, 0.12);
}
.range {
font-size: 14px;
color: var(--text-secondary);
}
.pct {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 700;
color: var(--highlight);
}
.progressBar {
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.06);
margin-bottom: 32px;
overflow: hidden;
}
.progressFill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--grad-center), var(--highlight));
transition: width 0.5s ease;
}
.summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 18px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid var(--glass-border);
margin-bottom: 16px;
}
.summary:last-child {
margin-bottom: 0;
}
.summaryLabel {
font-size: 13px;
color: var(--text-secondary);
}
.summaryValue {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 600;
}

View File

@@ -0,0 +1,34 @@
import { TIERS } from '../model/tiers'
import styles from './CommissionTable.module.css'
import { Tiers } from './Tiers'
interface Props {
amount: number
progress: number
commission: number
effectiveRate: number
}
export function CommissionTable({ amount, progress, commission, effectiveRate }: Props) {
return (
<div>
<div className={styles.title}>КОМИССИЯ СЕРВИСА</div>
<div className={styles.table}>
<Tiers tiers={TIERS} amount={amount} />
</div>
<div className={styles.progressBar}>
<div className={styles.progressFill} style={{ width: `${progress}%` }} />
</div>
<div className={styles.summary}>
<span className={styles.summaryLabel}>Комиссия</span>
<span className={styles.summaryValue}>
{commission.toLocaleString('ru-RU', { maximumFractionDigits: 2 })}
</span>
</div>
<div className={styles.summary}>
<span className={styles.summaryLabel}>Курс с комиссией</span>
<span className={styles.summaryValue}>{effectiveRate.toFixed(2)} </span>
</div>
</div>
)
}

View File

@@ -0,0 +1,240 @@
.section {
padding: 100px 48px;
background: var(--bg-deep);
}
.wrap {
max-width: 1200px;
margin: 0 auto;
background: var(--bg-mid);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 32px;
position: relative;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 24px;
margin-bottom: 40px;
}
.title {
font-size: clamp(36px, 4vw, 52px);
font-weight: 700;
}
.subtitle {
font-size: 14px;
color: var(--text-secondary);
margin-top: 8px;
letter-spacing: 1px;
}
.pills {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.pill {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: var(--text-secondary);
}
.pillValue {
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 6px 14px;
font-family: var(--font-mono);
font-size: 14px;
color: var(--text-primary);
}
.body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.tabs {
display: inline-flex;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--glass-border);
margin-bottom: 32px;
}
.tab {
padding: 12px 32px;
font-size: 14px;
font-weight: 600;
letter-spacing: 1px;
background: transparent;
color: var(--text-secondary);
transition: all 0.3s;
}
.tab[data-active] {
background: var(--grad-center);
color: var(--text-primary);
}
.tab:not([data-active]):hover {
background: rgba(255, 255, 255, 0.04);
}
.field {
margin-bottom: 24px;
}
.fieldLabel {
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 8px;
}
.fieldInput {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 0 16px;
transition: border-color 0.3s;
}
.fieldInput:focus-within {
border-color: var(--interactive);
}
.fieldInput input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 18px;
padding: 16px 0;
width: 100%;
}
.currency {
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
}
.currencyIcon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: #fff;
}
.currencyRub {
background: var(--interactive);
}
.currencyUsdt {
background: var(--success);
}
.swapWrap {
display: flex;
justify-content: center;
}
.swapBtn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--glass-border);
background: var(--glass-bg);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
color: var(--text-secondary);
}
.swapBtn:hover {
border-color: var(--highlight);
color: var(--highlight);
transform: rotate(180deg);
}
.bottom {
display: flex;
justify-content: center;
margin-top: 40px;
padding-top: 32px;
border-top: 1px solid var(--glass-border);
}
@media (max-width: 1024px) {
.body {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.section {
padding: 40px 32px;
}
.header {
margin-bottom: 1rem;
}
.tabs {
margin-bottom: 1.5rem;
display: flex;
}
.tab {
flex: 0 0 50%;
}
.field {
margin-bottom: 1rem;
}
.bottom {
margin-top: 1.5rem;
padding-top: 1rem;
}
.pills {
display: none;
}
}
@media (max-width: 640px) {
.section {
padding-left: 24px;
padding-right: 24px;
}
.wrap {
padding: 28px;
}
}

View File

@@ -0,0 +1,110 @@
import { GAS_PRICE, USDT_RATE } from '@shared/config/constants'
import { useConverter } from '../model/useConverter'
import { AgreementCheckbox } from './AgreementCheckbox'
import { CommissionTable } from './CommissionTable'
import styles from './Converter.module.css'
import { Title } from '@shared/ui/Title/Title'
export function Converter() {
const c = useConverter({ usdtRate: USDT_RATE })
return (
<section className={styles.section} id="converter">
<div className={styles.wrap}>
<div className={styles.header}>
<div>
<Title>Конвертация</Title>
<div className={styles.subtitle}>Данные обновляются в реальном времени</div>
</div>
<div className={styles.pills}>
<div className={styles.pill}>
Цена газа в RUB <span className={styles.pillValue}>{GAS_PRICE.toFixed(2)} RUB</span>
</div>
<div className={styles.pill}>
USDT/RUB <span className={styles.pillValue}>{USDT_RATE.toFixed(2)} </span>
</div>
</div>
</div>
<div className={styles.body}>
<div>
<div className={styles.tabs}>
<button
type="button"
className={styles.tab}
data-active={c.mode === 'buy' || undefined}
onClick={() => c.setMode('buy')}
>
КУПИТЬ
</button>
<button
type="button"
className={styles.tab}
data-active={c.mode === 'sell' || undefined}
onClick={() => c.setMode('sell')}
>
ПРОДАТЬ
</button>
</div>
<div className={styles.field}>
<div className={styles.fieldLabel}>Конвертируете</div>
<div className={styles.fieldInput}>
<input
type="text"
value={c.rubVal}
onChange={(e) => c.updateRub(e.target.value)}
placeholder="0"
inputMode="decimal"
/>
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyRub}`}></span> RUB
</div>
</div>
</div>
<div className={styles.swapWrap}>
<button
type="button"
className={styles.swapBtn}
onClick={c.toggleMode}
aria-label="Поменять направление"
>
<svg width={16} height={16} viewBox="0 0 16 16" fill="none">
<path
d="M8 2v12M4 10l4 4 4-4"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<div className={styles.field}>
<div className={styles.fieldLabel}>Получаете</div>
<div className={styles.fieldInput}>
<input type="text" value={c.usdtVal} readOnly />
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span> USDT
</div>
</div>
</div>
</div>
<CommissionTable
amount={c.numRub}
progress={c.progress}
commission={c.commission}
effectiveRate={c.effectiveRate}
/>
</div>
<div className={styles.bottom}>
<AgreementCheckbox checked={c.agreed} onToggle={() => c.setAgreed(!c.agreed)} />
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,27 @@
import { TIERS } from "../model/tiers"
import styles from './CommissionTable.module.css'
const ru = (n: number) => n.toLocaleString('ru-RU')
type TiersProps = {
tiers: typeof TIERS
amount: number
}
export const Tiers = ({ tiers, amount }: TiersProps) => {
return (
<>
{tiers.map((tier, i) => {
const active = amount >= tier.min && amount <= tier.max
return (
<div key={i} className={styles.row} data-active={active || undefined}>
<span className={styles.range}>
{ru(tier.min)} {ru(tier.max)}
</span>
<span className={styles.pct}>{tier.pct}%</span>
</div>
)
})}
</>
)
}