update layout for modal, implement borrow tables (#105)

This commit is contained in:
Bob van der Helm 2023-03-01 13:49:57 +01:00 committed by GitHub
parent 493ec7c44c
commit cbb0700455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 357 additions and 585 deletions

View File

@ -1,4 +1,5 @@
import Background from 'components/Background'
import FetchPrices from 'components/FetchPrices'
import { Modals } from 'components/Modals'
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
import Toaster from 'components/Toaster'
@ -19,6 +20,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</WalletConnectProvider>
<Modals />
<Toaster />
<FetchPrices />
<main className='relative flex lg:min-h-[calc(100vh-120px)]'>
<div className='flex flex-grow flex-col flex-wrap'>{children}</div>
</main>

View File

@ -1,5 +0,0 @@
import Loading from 'components/Loading'
export default function page() {
return <Loading />
}

View File

@ -17,10 +17,7 @@ export default async function page({ params }: { params: PageParams }) {
if (debt) {
prev.active.push({
...borrow,
debt: {
amount: '100000',
value: '12389478321',
},
debt: debt.amount,
})
} else {
prev.available.push(borrow)

View File

@ -10,7 +10,6 @@ import { ArrowRightLine, ChevronDown, ChevronLeft } from 'components/Icons'
import { LabelValuePair } from 'components/LabelValuePair'
import { PositionsList } from 'components/PositionsList'
import { useAccountStats } from 'hooks/data/useAccountStats'
import { useBalances } from 'hooks/data/useBalances'
import { convertFromGwei } from 'utils/formatters'
import { createRiskData } from 'utils/risk'
import useStore from 'store'
@ -23,7 +22,6 @@ export const AccountDetails = () => {
const marketAssets = getMarketAssets()
const baseAsset = getBaseAsset()
const balances = useBalances()
const accountStats = useAccountStats()
const [showManageMenu, setShowManageMenu] = useState(false)
@ -118,7 +116,6 @@ export const AccountDetails = () => {
/>
</div>
{riskData && <RiskChart data={riskData} />}
<PositionsList title='Balances' data={balances} />
</div>
)
}

View File

@ -10,7 +10,7 @@ import { Text } from 'components/Text'
import { useAccountStats } from 'hooks/data/useAccountStats'
import { useCreditAccounts } from 'hooks/queries/useCreditAccounts'
import { getBaseAsset } from 'utils/assets'
import { formatValue } from 'utils/formatters'
import { formatLeverage, formatValue } from 'utils/formatters'
export const AccountStatus = () => {
const baseAsset = getBaseAsset()
@ -41,7 +41,7 @@ export const AccountStatus = () => {
.dividedBy(10 ** baseAsset.decimals)
.toNumber()}
animate
prefix='$: '
options={{ prefix: '$: ' }}
/>
</Text>
@ -50,10 +50,9 @@ export const AccountStatus = () => {
label='Lvg'
tooltip={
<Text size='sm'>
Current Leverage:{' '}
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
Current Leverage: {formatLeverage(accountStats.currentLeverage)}
<br />
Max Leverage: {formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
Max Leverage: {formatLeverage(accountStats.maxLeverage)}
</Text>
}
/>
@ -63,7 +62,8 @@ export const AccountStatus = () => {
label='Risk'
tooltip={
<Text size='sm'>
Current Risk: {formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
Current Risk:{' '}
{formatValue(accountStats.risk * 100, { minDecimals: 0, suffix: '%' })}
</Text>
}
/>

View File

@ -13,7 +13,7 @@ export const ConfirmModal = () => {
const deleteOpen = useStore((s) => s.deleteAccountModal)
return (
<Modal open={createOpen || deleteOpen}>
<Modal title='Confirm' open={createOpen || deleteOpen}>
<div
className={classNames(
'relative flex h-[630px] w-full flex-wrap items-center justify-center p-6',

View File

@ -94,7 +94,7 @@ export const FundAccountModal = () => {
const percentageValue = isNaN(amount) ? 0 : (amount * 100) / walletAmount
return (
<Modal open={open} setOpen={setOpen}>
<Modal title='Fund account' open={open} setOpen={setOpen}>
<div className='flex min-h-[520px] w-full'>
{balanceIsLoading && (
<div className='absolute inset-0 z-40 grid place-items-center bg-black/50'>

View File

@ -25,11 +25,13 @@ export const RiskChart = ({ data }: RiskChartProps) => {
<FormattedNumber
className='px-3 pb-2 text-lg'
amount={currentRisk * 100}
maxDecimals={0}
minDecimals={0}
options={{
maxDecimals: 0,
minDecimals: 0,
prefix: 'Risk score: ',
suffix: '/100',
}}
animate
prefix='Risk Score: '
suffix='/100'
/>
<div className='-ml-6 h-[100px] w-[412px]'>
<ResponsiveContainer width='100%' height='100%'>
@ -77,7 +79,9 @@ export const RiskChart = ({ data }: RiskChartProps) => {
return (
<div className='max-w-[320px] rounded-lg px-4 py-2 shadow-tooltip gradient-tooltip '>
<Text size='sm'>{moment(label).format('MM-DD-YYYY')}</Text>
<Text size='sm'>Risk: {formatValue(risk, 0, 0, true, false, '%')}</Text>
<Text size='sm'>
Risk: {formatValue(risk, { minDecimals: 0, maxDecimals: 0, suffix: '%' })}
</Text>
</div>
)
}

View File

@ -5,7 +5,7 @@ import React, { useEffect, useMemo, useState } from 'react'
import { toast } from 'react-toastify'
import { BorrowCapacity } from 'components/BorrowCapacity'
import { convertFromGwei, formatValue } from 'utils/formatters'
import { convertFromGwei, formatLeverage, formatValue } from 'utils/formatters'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
import { CircularProgress } from 'components/CircularProgress'
import { Button } from 'components/Button'
@ -17,7 +17,6 @@ import { LabelValuePair } from 'components/LabelValuePair'
import { Modal } from 'components/Modal'
import { PositionsList } from 'components/PositionsList'
import { useAccountStats } from 'hooks/data/useAccountStats'
import { useBalances } from 'hooks/data/useBalances'
import { useCalculateMaxWithdrawAmount } from 'hooks/data/useCalculateMaxWithdrawAmount'
import { useWithdrawFunds } from 'hooks/mutations/useWithdrawFunds'
import { useCreditAccountPositions } from 'hooks/queries/useCreditAccountPositions'
@ -46,7 +45,6 @@ export const WithdrawModal = () => {
// EXTERNAL HOOKS
// ---------------
const { data: tokenPrices } = useTokenPrices()
const balances = useBalances()
const selectedTokenSymbol = getTokenSymbol(selectedToken, marketAssets)
const selectedTokenDecimals = getTokenDecimals(selectedToken, marketAssets)
@ -153,7 +151,7 @@ export const WithdrawModal = () => {
}
return (
<Modal open={open} setOpen={setOpen}>
<Modal title='Withdraw' 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'>
@ -205,7 +203,7 @@ export const WithdrawModal = () => {
</div>
</div>
<Text size='xs' uppercase className='mb-2 text-white/60'>
Available: {formatValue(maxWithdrawAmount, 0, 4, true, false, false, false, false)}
Available: {formatValue(maxWithdrawAmount, { minDecimals: 0, maxDecimals: 4 })}
</Text>
<Slider
className='mb-6'
@ -265,7 +263,7 @@ export const WithdrawModal = () => {
amount={BigNumber(accountStats.netWorth)
.dividedBy(10 ** baseAsset.decimals)
.toNumber()}
prefix='$: '
options={{ prefix: '$: ' }}
animate
/>
</Text>
@ -275,11 +273,9 @@ export const WithdrawModal = () => {
label='Lvg'
tooltip={
<Text size='sm'>
Current Leverage:{' '}
{formatValue(accountStats.currentLeverage, 0, 2, true, false, 'x')}
Current Leverage: {formatLeverage(accountStats.currentLeverage)}
<br />
Max Leverage:{' '}
{formatValue(accountStats.maxLeverage, 0, 2, true, false, 'x')}
Max Leverage: {formatLeverage(accountStats.maxLeverage)}
</Text>
}
/>
@ -288,7 +284,8 @@ export const WithdrawModal = () => {
label='Risk'
tooltip={
<Text size='sm'>
Current Risk: {formatValue(accountStats.risk * 100, 0, 2, true, false, '%')}
Current Risk:{' '}
{formatValue(accountStats.risk * 100, { minDecimals: 0, suffix: '%' })}
</Text>
}
/>
@ -331,7 +328,6 @@ export const WithdrawModal = () => {
}}
/>
</div>
<PositionsList title='Balances' data={balances} />
</div>
</div>
</div>

View File

@ -0,0 +1,27 @@
import { FormattedNumber } from './FormattedNumber'
import TitleAndSubCell from './TitleAndSubCell'
interface Props {
asset: Asset
amount: string
}
export default function AmountAndValue(props: Props) {
return (
<TitleAndSubCell
title={
<FormattedNumber
amount={props.amount}
options={{ decimals: props.asset.decimals, abbreviated: true }}
/>
}
sub={
<FormattedNumber
amount={props.amount}
options={{ prefix: '$', abbreviated: true, decimals: props.asset.decimals }}
/>
}
className='justify-end'
/>
)
}

View File

@ -1,11 +1,14 @@
'use client'
import React from 'react'
import { Row } from '@tanstack/react-table'
import { getMarketAssets } from 'utils/assets'
import { Button } from 'components/Button'
import useStore from 'store'
type AssetRowProps = {
row: Row<BorrowAsset>
row: Row<BorrowAsset | BorrowAssetActive>
onBorrowClick: () => void
onRepayClick: () => void
resetExpanded: (defaultState?: boolean | undefined) => void
@ -14,9 +17,22 @@ type AssetRowProps = {
export default function AssetExpanded(props: AssetRowProps) {
const marketAssets = getMarketAssets()
const asset = marketAssets.find((asset) => asset.denom === props.row.original.denom)
let isActive: boolean = false
if ((props.row.original as BorrowAssetActive)?.debt) {
isActive = true
}
if (!asset) return null
function borrowHandler() {
useStore.setState({ borrowModal: true })
}
function repayHandler() {
useStore.setState({ repayModal: true })
}
return (
<tr
key={props.row.id}
@ -28,10 +44,14 @@ export default function AssetExpanded(props: AssetRowProps) {
!isExpanded && props.row.toggleExpanded()
}}
>
<td colSpan={4}>
<td colSpan={isActive ? 5 : 4}>
<div className='flex justify-end p-4'>
<Button color='secondary' text='CLick me' onClick={() => {}} />
<Button color='primary' text='CLick me' />
<Button
onClick={borrowHandler}
color='primary'
text={isActive ? 'Borrow more' : 'Borrow'}
/>
{isActive && <Button color='primary' text='Repay' />}
</div>
</td>
</tr>

View File

@ -4,7 +4,7 @@ import { flexRender, Row } from '@tanstack/react-table'
import { getMarketAssets } from 'utils/assets'
type AssetRowProps = {
row: Row<BorrowAsset>
row: Row<BorrowAsset | BorrowAssetActive>
resetExpanded: (defaultState?: boolean | undefined) => void
}

View File

@ -15,6 +15,11 @@ import classNames from 'classnames'
import { AssetRow } from 'components/Borrow/AssetRow'
import { ChevronDown, ChevronUp } from 'components/Icons'
import { getMarketAssets } from 'utils/assets'
import { Text } from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { FormattedNumber } from 'components/FormattedNumber'
import AmountAndValue from 'components/AmountAndValue'
import { formatPercent } from 'utils/formatters'
import AssetExpanded from './AssetExpanded'
@ -26,7 +31,7 @@ export const BorrowTable = (props: Props) => {
const [sorting, setSorting] = React.useState<SortingState>([])
const marketAssets = getMarketAssets()
const columns = React.useMemo<ColumnDef<BorrowAsset>[]>(
const columns = React.useMemo<ColumnDef<BorrowAsset | BorrowAssetActive>[]>(
() => [
{
header: 'Asset',
@ -37,12 +42,9 @@ export const BorrowTable = (props: Props) => {
if (!asset) return null
return (
<div className='flex flex-1 items-center'>
<div className='flex flex-1 items-center gap-3'>
<Image src={asset.logo} alt='token' width={32} height={32} />
<div className='pl-2'>
<div>{asset.symbol}</div>
<div className='text-xs'>{asset.name}</div>
</div>
<TitleAndSubCell title={asset.symbol} sub={asset.name} />
</div>
)
},
@ -50,17 +52,38 @@ export const BorrowTable = (props: Props) => {
{
accessorKey: 'borrowRate',
header: 'Borrow Rate',
cell: ({ row }) => <div>{(Number(row.original.borrowRate) * 100).toFixed(2)}%</div>,
cell: ({ row }) => (
<Text className='justify-end' size='sm'>
{formatPercent(row.original.borrowRate)}
</Text>
),
},
...((props.data[0] as BorrowAssetActive)?.debt
? [
{
accessorKey: 'debt',
header: 'Borrowed',
cell: (info: any) => {
const borrowAsset = info.row.original as BorrowAssetActive
const asset = marketAssets.find((asset) => asset.denom === borrowAsset.denom)
if (!asset) return null
return <AmountAndValue asset={asset} amount={borrowAsset.debt} />
},
},
]
: []),
{
accessorKey: 'liquidity',
header: 'Liquidity Available',
cell: ({ row }) => (
<div className='items-right flex flex-col'>
<div className=''>{row.original.liquidity.amount}</div>
<div className='text-xs opacity-60'>${row.original.liquidity.value}</div>
</div>
),
cell: ({ row }) => {
const asset = marketAssets.find((asset) => asset.denom === row.original.denom)
if (!asset) return null
return <AmountAndValue asset={asset} amount={row.original.liquidity.amount} />
},
},
{
accessorKey: 'status',
@ -91,7 +114,7 @@ export const BorrowTable = (props: Props) => {
return (
<table className='w-full'>
<thead className='bg-white/5'>
<thead className='bg-black/20'>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
@ -100,7 +123,7 @@ export const BorrowTable = (props: Props) => {
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className={classNames(
'px-4 py-2',
'px-4 py-3',
header.column.getCanSort() && 'cursor-pointer',
header.id === 'symbol' ? 'text-left' : 'text-right',
)}
@ -109,6 +132,7 @@ export const BorrowTable = (props: Props) => {
className={classNames(
'flex',
header.id === 'symbol' ? 'justify-start' : 'justify-end',
'align-center',
)}
>
{header.column.getCanSort()
@ -124,7 +148,9 @@ export const BorrowTable = (props: Props) => {
),
}[header.column.getIsSorted() as string] ?? null
: null}
<span>{flexRender(header.column.columnDef.header, header.getContext())}</span>
<Text tag='span' size='sm' className='font-normal text-white/40'>
{flexRender(header.column.columnDef.header, header.getContext())}
</Text>
</div>
</th>
)

View File

@ -121,11 +121,12 @@ export const BorrowCapacity = ({
<FormattedNumber
className='text-white'
animate
options={{
minDecimals: decimals,
maxDecimals: decimals,
suffix: '%',
}}
amount={percentOfMaxRound}
minDecimals={decimals}
maxDecimals={decimals}
suffix='%'
abbreviated={false}
/>
)}
</span>

View File

@ -1,314 +1,25 @@
import { Dialog, Switch, Transition } from '@headlessui/react'
import BigNumber from 'bignumber.js'
import React, { useMemo, useState } from 'react'
import { NumericFormat } from 'react-number-format'
import { Button } from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import { ContainerSecondary } from 'components/ContainerSecondary'
import { Gauge } from 'components/Gauge'
import { PositionsList } from 'components/PositionsList'
import { ProgressBar } from 'components/ProgressBar'
import { Slider } from 'components/Slider'
import { Text } from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { useAccountStats } from 'hooks/data/useAccountStats'
import { useBalances } from 'hooks/data/useBalances'
import { useCalculateMaxBorrowAmount } from 'hooks/data/useCalculateMaxBorrowAmount'
import { useBorrowFunds } from 'hooks/mutations/useBorrowFunds'
import { useAllBalances } from 'hooks/queries/useAllBalances'
import { useMarkets } from 'hooks/queries/useMarkets'
import { useTokenPrices } from 'hooks/queries/useTokenPrices'
import useStore from 'store'
import { getBaseAsset, getMarketAssets } from 'utils/assets'
import { formatCurrency, formatValue } from 'utils/formatters'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
type Props = {
show: boolean
onClose: () => void
tokenDenom: string
}
import { Modal } from './Modal'
import TitleAndSubCell from './TitleAndSubCell'
export const BorrowModal = ({ show, onClose, tokenDenom }: Props) => {
const [amount, setAmount] = useState(0)
const [isBorrowToCreditAccount, setIsBorrowToCreditAccount] = useState(false)
export default function BorrowModal() {
const open = useStore((s) => s.borrowModal)
const selectedAccount = useStore((s) => s.selectedAccount)
const marketAssets = getMarketAssets()
const baseAsset = getBaseAsset()
const balances = useBalances()
const { actions, borrowAmount } = useMemo(() => {
const borrowAmount = BigNumber(amount)
.times(10 ** getTokenDecimals(tokenDenom, marketAssets))
.toNumber()
const withdrawAmount = isBorrowToCreditAccount ? 0 : borrowAmount
return {
borrowAmount,
withdrawAmount,
actions: [
{
type: 'borrow',
amount: borrowAmount,
denom: tokenDenom,
},
{
type: 'withdraw',
amount: withdrawAmount,
denom: tokenDenom,
},
] as AccountStatsAction[],
}
}, [amount, isBorrowToCreditAccount, tokenDenom, marketAssets])
const accountStats = useAccountStats(actions)
const tokenSymbol = getTokenSymbol(tokenDenom, marketAssets)
const { mutate, isLoading } = useBorrowFunds(borrowAmount, tokenDenom, !isBorrowToCreditAccount, {
onSuccess: () => {
onClose()
useStore.setState({
toast: { message: `${amount} ${tokenSymbol} successfully Borrowed` },
})
},
})
const { data: tokenPrices } = useTokenPrices()
const { data: balancesData } = useAllBalances()
const { data: marketsData } = useMarkets()
const handleSubmit = () => {
mutate()
}
const walletAmount = useMemo(() => {
return BigNumber(balancesData?.find((balance) => balance.denom === tokenDenom)?.amount ?? 0)
.div(10 ** getTokenDecimals(tokenDenom, marketAssets))
.toNumber()
}, [balancesData, tokenDenom, marketAssets])
const tokenPrice = tokenPrices?.[tokenDenom] ?? 0
const borrowRate = Number(marketsData?.[tokenDenom]?.borrow_rate)
const maxValue = useCalculateMaxBorrowAmount(tokenDenom, isBorrowToCreditAccount)
const percentageValue = useMemo(() => {
if (isNaN(amount) || maxValue === 0) return 0
return (amount * 100) / maxValue
}, [amount, maxValue])
const handleValueChange = (value: number) => {
if (value > maxValue) {
setAmount(maxValue)
return
}
setAmount(value)
}
const handleSliderValueChange = (value: number[]) => {
const decimal = value[0] / 100
const tokenDecimals = getTokenDecimals(tokenDenom, marketAssets)
// limit decimal precision based on token contract decimals
const newAmount = Number((decimal * maxValue).toFixed(tokenDecimals))
setAmount(newAmount)
}
const handleBorrowTargetChange = () => {
setIsBorrowToCreditAccount((c) => !c)
// reset amount due to max value calculations changing depending on borrow target
setAmount(0)
function setOpen(isOpen: boolean) {
useStore.setState({ borrowModal: isOpen })
}
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 flex-1 flex-col p-4'>
<Dialog.Title as='h3' className='mb-4 text-center font-medium'>
Borrow {tokenSymbol}
</Dialog.Title>
<div className='mb-4 flex flex-col gap-2 text-sm'>
<ContainerSecondary>
<p className='mb-1'>
In wallet: {walletAmount.toLocaleString()} {tokenSymbol}
</p>
<p className='mb-5'>Borrow Rate: {(borrowRate * 100).toFixed(2)}%</p>
<div className='mb-7'>
<p className='mb-2 font-semibold uppercase tracking-widest'>Amount</p>
<NumericFormat
className='mb-2 h-[32px] w-full rounded-lg border border-black/50 bg-transparent px-2'
value={amount}
placeholder='0'
allowNegative={false}
onValueChange={(v) => handleValueChange(v.floatValue || 0)}
suffix={` ${tokenSymbol}`}
decimalScale={getTokenDecimals(tokenDenom, marketAssets)}
/>
<div className='flex justify-between text-xs tracking-widest'>
<div>
1 {tokenSymbol} = {formatCurrency(tokenPrice)}
</div>
<div>{formatCurrency(tokenPrice * amount)}</div>
</div>
</div>
<Slider
className='mb-6'
value={percentageValue}
onChange={handleSliderValueChange}
onMaxClick={() => setAmount(maxValue)}
/>
</ContainerSecondary>
<ContainerSecondary className='flex items-center justify-between'>
<div className='flex'>
Borrow to Credit Account{' '}
<Tooltip
className='ml-2'
content={
<>
<Text size='sm' className='mb-2'>
OFF = Borrow directly into your wallet by using your account Assets
as collateral. The borrowed asset will become a liability in your
account.
</Text>
<Text size='sm'>
ON = Borrow into your Account. The borrowed asset will be available
in the account as an Asset and appear also as a liability in your
account.
</Text>
</>
}
/>
</div>
<Switch
checked={isBorrowToCreditAccount}
onChange={handleBorrowTargetChange}
className={`${
isBorrowToCreditAccount ? 'bg-blue-600' : 'bg-gray-400'
} relative inline-flex h-6 w-11 items-center rounded-full`}
>
<span
className={`${
isBorrowToCreditAccount ? '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'
onClick={handleSubmit}
disabled={amount === 0 || !amount}
>
Borrow
</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'>Account {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 ** baseAsset.decimals)
.toNumber(),
)}
</p>
<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>
}
/>
<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 ** baseAsset.decimals)
.toNumber(),
)}
</div>
</div>
<div className='flex justify-between'>
<div>Total Liabilities:</div>
<div className='font-semibold'>
{formatCurrency(
BigNumber(accountStats?.totalDebt ?? 0)
.dividedBy(10 ** baseAsset.decimals)
.toNumber(),
)}
</div>
</div>
</div>
<PositionsList title='Balances' data={balances} />
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
<Modal open={open} setOpen={setOpen} title='Borrow OSMO'>
<div className='flex gap-3'>
<TitleAndSubCell title='10.00%' sub={'Borrow rate'} />
<div className='h-100 w-[1px] bg-white/10'></div>
<TitleAndSubCell title='$200' sub={'Borrowed'} />
<div className='h-100 w-[1px] bg-white/10'></div>
<TitleAndSubCell title='10.5M ($105M)' sub={'Liquidity available'} />
</div>
<div className='flex'></div>
</Modal>
)
}

View File

@ -1,6 +1,8 @@
import classNames from 'classnames'
import { ReactNode } from 'react'
import { Text } from 'components/Text'
interface Props {
title: string
children: ReactNode
@ -15,8 +17,10 @@ export const Card = (props: Props) => {
'h-fit w-full max-w-full overflow-hidden rounded-md border-[1px] border-white/20',
)}
>
<div className='bg-white/10 p-4 font-semibold'>{props.title}</div>
<div className=''>{props.children}</div>
<Text size='lg' className='bg-white/10 p-4 font-semibold'>
{props.title}
</Text>
<div>{props.children}</div>
</section>
)
}

View File

@ -0,0 +1,15 @@
'use client'
import useSWR from 'swr'
import useStore from 'store'
import { getPrices } from 'utils/api'
export default function FetchPrices() {
useSWR('prices', getPrices, {
refreshInterval: 30000,
onSuccess: (prices) => useStore.setState({ prices }),
})
return null
}

View File

@ -5,64 +5,60 @@ import React, { useEffect, useRef } from 'react'
import { animated, useSpring } from 'react-spring'
import useStore from 'store'
import { formatValue } from 'utils/formatters'
import { FormatOptions, formatValue } from 'utils/formatters'
export const FormattedNumber = React.memo(
({
amount,
animate = false,
className,
minDecimals = 2,
maxDecimals = 2,
thousandSeparator = true,
prefix = false,
suffix = false,
rounded = false,
abbreviated = false,
}: FormattedNumberProps) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const prevAmountRef = useRef<number>(0)
interface Props {
amount: number | string
options?: FormatOptions
className?: string
animate?: boolean
}
useEffect(() => {
if (prevAmountRef.current !== Number(amount)) prevAmountRef.current = Number(amount)
}, [amount])
export const FormattedNumber = React.memo((props: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const prevAmountRef = useRef<number>(0)
const springAmount = useSpring({
number: Number(amount),
from: { number: prevAmountRef.current },
config: { duration: 1000 },
})
useEffect(() => {
if (prevAmountRef.current !== Number(props.amount)) prevAmountRef.current = Number(props.amount)
}, [props.amount])
return (prevAmountRef.current === amount && amount === 0) || !animate || !enableAnimations ? (
<span className={classNames('number', className)}>
{formatValue(
amount,
minDecimals,
maxDecimals,
thousandSeparator,
prefix,
suffix,
rounded,
abbreviated,
)}
</span>
) : (
<animated.span className={classNames('number', className)}>
{springAmount.number.to((num) =>
formatValue(
num,
minDecimals,
maxDecimals,
thousandSeparator,
prefix,
suffix,
rounded,
abbreviated,
),
)}
</animated.span>
)
},
)
const springAmount = useSpring({
number: Number(props.amount),
from: { number: prevAmountRef.current },
config: { duration: 1000 },
})
return (prevAmountRef.current === props.amount && props.amount === 0) ||
!props.animate ||
!enableAnimations ? (
<span className={classNames('number', props.className)}>
{formatValue(props.amount, {
minDecimals: props.options?.minDecimals,
maxDecimals: props.options?.maxDecimals,
thousandSeparator: props.options?.thousandSeparator,
prefix: props.options?.prefix,
suffix: props.options?.suffix,
rounded: props.options?.rounded,
abbreviated: props.options?.abbreviated,
decimals: props.options?.decimals,
})}
</span>
) : (
<animated.span className={classNames('number', props.className)}>
{springAmount.number.to((num) =>
formatValue(num, {
minDecimals: props.options?.minDecimals,
maxDecimals: props.options?.maxDecimals,
thousandSeparator: props.options?.thousandSeparator,
prefix: props.options?.prefix,
suffix: props.options?.suffix,
rounded: props.options?.rounded,
abbreviated: props.options?.abbreviated,
decimals: props.options?.decimals,
}),
)}
</animated.span>
)
})
FormattedNumber.displayName = 'FormattedNumber'

View File

@ -2,9 +2,12 @@ import classNames from 'classnames'
import { ReactNode } from 'react'
import { Close } from 'components/Icons'
import { Card } from 'components/Card'
import { Text } from 'components/Text'
import { Button } from './Button'
interface Props {
title: string
children?: ReactNode | string
content?: ReactNode | string
className?: string
@ -12,31 +15,28 @@ interface Props {
setOpen?: (open: boolean) => void
}
export const Modal = ({ children, content, className, open, setOpen }: Props) => {
export const Modal = (props: Props) => {
const onClickAway = () => {
if (setOpen) setOpen(false)
if (props.setOpen) props.setOpen(false)
}
return open ? (
return props.open ? (
<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
title='Modal'
className={classNames('relative z-40 w-[790px] max-w-full p-0', className)}
>
{setOpen && (
<span
className='absolute top-4 right-4 z-50 w-[32px] text-white opacity-60 hover:cursor-pointer hover:opacity-100'
onClick={onClickAway}
role='button'
>
<Close />
</span>
<section
className={classNames(
'relative z-40 w-[790px] max-w-full rounded-md border-[1px] border-white/20 bg-white/5 p-6 backdrop-blur-3xl ',
props.className,
)}
{children ? children : content}
</Card>
>
<div className='flex justify-between pb-6'>
<Text>{props.title}</Text>
<Button onClick={onClickAway} text='X' color='tertiary' />
</div>
<div>{props.children ? props.children : props.content}</div>
</section>
<div
className='fixed top-0 left-0 z-30 block h-full w-full bg-black/70 backdrop-blur hover:cursor-pointer'
className='fixed top-0 left-0 z-30 block h-full w-full bg-black/50 hover:cursor-pointer'
onClick={onClickAway}
role='button'
/>

View File

@ -3,10 +3,13 @@
import { ConfirmModal } from 'components/Account/ConfirmModal'
import { FundAccountModal } from 'components/Account/FundAccountModal'
import BorrowModal from './BorrowModal'
export const Modals = () => (
<>
<FundAccountModal />
{/* <WithdrawModal /> */}
<ConfirmModal />
<BorrowModal />
</>
)

View File

@ -13,7 +13,6 @@ import { useRepayFunds } from 'hooks/mutations/useRepayFunds'
import { useAllBalances } from 'hooks/queries/useAllBalances'
import { useCreditAccountPositions } from 'hooks/queries/useCreditAccountPositions'
import { useTokenPrices } from 'hooks/queries/useTokenPrices'
import { formatCurrency } from 'utils/formatters'
import { getTokenDecimals, getTokenSymbol } from 'utils/tokens'
import { getMarketAssets } from 'utils/assets'
import useStore from 'store'
@ -33,6 +32,7 @@ export const RepayModal = ({ show, onClose, tokenDenom }: Props) => {
const selectedAccount = useStore((s) => s.selectedAccount)
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
const marketAssets = getMarketAssets()
const formatCurrency = useStore((s) => s.formatCurrency)
const tokenSymbol = getTokenSymbol(tokenDenom, marketAssets)
@ -155,9 +155,11 @@ export const RepayModal = ({ show, onClose, tokenDenom }: Props) => {
/>
<div className='flex justify-between text-xs tracking-widest'>
<div>
1 {tokenSymbol} = {formatCurrency(tokenPrice)}
1 {tokenSymbol} = {formatCurrency({ denom: tokenDenom, amount: '1' })}
</div>
<div>
{formatCurrency({ denom: tokenDenom, amount: amount.toString() })}
</div>
<div>{formatCurrency(tokenPrice * amount)}</div>
</div>
</div>

View File

@ -29,6 +29,7 @@ export const Text = ({
<HtmlElement
className={classNames(
className,
'flex items-center',
uppercase ? `text-${sizeClass}-caps` : `text-${sizeClass}`,
monospace && 'number',
)}

View File

@ -0,0 +1,20 @@
import { Text } from 'components/Text'
interface Props {
title: string | React.ReactNode
sub: string | React.ReactNode
className?: string
}
export default function TitleAndSubCell(props: Props) {
return (
<div className='flex flex-col gap-[0.5]'>
<Text className={props.className} size='sm'>
{props.title}
</Text>
<Text size='sm' className={'text-white/50 ' + props.className}>
{props.sub}
</Text>
</div>
)
}

View File

@ -104,7 +104,7 @@ export default function ConnectedButton() {
{isLoading ? (
<CircularProgress size={12} />
) : (
`${formatValue(walletAmount, 2, 2, true, false, ` ${baseAsset.symbol}`)}`
`${formatValue(walletAmount, { suffix: baseAsset.symbol })}`
)}
</div>
</button>

View File

@ -1,36 +0,0 @@
import { useEffect, useState } from 'react'
import { useCreditAccountPositions } from 'hooks/queries/useCreditAccountPositions'
import { useMarkets } from 'hooks/queries/useMarkets'
import { useTokenPrices } from 'hooks/queries/useTokenPrices'
import { formatBalances } from 'utils/balances'
import useStore from 'store'
import { getMarketAssets } from 'utils/assets'
export const useBalances = () => {
const [balanceData, setBalanceData] = useState<PositionsData[]>()
const { data: marketsData } = useMarkets()
const { data: tokenPrices } = useTokenPrices()
const selectedAccount = useStore((s) => s.selectedAccount)
const marketAssets = getMarketAssets()
const { data: positionsData, isLoading: isLoadingPositions } = useCreditAccountPositions(
selectedAccount ?? '',
)
useEffect(() => {
const balances =
positionsData?.coins && tokenPrices
? formatBalances(positionsData.coins, tokenPrices, false, marketAssets)
: []
const debtBalances =
positionsData?.debts && tokenPrices
? formatBalances(positionsData.debts, tokenPrices, true, marketAssets, marketsData)
: []
setBalanceData([...balances, ...debtBalances])
}, [positionsData, marketsData, tokenPrices, marketAssets])
return balanceData
}

View File

@ -12,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const account = await (await fetch(`${URL_API}/accounts/${accountId}`)).json()
if (account) {
return res.status(200).json(account.debts)
return res.status(200).json([{ denom: 'uosmo', amount: '123876' }])
}
return res.status(404)

View File

@ -1,23 +1,52 @@
import { Coin } from '@cosmjs/stargate'
import BigNumber from 'bignumber.js'
import { GetState, SetState } from 'zustand'
import { getMarketAssets } from 'utils/assets'
import { formatValue } from 'utils/formatters'
export interface CommonSlice {
borrowModal: boolean
createAccountModal: boolean
deleteAccountModal: boolean
enableAnimations: boolean
repayModal: boolean
fundAccountModal: boolean
prices: Coin[]
isOpen: boolean
selectedAccount: string | null
withdrawModal: boolean
formatCurrency: (coin: Coin) => string
}
export function createCommonSlice(set: SetState<CommonSlice>, get: GetState<CommonSlice>) {
return {
borrowModal: false,
createAccountModal: false,
deleteAccountModal: false,
repayModal: false,
enableAnimations: true,
fundAccountModal: false,
prices: [],
isOpen: true,
selectedAccount: null,
withdrawModal: false,
formatCurrency: (coin: Coin) => {
const price = get().prices.find((price) => price.denom === coin.denom)
const marketAsset = getMarketAssets().find((asset) => asset.denom === coin.denom)
if (!price || !marketAsset) return ''
return formatValue(
new BigNumber(coin.amount)
.times(price.amount)
.dividedBy(10 ** marketAsset.decimals)
.toNumber(),
{
minDecimals: 0,
prefix: '$',
},
)
},
}
}

View File

@ -66,20 +66,21 @@ a {
}
/* ORBS */
@mixin orbs($count) {
@mixin orbs($count, $hue) {
$text-shadow: ();
@for $i from 0 through $count {
$text-shadow: $text-shadow,
(-0.5+ (random()) * 3) +
em
(-0.5+ (random()) * 3) +
em
7px
hsla((random() * 50)+210, 100%, 45%, 0.3);
(-0.5+ (random()) * 3) + em (-0.5+ (random()) * 3) + em 10px rgb(92, 5, 92);
// hsla((random() * 50)+$hue, 100%, 45%);
}
text-shadow: $text-shadow;
}
@mixin newOrbs($count, $color) {
filter: blur(4px);
background: radial-gradient(circle at center, rgba($color, 0.25) 0%, rgba($color, 0) 20%);
}
.background {
font-family: serif;
font-size: 90px;
@ -98,18 +99,27 @@ a {
width: 3em;
height: 3em;
content: '.';
color: transparent;
mix-blend-mode: screen;
}
&:after {
top: 10%;
left: 10%;
@include newOrbs(1, rgb(177, 47, 37));
}
&:before {
@include orbs(15);
top: 80%;
left: 80%;
@include newOrbs(1, rgb(83, 7, 129));
animation-duration: 300s;
animation-delay: -50s;
// animation-delay: -50s;
animation: 180s -15s move infinite ease-in-out alternate;
}
&:after {
@include orbs(25);
// @include orbs(15, 260);
animation-duration: 600s;
animation: 180s 0s move infinite ease-in-out alternate;
}

View File

@ -26,8 +26,5 @@ interface BorrowAsset {
}
interface BorrowAssetActive extends BorrowAsset {
debt: {
amount: string
value: string
}
debt: string
}

View File

@ -1,43 +0,0 @@
import { Coin } from '@cosmjs/stargate'
import { convertFromGwei, getTokenTotalUSDValue } from 'utils/formatters'
import { getTokenSymbol } from 'utils/tokens'
export const formatBalances = (
positionData: Coin[],
tokenPrices: KeyValuePair,
debt: boolean,
marketAssets: Asset[],
marketsData?: MarketData,
): PositionsData[] => {
const balances: PositionsData[] = []
positionData.forEach((coin) => {
const dataEntry: PositionsData = {
asset: {
amount: getTokenSymbol(coin.denom, marketAssets),
type: debt ? 'debt' : undefined,
},
value: {
amount: getTokenTotalUSDValue(coin.amount, coin.denom, marketAssets, tokenPrices),
format: 'number',
prefix: '$',
},
size: {
amount: convertFromGwei(coin.amount, coin.denom, marketAssets),
format: 'number',
maxDecimals: 4,
minDecimals: 0,
},
apy: {
amount: debt ? Number(marketsData?.[coin.denom].borrow_rate) * 100 : '-',
format: debt ? 'number' : 'string',
suffix: '%',
minDecimals: 0,
},
}
balances.push(dataEntry)
})
return balances
}

View File

@ -10,28 +10,6 @@ export function truncate(text = '', [h, t]: [number, number] = [6, 6]): string {
return text.length > h + t ? [head, tail].join('...') : text
}
export const formatCurrency = (value: string | number) => {
return Number(value).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
})
}
export const getTokenTotalUSDValue = (
amount: string,
denom: string,
marketAssets: Asset[],
tokenPrices?: KeyValuePair,
) => {
if (!tokenPrices) return 0
return (
BigNumber(amount)
.div(10 ** getTokenDecimals(denom, marketAssets))
.toNumber() * tokenPrices[denom]
)
}
export const convertFromGwei = (amount: string | number, denom: string, marketAssets: Asset[]) => {
return BigNumber(amount)
.div(10 ** getTokenDecimals(denom, marketAssets))
@ -44,26 +22,34 @@ export const convertToGwei = (amount: string | number, denom: string, marketAsse
.toNumber()
}
export const formatValue = (
amount: number | string,
minDecimals = 2,
maxDecimals = 2,
thousandSeparator = true,
prefix: boolean | string = false,
suffix: boolean | string = false,
rounded = false,
abbreviated = false,
): string => {
export interface FormatOptions {
decimals?: number
minDecimals?: number
maxDecimals?: number
thousandSeparator?: boolean
prefix?: string
suffix?: string
rounded?: boolean
abbreviated?: boolean
}
export const formatValue = (amount: number | string, options?: FormatOptions): string => {
let numberOfZeroDecimals: number | null = null
const minDecimals = options?.minDecimals ?? 2
const maxDecimals = options?.maxDecimals ?? 2
const thousandSeparator = options?.thousandSeparator ?? true
if (typeof amount === 'string') {
const decimals = amount.split('.')[1] ?? null
if (decimals && Number(decimals) === 0) {
numberOfZeroDecimals = decimals.length
}
}
let convertedAmount: number | string = +amount || 0
let convertedAmount: number | string = new BigNumber(amount)
.dividedBy(10 ** (options?.decimals ?? 0))
.toNumber()
const amountSuffix = abbreviated
const amountSuffix = options?.abbreviated
? convertedAmount >= 1_000_000_000
? 'B'
: convertedAmount >= 1_000_000
@ -73,19 +59,17 @@ export const formatValue = (
: false
: ''
const amountPrefix = prefix
if (amountSuffix === 'B') {
convertedAmount = Number(amount) / 1_000_000_000
convertedAmount = Number(convertedAmount) / 1_000_000_000
}
if (amountSuffix === 'M') {
convertedAmount = Number(amount) / 1_000_000
convertedAmount = Number(convertedAmount) / 1_000_000
}
if (amountSuffix === 'K') {
convertedAmount = Number(amount) / 1_000
convertedAmount = Number(convertedAmount) / 1_000
}
if (rounded) {
if (options?.rounded) {
convertedAmount = convertedAmount.toFixed(maxDecimals)
} else {
const amountFractions = String(convertedAmount).split('.')
@ -112,8 +96,8 @@ export const formatValue = (
}
let returnValue = ''
if (amountPrefix) {
returnValue += amountPrefix
if (options?.prefix) {
returnValue += options.prefix
}
returnValue += convertedAmount
@ -131,9 +115,23 @@ export const formatValue = (
returnValue += amountSuffix
}
if (suffix) {
returnValue += suffix
if (options?.suffix) {
returnValue += options.suffix
}
return returnValue
}
export function formatLeverage(leverage: number) {
return formatValue(leverage, {
minDecimals: 0,
suffix: 'x',
})
}
export function formatPercent(percent: number | string) {
return formatValue(+percent * 100, {
minDecimals: 0,
suffix: '%',
})
}