add staking page
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
<title>ЭКСА — Ваш мост в мир цифровых активов</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CvjPBUMw.js"></script>
|
<script type="module" crossorigin src="/assets/index-DO4w311N.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CyLzS1Ao.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BqMPDWqk.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -22,15 +22,6 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons > :last-child {
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fee {
|
.fee {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -156,10 +147,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle input {
|
.toggle input {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
accent-color: var(--interactive);
|
margin: 0;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--bg-deep);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:hover {
|
||||||
|
border-color: var(--interactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked {
|
||||||
|
background: var(--interactive);
|
||||||
|
border-color: var(--interactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 6px;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid #fff;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input:focus-visible {
|
||||||
|
outline: 2px solid var(--highlight);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote {
|
.quote {
|
||||||
@@ -224,18 +249,6 @@
|
|||||||
background: var(--bg-deep);
|
background: var(--bg-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholderIcon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholderTitle {
|
.placeholderTitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PrimaryButton, TokenIcon } from '@shared/ui'
|
import { PrimaryButton } from '@shared/ui'
|
||||||
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
||||||
import type { Pool, LpQuote } from '@features/pools'
|
import type { Pool, LpQuote } from '@features/pools'
|
||||||
import { coinIcon, formatFeeTier } from '../model/meta'
|
import { formatFeeTier } from '../model/meta'
|
||||||
import styles from './LpDepositCard.module.css'
|
import styles from './LpDepositCard.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -30,12 +30,10 @@ const decimalRe = /^(\d+\.?\d*|\.?\d*)$/
|
|||||||
function AmountInput({
|
function AmountInput({
|
||||||
symbol,
|
symbol,
|
||||||
value,
|
value,
|
||||||
color,
|
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
symbol: string
|
symbol: string
|
||||||
value: string
|
value: string
|
||||||
color: string
|
|
||||||
onChange: (v: string) => void
|
onChange: (v: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +51,6 @@ function AmountInput({
|
|||||||
aria-label={`Сумма ${symbol}`}
|
aria-label={`Сумма ${symbol}`}
|
||||||
/>
|
/>
|
||||||
<div className={styles.token}>
|
<div className={styles.token}>
|
||||||
<TokenIcon letter={symbol[0]} color={color} logo={coinIcon(symbol)} size={28} />
|
|
||||||
<span>{symbol}</span>
|
<span>{symbol}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,12 +107,6 @@ export function LpDepositCard({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
<span className={styles.placeholderIcon} aria-hidden>
|
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="9" cy="9" r="5" />
|
|
||||||
<circle cx="15" cy="15" r="5" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<p className={styles.placeholderTitle}>Выберите пул</p>
|
<p className={styles.placeholderTitle}>Выберите пул</p>
|
||||||
<p className={styles.placeholderText}>
|
<p className={styles.placeholderText}>
|
||||||
Выберите пул в таблице выше, чтобы внести ликвидность.
|
Выберите пул в таблице выше, чтобы внести ликвидность.
|
||||||
@@ -139,10 +130,6 @@ export function LpDepositCard({
|
|||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.top}>
|
<div className={styles.top}>
|
||||||
<span className={styles.pair}>
|
<span className={styles.pair}>
|
||||||
<span className={styles.icons}>
|
|
||||||
<TokenIcon letter={pool.symbol0[0]} color="var(--grad-center)" logo={coinIcon(pool.symbol0)} size={24} />
|
|
||||||
<TokenIcon letter={pool.symbol1[0]} color="var(--grad-edge)" logo={coinIcon(pool.symbol1)} size={24} />
|
|
||||||
</span>
|
|
||||||
{pool.symbol0}/{pool.symbol1}
|
{pool.symbol0}/{pool.symbol1}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.fee}>{formatFeeTier(pool.feeTier)}</span>
|
<span className={styles.fee}>{formatFeeTier(pool.feeTier)}</span>
|
||||||
@@ -150,8 +137,8 @@ export function LpDepositCard({
|
|||||||
|
|
||||||
<span className={styles.tag}>Суммы взноса</span>
|
<span className={styles.tag}>Суммы взноса</span>
|
||||||
<div className={styles.amounts}>
|
<div className={styles.amounts}>
|
||||||
<AmountInput symbol={pool.symbol0} value={amount0} color="var(--grad-center)" onChange={onAmount0Change} />
|
<AmountInput symbol={pool.symbol0} value={amount0} onChange={onAmount0Change} />
|
||||||
<AmountInput symbol={pool.symbol1} value={amount1} color="var(--grad-edge)" onChange={onAmount1Change} />
|
<AmountInput symbol={pool.symbol1} value={amount1} onChange={onAmount1Change} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className={styles.tag}>Диапазон цен ({pool.symbol1} за {pool.symbol0})</span>
|
<span className={styles.tag}>Диапазон цен ({pool.symbol1} за {pool.symbol0})</span>
|
||||||
|
|||||||
@@ -33,8 +33,27 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 4px;
|
padding-right: 6px;
|
||||||
margin-right: -4px;
|
margin-right: -6px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +119,6 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons > :last-child {
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fee {
|
.fee {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -202,18 +212,6 @@
|
|||||||
background: var(--bg-deep);
|
background: var(--bg-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyIcon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyTitle {
|
.emptyTitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Spinner, TokenIcon } from '@shared/ui'
|
import { Spinner } from '@shared/ui'
|
||||||
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
|
||||||
import type { LpPosition } from '@features/pools'
|
import type { LpPosition } from '@features/pools'
|
||||||
import { coinIcon, formatFeeTier } from '../model/meta'
|
import { formatFeeTier } from '../model/meta'
|
||||||
import styles from './LpPositionsPanel.module.css'
|
import styles from './LpPositionsPanel.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -32,12 +32,6 @@ export function LpPositionsPanel({ positions, onRemove, removePending, isLoading
|
|||||||
|
|
||||||
{!isLoading && !error && empty && (
|
{!isLoading && !error && empty && (
|
||||||
<div className={styles.emptyState}>
|
<div className={styles.emptyState}>
|
||||||
<span className={styles.emptyIcon} aria-hidden>
|
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
||||||
<circle cx="9" cy="9" r="5" />
|
|
||||||
<circle cx="15" cy="15" r="5" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<p className={styles.emptyTitle}>Активных позиций пока нет</p>
|
<p className={styles.emptyTitle}>Активных позиций пока нет</p>
|
||||||
<p className={styles.emptyText}>На данный момент у Вас нет позиций в пулах ликвидности.</p>
|
<p className={styles.emptyText}>На данный момент у Вас нет позиций в пулах ликвидности.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,15 +43,11 @@ export function LpPositionsPanel({ positions, onRemove, removePending, isLoading
|
|||||||
<div key={p.tokenId} className={styles.pos}>
|
<div key={p.tokenId} className={styles.pos}>
|
||||||
<div className={styles.posTop}>
|
<div className={styles.posTop}>
|
||||||
<span className={styles.coin}>
|
<span className={styles.coin}>
|
||||||
<span className={styles.icons}>
|
|
||||||
<TokenIcon letter={p.symbol0[0]} color="var(--grad-center)" logo={coinIcon(p.symbol0)} size={24} />
|
|
||||||
<TokenIcon letter={p.symbol1[0]} color="var(--grad-edge)" logo={coinIcon(p.symbol1)} size={24} />
|
|
||||||
</span>
|
|
||||||
{p.symbol0}/{p.symbol1}
|
{p.symbol0}/{p.symbol1}
|
||||||
<span className={styles.fee}>{formatFeeTier(p.feeTier)}</span>
|
<span className={styles.fee}>{formatFeeTier(p.feeTier)}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={`${styles.badge} ${p.inRange ? styles.badgeActive : styles.badgeWarn}`}>
|
<span className={`${styles.badge} ${p.inRange ? styles.badgeActive : styles.badgeWarn}`}>
|
||||||
{p.inRange ? 'В диапазоне' : 'Вне диапазона ⚠️'}
|
{p.inRange ? 'В диапазоне' : 'Вне диапазона'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,20 @@
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.row,
|
.row,
|
||||||
.rowSelected {
|
.rowSelected {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -89,22 +103,6 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pair {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Вторая монета пары слегка наезжает на первую. */
|
|
||||||
.icons > :last-child {
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pairName {
|
.pairName {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -123,11 +121,13 @@
|
|||||||
.btn,
|
.btn,
|
||||||
.btnSelected {
|
.btnSelected {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
min-width: 110px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Spinner, TokenIcon } from '@shared/ui'
|
import { useMemo, useState } from 'react'
|
||||||
|
import { Spinner } from '@shared/ui'
|
||||||
import type { Pool } from '@features/pools'
|
import type { Pool } from '@features/pools'
|
||||||
import { coinIcon, formatFeeTier } from '../model/meta'
|
import { formatFeeTier } from '../model/meta'
|
||||||
import styles from './PoolsTable.module.css'
|
import styles from './PoolsTable.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -13,6 +14,9 @@ interface Props {
|
|||||||
|
|
||||||
const DASH = '—'
|
const DASH = '—'
|
||||||
|
|
||||||
|
type SortKey = 'pair' | 'feeTier' | 'currentPrice' | 'tvlUSD' | 'volumeUSD24h' | 'aprPercent'
|
||||||
|
type SortDir = 'desc' | 'asc'
|
||||||
|
|
||||||
function formatUsd(value: number | null): string {
|
function formatUsd(value: number | null): string {
|
||||||
if (value == null) return DASH
|
if (value == null) return DASH
|
||||||
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`
|
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`
|
||||||
@@ -24,21 +28,72 @@ function formatPrice(value: number): string {
|
|||||||
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
function PairIcons({ symbol0, symbol1 }: { symbol0: string; symbol1: string }) {
|
// APR обрезаем до 3 знаков после запятой и убираем лишние нули.
|
||||||
return (
|
function formatApr(value: number | null): string {
|
||||||
<span className={styles.pair}>
|
if (value == null) return DASH
|
||||||
<span className={styles.icons}>
|
return `${parseFloat(value.toFixed(3))}%`
|
||||||
<TokenIcon letter={symbol0[0]} color="var(--grad-center)" logo={coinIcon(symbol0)} size={24} />
|
}
|
||||||
<TokenIcon letter={symbol1[0]} color="var(--grad-edge)" logo={coinIcon(symbol1)} size={24} />
|
|
||||||
</span>
|
function pairName(p: Pool): string {
|
||||||
<span className={styles.pairName}>
|
return `${p.symbol0}/${p.symbol1}`
|
||||||
{symbol0}/{symbol1}
|
}
|
||||||
</span>
|
|
||||||
</span>
|
// Числовые поля могут быть null — null уходит вниз независимо от направления.
|
||||||
)
|
function compare(a: Pool, b: Pool, key: SortKey, dir: SortDir): number {
|
||||||
|
if (key === 'pair') {
|
||||||
|
const cmp = pairName(a).localeCompare(pairName(b))
|
||||||
|
return dir === 'asc' ? cmp : -cmp
|
||||||
|
}
|
||||||
|
const av = a[key]
|
||||||
|
const bv = b[key]
|
||||||
|
if (av == null && bv == null) return 0
|
||||||
|
if (av == null) return 1
|
||||||
|
if (bv == null) return -1
|
||||||
|
const cmp = av - bv
|
||||||
|
return dir === 'asc' ? cmp : -cmp
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PoolsTable({ pools, selected, onSelect, loading, error }: Props) {
|
export function PoolsTable({ pools, selected, onSelect, loading, error }: Props) {
|
||||||
|
const [sortKey, setSortKey] = useState<SortKey | null>(null)
|
||||||
|
const [sortDir, setSortDir] = useState<SortDir | null>(null)
|
||||||
|
|
||||||
|
function handleSort(key: SortKey) {
|
||||||
|
if (sortKey !== key) {
|
||||||
|
// Новая колонка — начинаем с убывания.
|
||||||
|
setSortKey(key)
|
||||||
|
setSortDir('desc')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Цикл по той же колонке: убывание → возрастание → по умолчанию.
|
||||||
|
if (sortDir === 'desc') {
|
||||||
|
setSortDir('asc')
|
||||||
|
} else if (sortDir === 'asc') {
|
||||||
|
setSortKey(null)
|
||||||
|
setSortDir(null)
|
||||||
|
} else {
|
||||||
|
setSortDir('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedPools = useMemo(() => {
|
||||||
|
if (!sortKey || !sortDir) return pools
|
||||||
|
return [...pools].sort((a, b) => compare(a, b, sortKey, sortDir))
|
||||||
|
}, [pools, sortKey, sortDir])
|
||||||
|
|
||||||
|
function sortArrow(key: SortKey): string {
|
||||||
|
if (sortKey !== key || !sortDir) return ''
|
||||||
|
return sortDir === 'desc' ? ' ▼' : ' ▲'
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: { key: SortKey; label: string; align: 'left' | 'right' }[] = [
|
||||||
|
{ key: 'pair', label: 'Пул', align: 'left' },
|
||||||
|
{ key: 'feeTier', label: 'Комиссия', align: 'right' },
|
||||||
|
{ key: 'currentPrice', label: 'Цена', align: 'right' },
|
||||||
|
{ key: 'tvlUSD', label: 'TVL', align: 'right' },
|
||||||
|
{ key: 'volumeUSD24h', label: 'Объём 24ч', align: 'right' },
|
||||||
|
{ key: 'aprPercent', label: 'APR', align: 'right' },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.head}>
|
<div className={styles.head}>
|
||||||
@@ -56,17 +111,22 @@ export function PoolsTable({ pools, selected, onSelect, loading, error }: Props)
|
|||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={styles.thLeft}>Пул</th>
|
{columns.map((col) => (
|
||||||
<th>Комиссия</th>
|
<th
|
||||||
<th>Цена</th>
|
key={col.key}
|
||||||
<th>TVL</th>
|
className={`${styles.sortable} ${col.align === 'left' ? styles.thLeft : ''}`}
|
||||||
<th>Объём 24ч</th>
|
onClick={() => handleSort(col.key)}
|
||||||
<th>APR</th>
|
aria-sort={sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
|
||||||
|
>
|
||||||
|
{col.label}
|
||||||
|
<span className={styles.arrow}>{sortArrow(col.key)}</span>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
<th aria-label="Действие" />
|
<th aria-label="Действие" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pools.map((p) => {
|
{sortedPools.map((p) => {
|
||||||
const isSelected = p.poolAddress === selected
|
const isSelected = p.poolAddress === selected
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
@@ -75,7 +135,9 @@ export function PoolsTable({ pools, selected, onSelect, loading, error }: Props)
|
|||||||
onClick={() => onSelect(p.poolAddress)}
|
onClick={() => onSelect(p.poolAddress)}
|
||||||
>
|
>
|
||||||
<td className={styles.tdLeft}>
|
<td className={styles.tdLeft}>
|
||||||
<PairIcons symbol0={p.symbol0} symbol1={p.symbol1} />
|
<span className={styles.pairName}>
|
||||||
|
{p.symbol0}/{p.symbol1}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={styles.fee}>{formatFeeTier(p.feeTier)}</span>
|
<span className={styles.fee}>{formatFeeTier(p.feeTier)}</span>
|
||||||
@@ -83,9 +145,7 @@ export function PoolsTable({ pools, selected, onSelect, loading, error }: Props)
|
|||||||
<td className={styles.mono}>{formatPrice(p.currentPrice)}</td>
|
<td className={styles.mono}>{formatPrice(p.currentPrice)}</td>
|
||||||
<td className={styles.mono}>{formatUsd(p.tvlUSD)}</td>
|
<td className={styles.mono}>{formatUsd(p.tvlUSD)}</td>
|
||||||
<td className={styles.mono}>{formatUsd(p.volumeUSD24h)}</td>
|
<td className={styles.mono}>{formatUsd(p.volumeUSD24h)}</td>
|
||||||
<td className={`${styles.mono} ${styles.apr}`}>
|
<td className={`${styles.mono} ${styles.apr}`}>{formatApr(p.aprPercent)}</td>
|
||||||
{p.aprPercent != null ? `${p.aprPercent}%` : DASH}
|
|
||||||
</td>
|
|
||||||
<td className={styles.tdAction}>
|
<td className={styles.tdAction}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -11,6 +11,14 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Базовая высота колонок, чтобы при невыбранном пуле панель «Мои позиции»
|
||||||
|
не сжималась до плейсхолдера и не уходила в скролл. */
|
||||||
|
@media (min-width: 1101px) {
|
||||||
|
.grid > * {
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user