Files
frontend/src/widgets/converter-page/ui/ConverterSection.tsx
2026-05-25 15:20:38 +03:00

241 lines
8.6 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 { useState } from 'react'
import { useConverter, progressPercent } from '@widgets/currency-converter'
import { useDebounce } from '@shared/lib/hooks/useDebounce'
import { usePaymentQuote, usePaymentQuoteByRub, usePaymentConfig, useCreateOrder } from '@features/payment'
import { CommissionPanel } from './CommissionPanel'
import { AgreementCheck } from './AgreementCheck'
import styles from './ConverterSection.module.css'
import { GAS_PRICE, MIN_RUB_AMOUNT } from '@shared/config/constants'
export function ConverterSection() {
const c = useConverter({ usdtRate: 0 })
const [direction, setDirection] = useState<'usdt_to_rub' | 'rub_to_usdt'>('usdt_to_rub')
const [rubInputVal, setRubInputVal] = useState(String(MIN_RUB_AMOUNT))
const { data: config } = usePaymentConfig()
const configUsdtRate = Number(config?.usdt_exchange_rate) || 0
const gasPriceRub = Number(config?.gas_fee) || GAS_PRICE
const isUsdtToRub = direction === 'usdt_to_rub'
const debouncedUsdt = useDebounce(c.numRub, 400)
const { data: quoteUsdtToRub, isError: quoteError } = usePaymentQuote(isUsdtToRub ? debouncedUsdt : 0)
const numRubInput = Number.parseFloat(rubInputVal) || 0
const debouncedRub = useDebounce(numRubInput, 400)
const { data: quoteRubToUsdt, isError: quoteRubError } = usePaymentQuoteByRub(!isUsdtToRub ? debouncedRub : 0)
const rubBelowMin = !isUsdtToRub && numRubInput > 0 && numRubInput < MIN_RUB_AMOUNT
function updateRubInput(raw: string) {
setRubInputVal(raw.replace(/[^0-9.]/g, ''))
}
function handleSwap() {
setDirection(d => d === 'usdt_to_rub' ? 'rub_to_usdt' : 'usdt_to_rub')
}
const rubTotal = quoteUsdtToRub?.total_price ?? ''
const rubTotalNum = Number(rubTotal) || 0
const usdtFromRub = quoteRubToUsdt?.usdt_amount ?? ''
const usdtFromRubNum = Number(usdtFromRub) || 0
const commission = isUsdtToRub
? Number(quoteUsdtToRub?.service_fee) || 0
: Number(quoteRubToUsdt?.service_fee) || 0
const displayRubAmount = isUsdtToRub ? rubTotalNum : numRubInput
const effectiveRate = isUsdtToRub
? (c.numRub > 0 ? rubTotalNum / c.numRub : 0)
: (usdtFromRubNum > 0 ? numRubInput / usdtFromRubNum : 0)
const { mutate: submitOrder, isPending } = useCreateOrder()
function handlePay() {
if (isUsdtToRub) {
submitOrder({
usdt_amount: c.numRub,
usdt_exchange_rate: 1,
gas_fee: 1,
total_price: Number(rubTotal) || 0,
})
} else {
submitOrder({
usdt_amount: usdtFromRubNum,
usdt_exchange_rate: 1,
gas_fee: 1,
total_price: numRubInput,
})
}
}
const isPayDisabled = isUsdtToRub
? (!rubTotal || isPending || !c.agreed)
: (!usdtFromRub || isPending || !c.agreed || rubBelowMin)
return (
<div className={styles.wrap}>
<div className={styles.header}>
<div>
<h1 className={styles.title}>Конвертация</h1>
<div className={styles.subtitle}>Данные обновляются в реальном времени</div>
</div>
<div className={styles.pills}>
<div className={styles.pill}>
Цена газа в RUB <span className={styles.pillValue}>{gasPriceRub.toFixed(2)} RUB</span>
</div>
<div className={styles.pill}>
USDT/RUB <span className={styles.pillValue}>{configUsdtRate.toFixed(2)} </span>
</div>
</div>
</div>
<div className={styles.body}>
<div>
<div className={styles.tabs}>
<div className={styles.tab} data-active>
КУПИТЬ
</div>
</div>
{isUsdtToRub ? (
<>
<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.currencyUsdt}`}></span>
USDT
</div>
</div>
{quoteError && (
<div className={styles.fieldError}>
Сумма слишком большая и превышает 600 000
</div>
)}
</div>
<div className={styles.swapWrap}>
<button
type="button"
className={styles.swapBtn}
onClick={handleSwap}
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={rubTotal} readOnly placeholder="0" />
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyRub}`}></span>
RUB
</div>
</div>
</div>
</>
) : (
<>
<div className={styles.field}>
<div className={styles.fieldLabel}>Конвертируете</div>
<div className={styles.fieldInput}>
<input
type="text"
value={rubInputVal}
onChange={(e) => updateRubInput(e.target.value)}
placeholder="0"
inputMode="decimal"
/>
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyRub}`}></span>
RUB
</div>
</div>
{rubBelowMin && (
<div className={styles.fieldError}>
Минимальная сумма: {MIN_RUB_AMOUNT.toLocaleString('ru-RU')}
</div>
)}
{quoteRubError && !rubBelowMin && (
<div className={styles.fieldError}>
Сумма слишком большая и превышает 600 000
</div>
)}
</div>
<div className={styles.swapWrap}>
<button
type="button"
className={styles.swapBtn}
onClick={handleSwap}
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={usdtFromRub} readOnly placeholder="0" />
<div className={styles.currency}>
<span className={`${styles.currencyIcon} ${styles.currencyUsdt}`}></span>
USDT
</div>
</div>
</div>
</>
)}
</div>
<CommissionPanel
amount={displayRubAmount}
progress={progressPercent(displayRubAmount)}
commission={commission}
effectiveRate={effectiveRate}
/>
</div>
<div className={styles.bottom}>
<AgreementCheck checked={c.agreed} onToggle={() => c.setAgreed(!c.agreed)} />
</div>
<button
type="button"
className={styles.payBtn}
onClick={handlePay}
disabled={isPayDisabled}
>
{isPending ? 'Обработка...' : 'Оплатить'}
</button>
</div>
)
}