overall cleanup and refactoring + SettingsModal (#286)

This commit is contained in:
Linkie Link 2023-07-11 21:01:14 +02:00 committed by GitHub
parent ffe86a440c
commit df6d7a3ba2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 1399 additions and 1135 deletions

View File

@ -1,11 +1,11 @@
import { render } from '@testing-library/react'
import { CircularProgress } from 'components/CircularProgress'
import useStore from 'store'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
describe('<CircularProgress />', () => {
afterAll(() => {
useStore.clearState()
localStorage.removeItem(REDUCE_MOTION_KEY)
})
it('should render', () => {
@ -15,7 +15,7 @@ describe('<CircularProgress />', () => {
})
it('should render `...` when animations disabled', () => {
useStore.setState({ enableAnimations: false })
localStorage.setItem(REDUCE_MOTION_KEY, 'true')
const { getByText } = render(<CircularProgress />)
const threeDots = getByText('...')
@ -24,7 +24,7 @@ describe('<CircularProgress />', () => {
})
it('should render the component with animation classes when animations enabled', () => {
useStore.setState({ enableAnimations: true })
localStorage.setItem(REDUCE_MOTION_KEY, 'false')
const { container } = render(<CircularProgress />)
const progressWithAnimations = container.querySelector('.animate-progress')

View File

@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import LendingDetails from 'components/Earn/Lend/LendingDetails'
import LendingDetails from 'components/MarketAssetTable/MarketDetails'
import { ASSETS } from 'constants/assets'
import { BN } from 'utils/helpers'

View File

@ -1,13 +1,13 @@
import { render } from '@testing-library/react'
import Modal from 'components/Modal'
import UnlockModal from 'components/Modals/Unlock/UnlockModal'
import UnlockModal from 'components/Modals/Unlock'
import { TESTNET_VAULTS_META_DATA } from 'constants/vaults'
import useStore from 'store'
import { BN } from 'utils/helpers'
jest.mock('components/Modal')
const mockedModal = jest.mocked(Modal).mockImplementation(() => <div>Modal</div>)
const mockedModal = jest.mocked(Modal).mockImplementation(() => <div>Mock</div>)
const mockedDepositedVault: DepositedVault = {
...TESTNET_VAULTS_META_DATA[0],
@ -36,31 +36,26 @@ const mockedDepositedVault: DepositedVault = {
}
describe('<UnlockModal />', () => {
beforeAll(() => {
useStore.setState({ unlockModal: null })
afterAll(() => {
useStore.clearState()
})
it('should render', () => {
const { container } = render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledTimes(1)
expect(container).toBeInTheDocument()
})
describe('should set open attribute correctly', () => {
it('should set open = false when no modal is present in store', () => {
describe('should set content correctly', () => {
it('should have no content when no modal is present in store', () => {
useStore.setState({ unlockModal: null })
render(<UnlockModal />)
expect(mockedModal).toHaveBeenCalledWith(
expect.objectContaining({ open: false }),
expect.anything(),
)
expect(mockedModal).toHaveBeenCalledTimes(0)
})
it('should set open = true when no modal is present in store', () => {
it('should have content when modal is present in store', () => {
useStore.setState({ unlockModal: { vault: mockedDepositedVault } })
render(<UnlockModal />)
expect(mockedModal).toHaveBeenLastCalledWith(
expect.objectContaining({ open: true }),
expect.anything(),
)
expect(mockedModal).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -26,6 +26,7 @@ module.exports = {
// Handle module aliases
'^app/(.*)$': '<rootDir>/src/app/$1',
'^api/(.*)$': '<rootDir>/src/api/$1',
'^components/(.*)$': '<rootDir>/src/components/$1',
'^constants/(.*)$': '<rootDir>/src/constants/$1',
'^fonts/(.*)$': '<rootDir>/src/fonts/$1',

View File

@ -1,8 +1,8 @@
import { BN } from 'utils/helpers'
import getPrices from 'api/prices/getPrices'
import getMarkets from 'api/markets/getMarkets'
import { getEnabledMarketAssets } from 'utils/assets'
import getMarketLiquidities from 'api/markets/getMarketLiquidities'
import getMarkets from 'api/markets/getMarkets'
import getPrices from 'api/prices/getPrices'
import { getEnabledMarketAssets } from 'utils/assets'
import { BN } from 'utils/helpers'
export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
const liquidities = await getMarketLiquidities()
@ -20,7 +20,7 @@ export default async function getMarketBorrowings(): Promise<BorrowAsset[]> {
borrowRate: market.borrowRate ?? 0,
liquidity: {
amount: BN(amount),
value: BN(amount).times(price),
value: BN(amount).multipliedBy(price),
},
}
})

View File

@ -1,5 +1,5 @@
import { BN } from 'utils/helpers'
import getPrice from 'api/prices/getPrice'
import { BN } from 'utils/helpers'
const MARS_MAINNET_DENOM = 'ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580'
const MARS_OSMO_POOL_URL = 'https://lcd-osmosis.blockapsis.com/osmosis/gamm/v1beta1/pools/907'
@ -36,8 +36,8 @@ const calculateSpotPrice = (poolAssets: PoolAsset[]) => {
const assetOut = findPoolAssetByTokenDenom(poolAssets, 'uosmo') as PoolAsset
const numerator = BN(assetIn.token.amount).div(assetIn.weight)
const denominator = BN(assetOut.token.amount).div(assetOut.weight)
const numerator = BN(assetIn.token.amount).dividedBy(assetIn.weight)
const denominator = BN(assetOut.token.amount).dividedBy(assetOut.weight)
return numerator.dividedBy(denominator)
}

View File

@ -17,7 +17,7 @@ export default async function getVaultConfigs(
lpTokenOut: lpDenom,
}),
)
.times(1 - slippage)
.multipliedBy(1 - slippage)
.integerValue()
} catch (ex) {
throw ex

View File

@ -14,18 +14,23 @@ import { FormattedNumber } from 'components/FormattedNumber'
import { SortAsc, SortDesc, SortNone } from 'components/Icons'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import useStore from 'store'
import { convertToDisplayAmount, demagnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { BNCoin } from 'types/classes/BNCoin'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { convertToDisplayAmount, demagnify } from 'utils/formatters'
import { DISPLAY_CURRENCY_KEY } from 'constants/localStore'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
data: Account
}
export const AccountBalancesTable = (props: Props) => {
const displayCurrency = useStore((s) => s.displayCurrency)
const [displayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
const { data: prices } = usePrices()
const [sorting, setSorting] = React.useState<SortingState>([])
@ -104,13 +109,12 @@ export const AccountBalancesTable = (props: Props) => {
return (
<FormattedNumber
className='text-right text-xs'
amount={BN(
demagnify(
row.original.amount,
ASSETS.find((asset) => asset.denom === row.original.denom) ?? ASSETS[0],
),
amount={demagnify(
row.original.amount,
ASSETS.find((asset) => asset.denom === row.original.denom) ?? ASSETS[0],
)}
options={{ maxDecimals: 4 }}
animate
/>
)
},
@ -123,8 +127,9 @@ export const AccountBalancesTable = (props: Props) => {
return (
<FormattedNumber
className='text-xs'
amount={BN(row.original.apy)}
amount={row.original.apy}
options={{ maxDecimals: 2, minDecimals: 2, suffix: '%' }}
animate
/>
)
},
@ -146,7 +151,7 @@ export const AccountBalancesTable = (props: Props) => {
return (
<table className='w-full'>
<thead className='border-b border-b-white/5'>
<thead className='border-b border-white/5'>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {

View File

@ -89,12 +89,13 @@ function Item(props: ItemProps) {
{props.title}
</Text>
</div>
<div className='flex flex-grow items-center justify-end gap-2'>
<div className='flex flex-1 items-center justify-end gap-2'>
{props.isPercentage ? (
<FormattedNumber
amount={props.current}
amount={props.current.toNumber()}
options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }}
className='text-sm'
animate
/>
) : (
<DisplayCurrency
@ -109,9 +110,10 @@ function Item(props: ItemProps) {
</span>
{props.isPercentage ? (
<FormattedNumber
amount={props.change}
amount={props.change.toNumber()}
options={{ suffix: '%', minDecimals: 2, maxDecimals: 2 }}
className={classNames('text-sm', increase ? 'text-profit' : 'text-loss')}
animate
/>
) : (
<DisplayCurrency

View File

@ -2,7 +2,9 @@ import classNames from 'classnames'
import { Heart } from 'components/Icons'
import Text from 'components/Text'
import useStore from 'store'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
health: number
@ -11,7 +13,7 @@ interface Props {
}
export default function AccountHealth(props: Props) {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const healthBarWidth = (props.health / 100) * 53
return (
@ -33,7 +35,7 @@ export default function AccountHealth(props: Props) {
rx='2'
fill='url(#bar)'
style={{
transition: enableAnimations ? 'width 1s ease' : 'none',
transition: reduceMotion ? 'none' : 'width 1s ease',
}}
/>
<defs>

View File

@ -11,20 +11,21 @@ import {
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import useStore from 'store'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import { formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
export const RiskChart = ({ data }: RiskChartProps) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const accountStats = null
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const currentRisk = BN(0)
return (
<div className='flex w-full flex-wrap overflow-hidden py-2'>
<FormattedNumber
className='px-3 pb-2 text-lg'
amount={currentRisk.times(100)}
amount={currentRisk.multipliedBy(100).toNumber()}
options={{
maxDecimals: 0,
minDecimals: 0,
@ -92,7 +93,7 @@ export const RiskChart = ({ data }: RiskChartProps) => {
dataKey='risk'
stroke='#FFFFFF'
fill='url(#chartGradient)'
isAnimationActive={enableAnimations}
isAnimationActive={!reduceMotion}
/>
</AreaChart>
</ResponsiveContainer>

View File

@ -13,8 +13,9 @@ export default function AmountAndValue(props: Props) {
<TitleAndSubCell
title={
<FormattedNumber
amount={props.amount}
amount={props.amount.toNumber()}
options={{ decimals: props.asset.decimals, abbreviated: true }}
animate
/>
}
sub={

View File

@ -1,9 +1,11 @@
import classNames from 'classnames'
import useStore from 'store'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
export default function Background() {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
return (
<div className='background pointer-events-none fixed inset-0 h-full w-full overflow-hidden bg-body'>
@ -16,7 +18,7 @@ export default function Background() {
'left-[-10vw] top-[-10vw]',
'bg-orb-primary blur-orb-primary ',
'translate-x-0 translate-y-0 rounded-full opacity-20',
enableAnimations && 'animate-[float_120s_ease-in-out_infinite_2s]',
!reduceMotion && 'animate-[float_120s_ease-in-out_infinite_2s]',
)}
/>
<div
@ -28,7 +30,7 @@ export default function Background() {
'bottom-[-10vw] right-[-8vw]',
'bg-orb-secondary blur-orb-secondary',
'translate-x-0 translate-y-0 rounded-full opacity-30',
enableAnimations && 'animate-[float_150s_ease-in-out_infinite_1s]',
!reduceMotion && 'animate-[float_150s_ease-in-out_infinite_1s]',
)}
/>
<div
@ -40,7 +42,7 @@ export default function Background() {
'right-[-4vw] top-[-10vw]',
'bg-orb-tertiary blur-orb-tertiary ',
'translate-x-0 translate-y-0 rounded-full opacity-20',
enableAnimations && 'animate-[float_180s_ease-in_infinite]',
!reduceMotion && 'animate-[float_180s_ease-in_infinite]',
)}
/>
</div>

View File

@ -1,56 +0,0 @@
import { Row } from '@tanstack/react-table'
import Button from 'components/Button'
import useStore from 'store'
import { getEnabledMarketAssets } from 'utils/assets'
type AssetRowProps = {
row: Row<BorrowAsset | BorrowAssetActive>
onBorrowClick: () => void
onRepayClick: () => void
resetExpanded: (defaultState?: boolean | undefined) => void
}
export default function AssetExpanded(props: AssetRowProps) {
const marketAssets = getEnabledMarketAssets()
const asset = marketAssets.find((asset) => asset.denom === props.row.original.denom)
let isActive: boolean = false
if (!asset) return null
function borrowHandler() {
if (!asset) return null
useStore.setState({ borrowModal: { asset: asset, marketData: props.row.original } })
}
function repayHandler() {
if (!asset) return null
useStore.setState({
borrowModal: { asset: asset, marketData: props.row.original, isRepay: true },
})
}
return (
<tr
key={props.row.id}
className='cursor-pointer bg-black/20 transition-colors'
onClick={(e) => {
e.preventDefault()
const isExpanded = props.row.getIsExpanded()
props.resetExpanded()
!isExpanded && props.row.toggleExpanded()
}}
>
<td colSpan={isActive ? 5 : 4}>
<div className='flex justify-end gap-4 p-4'>
<Button
onClick={borrowHandler}
color='primary'
text={isActive ? 'Borrow more' : 'Borrow'}
/>
{isActive && <Button color='primary' text='Repay' onClick={repayHandler} />}
</div>
</td>
</tr>
)
}

View File

@ -1,40 +0,0 @@
import { flexRender, Row } from '@tanstack/react-table'
import classNames from 'classnames'
import { getEnabledMarketAssets } from 'utils/assets'
type AssetRowProps = {
row: Row<BorrowAsset | BorrowAssetActive>
resetExpanded: (defaultState?: boolean | undefined) => void
}
export const AssetRow = (props: AssetRowProps) => {
const marketAssets = getEnabledMarketAssets()
const asset = marketAssets.find((asset) => asset.denom === props.row.original.denom)
if (!asset) return null
return (
<tr
key={props.row.id}
className={classNames(
'cursor-pointer transition-colors',
props.row.getIsExpanded() ? ' bg-black/20' : 'bg-white/0 hover:bg-white/5',
)}
onClick={(e) => {
e.preventDefault()
const isExpanded = props.row.getIsExpanded()
props.resetExpanded()
!isExpanded && props.row.toggleExpanded()
}}
>
{props.row.getVisibleCells().map((cell, index) => {
return (
<td key={cell.id} className={'p-4 text-right'}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
)
})}
</tr>
)
}

View File

@ -0,0 +1,41 @@
import { useCallback } from 'react'
import Button from 'components/Button'
import { Plus } from 'components/Icons'
import useStore from 'store'
import { getEnabledMarketAssets } from 'utils/assets'
interface Props {
data: BorrowMarketTableData
}
export default function BorrowActionButtons(props: Props) {
const { asset, debt } = props.data
const marketAssets = getEnabledMarketAssets()
const currentAsset = marketAssets.find((a) => a.denom === asset.denom)
const borrowHandler = useCallback(() => {
if (!currentAsset) return null
useStore.setState({ borrowModal: { asset: currentAsset, marketData: props.data } })
}, [currentAsset, props.data])
const repayHandler = useCallback(() => {
if (!currentAsset) return null
useStore.setState({
borrowModal: { asset: currentAsset, marketData: props.data, isRepay: true },
})
}, [currentAsset, props.data])
return (
<div className='flex flex-row space-x-2'>
<Button
leftIcon={debt ? <Plus /> : undefined}
onClick={borrowHandler}
color='secondary'
text={debt ? 'Borrow more' : 'Borrow'}
className='min-w-40 text-center'
/>
{debt && <Button color='secondary' text='Repay' onClick={repayHandler} />}
</div>
)
}

View File

@ -1,42 +1,53 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from '@tanstack/react-table'
import classNames from 'classnames'
import React from 'react'
import { ColumnDef, Row, Table } from '@tanstack/react-table'
import { useCallback, useMemo } from 'react'
import AmountAndValue from 'components/AmountAndValue'
import AssetExpanded from 'components/Borrow/AssetExpanded'
import { AssetRow } from 'components/Borrow/AssetRow'
import { ChevronDown, SortAsc, SortDesc, SortNone } from 'components/Icons'
import AssetImage from 'components/AssetImage'
import BorrowActionButtons from 'components/Borrow/BorrowActionButtons'
import { FormattedNumber } from 'components/FormattedNumber'
import { ChevronDown, ChevronUp } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
import AssetListTable from 'components/MarketAssetTable'
import MarketAssetTableRow from 'components/MarketAssetTable/MarketAssetTableRow'
import MarketDetails from 'components/MarketAssetTable/MarketDetails'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { getEnabledMarketAssets } from 'utils/assets'
import { formatPercent } from 'utils/formatters'
import AssetImage from 'components/AssetImage'
import { BN } from 'utils/helpers'
type Props = {
data: BorrowAsset[] | BorrowAssetActive[]
interface Props {
title: string
data: BorrowMarketTableData[]
}
export const BorrowTable = (props: Props) => {
const [sorting, setSorting] = React.useState<SortingState>([])
export default function BorrowTable(props: Props) {
const { title, data } = props
const shouldShowAccountBorrowed = !!data[0]?.debt
const marketAssets = getEnabledMarketAssets()
const columns = React.useMemo<ColumnDef<BorrowAsset | BorrowAssetActive>[]>(
const rowRenderer = useCallback(
(row: Row<BorrowMarketTableData>, table: Table<BorrowMarketTableData>) => {
return (
<MarketAssetTableRow
key={`borrow-asset-${row.id}`}
isExpanded={row.getIsExpanded()}
resetExpanded={table.resetExpanded}
rowData={row}
expandedActionButtons={<BorrowActionButtons data={row.original} />}
expandedDetails={<MarketDetails data={row.original} />}
/>
)
},
[],
)
const columns = useMemo<ColumnDef<BorrowMarketTableData>[]>(
() => [
{
accessorKey: 'asset.name',
header: 'Asset',
id: 'symbol',
cell: ({ row }) => {
const asset = marketAssets.find((asset) => asset.denom === row.original.denom)
if (!asset) return null
const asset = row.original.asset
return (
<div className='flex flex-1 items-center gap-3'>
@ -50,6 +61,22 @@ export const BorrowTable = (props: Props) => {
)
},
},
...(shouldShowAccountBorrowed
? [
{
accessorKey: 'debt',
header: 'Borrowed',
cell: (info: any) => {
const borrowAsset = info.row.original as BorrowMarketTableData
const asset = marketAssets.find((asset) => asset.denom === borrowAsset.asset.denom)
if (!asset) return null
return <AmountAndValue asset={asset} amount={borrowAsset?.debt ?? BN(0)} />
},
},
]
: []),
{
accessorKey: 'borrowRate',
header: 'Borrow Rate',
@ -59,136 +86,44 @@ export const BorrowTable = (props: Props) => {
}
return (
<Text className='justify-end' size='sm'>
{formatPercent(row.original.borrowRate, 2)}
</Text>
<FormattedNumber
className='justify-end text-xs'
amount={row.original.borrowRate * 100}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '%' }}
animate
/>
)
},
},
...((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 }) => {
const asset = marketAssets.find((asset) => asset.denom === row.original.denom)
const { liquidity, asset: borrowAsset } = row.original
const asset = marketAssets.find((asset) => asset.denom === borrowAsset.denom)
if (!asset) return null
if (row.original.liquidity === null) {
if (liquidity === null) {
return <Loading />
}
return <AmountAndValue asset={asset} amount={row.original.liquidity.amount} />
return <AmountAndValue asset={asset} amount={liquidity.amount ?? BN(0)} />
},
},
{
accessorKey: 'status',
accessorKey: 'manage',
enableSorting: false,
header: 'Manage',
width: 150,
cell: ({ row }) => (
<div className='flex items-center justify-end'>
<div className={classNames('w-4', row.getIsExpanded() && 'rotate-180')}>
<ChevronDown />
</div>
<div className='w-4'>{row.getIsExpanded() ? <ChevronUp /> : <ChevronDown />}</div>
</div>
),
},
],
[marketAssets, props.data],
[shouldShowAccountBorrowed, marketAssets],
)
const table = useReactTable({
data: props.data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<table className='w-full'>
<thead className='bg-black/20'>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
return (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className={classNames(
'px-4 py-3',
header.column.getCanSort() && 'cursor-pointer',
header.id === 'symbol' ? 'text-left' : 'text-right',
)}
>
<div
className={classNames(
'flex',
header.id === 'symbol' ? 'justify-start' : 'justify-end',
'align-center',
)}
>
<span className='h-6 w-6 text-white'>
{header.column.getCanSort()
? {
asc: <SortAsc />,
desc: <SortDesc />,
false: <SortNone />,
}[header.column.getIsSorted() as string] ?? null
: null}
</span>
<Text
tag='span'
size='sm'
className='flex items-center font-normal text-white/40'
>
{flexRender(header.column.columnDef.header, header.getContext())}
</Text>
</div>
</th>
)
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
if (row.getIsExpanded()) {
return (
<React.Fragment key={`${row.id}_subrow`}>
<AssetRow key={`${row.id}_asset`} row={row} resetExpanded={table.resetExpanded} />
<AssetExpanded
key={`${row.id}_expanded`}
row={row}
onBorrowClick={() => {}}
onRepayClick={() => {}}
resetExpanded={table.resetExpanded}
/>
</React.Fragment>
)
}
return <AssetRow key={row.index} row={row} resetExpanded={table.resetExpanded} />
})}
</tbody>
</table>
)
return <AssetListTable title={title} rowRenderer={rowRenderer} columns={columns} data={data} />
}

View File

@ -1,90 +0,0 @@
import { Suspense, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import { getEnabledMarketAssets } from 'utils/assets'
import { BorrowTable } from 'components/Borrow/BorrowTable'
import useAccountDebts from 'hooks/useAccountDebts'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
interface Props {
type: 'active' | 'available'
}
function Content(props: Props) {
const { accountId } = useParams()
const { data: debtData } = useAccountDebts(accountId)
const { data: borrowData } = useMarketBorrowings()
const marketAssets = getEnabledMarketAssets()
const { available, active } = useMemo(
() =>
marketAssets.reduce(
(prev: { available: BorrowAsset[]; active: BorrowAssetActive[] }, curr) => {
const borrow = borrowData.find((borrow) => borrow.denom === curr.denom)
if (borrow) {
const debt = debtData?.find((debt) => debt.denom === curr.denom)
if (debt) {
prev.active.push({
...borrow,
debt: debt.amount,
})
} else {
prev.available.push(borrow)
}
}
return prev
},
{ available: [], active: [] },
),
[marketAssets, borrowData, debtData],
)
const assets = props.type === 'active' ? active : available
if (!assets.length) return null
if (props.type === 'active') {
return (
<Card
className='mb-4 h-fit w-full bg-white/5'
title={props.type === 'active' ? 'Borrowings' : 'Available to borrow'}
>
<BorrowTable data={assets} />
</Card>
)
}
return <BorrowTable data={assets} />
}
function Fallback() {
const marketAssets = getEnabledMarketAssets()
const available: BorrowAsset[] = marketAssets.reduce((prev: BorrowAsset[], curr) => {
prev.push({ ...curr, borrowRate: null, liquidity: null })
return prev
}, [])
return <BorrowTable data={available} />
}
export function AvailableBorrowings() {
return (
<Card className='h-fit w-full bg-white/5' title={'Available to borrow'}>
<Suspense fallback={<Fallback />}>
<Content type='available' />
</Suspense>
</Card>
)
}
export function ActiveBorrowings() {
return (
<Suspense fallback={null}>
<Content type='active' />
</Suspense>
)
}

View File

@ -4,8 +4,9 @@ import { useEffect, useState } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import useStore from 'store'
import { BN } from 'utils/helpers'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
balance: number
@ -30,8 +31,7 @@ export const BorrowCapacity = ({
hideValues,
decimals = 2,
}: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const [percentOfMaxRound, setPercentOfMaxRound] = useState(0)
const [percentOfMaxRange, setPercentOfMaxRange] = useState(0)
const [limitPercentOfMax, setLimitPercentOfMax] = useState(0)
@ -64,12 +64,12 @@ export const BorrowCapacity = ({
{!hideValues && (
<div
className={classNames(
enableAnimations && 'duration-800 transition-[opcity] delay-[1600ms]',
!reduceMotion && 'duration-800 transition-[opcity] delay-[1600ms]',
'text-3xs-caps',
limitPercentOfMax ? 'opacity-50' : 'opacity-0',
)}
>
<FormattedNumber animate amount={BN(limit)} />
<FormattedNumber animate amount={limit} />
</div>
)}
</div>
@ -83,7 +83,7 @@ export const BorrowCapacity = ({
<div
className={classNames(
'absolute left-0 h-full max-w-full rounded-l-3xl bg-body-dark',
enableAnimations && 'transition-[right] duration-1000 ease-linear',
!reduceMotion && 'transition-[right] duration-1000 ease-linear',
)}
style={{
right: `${limitPercentOfMax ? 100 - limitPercentOfMax : 100}%`,
@ -94,7 +94,7 @@ export const BorrowCapacity = ({
<div
className={classNames(
'h-full rounded-lg',
enableAnimations && 'transition-[width] duration-1000 ease-linear',
!reduceMotion && 'transition-[width] duration-1000 ease-linear',
)}
style={{
width: `${percentOfMaxRange || 0.02}%`,
@ -107,7 +107,7 @@ export const BorrowCapacity = ({
<div
className={classNames(
'absolute bottom-0 h-[120%] w-[1px] bg-white',
enableAnimations && 'transition-[left] duration-1000 ease-linear',
!reduceMotion && 'transition-[left] duration-1000 ease-linear',
)}
style={{ left: `${limitPercentOfMax || 0}%` }}
/>
@ -115,7 +115,7 @@ export const BorrowCapacity = ({
<span
className={classNames(
'absolute top-1/2 mt-[1px] w-full -translate-y-1/2 text-center text-2xs-caps',
enableAnimations && 'animate-fadein opacity-0',
!reduceMotion && 'animate-fadein opacity-0',
)}
>
{max !== 0 && (
@ -127,7 +127,7 @@ export const BorrowCapacity = ({
maxDecimals: decimals,
suffix: '%',
}}
amount={BN(percentOfMaxRound)}
amount={percentOfMaxRound}
/>
)}
</span>
@ -139,9 +139,9 @@ export const BorrowCapacity = ({
</Tooltip>
{!hideValues && (
<div className='mt-2 flex opacity-50 text-3xs-caps'>
<FormattedNumber animate amount={BN(balance)} className='mr-1' />
<FormattedNumber animate amount={balance} className='mr-1' />
<span className='mr-1'>of</span>
<FormattedNumber animate amount={BN(max)} />
<FormattedNumber animate amount={max} />
</div>
)}
</div>

View File

@ -55,6 +55,7 @@ export const buttonVariantClasses = {
solid: 'rounded-sm text-white shadow-button justify-center group',
transparent: 'rounded-sm bg-transparent p-0 transition duration-200 ease-in',
round: 'rounded-full p-0',
rounded: 'rounded-2xl',
}
export const circularProgressSize = {

View File

@ -16,7 +16,9 @@ import {
import { glowElement } from 'components/Button/utils'
import { CircularProgress } from 'components/CircularProgress'
import { ChevronDown } from 'components/Icons'
import useStore from 'store'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
children?: string | ReactNode
@ -27,7 +29,7 @@ interface Props {
showProgressIndicator?: boolean
size?: 'xs' | 'sm' | 'md' | 'lg'
text?: string | ReactNode
variant?: 'solid' | 'transparent' | 'round'
variant?: 'solid' | 'transparent' | 'round' | 'rounded'
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
leftIcon?: ReactElement
rightIcon?: ReactElement
@ -36,6 +38,7 @@ interface Props {
hasFocus?: boolean
dataTestId?: string
tabIndex?: number
textClassNames?: string
}
const Button = React.forwardRef(function Button(
@ -57,13 +60,14 @@ const Button = React.forwardRef(function Button(
hasFocus,
dataTestId,
tabIndex = 0,
textClassNames,
}: Props,
ref,
) {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const isDisabled = disabled || showProgressIndicator
const shouldShowText = text && !children
const shouldShowGlowElement = variant === 'solid' && !isDisabled
const shouldShowGlowElement = variant === 'solid' && !isDisabled && !reduceMotion
const buttonClassNames = useMemo(() => {
const buttonClasses = [
@ -82,7 +86,7 @@ const Button = React.forwardRef(function Button(
'relative z-1 flex items-center',
'cursor-pointer appearance-none break-normal outline-none',
'text-white transition-all',
enableAnimations && 'transition-color',
!reduceMotion && 'transition-color',
buttonClasses,
buttonVariantClasses[variant],
variant === 'solid' && color === 'tertiary' && buttonBorderClasses,
@ -91,7 +95,7 @@ const Button = React.forwardRef(function Button(
hasFocus && focusClasses[color],
className,
)
}, [className, color, enableAnimations, hasFocus, isDisabled, size, variant])
}, [className, color, reduceMotion, hasFocus, isDisabled, size, variant])
const [leftIconClassNames, rightIconClassNames] = useMemo(() => {
const hasContent = !!(text || children)
@ -116,7 +120,7 @@ const Button = React.forwardRef(function Button(
) : (
<>
{leftIcon && <span className={classNames(leftIconClassNames)}>{leftIcon}</span>}
{shouldShowText && <span>{text}</span>}
{shouldShowText && <span className={textClassNames}>{text}</span>}
{children && children}
{rightIcon && <span className={classNames(rightIconClassNames)}>{rightIcon}</span>}
{hasSubmenu && (
@ -126,7 +130,7 @@ const Button = React.forwardRef(function Button(
)}
</>
)}
{shouldShowGlowElement && glowElement(enableAnimations)}
{shouldShowGlowElement && glowElement(!reduceMotion)}
</button>
)
})

View File

@ -17,7 +17,7 @@ export default function Card(props: Props) {
id={props.id}
className={classNames(
props.className,
'relative isolate max-w-full overflow-hidden rounded-base',
'relative isolate max-w-full rounded-base',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-base before:p-[1px] before:border-glas',
)}
>

View File

@ -1,7 +1,9 @@
import classNames from 'classnames'
import Text from 'components/Text'
import useStore from 'store'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
color?: string
@ -10,15 +12,14 @@ interface Props {
}
export const CircularProgress = ({ color = '#FFFFFF', size = 20, className }: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const borderWidth = `${size / 10}px`
const borderColor = `${color} transparent transparent transparent`
const loaderClasses = classNames('inline-block relative', className)
const elementClasses =
'block absolute w-4/5 h-4/5 m-[10%] rounded-full animate-progress border-solid'
if (!enableAnimations)
if (reduceMotion)
return (
<div
className={classNames('flex items-center', loaderClasses)}

View File

@ -1,7 +1,12 @@
import { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { DISPLAY_CURRENCY_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { getDisplayCurrencies } from 'utils/assets'
import { convertToDisplayAmount } from 'utils/formatters'
interface Props {
@ -11,22 +16,33 @@ interface Props {
}
export default function DisplayCurrency(props: Props) {
const displayCurrency = useStore((s) => s.displayCurrency)
const displayCurrencies = getDisplayCurrencies()
const [displayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
const { data: prices } = usePrices()
const displayCurrencyAsset = useMemo(
() =>
displayCurrencies.find((asset) => asset.denom === displayCurrency) ?? displayCurrencies[0],
[displayCurrency, displayCurrencies],
)
return (
<FormattedNumber
className={props.className}
amount={convertToDisplayAmount(props.coin, displayCurrency, prices)}
amount={convertToDisplayAmount(props.coin, displayCurrency, prices).toNumber()}
options={{
minDecimals: 0,
maxDecimals: 2,
abbreviated: true,
prefix: `${props.isApproximation ? '~ ' : ''}${
displayCurrency.prefix ? displayCurrency.prefix : ''
displayCurrencyAsset.prefix ? displayCurrencyAsset.prefix : ''
}`,
suffix: displayCurrency.symbol ? ` ${displayCurrency.symbol}` : '',
suffix: displayCurrencyAsset.symbol ? ` ${displayCurrencyAsset.symbol}` : '',
}}
animate
/>
)
}

View File

@ -1,7 +1,7 @@
import { Suspense } from 'react'
import Card from 'components/Card'
import VaultCard from 'components/Earn/vault/VaultCard'
import VaultCard from 'components/Earn/Farm/VaultCard'
import useVaults from 'hooks/useVaults'
function Content() {

View File

@ -1,5 +1,5 @@
import Button from 'components/Button'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import VaultLogo from 'components/Earn/Farm/VaultLogo'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useCurrentAccount from 'hooks/useCurrentAccount'

View File

@ -12,9 +12,10 @@ import React from 'react'
import Button from 'components/Button'
import DisplayCurrency from 'components/DisplayCurrency'
import VaultExpanded from 'components/Earn/vault/VaultExpanded'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import { VaultRow } from 'components/Earn/vault/VaultRow'
import VaultExpanded from 'components/Earn/Farm/VaultExpanded'
import VaultLogo from 'components/Earn/Farm/VaultLogo'
import { VaultRow } from 'components/Earn/Farm/VaultRow'
import { FormattedNumber } from 'components/FormattedNumber'
import { ChevronDown, SortAsc, SortDesc, SortNone } from 'components/Icons'
import Loading from 'components/Loading'
import Text from 'components/Text'
@ -24,7 +25,7 @@ import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { VaultStatus } from 'types/enums/vault'
import { getAssetByDenom } from 'utils/assets'
import { convertPercentage, formatPercent, formatValue, produceCountdown } from 'utils/formatters'
import { produceCountdown } from 'utils/formatters'
type Props = {
data: Vault[] | DepositedVault[]
@ -116,9 +117,15 @@ export const VaultTable = (props: Props) => {
accessorKey: 'apy',
header: 'APY',
cell: ({ row }) => {
if (row.original.apy === null) return <Loading />
const vault = row.original as DepositedVault
if (vault.apy === null) return <Loading />
return (
<Text size='xs'>{row.original.apy ? formatPercent(row.original.apy, 2) : '-'}</Text>
<FormattedNumber
amount={(vault.apy ?? 0) * 100}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '%' }}
className='text-xs'
animate
/>
)
},
},
@ -126,10 +133,11 @@ export const VaultTable = (props: Props) => {
accessorKey: 'tvl',
header: 'TVL',
cell: ({ row }) => {
const vault = row.original as DepositedVault
if (props.isLoading) return <Loading />
const coin = new BNCoin({
denom: row.original.cap.denom,
amount: row.original.cap.used.toString(),
denom: vault.cap.denom,
amount: vault.cap.used.toString(),
})
return <DisplayCurrency coin={coin} className='text-xs' />
@ -139,24 +147,33 @@ export const VaultTable = (props: Props) => {
accessorKey: 'cap',
header: 'Depo. Cap',
cell: ({ row }) => {
const vault = row.original as DepositedVault
if (props.isLoading) return <Loading />
const percent = vault.cap.used
.dividedBy(vault.cap.max.multipliedBy(VAULT_DEPOSIT_BUFFER))
.multipliedBy(100)
.integerValue()
const percent = convertPercentage(
row.original.cap.used
.div(row.original.cap.max.times(VAULT_DEPOSIT_BUFFER))
.times(100)
.integerValue()
.toNumber(),
)
const decimals = getAssetByDenom(row.original.cap.denom)?.decimals ?? 6
const decimals = getAssetByDenom(vault.cap.denom)?.decimals ?? 6
return (
<TitleAndSubCell
title={formatValue(row.original.cap.max.integerValue().toNumber(), {
abbreviated: true,
decimals,
})}
sub={`${percent}% Filled`}
title={
<FormattedNumber
amount={vault.cap.max.toNumber()}
options={{ minDecimals: 2, abbreviated: true, decimals }}
className='text-xs'
animate
/>
}
sub={
<FormattedNumber
amount={percent.toNumber()}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '% Filled' }}
className='text-xs'
animate
/>
}
/>
)
},
@ -170,18 +187,17 @@ export const VaultTable = (props: Props) => {
return 'Deposit'
},
cell: ({ row }) => {
const vault = row.original as DepositedVault
function enterVaultHandler() {
useStore.setState({
vaultModal: {
vault: row.original,
selectedBorrowDenoms: [row.original.denoms.secondary],
vault,
selectedBorrowDenoms: [vault.denoms.secondary],
},
})
}
if (props.isLoading) return <Loading />
const vault = row.original as DepositedVault
return (
<div className='flex items-center justify-end'>
{vault.status ? (

View File

@ -2,8 +2,8 @@ import { Suspense, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import Card from 'components/Card'
import { VaultTable } from 'components/Earn/vault/VaultTable'
import VaultUnlockBanner from 'components/Earn/vault/VaultUnlockBanner'
import { VaultTable } from 'components/Earn/Farm/VaultTable'
import VaultUnlockBanner from 'components/Earn/Farm/VaultUnlockBanner'
import { IS_TESTNET } from 'constants/env'
import { TESTNET_VAULTS_META_DATA, VAULTS_META_DATA } from 'constants/vaults'
import useDepositedVaults from 'hooks/useDepositedVaults'

View File

@ -1,25 +1,25 @@
import { useCallback } from 'react'
import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
import Button from 'components/Button'
import { ArrowDownLine, ArrowUpLine, Enter } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
import useAlertDialog from 'hooks/useAlertDialog'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import useCurrentAccountDeposits from 'hooks/useCurrentAccountDeposits'
import useLendAndReclaimModal from 'hooks/useLendAndReclaimModal'
import { byDenom } from 'utils/array'
import useAutoLendEnabledAccountIds from 'hooks/useAutoLendEnabledAccountIds'
import { ACCOUNT_MENU_BUTTON_ID } from 'components/Account/AccountMenuContent'
interface Props {
data: LendingMarketTableData
}
const buttonClassnames = 'm-0 flex w-40 text-lg'
const buttonClassnames = 'm-0 flex w-40'
const iconClassnames = 'ml-0 mr-1 w-4 h-4'
function LendingActionButtons(props: Props) {
export default function LendingActionButtons(props: Props) {
const { asset, accountLentValue: accountLendValue } = props.data
const accountDeposits = useCurrentAccountDeposits()
const { openLend, openReclaim } = useLendAndReclaimModal()
@ -90,5 +90,3 @@ function LendingActionButtons(props: Props) {
</div>
)
}
export default LendingActionButtons

View File

@ -1,62 +0,0 @@
import TitleAndSubCell from 'components/TitleAndSubCell'
import { formatPercent, formatValue } from 'utils/formatters'
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
interface Props {
data: LendingMarketTableData
}
function LendingDetails({ data }: Props) {
const {
convertAmount,
getConversionRate,
symbol: displayCurrencySymbol,
} = useDisplayCurrencyPrice()
const {
asset,
marketMaxLtv,
marketDepositAmount,
marketLiquidityAmount,
marketLiquidationThreshold,
} = data
const formattedTotalSuppliedValue = formatValue(
convertAmount(asset, marketDepositAmount).toNumber(),
{
abbreviated: true,
suffix: ` ${displayCurrencySymbol}`,
},
)
const formattedPrice = formatValue(getConversionRate(asset.denom).toNumber(), {
maxDecimals: 2,
suffix: ` ${displayCurrencySymbol}`,
})
const totalBorrowed = marketDepositAmount.minus(marketLiquidityAmount)
const utilizationRatePercent = formatPercent(
totalBorrowed.dividedBy(marketDepositAmount).toNumber(),
2,
)
const details = [
{ info: formattedTotalSuppliedValue, title: 'Total Supplied' },
{ info: formatPercent(marketMaxLtv, 2), title: 'Max LTV' },
{ info: formatPercent(marketLiquidationThreshold, 2), title: 'Liquidation Threshold' },
{ info: formattedPrice, title: 'Oracle Price' },
{ info: utilizationRatePercent, title: 'Utilization Rate' },
]
return (
<div className='flex flex-1 justify-center rounded-md bg-white bg-opacity-5'>
{details.map((detail, index) => (
<TitleAndSubCell
key={index}
className='text-md text-center'
containerClassName='m-5 ml-10 mr-10 space-y-2'
title={detail.info}
sub={detail.title}
/>
))}
</div>
)
}
export default LendingDetails

View File

@ -1,40 +1,43 @@
import { ColumnDef, Row, Table } from '@tanstack/react-table'
import Image from 'next/image'
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import AssetImage from 'components/AssetImage'
import LendingActionButtons from 'components/Earn/Lend/LendingActionButtons'
import LendingDetails from 'components/Earn/Lend/LendingDetails'
import { FormattedNumber } from 'components/FormattedNumber'
import { ChevronDown, ChevronUp } from 'components/Icons'
import AssetListTable from 'components/MarketAssetTable'
import MarketAssetTableRow from 'components/MarketAssetTable/MarketAssetTableRow'
import Text from 'components/Text'
import MarketDetails from 'components/MarketAssetTable/MarketDetails'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
import { convertLiquidityRateToAPR, demagnify, formatValue } from 'utils/formatters'
import { convertLiquidityRateToAPR, demagnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props {
title: string
data: LendingMarketTableData[]
}
function LendingMarketsTable(props: Props) {
export default function LendingMarketsTable(props: Props) {
const { title, data } = props
const { symbol: displayCurrencySymbol } = useDisplayCurrencyPrice()
const shouldShowAccountDeposit = !!data[0]?.accountLentValue
const rowRenderer = (row: Row<LendingMarketTableData>, table: Table<LendingMarketTableData>) => {
return (
<MarketAssetTableRow
key={`lend-asset-${row.id}`}
isExpanded={row.getIsExpanded()}
resetExpanded={table.resetExpanded}
rowData={row}
expandedActionButtons={<LendingActionButtons data={row.original} />}
expandedDetails={<LendingDetails data={row.original} />}
/>
)
}
const rowRenderer = useCallback(
(row: Row<LendingMarketTableData>, table: Table<LendingMarketTableData>) => {
return (
<MarketAssetTableRow
key={`lend-asset-${row.id}`}
isExpanded={row.getIsExpanded()}
resetExpanded={table.resetExpanded}
rowData={row}
expandedActionButtons={<LendingActionButtons data={row.original} />}
expandedDetails={<MarketDetails data={row.original} />}
/>
)
},
[],
)
const columns = useMemo<ColumnDef<LendingMarketTableData>[]>(
() => [
@ -47,7 +50,7 @@ function LendingMarketsTable(props: Props) {
return (
<div className='flex flex-1 items-center gap-3'>
<Image src={asset.logo} alt={asset.symbol} width={32} height={32} />
<AssetImage asset={asset} size={32} />
<TitleAndSubCell
title={asset.symbol}
sub={asset.name}
@ -68,8 +71,8 @@ function LendingMarketsTable(props: Props) {
return (
<FormattedNumber
className='text-xs'
animate={true}
amount={accountDepositValue}
animate
amount={accountDepositValue.toNumber()}
options={{ suffix: ` ${displayCurrencySymbol}` }}
/>
)
@ -81,8 +84,14 @@ function LendingMarketsTable(props: Props) {
accessorKey: 'marketLiquidityRate',
header: 'APR',
cell: ({ row }) => {
const apr = convertLiquidityRateToAPR(row.original.marketLiquidityRate)
return <Text size='xs'>{apr.toFixed(2)}%</Text>
return (
<FormattedNumber
amount={convertLiquidityRateToAPR(row.original.marketLiquidityRate)}
options={{ minDecimals: 2, maxDecimals: 2, suffix: '%' }}
className='text-xs'
animate
/>
)
},
},
{
@ -94,19 +103,23 @@ function LendingMarketsTable(props: Props) {
demagnify(marketDepositAmount, asset),
)
const [formattedRemainingCap, formattedDepositCap] = [remainingCap, marketDepositCap].map(
(value) =>
formatValue(value.toNumber(), {
decimals: asset.decimals,
abbreviated: true,
}),
)
return (
<TitleAndSubCell
className='text-xs'
title={formattedDepositCap}
sub={`${formattedRemainingCap} left`}
title={
<FormattedNumber
amount={marketDepositCap.toNumber()}
options={{ abbreviated: true, decimals: asset.decimals }}
animate
/>
}
sub={
<FormattedNumber
amount={remainingCap.toNumber()}
options={{ abbreviated: true, decimals: asset.decimals, suffix: ` left` }}
animate
/>
}
/>
)
},
@ -117,7 +130,7 @@ function LendingMarketsTable(props: Props) {
header: 'Manage',
cell: ({ row }) => (
<div className='flex items-center justify-end'>
<div className='w-4'>{row.getIsExpanded() ? <ChevronDown /> : <ChevronUp />}</div>
<div className='w-4'>{row.getIsExpanded() ? <ChevronUp /> : <ChevronDown />}</div>
</div>
),
},
@ -127,5 +140,3 @@ function LendingMarketsTable(props: Props) {
return <AssetListTable title={title} rowRenderer={rowRenderer} columns={columns} data={data} />
}
export default LendingMarketsTable

View File

@ -2,62 +2,54 @@ import classNames from 'classnames'
import React, { useEffect, useRef } from 'react'
import { animated, useSpring } from 'react-spring'
import useStore from 'store'
import { FormatOptions, formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import { formatValue } from 'utils/formatters'
interface Props {
amount: BigNumber
amount: number
options?: FormatOptions
className?: string
animate?: boolean
}
export const FormattedNumber = React.memo((props: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const prevAmountRef = useRef<BigNumber>(BN(0))
export const FormattedNumber = React.memo(
(props: Props) => {
const [reduceMotion] = useLocalStorage<boolean>(
REDUCE_MOTION_KEY,
DEFAULT_SETTINGS.reduceMotion,
)
const prevAmountRef = useRef<number>(0)
useEffect(() => {
if (!prevAmountRef.current.eq(props.amount)) prevAmountRef.current = props.amount
}, [props.amount])
useEffect(() => {
if (prevAmountRef.current !== props.amount) prevAmountRef.current = props.amount
}, [props.amount])
const springAmount = useSpring({
number: props.amount.toNumber(),
from: { number: prevAmountRef.current.toNumber() },
config: { duration: 1000 },
})
const springAmount = useSpring({
number: props.amount,
from: { number: prevAmountRef.current },
config: { duration: 1000 },
})
return (prevAmountRef.current.eq(props.amount) && props.amount.isZero()) ||
!props.animate ||
!enableAnimations ? (
<span className={classNames('number', props.className)}>
{formatValue(props.amount.toString(), {
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>
)
})
if (
(prevAmountRef.current === props.amount && props.amount === 0) ||
!props.animate ||
reduceMotion
)
return (
<span className={classNames('number', props.className)}>
{formatValue(props.amount.toString(), props.options)}
</span>
)
return (
<animated.span className={classNames('number', props.className)}>
{springAmount.number.to((num) => formatValue(num, props.options))}
</animated.span>
)
},
(prevProps, nextProps) => prevProps.amount === nextProps.amount,
)
FormattedNumber.displayName = 'FormattedNumber'

View File

@ -1,10 +1,11 @@
import classNames from 'classnames'
import { ReactElement, ReactNode } from 'react'
import { Tooltip } from 'components/Tooltip'
import useStore from 'store'
import { FormattedNumber } from 'components/FormattedNumber'
import { BN } from 'utils/helpers'
import { Tooltip } from 'components/Tooltip'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
tooltip: string | ReactNode
@ -27,7 +28,7 @@ export const Gauge = ({
icon,
labelClassName,
}: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const radius = 16
const percentageValue = percentage > 100 ? 100 : percentage < 0 ? 0 : percentage
const circlePercent = 100 - percentageValue
@ -75,7 +76,7 @@ export const Gauge = ({
strokeDasharray='100'
pathLength='100'
style={{
transition: enableAnimations ? 'stroke-dashoffset 1s ease' : 'none',
transition: reduceMotion ? 'none' : 'stroke-dashoffset 1s ease',
}}
shapeRendering='geometricPrecision'
strokeLinecap='round'
@ -88,7 +89,7 @@ export const Gauge = ({
)}
<FormattedNumber
className={classNames(labelClassName, 'text-2xs')}
amount={BN(Math.round(percentage))}
amount={Math.round(percentage)}
options={{ maxDecimals: 0, minDecimals: 0 }}
animate
/>

View File

@ -3,7 +3,7 @@ import classNames from 'classnames'
import AccountMenu from 'components/Account/AccountMenu'
import DesktopNavigation from 'components/Navigation/DesktopNavigation'
import Settings from 'components/Settings'
import Wallet from 'components/Wallet/Wallet'
import Wallet from 'components/Wallet'
import useStore from 'store'
export const menuTree: { page: Page; label: string }[] = [

View File

@ -0,0 +1,8 @@
<svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20 8C20 8 17.995 5.26822 16.3662 3.63824C14.7373 2.00827 12.4864 1 10 1C5.02944 1 1 5.02944 1 10C1 14.9706 5.02944 19 10 19C14.1031 19 17.5649 16.2543 18.6482 12.5M20 8V2M20 8H14"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 362 B

View File

@ -2,6 +2,7 @@
export { default as Account } from 'components/Icons/Account.svg'
export { default as AccountArrowDown } from 'components/Icons/AccountArrowDown.svg'
export { default as ArrowChartLineUp } from 'components/Icons/ArrowChartLineUp.svg'
export { default as ArrowCircle } from 'components/Icons/ArrowCircle.svg'
export { default as ArrowCircledTopRight } from 'components/Icons/ArrowCircledTopRight.svg'
export { default as ArrowDownLine } from 'components/Icons/ArrowDownLine.svg'
export { default as ArrowRight } from 'components/Icons/ArrowRight.svg'

View File

@ -16,12 +16,12 @@ interface Props {
export const LabelValuePair = ({ label, value, className }: Props) => (
<div className={classNames('flex w-full', className)}>
<Text size='xs' className='flex-grow text-white/60'>
<Text size='xs' className='flex-1 text-white/60'>
{label}
</Text>
<Text size='xs' className='text-white/60'>
{value.format === 'number' ? (
<FormattedNumber animate {...value} amount={BN(value.amount)} />
<FormattedNumber animate {...value} amount={BN(value.amount).toNumber()} />
) : (
value.amount || ''
)}

View File

@ -48,7 +48,7 @@ function AssetListTableRow<TData>(props: Props<TData>) {
key={props.rowData.id}
className={classNames(
'cursor-pointer transition-colors',
'border-b border-white border-opacity-10',
props.rowData.getIsExpanded() ? 'bg-black/20' : 'bg-white/0 hover:bg-white/5',
)}
onClick={() => {

View File

@ -0,0 +1,93 @@
import { useMemo } from 'react'
import { FormattedNumber } from 'components/FormattedNumber'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useDisplayCurrencyPrice from 'hooks/useDisplayCurrencyPrice'
interface Props {
data: BorrowMarketTableData | LendingMarketTableData
}
interface Detail {
amount: number
options: FormatOptions
title: string
}
export default function MarketDetails({ data }: Props) {
const {
convertAmount,
getConversionRate,
symbol: displayCurrencySymbol,
} = useDisplayCurrencyPrice()
const {
asset,
marketMaxLtv,
marketDepositAmount,
marketLiquidityAmount,
marketLiquidationThreshold,
} = data
const totalBorrowed = marketDepositAmount.minus(marketLiquidityAmount)
const details: Detail[] = useMemo(
() => [
{
amount: convertAmount(asset, marketDepositAmount).toNumber(),
options: { abbreviated: true, suffix: ` ${displayCurrencySymbol}` },
title: 'Total Supplied',
},
{
amount: marketMaxLtv * 100,
options: { minDecimals: 2, maxDecimals: 2, suffix: '%' },
title: 'Max LTV',
},
{
amount: marketLiquidationThreshold * 100,
options: { minDecimals: 2, maxDecimals: 2, suffix: '%' },
title: 'Liquidation Threshold',
},
{
amount: getConversionRate(asset.denom).toNumber(),
options: { minDecimals: 2, maxDecimals: 2, suffix: ` ${displayCurrencySymbol}` },
title: 'Oracle Price',
},
{
amount: totalBorrowed.dividedBy(marketDepositAmount).multipliedBy(100).toNumber(),
options: { minDecimals: 2, maxDecimals: 2, suffix: '%' },
title: 'Utilization Rate',
},
],
[
asset,
marketDepositAmount,
marketLiquidationThreshold,
marketMaxLtv,
displayCurrencySymbol,
convertAmount,
getConversionRate,
totalBorrowed,
],
)
return (
<div className='flex flex-1 justify-center rounded-md bg-white bg-opacity-5'>
{details.map((detail, index) => (
<TitleAndSubCell
key={index}
className='text-center'
containerClassName='m-5 ml-10 mr-10 space-y-1'
title={
<FormattedNumber
className='text-center text-xs'
amount={detail.amount}
options={detail.options}
animate
/>
}
sub={detail.title}
/>
))}
</div>
)
}

View File

@ -1,10 +1,9 @@
import classNames from 'classnames'
import { ReactNode, useEffect, useRef } from 'react'
import EscButton from 'components/Button/EscButton'
import Card from 'components/Card'
import EscButton from './Button/EscButton'
interface Props {
header: string | ReactNode
headerClassName?: string
@ -14,12 +13,11 @@ interface Props {
className?: string
contentClassName?: string
modalClassName?: string
open: boolean
onClose: () => void
}
export default function Modal(props: Props) {
const ref: any = useRef(null)
const ref: React.RefObject<HTMLDialogElement> = useRef(null)
const modalClassName = props.modalClassName ?? 'max-w-modal'
function onClose() {
@ -28,19 +26,17 @@ export default function Modal(props: Props) {
}
useEffect(() => {
if (props.open) {
ref.current?.showModal()
} else {
ref.current?.close()
}
}, [props.open])
ref.current?.showModal()
document.body.classList.add('h-screen', 'overflow-hidden')
}, [])
// close dialog on unmount
useEffect(() => {
const dialog = ref.current
return () => {
dialog.removeAttribute('open')
dialog.close()
dialog?.removeAttribute('open')
dialog?.close()
document.body.classList.remove('h-screen', 'overflow-hidden')
}
}, [])
@ -51,13 +47,14 @@ export default function Modal(props: Props) {
className={classNames(
`w-screen border-none bg-transparent text-white`,
'focus-visible:outline-none',
'max-h-full scrollbar-hide',
'backdrop:bg-black/50 backdrop:backdrop-blur-sm',
modalClassName,
)}
>
<Card
className={classNames(
'relative flex max-w-full flex-grow bg-white/5 backdrop-blur-3xl',
'relative flex max-w-full flex-1 bg-white/5 backdrop-blur-3xl',
props.className,
)}
>
@ -65,7 +62,9 @@ export default function Modal(props: Props) {
{props.header}
{!props.hideCloseBtn && <EscButton onClick={props.onClose} />}
</div>
<div className={classNames(props.contentClassName, 'flex-grow')}>
<div
className={classNames(props.contentClassName, 'flex-1 overflow-y-scroll scrollbar-hide')}
>
{props.children ? props.children : props.content}
</div>
</Card>

View File

@ -6,12 +6,12 @@ import {
SortingState,
useReactTable,
} from '@tanstack/react-table'
import { useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import { useEffect, useMemo, useState } from 'react'
import { SortAsc, SortDesc, SortNone } from 'components/Icons'
import Text from 'components/Text'
import useAddVaultAssetTableColumns from 'components/Modals/AddVaultAssets/useAddVaultAssetTableColumns'
import Text from 'components/Text'
interface Props {
assets: BorrowAsset[]
@ -61,7 +61,7 @@ export default function AddVaultAssetTable(props: Props) {
return (
<table className='w-full'>
<thead className='border-b border-b-white/5'>
<thead className='border-b border-white/5'>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header, index) => {

View File

@ -1,10 +1,10 @@
import { useCallback, useState } from 'react'
import Modal from 'components/Modal'
import useStore from 'store'
import Text from 'components/Text'
import { CircularProgress } from 'components/CircularProgress'
import Modal from 'components/Modal'
import AddVaultAssetsModalContent from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModalContent'
import Text from 'components/Text'
import useStore from 'store'
export default function AddVaultBorrowAssetsModal() {
const modal = useStore((s) => s.addVaultBorrowingsModal)
@ -24,9 +24,9 @@ export default function AddVaultBorrowAssetsModal() {
const showContent = modal && vaultModal?.vault
if (!showContent) return null
return (
<Modal
open={!!(modal && showContent)}
header={<Text>Add Assets</Text>}
onClose={onClose}
modalClassName='max-w-modal-xs'

View File

@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from 'react'
import AddVaultAssetTable from 'components/Modals/AddVaultAssets/AddVaultAssetTable'
import SearchBar from 'components/SearchBar'
import Text from 'components/Text'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import AddVaultAssetTable from 'components/Modals/AddVaultAssets/AddVaultAssetTable'
import useStore from 'store'
interface Props {
@ -75,7 +75,7 @@ export default function AddVaultAssetsModalContent(props: Props) {
return (
<>
<div className='border-b border-b-white/5 bg-white/10 px-4 py-3'>
<div className='border-b border-white/5 bg-white/10 px-4 py-3'>
<SearchBar
value={searchString}
placeholder={`Search for e.g. "ETH" or "Ethereum"`}

View File

@ -1,11 +1,11 @@
import Button from 'components/Button'
import { ExclamationMarkCircled } from 'components/Icons'
import Modal from 'components/Modal'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
import Text from 'components/Text'
import useAlertDialog from 'hooks/useAlertDialog'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
function AlertDialogController() {
export default function AlertDialogController() {
const { config, close } = useAlertDialog()
if (!config) return null
@ -28,14 +28,13 @@ function AlertDialog(props: Props) {
return (
<Modal
open
onClose={props.close}
header={
<div className='grid h-12 w-12 place-items-center rounded-sm bg-white/5'>
{icon ?? <ExclamationMarkCircled width={18} />}
</div>
}
modalClassName='w-[577px]'
modalClassName='max-w-modal-sm'
headerClassName='p-8'
contentClassName='px-8 pb-8'
hideCloseBtn
@ -62,5 +61,3 @@ function AlertDialog(props: Props) {
</Modal>
)
}
export default AlertDialogController

View File

@ -15,7 +15,6 @@ import { BN } from 'utils/helpers'
interface Props {
asset: Asset
title: string
isOpen: boolean
coinBalances: Coin[]
contentHeader?: JSX.Element
actionButtonText: string
@ -30,7 +29,6 @@ export default function AssetAmountSelectActionModal(props: Props) {
const {
asset,
title,
isOpen,
coinBalances,
contentHeader = null,
actionButtonText,
@ -52,12 +50,11 @@ export default function AssetAmountSelectActionModal(props: Props) {
)
const handleActionClick = useCallback(() => {
onAction(amount, amount.eq(maxAmount))
onAction(amount, amount.isEqualTo(maxAmount))
}, [amount, maxAmount, onAction])
return (
<Modal
open={isOpen}
onClose={onClose}
header={
<span className='flex items-center gap-4 px-4'>
@ -69,9 +66,9 @@ export default function AssetAmountSelectActionModal(props: Props) {
contentClassName='flex flex-col min-h-[400px]'
>
{contentHeader}
<div className='flex flex-grow items-start gap-6 p-6'>
<div className='flex flex-1 items-start gap-6 p-6'>
<Card
className='flex flex-grow bg-white/5 p-4'
className='flex flex-1 bg-white/5 p-4'
contentClassName='gap-6 flex flex-col justify-between h-full'
>
<TokenInputWithSlider

View File

@ -7,7 +7,7 @@ import Card from 'components/Card'
import Divider from 'components/Divider'
import { ArrowRight } from 'components/Icons'
import Modal from 'components/Modal'
import Select from 'components/Select/Select'
import Select from 'components/Select'
import Text from 'components/Text'
import TitleAndSubCell from 'components/TitleAndSubCell'
import TokenInputWithSlider from 'components/TokenInputWithSlider'
@ -21,8 +21,7 @@ import { formatPercent, formatValue } from 'utils/formatters'
import { BN } from 'utils/helpers'
function getDebtAmount(modal: BorrowModal | null) {
if (!(modal?.marketData as BorrowAssetActive)?.debt) return '0'
return BN((modal?.marketData as BorrowAssetActive).debt).toString()
return BN((modal?.marketData as BorrowMarketTableData)?.debt ?? 0).toString()
}
function getAssetLogo(modal: BorrowModal | null) {
@ -119,9 +118,9 @@ export default function BorrowModal() {
})
}, [amount, modal?.asset, currentAccount, isRepay])
if (!modal) return null
return (
<Modal
open={!!modal}
onClose={onClose}
header={
<span className='flex items-center gap-4 px-4'>
@ -134,7 +133,7 @@ export default function BorrowModal() {
headerClassName='gradient-header pl-2 pr-2.5 py-2.5 border-b-white/5 border-b'
contentClassName='flex flex-col'
>
<div className='flex gap-3 border-b border-b-white/5 px-6 py-4 gradient-header'>
<div className='flex gap-3 border-b border-white/5 px-6 py-4 gradient-header'>
<TitleAndSubCell
title={formatPercent(modal?.marketData.borrowRate || '0')}
sub={'Borrow rate'}
@ -153,9 +152,9 @@ export default function BorrowModal() {
sub={'Liquidity available'}
/>
</div>
<div className='flex flex-grow items-start gap-6 p-6'>
<div className='flex flex-1 items-start gap-6 p-6'>
<Card
className='flex flex-grow bg-white/5 p-4'
className='flex flex-1 bg-white/5 p-4'
contentClassName='gap-6 flex flex-col justify-between h-full min-h-[380px]'
>
<div className='flex w-full flex-wrap'>
@ -176,6 +175,7 @@ export default function BorrowModal() {
options={accountOptions ?? []}
title='Accounts'
defaultValue={selectedAccount?.id}
containerClassName='w-full'
onChange={(account) => {
accounts && setSelectedAccount(accounts?.find((a) => a.id === account))
}}

View File

@ -83,9 +83,9 @@ export default function FundWithdrawModalContent(props: Props) {
}
return (
<div className='flex flex-grow items-start gap-6 p-6'>
<div className='flex flex-1 items-start gap-6 p-6'>
<Card
className='flex flex-grow bg-white/5 p-4'
className='flex flex-1 bg-white/5 p-4'
contentClassName='gap-6 flex flex-col justify-between h-full'
>
<TokenInputWithSlider

View File

@ -1,9 +1,9 @@
import { CircularProgress } from 'components/CircularProgress'
import Modal from 'components/Modal'
import FundWithdrawModalContent from 'components/Modals/FundWithdraw/FundAndWithdrawModalContent'
import Text from 'components/Text'
import useCurrentAccount from 'hooks/useCurrentAccount'
import useStore from 'store'
import { CircularProgress } from 'components/CircularProgress'
import FundWithdrawModalContent from 'components/Modals/FundWithdraw/FundAndWithdrawModalContent'
export default function FundAndWithdrawModal() {
const currentAccount = useCurrentAccount()
@ -14,9 +14,9 @@ export default function FundAndWithdrawModal() {
useStore.setState({ fundAndWithdrawModal: null })
}
if (!modal) return null
return (
<Modal
open={!!modal}
onClose={onClose}
header={
<span className='flex items-center gap-4 px-4'>

View File

@ -1,6 +1,6 @@
import DisplayCurrency from 'components/DisplayCurrency'
import TitleAndSubCell from 'components/TitleAndSubCell'
import { FormattedNumber } from 'components/FormattedNumber'
import TitleAndSubCell from 'components/TitleAndSubCell'
import useAssetIncentivesApy from 'hooks/useAssetIncentiveApy'
import useCurrentWalletBalance from 'hooks/useCurrentWalletBalance'
import { BNCoin } from 'types/classes/BNCoin'
@ -15,17 +15,18 @@ function DetailsHeader({ data }: Props) {
const balanceInWallet = useCurrentWalletBalance(asset.denom)
return (
<div className='flex gap-6 border-b border-b-white/5 px-6 py-4 gradient-header'>
<div className='flex gap-6 border-b border-white/5 px-6 py-4 gradient-header'>
{assetApy && (
<>
<TitleAndSubCell
title={
<>
<FormattedNumber amount={assetApy} options={{ suffix: '%' }} />
<FormattedNumber amount={assetApy.toNumber()} options={{ suffix: '%' }} animate />
<FormattedNumber
className='ml-2 text-xs'
amount={assetApy.div(365)}
amount={assetApy.dividedBy(365).toNumber()}
options={{ suffix: '%/day' }}
animate
/>
</>
}

View File

@ -66,7 +66,6 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
return (
<AssetAmountSelectActionModal
asset={asset}
isOpen={true}
contentHeader={<DetailsHeader data={data} />}
coinBalances={coinBalances}
actionButtonText={actionText}

View File

@ -4,21 +4,23 @@ import {
BorrowModal,
FundAndWithdrawModal,
LendAndReclaimModalController,
SettingsModal,
UnlockModal,
VaultModal,
WithdrawFromVaults,
WithdrawFromVaultsModal,
} from 'components/Modals'
export default function ModalsContainer() {
return (
<>
<VaultModal />
<AddVaultBorrowAssetsModal />
<BorrowModal />
<FundAndWithdrawModal />
<AddVaultBorrowAssetsModal />
<UnlockModal />
<LendAndReclaimModalController />
<WithdrawFromVaults />
<SettingsModal />
<UnlockModal />
<VaultModal />
<WithdrawFromVaultsModal />
<AlertDialogController />
</>
)

View File

@ -0,0 +1,32 @@
import classNames from 'classnames'
import { ReactNode } from 'react'
import Text from 'components/Text'
interface Props {
label: string
decsription: string
className?: string
children: ReactNode | ReactNode[]
}
export default function SettingsOptions(props: Props) {
return (
<div
className={classNames(
'mb-6 flex w-full items-start justify-between border-b border-white/5',
props.className,
)}
>
<div className='flex w-120 flex-wrap'>
<Text size='lg' className='mb-2 w-full'>
{props.label}
</Text>
<Text size='xs' className='text-white/50'>
{props.decsription}
</Text>
</div>
<div className='flex w-60 flex-wrap justify-end'>{props.children}</div>
</div>
)
}

View File

@ -0,0 +1,42 @@
import classNames from 'classnames'
import Switch from 'components/Switch'
import Text from 'components/Text'
interface Props {
onChange: (value: boolean) => void
name: string
value: boolean
label: string
decsription: string
className?: string
withStatus?: boolean
}
export default function SettingsSwitch(props: Props) {
return (
<div
className={classNames(
'mb-6 flex w-full items-start justify-between border-b border-white/5 pb-6',
props.className,
)}
>
<div className='flex w-100 flex-wrap'>
<Text size='lg' className='mb-2 w-full'>
{props.label}
</Text>
<Text size='xs' className='text-white/50'>
{props.decsription}
</Text>
</div>
<div className='flex w-60 flex-wrap justify-end'>
<Switch name={props.name} checked={props.value} onChange={props.onChange} />
{props.withStatus && (
<Text size='sm' className='mt-2 w-full text-end'>
{props.value ? 'ON' : 'OFF'}
</Text>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,306 @@
import classNames from 'classnames'
import { useCallback, useMemo, useState } from 'react'
import AssetImage from 'components/AssetImage'
import Button from 'components/Button'
import { ArrowCircle, Enter } from 'components/Icons'
import Modal from 'components/Modal'
import SettingsOptions from 'components/Modals/Settings/SettingsOptions'
import SettingsSwitch from 'components/Modals/Settings/SettingsSwitch'
import NumberInput from 'components/NumberInput'
import Select from 'components/Select'
import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import {
DISPLAY_CURRENCY_KEY,
LEND_ASSETS_KEY,
PREFERRED_ASSET_KEY,
REDUCE_MOTION_KEY,
SLIPPAGE_KEY,
} from 'constants/localStore'
import useAlertDialog from 'hooks/useAlertDialog'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
import { getAllAssets, getDisplayCurrencies } from 'utils/assets'
import { BN } from 'utils/helpers'
const slippages = [0.02, 0.03]
export default function SettingsModal() {
const modal = useStore((s) => s.settingsModal)
const { open: showResetDialog } = useAlertDialog()
const displayCurrencies = getDisplayCurrencies()
const assets = getAllAssets()
const [customSlippage, setCustomSlippage] = useState<number>(0)
const [inputRef, setInputRef] = useState<React.RefObject<HTMLInputElement>>()
const [isCustom, setIsCustom] = useState(false)
const [displayCurrency, setDisplayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
const [preferredAsset, setPreferredAsset] = useLocalStorage<string>(
PREFERRED_ASSET_KEY,
DEFAULT_SETTINGS.preferredAsset,
)
const [reduceMotion, setReduceMotion] = useLocalStorage<boolean>(
REDUCE_MOTION_KEY,
DEFAULT_SETTINGS.reduceMotion,
)
const [lendAssets, setLendAssets] = useLocalStorage<boolean>(
LEND_ASSETS_KEY,
DEFAULT_SETTINGS.lendAssets,
)
const [slippage, setSlippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
const displayCurrenciesOptions = useMemo(
() =>
displayCurrencies.map((asset, index) => ({
label: (
<div className='flex w-full gap-2' key={index}>
<AssetImage asset={asset} size={16} />
<Text size='sm' className='leading-4'>
{asset.symbol}
</Text>
</div>
),
value: asset.denom,
})),
[displayCurrencies],
)
const preferredAssetsOptions = useMemo(
() =>
assets.map((asset, index) => ({
label: [
<div className='flex w-full gap-2' key={index}>
<AssetImage asset={asset} size={16} />
<Text size='sm' className='leading-4'>
{asset.symbol}
</Text>
</div>,
],
value: asset.denom,
})),
[assets],
)
const handleReduceMotion = useCallback(
(value: boolean) => {
setReduceMotion(!value)
},
[setReduceMotion],
)
const handleLendAssets = useCallback(
(value: boolean) => {
setLendAssets(value)
},
[setLendAssets],
)
const handlePreferredAsset = useCallback(
(value: string) => {
setPreferredAsset(value)
},
[setPreferredAsset],
)
const handleDisplayCurrency = useCallback(
(value: string) => {
setDisplayCurrency(value)
},
[setDisplayCurrency],
)
const handleSlippageInputFocus = useCallback(() => {
setIsCustom(true)
}, [])
const handleSlippage = useCallback(
(value: number) => {
setSlippage(value)
},
[setSlippage],
)
const handleSlippageInputBlur = useCallback(() => {
setIsCustom(false)
if (!customSlippage) {
setCustomSlippage(0)
handleSlippage(slippages[0])
return
}
const value = Number(customSlippage || 0)
if (slippages.includes(value)) {
setCustomSlippage(0)
handleSlippage(value)
return
}
handleSlippage(BN(customSlippage).toNumber())
}, [customSlippage, handleSlippage])
const handleSlippageInput = useCallback(
(value: BigNumber) => {
if (!value.toString()) {
return
}
setCustomSlippage(value.dividedBy(100).toNumber())
handleSlippage(value.dividedBy(100).toNumber())
},
[handleSlippage],
)
const handleResetSettings = useCallback(() => {
handleDisplayCurrency(DEFAULT_SETTINGS.displayCurrency)
handlePreferredAsset(DEFAULT_SETTINGS.preferredAsset)
handleSlippage(DEFAULT_SETTINGS.slippage)
handleReduceMotion(!DEFAULT_SETTINGS.reduceMotion)
handleLendAssets(DEFAULT_SETTINGS.lendAssets)
}, [
handleDisplayCurrency,
handleReduceMotion,
handleLendAssets,
handlePreferredAsset,
handleSlippage,
])
const showResetModal = useCallback(() => {
showResetDialog({
icon: (
<div className='flex h-full w-full p-3'>
<ArrowCircle />
</div>
),
title: 'Are you sure you want to restore to default?',
description:
'Once you reset to default settings you cant revert it, and will result in the permanent loss of your current settings',
positiveButton: {
text: 'Yes',
icon: <Enter />,
onClick: handleResetSettings,
},
})
}, [showResetDialog, handleResetSettings])
const handleCloseModal = useCallback(() => {
useStore.setState({ settingsModal: false })
}, [])
if (!modal) return null
return (
<Modal
onClose={handleCloseModal}
header={
<span className='flex flex-wrap items-center'>
<Text size='3xl' className='w-full pb-1'>
Global Settings
</Text>
<Text size='sm' className='text-white/50'>
Customise to match your workflow
</Text>
</span>
}
headerClassName='p-6'
contentClassName='flex flex-wrap px-6 pb-6 pt-4'
>
<SettingsSwitch
onChange={handleLendAssets}
name='lendAssets'
value={lendAssets}
label='Lend assets in credit account'
decsription='By turning this on you will automatically lend out all the assets you deposit into your credit account to earn yield.'
withStatus
/>
<SettingsSwitch
onChange={handleReduceMotion}
name='reduceMotion'
value={!reduceMotion}
label='Reduce Motion'
decsription='Turns off all animations inside the dApp. Turning animations off can increase the
overall performance on lower-end hardware.'
withStatus
/>
<SettingsOptions
label='Preferred asset'
decsription='By selecting a different asset you always have the trading pair or asset selector
pre-filled with this asset.'
className='pb-6'
>
<Select
label='Global'
options={preferredAssetsOptions}
defaultValue={preferredAsset}
onChange={handlePreferredAsset}
className='relative w-60 rounded-base border border-white/10'
containerClassName='justify-end mb-4'
/>
<Select
label='Display Currency'
options={displayCurrenciesOptions}
defaultValue={displayCurrency}
onChange={handleDisplayCurrency}
className='relative w-60 rounded-base border border-white/10'
containerClassName='justify-end'
/>
</SettingsOptions>
<SettingsOptions
label='Slippage tolerance'
decsription='Some vaults require token swaps. The transaction will fail if the price of the swap asset changes unfavourably by more than this percentage'
className='pb-21'
>
{slippages.map((value) => (
<Button
key={`slippage-${value}`}
color='secondary'
variant='rounded'
onClick={() => {
handleSlippage(value)
}}
className={classNames(
'mr-3 text-[16px]',
slippage === value && !isCustom && 'bg-white/10',
)}
text={`${value * 100}%`}
/>
))}
<Button
onClick={() => inputRef?.current?.focus()}
color='secondary'
variant='rounded'
className={classNames('w-16', !slippages.includes(slippage) && 'bg-white/10')}
>
<NumberInput
asset={{ decimals: 0, symbol: '%' }}
onRef={setInputRef}
onChange={handleSlippageInput}
onBlur={handleSlippageInputBlur}
onFocus={handleSlippageInputFocus}
amount={BN(customSlippage).multipliedBy(100)}
max={BN(10)}
min={BN(0)}
maxDecimals={1}
maxLength={2}
style={{ fontSize: 16 }}
placeholder='...'
className='!w-6'
/>
%
</Button>
</SettingsOptions>
<div className='flex w-full justify-between'>
<Button
color='quaternary'
variant='transparent'
onClick={showResetModal}
leftIcon={<ArrowCircle />}
text='Reset to default settings'
/>
<Button text='Confirm' onClick={handleCloseModal} />
</div>
</Modal>
)
}

View File

@ -2,10 +2,10 @@ import { useState } from 'react'
import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
import Text from 'components/Text'
import useStore from 'store'
import { hardcodedFee } from 'utils/constants'
import { NoIcon, YesIcon } from 'components/Modals/AlertDialog/ButtonIcons'
interface Props {
depositedVault: DepositedVault

View File

@ -1,4 +1,3 @@
import { CircularProgress } from 'components/CircularProgress'
import { LockUnlocked } from 'components/Icons'
import Modal from 'components/Modal'
import UnlockModalContent from 'components/Modals/Unlock/UnlockModalContent'
@ -11,9 +10,9 @@ export default function UnlockModal() {
useStore.setState({ unlockModal: null })
}
if (!modal) return null
return (
<Modal
open={!!modal}
onClose={onClose}
header={
<div className='grid h-12 w-12 place-items-center rounded-sm bg-white/5'>
@ -25,11 +24,7 @@ export default function UnlockModal() {
contentClassName='px-8 pb-8'
hideCloseBtn
>
{modal ? (
<UnlockModalContent depositedVault={modal.vault} onClose={onClose} />
) : (
<CircularProgress />
)}
<UnlockModalContent depositedVault={modal.vault} onClose={onClose} />
</Modal>
)
}

View File

@ -1,24 +1,23 @@
import { useEffect, useMemo, useState } from 'react'
import BigNumber from 'bignumber.js'
import React from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { BN } from 'utils/helpers'
import { findCoinByDenom, getAssetByDenom } from 'utils/assets'
import Button from 'components/Button'
import TokenInput from 'components/TokenInput'
import Divider from 'components/Divider'
import Text from 'components/Text'
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
import { formatPercent } from 'utils/formatters'
import Slider from 'components/Slider'
import usePrices from 'hooks/usePrices'
import useMarketAssets from 'hooks/useMarketAssets'
import { calculateMaxBorrowAmounts } from 'utils/vaults'
import useStore from 'store'
import DisplayCurrency from 'components/DisplayCurrency'
import usePrice from 'hooks/usePrice'
import { BNCoin } from 'types/classes/BNCoin'
import Divider from 'components/Divider'
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
import Slider from 'components/Slider'
import Text from 'components/Text'
import TokenInput from 'components/TokenInput'
import useDepositVault from 'hooks/broadcast/useDepositVault'
import useMarketAssets from 'hooks/useMarketAssets'
import usePrice from 'hooks/usePrice'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { findCoinByDenom, getAssetByDenom } from 'utils/assets'
import { formatPercent } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { calculateMaxBorrowAmounts } from 'utils/vaults'
export interface VaultBorrowingsProps {
account: Account
@ -49,11 +48,11 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
})
const primaryValue = useMemo(
() => props.primaryAmount.times(primaryPrice),
() => props.primaryAmount.multipliedBy(primaryPrice),
[props.primaryAmount, primaryPrice],
)
const secondaryValue = useMemo(
() => props.secondaryAmount.times(secondaryPrice),
() => props.secondaryAmount.multipliedBy(secondaryPrice),
[props.secondaryAmount, secondaryPrice],
)
@ -62,7 +61,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
const price = prices.find((price) => price.denom === curr.denom)?.amount
if (!price) return prev
return prev.plus(curr.amount.times(price))
return prev.plus(curr.amount.multipliedBy(price))
}, BN(0) as BigNumber)
}, [props.borrowings, prices])
@ -113,7 +112,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
new BNCoin({
denom,
amount: (
maxAmount.plus(currentAmount).times(value).div(100).decimalPlaces(0) || BN(0)
maxAmount.plus(currentAmount).multipliedBy(value).dividedBy(100).decimalPlaces(0) || BN(0)
).toString(),
}),
]
@ -164,7 +163,7 @@ export default function VaultBorrowings(props: VaultBorrowingsProps) {
}
return (
<div className='flex flex-grow flex-col gap-4 p-4'>
<div className='flex flex-1 flex-col gap-4 p-4'>
{props.borrowings.map((coin) => {
const asset = getAssetByDenom(coin.denom)
const maxAmount = maxAmounts.find((maxAmount) => maxAmount.denom === coin.denom)?.amount

View File

@ -1,12 +1,11 @@
import BigNumber from 'bignumber.js'
import { useMemo } from 'react'
import DisplayCurrency from 'components/DisplayCurrency'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { formatAmountWithSymbol } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { BNCoin } from 'types/classes/BNCoin'
interface Props {
borrowings: BNCoin[]
@ -22,7 +21,7 @@ export default function VaultDepositSubTitle(props: Props) {
props.borrowings.map((coin) => {
const price = prices.find((p) => p.denom === coin.denom)?.amount
if (!price || coin.amount.isZero()) return
borrowingValue = borrowingValue.plus(coin.amount.times(price))
borrowingValue = borrowingValue.plus(coin.amount.multipliedBy(price))
texts.push(
formatAmountWithSymbol({
denom: coin.denom,

View File

@ -4,17 +4,17 @@ import { useMemo, useState } from 'react'
import Button from 'components/Button'
import DisplayCurrency from 'components/DisplayCurrency'
import Divider from 'components/Divider'
import { Gauge } from 'components/Gauge'
import { ArrowRight, ExclamationMarkCircled } from 'components/Icons'
import Slider from 'components/Slider'
import Switch from 'components/Switch'
import Text from 'components/Text'
import TokenInput from 'components/TokenInput'
import usePrice from 'hooks/usePrice'
import { getAmount } from 'utils/accounts'
import { BN } from 'utils/helpers'
import { Gauge } from 'components/Gauge'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { getAmount } from 'utils/accounts'
import { BN } from 'utils/helpers'
interface Props {
primaryAmount: BigNumber
@ -38,11 +38,11 @@ export default function VaultDeposit(props: Props) {
const secondaryPrice = usePrice(props.secondaryAsset.denom)
const primaryValue = useMemo(
() => props.primaryAmount.times(primaryPrice),
() => props.primaryAmount.multipliedBy(primaryPrice),
[props.primaryAmount, primaryPrice],
)
const secondaryValue = useMemo(
() => props.secondaryAmount.times(secondaryPrice),
() => props.secondaryAmount.multipliedBy(secondaryPrice),
[props.secondaryAmount, secondaryPrice],
)
const totalValue = useMemo(
@ -51,7 +51,7 @@ export default function VaultDeposit(props: Props) {
)
const primaryValuePercentage = useMemo(() => {
const value = primaryValue.div(totalValue).times(100).decimalPlaces(2).toNumber()
const value = primaryValue.dividedBy(totalValue).multipliedBy(100).decimalPlaces(2).toNumber()
return isNaN(value) ? 50 : value
}, [primaryValue, totalValue])
const secondaryValuePercentage = useMemo(
@ -63,8 +63,8 @@ export default function VaultDeposit(props: Props) {
() =>
BN(
Math.min(
availablePrimaryAmount.times(primaryPrice).toNumber(),
availableSecondaryAmount.times(secondaryPrice).toNumber(),
availablePrimaryAmount.multipliedBy(primaryPrice).toNumber(),
availableSecondaryAmount.multipliedBy(secondaryPrice).toNumber(),
),
),
[availablePrimaryAmount, primaryPrice, availableSecondaryAmount, secondaryPrice],
@ -83,7 +83,8 @@ export default function VaultDeposit(props: Props) {
)
const [percentage, setPercentage] = useState(
primaryValue.dividedBy(maxAssetValueNonCustom).times(100).decimalPlaces(0).toNumber() || 0,
primaryValue.dividedBy(maxAssetValueNonCustom).multipliedBy(100).decimalPlaces(0).toNumber() ||
0,
)
const disableInput =
(availablePrimaryAmount.isZero() || availableSecondaryAmount.isZero()) && !props.isCustomRatio
@ -103,7 +104,7 @@ export default function VaultDeposit(props: Props) {
amount = primaryMax
}
props.onChangePrimaryAmount(amount)
setPercentage(amount.dividedBy(primaryMax).times(100).decimalPlaces(0).toNumber())
setPercentage(amount.dividedBy(primaryMax).multipliedBy(100).decimalPlaces(0).toNumber())
if (!props.isCustomRatio) {
props.onChangeSecondaryAmount(secondaryMax.multipliedBy(amount.dividedBy(primaryMax)))
}
@ -114,7 +115,7 @@ export default function VaultDeposit(props: Props) {
amount = secondaryMax
}
props.onChangeSecondaryAmount(amount)
setPercentage(amount.dividedBy(secondaryMax).times(100).decimalPlaces(0).toNumber())
setPercentage(amount.dividedBy(secondaryMax).multipliedBy(100).decimalPlaces(0).toNumber())
if (!props.isCustomRatio) {
props.onChangePrimaryAmount(primaryMax.multipliedBy(amount.dividedBy(secondaryMax)))
}
@ -152,7 +153,7 @@ export default function VaultDeposit(props: Props) {
strokeWidth={3}
/>
</div>
<div className='flex h-full flex-grow flex-col justify-between gap-6'>
<div className='flex h-full flex-1 flex-col justify-between gap-6'>
<TokenInput
onChange={onChangePrimaryDeposit}
amount={props.primaryAmount}

View File

@ -27,8 +27,8 @@ export default function VaultDepositSubTitle(props: Props) {
})
const positionValue = props.primaryAmount
.times(primaryPrice)
.plus(props.secondaryAmount.times(secondaryPrice))
.multipliedBy(primaryPrice)
.plus(props.secondaryAmount.multipliedBy(secondaryPrice))
.toNumber()
const showPrimaryText = !props.primaryAmount.isZero()

View File

@ -3,14 +3,14 @@ import { useCallback, useMemo, useState } from 'react'
import Accordion from 'components/Accordion'
import AccountSummary from 'components/Account/AccountSummary'
import useIsOpenArray from 'hooks/useIsOpenArray'
import { BN } from 'utils/helpers'
import useUpdateAccount from 'hooks/useUpdateAccount'
import VaultBorrowings from 'components/Modals/Vault/VaultBorrowings'
import VaultBorrowingsSubTitle from 'components/Modals/Vault/VaultBorrowingsSubTitle'
import VaultDeposit from 'components/Modals/Vault/VaultDeposits'
import VaultBorrowings from 'components/Modals/Vault/VaultBorrowings'
import VaultDepositSubTitle from 'components/Modals/Vault/VaultDepositsSubTitle'
import useIsOpenArray from 'hooks/useIsOpenArray'
import useUpdateAccount from 'hooks/useUpdateAccount'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
interface Props {
vault: Vault | DepositedVault
@ -82,7 +82,7 @@ export default function VaultModalContent(props: Props) {
}
return (
<div className='flex flex-grow items-start gap-6 p-6'>
<div className='flex flex-1 items-start gap-6 p-6'>
<Accordion
className='h-[546px] overflow-y-scroll scrollbar-hide'
items={[

View File

@ -1,4 +1,4 @@
import VaultLogo from 'components/Earn/vault/VaultLogo'
import VaultLogo from 'components/Earn/Farm/VaultLogo'
import Modal from 'components/Modal'
import VaultModalContent from 'components/Modals/Vault/VaultModalContent'
import Text from 'components/Text'
@ -38,7 +38,6 @@ function VaultModal(props: Props) {
return (
<Modal
open={true}
onClose={onClose}
header={
<span className='flex items-center gap-4 px-4'>

View File

@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom'
import Button from 'components/Button'
import { CircularProgress } from 'components/CircularProgress'
import DisplayCurrency from 'components/DisplayCurrency'
import VaultLogo from 'components/Earn/vault/VaultLogo'
import VaultLogo from 'components/Earn/Farm/VaultLogo'
import { FormattedNumber } from 'components/FormattedNumber'
import Modal from 'components/Modal'
import Text from 'components/Text'
@ -13,9 +13,8 @@ import { BNCoin } from 'types/classes/BNCoin'
import { getAssetByDenom } from 'utils/assets'
import { hardcodedFee } from 'utils/constants'
import { demagnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
export default function WithdrawFromVaults() {
export default function WithdrawFromVaultsModal() {
const modal = useStore((s) => s.withdrawFromVaultsModal)
const { accountId } = useParams()
const [isConfirming, setIsConfirming] = useState(false)
@ -38,9 +37,10 @@ export default function WithdrawFromVaults() {
onClose()
}
if (!modal) return null
return (
<Modal
open={!!modal}
onClose={onClose}
header={
<span className='flex items-center'>
@ -63,7 +63,7 @@ export default function WithdrawFromVaults() {
return (
<div className='flex items-center gap-4' key={vault.unlockId}>
<VaultLogo vault={vault} />
<div className='flex flex-grow flex-wrap'>
<div className='flex flex-1 flex-wrap'>
<Text className='w-full'>{vault.name}</Text>
<Text size='sm' className='w-full text-white/50'>
Unlocked
@ -72,14 +72,16 @@ export default function WithdrawFromVaults() {
<div className='flex flex-wrap'>
<DisplayCurrency coin={coin} className='w-full text-right' />
<FormattedNumber
amount={BN(demagnify(vault.amounts.primary, primaryAsset))}
amount={demagnify(vault.amounts.primary, primaryAsset)}
className='w-full text-right text-sm text-white/50'
options={{ suffix: ` ${vault.symbols.primary}` }}
animate
/>
<FormattedNumber
amount={BN(demagnify(vault.amounts.secondary, secondaryAsset))}
amount={demagnify(vault.amounts.secondary, secondaryAsset)}
className='w-full text-right text-sm text-white/50'
options={{ suffix: ` ${vault.symbols.secondary}` }}
animate
/>
</div>
</div>

View File

@ -1,8 +1,9 @@
export { default as AddVaultBorrowAssetsModal } from 'components/Modals/AddVaultAssets/AddVaultBorrowAssetsModal'
export { default as BorrowModal } from 'components/Modals/Borrow/BorrowModal'
export { default as FundAndWithdrawModal } from 'components/Modals/FundWithdraw/FundAndWithdrawModal'
export { default as LendAndReclaimModalController } from 'components/Modals/LendAndReclaim'
export { default as UnlockModal } from 'components/Modals/Unlock/UnlockModal'
export { default as VaultModal } from 'components/Modals/Vault'
export { default as WithdrawFromVaults } from 'components/Modals/WithdrawFromVaults/WithdrawFromVaults'
export { default as AlertDialogController } from 'components/Modals/AlertDialog'
export { default as BorrowModal } from 'components/Modals/BorrowModal'
export { default as FundAndWithdrawModal } from 'components/Modals/FundWithdraw'
export { default as LendAndReclaimModalController } from 'components/Modals/LendAndReclaim'
export { default as SettingsModal } from 'components/Modals/Settings'
export { default as UnlockModal } from 'components/Modals/Unlock'
export { default as VaultModal } from 'components/Modals/Vault'
export { default as WithdrawFromVaultsModal } from 'components/Modals/WithdrawFromVaultsModal'

View File

@ -13,7 +13,7 @@ export default function DesktopNavigation() {
}
return (
<div className='flex flex-grow items-center'>
<div className='flex flex-1 items-center'>
<NavLink href={getRoute('trade', address, accountId)} isActive={false}>
<span className='block h-10 w-10'>
<Logo />

View File

@ -6,7 +6,7 @@ import { demagnify, formatValue, magnify } from 'utils/formatters'
import { BN } from 'utils/helpers'
interface Props {
asset: Asset
asset: Asset | PseudoAsset
amount: BigNumber
min?: BigNumber
max?: BigNumber
@ -16,6 +16,7 @@ interface Props {
allowNegative?: boolean
style?: {}
disabled?: boolean
placeholder?: string
onChange: (amount: BigNumber) => void
onBlur?: () => void
onFocus?: () => void
@ -37,11 +38,11 @@ export default function NumberInput(props: Props) {
formatValue(props.amount.toNumber(), {
decimals: props.asset.decimals,
minDecimals: 0,
maxDecimals: props.asset.decimals,
maxDecimals: props.maxDecimals,
thousandSeparator: false,
}),
)
}, [props.amount, props.asset])
}, [props.amount, props.asset, props.maxDecimals])
useEffect(() => {
if (!props.onRef) return
@ -157,7 +158,7 @@ export default function NumberInput(props: Props) {
props.className,
)}
style={props.style}
placeholder='0'
placeholder={props.placeholder ?? '0'}
/>
)
}

View File

@ -1,5 +1,6 @@
import classNames from 'classnames'
import { ChangeEvent, forwardRef } from 'react'
import { ChangeEvent, LegacyRef } from 'react'
import React from 'react'
import { Search } from 'components/Icons'
@ -10,7 +11,7 @@ interface Props {
onChange: (value: string) => void
}
const SearchBar = (props: Props) => {
const SearchBar = (props: Props, ref: LegacyRef<HTMLDivElement>) => {
function onChange(event: ChangeEvent<HTMLInputElement>) {
props.onChange(event.target.value)
}
@ -22,11 +23,12 @@ const SearchBar = (props: Props) => {
'relative isolate max-w-full overflow-hidden rounded-sm',
'before:content-[" "] before:absolute before:inset-0 before:-z-1 before:rounded-sm before:p-[1px] before:border-glas',
)}
ref={ref}
>
<Search width={14} height={14} className='mr-2.5 text-white' />
<input
value={props.value}
className='h-full w-full bg-transparent text-xs placeholder-white/30 outline-none'
className='h-full w-full bg-transparent text-xs placeholder-white/50 outline-none'
placeholder={props.placeholder}
onChange={(event) => onChange(event)}
autoFocus={props.autofocus}
@ -35,4 +37,4 @@ const SearchBar = (props: Props) => {
)
}
export default forwardRef(SearchBar)
export default React.forwardRef(SearchBar)

View File

@ -1,42 +1,50 @@
import classNames from 'classnames'
import AssetImage from 'components/AssetImage'
import DisplayCurrency from 'components/DisplayCurrency'
import { ChevronDown } from 'components/Icons'
import { ChevronDown, ChevronRight } from 'components/Icons'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import { formatValue } from 'utils/formatters'
import AssetImage from 'components/AssetImage'
import { BNCoin } from 'types/classes/BNCoin'
import { formatValue } from 'utils/formatters'
interface Props extends Option {
interface Props extends SelectOption {
isSelected?: boolean
isDisplay?: boolean
isClicked?: boolean
onClick?: (value: string) => void
displayClassName?: string
}
export default function Option(props: Props) {
const isCoin = !!props.denom
function handleOnClick(value: string | undefined) {
if (!props.onClick || !value) return
props.onClick(value)
}
if (isCoin) {
const asset = ASSETS.find((asset) => asset.denom === props.denom) || ASSETS[0]
const asset = ASSETS.find((asset) => asset.denom === props.denom) ?? ASSETS[0]
const balance = props.amount ?? '0'
if (props.isDisplay) {
return (
<div
className={classNames('flex items-center gap-2 bg-white/10 p-3', 'hover:cursor-pointer')}
className={classNames(
'flex h-full w-auto items-center gap-2 bg-white/10 px-2',
'hover:cursor-pointer',
props.displayClassName,
)}
>
<AssetImage asset={asset} size={20} />
<span>{asset.symbol}</span>
<span
<span className='flex'>{asset.symbol}</span>
<ChevronRight
className={classNames(
'inline-block w-2.5 transition-transform',
props.isClicked ? 'rotate-0' : '-rotate-90',
'block h-3 w-1.5 transition-transform',
props.isClicked ? 'rotate-90' : 'rotate-0',
)}
>
<ChevronDown />
</span>
/>
</div>
)
}
@ -50,7 +58,7 @@ export default function Option(props: Props) {
'hover:cursor-pointer hover:bg-white/20',
!props.isSelected ? 'bg-white/10' : 'pointer-events-none',
)}
onClick={() => props?.onClick && props.onClick(asset.denom)}
onClick={() => handleOnClick(asset.denom)}
>
<div className='row-span-2 flex h-full items-center justify-center'>
<AssetImage asset={asset} size={32} />
@ -74,15 +82,12 @@ export default function Option(props: Props) {
)
}
const label = props.label
if (props.isDisplay) {
return (
<div
className={classNames(
'flex w-full items-center justify-between bg-white/10 p-3 hover:cursor-pointer',
)}
className={classNames('flex w-full items-center justify-between p-3 hover:cursor-pointer')}
>
<span>{label}</span>
<span className='flex flex-1'>{props.label}</span>
<span
className={classNames(
'inline-block w-2.5 transition-transform',
@ -101,9 +106,9 @@ export default function Option(props: Props) {
'block p-3 hover:cursor-pointer hover:bg-white/20',
props.isSelected && 'bg-white/10',
)}
onClick={() => props?.onClick && props.onClick(props.value)}
onClick={() => handleOnClick(props.value)}
>
{label}
{props.label}
</div>
)
}

View File

@ -1,105 +0,0 @@
import classNames from 'classnames'
import { useEffect, useState } from 'react'
import { ChevronDown } from 'components/Icons'
import Overlay from 'components/Overlay'
import Option from 'components/Select/Option'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
interface Props {
options: Option[]
defaultValue?: string
onChange: (value: string) => void
isParent?: boolean
className?: string
title?: string
}
export default function Select(props: Props) {
const [value, setValue] = useState(props.defaultValue)
const selectedOption = value
? props.options.find((option) => option?.value === value || option?.denom === value)
: null
const [selected, setSelected] = useState<Option>(selectedOption)
const [showDropdown, setShowDropdown] = useToggle()
function handleChange(optionValue: string) {
setValue(optionValue)
setSelected(
props.options.find(
(option) => option?.value === optionValue || option?.denom === optionValue,
),
)
setShowDropdown(false)
props.onChange(optionValue)
}
useEffect(() => {
if (props.defaultValue && value === props.defaultValue && selected) return
setValue(props.defaultValue)
setSelected(
props.options.find(
(option) => option?.value === props.defaultValue || option?.denom === props.defaultValue,
),
)
}, [value, props.defaultValue, props.options, selected])
return (
<div
data-testid='select-component'
className={classNames(
props.isParent && 'relative',
'flex min-w-fit items-center gap-2',
props.className,
)}
role='select'
onClick={() => setShowDropdown(!showDropdown)}
>
{selected ? (
<Option {...selected} isClicked={showDropdown} isDisplay />
) : (
<div
className={classNames('flex items-center gap-2 bg-white/10 p-3', 'hover:cursor-pointer')}
>
<Text className='w-full opacity-50 hover:cursor-pointer'>Select</Text>
<span
className={classNames(
'inline-block w-2.5 transition-transform',
showDropdown ? 'rotate-0' : '-rotate-90',
)}
>
<ChevronDown />
</span>
</div>
)}
<Overlay
show={showDropdown}
className={classNames('left-0 top-[calc(100%+8px)] isolate w-full')}
setShow={setShowDropdown}
hasBackdropIsolation
>
<div className='relative isolate w-full overflow-hidden rounded-sm'>
{props.title && (
<Text size='lg' className='block bg-white/25 p-4 font-bold'>
{props.title}
</Text>
)}
{props.options.map((option: Option, index: number) => (
<Option
key={index}
{...option}
isSelected={
option?.value
? option?.value === selected?.value
: option?.denom === selected?.denom
}
onClick={handleChange}
/>
))}
</div>
</Overlay>
</div>
)
}

View File

@ -0,0 +1,124 @@
import classNames from 'classnames'
import { useEffect, useState } from 'react'
import { ChevronDown } from 'components/Icons'
import Overlay from 'components/Overlay'
import Option from 'components/Select/Option'
import Text from 'components/Text'
import useToggle from 'hooks/useToggle'
interface Props {
options: SelectOption[]
defaultValue?: string
onChange: (value: string) => void
isParent?: boolean
className?: string
title?: string
label?: string
displayClassName?: string
containerClassName?: string
}
export default function Select(props: Props) {
const [value, setValue] = useState(props.defaultValue)
const selectedOption = value
? props.options.find((option) => option?.value === value || option?.denom === value)
: undefined
const [selected, setSelected] = useState<SelectOption | undefined>(undefined)
const [showDropdown, setShowDropdown] = useToggle()
function handleChange(optionValue: string) {
setValue(optionValue)
const option = props.options.find(
(option) => option?.value === optionValue || option?.denom === optionValue,
)
if (!option) return
setSelected(option)
setShowDropdown(false)
props.onChange(optionValue)
}
useEffect(() => {
if (selectedOption && !selected) setSelected(selectedOption)
if (props.defaultValue && value === props.defaultValue) return
setValue(props.defaultValue)
const option = props.options.find(
(option) => option?.value === props.defaultValue || option?.denom === props.defaultValue,
)
if (!option) return
setSelected(option)
}, [value, props.defaultValue, props.options, selected, selectedOption])
return (
<div className={classNames('flex flex-col flex-wrap', props.containerClassName)}>
{props.label && (
<Text size='sm' className='mb-2 w-full'>
{props.label}
</Text>
)}
<div
data-testid='select-component'
className={classNames(
props.isParent && 'relative',
'flex min-w-fit items-center gap-2 bg-white/10',
props.className,
)}
role='select'
onClick={() => setShowDropdown(!showDropdown)}
>
{selected ? (
<Option
{...selected}
isClicked={showDropdown}
displayClassName={props.displayClassName}
isDisplay
/>
) : (
<div
className={classNames(
'flex items-center gap-2 bg-white/10 p-3',
'hover:cursor-pointer',
)}
>
<Text className='w-full opacity-50 hover:cursor-pointer'>Select</Text>
<span
className={classNames(
'inline-block w-2.5 transition-transform',
showDropdown ? 'rotate-0' : '-rotate-90',
)}
>
<ChevronDown />
</span>
</div>
)}
<Overlay
show={showDropdown}
className={classNames('left-0 top-[calc(100%+8px)] isolate w-full')}
setShow={setShowDropdown}
hasBackdropIsolation
>
<div className='relative isolate w-full overflow-hidden rounded-sm'>
{props.title && (
<Text size='lg' className='block bg-white/25 p-4 font-bold'>
{props.title}
</Text>
)}
{props.options.map((option: SelectOption, index: number) => (
<Option
key={index}
{...option}
isSelected={
option?.value
? option?.value === selected?.value
: option?.denom === selected?.denom
}
onClick={handleChange}
/>
))}
</div>
</Overlay>
</div>
</div>
)
}

View File

@ -1,112 +1,14 @@
import Button from 'components/Button'
import { Gear } from 'components/Icons'
import Overlay from 'components/Overlay'
import Switch from 'components/Switch'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { ASSETS } from 'constants/assets'
import { DISPLAY_CURRENCY_KEY, ENABLE_ANIMATIONS_KEY } from 'constants/localStore'
import { useAnimations } from 'hooks/useAnimations'
import useToggle from 'hooks/useToggle'
import useStore from 'store'
import { getDisplayCurrencies } from 'utils/assets'
export default function Settings() {
useAnimations()
const [showMenu, setShowMenu] = useToggle()
const enableAnimations = useStore((s) => s.enableAnimations)
const displayCurrency = useStore((s) => s.displayCurrency)
const displayCurrencies = getDisplayCurrencies()
const storageDisplayCurrency = localStorage.getItem(DISPLAY_CURRENCY_KEY)
if (storageDisplayCurrency) {
const storedDisplayCurrency = ASSETS.find(
(asset) => asset.symbol === JSON.parse(storageDisplayCurrency).symbol,
)
if (storedDisplayCurrency && storedDisplayCurrency !== displayCurrency) {
setDisplayCurrency(storedDisplayCurrency)
}
}
function handleReduceMotion() {
useStore.setState({ enableAnimations: !enableAnimations })
if (typeof window !== 'undefined')
window.localStorage.setItem(ENABLE_ANIMATIONS_KEY, enableAnimations ? 'false' : 'true')
}
function handleCurrencyChange(e: React.ChangeEvent<HTMLSelectElement>) {
const displayCurrency = displayCurrencies.find((c) => c.symbol === e.target.value)
if (!displayCurrency) return
setDisplayCurrency(displayCurrency)
}
function setDisplayCurrency(displayCurrency: Asset) {
useStore.setState({ displayCurrency: displayCurrency })
localStorage.setItem(DISPLAY_CURRENCY_KEY, JSON.stringify(displayCurrency))
}
return (
<div className='relative'>
<Button
variant='solid'
color='tertiary'
leftIcon={<Gear />}
onClick={() => setShowMenu(!showMenu)}
hasFocus={showMenu}
/>
<Overlay className='right-0 mt-2 w-[240px]' show={showMenu} setShow={setShowMenu}>
<div className='flex w-full flex-wrap p-4'>
<Text size='sm' uppercase={true} className='w-full pb-4'>
Settings
</Text>
<div className='flex w-full'>
<div className='flex flex-1'>
<Text size='sm' className='mr-2'>
Reduce Motion
</Text>
<Tooltip
type='info'
interactive
content={
<Text size='sm'>
Turns off all animations inside the dApp. Turning animations off can increase
the overall performance on lower-end hardware.
</Text>
}
/>
</div>
<Switch name='reduceMotion' checked={!enableAnimations} onChange={handleReduceMotion} />
</div>
<div className='mt-4 flex w-full flex-col'>
<div className='flex'>
<Text size='sm' className='mr-2'>
Display Currency
</Text>
<Tooltip
type='info'
content={
<Text size='sm'>
Sets the denomination of values to a different currency. While OSMO is the
currency the TWAP oracles return. All other values are fetched from liquidity
pools.
</Text>
}
/>
</div>
<select
value={displayCurrency.symbol}
onChange={handleCurrencyChange}
className='mt-2 w-full rounded-sm border border-white/20 bg-transparent p-1 text-sm'
>
{displayCurrencies.map((currency) => (
<option key={currency.denom}>{currency.symbol}</option>
))}
</select>
</div>
</div>
</Overlay>
</div>
<Button
variant='solid'
color='tertiary'
leftIcon={<Gear />}
onClick={() => useStore.setState({ settingsModal: true })}
/>
)
}

View File

@ -195,7 +195,7 @@ function Track(props: TrackProps) {
}
return (
<div className='relative h-1 flex-grow overflow-hidden rounded-sm bg-transparent'>
<div className='relative h-1 flex-1 overflow-hidden rounded-sm bg-transparent'>
<div className='absolute z-1 h-3 bg-martian-red ' style={{ width: `${percentage}%` }} />
<div className='absolute h-3 w-full bg-white/20' />
</div>

View File

@ -3,7 +3,7 @@ import classNames from 'classnames'
interface Props {
name: string
checked: boolean
onChange: () => void
onChange: (value: boolean) => void
className?: string
disabled?: boolean
}
@ -23,7 +23,7 @@ export default function Switch(props: Props) {
name={props.name}
className={classNames('peer hidden')}
checked={props.checked}
onChange={props.onChange}
onChange={() => props.onChange(!props.checked)}
/>
<label
htmlFor={props.name}

View File

@ -1,15 +1,16 @@
import classNames from 'classnames'
import { toast as createToast, Slide, ToastContainer } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
import { mutate } from 'swr'
import Button from 'components/Button'
import { CheckCircled, Cross, CrossCircled } from 'components/Icons'
import Text from 'components/Text'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import useStore from 'store'
export default function Toaster() {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
const toast = useStore((s) => s.toast)
if (toast) {
@ -66,7 +67,7 @@ export default function Toaster() {
position='top-right'
newestOnTop
closeOnClick
transition={enableAnimations ? Slide : undefined}
transition={reduceMotion ? undefined : Slide}
className='p-0'
toastClassName='top-[73px] z-20 m-0 mb-4 flex w-full bg-transparent p-0'
bodyClassName='p-0 m-0 w-full flex'

View File

@ -1,20 +1,19 @@
import BigNumber from 'bignumber.js'
import classNames from 'classnames'
import Image from 'next/image'
import AssetImage from 'components/AssetImage'
import Button from 'components/Button'
import DisplayCurrency from 'components/DisplayCurrency'
import { FormattedNumber } from 'components/FormattedNumber'
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
import NumberInput from 'components/NumberInput'
import Select from 'components/Select/Select'
import Select from 'components/Select'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import { ASSETS } from 'constants/assets'
import useStore from 'store'
import { BN } from 'utils/helpers'
import { FormattedNumber } from 'components/FormattedNumber'
import Button from 'components/Button'
import { ExclamationMarkTriangle, TrashBin } from 'components/Icons'
import { Tooltip } from 'components/Tooltip'
import AssetImage from 'components/AssetImage'
import { BNCoin } from 'types/classes/BNCoin'
import { BN } from 'utils/helpers'
interface Props {
amount: BigNumber
@ -65,7 +64,8 @@ export default function TokenInput(props: Props) {
defaultValue={props.asset.denom}
onChange={onChangeAsset}
title={props.accountId ? `Account ${props.accountId}` : 'Your Wallet'}
className='border-r border-white/20 bg-white/5'
className='h-full border-r border-white/20 bg-white/5'
displayClassName='rounded-l-sm'
/>
) : (
<div className='flex min-w-fit items-center gap-2 border-r border-white/20 bg-white/5 p-3'>
@ -80,7 +80,7 @@ export default function TokenInput(props: Props) {
onChange={props.onChange}
amount={props.amount}
max={props.max}
className='border-none p-3'
className='flex-1 border-none p-3'
/>
{props.onDelete && (
<div role='button' className='grid items-center pr-2' onClick={props.onDelete}>
@ -109,8 +109,9 @@ export default function TokenInput(props: Props) {
</Text>
<FormattedNumber
className='mr-1 text-xs text-white/50'
amount={props.max}
amount={props.max.toNumber()}
options={{ decimals: props.asset.decimals }}
animate
/>
<Button
dataTestId='token-input-max-button'

View File

@ -24,7 +24,7 @@ export default function TokenInputWithSlider(props: Props) {
const [percentage, setPercentage] = useState(0)
function onChangeSlider(percentage: number) {
const newAmount = BN(percentage).div(100).times(props.max).integerValue()
const newAmount = BN(percentage).dividedBy(100).multipliedBy(props.max).integerValue()
setPercentage(percentage)
setAmount(newAmount)
props.onChange(newAmount)
@ -32,7 +32,7 @@ export default function TokenInputWithSlider(props: Props) {
function onChangeAmount(newAmount: BigNumber) {
setAmount(newAmount)
setPercentage(BN(newAmount).div(props.max).times(100).toNumber())
setPercentage(BN(newAmount).dividedBy(props.max).multipliedBy(100).toNumber())
props.onChange(newAmount)
}

View File

@ -3,8 +3,10 @@ import classNames from 'classnames'
import { ReactNode } from 'react'
import { Questionmark } from 'components/Icons'
import useStore from 'store'
import TooltipContent from 'components/Tooltip/TooltipContent'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
interface Props {
content: ReactNode | string
@ -19,7 +21,7 @@ interface Props {
export type TooltipType = 'info' | 'warning' | 'error'
export const Tooltip = (props: Props) => {
const enableAnimations = useStore((s) => s.enableAnimations)
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
return (
<Tippy
@ -34,7 +36,7 @@ export const Tooltip = (props: Props) => {
className={classNames(
props.underline &&
'border-b-1 cursor-pointer border border-x-0 border-t-0 border-dashed border-white/50 hover:border-transparent',
enableAnimations && 'transition-all',
!reduceMotion && 'transition-all',
props.className,
)}
>

View File

@ -14,6 +14,7 @@ export default function AssetButton(props: Props) {
color='tertiary'
variant='transparent'
className='w-full border border-white/20'
textClassNames='flex flex-1'
size='md'
hasSubmenu
{...props}

View File

@ -1,10 +1,10 @@
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useState } from 'react'
import { SwapIcon } from 'components/Icons'
import Text from 'components/Text'
import { ASSETS } from 'constants/assets'
import AssetButton from 'components/Trade/TradeModule/AssetSelector/AssetButton'
import AssetOverlay, { OverlayState } from 'components/Trade/TradeModule/AssetSelector/AssetOverlay'
import { ASSETS } from 'constants/assets'
export default function AssetSelector() {
const [overlayState, setOverlayState] = useState<OverlayState>('closed')
@ -38,11 +38,6 @@ export default function AssetSelector() {
[setOverlayState],
)
const buyAssets = useMemo(
() => ASSETS.filter((asset) => asset.denom !== sellAsset.denom),
[sellAsset],
)
return (
<div className='grid-rows-auto relative grid grid-cols-[1fr_min-content_1fr] gap-y-2 bg-white/5 p-3'>
<Text size='sm'>Buy</Text>

View File

@ -1,29 +1,9 @@
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import classNames from 'classnames'
import { useState } from 'react'
import Loading from 'components/Loading'
import Text from 'components/Text'
import Divider from 'components/Divider'
import RangeInput from 'components/RangeInput'
import AssetSelector from 'components/Trade/TradeModule/AssetSelector/AssetSelector'
function Content() {
const params = useParams()
const address = params.address
const currentAccount = params.accountId
const hasAccount = !isNaN(Number(currentAccount))
if (!address) return <Text size='sm'>You need to be connected to trade</Text>
if (!hasAccount) return <Text size='sm'>Select an Account to trade</Text>
return <Text size='sm'>{`Trade with Account ${currentAccount}`}</Text>
}
function Fallback() {
return <Loading className='h-4 w-50' />
}
import AssetSelector from 'components/Trade/TradeModule/AssetSelector'
export default function TradeModule() {
const [value, setValue] = useState(0)
@ -38,10 +18,13 @@ export default function TradeModule() {
>
<AssetSelector />
<Divider />
<div className='p-4'>
<RangeInput max={4000} marginThreshold={2222} value={value} onChange={setValue} />
</div>
<RangeInput
max={4000}
marginThreshold={2222}
value={value}
onChange={setValue}
wrapperClassName='p-4'
/>
</div>
)
}

View File

@ -17,10 +17,10 @@ import Overlay from 'components/Overlay'
import Text from 'components/Text'
import { IS_TESTNET } from 'constants/env'
import useToggle from 'hooks/useToggle'
import useWalletBalances from 'hooks/useWalletBalances'
import useStore from 'store'
import { getBaseAsset, getEnabledMarketAssets } from 'utils/assets'
import { formatValue, truncate } from 'utils/formatters'
import useWalletBalances from 'hooks/useWalletBalances'
import { BN } from 'utils/helpers'
export default function ConnectedButton() {
@ -64,7 +64,7 @@ export default function ConnectedButton() {
if (!walletBalances || walletBalances.length === 0) return
const newAmount = BigNumber(
walletBalances?.find((coin: Coin) => coin.denom === baseAsset.denom)?.amount ?? 0,
).div(10 ** baseAsset.decimals)
).dividedBy(10 ** baseAsset.decimals)
if (walletAmount.isEqualTo(newAmount)) return
setWalletAmount(newAmount)
@ -120,7 +120,7 @@ export default function ConnectedButton() {
<FormattedNumber
animate
className='flex items-end text-2xl '
amount={walletAmount}
amount={walletAmount.toNumber()}
/>
</div>
</div>

View File

@ -32,7 +32,7 @@ export const WalletConnectProvider: FC<Props> = ({ children }) => {
'relative z-50 w-[460px] max-w-full rounded-base border border-white/20 bg-white/5 p-6 pb-4 backdrop-blur-3xl flex flex-wrap focus-visible:outline-none',
modalOverlay:
'fixed inset-0 bg-black/60 w-full h-full z-40 flex items-center justify-center cursor-pointer m-0 backdrop-blur-sm',
modalHeader: 'text-lg text-white mb-4 flex-grow',
modalHeader: 'text-lg text-white mb-4 flex-1',
modalCloseButton: 'inline-block',
walletList: 'w-full',
wallet:

View File

@ -0,0 +1,9 @@
import { ASSETS } from 'constants/assets'
export const DEFAULT_SETTINGS: Settings = {
reduceMotion: false,
lendAssets: false,
preferredAsset: ASSETS[0].denom,
displayCurrency: ASSETS[0].denom,
slippage: 0.02,
}

View File

@ -1,4 +1,7 @@
export const PREFERRED_ASSET_KEY = 'favouriteAsset'
export const DISPLAY_CURRENCY_KEY = 'displayCurrency'
export const ENABLE_ANIMATIONS_KEY = 'enableAnimations'
export const REDUCE_MOTION_KEY = 'reduceMotion'
export const FAVORITE_ASSETS = 'favoriteAssets'
export const LEND_ASSETS_KEY = 'lendAssets'
export const AUTO_LEND_ENABLED_ACCOUNT_IDS_KEY = 'autoLendEnabledAccountIds'
export const SLIPPAGE_KEY = 'slippage'

View File

@ -1,18 +1,20 @@
import debounce from 'debounce-promise'
import { useMemo, useState } from 'react'
import { hardcodedFee } from 'utils/constants'
import getMinLpToReceive from 'api/vaults/getMinLpToReceive'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { SLIPPAGE_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import useStore from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { Action } from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { hardcodedFee } from 'utils/constants'
import { BN } from 'utils/helpers'
import {
getEnterVaultActions,
getVaultDepositCoinsAndValue,
getVaultSwapActions,
} from 'utils/vaults'
import { BN } from 'utils/helpers'
interface Props {
vault: Vault
@ -22,7 +24,7 @@ interface Props {
export default function useDepositVault(props: Props): { actions: Action[]; fee: StdFee } {
const [minLpToReceive, setMinLpToReceive] = useState<BigNumber>(BN(0))
const { data: prices } = usePrices()
const slippage = useStore((s) => s.slippage)
const [slippage] = useLocalStorage<number>(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
const borrowings: BNCoin[] = useMemo(
() => props.borrowings.filter((borrowing) => borrowing.amount.gt(0)),
@ -60,7 +62,7 @@ export default function useDepositVault(props: Props): { actions: Action[]; fee:
slippage,
)
if (!lpAmount || lpAmount.eq(minLpToReceive)) return
if (!lpAmount || lpAmount.isEqualTo(minLpToReceive)) return
setMinLpToReceive(lpAmount)
}, [
primaryCoin,

View File

@ -1,33 +0,0 @@
import { useCallback, useEffect } from 'react'
import { ENABLE_ANIMATIONS_KEY } from 'constants/localStore'
import useStore from 'store'
export const useAnimations = () => {
const enableAnimations = useStore((s) => s.enableAnimations)
const enableAnimationsLocalStorage =
typeof window !== 'undefined' ? window.localStorage.getItem(ENABLE_ANIMATIONS_KEY) : false
const queryChangeHandler = useCallback(
(event: MediaQueryListEvent) => {
if (enableAnimationsLocalStorage) return
useStore.setState({ enableAnimations: !event?.matches ?? true })
},
[enableAnimationsLocalStorage],
)
useEffect(() => {
const mediaQuery: MediaQueryList = window.matchMedia('(prefers-reduced-motion: reduce)')
const storeSetting = enableAnimationsLocalStorage === 'true'
useStore.setState({ enableAnimations: storeSetting })
if (mediaQuery) {
if (enableAnimationsLocalStorage) return
useStore.setState({ enableAnimations: !mediaQuery.matches })
mediaQuery.addEventListener('change', queryChangeHandler)
return () => mediaQuery.removeEventListener('change', queryChangeHandler)
}
}, [enableAnimations, enableAnimationsLocalStorage, queryChangeHandler])
return enableAnimations
}

View File

@ -0,0 +1,51 @@
import { useMemo } from 'react'
import useDepositEnabledMarkets from 'hooks/useDepositEnabledMarkets'
import useMarketBorrowings from 'hooks/useMarketBorrowings'
import useMarketDeposits from 'hooks/useMarketDeposits'
import useMarketLiquidities from 'hooks/useMarketLiquidities'
import { byDenom } from 'utils/array'
import { getAssetByDenom } from 'utils/assets'
import { BN } from 'utils/helpers'
import useCurrentAccountDebts from './useCurrentAccountDebts'
export default function useBorrowMarketAssetsTableData(): {
accountBorrowedAssets: BorrowMarketTableData[]
availableAssets: BorrowMarketTableData[]
} {
const markets = useDepositEnabledMarkets()
const accountDebts = useCurrentAccountDebts()
const { data: borrowData } = useMarketBorrowings()
const { data: marketDeposits } = useMarketDeposits()
const { data: marketLiquidities } = useMarketLiquidities()
return useMemo(() => {
const accountBorrowedAssets: BorrowMarketTableData[] = [],
availableAssets: BorrowMarketTableData[] = []
markets.forEach(({ denom, liquidityRate, liquidationThreshold, maxLtv }) => {
const asset = getAssetByDenom(denom) as Asset
const borrow = borrowData.find((borrow) => borrow.denom === denom)
const marketDepositAmount = BN(marketDeposits.find(byDenom(denom))?.amount ?? 0)
const marketLiquidityAmount = BN(marketLiquidities.find(byDenom(denom))?.amount ?? 0)
const debt = accountDebts?.find((debt) => debt.denom === denom)
if (!borrow) return
const borrowMarketAsset: BorrowMarketTableData = {
...borrow,
asset,
debt: debt?.amount,
marketDepositAmount,
marketLiquidityAmount,
marketLiquidityRate: liquidityRate,
marketLiquidationThreshold: liquidationThreshold,
marketMaxLtv: maxLtv,
}
;(borrowMarketAsset.debt ? accountBorrowedAssets : availableAssets).push(borrowMarketAsset)
})
return { accountBorrowedAssets, availableAssets }
}, [accountDebts, borrowData, markets, marketDeposits, marketLiquidities])
}

View File

@ -0,0 +1,6 @@
import useCurrentAccount from 'hooks/useCurrentAccount'
export default function useCurrentAccountDebts() {
const account = useCurrentAccount()
return account?.debts ?? []
}

View File

@ -1,21 +1,35 @@
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import useStore from 'store'
import { BN } from 'utils/helpers'
import { byDenom } from 'utils/array'
import { ASSETS } from 'constants/assets'
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { DISPLAY_CURRENCY_KEY } from 'constants/localStore'
import useLocalStorage from 'hooks/useLocalStorage'
import usePrices from 'hooks/usePrices'
import { byDenom } from 'utils/array'
import { getDisplayCurrencies } from 'utils/assets'
import { BN } from 'utils/helpers'
function useDisplayCurrencyPrice() {
const { data: prices } = usePrices()
const displayCurrency = useStore((s) => s.displayCurrency)
const displayCurrencies = getDisplayCurrencies()
const [displayCurrency] = useLocalStorage<string>(
DISPLAY_CURRENCY_KEY,
DEFAULT_SETTINGS.displayCurrency,
)
const displayCurrencyAsset = useMemo(
() =>
displayCurrencies.find((asset) => asset.denom === displayCurrency) ?? displayCurrencies[0],
[displayCurrency, displayCurrencies],
)
const getConversionRate = useCallback(
(denom: string) => {
const assetPrice = prices.find(byDenom(denom))
const displayCurrencyPrice = prices.find(byDenom(displayCurrency.denom))
const displayCurrencyPrice = prices.find(byDenom(displayCurrency))
if (assetPrice && displayCurrencyPrice) {
return BN(assetPrice.amount).div(displayCurrencyPrice.amount)
return BN(assetPrice.amount).dividedBy(displayCurrencyPrice.amount)
} else {
throw 'Given denom or display currency price has not found'
}
@ -32,7 +46,7 @@ function useDisplayCurrencyPrice() {
return {
getConversionRate,
convertAmount,
symbol: displayCurrency.symbol,
symbol: displayCurrencyAsset?.symbol,
}
}

View File

@ -1,11 +1,13 @@
import { AvailableBorrowings } from 'components/Borrow/Borrowings'
import { ActiveBorrowings } from 'components/Borrow/Borrowings'
import BorrowTable from 'components/Borrow/BorrowTable'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
export default function BorrowPage() {
const { accountBorrowedAssets, availableAssets } = useBorrowMarketAssetsTableData()
return (
<>
<ActiveBorrowings />
<AvailableBorrowings />
<BorrowTable data={accountBorrowedAssets} title='Borrowed Assets' />
<BorrowTable data={availableAssets} title='Available to borrow' />
</>
)
}

View File

@ -1,5 +1,5 @@
import { AvailableVaults, DepositedVaults } from 'components/Earn/Farm/Vaults'
import Tab from 'components/Earn/Tab'
import { AvailableVaults, DepositedVaults } from 'components/Earn/vault/Vaults'
export default function FarmPage() {
return (

View File

@ -1,4 +1,4 @@
import AccountOverview from 'components/Portfolio/AccountOverview'
import AccountOverview from 'components/Account/AccountOverview'
export default function PortfolioPage() {
return <AccountOverview />

View File

@ -4,7 +4,7 @@ export default function Document() {
return (
<Html className='m-0 p-0' lang='en'>
<Head />
<body className='m-0 cursor-default bg-body p-0 font-sans text-white'>
<body className='m-0 cursor-default bg-body p-0 font-sans text-white scrollbar-hide'>
<Main />
<NextScript />
</body>

Some files were not shown because too many files have changed in this diff Show More