Mp 2544 Vault deposit (#240)
* Show guages on deposit * Finish VaultDeposit * resolve PR comments
This commit is contained in:
parent
ac7e82b0a4
commit
9d09ebdf77
@ -12,29 +12,39 @@ export interface Item {
|
|||||||
title: string
|
title: string
|
||||||
renderContent: () => React.ReactNode
|
renderContent: () => React.ReactNode
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
|
subTitle?: string | React.ReactNode
|
||||||
toggleOpen: (index: number) => void
|
toggleOpen: (index: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AccordionContent(props: Props) {
|
export default function AccordionContent(props: Props) {
|
||||||
|
const { title, renderContent, isOpen, subTitle, toggleOpen } = props.item
|
||||||
|
|
||||||
|
const shouldShowSubTitle = subTitle && !isOpen
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={props.item.title} className='group border-b-white/10 [&:not(:last-child)]:border-b'>
|
<div key={title} className='group border-b-white/10 [&:not(:last-child)]:border-b'>
|
||||||
<div
|
<div
|
||||||
onClick={() => props.item.toggleOpen(props.index)}
|
onClick={() => toggleOpen(props.index)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'mb-0 flex cursor-pointer items-center justify-between border-t border-white/10 bg-white/10 p-4 text-white',
|
'mb-0 flex cursor-pointer items-center justify-between border-t border-white/10 bg-white/10 p-4 text-white',
|
||||||
'group-[&:first-child]:border-t-0 group-[[open]]:border-b',
|
'group-[&:first-child]:border-t-0 group-[[open]]:border-b',
|
||||||
'[&::marker]:hidden [&::marker]:content-[""]',
|
'[&::marker]:hidden [&::marker]:content-[""]',
|
||||||
props.item.isOpen && 'border-b [&:first-child]:border-t-0',
|
isOpen && 'border-b [&:first-child]:border-t-0',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Text>{props.item.title}</Text>
|
<div>
|
||||||
|
<Text>{title}</Text>
|
||||||
|
{shouldShowSubTitle && (
|
||||||
|
<Text size='xs' className='mt-1 text-white/60'>
|
||||||
|
{subTitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className='block pr-1 group-[[open]]:hidden'>
|
<div className='block pr-1 group-[[open]]:hidden'>
|
||||||
{props.item.isOpen ? <ChevronRight /> : <ChevronDown />}
|
{isOpen ? <ChevronDown /> : <ChevronRight />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.item.isOpen && (
|
{isOpen && <div className='bg-white/5 transition-[padding]'>{renderContent()}</div>}
|
||||||
<div className='bg-white/5 transition-[padding]'>{props.item.renderContent()}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export default function AccountDetails() {
|
|||||||
className='fixed right-4 top-[89px] w-16 rounded-base border border-white/20 bg-white/5 backdrop-blur-sticky'
|
className='fixed right-4 top-[89px] w-16 rounded-base border border-white/20 bg-white/5 backdrop-blur-sticky'
|
||||||
>
|
>
|
||||||
<div className='flex w-full flex-wrap justify-center py-4'>
|
<div className='flex w-full flex-wrap justify-center py-4'>
|
||||||
<Gauge tooltip='Health Factor' value={0.2} icon={<Heart />} />
|
<Gauge tooltip='Health Factor' percentage={20} icon={<Heart />} />
|
||||||
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
||||||
Health
|
Health
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -3,33 +3,42 @@ import { ReactElement, ReactNode } from 'react'
|
|||||||
|
|
||||||
import { Tooltip } from 'components/Tooltip'
|
import { Tooltip } from 'components/Tooltip'
|
||||||
import useStore from 'store'
|
import useStore from 'store'
|
||||||
|
import { FormattedNumber } from 'components/FormattedNumber'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tooltip: string | ReactNode
|
tooltip: string | ReactNode
|
||||||
|
strokeColor?: string
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
background?: string
|
background?: string
|
||||||
diameter?: number
|
diameter?: number
|
||||||
value: number
|
percentage: number
|
||||||
label?: string
|
labelClassName?: string
|
||||||
icon?: ReactElement
|
icon?: ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Gauge = ({
|
export const Gauge = ({
|
||||||
background = '#FFFFFF22',
|
background = '#FFFFFF22',
|
||||||
|
strokeColor,
|
||||||
|
strokeWidth = 4,
|
||||||
diameter = 40,
|
diameter = 40,
|
||||||
value = 0,
|
percentage = 0,
|
||||||
tooltip,
|
tooltip,
|
||||||
icon,
|
icon,
|
||||||
|
labelClassName,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
const enableAnimations = useStore((s) => s.enableAnimations)
|
||||||
const radius = 16
|
const radius = 16
|
||||||
const percentage = value * 100
|
|
||||||
const percentageValue = percentage > 100 ? 100 : percentage < 0 ? 0 : percentage
|
const percentageValue = percentage > 100 ? 100 : percentage < 0 ? 0 : percentage
|
||||||
const circlePercent = 100 - percentageValue
|
const circlePercent = 100 - percentageValue
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip type='info' content={tooltip}>
|
<Tooltip type='info' content={tooltip}>
|
||||||
<div className={classNames('relative', `w-${diameter / 4} h-${diameter / 4}`)}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'relative grid place-items-center',
|
||||||
|
`w-${diameter / 4} h-${diameter / 4}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox='2 -2 28 36'
|
viewBox='2 -2 28 36'
|
||||||
width={diameter}
|
width={diameter}
|
||||||
@ -37,15 +46,17 @@ export const Gauge = ({
|
|||||||
style={{ transform: 'rotate(-90deg)' }}
|
style={{ transform: 'rotate(-90deg)' }}
|
||||||
className='absolute left-0 top-0'
|
className='absolute left-0 top-0'
|
||||||
>
|
>
|
||||||
<linearGradient id='gradient'>
|
{!strokeColor && (
|
||||||
<stop stopColor='rgba(255, 160, 187)' offset='0%'></stop>
|
<linearGradient id='gradient'>
|
||||||
<stop stopColor='rgba(186, 8, 189)' offset='50%'></stop>
|
<stop stopColor='rgba(255, 160, 187)' offset='0%'></stop>
|
||||||
<stop stopColor='rgba(255, 160, 187)' offset='100%'></stop>
|
<stop stopColor='rgba(186, 8, 189)' offset='50%'></stop>
|
||||||
</linearGradient>
|
<stop stopColor='rgba(255, 160, 187)' offset='100%'></stop>
|
||||||
|
</linearGradient>
|
||||||
|
)}
|
||||||
<circle
|
<circle
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke={background}
|
stroke={background}
|
||||||
strokeWidth={4}
|
strokeWidth={strokeWidth}
|
||||||
strokeDashoffset='0'
|
strokeDashoffset='0'
|
||||||
r={radius}
|
r={radius}
|
||||||
cx={radius}
|
cx={radius}
|
||||||
@ -57,8 +68,8 @@ export const Gauge = ({
|
|||||||
cx={radius}
|
cx={radius}
|
||||||
cy={radius}
|
cy={radius}
|
||||||
fill='transparent'
|
fill='transparent'
|
||||||
stroke='url(#gradient)'
|
stroke={strokeColor ? strokeColor : `url(#gradient)`}
|
||||||
strokeWidth={5}
|
strokeWidth={strokeWidth}
|
||||||
strokeDashoffset={circlePercent}
|
strokeDashoffset={circlePercent}
|
||||||
strokeDasharray='100'
|
strokeDasharray='100'
|
||||||
pathLength='100'
|
pathLength='100'
|
||||||
@ -74,6 +85,12 @@ export const Gauge = ({
|
|||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<FormattedNumber
|
||||||
|
className={classNames(labelClassName, 'text-2xs')}
|
||||||
|
amount={Math.round(percentage)}
|
||||||
|
options={{ maxDecimals: 0, minDecimals: 0 }}
|
||||||
|
animate
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
import Button from 'components/Button'
|
import Button from 'components/Button'
|
||||||
|
import DisplayCurrency from 'components/DisplayCurrency'
|
||||||
import Divider from 'components/Divider'
|
import Divider from 'components/Divider'
|
||||||
import { FormattedNumber } from 'components/FormattedNumber'
|
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
|
||||||
import { ArrowRight } from 'components/Icons'
|
|
||||||
import Slider from 'components/Slider'
|
import Slider from 'components/Slider'
|
||||||
import Switch from 'components/Switch'
|
import Switch from 'components/Switch'
|
||||||
import Text from 'components/Text'
|
import Text from 'components/Text'
|
||||||
@ -12,89 +12,117 @@ import TokenInput from 'components/TokenInput'
|
|||||||
import usePrice from 'hooks/usePrice'
|
import usePrice from 'hooks/usePrice'
|
||||||
import { getAmount } from 'utils/accounts'
|
import { getAmount } from 'utils/accounts'
|
||||||
import { BN } from 'utils/helpers'
|
import { BN } from 'utils/helpers'
|
||||||
|
import { Gauge } from 'components/Gauge'
|
||||||
|
import useStore from 'store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
primaryAmount: BigNumber
|
||||||
|
secondaryAmount: BigNumber
|
||||||
primaryAsset: Asset
|
primaryAsset: Asset
|
||||||
secondaryAsset: Asset
|
secondaryAsset: Asset
|
||||||
account: Account
|
account: Account
|
||||||
onChangeDeposits: (deposits: Map<string, BigNumber>) => void
|
isCustomRatio: boolean
|
||||||
|
onChangeIsCustomRatio: (isCustomRatio: boolean) => void
|
||||||
|
onChangePrimaryAmount: (amount: BigNumber) => void
|
||||||
|
onChangeSecondaryAmount: (amount: BigNumber) => void
|
||||||
toggleOpen: (index: number) => void
|
toggleOpen: (index: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VaultDeposit(props: Props) {
|
export default function VaultDeposit(props: Props) {
|
||||||
const [isCustomAmount, setIsCustomAmount] = useState(false)
|
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||||
const [percentage, setPercentage] = useState(0)
|
|
||||||
const [deposits, setDeposits] = useState<Map<string, BigNumber>>(new Map())
|
|
||||||
|
|
||||||
const availablePrimaryAmount = getAmount(props.primaryAsset.denom, props.account.deposits)
|
const availablePrimaryAmount = getAmount(props.primaryAsset.denom, props.account.deposits)
|
||||||
const availableSecondaryAmount = getAmount(props.secondaryAsset.denom, props.account.deposits)
|
const availableSecondaryAmount = getAmount(props.secondaryAsset.denom, props.account.deposits)
|
||||||
const primaryPrice = usePrice(props.primaryAsset.denom)
|
const primaryPrice = usePrice(props.primaryAsset.denom)
|
||||||
const secondaryPrice = usePrice(props.secondaryAsset.denom)
|
const secondaryPrice = usePrice(props.secondaryAsset.denom)
|
||||||
|
|
||||||
const maxAssetValueNonCustom = BN(
|
const primaryValue = useMemo(
|
||||||
Math.min(availablePrimaryAmount.toNumber(), availableSecondaryAmount.toNumber()),
|
() => props.primaryAmount.times(primaryPrice),
|
||||||
|
[props.primaryAmount, primaryPrice],
|
||||||
)
|
)
|
||||||
const primaryMax = isCustomAmount
|
const secondaryValue = useMemo(
|
||||||
? availablePrimaryAmount
|
() => props.secondaryAmount.times(secondaryPrice),
|
||||||
: maxAssetValueNonCustom.dividedBy(primaryPrice)
|
[props.secondaryAmount, secondaryPrice],
|
||||||
const secondaryMax = isCustomAmount
|
)
|
||||||
? availableSecondaryAmount
|
const totalValue = useMemo(
|
||||||
: maxAssetValueNonCustom.dividedBy(secondaryPrice)
|
() => primaryValue.plus(secondaryValue),
|
||||||
|
[primaryValue, secondaryValue],
|
||||||
|
)
|
||||||
|
|
||||||
|
const primaryValuePercentage = useMemo(
|
||||||
|
() => primaryValue.div(totalValue).times(100).decimalPlaces(2).toNumber() || 50,
|
||||||
|
[primaryValue, totalValue],
|
||||||
|
)
|
||||||
|
const secondaryValuePercentage = useMemo(
|
||||||
|
() => new BigNumber(100).minus(primaryValuePercentage).decimalPlaces(2).toNumber() || 50,
|
||||||
|
[primaryValuePercentage],
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxAssetValueNonCustom = useMemo(
|
||||||
|
() =>
|
||||||
|
BN(
|
||||||
|
Math.min(
|
||||||
|
availablePrimaryAmount.times(primaryPrice).toNumber(),
|
||||||
|
availableSecondaryAmount.times(secondaryPrice).toNumber(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[availablePrimaryAmount, primaryPrice, availableSecondaryAmount, secondaryPrice],
|
||||||
|
)
|
||||||
|
const primaryMax = useMemo(
|
||||||
|
() =>
|
||||||
|
props.isCustomRatio ? availablePrimaryAmount : maxAssetValueNonCustom.dividedBy(primaryPrice),
|
||||||
|
[props.isCustomRatio, availablePrimaryAmount, primaryPrice, maxAssetValueNonCustom],
|
||||||
|
)
|
||||||
|
const secondaryMax = useMemo(
|
||||||
|
() =>
|
||||||
|
props.isCustomRatio
|
||||||
|
? availableSecondaryAmount
|
||||||
|
: maxAssetValueNonCustom.dividedBy(secondaryPrice),
|
||||||
|
[props.isCustomRatio, availableSecondaryAmount, secondaryPrice, maxAssetValueNonCustom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [percentage, setPercentage] = useState(
|
||||||
|
primaryValue.dividedBy(maxAssetValueNonCustom).times(100).decimalPlaces(0).toNumber(),
|
||||||
|
)
|
||||||
|
const disableInput =
|
||||||
|
(availablePrimaryAmount.isZero() || availableSecondaryAmount.isZero()) && !props.isCustomRatio
|
||||||
|
|
||||||
function handleSwitch() {
|
function handleSwitch() {
|
||||||
const isCustomAmountNew = !isCustomAmount
|
const isCustomRatioNew = !props.isCustomRatio
|
||||||
if (!isCustomAmountNew) {
|
if (!isCustomRatioNew) {
|
||||||
setDeposits((deposits) => {
|
props.onChangePrimaryAmount(BN(0))
|
||||||
deposits.clear()
|
props.onChangeSecondaryAmount(BN(0))
|
||||||
return new Map(deposits)
|
|
||||||
})
|
|
||||||
setPercentage(0)
|
setPercentage(0)
|
||||||
}
|
}
|
||||||
setIsCustomAmount(isCustomAmountNew)
|
props.onChangeIsCustomRatio(isCustomRatioNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangePrimaryDeposit(amount: BigNumber) {
|
function onChangePrimaryDeposit(amount: BigNumber) {
|
||||||
onChangeDeposit(props.primaryAsset.denom, amount)
|
if (amount.isGreaterThan(primaryMax)) {
|
||||||
if (!isCustomAmount) {
|
amount = primaryMax
|
||||||
onChangeDeposit(
|
}
|
||||||
props.secondaryAsset.denom,
|
props.onChangePrimaryAmount(amount)
|
||||||
secondaryMax.multipliedBy(amount.dividedBy(primaryMax)),
|
setPercentage(amount.dividedBy(primaryMax).times(100).decimalPlaces(0).toNumber())
|
||||||
)
|
if (!props.isCustomRatio) {
|
||||||
|
props.onChangeSecondaryAmount(secondaryMax.multipliedBy(amount.dividedBy(primaryMax)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSecondaryDeposit(amount: BigNumber) {
|
function onChangeSecondaryDeposit(amount: BigNumber) {
|
||||||
onChangeDeposit(props.secondaryAsset.denom, amount)
|
if (amount.isGreaterThan(secondaryMax)) {
|
||||||
if (!isCustomAmount) {
|
amount = secondaryMax
|
||||||
onChangeDeposit(
|
|
||||||
props.primaryAsset.denom,
|
|
||||||
primaryMax.multipliedBy(amount.dividedBy(secondaryMax)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
props.onChangeSecondaryAmount(amount)
|
||||||
|
setPercentage(amount.dividedBy(secondaryMax).times(100).decimalPlaces(0).toNumber())
|
||||||
function onChangeDeposit(denom: string, amount: BigNumber) {
|
if (!props.isCustomRatio) {
|
||||||
if (amount.isZero()) {
|
props.onChangePrimaryAmount(primaryMax.multipliedBy(amount.dividedBy(secondaryMax)))
|
||||||
return setDeposits((deposits) => {
|
|
||||||
deposits.delete(denom)
|
|
||||||
return new Map(deposits)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeposits((deposits) => {
|
|
||||||
deposits.set(denom, amount)
|
|
||||||
props.onChangeDeposits(deposits)
|
|
||||||
return new Map(deposits)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeSlider(value: number) {
|
function onChangeSlider(value: number) {
|
||||||
setPercentage(value)
|
setPercentage(value)
|
||||||
setDeposits((deposits) => {
|
props.onChangePrimaryAmount(primaryMax.multipliedBy(value / 100))
|
||||||
deposits.set(props.primaryAsset.denom, primaryMax.multipliedBy(value / 100))
|
props.onChangeSecondaryAmount(secondaryMax.multipliedBy(value / 100))
|
||||||
deposits.set(props.secondaryAsset.denom, secondaryMax.multipliedBy(value / 100))
|
|
||||||
return new Map(deposits)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWarningText(asset: Asset) {
|
function getWarningText(asset: Asset) {
|
||||||
@ -102,39 +130,89 @@ export default function VaultDeposit(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-full flex-col justify-between gap-6 p-4'>
|
<div className='flex flex-col'>
|
||||||
<TokenInput
|
<div className='flex gap-4 p-4'>
|
||||||
onChange={onChangePrimaryDeposit}
|
<div className='flex flex-col items-center justify-between gap-1 pb-[30px] pt-2'>
|
||||||
amount={deposits.get(props.primaryAsset.denom) ?? BN(0)}
|
<Gauge
|
||||||
max={primaryMax}
|
percentage={primaryValuePercentage}
|
||||||
maxText='Balance'
|
tooltip={`${primaryValuePercentage}% of value is ${props.primaryAsset.symbol}`}
|
||||||
asset={props.primaryAsset}
|
labelClassName='text-martian-red'
|
||||||
warning={primaryMax.isZero() ? getWarningText(props.primaryAsset) : undefined}
|
diameter={32}
|
||||||
/>
|
strokeColor='#FF645F'
|
||||||
{!isCustomAmount && <Slider value={percentage} onChange={onChangeSlider} />}
|
strokeWidth={3}
|
||||||
<TokenInput
|
/>
|
||||||
onChange={onChangeSecondaryDeposit}
|
<div className='h-full w-[1px] rounded-xl bg-white/10'></div>
|
||||||
amount={deposits.get(props.secondaryAsset.denom) ?? BN(0)}
|
<Gauge
|
||||||
max={secondaryMax}
|
percentage={secondaryValuePercentage}
|
||||||
maxText='Balance'
|
tooltip={`${secondaryValuePercentage}% of value is ${props.secondaryAsset.symbol}`}
|
||||||
asset={props.secondaryAsset}
|
labelClassName='text-martian-red'
|
||||||
warning={secondaryMax.isZero() ? getWarningText(props.secondaryAsset) : undefined}
|
diameter={32}
|
||||||
/>
|
strokeColor='#FF645F'
|
||||||
<Divider />
|
strokeWidth={3}
|
||||||
<div className='flex justify-between'>
|
/>
|
||||||
<Text className='text-white/50'>Custom amount</Text>
|
</div>
|
||||||
<Switch checked={isCustomAmount} onChange={handleSwitch} name='customAmount' />
|
<div className='flex h-full flex-grow flex-col justify-between gap-6'>
|
||||||
|
<TokenInput
|
||||||
|
onChange={onChangePrimaryDeposit}
|
||||||
|
amount={props.primaryAmount}
|
||||||
|
max={availablePrimaryAmount}
|
||||||
|
maxText='Balance'
|
||||||
|
asset={props.primaryAsset}
|
||||||
|
warning={
|
||||||
|
availablePrimaryAmount.isZero() ? getWarningText(props.primaryAsset) : undefined
|
||||||
|
}
|
||||||
|
disabled={disableInput}
|
||||||
|
/>
|
||||||
|
{!props.isCustomRatio && (
|
||||||
|
<Slider value={percentage} onChange={onChangeSlider} disabled={disableInput} />
|
||||||
|
)}
|
||||||
|
<TokenInput
|
||||||
|
onChange={onChangeSecondaryDeposit}
|
||||||
|
amount={props.secondaryAmount}
|
||||||
|
max={availableSecondaryAmount}
|
||||||
|
maxText='Balance'
|
||||||
|
asset={props.secondaryAsset}
|
||||||
|
warning={
|
||||||
|
availableSecondaryAmount.isZero() ? getWarningText(props.secondaryAsset) : undefined
|
||||||
|
}
|
||||||
|
disabled={disableInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-between'>
|
<div className='flex flex-col gap-6 p-4 pt-2'>
|
||||||
<Text className='text-white/50'>{`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Position Value`}</Text>
|
{disableInput ? (
|
||||||
<FormattedNumber amount={0} options={{ prefix: '$' }} />
|
<div>
|
||||||
|
<Divider />
|
||||||
|
<div className='flex items-center gap-4 py-4'>
|
||||||
|
<div className='w-5'>
|
||||||
|
<ExclamationMarkCircled className='w-5 gap-3 text-white' />
|
||||||
|
</div>
|
||||||
|
<Text size='xs'>
|
||||||
|
You currently have little to none of one asset. Toggle custom ratio to supply your
|
||||||
|
assets asymmetrically.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Divider />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<Text className='text-white/50'>Custom ratio</Text>
|
||||||
|
<Switch checked={props.isCustomRatio} onChange={handleSwitch} name='customRatio' />
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<Text className='text-white/50'>{`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Deposit Value`}</Text>
|
||||||
|
<DisplayCurrency coin={{ denom: baseCurrency.denom, amount: totalValue.toString() }} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => props.toggleOpen(1)}
|
||||||
|
className='w-full'
|
||||||
|
text='Continue'
|
||||||
|
rightIcon={<ArrowRight />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => props.toggleOpen(1)}
|
|
||||||
className='w-full'
|
|
||||||
text='Continue'
|
|
||||||
rightIcon={<ArrowRight />}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
49
src/components/Modals/vault/VaultDepositSubTitle.tsx
Normal file
49
src/components/Modals/vault/VaultDepositSubTitle.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import BigNumber from 'bignumber.js'
|
||||||
|
|
||||||
|
import DisplayCurrency from 'components/DisplayCurrency'
|
||||||
|
import usePrice from 'hooks/usePrice'
|
||||||
|
import useStore from 'store'
|
||||||
|
import { formatAmountWithSymbol } from 'utils/formatters'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
primaryAmount: BigNumber
|
||||||
|
secondaryAmount: BigNumber
|
||||||
|
primaryAsset: Asset
|
||||||
|
secondaryAsset: Asset
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VaultDepositSubTitle(props: Props) {
|
||||||
|
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||||
|
const primaryPrice = usePrice(props.primaryAsset.denom)
|
||||||
|
const secondaryPrice = usePrice(props.secondaryAsset.denom)
|
||||||
|
const primaryText = formatAmountWithSymbol({
|
||||||
|
denom: props.primaryAsset.denom,
|
||||||
|
amount: props.primaryAmount.toString(),
|
||||||
|
})
|
||||||
|
const secondaryText = formatAmountWithSymbol({
|
||||||
|
denom: props.secondaryAsset.denom,
|
||||||
|
amount: props.secondaryAmount.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const positionValue = props.primaryAmount
|
||||||
|
.times(primaryPrice)
|
||||||
|
.plus(props.secondaryAmount.times(secondaryPrice))
|
||||||
|
.toNumber()
|
||||||
|
|
||||||
|
const showPrimaryText = !props.primaryAmount.isZero()
|
||||||
|
const showSecondaryText = !props.secondaryAmount.isZero()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showPrimaryText && primaryText}
|
||||||
|
{showPrimaryText && showSecondaryText && ' + '}
|
||||||
|
{showSecondaryText && secondaryText}
|
||||||
|
{(showPrimaryText || showSecondaryText) && (
|
||||||
|
<>
|
||||||
|
{` = `}
|
||||||
|
<DisplayCurrency coin={{ denom: baseCurrency.denom, amount: positionValue.toString() }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import { useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
import Accordion from 'components/Accordion'
|
import Accordion from 'components/Accordion'
|
||||||
import AccountSummary from 'components/Account/AccountSummary'
|
import AccountSummary from 'components/Account/AccountSummary'
|
||||||
|
import VaultBorrowings from 'components/Modals/vault/VaultBorrowings'
|
||||||
|
import VaultDeposit from 'components/Modals/vault/VaultDeposit'
|
||||||
|
import VaultDepositSubTitle from 'components/Modals/vault/VaultDepositSubTitle'
|
||||||
import useIsOpenArray from 'hooks/useIsOpenArray'
|
import useIsOpenArray from 'hooks/useIsOpenArray'
|
||||||
|
import { BN } from 'utils/helpers'
|
||||||
import VaultDeposit from './VaultDeposit'
|
|
||||||
import VaultBorrowings from './VaultBorrowings'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vault: Vault
|
vault: Vault
|
||||||
@ -17,7 +18,23 @@ interface Props {
|
|||||||
|
|
||||||
export default function VaultModalContent(props: Props) {
|
export default function VaultModalContent(props: Props) {
|
||||||
const [isOpen, toggleOpen] = useIsOpenArray(2, false)
|
const [isOpen, toggleOpen] = useIsOpenArray(2, false)
|
||||||
const [deposits, setDeposits] = useState<Map<string, BigNumber>>(new Map())
|
const [primaryAmount, setPrimaryAmount] = useState<BigNumber>(BN(0))
|
||||||
|
const [secondaryAmount, setSecondaryAmount] = useState<BigNumber>(BN(0))
|
||||||
|
const [isCustomRatio, setIsCustomRatio] = useState(false)
|
||||||
|
|
||||||
|
const onChangePrimaryAmount = useCallback(
|
||||||
|
(amount: BigNumber) => setPrimaryAmount(amount),
|
||||||
|
[setPrimaryAmount],
|
||||||
|
)
|
||||||
|
const onChangeSecondaryAmount = useCallback(
|
||||||
|
(amount: BigNumber) => setSecondaryAmount(amount),
|
||||||
|
[setSecondaryAmount],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeIsCustomRatio = useCallback(
|
||||||
|
(isCustomRatio: boolean) => setIsCustomRatio(isCustomRatio),
|
||||||
|
[setIsCustomRatio],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||||
@ -26,14 +43,27 @@ export default function VaultModalContent(props: Props) {
|
|||||||
{
|
{
|
||||||
renderContent: () => (
|
renderContent: () => (
|
||||||
<VaultDeposit
|
<VaultDeposit
|
||||||
onChangeDeposits={(deposits) => setDeposits(deposits)}
|
primaryAmount={primaryAmount}
|
||||||
|
secondaryAmount={secondaryAmount}
|
||||||
|
onChangePrimaryAmount={onChangePrimaryAmount}
|
||||||
|
onChangeSecondaryAmount={onChangeSecondaryAmount}
|
||||||
primaryAsset={props.primaryAsset}
|
primaryAsset={props.primaryAsset}
|
||||||
secondaryAsset={props.secondaryAsset}
|
secondaryAsset={props.secondaryAsset}
|
||||||
account={props.account}
|
account={props.account}
|
||||||
toggleOpen={toggleOpen}
|
toggleOpen={toggleOpen}
|
||||||
|
isCustomRatio={isCustomRatio}
|
||||||
|
onChangeIsCustomRatio={onChangeIsCustomRatio}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
title: 'Deposit',
|
title: 'Deposit',
|
||||||
|
subTitle: (
|
||||||
|
<VaultDepositSubTitle
|
||||||
|
primaryAmount={primaryAmount}
|
||||||
|
secondaryAmount={secondaryAmount}
|
||||||
|
primaryAsset={props.primaryAsset}
|
||||||
|
secondaryAsset={props.secondaryAsset}
|
||||||
|
/>
|
||||||
|
),
|
||||||
isOpen: isOpen[0],
|
isOpen: isOpen[0],
|
||||||
toggleOpen: (index: number) => toggleOpen(index),
|
toggleOpen: (index: number) => toggleOpen(index),
|
||||||
},
|
},
|
||||||
|
@ -25,16 +25,18 @@ interface Props {
|
|||||||
export default function NumberInput(props: Props) {
|
export default function NumberInput(props: Props) {
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
const cursorRef = React.useRef(0)
|
const cursorRef = React.useRef(0)
|
||||||
// const max = props.max ? demagnify(props.max, props.asset) : undefined
|
|
||||||
|
|
||||||
const [formattedAmount, setFormattedAmount] = useState(
|
const [formattedAmount, setFormattedAmount] = useState(
|
||||||
props.amount.shiftedBy(-1 * props.asset.decimals).toString(),
|
props.amount.shiftedBy(-1 * props.asset.decimals).toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (props.amount.isZero()) return setFormattedAmount('')
|
||||||
|
|
||||||
setFormattedAmount(
|
setFormattedAmount(
|
||||||
formatValue(props.amount.toNumber(), {
|
formatValue(props.amount.toNumber(), {
|
||||||
decimals: props.asset.decimals,
|
decimals: props.asset.decimals,
|
||||||
|
minDecimals: 0,
|
||||||
maxDecimals: props.asset.decimals,
|
maxDecimals: props.asset.decimals,
|
||||||
thousandSeparator: false,
|
thousandSeparator: false,
|
||||||
}),
|
}),
|
||||||
@ -95,6 +97,11 @@ export default function NumberInput(props: Props) {
|
|||||||
const isTooLong = props.maxLength !== undefined && numberCount > props.maxLength
|
const isTooLong = props.maxLength !== undefined && numberCount > props.maxLength
|
||||||
const exceedsMaxDecimals = props.maxDecimals !== undefined && decimals > props.maxDecimals
|
const exceedsMaxDecimals = props.maxDecimals !== undefined && decimals > props.maxDecimals
|
||||||
|
|
||||||
|
if (formattedAmount === '') {
|
||||||
|
updateValues('0', BN(0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isNegative && !props.allowNegative) return
|
if (isNegative && !props.allowNegative) return
|
||||||
|
|
||||||
if (isSeparator && formattedAmount.length === 1) {
|
if (isSeparator && formattedAmount.length === 1) {
|
||||||
@ -140,7 +147,7 @@ export default function NumberInput(props: Props) {
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type='text'
|
type='text'
|
||||||
value={formattedAmount === '0' ? '' : formattedAmount}
|
value={formattedAmount === '0' ? '0' : formattedAmount}
|
||||||
onFocus={onInputFocus}
|
onFocus={onInputFocus}
|
||||||
onChange={(e) => onInputChange(e.target.value)}
|
onChange={(e) => onInputChange(e.target.value)}
|
||||||
onBlur={props.onBlur}
|
onBlur={props.onBlur}
|
||||||
|
@ -83,7 +83,7 @@ export default function Slider(props: Props) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'relative min-h-3 w-full transition-opacity',
|
'relative min-h-3 w-full transition-opacity',
|
||||||
props.className,
|
props.className,
|
||||||
props.disabled && 'pointer-events-none opacity-50',
|
props.disabled && 'pointer-events-none',
|
||||||
)}
|
)}
|
||||||
onMouseEnter={handleSliderRect}
|
onMouseEnter={handleSliderRect}
|
||||||
>
|
>
|
||||||
@ -95,15 +95,40 @@ export default function Slider(props: Props) {
|
|||||||
className='absolute z-2 w-full cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none'
|
className='absolute z-2 w-full cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none'
|
||||||
/>
|
/>
|
||||||
<div className='absolute flex w-full items-center gap-1'>
|
<div className='absolute flex w-full items-center gap-1'>
|
||||||
<Mark onClick={props.onChange} value={0} sliderValue={props.value} />
|
<Mark
|
||||||
|
onClick={props.onChange}
|
||||||
|
value={0}
|
||||||
|
sliderValue={props.value}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
<Track maxValue={23} sliderValue={props.value} />
|
<Track maxValue={23} sliderValue={props.value} />
|
||||||
<Mark onClick={props.onChange} value={25} sliderValue={props.value} />
|
<Mark
|
||||||
|
onClick={props.onChange}
|
||||||
|
value={25}
|
||||||
|
sliderValue={props.value}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
<Track maxValue={48} sliderValue={props.value} />
|
<Track maxValue={48} sliderValue={props.value} />
|
||||||
<Mark onClick={props.onChange} value={50} sliderValue={props.value} />
|
<Mark
|
||||||
|
onClick={props.onChange}
|
||||||
|
value={50}
|
||||||
|
sliderValue={props.value}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
<Track maxValue={73} sliderValue={props.value} />
|
<Track maxValue={73} sliderValue={props.value} />
|
||||||
<Mark onClick={props.onChange} value={75} sliderValue={props.value} />
|
<Mark
|
||||||
|
onClick={props.onChange}
|
||||||
|
value={75}
|
||||||
|
sliderValue={props.value}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
<Track maxValue={98} sliderValue={props.value} />
|
<Track maxValue={98} sliderValue={props.value} />
|
||||||
<Mark onClick={props.onChange} value={100} sliderValue={props.value} />
|
<Mark
|
||||||
|
onClick={props.onChange}
|
||||||
|
value={100}
|
||||||
|
sliderValue={props.value}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}>
|
<div onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}>
|
||||||
<DraggableElement
|
<DraggableElement
|
||||||
@ -139,6 +164,7 @@ interface MarkProps {
|
|||||||
value: number
|
value: number
|
||||||
sliderValue: number
|
sliderValue: number
|
||||||
onClick: (value: number) => void
|
onClick: (value: number) => void
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function Mark(props: MarkProps) {
|
function Mark(props: MarkProps) {
|
||||||
@ -148,6 +174,7 @@ function Mark(props: MarkProps) {
|
|||||||
className={`z-20 h-3 w-3 rotate-45 rounded-xs border-[1px] border-white/20 hover:border-[2px] hover:border-white ${
|
className={`z-20 h-3 w-3 rotate-45 rounded-xs border-[1px] border-white/20 hover:border-[2px] hover:border-white ${
|
||||||
props.sliderValue >= props.value ? 'bg-martian-red hover:border-white' : 'bg-grey-medium'
|
props.sliderValue >= props.value ? 'bg-martian-red hover:border-white' : 'bg-grey-medium'
|
||||||
}`}
|
}`}
|
||||||
|
disabled={props.disabled}
|
||||||
></button>
|
></button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,7 @@ export default function TokenInput(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid='token-input-component'
|
data-testid='token-input-component'
|
||||||
className={classNames(
|
className={classNames('flex w-full flex-col gap-2 transition-opacity', props.className)}
|
||||||
'flex w-full flex-col gap-2 transition-opacity',
|
|
||||||
props.className,
|
|
||||||
props.disabled && 'pointer-events-none opacity-50',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid='token-input-wrapper'
|
data-testid='token-input-wrapper'
|
||||||
@ -109,9 +105,10 @@ export default function TokenInput(props: Props) {
|
|||||||
<Button
|
<Button
|
||||||
dataTestId='token-input-max-button'
|
dataTestId='token-input-max-button'
|
||||||
color='tertiary'
|
color='tertiary'
|
||||||
className='h-4 bg-white/20 px-1.5 py-0.5 text-2xs'
|
className='!h-4 !min-h-0 bg-white/20 !px-2 !py-0.5 text-2xs'
|
||||||
variant='transparent'
|
variant='transparent'
|
||||||
onClick={onMaxBtnClick}
|
onClick={onMaxBtnClick}
|
||||||
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
MAX
|
MAX
|
||||||
</Button>
|
</Button>
|
||||||
|
Loading…
Reference in New Issue
Block a user