Files
frontend/src/widgets/pools/ui/PoolsTable.tsx
2026-06-17 18:24:14 +03:00

171 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react'
import { Spinner } from '@shared/ui'
import type { Pool } from '@features/pools'
import { formatFeeTier } from '../model/meta'
import styles from './PoolsTable.module.css'
interface Props {
pools: Pool[]
selected: string | null
onSelect: (poolAddress: string) => void
loading?: boolean
error?: string | null
}
const DASH = '—'
type SortKey = 'pair' | 'feeTier' | 'currentPrice' | 'tvlUSD' | 'volumeUSD24h' | 'aprPercent'
type SortDir = 'desc' | 'asc'
function formatUsd(value: number | null): string {
if (value == null) return DASH
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`
if (value >= 1_000) return `$${(value / 1_000).toFixed(2)}K`
return `$${value.toFixed(2)}`
}
function formatPrice(value: number): string {
return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
// APR обрезаем до 3 знаков после запятой и убираем лишние нули.
function formatApr(value: number | null): string {
if (value == null) return DASH
return `${parseFloat(value.toFixed(3))}%`
}
function pairName(p: Pool): string {
return `${p.symbol0}/${p.symbol1}`
}
// Числовые поля могут быть 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) {
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 (
<div className={styles.card}>
<div className={styles.head}>
<span className={styles.title}>Курируемые пулы</span>
</div>
{loading && <Spinner label="Загрузка пулов" />}
{!loading && error && <div className={styles.message}>{error}</div>}
{!loading && !error && pools.length === 0 && (
<div className={styles.message}>Курируемых пулов пока нет.</div>
)}
{!loading && !error && pools.length > 0 && (
<div className={styles.scroll}>
<table className={styles.table}>
<thead>
<tr>
{columns.map((col) => (
<th
key={col.key}
className={`${styles.sortable} ${col.align === 'left' ? styles.thLeft : ''}`}
onClick={() => handleSort(col.key)}
aria-sort={sortKey === col.key ? (sortDir === 'asc' ? 'ascending' : 'descending') : 'none'}
>
{col.label}
<span className={styles.arrow}>{sortArrow(col.key)}</span>
</th>
))}
<th aria-label="Действие" />
</tr>
</thead>
<tbody>
{sortedPools.map((p) => {
const isSelected = p.poolAddress === selected
return (
<tr
key={p.poolAddress}
className={isSelected ? styles.rowSelected : styles.row}
onClick={() => onSelect(p.poolAddress)}
>
<td className={styles.tdLeft}>
<span className={styles.pairName}>
{p.symbol0}/{p.symbol1}
</span>
</td>
<td>
<span className={styles.fee}>{formatFeeTier(p.feeTier)}</span>
</td>
<td className={styles.mono}>{formatPrice(p.currentPrice)}</td>
<td className={styles.mono}>{formatUsd(p.tvlUSD)}</td>
<td className={styles.mono}>{formatUsd(p.volumeUSD24h)}</td>
<td className={`${styles.mono} ${styles.apr}`}>{formatApr(p.aprPercent)}</td>
<td className={styles.tdAction}>
<button
type="button"
className={isSelected ? styles.btnSelected : styles.btn}
onClick={(e) => {
e.stopPropagation()
onSelect(p.poolAddress)
}}
>
{isSelected ? 'Выбран' : 'Выбрать'}
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}