add pull page

This commit is contained in:
2026-06-17 17:54:30 +03:00
parent 474e9a3727
commit 7966ef1c52
27 changed files with 1487 additions and 1747 deletions

View File

@@ -0,0 +1,207 @@
import { PrimaryButton, TokenIcon } from '@shared/ui'
import { truncateDecimals } from '@shared/lib/utils/truncateDecimals'
import { coinIcon, formatFeeTier, type Pool, type LpQuote } from '../model/meta'
import styles from './LpDepositCard.module.css'
interface Props {
pool: Pool | null
quote: LpQuote
amount0: string
amount1: string
priceLower: string
priceUpper: string
slippage: string
useNativeEth: boolean
onAmount0Change: (v: string) => void
onAmount1Change: (v: string) => void
onPriceLowerChange: (v: string) => void
onPriceUpperChange: (v: string) => void
onSlippageChange: (v: string) => void
onUseNativeEthChange: (v: boolean) => void
onSubmit: () => void
}
const DASH = '—'
const decimalRe = /^(\d+\.?\d*|\.?\d*)$/
function AmountInput({
symbol,
value,
color,
onChange,
}: {
symbol: string
value: string
color: string
onChange: (v: string) => void
}) {
return (
<div className={styles.amountRow}>
<input
className={styles.amountInput}
type="text"
inputMode="decimal"
value={value}
onChange={(e) => {
const v = e.target.value
if (decimalRe.test(v) || v === '') onChange(v)
}}
placeholder="0"
aria-label={`Сумма ${symbol}`}
/>
<div className={styles.token}>
<TokenIcon letter={symbol[0]} color={color} logo={coinIcon(symbol)} size={28} />
<span>{symbol}</span>
</div>
</div>
)
}
function PriceInput({
label,
value,
onChange,
}: {
label: string
value: string
onChange: (v: string) => void
}) {
return (
<label className={styles.priceField}>
<span className={styles.priceLabel}>{label}</span>
<input
className={styles.priceInput}
type="text"
inputMode="decimal"
value={value}
onChange={(e) => {
const v = e.target.value
if (decimalRe.test(v) || v === '') onChange(v)
}}
placeholder="0"
/>
</label>
)
}
export function LpDepositCard({
pool,
quote,
amount0,
amount1,
priceLower,
priceUpper,
slippage,
useNativeEth,
onAmount0Change,
onAmount1Change,
onPriceLowerChange,
onPriceUpperChange,
onSlippageChange,
onUseNativeEthChange,
onSubmit,
}: Props) {
if (!pool) {
return (
<div className={styles.card}>
<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.placeholderText}>
Выберите пул в таблице выше, чтобы внести ликвидность.
</p>
</div>
</div>
)
}
const insufficient = quote.balance.sufficient === false
const apr = quote.aprPercent != null ? `${quote.aprPercent}%` : DASH
const feeUsd = quote.appFeeTotalUsd != null ? `$${quote.appFeeTotalUsd.toFixed(2)}` : DASH
return (
<div className={styles.card}>
<div className={styles.top}>
<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}
</span>
<span className={styles.fee}>{formatFeeTier(pool.feeTier)}</span>
</div>
<span className={styles.tag}>Суммы взноса</span>
<div className={styles.amounts}>
<AmountInput symbol={pool.symbol0} value={amount0} color="var(--grad-center)" onChange={onAmount0Change} />
<AmountInput symbol={pool.symbol1} value={amount1} color="var(--grad-edge)" onChange={onAmount1Change} />
</div>
<span className={styles.tag}>Диапазон цен ({pool.symbol1} за {pool.symbol0})</span>
<div className={styles.range}>
<PriceInput label="Мин." value={priceLower} onChange={onPriceLowerChange} />
<PriceInput label="Макс." value={priceUpper} onChange={onPriceUpperChange} />
</div>
<div className={styles.options}>
<label className={styles.slippage}>
<span className={styles.priceLabel}>Проскальзывание, %</span>
<input
className={styles.priceInput}
type="text"
inputMode="decimal"
value={slippage}
onChange={(e) => {
const v = e.target.value
if (decimalRe.test(v) || v === '') onSlippageChange(v)
}}
placeholder="1"
/>
</label>
<label className={styles.toggle}>
<input
type="checkbox"
checked={useNativeEth}
onChange={(e) => onUseNativeEthChange(e.target.checked)}
/>
<span>Использовать нативный ETH</span>
</label>
</div>
<div className={styles.quote}>
<div className={styles.quoteRow}>
<span className={styles.quoteLabel}>Взнос {pool.symbol0}</span>
<span className={styles.quoteValue}>{truncateDecimals(quote.amount0DesiredHuman, 8)}</span>
</div>
<div className={styles.quoteRow}>
<span className={styles.quoteLabel}>Взнос {pool.symbol1}</span>
<span className={styles.quoteValue}>{truncateDecimals(quote.amount1DesiredHuman, 8)}</span>
</div>
<div className={styles.quoteRow}>
<span className={styles.quoteLabel}>Комиссия сервиса (0.7%)</span>
<span className={styles.quoteValue}>{feeUsd}</span>
</div>
<div className={styles.quoteRow}>
<span className={styles.quoteLabel}>Годовая доходность (APR)</span>
<span className={`${styles.quoteValue} ${styles.success}`}>{apr}</span>
</div>
</div>
{insufficient && (
<p className={styles.warning}>
Недостаточно средств: не хватает {quote.balance.token1.symbol}. Пополните баланс.
</p>
)}
<div className={styles.submit}>
<PrimaryButton label="Внести в пул" type="button" onClick={onSubmit} disabled={insufficient} />
</div>
</div>
)
}