WIP: Mp 2542 vault deposit (#216)
* Change accordion interactions * finish interaction * set min width accountsummary * finish user interaction of accordion * finish deposit interaction * Fix Accordion * Refactor TokenInput * Refactor VaultModalContent * fix minor build errors
This commit is contained in:
parent
245b94b7b9
commit
173f764980
@ -1,46 +1,30 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import { Plus, Subtract } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
|
||||
import AccordionContent, { Item } from './AccordionContent'
|
||||
|
||||
interface Props {
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
interface Item {
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
open?: boolean
|
||||
allowMultipleOpen?: boolean
|
||||
}
|
||||
|
||||
export default function Accordion(props: Props) {
|
||||
if (props.allowMultipleOpen) {
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
{props.items.map((item, index) => (
|
||||
<AccordionContent key={item.title} item={item} index={index} />
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='w-full'>
|
||||
{props.items.map((item) => (
|
||||
<details
|
||||
key={item.title}
|
||||
open={item.open}
|
||||
className='group border-b-white/10 [&:not(:last-child)]:border-b'
|
||||
>
|
||||
<summary
|
||||
className={classNames(
|
||||
'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',
|
||||
'[&::marker]:hidden [&::marker]:content-[""]',
|
||||
)}
|
||||
>
|
||||
<Text>{item.title}</Text>
|
||||
<div className='block h-[14px] w-[14px] group-[[open]]:hidden'>
|
||||
<Plus />
|
||||
</div>
|
||||
<div className='hidden h-[1px] w-[14px] group-[[open]]:block'>
|
||||
<Subtract />
|
||||
</div>
|
||||
</summary>
|
||||
<div className='bg-white/5 transition-[padding]'>{item.content}</div>
|
||||
</details>
|
||||
<div className='w-full'>
|
||||
{props.items.map((item, index) => (
|
||||
<Card key={item.title} className='mb-4'>
|
||||
<AccordionContent item={item} index={index} />
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
40
src/components/AccordionContent.tsx
Normal file
40
src/components/AccordionContent.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { ChevronDown, ChevronRight } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
|
||||
interface Props {
|
||||
item: Item
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
title: string
|
||||
renderContent: () => React.ReactNode
|
||||
isOpen?: boolean
|
||||
toggleOpen: (index: number) => void
|
||||
}
|
||||
|
||||
export default function AccordionContent(props: Props) {
|
||||
return (
|
||||
<div key={props.item.title} className='group border-b-white/10 [&:not(:last-child)]:border-b'>
|
||||
<div
|
||||
onClick={() => props.item.toggleOpen(props.index)}
|
||||
className={classNames(
|
||||
'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',
|
||||
'[&::marker]:hidden [&::marker]:content-[""]',
|
||||
props.item.isOpen && 'border-b [&:first-child]:border-t-0',
|
||||
)}
|
||||
>
|
||||
<Text>{props.item.title}</Text>
|
||||
<div className='block pr-1 group-[[open]]:hidden'>
|
||||
{props.item.isOpen ? <ChevronRight /> : <ChevronDown />}
|
||||
</div>
|
||||
</div>
|
||||
{props.item.isOpen && (
|
||||
<div className='bg-white/5 transition-[padding]'>{props.item.renderContent()}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -21,7 +21,7 @@ interface Props {
|
||||
data: Account
|
||||
}
|
||||
|
||||
export const AcccountBalancesTable = (props: Props) => {
|
||||
export const AccountBalancesTable = (props: Props) => {
|
||||
const displayCurrency = useStore((s) => s.displayCurrency)
|
||||
const prices = useStore((s) => s.prices)
|
||||
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Accordion from 'components/Accordion'
|
||||
import { AcccountBalancesTable } from 'components/Account/AccountBalancesTable'
|
||||
import { AccountBalancesTable } from 'components/Account/AccountBalancesTable'
|
||||
import AccountComposition from 'components/Account/AccountComposition'
|
||||
import AccountHealth from 'components/Account/AccountHealth'
|
||||
import Card from 'components/Card'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { ArrowChartLineUp } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import useIsOpenArray from 'hooks/useIsOpenArray'
|
||||
import useStore from 'store'
|
||||
import { calculateAccountDeposits } from 'utils/accounts'
|
||||
import { BN } from 'utils/helpers'
|
||||
@ -16,13 +17,15 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function AccountSummary(props: Props) {
|
||||
const [isOpen, toggleOpen] = useIsOpenArray(2, true)
|
||||
|
||||
const prices = useStore((s) => s.prices)
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const accountBalance = props.account ? calculateAccountDeposits(props.account, prices) : BN(0)
|
||||
if (!props.account) return null
|
||||
|
||||
return (
|
||||
<div className='flex max-w-[345px] basis-[345px] flex-wrap'>
|
||||
<div className='flex min-w-[345px] basis-[345px] flex-wrap'>
|
||||
<Card className='mb-4 min-w-fit bg-white/10' contentClassName='flex'>
|
||||
<Item>
|
||||
<DisplayCurrency
|
||||
@ -44,11 +47,22 @@ export default function AccountSummary(props: Props) {
|
||||
items={[
|
||||
{
|
||||
title: `Subaccount ${props.account.id} Composition`,
|
||||
content: <AccountComposition account={props.account} change={props.change} />,
|
||||
open: true,
|
||||
renderContent: () =>
|
||||
props.account ? (
|
||||
<AccountComposition account={props.account} change={props.change} />
|
||||
) : null,
|
||||
isOpen: isOpen[0],
|
||||
toggleOpen: (index: number) => toggleOpen(index),
|
||||
},
|
||||
{
|
||||
title: 'Balances',
|
||||
renderContent: () =>
|
||||
props.account ? <AccountBalancesTable data={props.account} /> : null,
|
||||
isOpen: isOpen[1],
|
||||
toggleOpen: (index: number) => toggleOpen(index),
|
||||
},
|
||||
{ title: 'Balances', content: <AcccountBalancesTable data={props.account} /> },
|
||||
]}
|
||||
allowMultipleOpen
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 16 10">
|
||||
<path fill="currentColor" d="M0.2,0.7C0.5,0.4,1,0.4,1.3,0.7l0.1,0.1L8,7.5l6.6-6.7c0.3-0.3,0.7-0.3,1.1-0.1l0.1,0.1
|
||||
c0.3,0.3,0.3,0.8,0.1,1.1l-0.1,0.1L8.6,9.3C8.3,9.6,7.8,9.6,7.5,9.3L7.4,9.3L0.2,1.9C-0.1,1.6-0.1,1.1,0.2,0.7z"/>
|
||||
<svg width="12" height="6" viewBox="0 0 12 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 0.5L6 5.5L11 0.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 195 B |
@ -1,4 +1,3 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 10 16">
|
||||
<path fill='currentColor' d="M0.7,15.8c-0.3-0.3-0.3-0.7-0.1-1.1l0.1-0.1L7.5,8L0.7,1.4C0.4,1.1,0.4,0.7,0.7,0.3l0.1-0.1C1-0.1,1.5-0.1,1.8,0.2l0.1,0.1
|
||||
l7.3,7.2c0.3,0.3,0.3,0.7,0.1,1.1L9.3,8.6l-7.3,7.2C1.6,16.1,1.1,16.1,0.7,15.8z"/>
|
||||
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 11L5.5 6L0.5 1" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 195 B |
@ -164,6 +164,7 @@ export default function BorrowModal() {
|
||||
amount={amount}
|
||||
max={max}
|
||||
className='w-full'
|
||||
maxText='Max'
|
||||
/>
|
||||
<Divider className='my-6' />
|
||||
<Text size='lg' className='pb-2'>
|
||||
|
@ -1,138 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import AccountSummary from 'components/Account/AccountSummary'
|
||||
import { Button } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import Divider from 'components/Divider'
|
||||
import { ArrowRight } from 'components/Icons'
|
||||
import Modal from 'components/Modal'
|
||||
import Text from 'components/Text'
|
||||
import TokenInputWithSlider from 'components/TokenInputWithSlider'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useToggle from 'hooks/useToggle'
|
||||
import useStore from 'store'
|
||||
import { getAmount } from 'utils/accounts'
|
||||
import { hardcodedFee } from 'utils/contants'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
export default function FundAndWithdrawModal() {
|
||||
const currentAccount = useCurrentAccount()
|
||||
const modal = useStore<string | null>((s) => s.fundAndWithdrawModal)
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const withdraw = useStore((s) => s.withdraw)
|
||||
const deposit = useStore((s) => s.deposit)
|
||||
const [amount, setAmount] = useState(BN(0))
|
||||
const [currentAsset, setCurrentAsset] = useState(baseCurrency)
|
||||
const [change, setChange] = useState<AccountChange | undefined>()
|
||||
const [isConfirming, setIsConfirming] = useToggle()
|
||||
const balances = useStore((s) => s.balances)
|
||||
const isFunding = modal === 'fund'
|
||||
|
||||
function resetState() {
|
||||
setCurrentAsset(baseCurrency)
|
||||
setAmount(BN(0))
|
||||
setChange(undefined)
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
resetState()
|
||||
useStore.setState({ fundAndWithdrawModal: null })
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
if (!currentAccount) return
|
||||
setIsConfirming(true)
|
||||
let result
|
||||
if (isFunding) {
|
||||
result = await deposit({
|
||||
fee: hardcodedFee,
|
||||
accountId: currentAccount.id,
|
||||
coin: {
|
||||
denom: currentAsset.denom,
|
||||
amount: amount.toString(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
result = await withdraw({
|
||||
fee: hardcodedFee,
|
||||
accountId: currentAccount.id,
|
||||
coin: {
|
||||
denom: currentAsset.denom,
|
||||
amount: amount.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setIsConfirming(false)
|
||||
if (result) {
|
||||
resetState()
|
||||
useStore.setState({ fundAndWithdrawModal: null })
|
||||
}
|
||||
}
|
||||
|
||||
const max = isFunding
|
||||
? getAmount(currentAsset.denom, balances ?? [])
|
||||
: currentAccount
|
||||
? getAmount(currentAsset.denom, currentAccount.deposits)
|
||||
: BN(0)
|
||||
|
||||
useEffect(() => {
|
||||
setChange({
|
||||
deposits: [
|
||||
{
|
||||
amount: isFunding ? BN(0).plus(amount).toString() : BN(0).minus(amount).toString(),
|
||||
denom: currentAsset.denom,
|
||||
},
|
||||
],
|
||||
})
|
||||
}, [amount, currentAsset, currentAccount, isFunding])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!modal}
|
||||
onClose={onClose}
|
||||
header={
|
||||
<span className='flex items-center gap-4 px-4'>
|
||||
<Text>
|
||||
{isFunding
|
||||
? `Fund Account ${currentAccount?.id ?? '0'}`
|
||||
: `Withdraw from Account ${currentAccount?.id ?? '0'}`}
|
||||
</Text>
|
||||
</span>
|
||||
}
|
||||
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
|
||||
contentClassName='flex flex-col min-h-[400px]'
|
||||
>
|
||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||
<Card
|
||||
className='flex flex-grow bg-white/5 p-4'
|
||||
contentClassName='gap-6 flex flex-col justify-between h-full'
|
||||
>
|
||||
<TokenInputWithSlider
|
||||
asset={currentAsset}
|
||||
onChange={(val) => {
|
||||
setAmount(val)
|
||||
}}
|
||||
onChangeAsset={(asset) => {
|
||||
setCurrentAsset(asset)
|
||||
}}
|
||||
amount={amount}
|
||||
max={max}
|
||||
hasSelect
|
||||
balances={isFunding ? balances : currentAccount?.deposits ?? []}
|
||||
accountId={!isFunding ? currentAccount?.id ?? '0' : undefined}
|
||||
/>
|
||||
<Divider />
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
showProgressIndicator={isConfirming}
|
||||
className='w-full'
|
||||
text={isFunding ? 'Fund' : 'Withdraw'}
|
||||
rightIcon={<ArrowRight />}
|
||||
/>
|
||||
</Card>
|
||||
<AccountSummary account={currentAccount} change={change} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import BorrowModal from 'components/Modals/BorrowModal'
|
||||
import FundAndWithdrawModal from 'components/Modals/FundAndWithdrawModal'
|
||||
import VaultModal from 'components/Modals/VaultModal'
|
||||
import FundAndWithdrawModal from 'components/Modals/fundwithdraw/FundAndWithdrawModal'
|
||||
import VaultModal from 'components/Modals/vault/VaultModal'
|
||||
|
||||
export default function ModalsContainer() {
|
||||
return (
|
||||
|
@ -1,119 +0,0 @@
|
||||
import { BigNumber } from 'bignumber.js'
|
||||
import { useState } from 'react'
|
||||
|
||||
import AccountSummary from 'components/Account/AccountSummary'
|
||||
import { Button } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import Divider from 'components/Divider'
|
||||
import VaultLogo from 'components/Earn/vault/VaultLogo'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import { ArrowRight } from 'components/Icons'
|
||||
import Modal from 'components/Modal'
|
||||
import Slider from 'components/Slider'
|
||||
import Switch from 'components/Switch'
|
||||
import Text from 'components/Text'
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
import TokenInput from 'components/TokenInput'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useStore from 'store'
|
||||
import { getAmount } from 'utils/accounts'
|
||||
import { formatValue } from 'utils/formatters'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
export default function VaultModal() {
|
||||
const currentAccount = useCurrentAccount()
|
||||
const modal = useStore((s) => s.vaultModal)
|
||||
const [amount, setAmount] = useState(BN(0))
|
||||
const [percentage, setPercentage] = useState(0)
|
||||
const [isCustomAmount, setIsCustomAmount] = useState(false)
|
||||
|
||||
function handleSwitch() {
|
||||
setIsCustomAmount(() => !isCustomAmount)
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
useStore.setState({ vaultModal: null })
|
||||
setAmount(BN(0))
|
||||
setPercentage(0)
|
||||
}
|
||||
|
||||
function onChangeSlider(value: number) {}
|
||||
function onChangePrimary(value: BigNumber) {}
|
||||
function onChangeSecondary(value: BigNumber) {}
|
||||
|
||||
const primaryAsset =
|
||||
ASSETS.find((asset) => asset.denom === modal?.vault.denoms.primary) ?? ASSETS[0]
|
||||
const secondaryAsset =
|
||||
ASSETS.find((asset) => asset.denom === modal?.vault.denoms.secondary) ?? ASSETS[0]
|
||||
|
||||
const hasValidData = primaryAsset && currentAccount && secondaryAsset
|
||||
const maxPrimaryAmount = hasValidData
|
||||
? getAmount(primaryAsset.denom, currentAccount.deposits)
|
||||
: BN(0)
|
||||
const maxSecondaryAmount = hasValidData
|
||||
? getAmount(secondaryAsset.denom, currentAccount.deposits)
|
||||
: BN(0)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!(modal && hasValidData)}
|
||||
onClose={onClose}
|
||||
header={
|
||||
modal && (
|
||||
<span className='flex items-center gap-4 px-4'>
|
||||
<VaultLogo vault={modal.vault} />
|
||||
<Text>{`${modal.vault.symbols.primary} - ${modal.vault.symbols.secondary}`}</Text>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
|
||||
contentClassName='flex flex-col'
|
||||
>
|
||||
<div className='flex gap-3 border-b border-b-white/5 px-6 py-4 gradient-header'>
|
||||
<TitleAndSubCell
|
||||
title={formatValue(1000000, { abbreviated: true, decimals: 6 })}
|
||||
sub={'Borrowed'}
|
||||
/>
|
||||
<div className='h-100 w-[1px] bg-white/10'></div>
|
||||
<TitleAndSubCell title={`${1000} (${10000})`} sub={'Liquidity available'} />
|
||||
</div>
|
||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||
<Card
|
||||
className='flex flex-grow bg-white/5 p-4'
|
||||
contentClassName='gap-6 flex flex-col justify-between h-full'
|
||||
>
|
||||
<TokenInput
|
||||
onChange={onChangePrimary}
|
||||
amount={amount}
|
||||
max={maxPrimaryAmount}
|
||||
asset={primaryAsset}
|
||||
/>
|
||||
<Slider value={percentage} onChange={onChangeSlider} />
|
||||
<TokenInput
|
||||
onChange={onChangeSecondary}
|
||||
amount={amount}
|
||||
max={maxSecondaryAmount}
|
||||
asset={secondaryAsset}
|
||||
/>
|
||||
<Divider />
|
||||
<div className='flex justify-between'>
|
||||
<Text className='text-white/50'>Custom amount</Text>
|
||||
<Switch checked={isCustomAmount} onChange={handleSwitch} name='customAmount' />
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<Text className='text-white/50'>{`${primaryAsset.symbol}-${secondaryAsset.symbol} Position Value`}</Text>
|
||||
<FormattedNumber amount={0} options={{ prefix: '$' }} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {}}
|
||||
className='w-full'
|
||||
text='Continue'
|
||||
rightIcon={<ArrowRight />}
|
||||
/>
|
||||
</Card>
|
||||
<AccountSummary account={currentAccount} />
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
41
src/components/Modals/fundwithdraw/FundAndWithdrawModal.tsx
Normal file
41
src/components/Modals/fundwithdraw/FundAndWithdrawModal.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Modal from 'components/Modal'
|
||||
import Text from 'components/Text'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useStore from 'store'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
|
||||
import FundWithdrawModalContent from './FundWithdrawModalContent'
|
||||
|
||||
export default function FundAndWithdrawModal() {
|
||||
const currentAccount = useCurrentAccount()
|
||||
const modal = useStore<string | null>((s) => s.fundAndWithdrawModal)
|
||||
const isFunding = modal === 'fund'
|
||||
|
||||
function onClose() {
|
||||
useStore.setState({ fundAndWithdrawModal: null })
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!modal}
|
||||
onClose={onClose}
|
||||
header={
|
||||
<span className='flex items-center gap-4 px-4'>
|
||||
<Text>
|
||||
{isFunding
|
||||
? `Fund Account ${currentAccount?.id ?? '0'}`
|
||||
: `Withdraw from Account ${currentAccount?.id ?? '0'}`}
|
||||
</Text>
|
||||
</span>
|
||||
}
|
||||
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
|
||||
contentClassName='flex flex-col min-h-[400px]'
|
||||
>
|
||||
{modal && currentAccount ? (
|
||||
<FundWithdrawModalContent account={currentAccount} isFunding={isFunding} />
|
||||
) : (
|
||||
<CircularProgress />
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
113
src/components/Modals/fundwithdraw/FundWithdrawModalContent.tsx
Normal file
113
src/components/Modals/fundwithdraw/FundWithdrawModalContent.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useState } from 'react'
|
||||
|
||||
import AccountSummary from 'components/Account/AccountSummary'
|
||||
import { Button } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import Divider from 'components/Divider'
|
||||
import { ArrowRight } from 'components/Icons'
|
||||
import TokenInputWithSlider from 'components/TokenInputWithSlider'
|
||||
import useToggle from 'hooks/useToggle'
|
||||
import useStore from 'store'
|
||||
import { getAmount } from 'utils/accounts'
|
||||
import { hardcodedFee } from 'utils/contants'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
interface Props {
|
||||
account: Account
|
||||
isFunding: boolean
|
||||
}
|
||||
|
||||
export default function FundWithdrawModalContent(props: Props) {
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const withdraw = useStore((s) => s.withdraw)
|
||||
const deposit = useStore((s) => s.deposit)
|
||||
const balances = useStore((s) => s.balances)
|
||||
const [isConfirming, setIsConfirming] = useToggle()
|
||||
const [currentAsset, setCurrentAsset] = useState(baseCurrency)
|
||||
const [amount, setAmount] = useState(BN(0))
|
||||
const [change, setChange] = useState<AccountChange | undefined>()
|
||||
|
||||
const max = props.isFunding
|
||||
? getAmount(currentAsset.denom, balances ?? [])
|
||||
: props.account
|
||||
? getAmount(currentAsset.denom, props.account.deposits)
|
||||
: BN(0)
|
||||
|
||||
function onChangeAmount(val: BigNumber) {
|
||||
setAmount(val)
|
||||
setChange({
|
||||
deposits: [
|
||||
{
|
||||
amount: props.isFunding ? BN(0).plus(amount).toString() : BN(0).minus(amount).toString(),
|
||||
denom: currentAsset.denom,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
setCurrentAsset(baseCurrency)
|
||||
setAmount(BN(0))
|
||||
setChange(undefined)
|
||||
}
|
||||
|
||||
async function onConfirm() {
|
||||
setIsConfirming(true)
|
||||
let result
|
||||
if (props.isFunding) {
|
||||
result = await deposit({
|
||||
fee: hardcodedFee,
|
||||
accountId: props.account.id,
|
||||
coin: {
|
||||
denom: currentAsset.denom,
|
||||
amount: amount.toString(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
result = await withdraw({
|
||||
fee: hardcodedFee,
|
||||
accountId: props.account.id,
|
||||
coin: {
|
||||
denom: currentAsset.denom,
|
||||
amount: amount.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setIsConfirming(false)
|
||||
if (result) {
|
||||
resetState()
|
||||
useStore.setState({ fundAndWithdrawModal: null })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||
<Card
|
||||
className='flex flex-grow bg-white/5 p-4'
|
||||
contentClassName='gap-6 flex flex-col justify-between h-full'
|
||||
>
|
||||
<TokenInputWithSlider
|
||||
asset={currentAsset}
|
||||
onChange={onChangeAmount}
|
||||
onChangeAsset={setCurrentAsset}
|
||||
amount={amount}
|
||||
max={max}
|
||||
balances={props.isFunding ? balances : props.account.deposits ?? []}
|
||||
accountId={!props.isFunding ? props.account.id : undefined}
|
||||
hasSelect
|
||||
/>
|
||||
<Divider />
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
showProgressIndicator={isConfirming}
|
||||
className='w-full'
|
||||
text={props.isFunding ? 'Fund' : 'Withdraw'}
|
||||
rightIcon={<ArrowRight />}
|
||||
/>
|
||||
</Card>
|
||||
<AccountSummary account={props.account} change={change} />
|
||||
</div>
|
||||
)
|
||||
}
|
3
src/components/Modals/vault/VaultBorrowings.tsx
Normal file
3
src/components/Modals/vault/VaultBorrowings.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function VaultBorrowings() {
|
||||
return null
|
||||
}
|
134
src/components/Modals/vault/VaultDeposit.tsx
Normal file
134
src/components/Modals/vault/VaultDeposit.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from 'components/Button'
|
||||
import Divider from 'components/Divider'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import { ArrowRight } from 'components/Icons'
|
||||
import Slider from 'components/Slider'
|
||||
import Switch from 'components/Switch'
|
||||
import Text from 'components/Text'
|
||||
import TokenInput from 'components/TokenInput'
|
||||
import usePrice from 'hooks/usePrice'
|
||||
import { getAmount } from 'utils/accounts'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
interface Props {
|
||||
primaryAsset: Asset
|
||||
secondaryAsset: Asset
|
||||
account: Account
|
||||
onChangeDeposits: (deposits: Map<string, BigNumber>) => void
|
||||
toggleOpen: (index: number) => void
|
||||
}
|
||||
|
||||
export default function VaultDeposit(props: Props) {
|
||||
const [isCustomAmount, setIsCustomAmount] = useState(false)
|
||||
const [percentage, setPercentage] = useState(0)
|
||||
const [deposits, setDeposits] = useState<Map<string, BigNumber>>(new Map())
|
||||
|
||||
const availablePrimaryAmount = getAmount(props.primaryAsset.denom, props.account.deposits)
|
||||
const availableSecondaryAmount = getAmount(props.secondaryAsset.denom, props.account.deposits)
|
||||
const primaryPrice = usePrice(props.primaryAsset.denom)
|
||||
const secondaryPrice = usePrice(props.secondaryAsset.denom)
|
||||
|
||||
const maxAssetValueNonCustom = BN(
|
||||
Math.min(availablePrimaryAmount.toNumber(), availableSecondaryAmount.toNumber()),
|
||||
)
|
||||
const primaryMax = isCustomAmount
|
||||
? availablePrimaryAmount
|
||||
: maxAssetValueNonCustom.dividedBy(primaryPrice)
|
||||
const secondaryMax = isCustomAmount
|
||||
? availableSecondaryAmount
|
||||
: maxAssetValueNonCustom.dividedBy(secondaryPrice)
|
||||
|
||||
function handleSwitch() {
|
||||
const isCustomAmountNew = !isCustomAmount
|
||||
if (!isCustomAmountNew) {
|
||||
setDeposits((deposits) => {
|
||||
deposits.clear()
|
||||
return new Map(deposits)
|
||||
})
|
||||
setPercentage(0)
|
||||
}
|
||||
setIsCustomAmount(isCustomAmountNew)
|
||||
}
|
||||
|
||||
function onChangePrimaryDeposit(amount: BigNumber) {
|
||||
onChangeDeposit(props.primaryAsset.denom, amount)
|
||||
if (!isCustomAmount) {
|
||||
onChangeDeposit(
|
||||
props.secondaryAsset.denom,
|
||||
secondaryMax.multipliedBy(amount.dividedBy(primaryMax)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeSecondaryDeposit(amount: BigNumber) {
|
||||
onChangeDeposit(props.secondaryAsset.denom, amount)
|
||||
if (!isCustomAmount) {
|
||||
onChangeDeposit(
|
||||
props.primaryAsset.denom,
|
||||
primaryMax.multipliedBy(amount.dividedBy(secondaryMax)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeDeposit(denom: string, amount: BigNumber) {
|
||||
if (amount.isZero()) {
|
||||
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) {
|
||||
setPercentage(value)
|
||||
setDeposits((deposits) => {
|
||||
deposits.set(props.primaryAsset.denom, primaryMax.multipliedBy(value / 100))
|
||||
deposits.set(props.secondaryAsset.denom, secondaryMax.multipliedBy(value / 100))
|
||||
return new Map(deposits)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col justify-between gap-6 p-4'>
|
||||
<TokenInput
|
||||
onChange={onChangePrimaryDeposit}
|
||||
amount={deposits.get(props.primaryAsset.denom) ?? BN(0)}
|
||||
max={primaryMax}
|
||||
maxText='Balance'
|
||||
asset={props.primaryAsset}
|
||||
/>
|
||||
{!isCustomAmount && <Slider value={percentage} onChange={onChangeSlider} />}
|
||||
<TokenInput
|
||||
onChange={onChangeSecondaryDeposit}
|
||||
amount={deposits.get(props.secondaryAsset.denom) ?? BN(0)}
|
||||
max={secondaryMax}
|
||||
maxText='Balance'
|
||||
asset={props.secondaryAsset}
|
||||
/>
|
||||
<Divider />
|
||||
<div className='flex justify-between'>
|
||||
<Text className='text-white/50'>Custom amount</Text>
|
||||
<Switch checked={isCustomAmount} onChange={handleSwitch} name='customAmount' />
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<Text className='text-white/50'>{`${props.primaryAsset.symbol}-${props.secondaryAsset.symbol} Position Value`}</Text>
|
||||
<FormattedNumber amount={0} options={{ prefix: '$' }} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => props.toggleOpen(1)}
|
||||
className='w-full'
|
||||
text='Continue'
|
||||
rightIcon={<ArrowRight />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
53
src/components/Modals/vault/VaultModal.tsx
Normal file
53
src/components/Modals/vault/VaultModal.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import VaultLogo from 'components/Earn/vault/VaultLogo'
|
||||
import Modal from 'components/Modal'
|
||||
import Text from 'components/Text'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useStore from 'store'
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
|
||||
import VaultModalContent from './VaultModalContent'
|
||||
|
||||
export default function VaultModal() {
|
||||
const currentAccount = useCurrentAccount()
|
||||
const modal = useStore((s) => s.vaultModal)
|
||||
|
||||
const primaryAsset =
|
||||
ASSETS.find((asset) => asset.denom === modal?.vault.denoms.primary) ?? ASSETS[0]
|
||||
const secondaryAsset =
|
||||
ASSETS.find((asset) => asset.denom === modal?.vault.denoms.secondary) ?? ASSETS[0]
|
||||
|
||||
const hasValidData = primaryAsset && currentAccount && secondaryAsset
|
||||
|
||||
function onClose() {
|
||||
useStore.setState({ vaultModal: null })
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!(modal && hasValidData)}
|
||||
onClose={onClose}
|
||||
header={
|
||||
modal && (
|
||||
<span className='flex items-center gap-4 px-4'>
|
||||
<VaultLogo vault={modal.vault} />
|
||||
<Text>{`${modal.vault.symbols.primary} - ${modal.vault.symbols.secondary}`}</Text>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
|
||||
contentClassName='flex flex-col'
|
||||
>
|
||||
{modal?.vault && currentAccount ? (
|
||||
<VaultModalContent
|
||||
vault={modal.vault}
|
||||
primaryAsset={primaryAsset}
|
||||
secondaryAsset={secondaryAsset}
|
||||
account={currentAccount}
|
||||
/>
|
||||
) : (
|
||||
<CircularProgress />
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
52
src/components/Modals/vault/VaultModalContent.tsx
Normal file
52
src/components/Modals/vault/VaultModalContent.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Accordion from 'components/Accordion'
|
||||
import AccountSummary from 'components/Account/AccountSummary'
|
||||
import useIsOpenArray from 'hooks/useIsOpenArray'
|
||||
|
||||
import VaultDeposit from './VaultDeposit'
|
||||
import VaultBorrowings from './VaultBorrowings'
|
||||
|
||||
interface Props {
|
||||
vault: Vault
|
||||
primaryAsset: Asset
|
||||
secondaryAsset: Asset
|
||||
account: Account
|
||||
}
|
||||
|
||||
export default function VaultModalContent(props: Props) {
|
||||
const [isOpen, toggleOpen] = useIsOpenArray(2, false)
|
||||
const [deposits, setDeposits] = useState<Map<string, BigNumber>>(new Map())
|
||||
|
||||
return (
|
||||
<div className='flex flex-grow items-start gap-6 p-6'>
|
||||
<Accordion
|
||||
items={[
|
||||
{
|
||||
renderContent: () => (
|
||||
<VaultDeposit
|
||||
onChangeDeposits={(deposits) => setDeposits(deposits)}
|
||||
primaryAsset={props.primaryAsset}
|
||||
secondaryAsset={props.secondaryAsset}
|
||||
account={props.account}
|
||||
toggleOpen={toggleOpen}
|
||||
/>
|
||||
),
|
||||
title: 'Deposit',
|
||||
isOpen: isOpen[0],
|
||||
toggleOpen: (index: number) => toggleOpen(index),
|
||||
},
|
||||
{
|
||||
renderContent: () => <VaultBorrowings />,
|
||||
title: 'Borrow',
|
||||
isOpen: isOpen[1],
|
||||
toggleOpen: (index: number) => toggleOpen(index),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<AccountSummary account={props.account} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -2,7 +2,7 @@ import classNames from 'classnames'
|
||||
import { Suspense } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { AcccountBalancesTable } from 'components/Account/AccountBalancesTable'
|
||||
import { AccountBalancesTable } from 'components/Account/AccountBalancesTable'
|
||||
import AccountComposition from 'components/Account/AccountComposition'
|
||||
import Card from 'components/Card'
|
||||
import Loading from 'components/Loading'
|
||||
@ -35,7 +35,7 @@ function Content() {
|
||||
<Card className='h-fit w-full bg-white/5' title={`Account ${account.id}`} key={index}>
|
||||
<AccountComposition account={account} />
|
||||
<Text className='mt-3 w-full bg-white/10 px-4 py-2 text-white/40'>Balances</Text>
|
||||
<AcccountBalancesTable data={account} />
|
||||
<AccountBalancesTable data={account} />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
@ -9,8 +9,9 @@ import Select from 'components/Select/Select'
|
||||
import Text from 'components/Text'
|
||||
import { ASSETS } from 'constants/assets'
|
||||
import useStore from 'store'
|
||||
import { magnify } from 'utils/formatters'
|
||||
import { BN } from 'utils/helpers'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import { Button } from 'components/Button'
|
||||
|
||||
interface Props {
|
||||
amount: BigNumber
|
||||
@ -24,6 +25,7 @@ interface Props {
|
||||
interface SingleProps extends Props {
|
||||
asset: Asset
|
||||
max: BigNumber
|
||||
maxText: string
|
||||
hasSelect?: boolean
|
||||
onChangeAsset?: (asset: Asset, max: BigNumber) => void
|
||||
}
|
||||
@ -31,6 +33,7 @@ interface SingleProps extends Props {
|
||||
interface SelectProps extends Props {
|
||||
asset?: Asset
|
||||
max?: BigNumber
|
||||
maxText?: string
|
||||
hasSelect: boolean
|
||||
onChangeAsset: (asset: Asset, max: BigNumber) => void
|
||||
}
|
||||
@ -43,7 +46,10 @@ export default function TokenInput(props: SingleProps | SelectProps) {
|
||||
amount: '0',
|
||||
})
|
||||
|
||||
const selectedAssetDenom = props.asset ? props.asset.denom : baseCurrency.denom
|
||||
// TODO: Refactor the useEffect
|
||||
useEffect(() => {
|
||||
props.onChangeAsset && props.onChangeAsset(asset, coin ? BN(coin.amount) : BN(0))
|
||||
}, [coin, asset])
|
||||
|
||||
const updateAsset = useCallback(
|
||||
(coinDenom: string) => {
|
||||
@ -55,29 +61,11 @@ export default function TokenInput(props: SingleProps | SelectProps) {
|
||||
[props.balances, baseCurrency],
|
||||
)
|
||||
|
||||
function setDefaultAsset() {
|
||||
if (!props.balances || props.balances?.length === 0) return setAsset(baseCurrency)
|
||||
if (props.balances.length === 1) {
|
||||
const balances = props.balances ?? []
|
||||
return setAsset(ASSETS.find((asset) => asset.denom === balances[0].denom) ?? baseCurrency)
|
||||
}
|
||||
return setAsset(ASSETS.find((asset) => asset.denom === selectedAssetDenom) ?? baseCurrency)
|
||||
function onMaxBtnClick() {
|
||||
if (!props.max) return
|
||||
props.onChange(BN(props.max))
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setDefaultAsset()
|
||||
updateAsset(asset.denom)
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
props.onChangeAsset && props.onChangeAsset(asset, coin ? BN(coin.amount) : BN(0))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [coin, asset])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@ -113,14 +101,27 @@ export default function TokenInput(props: SingleProps | SelectProps) {
|
||||
</div>
|
||||
|
||||
<div className='flex'>
|
||||
<div className='flex flex-1'>
|
||||
<Text size='xs' className='text-white/50' monospace>
|
||||
{`1 ${asset.symbol} =`}
|
||||
</Text>
|
||||
<DisplayCurrency
|
||||
className='inline pl-0.5 text-xs text-white/50'
|
||||
coin={{ denom: asset.denom, amount: magnify(1, asset).toString() }}
|
||||
/>
|
||||
<div className='flex flex-1 items-center'>
|
||||
{props.max && props.maxText && (
|
||||
<>
|
||||
<Text size='xs' className='mr-1 text-white' monospace>
|
||||
{`${props.maxText}:`}
|
||||
</Text>
|
||||
<FormattedNumber
|
||||
className='mr-1 text-xs text-white/50'
|
||||
amount={props.max?.toNumber() || 0}
|
||||
options={{ decimals: asset.decimals }}
|
||||
/>
|
||||
<Button
|
||||
color='tertiary'
|
||||
className='h-4 bg-white/20 px-1.5 py-0.5 text-2xs'
|
||||
variant='transparent'
|
||||
onClick={onMaxBtnClick}
|
||||
>
|
||||
MAX
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<DisplayCurrency
|
||||
|
@ -17,6 +17,7 @@ interface Props {
|
||||
|
||||
interface SingleProps extends Props {
|
||||
max: BigNumber
|
||||
maxText: string
|
||||
asset: Asset
|
||||
hasSelect?: boolean
|
||||
onChangeAsset?: (asset: Asset) => void
|
||||
@ -24,6 +25,7 @@ interface SingleProps extends Props {
|
||||
|
||||
interface SelectProps extends Props {
|
||||
max?: BigNumber
|
||||
maxText?: string
|
||||
asset?: Asset
|
||||
onChangeAsset: (asset: Asset) => void
|
||||
hasSelect: boolean
|
||||
@ -82,6 +84,7 @@ export default function TokenInputWithSlider(props: SingleProps | SelectProps) {
|
||||
onChangeAsset={(asset: Asset, max: BigNumber) => onAssetChange(asset, max)}
|
||||
amount={amount}
|
||||
max={max}
|
||||
maxText={props.maxText || ''}
|
||||
className='mb-4'
|
||||
disabled={props.disabled}
|
||||
hasSelect={props.hasSelect}
|
||||
|
16
src/hooks/useIsOpenArray.tsx
Normal file
16
src/hooks/useIsOpenArray.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function useIsOpenArray(length: number, allowMultiple: boolean) {
|
||||
const [isOpen, setIsOpen] = useState<boolean[]>(Array.from({ length }, (_, i) => i === 0))
|
||||
|
||||
function toggleOpen(index: number) {
|
||||
setIsOpen((prev) => {
|
||||
return prev.map((_, i) => {
|
||||
if (i === index) return !prev[i]
|
||||
return allowMultiple ? prev[i] : false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return [isOpen, toggleOpen] as const
|
||||
}
|
7
src/hooks/usePrice.tsx
Normal file
7
src/hooks/usePrice.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import usePrices from './usePrices'
|
||||
|
||||
export default function usePrice(denom: string) {
|
||||
const { data: prices } = usePrices()
|
||||
|
||||
return prices.find((coin) => coin.denom === denom)?.amount ?? 0
|
||||
}
|
Loading…
Reference in New Issue
Block a user