refactor(converter): shared page layout + reusable conversion logic/UI

Pages:
- add WalletLayout route (WalletHeader + main + Footer via <Outlet/>),
  wrap converter/swap/bridge/transactions; thin pages, drop duplicated shell CSS
- extract SwapBridgeTabs shared between swap/bridge pages

Converter reuse (FSD layers, no widget->widget imports):
- move commission tiers to entities/commission (+ CommissionTable ui)
- shared calc hook features/payment/model/useCurrencyConversion;
  useConverterSection becomes thin wrapper; HomePage Converter reuses it
- move ConvertField/DirectionSwapButton to shared/ui; delete dead useConverter

Tooling:
- add eslint.config.js (ESLint 9 flat config); fix no-explicit-any in WalletPage

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 14:39:53 +03:00
parent bdc8bd3d93
commit 9b1d6ffb5d
45 changed files with 684 additions and 1129 deletions

View File

@@ -0,0 +1,3 @@
export { TIERS, TIER_MIN, TIER_MAX, findTier, progressPercent } from './model/tiers'
export type { Tier } from './model/tiers'
export { CommissionTable } from './ui/CommissionTable'

View File

@@ -0,0 +1,27 @@
export interface Tier {
min: number
max: number
pct: number
}
export const TIERS: readonly Tier[] = [
{ min: 5_000, max: 30_000, pct: 8 },
{ min: 30_001, max: 100_000, pct: 6 },
{ min: 100_001, max: 600_000, pct: 4 },
] as const
export const TIER_MIN = TIERS[0].min
export const TIER_MAX = TIERS[TIERS.length - 1].max
export function findTier(amount: number): Pick<Tier, 'pct'> {
const match = TIERS.find((t) => amount >= t.min && amount <= t.max)
if (match) return match
if (amount > TIER_MAX) return { pct: TIERS[TIERS.length - 1].pct }
return { pct: TIERS[0].pct }
}
export function progressPercent(amount: number): number {
if (amount <= TIER_MIN) return 0
if (amount >= TIER_MAX) return 100
return ((amount - TIER_MIN) / (TIER_MAX - TIER_MIN)) * 100
}

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,49 @@
import { TIERS } from '../model/tiers'
import styles from './CommissionTable.module.css'
const ru = (n: number) => n.toLocaleString('ru-RU')
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.map((tier, i) => (
<div
key={i}
className={styles.row}
data-active={(amount >= tier.min && amount <= tier.max) || undefined}
>
<span className={styles.range}>
{ru(tier.min)} {ru(tier.max)}
</span>
<span className={styles.pct}>{tier.pct}%</span>
</div>
))}
</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>
)
}