Mp 1671 header (#59)
* MP-1674: replaced the logo and added dekstop only nav * MP-1677: borrowCapacity implemented into the SubAccount Nav * MP-1677: adjusted the SubAccount navigation * M1677: fixed the button and SearchInput component * MP-1674: fixed the NavLink component * MP-1674: fixed the SubAccount navigation * tidy: cleaning up the trading view * MP-1674: added withdraw and funding functions * MP-1674: worked on the AccountStats * MP-1671: modal work * MP-1647: improvised CreditAccount expander * tidy: fixed the page structure * MP-1758: finished the SearchInput layout * MP-1759: updated the semicircle graphs * MP-1759: SemiCircle to Gauge * fix: implemented animated numbers * tidy: refactor * MP-1759: added Tooltip to the Gauge * fix: replace animate={true} with animate * fix: fixed the Gauge timing * fix: updated the BorrowCapacity styles * fix: renamed SubAccount to Account * fix: Text should not be a button, Button should be * tidy: format * fix: Text no clicky * fix: replaced all the Text appearances with onClick
80
components/Account/AccountStatus.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import { BorrowCapacity } from 'components/BorrowCapacity'
|
||||
import Button from 'components/Button'
|
||||
import FormattedNumber from 'components/FormattedNumber'
|
||||
import Gauge from 'components/Gauge'
|
||||
import Text from 'components/Text'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useCreditAccounts from 'hooks/useCreditAccounts'
|
||||
import { chain } from 'utils/chains'
|
||||
import { formatValue } from 'utils/formatters'
|
||||
|
||||
interface Props {
|
||||
createCreditAccount: () => void
|
||||
}
|
||||
|
||||
const AccountStatus = ({ createCreditAccount }: Props) => {
|
||||
const accountStats = useAccountStats()
|
||||
const { data: creditAccountsList, isLoading: isLoadingCreditAccounts } = useCreditAccounts()
|
||||
const hasCreditAccounts = creditAccountsList && creditAccountsList.length > 0
|
||||
|
||||
if (!hasCreditAccounts) {
|
||||
return (
|
||||
<Button className='my-3 mr-6' onClick={() => createCreditAccount()}>
|
||||
Create Credit Account
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-[400px] items-center justify-between gap-3 border-l border-l-white/20 px-3 py-3'>
|
||||
{accountStats && (
|
||||
<>
|
||||
<Text size='sm' className='flex flex-grow text-white'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
animate
|
||||
prefix='$: '
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Gauge
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Leverage:{' '}
|
||||
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
|
||||
<br />
|
||||
Max Leverage: {formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
<Gauge
|
||||
value={accountStats.risk}
|
||||
label='Risk'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Risk: {formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<BorrowCapacity
|
||||
limit={80}
|
||||
max={100}
|
||||
balance={100 - accountStats.health * 100}
|
||||
barHeight='16px'
|
||||
hideValues={true}
|
||||
showTitle={false}
|
||||
className='w-[140px]'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default AccountStatus
|
213
components/Account/FundAccountModal.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import { Switch } from '@headlessui/react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import MarsProtocolLogo from 'components/Icons/mars-protocol.svg'
|
||||
import Modal from 'components/Modal'
|
||||
import Slider from 'components/Slider'
|
||||
import Text from 'components/Text'
|
||||
import useDepositCreditAccount from 'hooks/mutations/useDepositCreditAccount'
|
||||
import useAllBalances from 'hooks/useAllBalances'
|
||||
import useAllowedCoins from 'hooks/useAllowedCoins'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
const FundAccountModal = ({ open, setOpen }: Props) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
const [selectedToken, setSelectedToken] = useState('')
|
||||
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
|
||||
const [lendAssets, setLendAssets] = useLocalStorageState(`lendAssets_${selectedAccount}`, {
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
const { data: balancesData } = useAllBalances()
|
||||
const { data: allowedCoinsData, isLoading: isLoadingAllowedCoins } = useAllowedCoins()
|
||||
const { mutate, isLoading } = useDepositCreditAccount(
|
||||
selectedAccount || '',
|
||||
selectedToken,
|
||||
BigNumber(amount)
|
||||
.times(10 ** getTokenDecimals(selectedToken))
|
||||
.toNumber(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAmount(0)
|
||||
toast.success(`${amount} ${getTokenSymbol(selectedToken)} successfully Deposited`)
|
||||
setOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (allowedCoinsData && allowedCoinsData.length > 0) {
|
||||
// initialize selected token when allowedCoins fetch data is available
|
||||
setSelectedToken(allowedCoinsData[0])
|
||||
}
|
||||
}, [allowedCoinsData])
|
||||
|
||||
const walletAmount = useMemo(() => {
|
||||
if (!selectedToken) return 0
|
||||
|
||||
return BigNumber(balancesData?.find((balance) => balance.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** getTokenDecimals(selectedToken))
|
||||
.toNumber()
|
||||
}, [balancesData, selectedToken])
|
||||
|
||||
const handleValueChange = (value: number) => {
|
||||
if (value > walletAmount) {
|
||||
setAmount(walletAmount)
|
||||
return
|
||||
}
|
||||
|
||||
setAmount(value)
|
||||
}
|
||||
|
||||
const maxValue = walletAmount
|
||||
const percentageValue = isNaN(amount) ? 0 : (amount * 100) / maxValue
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<div className='flex min-h-[520px] w-full'>
|
||||
{isLoading && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col items-start justify-between bg-fund-modal bg-cover p-6'>
|
||||
<div>
|
||||
<Text size='2xs' uppercase={true} className='mb-3 text-white'>
|
||||
About
|
||||
</Text>
|
||||
<Text size='xl' className='mb-4 text-white'>
|
||||
Bringing the next generation of video creation to the Metaverse.
|
||||
<br />
|
||||
Powered by deep-learning.
|
||||
</Text>
|
||||
</div>
|
||||
<div className='w-[153px] text-white'>
|
||||
<MarsProtocolLogo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col p-6'>
|
||||
<Text size='xl' uppercase={true} className='mb-4 text-white'>
|
||||
Account {selectedAccount}
|
||||
</Text>
|
||||
<div className='p-3" mb-2'>
|
||||
<Text size='sm' uppercase={true} className='mb-1 text-white'>
|
||||
Fund Account
|
||||
</Text>
|
||||
<Text size='sm' className='mb-6 text-white/60'>
|
||||
Transfer assets from your injective wallet to your Mars credit account. If you don’t
|
||||
have any assets in your injective wallet use the injective bridge to transfer funds to
|
||||
your injective wallet.
|
||||
</Text>
|
||||
{isLoadingAllowedCoins ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<>
|
||||
<div className='mb-4 rounded-md border border-white/20'>
|
||||
<div className='mb-1 flex justify-between border-b border-white/20 p-2'>
|
||||
<Text size='sm' className='text-white'>
|
||||
Asset:
|
||||
</Text>
|
||||
<select
|
||||
className='bg-transparent text-white outline-0'
|
||||
onChange={(e) => {
|
||||
setSelectedToken(e.target.value)
|
||||
|
||||
if (e.target.value !== selectedToken) setAmount(0)
|
||||
}}
|
||||
value={selectedToken}
|
||||
>
|
||||
{allowedCoinsData?.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{getTokenSymbol(entry)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className='flex justify-between p-2'>
|
||||
<Text size='sm' className='text-white'>
|
||||
Amount:
|
||||
</Text>
|
||||
<input
|
||||
type='number'
|
||||
className='appearance-none bg-transparent text-right text-white'
|
||||
value={amount}
|
||||
min='0'
|
||||
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value === '') setAmount(0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Text size='xs' uppercase={true} className='mb-2 text-white/60'>
|
||||
{`In wallet: ${walletAmount.toLocaleString()} ${getTokenSymbol(selectedToken)}`}
|
||||
</Text>
|
||||
<Slider
|
||||
className='mb-6'
|
||||
value={percentageValue}
|
||||
onChange={(value) => {
|
||||
const decimal = value[0] / 100
|
||||
const tokenDecimals = getTokenDecimals(selectedToken)
|
||||
// limit decimal precision based on token contract decimals
|
||||
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
|
||||
|
||||
setAmount(newAmount)
|
||||
}}
|
||||
onMaxClick={() => setAmount(maxValue)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div>
|
||||
<Text size='sm' uppercase={true} className='mb-1 text-white'>
|
||||
Lending Assets
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/60'>
|
||||
Lend assets from account to earn yield.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={lendAssets}
|
||||
onChange={setLendAssets}
|
||||
className={`${
|
||||
lendAssets ? 'bg-blue-600' : 'bg-gray-400'
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
lendAssets ? 'translate-x-6' : 'translate-x-1'
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<Button
|
||||
className='mt-auto w-full'
|
||||
onClick={() => mutate()}
|
||||
disabled={amount === 0 || !amount}
|
||||
>
|
||||
Fund Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default FundAccountModal
|
191
components/Account/SubAccountNavigation.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
|
||||
import { UseMutateFunction } from '@tanstack/react-query'
|
||||
import classNames from 'classnames'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { FundAccountModal, WithdrawModal } from 'components/Account'
|
||||
import Button from 'components/Button'
|
||||
import ArrowDown from 'components/Icons/arrow-down.svg'
|
||||
import ArrowUp from 'components/Icons/arrow-up.svg'
|
||||
import ChevronDownIcon from 'components/Icons/expand.svg'
|
||||
import Overlay from 'components/Overlay'
|
||||
import Text from 'components/Text'
|
||||
|
||||
interface Props {
|
||||
creditAccountsList: string[]
|
||||
selectedAccount: string | null
|
||||
deleteCreditAccount: UseMutateFunction<ExecuteResult | undefined, Error, void, unknown>
|
||||
setSelectedAccount: (id: string) => void
|
||||
createCreditAccount: () => void
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_CREDIT_ACCOUNTS = 5
|
||||
|
||||
const SubAccountNavigation = ({
|
||||
creditAccountsList,
|
||||
createCreditAccount,
|
||||
deleteCreditAccount,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
}: Props) => {
|
||||
const { firstCreditAccounts, restCreditAccounts } = useMemo(() => {
|
||||
return {
|
||||
firstCreditAccounts: creditAccountsList?.slice(0, MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
||||
restCreditAccounts: creditAccountsList?.slice(MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
||||
}
|
||||
}, [creditAccountsList])
|
||||
|
||||
const [showManageMenu, setShowManageMenu] = useState(false)
|
||||
const [showMoreMenu, setShowMoreMenu] = useState(false)
|
||||
const [showFundWalletModal, setShowFundWalletModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{firstCreditAccounts.map((account) => (
|
||||
<Button
|
||||
key={account}
|
||||
className={classNames(
|
||||
'cursor-pointer whitespace-nowrap px-4 text-base hover:text-white',
|
||||
selectedAccount === account ? 'text-white' : ' text-white/40',
|
||||
)}
|
||||
variant='text'
|
||||
onClick={() => setSelectedAccount(account)}
|
||||
>
|
||||
Account {account}
|
||||
</Button>
|
||||
))}
|
||||
<div className='relative'>
|
||||
{restCreditAccounts.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
className='flex items-center px-3 py-3 text-base hover:text-white'
|
||||
variant='text'
|
||||
onClick={() => setShowMoreMenu(!showMoreMenu)}
|
||||
>
|
||||
More
|
||||
<span className='ml-1 inline-block w-3'>
|
||||
<ChevronDownIcon />
|
||||
</span>
|
||||
</Button>
|
||||
<Overlay show={showMoreMenu} setShow={setShowMoreMenu}>
|
||||
<div className='flex w-[120px] flex-wrap p-4'>
|
||||
{restCreditAccounts.map((account) => (
|
||||
<Button
|
||||
key={account}
|
||||
variant='text'
|
||||
className={classNames(
|
||||
'w-full whitespace-nowrap py-2 text-sm',
|
||||
selectedAccount === account
|
||||
? 'text-secondary'
|
||||
: 'cursor-pointer text-accent-dark hover:text-secondary',
|
||||
)}
|
||||
onClick={() => {
|
||||
setShowMoreMenu(!showMoreMenu)
|
||||
setSelectedAccount(account)
|
||||
}}
|
||||
>
|
||||
Account {account}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Overlay>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<Button
|
||||
className={classNames(
|
||||
'flex items-center px-3 py-3 text-base hover:text-white',
|
||||
showManageMenu ? 'text-white' : 'text-white/40',
|
||||
)}
|
||||
onClick={() => setShowManageMenu(!showManageMenu)}
|
||||
variant='text'
|
||||
>
|
||||
Manage
|
||||
<span className='ml-1 inline-block w-3'>
|
||||
<ChevronDownIcon />
|
||||
</span>
|
||||
</Button>
|
||||
<Overlay className='-left-[86px]' show={showManageMenu} setShow={setShowManageMenu}>
|
||||
<div className='flex w-[274px] flex-wrap'>
|
||||
<Text
|
||||
size='sm'
|
||||
uppercase={true}
|
||||
className='w-full px-4 pt-4 text-center text-accent-dark'
|
||||
>
|
||||
Manage
|
||||
</Text>
|
||||
<div className='flex w-full justify-between border-b border-b-black/10 p-4'>
|
||||
<Button
|
||||
className='flex w-[115px] items-center justify-center pl-0 pr-2'
|
||||
onClick={() => {
|
||||
setShowFundWalletModal(true)
|
||||
setShowManageMenu(!showManageMenu)
|
||||
}}
|
||||
>
|
||||
<span className='mr-1 w-3'>
|
||||
<ArrowUp />
|
||||
</span>
|
||||
Fund
|
||||
</Button>
|
||||
<Button
|
||||
className='flex w-[115px] items-center justify-center pl-0 pr-2'
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
setShowWithdrawModal(true)
|
||||
setShowManageMenu(!showManageMenu)
|
||||
}}
|
||||
>
|
||||
<span className='mr-1 w-3'>
|
||||
<ArrowDown />
|
||||
</span>
|
||||
Withdraw
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap p-4'>
|
||||
<Button
|
||||
className='w-full cursor-pointer whitespace-nowrap py-2 text-left text-sm text-accent-dark hover:text-secondary'
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowManageMenu(!showManageMenu)
|
||||
createCreditAccount()
|
||||
}}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
<Button
|
||||
className='w-full cursor-pointer whitespace-nowrap py-2 text-left text-sm text-accent-dark hover:text-secondary'
|
||||
variant='text'
|
||||
onClick={() => {
|
||||
setShowManageMenu(!showManageMenu)
|
||||
deleteCreditAccount()
|
||||
}}
|
||||
>
|
||||
Close Account
|
||||
</Button>
|
||||
<Button
|
||||
className='w-full cursor-pointer whitespace-nowrap py-2 text-left text-sm text-accent-dark hover:text-secondary'
|
||||
variant='text'
|
||||
onClick={() => alert('TODO')}
|
||||
>
|
||||
Transfer Balance
|
||||
</Button>
|
||||
<Button
|
||||
className='w-full cursor-pointer whitespace-nowrap py-2 text-left text-sm text-accent-dark hover:text-secondary'
|
||||
variant='text'
|
||||
onClick={() => alert('TODO')}
|
||||
>
|
||||
Rearrange
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
<FundAccountModal open={showFundWalletModal} setOpen={setShowFundWalletModal} />
|
||||
<WithdrawModal open={showWithdrawModal} setOpen={setShowWithdrawModal} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubAccountNavigation
|
428
components/Account/WithdrawModal.tsx
Normal file
@ -0,0 +1,428 @@
|
||||
import { Switch } from '@headlessui/react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classNames from 'classnames'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { BorrowCapacity } from 'components/BorrowCapacity'
|
||||
import Button from 'components/Button'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import FormattedNumber from 'components/FormattedNumber'
|
||||
import Gauge from 'components/Gauge'
|
||||
import Modal from 'components/Modal'
|
||||
import Slider from 'components/Slider'
|
||||
import Text from 'components/Text'
|
||||
import useWithdrawFunds from 'hooks/mutations/useWithdrawFunds'
|
||||
import useAccountStats, { AccountStatsAction } from 'hooks/useAccountStats'
|
||||
import useAllBalances from 'hooks/useAllBalances'
|
||||
import useCalculateMaxWithdrawAmount from 'hooks/useCalculateMaxWithdrawAmount'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { chain } from 'utils/chains'
|
||||
import { formatValue } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
interface Props {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
const WithdrawModal = ({ open, setOpen }: Props) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
const [selectedToken, setSelectedToken] = useState('')
|
||||
const [isBorrowEnabled, setIsBorrowEnabled] = useState(false)
|
||||
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||
selectedAccount ?? '',
|
||||
)
|
||||
|
||||
const { data: balancesData } = useAllBalances()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: marketsData } = useMarkets()
|
||||
|
||||
const selectedTokenSymbol = getTokenSymbol(selectedToken)
|
||||
const selectedTokenDecimals = getTokenDecimals(selectedToken)
|
||||
|
||||
const tokenAmountInCreditAccount = useMemo(() => {
|
||||
return BigNumber(positionsData?.coins.find((coin) => coin.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
}, [positionsData, selectedTokenDecimals, selectedToken])
|
||||
|
||||
const { actions, borrowAmount, withdrawAmount } = useMemo(() => {
|
||||
const borrowAmount =
|
||||
amount > tokenAmountInCreditAccount
|
||||
? BigNumber(amount)
|
||||
.minus(tokenAmountInCreditAccount)
|
||||
.times(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
: 0
|
||||
|
||||
const withdrawAmount = BigNumber(amount)
|
||||
.times(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
|
||||
return {
|
||||
borrowAmount,
|
||||
withdrawAmount,
|
||||
actions: [
|
||||
{
|
||||
type: 'borrow',
|
||||
amount: borrowAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
{
|
||||
type: 'withdraw',
|
||||
amount: withdrawAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
] as AccountStatsAction[],
|
||||
}
|
||||
}, [amount, selectedToken, selectedTokenDecimals, tokenAmountInCreditAccount])
|
||||
|
||||
const accountStats = useAccountStats(actions)
|
||||
|
||||
const { mutate, isLoading } = useWithdrawFunds(withdrawAmount, borrowAmount, selectedToken, {
|
||||
onSuccess: () => {
|
||||
setOpen(false)
|
||||
toast.success(`${amount} ${selectedTokenSymbol} successfully withdrawn`)
|
||||
},
|
||||
})
|
||||
|
||||
const maxWithdrawAmount = useCalculateMaxWithdrawAmount(selectedToken, isBorrowEnabled)
|
||||
|
||||
const walletAmount = useMemo(() => {
|
||||
if (!selectedToken) return 0
|
||||
|
||||
return BigNumber(balancesData?.find((balance) => balance.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
}, [balancesData, selectedToken, selectedTokenDecimals])
|
||||
|
||||
useEffect(() => {
|
||||
if (positionsData && positionsData.coins.length > 0) {
|
||||
// initialize selected token when allowedCoins fetch data is available
|
||||
setSelectedToken(positionsData.coins[0].denom)
|
||||
}
|
||||
}, [positionsData])
|
||||
|
||||
const handleTokenChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedToken(e.target.value)
|
||||
|
||||
if (e.target.value !== selectedToken) setAmount(0)
|
||||
}
|
||||
|
||||
const handleValueChange = (value: number) => {
|
||||
if (value > maxWithdrawAmount) {
|
||||
setAmount(maxWithdrawAmount)
|
||||
return
|
||||
}
|
||||
|
||||
setAmount(value)
|
||||
}
|
||||
|
||||
const handleBorrowChange = () => {
|
||||
setIsBorrowEnabled((c) => !c)
|
||||
// reset amount due to max value calculations changing depending on wheter the user is borrowing or not
|
||||
setAmount(0)
|
||||
}
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
|
||||
return (
|
||||
BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.toNumber() * tokenPrices[denom]
|
||||
)
|
||||
}
|
||||
|
||||
const percentageValue = useMemo(() => {
|
||||
if (isNaN(amount) || maxWithdrawAmount === 0) return 0
|
||||
|
||||
return (amount * 100) / maxWithdrawAmount
|
||||
}, [amount, maxWithdrawAmount])
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<div className='flex min-h-[470px] w-full flex-wrap'>
|
||||
{isLoading && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Text
|
||||
className='flex w-full border-b border-white/20 px-8 pt-4 pb-2 text-white'
|
||||
size='2xl'
|
||||
uppercase={true}
|
||||
>
|
||||
Withdraw from Account {selectedAccount}
|
||||
</Text>
|
||||
<div className='flex w-full'>
|
||||
<div className='flex flex-1 flex-col border-r border-white/20'>
|
||||
<div className='border-b border-white/20 p-6'>
|
||||
<div className='mb-4 rounded-md border border-white/20'>
|
||||
<div className='mb-1 flex justify-between border-b border-white/20 p-2'>
|
||||
<Text size='sm' className='text-white'>
|
||||
Asset:
|
||||
</Text>
|
||||
<select
|
||||
className='bg-transparent text-white outline-0'
|
||||
onChange={handleTokenChange}
|
||||
value={selectedToken}
|
||||
>
|
||||
{positionsData?.coins?.map((coin) => (
|
||||
<option key={coin.denom} value={coin.denom}>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className='flex justify-between p-2'>
|
||||
<Text size='sm' className='text-white'>
|
||||
Amount:
|
||||
</Text>
|
||||
<input
|
||||
type='number'
|
||||
className='appearance-none bg-transparent text-right text-white'
|
||||
value={amount}
|
||||
min='0'
|
||||
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value === '') setAmount(0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Text size='xs' uppercase={true} className='mb-2 text-white/60'>
|
||||
Available: {formatValue(maxWithdrawAmount, 0, 4, true, false, false, false, false)}
|
||||
</Text>
|
||||
<Slider
|
||||
className='mb-6'
|
||||
value={percentageValue}
|
||||
onChange={(value) => {
|
||||
const decimal = value[0] / 100
|
||||
// limit decimal precision based on token contract decimals
|
||||
const newAmount = Number(
|
||||
(decimal * maxWithdrawAmount).toFixed(selectedTokenDecimals),
|
||||
)
|
||||
|
||||
setAmount(newAmount)
|
||||
}}
|
||||
onMaxClick={() => setAmount(maxWithdrawAmount)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-between p-6'>
|
||||
<div className='flex flex-1 flex-wrap'>
|
||||
<Text size='sm' className='text-white' uppercase={true}>
|
||||
Withdraw with borrowing
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/60'>
|
||||
Borrow assets from account to withdraw to your wallet
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isBorrowEnabled}
|
||||
onChange={handleBorrowChange}
|
||||
className={classNames(
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full',
|
||||
isBorrowEnabled ? 'bg-blue-600' : 'bg-gray-400',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
isBorrowEnabled ? 'translate-x-6' : 'translate-x-1'
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className='flex p-6'>
|
||||
<Button className='mt-auto w-full' onClick={() => mutate()}>
|
||||
Withdraw
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-[430px] flex-wrap content-start bg-black/60'>
|
||||
<div className='flex w-full flex-wrap border-b border-white/20 p-6'>
|
||||
<Text size='xl' className='mb-4 flex w-full text-white'>
|
||||
Account {selectedAccount}
|
||||
</Text>
|
||||
{accountStats && (
|
||||
<div className='flex w-full items-center gap-x-3'>
|
||||
<Text size='sm' className='flex flex-grow text-white'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
prefix='$: '
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Gauge
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Leverage:{' '}
|
||||
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
|
||||
<br />
|
||||
Max Leverage:{' '}
|
||||
{formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<Gauge
|
||||
value={accountStats.risk}
|
||||
label='Risk'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Risk: {formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<BorrowCapacity
|
||||
limit={80}
|
||||
max={100}
|
||||
balance={100 - accountStats.health * 100}
|
||||
barHeight='16px'
|
||||
hideValues={true}
|
||||
showTitle={false}
|
||||
className='w-[140px]'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap border-b border-white/20 p-6'>
|
||||
<div className='mb-2 flex w-full'>
|
||||
<Text size='xs' className='flex-grow text-white/60'>
|
||||
Total Position:
|
||||
</Text>
|
||||
|
||||
<Text size='xs' className='text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
prefix='$'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
<div className='flex w-full justify-between'>
|
||||
<Text size='xs' className='flex-grow text-white/60'>
|
||||
Total Liabilities:
|
||||
</Text>
|
||||
<Text size='xs' className=' text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
prefix='$'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap'>
|
||||
<Text uppercase={true} className='w-full bg-black/20 px-6 py-2 text-white/40'>
|
||||
Balances
|
||||
</Text>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className='flex w-full flex-wrap'>
|
||||
<div className='mb-2 flex w-full border-b border-white/20 bg-black/20 px-6 py-2 '>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Asset
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Value
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Size
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
APY
|
||||
</Text>
|
||||
</div>
|
||||
{positionsData?.coins.map((coin) => (
|
||||
<div key={coin.denom} className='flex w-full px-4 py-2'>
|
||||
<Text
|
||||
size='xs'
|
||||
className='flex-1 border-l-4 border-profit pl-2 text-white/60'
|
||||
>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={getTokenTotalUSDValue(coin.amount, coin.denom)}
|
||||
prefix='$'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()}
|
||||
minDecimals={0}
|
||||
maxDecimals={4}
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
-
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
{positionsData?.debts.map((coin) => (
|
||||
<div key={coin.denom} className='flex w-full px-4 py-2'>
|
||||
<Text size='xs' className='flex-1 border-l-4 border-loss pl-2 text-white/60'>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={getTokenTotalUSDValue(coin.amount, coin.denom)}
|
||||
prefix='-$'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()}
|
||||
minDecimals={0}
|
||||
maxDecimals={4}
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={Number(marketsData?.[coin.denom].borrow_rate) * 100}
|
||||
minDecimals={0}
|
||||
maxDecimals={2}
|
||||
prefix='-'
|
||||
suffix='%'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default WithdrawModal
|
4
components/Account/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as AccountStatus } from './AccountStatus'
|
||||
export { default as FundAccountModal } from './FundAccountModal'
|
||||
export { default as SubAccountNavigation } from './SubAccountNavigation'
|
||||
export { default as WithdrawModal } from './WithdrawModal'
|
@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import Number from 'components/Number'
|
||||
import FormattedNumber from 'components/FormattedNumber'
|
||||
import Text from 'components/Text'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
interface Props {
|
||||
@ -13,6 +14,7 @@ interface Props {
|
||||
showPercentageText?: boolean
|
||||
className?: string
|
||||
hideValues?: boolean
|
||||
decimals?: number
|
||||
}
|
||||
|
||||
export const BorrowCapacity = ({
|
||||
@ -24,42 +26,25 @@ export const BorrowCapacity = ({
|
||||
showPercentageText = true,
|
||||
className,
|
||||
hideValues,
|
||||
decimals = 2,
|
||||
}: Props) => {
|
||||
const [percentOfMaxRound, setPercentOfMaxRound] = useState(0)
|
||||
const [percentOfMaxRange, setPercentOfMaxRange] = useState(0)
|
||||
const [percentOfMaxMargin, setPercentOfMaxMargin] = useState('10px')
|
||||
const [limitPercentOfMax, setLimitPercentOfMax] = useState(0)
|
||||
const [timer, setTimer] = useState<ReturnType<typeof setTimeout>>()
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
clearTimeout(timer)
|
||||
const percent = max === 0 ? 0 : (balance / max) * 100
|
||||
const delta = percent - percentOfMaxRound
|
||||
const startingPoint = percentOfMaxRound
|
||||
|
||||
if (max === 0) {
|
||||
setPercentOfMaxMargin('10px')
|
||||
setPercentOfMaxRound(0)
|
||||
setPercentOfMaxRange(0)
|
||||
setLimitPercentOfMax(0)
|
||||
return
|
||||
}
|
||||
|
||||
const pOfMax = +((balance / max) * 100).toFixed(2)
|
||||
setPercentOfMaxRound(+(Math.round(pOfMax * 100) / 100).toFixed(1))
|
||||
const pOfMax = +((balance / max) * 100)
|
||||
setPercentOfMaxRound(+(Math.round(pOfMax * 100) / 100))
|
||||
setPercentOfMaxRange(Math.min(Math.max(pOfMax, 0), 100))
|
||||
setLimitPercentOfMax((limit / max) * 100)
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
setTimer(
|
||||
setTimeout(() => {
|
||||
const currentPercentOfMax = startingPoint + (delta / 20) * i
|
||||
setPercentOfMaxMargin(currentPercentOfMax > 15 ? '-60px' : '10px')
|
||||
}, 50 * (i + 1)),
|
||||
)
|
||||
}
|
||||
return () => clearTimeout(timer)
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[balance, max],
|
||||
)
|
||||
@ -82,46 +67,48 @@ export const BorrowCapacity = ({
|
||||
limitPercentOfMax ? 'opacity-60' : 'opacity-0',
|
||||
)}
|
||||
>
|
||||
<Number animate={true} amount={limit} />
|
||||
<FormattedNumber animate amount={limit} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content='Tooltip'>
|
||||
<div className='relative' style={{ height: barHeight }}>
|
||||
<div className='absolute h-full w-full rounded-3xl shadow-inset gradient-hatched'>
|
||||
<Tooltip content={<Text size='sm'>Borrow Capacity Tooltip</Text>}>
|
||||
<div className='relative'>
|
||||
<div
|
||||
className='absolute bottom-0 h-[120%] w-[1px] bg-white transition-[left] duration-1000 ease-linear'
|
||||
style={{ left: `${limitPercentOfMax || 0}%` }}
|
||||
/>
|
||||
className='overflow-hidden rounded-3xl border-r-2 border-r-loss '
|
||||
style={{ height: barHeight }}
|
||||
>
|
||||
<div className='absolute h-full w-full rounded-lg shadow-inset gradient-hatched '>
|
||||
<div
|
||||
className='absolute left-0 h-full max-w-full rounded-l-3xl bg-body-dark transition-[right] duration-1000 ease-linear'
|
||||
className='ease-loss absolute left-0 h-full max-w-full rounded-l-3xl bg-body-dark transition-[right] duration-1000'
|
||||
style={{
|
||||
right: `${limitPercentOfMax ? 100 - limitPercentOfMax : 100}%`,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
|
||||
<div className='absolute top-0 h-full w-full overflow-hidden'>
|
||||
<div className='absolute top-0 h-full w-full'>
|
||||
<div
|
||||
className='h-full rounded-l transition-[width] duration-1000 ease-linear'
|
||||
className='h-full rounded-lg transition-[width] duration-1000 ease-linear'
|
||||
style={{
|
||||
width: `${percentOfMaxRange || 0.01}%`,
|
||||
width: `${percentOfMaxRange || 0.02}%`,
|
||||
WebkitMask: 'linear-gradient(#fff 0 0)',
|
||||
}}
|
||||
>
|
||||
<div className='absolute top-0 bottom-0 left-0 right-0 gradient-limit' />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='absolute bottom-0 h-[120%] w-[1px] bg-white transition-[left] duration-1000 ease-linear'
|
||||
style={{ left: `${limitPercentOfMax || 0}%` }}
|
||||
/>
|
||||
{showPercentageText ? (
|
||||
<span
|
||||
className='absolute top-1/2 w-[53px] -translate-y-1/2 transition-[left] duration-1000 ease-linear text-2xs-caps'
|
||||
style={{
|
||||
left: `${percentOfMaxRange}%`,
|
||||
marginLeft: percentOfMaxMargin,
|
||||
}}
|
||||
>
|
||||
<span className='absolute top-1/2 mt-[1px] w-full -translate-y-1/2 animate-fadein text-center opacity-0 text-2xs-caps'>
|
||||
{max !== 0 && (
|
||||
<Number
|
||||
animate={true}
|
||||
<FormattedNumber
|
||||
className='text-white'
|
||||
animate
|
||||
amount={percentOfMaxRound}
|
||||
minDecimals={decimals}
|
||||
maxDecimals={decimals}
|
||||
suffix='%'
|
||||
abbreviated={false}
|
||||
/>
|
||||
@ -131,12 +118,13 @@ export const BorrowCapacity = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!hideValues && (
|
||||
<div className='mt-2 flex opacity-60 text-3xs-caps'>
|
||||
<Number animate={true} amount={balance} className='mr-1' />
|
||||
<FormattedNumber animate amount={balance} className='mr-1' />
|
||||
<span className='mr-1'>of</span>
|
||||
<Number animate={true} amount={max} />
|
||||
<FormattedNumber animate amount={max} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -7,8 +7,8 @@ import { toast } from 'react-toastify'
|
||||
import Button from 'components/Button'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
import Gauge from 'components/Gauge'
|
||||
import ProgressBar from 'components/ProgressBar'
|
||||
import SemiCircleProgress from 'components/SemiCircleProgress'
|
||||
import Slider from 'components/Slider'
|
||||
import Text from 'components/Text'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
@ -21,7 +21,7 @@ import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { chain } from 'utils/chains'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { formatCurrency, formatValue } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
type Props = {
|
||||
@ -261,14 +261,29 @@ const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
<Gauge
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Leverage:{' '}
|
||||
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
|
||||
<br />
|
||||
Max Leverage:{' '}
|
||||
{formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<Gauge
|
||||
value={accountStats.risk}
|
||||
label='Risk'
|
||||
tooltip={
|
||||
<Text size='sm'>
|
||||
Current Risk:{' '}
|
||||
{formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SemiCircleProgress value={accountStats.risk} label='Risk' />
|
||||
<ProgressBar value={accountStats.health} />
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactNode } from 'react'
|
||||
import React, { LegacyRef, ReactNode } from 'react'
|
||||
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
|
||||
@ -12,17 +12,17 @@ interface Props {
|
||||
showProgressIndicator?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
text?: string | ReactNode
|
||||
variant?: 'solid' | 'transparent' | 'round'
|
||||
variant?: 'solid' | 'transparent' | 'round' | 'text'
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
const colorClasses = {
|
||||
primary:
|
||||
'border-none bg-primary hover:bg-primary-highlight active:bg-primary-highlight-10 focus:bg-primary-highlight',
|
||||
'border-none text-white bg-primary hover:bg-primary-highlight active:bg-primary-highlight-10 focus:bg-primary-highlight',
|
||||
secondary:
|
||||
'border-none bg-secondary hover:bg-secondary-highlight active:bg-secondary-highlight-10 focus:bg-secondary-highlight',
|
||||
'border-none text-white bg-secondary hover:bg-secondary-highlight active:bg-secondary-highlight-10 focus:bg-secondary-highlight',
|
||||
tertiary:
|
||||
'border bg-secondary-dark/60 border-white/60 hover:bg-secondary-dark hover:border-white active:bg-secondary-dark-10 active:border-white focus:bg-secondary-dark focus:border-white',
|
||||
'border text-white bg-secondary-dark/60 border-white/60 hover:bg-secondary-dark hover:border-white active:bg-secondary-dark-10 active:border-white focus:bg-secondary-dark focus:border-white',
|
||||
quaternary:
|
||||
'border bg-transparent text-white/60 border-transparent hover:text-white hover:border-white active:text-white active:border-white',
|
||||
}
|
||||
@ -54,9 +54,11 @@ const variantClasses = {
|
||||
solid: 'text-white',
|
||||
transparent: 'bg-transparent p-0',
|
||||
round: 'rounded-full p-0',
|
||||
text: 'border-none bg-transparent',
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
const Button = React.forwardRef(function Button(
|
||||
{
|
||||
children,
|
||||
className = '',
|
||||
color = 'primary',
|
||||
@ -67,18 +69,36 @@ const Button = ({
|
||||
text,
|
||||
variant = 'solid',
|
||||
onClick,
|
||||
}: Props) => {
|
||||
}: Props,
|
||||
ref,
|
||||
) {
|
||||
const buttonClasses = []
|
||||
|
||||
switch (variant) {
|
||||
case 'round':
|
||||
buttonClasses.push(sizeClasses[size], roundSizeClasses[size], colorClasses[color])
|
||||
break
|
||||
|
||||
case 'transparent':
|
||||
buttonClasses.push(sizeClasses[size], transparentColorClasses[color])
|
||||
break
|
||||
|
||||
case 'solid':
|
||||
buttonClasses.push(sizeClasses[size], colorClasses[color])
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'cursor-pointer appearance-none break-normal rounded-3xl outline-none transition-colors',
|
||||
variant === 'round' ? `${sizeClasses[size]} ${roundSizeClasses[size]}` : sizeClasses[size],
|
||||
variant === 'transparent' ? transparentColorClasses[color] : colorClasses[color],
|
||||
variantClasses[variant],
|
||||
buttonClasses,
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
className,
|
||||
)}
|
||||
id={id}
|
||||
ref={ref as LegacyRef<HTMLButtonElement>}
|
||||
onClick={disabled ? () => {} : onClick}
|
||||
>
|
||||
{text && !children && !showProgressIndicator && <span>{text}</span>}
|
||||
@ -88,6 +108,6 @@ const Button = ({
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Button
|
||||
|
@ -11,7 +11,7 @@ const Card = ({ children, className }: Props) => {
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'h-fit w-full max-w-full rounded-xl border-[7px] border-accent-highlight p-4 gradient-card',
|
||||
'h-fit w-full max-w-full overflow-hidden rounded-xl border-[7px] border-accent-highlight p-4 gradient-card',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { Coin } from '@cosmjs/stargate'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import Image from 'next/image'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { ChainId, Wallet } from 'types'
|
||||
import { Wallet } from 'types'
|
||||
import { getInjectiveAddress } from 'utils/address'
|
||||
import { chain } from 'utils/chains'
|
||||
import { getExperimentalChainConfigBasedOnChainId } from 'utils/experimental-chains'
|
||||
|
172
components/CreditManager.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
import FormattedNumber from 'components/FormattedNumber'
|
||||
import ArrowRightLine from 'components/Icons/arrow-right-line.svg'
|
||||
import Text from 'components/Text'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { chain } from 'utils/chains'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
const CreditManager = () => {
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
|
||||
|
||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||
selectedAccount ?? '',
|
||||
)
|
||||
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: marketsData } = useMarkets()
|
||||
const accountStats = useAccountStats()
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
|
||||
return (
|
||||
BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.toNumber() * tokenPrices[denom]
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-[400px] basis-[400px] flex-wrap content-start border-white/20 bg-header placeholder:border-l'>
|
||||
<div className='flex w-full flex-wrap items-center border-b border-white/20'>
|
||||
<Text size='xl' uppercase={true} className='flex-grow text-center text-white'>
|
||||
Account {selectedAccount}
|
||||
</Text>
|
||||
<div className='flex border-l border-white/20 p-4' onClick={() => {}}>
|
||||
<span className='w-5 hover:cursor-pointer' onClick={toggleCreditManager}>
|
||||
<ArrowRightLine />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap p-2'>
|
||||
<div className='mb-2 flex w-full'>
|
||||
<Text size='xs' className='flex-grow text-white/60'>
|
||||
Total Position:
|
||||
</Text>
|
||||
|
||||
<Text size='xs' className='text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
animate
|
||||
prefix='$'
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
<div className='flex w-full justify-between'>
|
||||
<Text size='xs' className='flex-grow text-white/60'>
|
||||
Total Liabilities:
|
||||
</Text>
|
||||
<Text size='xs' className=' text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber()}
|
||||
animate
|
||||
prefix='$'
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full flex-wrap'>
|
||||
<Text uppercase={true} className='w-full bg-black/20 px-4 py-2 text-white/40'>
|
||||
Balances
|
||||
</Text>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<div className='flex w-full flex-wrap'>
|
||||
<div className='mb-2 flex w-full border-b border-white/20 bg-black/20 px-4 py-2'>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Asset
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Value
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
Size
|
||||
</Text>
|
||||
<Text size='xs' uppercase={true} className='flex-1 text-white'>
|
||||
APY
|
||||
</Text>
|
||||
</div>
|
||||
{positionsData?.coins.map((coin) => (
|
||||
<div key={coin.denom} className='flex w-full px-4 py-2'>
|
||||
<Text size='xs' className='flex-1 border-l-4 border-profit pl-2 text-white/60'>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={getTokenTotalUSDValue(coin.amount, coin.denom)}
|
||||
animate
|
||||
prefix='$'
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()}
|
||||
animate
|
||||
minDecimals={0}
|
||||
maxDecimals={4}
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
-
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
{positionsData?.debts.map((coin) => (
|
||||
<div key={coin.denom} className='flex w-full px-4 py-2'>
|
||||
<Text size='xs' className='flex-1 border-l-4 border-loss pl-2 text-white/60'>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={getTokenTotalUSDValue(coin.amount, coin.denom)}
|
||||
prefix='-$'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()}
|
||||
minDecimals={0}
|
||||
maxDecimals={4}
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
<Text size='xs' className='flex-1 text-white/60'>
|
||||
<FormattedNumber
|
||||
amount={Number(marketsData?.[coin.denom].borrow_rate) * 100}
|
||||
minDecimals={0}
|
||||
maxDecimals={2}
|
||||
prefix='-'
|
||||
suffix='%'
|
||||
animate
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreditManager
|
@ -1,167 +0,0 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
import FundAccountModal from 'components/FundAccountModal'
|
||||
import WithdrawModal from 'components/WithdrawModal'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { chain } from 'utils/chains'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
|
||||
const CreditManager = () => {
|
||||
const [showFundWalletModal, setShowFundWalletModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
|
||||
// recreate modals and reset state whenever ref changes
|
||||
const modalId = useRef(0)
|
||||
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
|
||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||
selectedAccount ?? '',
|
||||
)
|
||||
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: marketsData } = useMarkets()
|
||||
const accountStats = useAccountStats()
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
|
||||
return (
|
||||
BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.toNumber() * tokenPrices[denom]
|
||||
)
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return (
|
||||
<div className='absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-grey-medium p-2'>
|
||||
<ContainerSecondary>You must have a connected wallet</ContainerSecondary>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='absolute inset-0 left-auto w-[400px] border-l border-white/20 bg-grey-medium p-2'>
|
||||
<ContainerSecondary className='mb-2 flex gap-3'>
|
||||
<Button
|
||||
className='flex-1 rounded-md'
|
||||
onClick={() => {
|
||||
setShowFundWalletModal(true)
|
||||
modalId.current += 1
|
||||
}}
|
||||
>
|
||||
Fund
|
||||
</Button>
|
||||
<Button
|
||||
className='flex-1 rounded-md'
|
||||
onClick={() => {
|
||||
setShowWithdrawModal(true)
|
||||
modalId.current += 1
|
||||
}}
|
||||
disabled={!positionsData || positionsData.coins.length === 0}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary className='mb-2 text-sm'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div>Total Position:</div>
|
||||
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div>Total Liabilities:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary>
|
||||
<h4 className='font-bold'>Balances</h4>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex text-xs font-semibold'>
|
||||
<div className='flex-1'>Asset</div>
|
||||
<div className='flex-1'>Value</div>
|
||||
<div className='flex-1'>Size</div>
|
||||
<div className='flex-1'>APY</div>
|
||||
</div>
|
||||
{positionsData?.coins.map((coin) => (
|
||||
<div key={coin.denom} className='flex text-xs text-black/40'>
|
||||
<div className='flex-1'>{getTokenSymbol(coin.denom)}</div>
|
||||
<div className='flex-1'>
|
||||
{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: getTokenDecimals(coin.denom),
|
||||
})}
|
||||
</div>
|
||||
<div className='flex-1'>-</div>
|
||||
</div>
|
||||
))}
|
||||
{positionsData?.debts.map((coin) => (
|
||||
<div key={coin.denom} className='flex text-xs text-red-500'>
|
||||
<div className='flex-1 text-black/40'>{getTokenSymbol(coin.denom)}</div>
|
||||
<div className='flex-1'>
|
||||
-{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
-
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 6,
|
||||
})}
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
-{(Number(marketsData?.[coin.denom].borrow_rate) * 100).toFixed(1)}%
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ContainerSecondary>
|
||||
<FundAccountModal
|
||||
key={`fundModal_${modalId.current}`}
|
||||
show={showFundWalletModal}
|
||||
onClose={() => setShowFundWalletModal(false)}
|
||||
/>
|
||||
<WithdrawModal
|
||||
key={`withdrawModal_${modalId.current}`}
|
||||
show={showWithdrawModal}
|
||||
onClose={() => setShowWithdrawModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreditManager
|
@ -17,7 +17,7 @@ interface Props {
|
||||
abbreviated?: boolean
|
||||
}
|
||||
|
||||
const Number = ({
|
||||
const FormattedNumber = ({
|
||||
amount,
|
||||
animate = false,
|
||||
className,
|
||||
@ -72,4 +72,4 @@ const Number = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Number)
|
||||
export default React.memo(FormattedNumber)
|
@ -1,220 +0,0 @@
|
||||
import { Dialog, Switch, Transition } from '@headlessui/react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import Image from 'next/image'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
|
||||
import Slider from 'components/Slider'
|
||||
import useDepositCreditAccount from 'hooks/mutations/useDepositCreditAccount'
|
||||
import useAllBalances from 'hooks/useAllBalances'
|
||||
import useAllowedCoins from 'hooks/useAllowedCoins'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
import Button from 'components/Button'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
|
||||
const FundAccountModal = ({ show, onClose }: any) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
const [selectedToken, setSelectedToken] = useState('')
|
||||
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
|
||||
const [lendAssets, setLendAssets] = useLocalStorageState(`lendAssets_${selectedAccount}`, {
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
const { data: balancesData } = useAllBalances()
|
||||
const { data: allowedCoinsData, isLoading: isLoadingAllowedCoins } = useAllowedCoins()
|
||||
const { mutate, isLoading } = useDepositCreditAccount(
|
||||
selectedAccount || '',
|
||||
selectedToken,
|
||||
BigNumber(amount)
|
||||
.times(10 ** getTokenDecimals(selectedToken))
|
||||
.toNumber(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAmount(0)
|
||||
toast.success(`${amount} ${getTokenSymbol(selectedToken)} successfully Deposited`)
|
||||
onClose()
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (allowedCoinsData && allowedCoinsData.length > 0) {
|
||||
// initialize selected token when allowedCoins fetch data is available
|
||||
setSelectedToken(allowedCoinsData[0])
|
||||
}
|
||||
}, [allowedCoinsData])
|
||||
|
||||
const walletAmount = useMemo(() => {
|
||||
if (!selectedToken) return 0
|
||||
|
||||
return BigNumber(balancesData?.find((balance) => balance.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** getTokenDecimals(selectedToken))
|
||||
.toNumber()
|
||||
}, [balancesData, selectedToken])
|
||||
|
||||
const handleValueChange = (value: number) => {
|
||||
if (value > walletAmount) {
|
||||
setAmount(walletAmount)
|
||||
return
|
||||
}
|
||||
|
||||
setAmount(value)
|
||||
}
|
||||
|
||||
const maxValue = walletAmount
|
||||
const percentageValue = isNaN(amount) ? 0 : (amount * 100) / maxValue
|
||||
|
||||
return (
|
||||
<Transition appear show={show} as={React.Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-80' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4'>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='flex min-h-[520px] w-full max-w-3xl transform overflow-hidden rounded-2xl bg-[#585A74] align-middle shadow-xl transition-all'>
|
||||
{isLoading && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-1 flex-col items-start justify-between bg-[#4A4C60] p-6'>
|
||||
<div>
|
||||
<p className='text-bold mb-3 text-xs uppercase text-white/50'>About</p>
|
||||
<h4 className='mb-4 text-xl leading-8'>
|
||||
Bringing the next generation of video creation to the Metaverse.
|
||||
<br />
|
||||
Powered by deep-learning.
|
||||
</h4>
|
||||
</div>
|
||||
<Image src='/logo.svg' alt='mars' width={50} height={50} />
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<Dialog.Title as='h3' className='mb-4 text-center font-medium'>
|
||||
Fund Account {selectedAccount}
|
||||
</Dialog.Title>
|
||||
<ContainerSecondary className='mb-2 p-3'>
|
||||
<p className='mb-6 text-sm text-[#585A74]/50'>
|
||||
Transfer assets from your injective wallet to your Mars credit account. If you
|
||||
don’t have any assets in your injective wallet use the injective bridge to
|
||||
transfer funds to your injective wallet.
|
||||
</p>
|
||||
{isLoadingAllowedCoins ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<>
|
||||
<div className='mb-2 rounded-md border border-[#585A74] text-sm'>
|
||||
<div className='mb-1 flex justify-between border-b border-[#585A74] p-2'>
|
||||
<div className='font-bold'>Asset:</div>
|
||||
<select
|
||||
className='bg-transparent outline-0'
|
||||
onChange={(e) => {
|
||||
setSelectedToken(e.target.value)
|
||||
|
||||
if (e.target.value !== selectedToken) setAmount(0)
|
||||
}}
|
||||
value={selectedToken}
|
||||
>
|
||||
{allowedCoinsData?.map((entry) => (
|
||||
<option key={entry} value={entry}>
|
||||
{getTokenSymbol(entry)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className='flex justify-between p-2'>
|
||||
<div className='font-bold'>Amount:</div>
|
||||
<input
|
||||
type='number'
|
||||
className='bg-transparent text-right outline-0'
|
||||
value={amount}
|
||||
min='0'
|
||||
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||
onBlur={(e) => {
|
||||
if (e.target.value === '') setAmount(0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mb-2 text-sm'>In wallet: {walletAmount.toLocaleString()}</p>
|
||||
<Slider
|
||||
className='mb-6'
|
||||
value={percentageValue}
|
||||
onChange={(value) => {
|
||||
const decimal = value[0] / 100
|
||||
const tokenDecimals = getTokenDecimals(selectedToken)
|
||||
// limit decimal precision based on token contract decimals
|
||||
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
|
||||
|
||||
setAmount(newAmount)
|
||||
}}
|
||||
onMaxClick={() => setAmount(maxValue)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary className='mb-2 flex items-center justify-between'>
|
||||
<div>
|
||||
<h3 className='font-bold'>Lending Assets</h3>
|
||||
<div className='text-sm text-[#585A74]/50'>
|
||||
Lend assets from account to earn yield.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={lendAssets}
|
||||
onChange={setLendAssets}
|
||||
className={`${
|
||||
lendAssets ? 'bg-blue-600' : 'bg-gray-400'
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
lendAssets ? 'translate-x-6' : 'translate-x-1'
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</Switch>
|
||||
</ContainerSecondary>
|
||||
<Button
|
||||
className='mt-auto w-full'
|
||||
onClick={() => mutate()}
|
||||
disabled={amount === 0 || !amount}
|
||||
>
|
||||
Fund Account
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default FundAccountModal
|
86
components/Gauge.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import classNames from 'classnames'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
type Props = {
|
||||
tooltip: string | ReactNode
|
||||
strokeWidth?: number
|
||||
background?: string
|
||||
diameter?: number
|
||||
value: number
|
||||
label?: string
|
||||
}
|
||||
|
||||
const Gauge = ({ background = '#15161A', diameter = 40, value = 0, label, tooltip }: Props) => {
|
||||
const percentage = value * 100
|
||||
const percentageValue = percentage > 100 ? 100 : percentage < 0 ? 0 : percentage
|
||||
const semiCirclePercentage = Math.abs(percentageValue / 2 - 50)
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip}>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative overflow-hidden',
|
||||
`w-${diameter / 4} h-${diameter / 8 + 1}`,
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
viewBox='2 -2 28 36'
|
||||
width={diameter}
|
||||
height={diameter}
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
className='absolute top-0 left-0'
|
||||
>
|
||||
<linearGradient id='gradient'>
|
||||
<stop stopColor='#C13338' offset='0%'></stop>
|
||||
<stop stopColor='#4F3D9F' offset='50%'></stop>
|
||||
<stop stopColor='#15BFA9' offset='100%'></stop>
|
||||
</linearGradient>
|
||||
<circle
|
||||
fill='none'
|
||||
stroke={background}
|
||||
strokeWidth={4}
|
||||
strokeDasharray='50 100'
|
||||
strokeLinecap='round'
|
||||
r='16'
|
||||
cx='16'
|
||||
cy='16'
|
||||
shapeRendering='geometricPrecision'
|
||||
/>
|
||||
<circle
|
||||
r='16'
|
||||
cx='16'
|
||||
cy='16'
|
||||
fill='none'
|
||||
strokeLinecap='round'
|
||||
stroke='url(#gradient)'
|
||||
strokeDasharray='50 100'
|
||||
strokeWidth={5}
|
||||
style={{
|
||||
strokeDashoffset: semiCirclePercentage,
|
||||
transition: 'stroke-dashoffset 1s ease',
|
||||
}}
|
||||
shapeRendering='geometricPrecision'
|
||||
/>
|
||||
</svg>
|
||||
{label && (
|
||||
<span
|
||||
className='text-xs'
|
||||
style={{
|
||||
width: '100%',
|
||||
left: '0',
|
||||
textAlign: 'center',
|
||||
bottom: '-2px',
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default Gauge
|
@ -1,4 +1,4 @@
|
||||
;<svg version='1.1' viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg version='1.1' viewBox='0 0 48 48' xmlns='http://www.w3.org/2000/svg'>
|
||||
<circle
|
||||
cx='24'
|
||||
cy='24'
|
||||
|
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 672 B |
@ -1,3 +1,5 @@
|
||||
<svg version='1.1' viewBox='0 0 8 6' xmlns='http://www.w3.org/2000/svg' fill="currentColor">
|
||||
<path d='M4 6L0.535899 -1.75695e-07L7.4641 4.29987e-07L4 6Z' />
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24">
|
||||
<path fill='currentColor' d="M10.67,1.33v18.11l-6.21-6.21c-0.52-0.52-1.36-0.52-1.89,0c-0.52,0.52-0.52,1.36,0,1.89l8.49,8.49
|
||||
c0.52,0.52,1.36,0.52,1.89,0l8.49-8.49c0.52-0.52,0.52-1.36,0-1.89c-0.52-0.52-1.36-0.52-1.89,0l-6.21,6.21V1.33
|
||||
C13.33,0.6,12.74,0,12,0S10.67,0.6,10.67,1.33z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 165 B After Width: | Height: | Size: 382 B |
6
components/Icons/arrow-left-line.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 14 13">
|
||||
<path fill="currentColor" d="M13.8,6.6c0-0.5-0.3-0.8-0.9-0.8H6L4.4,5.8l2.2-2l1.5-1.5c0.2-0.2,0.3-0.4,0.3-0.6c0-0.5-0.3-0.8-0.8-0.8
|
||||
C7.3,0.9,7.1,1,6.9,1.2L2.1,6C2,6.1,1.9,6.2,1.9,6.4V1.6c0-0.5-0.4-0.8-0.8-0.8c-0.5,0-0.8,0.3-0.8,0.8v9.9c0,0.5,0.4,0.8,0.8,0.8
|
||||
c0.5,0,0.8-0.3,0.8-0.8V6.8c0,0.2,0.1,0.3,0.3,0.4L6.9,12c0.2,0.2,0.4,0.3,0.6,0.3c0.5,0,0.8-0.3,0.8-0.8c0-0.2-0.1-0.5-0.3-0.6
|
||||
L6.5,9.3l-2.2-2L6,7.4h7C13.4,7.4,13.8,7.1,13.8,6.6z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 537 B |
6
components/Icons/arrow-up.svg
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 24 24" >
|
||||
<path fill='currentColor' d="M13.33,22.67V4.55l6.21,6.21c0.52,0.52,1.36,0.52,1.89,0c0.52-0.52,0.52-1.36,0-1.89l-8.49-8.49
|
||||
c-0.52-0.52-1.36-0.52-1.89,0L2.57,8.88c-0.52,0.52-0.52,1.36,0,1.89c0.52,0.52,1.36,0.52,1.89,0l6.21-6.21v18.11
|
||||
c0,0.74,0.6,1.33,1.33,1.33C12.74,24,13.33,23.4,13.33,22.67z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 397 B |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 3.8 KiB |
90
components/Icons/mars-protocol.svg
Normal file
@ -0,0 +1,90 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 242 80">
|
||||
<path fill-rule='evenodd' clip-rule='evenodd' fill='currentColor' d="M40.3,0l12.4,2l11.2,5.7l8.8,8.8l5.7,11.2l2,12.4l-2,12.4
|
||||
l-5.7,11.2l-8.8,8.8L52.7,78l-12.4,2L28,78l-11.2-5.7L8,63.5L2.3,52.4L0.3,40l2-12.4L8,16.5l8.8-8.8L28,2L40.3,0z M34.6,31.5
|
||||
l5.7,7.9l5.7-7.9H34.6z M34,31.9l5.7,7.9l-9.3,3L34,31.9z M29.3,43.2l-8-8.7l-2.8,12.2L29.3,43.2z M17.8,46.4l2.7-12.1l-9.3,2.9
|
||||
L17.8,46.4z M4.6,39.3l5.6-1.8L8.8,48.8L4.6,39.3z M1.8,40.1l1.8-0.6l-0.7,8L1.8,40.1z M3.6,51.5l-0.2-1l0.8-10.1l4.2,9.4L3.6,51.5z
|
||||
M8.8,50.6l2.1,10.1l-6.6-7.7l-0.4-0.9L8.8,50.6z M9.7,50.3l8,8.6l-0.1-11.2L9.7,50.3z M18.7,47.4l9.4,8.3l1.4-11.8L18.7,47.4z
|
||||
M27.7,56.3l-9.3-8.2l0.1,11.3L27.7,56.3z M17.4,59.7l-7.7-8.3l2.2,10.2L17.4,59.7z M10.9,61.9l-5.2-6.1l3.4,6.7L10.9,61.9z
|
||||
M30.2,44l-1.4,11.8l10.8-4.9L30.2,44z M40,51.5l-10.8,4.9L40,62.9V51.5z M28.7,66.8l0.1-9.8l10.7,6.3L28.7,66.8z M28.6,73.8
|
||||
l0.1-5.9L39,72.7L28.6,73.8z M28.6,76.7l0-1.9l7.4,3.1L28.6,76.7z M40,78.5L39,78.3l-9.3-3.9L40,73.3V78.5z M40.7,72.3l10.7-4.9
|
||||
L40.7,64V72.3z M40.7,73.3v5.2l1-0.2l9.3-3.9L40.7,73.3z M44.7,77.8l7.4-1.2l0-1.9L44.7,77.8z M52.1,73.8L52,67.9l-10.3,4.8
|
||||
L52.1,73.8z M52,66.8l-0.1-9.8l-10.7,6.3L52,66.8z M51.5,56.5l-10.8,6.4V51.5L51.5,56.5z M41.1,50.9l9.4-6.8l1.4,11.8L41.1,50.9z
|
||||
M51.2,43.9l1.4,11.8l9.4-8.3L51.2,43.9z M62.2,59.4L53,56.3l9.3-8.2L62.2,59.4z M68.8,61.6l-5.6-1.9l7.7-8.3L68.8,61.6z M71.6,62.5
|
||||
l-1.8-0.6l5.2-6.1L71.6,62.5z M76.8,52.2l-0.4,0.9l-6.6,7.7l2.1-10.1L76.8,52.2z M71.2,49.6l-7.9-2.6l6.5-9.1L71.2,49.6z M62.2,46.7
|
||||
l-10.8-3.5l8-8.7L62.2,46.7z M60.1,34.3l9.3,2.9l-6.6,9.2L60.1,34.3z M70.5,37.5l5.6,1.8l-4.2,9.5L70.5,37.5z M77,39.6l0.7,8
|
||||
l1.2-7.4L77,39.6z M77.2,50.6L77,51.5l-4.9-1.6l4.2-9.4L77.2,50.6z M50.9,42.7l-3.6-11.1L58.9,34L50.9,42.7z M47.4,30.9l6.7-9.2
|
||||
l5,11.5L47.4,30.9z M59.6,14.1l-4.9,6.7l10.6,3.5L59.6,14.1z M69.2,18.4l-9-5.2l3-4.2l0.7,0.7L69.2,18.4z M54.8,21.6L59.7,33
|
||||
l5.8-7.9L54.8,21.6z M66.1,11.9l5.3,5.3l-1.1,1.6L66.1,11.9z M60.6,14.4l9,5.2l-3.5,4.7L60.6,14.4z M60.3,33.5l5.7-7.7l3.5,10.7
|
||||
L60.3,33.5L60.3,33.5L60.3,33.5z M78.8,39.4L77,38.8l0.6-6.6L78.8,39.4z M77,28.5l0.1,0.9l-0.8,7.9l-2.5-7.8L77,28.5z M70.5,36.8
|
||||
l2.8-6.5l2.7,8.2L70.5,36.8z M70.7,19.4l4.4,5l-3.3-6.5L70.7,19.4z M76.4,27l0.4,0.8l-3.2,1L71.1,21L76.4,27z M70.1,20.2l2.7,8.2
|
||||
l-6.1-3.6L70.1,20.2z M71,50.3L63,47.8L63,59L71,50.3z M52.6,57L52.6,57L52.6,57l9.2,3l-9.1,6.6L52.6,57z M52.8,76.3l0-1.9l6.5-1.4
|
||||
L52.8,76.3z M62.6,71.3l-0.8,0.4l-7.8,1.7l6.6-4.8L62.6,71.3z M52.7,67.7l7,0.7l-7,5.1L52.7,67.7z M68.3,62.2l-5.4-1.8l-1.5,6.9
|
||||
L68.3,62.2z M63.8,70.3l-0.6,0.6l-2-2.7l6.6-4.8L63.8,70.3z M65.9,68.2l3.4-5.7l1.8,0.6L65.9,68.2z M40,72.3V64l-10.7,3.4L40,72.3z
|
||||
M28,56.9L28,56.9L28,56.9l0,9.7L18.8,60L28,56.9z M9.6,63.1l1.8-0.6l3.4,5.7L9.6,63.1z M17.4,70.9l-0.6-0.6l-4-6.9l6.6,4.8
|
||||
L17.4,70.9z M17.8,60.4l1.5,6.9l-7-5.1L17.8,60.4z M27.9,73.5l0.1-5.7l-7,0.7L27.9,73.5z M18.8,71.7L18,71.3l2-2.7l6.6,4.8
|
||||
L18.8,71.7z M21.4,73l6.5,1.4l0,1.9L21.4,73z M9.5,49.6l7.9-2.6L10.9,38L9.5,49.6z M29.8,42.7l3.6-11.1L21.7,34L29.8,42.7z
|
||||
M25.9,20.8l-10.6,3.5L21,14.1L25.9,20.8z M26.6,21.7l-5,11.5l11.6-2.3L26.6,21.7z M20.9,33l4.9-11.4l-10.7,3.6L20.9,33z M14.5,24.3
|
||||
L11,19.6l9-5.2L14.5,24.3z M10.4,18.8l-1.1-1.5l5.3-5.3L10.4,18.8z M16.7,9.8l-5.2,8.7l9-5.2l-3-4.2L16.7,9.8z M20.4,33.5l-5.7-7.7
|
||||
l-3.5,10.7L20.4,33.5L20.4,33.5L20.4,33.5z M8.9,17.9l1.1,1.5l-4.4,4.9L8.9,17.9z M3.9,27.8L4.3,27l5.3-6L7,28.8L3.9,27.8z
|
||||
M10.6,20.2l-2.7,8.2l6.1-3.6L10.6,20.2z M3.6,28.5l3.2,1l-2.5,7.8l-0.8-7.9L3.6,28.5z M3,32.2l0.6,6.6l-1.8,0.6L3,32.2z M4.7,38.5
|
||||
l2.7-8.2l2.8,6.5L4.7,38.5z M26.5,20.4l-4.9-6.7l11.5-2.3L26.5,20.4z M33.9,30.5l-6.7-9.2l12.5-1.2L33.9,30.5z M34,11.4l5.7,8
|
||||
l-12.4,1.1L34,11.4z M30,5.8l3.4,4.8l-11.1,2.2L30,5.8z M28.3,3.4L29.4,5l-7.8,1.8L28.3,3.4z M28.7,5.9l-9.9,2.3L18,8.7l3,4.2
|
||||
L28.7,5.9z M41,20.1l5.8,10.3l6.7-9.2L41,20.1z M59.6,12.8l3-4.2l-0.9-0.4l-9.8-2.3L59.6,12.8z M59,6.8l-6.7-3.4L51.2,5L59,6.8z
|
||||
M50.7,5.8l-3.4,4.8l11.2,2.2L50.7,5.8z M46.7,11.4l-5.7,8l12.4,1.1L46.7,11.4z M59,13.6l-4.9,6.7l-6.6-9L59,13.6z M46.1,30.8
|
||||
l-5.8-10.3l-5.8,10.3H46.1z M40.4,19L40.4,19L40.4,19l-5.7-7.8H46L40.4,19z M51.6,3.3l-1.1,1.5l-6.1-2.6L51.6,3.3z M40.7,1.5
|
||||
l0.9,0.1l7.3,3.2h-8.2V1.5z M46.6,10.3l-5.3-4.7h8.6L46.6,10.3z M30.7,5.6l3.3,4.7l5.3-4.7H30.7z M30.2,4.8l-1.1-1.5l7.2-1.1
|
||||
L30.2,4.8z M39.1,1.7l-7.3,3.2H40V1.5L39.1,1.7z M46.6,31.9l-5.7,7.9l9.3,3L46.6,31.9z M50,43.5l-9.3-3v9.7L50,43.5z M40,50.2v-9.7
|
||||
l-9.3,3L40,50.2z M35,10.4l5.4-4.7l5.4,4.7H35z M13.9,25.8l-6.1,3.6l2.8,6.6L13.9,25.8z M18.6,60.8l1.6,7l7.1-0.7L18.6,60.8z
|
||||
M53.4,67.1l7.1,0.7l1.6-7L53.4,67.1z M66.8,25.8l6.1,3.6L70.1,36L66.8,25.8z"/>
|
||||
<path fill='currentColor' d="M100.8,29l2.7,4.6l2.7-4.6l0.6,5.5h1.6l-1.2-9.4l-3.7,6.1l-3.7-6.1l-1.2,9.4h1.6L100.8,29z"/>
|
||||
<path fill='currentColor' d="M123.6,32.2h4.7l-0.3-1.3h-4.2L123.6,32.2z M126,28l1.4,3.3l0,0.4l1.2,2.7h1.7L126,25l-4.4,9.5h1.7
|
||||
l1.2-2.8l0-0.3L126,28z"/>
|
||||
<path fill='currentColor' d="M145.9,30.2l2.9,4.3h1.8l-3-4.3H145.9z M144.1,25.5v9h1.5v-9H144.1z M145,26.8h1.8
|
||||
c0.3,0,0.6,0.1,0.9,0.2c0.3,0.1,0.5,0.3,0.6,0.5c0.1,0.2,0.2,0.5,0.2,0.8c0,0.3-0.1,0.6-0.2,0.8c-0.1,0.2-0.3,0.4-0.6,0.5
|
||||
c-0.3,0.1-0.6,0.2-0.9,0.2H145v1.2h1.9c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,0.9-0.6,1.1-1s0.4-0.9,0.4-1.5c0-0.6-0.1-1-0.4-1.5
|
||||
c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.3-1.7-0.3H145V26.8z"/>
|
||||
<path fill='currentColor' d="M164.9,31.6l-1.1,0.8c0.2,0.4,0.5,0.8,0.8,1.1s0.7,0.6,1.2,0.8c0.5,0.2,0.9,0.3,1.5,0.3
|
||||
c0.4,0,0.8-0.1,1.1-0.2c0.4-0.1,0.7-0.3,1-0.5c0.3-0.2,0.5-0.5,0.7-0.8c0.2-0.3,0.2-0.7,0.2-1.1c0-0.4-0.1-0.7-0.2-1
|
||||
c-0.1-0.3-0.3-0.5-0.5-0.8c-0.2-0.2-0.5-0.4-0.7-0.6c-0.3-0.1-0.5-0.3-0.8-0.4c-0.5-0.2-0.9-0.3-1.2-0.5c-0.3-0.2-0.5-0.3-0.7-0.5
|
||||
c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.3,0.1-0.5,0.3-0.7c0.2-0.2,0.5-0.3,0.9-0.3c0.3,0,0.6,0.1,0.8,0.2c0.2,0.1,0.4,0.3,0.6,0.5
|
||||
c0.2,0.2,0.3,0.4,0.4,0.6l1.2-0.7c-0.2-0.3-0.4-0.6-0.6-0.9c-0.3-0.3-0.6-0.5-1-0.7c-0.4-0.2-0.9-0.3-1.4-0.3c-0.5,0-1,0.1-1.4,0.3
|
||||
c-0.4,0.2-0.8,0.5-1,0.8c-0.2,0.4-0.4,0.8-0.4,1.3c0,0.4,0.1,0.8,0.2,1.1c0.2,0.3,0.4,0.5,0.6,0.7c0.2,0.2,0.5,0.4,0.8,0.5
|
||||
c0.3,0.1,0.5,0.2,0.7,0.3c0.4,0.1,0.7,0.3,1,0.4c0.3,0.1,0.5,0.3,0.7,0.5c0.2,0.2,0.2,0.5,0.2,0.8c0,0.3-0.1,0.6-0.4,0.9
|
||||
c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.1-0.9-0.2c-0.3-0.1-0.5-0.3-0.8-0.6C165.3,32.2,165.1,31.9,164.9,31.6z"/>
|
||||
<path fill='currentColor' d="M99.1,44.8v9h1.5v-9H99.1z M100,46.2h1.8c0.5,0,0.9,0.1,1.3,0.4c0.3,0.2,0.5,0.6,0.5,1.1
|
||||
s-0.2,0.8-0.5,1.1c-0.3,0.2-0.7,0.4-1.3,0.4H100v1.3h1.8c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,0.9-0.6,1.1-1c0.3-0.4,0.4-0.9,0.4-1.5
|
||||
c0-0.6-0.1-1.1-0.4-1.5c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.3-1.7-0.3H100V46.2z"/>
|
||||
<path fill='currentColor' d="M119.2,49.5l2.9,4.3h1.8l-3-4.3H119.2z M117.4,44.8v9h1.5v-9H117.4z M118.3,46.1h1.8
|
||||
c0.3,0,0.6,0.1,0.9,0.2c0.3,0.1,0.5,0.3,0.6,0.5c0.1,0.2,0.2,0.5,0.2,0.8c0,0.3-0.1,0.6-0.2,0.8c-0.1,0.2-0.3,0.4-0.6,0.5
|
||||
c-0.3,0.1-0.6,0.2-0.9,0.2h-1.8v1.2h1.9c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,0.9-0.6,1.1-1c0.3-0.4,0.4-0.9,0.4-1.5c0-0.6-0.1-1-0.4-1.5
|
||||
c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.3-1.7-0.3h-1.9V46.1z"/>
|
||||
<path fill='currentColor' d="M137,49.3c0-0.6,0.1-1.2,0.4-1.6c0.3-0.5,0.6-0.9,1.1-1.1c0.5-0.3,1-0.4,1.6-0.4
|
||||
c0.6,0,1.1,0.1,1.6,0.4c0.5,0.3,0.8,0.7,1.1,1.1c0.3,0.5,0.4,1,0.4,1.6s-0.1,1.2-0.4,1.7c-0.3,0.5-0.6,0.9-1.1,1.1
|
||||
c-0.5,0.3-1,0.4-1.6,0.4c-0.6,0-1.1-0.1-1.6-0.4c-0.5-0.3-0.8-0.6-1.1-1.1C137.1,50.5,137,49.9,137,49.3z M135.4,49.3
|
||||
c0,0.7,0.1,1.3,0.3,1.9c0.2,0.6,0.6,1.1,1,1.5c0.4,0.4,0.9,0.7,1.5,1c0.6,0.2,1.2,0.3,1.9,0.3c0.7,0,1.3-0.1,1.9-0.3
|
||||
c0.6-0.2,1.1-0.6,1.5-1c0.4-0.4,0.7-0.9,1-1.5s0.3-1.2,0.3-1.9c0-0.7-0.1-1.3-0.3-1.9c-0.2-0.6-0.6-1-1-1.5c-0.4-0.4-0.9-0.7-1.5-1
|
||||
c-0.6-0.2-1.2-0.3-1.9-0.3c-0.7,0-1.3,0.1-1.8,0.3c-0.6,0.2-1.1,0.6-1.5,1c-0.4,0.4-0.7,0.9-1,1.5C135.5,48,135.4,48.6,135.4,49.3z"
|
||||
/>
|
||||
<path fill='currentColor' d="M155.7,46.2h2.4v7.6h1.5v-7.6h2.4v-1.4h-6.2V46.2z"/>
|
||||
<path fill='currentColor' d="M174.4,49.3c0-0.6,0.1-1.2,0.4-1.6c0.3-0.5,0.6-0.9,1.1-1.1c0.5-0.3,1-0.4,1.6-0.4
|
||||
c0.6,0,1.1,0.1,1.6,0.4c0.5,0.3,0.8,0.7,1.1,1.1c0.3,0.5,0.4,1,0.4,1.6s-0.1,1.2-0.4,1.7c-0.3,0.5-0.6,0.9-1.1,1.1
|
||||
c-0.5,0.3-1,0.4-1.6,0.4c-0.6,0-1.1-0.1-1.6-0.4c-0.5-0.3-0.8-0.6-1.1-1.1C174.5,50.5,174.4,49.9,174.4,49.3z M172.8,49.3
|
||||
c0,0.7,0.1,1.3,0.3,1.9c0.2,0.6,0.6,1.1,1,1.5c0.4,0.4,0.9,0.7,1.5,1c0.6,0.2,1.2,0.3,1.9,0.3c0.7,0,1.3-0.1,1.9-0.3
|
||||
c0.6-0.2,1.1-0.6,1.5-1c0.4-0.4,0.7-0.9,1-1.5c0.2-0.6,0.3-1.2,0.3-1.9c0-0.7-0.1-1.3-0.3-1.9c-0.2-0.6-0.6-1-1-1.5
|
||||
c-0.4-0.4-0.9-0.7-1.5-1c-0.6-0.2-1.2-0.3-1.9-0.3c-0.7,0-1.3,0.1-1.8,0.3c-0.6,0.2-1.1,0.6-1.5,1c-0.4,0.4-0.7,0.9-1,1.5
|
||||
C172.9,48,172.8,48.6,172.8,49.3z"/>
|
||||
<path fill='currentColor' d="M195.6,49.3c0-0.6,0.1-1.2,0.4-1.7c0.3-0.5,0.7-0.8,1.1-1.1c0.5-0.3,1-0.4,1.5-0.4
|
||||
c0.4,0,0.8,0.1,1.2,0.2c0.3,0.1,0.6,0.3,0.9,0.5c0.3,0.2,0.5,0.4,0.7,0.6v-1.8c-0.4-0.3-0.8-0.5-1.2-0.7c-0.4-0.2-1-0.2-1.6-0.2
|
||||
c-0.7,0-1.3,0.1-1.8,0.3c-0.6,0.2-1.1,0.5-1.5,1c-0.4,0.4-0.7,0.9-1,1.5c-0.2,0.6-0.3,1.2-0.3,1.9s0.1,1.3,0.3,1.9
|
||||
c0.2,0.6,0.6,1.1,1,1.5c0.4,0.4,0.9,0.7,1.5,1c0.6,0.2,1.2,0.3,1.8,0.3c0.7,0,1.2-0.1,1.6-0.2c0.4-0.2,0.8-0.4,1.2-0.7v-1.8
|
||||
c-0.2,0.2-0.4,0.5-0.7,0.7c-0.3,0.2-0.6,0.3-0.9,0.4c-0.3,0.1-0.7,0.2-1.2,0.2c-0.5,0-1.1-0.1-1.5-0.4c-0.5-0.3-0.8-0.6-1.1-1.1
|
||||
C195.7,50.5,195.6,50,195.6,49.3z"/>
|
||||
<path fill='currentColor' d="M215.3,49.3c0-0.6,0.1-1.2,0.4-1.6c0.3-0.5,0.6-0.9,1.1-1.1c0.5-0.3,1-0.4,1.6-0.4
|
||||
c0.6,0,1.1,0.1,1.6,0.4c0.5,0.3,0.8,0.7,1.1,1.1c0.3,0.5,0.4,1,0.4,1.6s-0.1,1.2-0.4,1.7c-0.3,0.5-0.6,0.9-1.1,1.1
|
||||
c-0.5,0.3-1,0.4-1.6,0.4c-0.6,0-1.1-0.1-1.6-0.4c-0.5-0.3-0.8-0.6-1.1-1.1C215.4,50.5,215.3,49.9,215.3,49.3z M213.7,49.3
|
||||
c0,0.7,0.1,1.3,0.3,1.9c0.2,0.6,0.6,1.1,1,1.5c0.4,0.4,0.9,0.7,1.5,1c0.6,0.2,1.2,0.3,1.9,0.3c0.7,0,1.3-0.1,1.9-0.3
|
||||
c0.6-0.2,1.1-0.6,1.5-1c0.4-0.4,0.7-0.9,1-1.5c0.2-0.6,0.3-1.2,0.3-1.9c0-0.7-0.1-1.3-0.3-1.9c-0.2-0.6-0.6-1-1-1.5
|
||||
c-0.4-0.4-0.9-0.7-1.5-1c-0.6-0.2-1.2-0.3-1.9-0.3c-0.7,0-1.3,0.1-1.8,0.3c-0.6,0.2-1.1,0.6-1.5,1c-0.4,0.4-0.7,0.9-1,1.5
|
||||
C213.8,48,213.7,48.6,213.7,49.3z"/>
|
||||
<path fill='currentColor' d="M235.4,44.8v9h5.1v-1.4h-3.6v-7.6H235.4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 9.7 KiB |
3
components/Icons/search.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg fill='none' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' viewBox='0 0 24 24'>
|
||||
<path d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||||
</svg>
|
After Width: | Height: | Size: 191 B |
3
components/Icons/triangle-down.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg version='1.1' viewBox='0 0 8 6' xmlns='http://www.w3.org/2000/svg' fill="currentColor">
|
||||
<path d='M4 6L0.535899 -1.75695e-07L7.4641 4.29987e-07L4 6Z' />
|
||||
</svg>
|
After Width: | Height: | Size: 165 B |
@ -2,13 +2,18 @@ import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import CreditManager from 'components/CreditManager'
|
||||
import Navigation from 'components/Navigation'
|
||||
import ArrowLeftLine from 'components/Icons/arrow-left-line.svg'
|
||||
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
|
||||
import useCreditAccounts from 'hooks/useCreditAccounts'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
|
||||
const isOpen = useCreditManagerStore((s) => s.isOpen)
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const { data: creditAccountsList, isLoading: isLoadingCreditAccounts } = useCreditAccounts()
|
||||
const hasCreditAccounts = creditAccountsList && creditAccountsList.length > 0
|
||||
|
||||
const filter = {
|
||||
day: 'brightness-100 hue-rotate-0',
|
||||
@ -25,10 +30,17 @@ const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className='relative min-h-screen w-full'>
|
||||
<div className={backgroundClasses} />
|
||||
<Navigation />
|
||||
<main className='relative flex-1 p-6'>
|
||||
{children}
|
||||
{isOpen && <CreditManager />}
|
||||
<DesktopNavigation />
|
||||
<main className='relative flex lg:h-[calc(100vh-120px)]'>
|
||||
<div className='flex flex-grow flex-wrap p-6'>{children}</div>
|
||||
{isOpen && hasCreditAccounts && <CreditManager />}
|
||||
{!isOpen && hasCreditAccounts && (
|
||||
<div className='absolute top-0 right-0 border-l border-b border-white/20 bg-header p-4'>
|
||||
<div className='w-5 hover:cursor-pointer' onClick={toggleCreditManager}>
|
||||
<ArrowLeftLine />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
@ -9,29 +9,27 @@ interface Props {
|
||||
content?: ReactNode | string
|
||||
className?: string
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
setOpen?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const Modal = ({ children, content, className, open, setOpen }: Props) => {
|
||||
const onClickAway = () => {
|
||||
setOpen(false)
|
||||
if (setOpen) setOpen(false)
|
||||
}
|
||||
|
||||
return open ? (
|
||||
<>
|
||||
<Card
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 right-0 bottom-0 z-40 mx-auto my-0 w-[790px] max-w-full',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className='fixed top-0 left-0 z-20 h-screen w-screen'>
|
||||
<div className='relative flex h-full w-full items-center justify-center'>
|
||||
<Card className={classNames('relative z-40 w-[790px] max-w-full p-0', className)}>
|
||||
{setOpen && (
|
||||
<span
|
||||
className='absolute top-4 right-4 w-[32px] opacity-40 hover:cursor-pointer hover:opacity-80'
|
||||
className='absolute top-4 right-4 z-50 w-[32px] text-white opacity-60 hover:cursor-pointer hover:opacity-100'
|
||||
onClick={onClickAway}
|
||||
role='button'
|
||||
>
|
||||
<CloseIcon />
|
||||
</span>
|
||||
)}
|
||||
{children ? children : content}
|
||||
</Card>
|
||||
<div
|
||||
@ -39,7 +37,8 @@ const Modal = ({ children, content, className, open, setOpen }: Props) => {
|
||||
onClick={onClickAway}
|
||||
role='button'
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
|
@ -1,233 +0,0 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import ChevronDownIcon from 'components/Icons/expand.svg'
|
||||
import Button from 'components/Button'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import ArrowRightLineIcon from 'components/Icons/arrow-right-line.svg'
|
||||
import ProgressBar from 'components/ProgressBar'
|
||||
import SearchInput from 'components/SearchInput'
|
||||
import SemiCircleProgress from 'components/SemiCircleProgress'
|
||||
import Wallet from 'components/Wallet'
|
||||
import useCreateCreditAccount from 'hooks/mutations/useCreateCreditAccount'
|
||||
import useDeleteCreditAccount from 'hooks/mutations/useDeleteCreditAccount'
|
||||
import useAccountStats from 'hooks/useAccountStats'
|
||||
import useCreditAccounts from 'hooks/useCreditAccounts'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { chain } from 'utils/chains'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
|
||||
// TODO: will require some tweaks depending on how lower viewport mocks pans out
|
||||
const MAX_VISIBLE_CREDIT_ACCOUNTS = 5
|
||||
|
||||
const navItems = [
|
||||
{ href: '/trade', label: 'Trade' },
|
||||
{ href: '/earn', label: 'Earn' },
|
||||
{ href: '/borrow', label: 'Borrow' },
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/council', label: 'Council' },
|
||||
]
|
||||
|
||||
const NavLink = ({ href, children }: { href: string; children: string }) => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a className={`${router.pathname === href ? 'text-white' : ''} hover:text-white`}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Navigation = () => {
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const setSelectedAccount = useCreditManagerStore((s) => s.actions.setSelectedAccount)
|
||||
const toggleCreditManager = useCreditManagerStore((s) => s.actions.toggleCreditManager)
|
||||
|
||||
const { data: creditAccountsList, isLoading: isLoadingCreditAccounts } = useCreditAccounts()
|
||||
const { mutate: createCreditAccount, isLoading: isLoadingCreate } = useCreateCreditAccount()
|
||||
const { mutate: deleteCreditAccount, isLoading: isLoadingDelete } = useDeleteCreditAccount(
|
||||
selectedAccount || '',
|
||||
)
|
||||
|
||||
const accountStats = useAccountStats()
|
||||
|
||||
const { firstCreditAccounts, restCreditAccounts } = useMemo(() => {
|
||||
return {
|
||||
firstCreditAccounts: creditAccountsList?.slice(0, MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
||||
restCreditAccounts: creditAccountsList?.slice(MAX_VISIBLE_CREDIT_ACCOUNTS) ?? [],
|
||||
}
|
||||
}, [creditAccountsList])
|
||||
|
||||
const isConnected = !!address
|
||||
const hasCreditAccounts = creditAccountsList && creditAccountsList.length > 0
|
||||
|
||||
const rightSideContent = () => {
|
||||
if ((!isConnected && !hasCreditAccounts) || isLoadingCreditAccounts) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!hasCreditAccounts) {
|
||||
return <Button onClick={() => createCreditAccount()}>Create Credit Account</Button>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-4'>
|
||||
{accountStats && (
|
||||
<>
|
||||
<p>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
/>
|
||||
</div>
|
||||
<SemiCircleProgress value={accountStats.risk} label='Risk' />
|
||||
<ProgressBar value={accountStats.health} />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className='flex w-16 cursor-pointer justify-center hover:text-white'
|
||||
onClick={toggleCreditManager}
|
||||
>
|
||||
<ArrowRightLineIcon />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
{/* Main navigation bar */}
|
||||
<div className='flex items-center justify-between border-b border-white/20 px-6 py-3'>
|
||||
<Link href='/' passHref>
|
||||
<a>
|
||||
<Image src='/logo.svg' alt='mars' width={40} height={40} />
|
||||
</a>
|
||||
</Link>
|
||||
<div className='flex gap-5 px-12 text-white/40'>
|
||||
{navItems.map((item, index) => (
|
||||
<NavLink key={index} href={item.href}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<Wallet />
|
||||
</div>
|
||||
{/* Sub navigation bar */}
|
||||
<div className='flex items-center justify-between border-b border-white/20 px-6 py-3 text-sm text-white/40'>
|
||||
<div className='flex items-center'>
|
||||
<SearchInput />
|
||||
{isConnected && hasCreditAccounts && (
|
||||
<>
|
||||
{firstCreditAccounts.map((account) => (
|
||||
<div
|
||||
key={account}
|
||||
className={`cursor-pointer px-4 hover:text-white ${
|
||||
selectedAccount === account ? 'text-white' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedAccount(account)}
|
||||
>
|
||||
Account {account}
|
||||
</div>
|
||||
))}
|
||||
{restCreditAccounts.length > 0 && (
|
||||
<Popover className='relative'>
|
||||
<Popover.Button>
|
||||
<div className='flex cursor-pointer items-center px-3 hover:text-white'>
|
||||
More
|
||||
<span className='ml-1 h-4 w-4'>
|
||||
<ChevronDownIcon />
|
||||
</span>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className='absolute z-10 w-[200px] pt-2'>
|
||||
<div className='rounded-2xl bg-white p-4 text-gray-900'>
|
||||
{restCreditAccounts.map((account) => (
|
||||
<div
|
||||
key={account}
|
||||
className={`cursor-pointer hover:text-orange-500 ${
|
||||
selectedAccount === account ? 'text-orange-500' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedAccount(account)}
|
||||
>
|
||||
Account {account}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)}
|
||||
<Popover className='relative'>
|
||||
<Popover.Button>
|
||||
<div className='flex cursor-pointer items-center px-3 hover:text-white'>
|
||||
Manage
|
||||
<ChevronDownIcon className='ml-1 h-4 w-4' />
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className='absolute z-10 w-[200px] pt-2'>
|
||||
{({ close }) => (
|
||||
<div className='rounded-2xl bg-white p-4 text-gray-900'>
|
||||
<div
|
||||
className='mb-2 cursor-pointer hover:text-orange-500'
|
||||
onClick={() => {
|
||||
close()
|
||||
createCreditAccount()
|
||||
}}
|
||||
>
|
||||
Create Account
|
||||
</div>
|
||||
<div
|
||||
className='mb-2 cursor-pointer hover:text-orange-500'
|
||||
onClick={() => {
|
||||
close()
|
||||
deleteCreditAccount()
|
||||
}}
|
||||
>
|
||||
Close Account
|
||||
</div>
|
||||
<div
|
||||
className='mb-2 cursor-pointer hover:text-orange-500'
|
||||
onClick={() => alert('TODO')}
|
||||
>
|
||||
Transfer Balance
|
||||
</div>
|
||||
<div
|
||||
className='cursor-pointer hover:text-orange-500'
|
||||
onClick={() => alert('TODO')}
|
||||
>
|
||||
Rearrange
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{rightSideContent()}
|
||||
</div>
|
||||
{(isLoadingCreate || isLoadingDelete) && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navigation
|
80
components/Navigation/DesktopNavigation.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { AccountStatus, SubAccountNavigation } from 'components/Account'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import Logo from 'components/Icons/logo.svg'
|
||||
import Modal from 'components/Modal'
|
||||
import { menuTree, NavLink } from 'components/Navigation'
|
||||
import SearchInput from 'components/Navigation/SearchInput'
|
||||
import Text from 'components/Text'
|
||||
import Wallet from 'components/Wallet'
|
||||
import useCreateCreditAccount from 'hooks/mutations/useCreateCreditAccount'
|
||||
import useDeleteCreditAccount from 'hooks/mutations/useDeleteCreditAccount'
|
||||
import useCreditAccounts from 'hooks/useCreditAccounts'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
|
||||
const Navigation = () => {
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const setSelectedAccount = useCreditManagerStore((s) => s.actions.setSelectedAccount)
|
||||
const { mutate: createCreditAccount, isLoading: isLoadingCreate } = useCreateCreditAccount()
|
||||
const { mutate: deleteCreditAccount, isLoading: isLoadingDelete } = useDeleteCreditAccount(
|
||||
selectedAccount || '',
|
||||
)
|
||||
|
||||
const { data: creditAccountsList, isLoading: isLoadingCreditAccounts } = useCreditAccounts()
|
||||
|
||||
const isConnected = !!address
|
||||
const hasCreditAccounts = creditAccountsList && creditAccountsList.length > 0
|
||||
|
||||
return (
|
||||
<div className='relative hidden bg-header lg:block'>
|
||||
<div className='flex items-center justify-between border-b border-white/20 px-6 py-3'>
|
||||
<div className='flex flex-grow items-center'>
|
||||
<Link href='/trade' passHref>
|
||||
<span className='h-10 w-10'>
|
||||
<Logo />
|
||||
</span>
|
||||
</Link>
|
||||
<div className='flex gap-8 px-6'>
|
||||
{menuTree.map((item, index) => (
|
||||
<NavLink key={index} href={item.href}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Wallet />
|
||||
</div>
|
||||
{/* Sub navigation bar */}
|
||||
<div className='flex items-center justify-between border-b border-white/20 pl-6 text-sm text-white/40'>
|
||||
<div className='flex items-center'>
|
||||
<SearchInput />
|
||||
{isConnected && hasCreditAccounts && (
|
||||
<SubAccountNavigation
|
||||
selectedAccount={selectedAccount}
|
||||
createCreditAccount={createCreditAccount}
|
||||
deleteCreditAccount={deleteCreditAccount}
|
||||
setSelectedAccount={setSelectedAccount}
|
||||
creditAccountsList={creditAccountsList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isConnected && <AccountStatus createCreditAccount={createCreditAccount} />}
|
||||
</div>
|
||||
<Modal open={isLoadingCreate || isLoadingDelete}>
|
||||
<div className='w-full p-6'>
|
||||
<Text size='2xl' uppercase={true} className='mb-6 w-full text-center'>
|
||||
Confirm Transaction
|
||||
</Text>
|
||||
<div className='flex w-full justify-center pb-6'>
|
||||
<CircularProgress size={40} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navigation
|
29
components/Navigation/NavLink.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ReactNode } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
href: string
|
||||
children: string | ReactNode
|
||||
}
|
||||
|
||||
const NavLink = ({ href, children }: Props) => {
|
||||
const router = useRouter()
|
||||
const isActive = router.pathname === href
|
||||
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a
|
||||
className={classNames(
|
||||
'text-lg-caps hover:text-white active:text-white',
|
||||
isActive ? 'pointer-events-none text-white' : 'text-white/60',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavLink
|
14
components/Navigation/SearchInput.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import SearchIcon from 'components/Icons/search.svg'
|
||||
const SearchInput = () => (
|
||||
<div className='relative mt-[1px] py-2 text-white'>
|
||||
<span className='absolute top-1/2 left-0 flex h-6 w-8 -translate-y-1/2 items-center pl-2'>
|
||||
<SearchIcon />
|
||||
</span>
|
||||
<input
|
||||
className='w-[280px] rounded-md border border-white/20 bg-black/30 py-2 pl-10 text-sm text-white placeholder:text-white/40 focus:outline-none'
|
||||
placeholder='Search'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default SearchInput
|
3
components/Navigation/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as DesktopNavigation } from './DesktopNavigation'
|
||||
export { default as menuTree } from './menuTree'
|
||||
export { default as NavLink } from './NavLink'
|
9
components/Navigation/menuTree.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const navItems = [
|
||||
{ href: '/trade', label: 'Trade' },
|
||||
{ href: '/earn', label: 'Earn' },
|
||||
{ href: '/borrow', label: 'Borrow' },
|
||||
{ href: '/portfolio', label: 'Portfolio' },
|
||||
{ href: '/council', label: 'Council' },
|
||||
]
|
||||
|
||||
export default navItems
|
@ -18,7 +18,7 @@ const Overlay = ({ children, content, className, show, setShow }: Props) => {
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute z-50 flex max-w-full rounded-lg p-6 text-accent-dark shadow-overlay gradient-popover',
|
||||
'max-w-screen absolute z-50 rounded-lg shadow-overlay gradient-popover',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const SearchInput = () => (
|
||||
<div className='relative text-white'>
|
||||
<span className='absolute inset-y-0 left-0 flex items-center pl-2'>
|
||||
<svg
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth='2'
|
||||
viewBox='0 0 24 24'
|
||||
className='h-6 w-6'
|
||||
>
|
||||
<path d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
className='rounded-md bg-black/70 py-2 pl-10 text-sm text-white focus:outline-none'
|
||||
placeholder='Search'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default SearchInput
|
@ -1,112 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
background?: string
|
||||
diameter?: 60
|
||||
orientation?: any
|
||||
direction?: any
|
||||
value: number
|
||||
label?: string
|
||||
}
|
||||
|
||||
const SemiCircleProgress = ({
|
||||
stroke = '#02B732',
|
||||
strokeWidth = 6,
|
||||
background = '#D0D0CE',
|
||||
diameter = 60,
|
||||
orientation = 'up',
|
||||
direction = 'right',
|
||||
value = 0,
|
||||
label,
|
||||
}: Props) => {
|
||||
const coordinateForCircle = diameter / 2
|
||||
const radius = (diameter - 2 * strokeWidth) / 2
|
||||
const circumference = Math.PI * radius
|
||||
const percentage = value * 100
|
||||
|
||||
let percentageValue
|
||||
if (percentage > 100) {
|
||||
percentageValue = 100
|
||||
} else if (percentage < 0) {
|
||||
percentageValue = 0
|
||||
} else {
|
||||
percentageValue = percentage
|
||||
}
|
||||
|
||||
const semiCirclePercentage = percentageValue * (circumference / 100)
|
||||
|
||||
let rotation
|
||||
if (orientation === 'down') {
|
||||
if (direction === 'left') {
|
||||
rotation = 'rotate(180deg) rotateY(180deg)'
|
||||
} else {
|
||||
rotation = 'rotate(180deg)'
|
||||
}
|
||||
} else {
|
||||
if (direction === 'right') {
|
||||
rotation = 'rotateY(180deg)'
|
||||
}
|
||||
}
|
||||
|
||||
let strokeColorClass = 'stroke-green-500'
|
||||
if (value > 2 / 3) {
|
||||
strokeColorClass = 'stroke-red-500'
|
||||
} else if (value > 1 / 3) {
|
||||
strokeColorClass = 'stroke-yellow-500'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='semicircle-container' style={{ position: 'relative' }}>
|
||||
<svg
|
||||
width={diameter}
|
||||
height={diameter / 2}
|
||||
style={{ transform: rotation, overflow: 'hidden' }}
|
||||
>
|
||||
<circle
|
||||
cx={coordinateForCircle}
|
||||
cy={coordinateForCircle}
|
||||
r={radius}
|
||||
fill='none'
|
||||
stroke={background}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
strokeDashoffset: circumference,
|
||||
}}
|
||||
/>
|
||||
<circle
|
||||
className={strokeColorClass}
|
||||
cx={coordinateForCircle}
|
||||
cy={coordinateForCircle}
|
||||
r={radius}
|
||||
fill='none'
|
||||
// stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
style={{
|
||||
strokeDashoffset: semiCirclePercentage,
|
||||
transition: 'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
{label && (
|
||||
<span
|
||||
className='text-xs'
|
||||
style={{
|
||||
width: '100%',
|
||||
left: '0',
|
||||
textAlign: 'center',
|
||||
bottom: orientation === 'down' ? 'auto' : '-4px',
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SemiCircleProgress
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { ReactNode } from 'react'
|
||||
import React, { LegacyRef, ReactNode } from 'react'
|
||||
|
||||
interface Props extends React.HTMLProps<HTMLAnchorElement> {
|
||||
children?: string | ReactNode
|
||||
@ -8,6 +8,7 @@ interface Props extends React.HTMLProps<HTMLAnchorElement> {
|
||||
externalLink?: boolean
|
||||
textSize?: 'small' | 'medium' | 'large'
|
||||
text?: string | ReactNode
|
||||
uppercase?: boolean
|
||||
}
|
||||
|
||||
const colorClasses = {
|
||||
@ -17,7 +18,7 @@ const colorClasses = {
|
||||
'text-secondary hover:text-secondary-highlight active:text-secondary-highlight-10 focus:text-secondary-highlight',
|
||||
tertiary:
|
||||
'text-secondary-dark/60 hover:text-secondary-dark active:text-secondary-dark-10 focus:text-secondary-dark',
|
||||
quaternary: 'text-transparent text-white/60 hover:text-white active:text-white',
|
||||
quaternary: 'hover:text-white active:text-white',
|
||||
}
|
||||
const textSizeClasses = {
|
||||
small: 'text-sm',
|
||||
@ -25,7 +26,8 @@ const textSizeClasses = {
|
||||
large: 'text-lg',
|
||||
}
|
||||
|
||||
const TextLink = ({
|
||||
const TextLink = React.forwardRef(function TextLink(
|
||||
{
|
||||
children,
|
||||
className = '',
|
||||
color = 'primary',
|
||||
@ -34,23 +36,25 @@ const TextLink = ({
|
||||
href,
|
||||
textSize = 'small',
|
||||
text,
|
||||
uppercase,
|
||||
onClick,
|
||||
...restProps
|
||||
}: Props) => {
|
||||
const linkClasses = classNames(
|
||||
textSizeClasses[textSize],
|
||||
}: Props,
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
className={classNames(
|
||||
uppercase ? `${textSizeClasses[textSize]}-caps` : textSizeClasses[textSize],
|
||||
colorClasses[color],
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
className,
|
||||
)
|
||||
|
||||
return (
|
||||
<a
|
||||
className={linkClasses}
|
||||
)}
|
||||
ref={ref as LegacyRef<HTMLAnchorElement>}
|
||||
target={externalLink ? '_blank' : '_self'}
|
||||
rel='noreferrer'
|
||||
onClick={
|
||||
onClick && href
|
||||
onClick && !href
|
||||
? (e) => {
|
||||
e.preventDefault()
|
||||
if (disabled) return
|
||||
@ -65,6 +69,6 @@ const TextLink = ({
|
||||
{children && children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default TextLink
|
||||
|
@ -1,356 +0,0 @@
|
||||
import { Dialog, Switch, Transition } from '@headlessui/react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import Slider from 'components/Slider'
|
||||
import useWithdrawFunds from 'hooks/mutations/useWithdrawFunds'
|
||||
import useAllBalances from 'hooks/useAllBalances'
|
||||
import useCalculateMaxWithdrawAmount from 'hooks/useCalculateMaxWithdrawAmount'
|
||||
import useCreditAccountPositions from 'hooks/useCreditAccountPositions'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import { formatCurrency } from 'utils/formatters'
|
||||
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
|
||||
import useAccountStats, { AccountStatsAction } from 'hooks/useAccountStats'
|
||||
import { chain } from 'utils/chains'
|
||||
import Button from 'components/Button'
|
||||
import ContainerSecondary from 'components/ContainerSecondary'
|
||||
import ProgressBar from 'components/ProgressBar'
|
||||
import SemiCircleProgress from 'components/SemiCircleProgress'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
|
||||
const WithdrawModal = ({ show, onClose }: any) => {
|
||||
const [amount, setAmount] = useState(0)
|
||||
const [selectedToken, setSelectedToken] = useState('')
|
||||
const [isBorrowEnabled, setIsBorrowEnabled] = useState(false)
|
||||
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
|
||||
selectedAccount ?? '',
|
||||
)
|
||||
|
||||
const { data: balancesData } = useAllBalances()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
const { data: marketsData } = useMarkets()
|
||||
|
||||
const selectedTokenSymbol = getTokenSymbol(selectedToken)
|
||||
const selectedTokenDecimals = getTokenDecimals(selectedToken)
|
||||
|
||||
const tokenAmountInCreditAccount = useMemo(() => {
|
||||
return BigNumber(positionsData?.coins.find((coin) => coin.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
}, [positionsData, selectedTokenDecimals, selectedToken])
|
||||
|
||||
const { actions, borrowAmount, withdrawAmount } = useMemo(() => {
|
||||
const borrowAmount =
|
||||
amount > tokenAmountInCreditAccount
|
||||
? BigNumber(amount)
|
||||
.minus(tokenAmountInCreditAccount)
|
||||
.times(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
: 0
|
||||
|
||||
const withdrawAmount = BigNumber(amount)
|
||||
.times(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
|
||||
return {
|
||||
borrowAmount,
|
||||
withdrawAmount,
|
||||
actions: [
|
||||
{
|
||||
type: 'borrow',
|
||||
amount: borrowAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
{
|
||||
type: 'withdraw',
|
||||
amount: withdrawAmount,
|
||||
denom: selectedToken,
|
||||
},
|
||||
] as AccountStatsAction[],
|
||||
}
|
||||
}, [amount, selectedToken, selectedTokenDecimals, tokenAmountInCreditAccount])
|
||||
|
||||
const accountStats = useAccountStats(actions)
|
||||
|
||||
const { mutate, isLoading } = useWithdrawFunds(withdrawAmount, borrowAmount, selectedToken, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
toast.success(`${amount} ${selectedTokenSymbol} successfully withdrawn`)
|
||||
},
|
||||
})
|
||||
|
||||
const maxWithdrawAmount = useCalculateMaxWithdrawAmount(selectedToken, isBorrowEnabled)
|
||||
|
||||
const walletAmount = useMemo(() => {
|
||||
if (!selectedToken) return 0
|
||||
|
||||
return BigNumber(balancesData?.find((balance) => balance.denom === selectedToken)?.amount ?? 0)
|
||||
.div(10 ** selectedTokenDecimals)
|
||||
.toNumber()
|
||||
}, [balancesData, selectedToken, selectedTokenDecimals])
|
||||
|
||||
useEffect(() => {
|
||||
if (positionsData && positionsData.coins.length > 0) {
|
||||
// initialize selected token when allowedCoins fetch data is available
|
||||
setSelectedToken(positionsData.coins[0].denom)
|
||||
}
|
||||
}, [positionsData])
|
||||
|
||||
const handleTokenChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedToken(e.target.value)
|
||||
|
||||
if (e.target.value !== selectedToken) setAmount(0)
|
||||
}
|
||||
|
||||
const handleValueChange = (value: number) => {
|
||||
if (value > maxWithdrawAmount) {
|
||||
setAmount(maxWithdrawAmount)
|
||||
return
|
||||
}
|
||||
|
||||
setAmount(value)
|
||||
}
|
||||
|
||||
const handleBorrowChange = () => {
|
||||
setIsBorrowEnabled((c) => !c)
|
||||
// reset amount due to max value calculations changing depending on wheter the user is borrowing or not
|
||||
setAmount(0)
|
||||
}
|
||||
|
||||
const getTokenTotalUSDValue = (amount: string, denom: string) => {
|
||||
// early return if prices are not fetched yet
|
||||
if (!tokenPrices) return 0
|
||||
|
||||
return (
|
||||
BigNumber(amount)
|
||||
.div(10 ** getTokenDecimals(denom))
|
||||
.toNumber() * tokenPrices[denom]
|
||||
)
|
||||
}
|
||||
|
||||
const percentageValue = useMemo(() => {
|
||||
if (isNaN(amount) || maxWithdrawAmount === 0) return 0
|
||||
|
||||
return (amount * 100) / maxWithdrawAmount
|
||||
}, [amount, maxWithdrawAmount])
|
||||
|
||||
return (
|
||||
<Transition appear show={show} as={React.Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-80' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4'>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
<Dialog.Panel className='flex w-full max-w-3xl transform overflow-hidden rounded-2xl bg-[#585A74] align-middle shadow-xl transition-all'>
|
||||
{isLoading && (
|
||||
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
<div className='flex w-1/2 flex-col p-4'>
|
||||
<Dialog.Title as='h3' className='mb-4 text-center text-lg font-medium'>
|
||||
Withdraw from Account {selectedAccount}
|
||||
</Dialog.Title>
|
||||
<div>
|
||||
<ContainerSecondary className='mb-3 p-3'>
|
||||
<div className='mb-4 text-sm'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div className='font-bold'>Asset:</div>
|
||||
<select className='bg-transparent' onChange={handleTokenChange}>
|
||||
{positionsData?.coins?.map((coin) => (
|
||||
<option key={coin.denom} value={coin.denom}>
|
||||
{getTokenSymbol(coin.denom)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div className='font-bold'>Amount:</div>
|
||||
<input
|
||||
type='number'
|
||||
className='border border-black/50 bg-transparent px-2'
|
||||
value={amount}
|
||||
min='0'
|
||||
onChange={(e) => handleValueChange(e.target.valueAsNumber)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className='mb-2 text-sm'>In wallet: {walletAmount.toLocaleString()}</p>
|
||||
<Slider
|
||||
className='mb-6'
|
||||
value={percentageValue}
|
||||
onChange={(value) => {
|
||||
const decimal = value[0] / 100
|
||||
// limit decimal precision based on token contract decimals
|
||||
const newAmount = Number(
|
||||
(decimal * maxWithdrawAmount).toFixed(selectedTokenDecimals),
|
||||
)
|
||||
|
||||
setAmount(newAmount)
|
||||
}}
|
||||
onMaxClick={() => setAmount(maxWithdrawAmount)}
|
||||
/>
|
||||
</ContainerSecondary>
|
||||
<ContainerSecondary className='mb-10 flex items-center justify-between'>
|
||||
<div className='text-left'>
|
||||
<h3 className='font-bold'>Withdraw with borrowing</h3>
|
||||
<div className='text-sm text-[#585A74]/50'>Explanation....</div>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={isBorrowEnabled}
|
||||
onChange={handleBorrowChange}
|
||||
className={`${
|
||||
isBorrowEnabled ? 'bg-blue-600' : 'bg-gray-400'
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
isBorrowEnabled ? 'translate-x-6' : 'translate-x-1'
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</Switch>
|
||||
</ContainerSecondary>
|
||||
</div>
|
||||
<Button className='mt-auto w-full' onClick={() => mutate()}>
|
||||
Withdraw
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex w-1/2 flex-col justify-center bg-[#4A4C60] p-4'>
|
||||
<p className='text-bold mb-3 text-xs uppercase text-white/50'>About</p>
|
||||
<h4 className='mb-4 text-xl'>Subaccount {selectedAccount}</h4>
|
||||
<div className='mb-2 rounded-md border border-white/20 p-3'>
|
||||
{accountStats && (
|
||||
<div className='flex items-center gap-x-3'>
|
||||
<p className='flex-1 text-xs'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats.netWorth)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</p>
|
||||
{/* TOOLTIP */}
|
||||
<div title={`${String(accountStats.currentLeverage.toFixed(1))}x`}>
|
||||
<SemiCircleProgress
|
||||
value={accountStats.currentLeverage / accountStats.maxLeverage}
|
||||
label='Lvg'
|
||||
/>
|
||||
</div>
|
||||
<SemiCircleProgress value={accountStats.risk} label='Risk' />
|
||||
<ProgressBar value={accountStats.health} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='mb-2 rounded-md border border-white/20 p-3 text-sm'>
|
||||
<div className='mb-1 flex justify-between'>
|
||||
<div>Total Position:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalPosition ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<div>Total Liabilities:</div>
|
||||
<div className='font-semibold'>
|
||||
{formatCurrency(
|
||||
BigNumber(accountStats?.totalDebt ?? 0)
|
||||
.dividedBy(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toNumber(),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='rounded-md border border-white/20 p-3'>
|
||||
<h4 className='mb-2 font-bold'>Balances</h4>
|
||||
{isLoadingPositions ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<table className='w-full border-separate border-spacing-1'>
|
||||
<thead className='text-left text-xs font-semibold'>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Value</th>
|
||||
<th>Size</th>
|
||||
<th className='text-right'>APY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{accountStats?.assets.map((coin) => (
|
||||
<tr key={coin.denom} className='text-xs text-white/50'>
|
||||
<td>{getTokenSymbol(coin.denom)}</td>
|
||||
<td>
|
||||
{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</td>
|
||||
<td>
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: getTokenDecimals(coin.denom),
|
||||
})}
|
||||
</td>
|
||||
<td className='text-right'>-</td>
|
||||
</tr>
|
||||
))}
|
||||
{accountStats?.debts.map((coin) => (
|
||||
<tr key={coin.denom} className='text-xs text-red-500'>
|
||||
<td className='text-white/50'>{getTokenSymbol(coin.denom)}</td>
|
||||
<td>
|
||||
-{formatCurrency(getTokenTotalUSDValue(coin.amount, coin.denom))}
|
||||
</td>
|
||||
<td>
|
||||
-
|
||||
{BigNumber(coin.amount)
|
||||
.div(10 ** getTokenDecimals(coin.denom))
|
||||
.toNumber()
|
||||
.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 6,
|
||||
})}
|
||||
</td>
|
||||
<td className='text-right'>
|
||||
-{(Number(marketsData?.[coin.denom].borrow_rate) * 100).toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default WithdrawModal
|
@ -53,15 +53,19 @@ const calculateStatsFromAccountPositions = (assets: Asset[], debts: Debt[]) => {
|
||||
|
||||
const netWorth = BigNumber(totalPosition).minus(totalDebt).toNumber()
|
||||
|
||||
const liquidationLTVsWeightedAverage = BigNumber(totalWeightedPositions)
|
||||
.div(totalPosition)
|
||||
.toNumber()
|
||||
const liquidationLTVsWeightedAverage =
|
||||
totalWeightedPositions === 0
|
||||
? 0
|
||||
: BigNumber(totalWeightedPositions).div(totalPosition).toNumber()
|
||||
|
||||
const maxLeverage = BigNumber(1)
|
||||
.div(BigNumber(1).minus(liquidationLTVsWeightedAverage))
|
||||
.toNumber()
|
||||
const currentLeverage = BigNumber(totalPosition).div(netWorth).toNumber()
|
||||
const health = BigNumber(1).minus(BigNumber(currentLeverage).div(maxLeverage)).toNumber() || 1
|
||||
const currentLeverage = netWorth === 0 ? 0 : BigNumber(totalPosition).div(netWorth).toNumber()
|
||||
const health =
|
||||
maxLeverage === 0
|
||||
? 1
|
||||
: BigNumber(1).minus(BigNumber(currentLeverage).div(maxLeverage)).toNumber() || 1
|
||||
|
||||
const risk = liquidationLTVsWeightedAverage
|
||||
? getRiskFromAverageLiquidationLTVs(liquidationLTVsWeightedAverage)
|
||||
|
@ -13,6 +13,15 @@ const nextConfig = {
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
|
||||
hideSourceMaps: true,
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/trade',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/i,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import BigNumber from 'bignumber.js'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import BorrowTable from 'components/Borrow/BorrowTable'
|
||||
import BorrowModal from 'components/BorrowModal'
|
||||
@ -116,7 +116,7 @@ const Borrow = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='flex w-full items-start gap-4'>
|
||||
<div className='flex-1'>
|
||||
<Card className='mb-4'>
|
||||
<div>
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import Text from 'components/Text'
|
||||
|
||||
const Council = () => {
|
||||
return (
|
||||
<div>
|
||||
<Card>Council Placeholder</Card>
|
||||
<div className='flex w-full'>
|
||||
<Card>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Council Placeholder
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import Text from 'components/Text'
|
||||
|
||||
const Earn = () => {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<Card className='flex-1'>Yield Module</Card>
|
||||
<Card className='w-[450px]'>Placeholder</Card>
|
||||
<div className='flex w-full gap-4'>
|
||||
<Card>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Yield Module
|
||||
</Text>
|
||||
</Card>
|
||||
<div className='w-[450px]'>
|
||||
<Card>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Placeholder
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
369
pages/index.tsx
@ -1,369 +0,0 @@
|
||||
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import type { NextPage } from 'next'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import CircularProgress from 'components/CircularProgress'
|
||||
import Text from 'components/Text'
|
||||
import { contractAddresses } from 'config/contracts'
|
||||
import useMarkets from 'hooks/useMarkets'
|
||||
import useTokenPrices from 'hooks/useTokenPrices'
|
||||
import useCreditManagerStore from 'stores/useCreditManagerStore'
|
||||
import useWalletStore from 'stores/useWalletStore'
|
||||
import { queryKeys } from 'types/query-keys-factory'
|
||||
import { chain } from 'utils/chains'
|
||||
import { hardcodedFee } from 'utils/contants'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [sendAmount, setSendAmount] = useState('')
|
||||
const [recipientAddress, setRecipientAddress] = useState('')
|
||||
|
||||
const [allTokens, setAllTokens] = useState<string[] | null>(null)
|
||||
const [walletTokens, setWalletTokens] = useState<string[] | null>(null)
|
||||
|
||||
const [borrowAmount, setBorrowAmount] = useState(0)
|
||||
|
||||
const [error, setError] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const address = useWalletStore((s) => s.address)
|
||||
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [signingClient, setSigningClient] = useState<SigningCosmWasmClient>()
|
||||
|
||||
const { data: marketsData } = useMarkets()
|
||||
const { data: tokenPrices } = useTokenPrices()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (!window.keplr) return
|
||||
|
||||
const offlineSigner = window.keplr.getOfflineSigner(chain.chainId)
|
||||
const clientInstance = await SigningCosmWasmClient.connectWithSigner(chain.rpc, offlineSigner)
|
||||
|
||||
setSigningClient(clientInstance)
|
||||
})()
|
||||
}, [address])
|
||||
|
||||
const handleSendClick = async () => {
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// console.log(await signingClient.getHeight());
|
||||
|
||||
// console.log(
|
||||
// "contract info",
|
||||
// signingClient.getContract(
|
||||
// "osmo1zf26ahe5gqjtvnedh7ems7naf2wtw3z4ll6atf3t0hptal8ss4vq2mlx6w"
|
||||
// )
|
||||
// );
|
||||
|
||||
const res = await signingClient?.sendTokens(
|
||||
address,
|
||||
recipientAddress,
|
||||
[
|
||||
{
|
||||
denom: chain.stakeCurrency.coinMinimalDenom,
|
||||
amount: BigNumber(sendAmount)
|
||||
.times(10 ** chain.stakeCurrency.coinDecimals)
|
||||
.toString(),
|
||||
},
|
||||
],
|
||||
hardcodedFee,
|
||||
)
|
||||
|
||||
console.log('txResponse', res)
|
||||
toast.success(
|
||||
<div>
|
||||
<a
|
||||
href={`https://testnet.mintscan.io/osmosis-testnet/txs/${res?.transactionHash}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Check transaction
|
||||
</a>
|
||||
</div>,
|
||||
{ autoClose: false },
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateCreditAccount = async () => {
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// 200000 gas used
|
||||
const executeMsg = {
|
||||
create_credit_account: {},
|
||||
}
|
||||
|
||||
const createResult = await signingClient?.execute(
|
||||
address,
|
||||
contractAddresses.creditManager,
|
||||
executeMsg,
|
||||
hardcodedFee,
|
||||
)
|
||||
|
||||
console.log('mint result', createResult)
|
||||
toast.success(
|
||||
<div>
|
||||
<a
|
||||
href={`https://testnet.mintscan.io/osmosis-testnet/txs/${createResult?.transactionHash}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Check transaction
|
||||
</a>
|
||||
</div>,
|
||||
{ autoClose: false },
|
||||
)
|
||||
|
||||
queryClient.invalidateQueries(queryKeys.creditAccounts(address))
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/mars-protocol/rover/blob/master/scripts/types/generated/account-nft/AccountNft.types.ts
|
||||
const handleGetCreditAccounts = async () => {
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const allTokensQueryMsg = {
|
||||
all_tokens: {},
|
||||
}
|
||||
|
||||
const allTokensResponse = await signingClient?.queryContractSmart(
|
||||
contractAddresses.accountNft,
|
||||
allTokensQueryMsg,
|
||||
)
|
||||
|
||||
setAllTokens(allTokensResponse.tokens)
|
||||
|
||||
console.log('all tokens', allTokensResponse)
|
||||
|
||||
// Returns de owner of a specific "credit account"
|
||||
// const ownerOfQueryMsg = {
|
||||
// owner_of: {
|
||||
// include_expired: false,
|
||||
// token_id: "1",
|
||||
// },
|
||||
// };
|
||||
|
||||
// const ownerResponse = await signingClient.queryContractSmart(
|
||||
// contractAddresses.accountNft,
|
||||
// ownerOfQueryMsg
|
||||
// );
|
||||
|
||||
// console.log("res owner", ownerResponse);
|
||||
|
||||
const tokensQueryMsg = {
|
||||
tokens: {
|
||||
owner: address,
|
||||
},
|
||||
}
|
||||
|
||||
const tokensResponse = await signingClient?.queryContractSmart(
|
||||
contractAddresses.accountNft,
|
||||
tokensQueryMsg,
|
||||
)
|
||||
|
||||
console.log('res tokens', tokensResponse)
|
||||
setWalletTokens(tokensResponse.tokens)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBorrowClick = async () => {
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
if (!selectedAccount) return
|
||||
|
||||
const executeMsg = {
|
||||
update_credit_account: {
|
||||
account_id: selectedAccount,
|
||||
actions: [
|
||||
{
|
||||
borrow: {
|
||||
denom: 'uosmo',
|
||||
amount: BigNumber(borrowAmount)
|
||||
.times(10 ** 6)
|
||||
.toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const borrowResult = await signingClient?.execute(
|
||||
address,
|
||||
contractAddresses.creditManager,
|
||||
executeMsg,
|
||||
hardcodedFee,
|
||||
)
|
||||
|
||||
console.log('borrow result', borrowResult)
|
||||
toast.success(
|
||||
<div>
|
||||
<a
|
||||
href={`https://testnet.mintscan.io/osmosis-testnet/txs/${borrowResult?.transactionHash}`}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Check transaction
|
||||
</a>
|
||||
</div>,
|
||||
{ autoClose: false },
|
||||
)
|
||||
|
||||
queryClient.invalidateQueries(queryKeys.creditAccounts(address))
|
||||
queryClient.invalidateQueries(queryKeys.creditAccountsPositions(selectedAccount))
|
||||
queryClient.invalidateQueries(queryKeys.tokenBalance(address, 'uosmo'))
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-6xl flex-col gap-y-6'>
|
||||
<Card>
|
||||
<Text tag='h4' className='mb-5'>
|
||||
Send Tokens
|
||||
</Text>
|
||||
<div className='mb-5 flex flex-wrap gap-2'>
|
||||
<div>
|
||||
<Text>Address:</Text>
|
||||
<input
|
||||
className='rounded-lg bg-black/40 px-3 py-1'
|
||||
value={recipientAddress}
|
||||
placeholder='address'
|
||||
onChange={(e) => setRecipientAddress(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text>Amount:</Text>
|
||||
<input
|
||||
type='number'
|
||||
className='rounded-lg bg-black/40 px-3 py-1'
|
||||
value={sendAmount}
|
||||
placeholder='amount'
|
||||
onChange={(e) => setSendAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button color='secondary' onClick={handleSendClick}>
|
||||
Send
|
||||
</Button>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text tag='h4' className='mb-5'>
|
||||
Create Credit Account (Mint NFT)
|
||||
</Text>
|
||||
<Button color='secondary' onClick={handleCreateCreditAccount}>
|
||||
Create
|
||||
</Button>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text tag='h4' className='mb-5'>
|
||||
Get all Credit Accounts
|
||||
</Text>
|
||||
<Button color='secondary' onClick={handleGetCreditAccounts}>
|
||||
Fetch
|
||||
</Button>
|
||||
</Card>
|
||||
<Card>
|
||||
<Text tag='h4' className='mb-5'>
|
||||
Borrow OSMO
|
||||
</Text>
|
||||
<input
|
||||
className='rounded-lg bg-black/40 px-3 py-1'
|
||||
type='number'
|
||||
onChange={(e) => setBorrowAmount(e.target.valueAsNumber)}
|
||||
/>
|
||||
<Button className='ml-4' color='secondary' onClick={handleBorrowClick}>
|
||||
Borrow
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<div>
|
||||
{allTokens && (
|
||||
<div className='mb-4'>
|
||||
<div className='flex items-end'>
|
||||
<Text size='2xl'>All Tokens</Text>
|
||||
<Text size='sm' className='ml-2'>
|
||||
- {allTokens.length} total
|
||||
</Text>
|
||||
</div>
|
||||
{allTokens.map((token) => (
|
||||
<p key={token}>{token}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{walletTokens && (
|
||||
<>
|
||||
<div className='flex items-end'>
|
||||
<Text size='2xl'>Your Tokens</Text>
|
||||
<Text size='sm' className='ml-2'>
|
||||
- {walletTokens.length} total
|
||||
</Text>
|
||||
</div>
|
||||
{walletTokens.map((token) => (
|
||||
<p key={token}>{token}</p>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{tokenPrices && (
|
||||
<div className='mb-6'>
|
||||
<h3 className='text-xl font-semibold'>Token Prices:</h3>
|
||||
<pre>{JSON.stringify(tokenPrices, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
{marketsData && (
|
||||
<div>
|
||||
<h3 className='text-xl font-semibold'>Markets Data:</h3>
|
||||
<pre>{JSON.stringify(marketsData, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<div className='mt-8 rounded-base bg-white p-4'>
|
||||
<Text className='text-red-500'>{error}</Text>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
@ -1,10 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import Modal from 'components/Modal'
|
||||
import Number from 'components/Number'
|
||||
import Overlay from 'components/Overlay'
|
||||
import FormattedNumber from 'components/FormattedNumber'
|
||||
import Text from 'components/Text'
|
||||
|
||||
const mockedAccounts = [
|
||||
@ -51,17 +46,12 @@ const mockedAccounts = [
|
||||
]
|
||||
|
||||
const Portfolio = () => {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
const [open, setOpen] = useState<boolean>(true)
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex w-full items-start gap-4'>
|
||||
<Card className='flex-1'>
|
||||
<span onClick={() => setShow(!show)} role='button'>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Portfolio Module
|
||||
</span>
|
||||
<Overlay show={show} setShow={setShow}>
|
||||
A test overlay
|
||||
</Overlay>
|
||||
</Text>
|
||||
</Card>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
{mockedAccounts.map((account) => (
|
||||
@ -72,7 +62,7 @@ const Portfolio = () => {
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.networth} animate={true} prefix='$' />
|
||||
<FormattedNumber amount={account.networth} animate prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Net worth
|
||||
@ -80,7 +70,7 @@ const Portfolio = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.totalPositionValue} animate={true} prefix='$' />
|
||||
<FormattedNumber amount={account.totalPositionValue} animate prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Total Position Value
|
||||
@ -88,7 +78,7 @@ const Portfolio = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.debt} animate={true} prefix='$' />
|
||||
<FormattedNumber amount={account.debt} animate prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Debt
|
||||
@ -96,9 +86,9 @@ const Portfolio = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Text className={account.profit > 0 ? 'text-green-400' : 'text-red-500'}>
|
||||
<Number
|
||||
<FormattedNumber
|
||||
amount={account.debt}
|
||||
animate={true}
|
||||
animate
|
||||
prefix={account.profit > 0 ? '+$' : '$'}
|
||||
/>
|
||||
</Text>
|
||||
@ -108,7 +98,7 @@ const Portfolio = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.leverage} minDecimals={0} suffix='x' />
|
||||
<FormattedNumber amount={account.leverage} minDecimals={0} suffix='x' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Current Leverage
|
||||
@ -116,7 +106,7 @@ const Portfolio = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.maxLeverage} minDecimals={0} suffix='x' />
|
||||
<FormattedNumber amount={account.maxLeverage} minDecimals={0} suffix='x' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Max Leverage
|
||||
@ -126,77 +116,6 @@ const Portfolio = () => {
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(!open)
|
||||
}}
|
||||
text='Toggle Modal'
|
||||
/>
|
||||
</div>
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
{mockedAccounts.map((account) => (
|
||||
<div key={account.id} className='mb-8'>
|
||||
<Text size='lg' uppercase={true} className='mb-4 px-10 text-center'>
|
||||
{account.label}
|
||||
</Text>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.networth} animate={true} prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Net worth
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.totalPositionValue} animate={true} prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Total Position Value
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.debt} animate={true} prefix='$' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Debt
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text className={account.profit > 0 ? 'text-green-400' : 'text-red-500'}>
|
||||
<Number
|
||||
amount={account.debt}
|
||||
animate={true}
|
||||
prefix={account.profit > 0 ? '+$' : '$'}
|
||||
/>
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
P&L
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.leverage} minDecimals={0} suffix='x' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Current Leverage
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text>
|
||||
<Number amount={account.maxLeverage} minDecimals={0} suffix='x' />
|
||||
</Text>
|
||||
<Text size='sm' className='text-white/40'>
|
||||
Max Leverage
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,22 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
import Card from 'components/Card'
|
||||
import Text from 'components/Text'
|
||||
import TradeActionModule from 'components/Trade/TradeActionModule'
|
||||
|
||||
const Trade = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-4 flex gap-4'>
|
||||
<Card className='flex-1'>Graph/Tradingview Module</Card>
|
||||
<div className='flex w-full flex-wrap'>
|
||||
<div className='mb-4 flex flex-grow gap-4'>
|
||||
<Card className='flex-1'>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Tradingview Graph
|
||||
</Text>
|
||||
</Card>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Card>
|
||||
<TradeActionModule />
|
||||
</Card>
|
||||
<Card>Orderbook module (optional)</Card>
|
||||
<Card>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Orderbook module (optional)
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>Credit Account essential module</Card>
|
||||
</div>
|
||||
<Card>Trader order overview</Card>
|
||||
<Card>
|
||||
<Text size='lg' uppercase={true}>
|
||||
Order history
|
||||
</Text>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
public/images/fund-modal-bg.png
Normal file
After Width: | Height: | Size: 210 KiB |
@ -13,7 +13,7 @@ interface CreditManagerStore {
|
||||
const useCreditManagerStore = create<CreditManagerStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
isOpen: false,
|
||||
isOpen: true,
|
||||
selectedAccount: null,
|
||||
actions: {
|
||||
toggleCreditManager: () => set(() => ({ isOpen: !get().isOpen })),
|
||||
|
@ -12,6 +12,8 @@ module.exports = {
|
||||
'text-xs',
|
||||
'text-sm-caps',
|
||||
'text-sm',
|
||||
'text-base-caps',
|
||||
'text-base',
|
||||
'text-lg-caps',
|
||||
'text-lg',
|
||||
'text-xl-caps',
|
||||
@ -29,9 +31,11 @@ module.exports = {
|
||||
extend: {
|
||||
animation: {
|
||||
progress: 'spin 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite',
|
||||
fadein: 'fadein 1s ease-in-out forwards',
|
||||
},
|
||||
backgroundImage: {
|
||||
mars: 'url(/bg.svg)',
|
||||
mars: 'url(/images/bg.svg)',
|
||||
'fund-modal': 'url(/images/fund-modal-bg.png)',
|
||||
},
|
||||
backgroundSize: {
|
||||
desktop: '100% auto',
|
||||
@ -71,6 +75,7 @@ module.exports = {
|
||||
'grey-highlight': '#4c4c4c',
|
||||
'grey-light': '#bfbfbf',
|
||||
'grey-medium': '#5f697a',
|
||||
header: 'rgba(59, 25, 40, 0.4);',
|
||||
input: '#282a33',
|
||||
loss: '#f96363',
|
||||
mars: '#a03b45',
|
||||
@ -111,6 +116,12 @@ module.exports = {
|
||||
hueRotate: {
|
||||
'-82': '-82deg',
|
||||
},
|
||||
keyframes: {
|
||||
fadein: {
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
},
|
||||
letterSpacing: {
|
||||
normal: 0,
|
||||
wide: '2px',
|
||||
@ -204,6 +215,13 @@ module.exports = {
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
letterSpacing: theme('letterSpacing.wider'),
|
||||
},
|
||||
'.text-base-caps': {
|
||||
fontSize: '15px',
|
||||
lineHeight: '20px',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
letterSpacing: theme('letterSpacing.wider'),
|
||||
},
|
||||
'.text-lg-caps': {
|
||||
fontSize: '16.88px',
|
||||
lineHeight: '24px',
|
||||
|
22
yarn.lock
@ -1432,11 +1432,6 @@
|
||||
resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.0.tgz"
|
||||
integrity sha512-/nDsijOXRwXVLpUBEiYuWguIBSIN3ZbKyah+KPUiD8bdIKtX1U/k+qLYUEr7NCQnSF2e4w1dr8me42ECuG3cvw==
|
||||
|
||||
"@heroicons/react@^2.0.11":
|
||||
version "2.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.11.tgz#2c6cf4c66d81142ec87c102502407d8c353558bb"
|
||||
integrity sha512-bASjOgSSaYj8HqXWsOqaBiB6ZLalE/g90WYGgZ5lPm4KCCG7wPXntY4kzHf5NrLh6UBAcnPwvbiw1Ne9GYfJtw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz"
|
||||
@ -2296,18 +2291,6 @@
|
||||
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/lodash.isequal@^4.5.6":
|
||||
version "4.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b"
|
||||
integrity sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.188"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.188.tgz#e4990c4c81f7c9b00c5ff8eae389c10f27980da5"
|
||||
integrity sha512-zmEmF5OIM3rb7SbLCFYoQhO4dGt2FRM9AMkxvA3LaADOF1n8in/zGJlWji9fmafLoNyz+FoL6FE0SLtGIArD7w==
|
||||
|
||||
"@types/long@^4.0.1":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
|
||||
@ -4827,11 +4810,6 @@ lodash.debounce@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||
|