Merge branch 'develop' of https://github.com/mars-protocol/mars-v2-frontend into develop
This commit is contained in:
commit
1b8ff00e91
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mars-v2-frontend",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "yarn validate-env && next build",
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { cacheFn, pythPriceCache } from 'api/cache'
|
||||
import { ENV } from 'constants/env'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
import { cacheFn, pythPriceCache } from '../cache'
|
||||
|
||||
export default async function fetchPythPrices(...priceFeedIds: string[]) {
|
||||
try {
|
||||
const pricesUrl = new URL(`${ENV.PYTH_ENDPOINT}/latest_price_feeds`)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HealthGauge } from 'components/HealthGauge'
|
||||
import { HealthGauge } from 'components/Account/Health/HealthGauge'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
|
||||
@ -7,7 +7,7 @@ export default function Skeleton() {
|
||||
<div className='absolute flex items-start w-16 gap-4 right-4 top-6 opacity-90'>
|
||||
<div className='relative flex flex-wrap w-16 border min-w-16 group rounded-base border-white/20'>
|
||||
<div className='flex flex-wrap justify-center w-full py-4'>
|
||||
<HealthGauge health={0} />
|
||||
<HealthGauge health={0} healthFactor={0} />
|
||||
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
||||
Health
|
||||
</Text>
|
||||
|
@ -5,12 +5,12 @@ import AccountBalancesTable from 'components/Account/AccountBalancesTable'
|
||||
import AccountComposition from 'components/Account/AccountComposition'
|
||||
import AccountDetailsLeverage from 'components/Account/AccountDetails/AccountDetailsLeverage'
|
||||
import Skeleton from 'components/Account/AccountDetails/Skeleton'
|
||||
import { HealthGauge } from 'components/Account/Health/HealthGauge'
|
||||
import EscButton from 'components/Button/EscButton'
|
||||
import { glowElement } from 'components/Button/utils'
|
||||
import Card from 'components/Card'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import { HealthGauge } from 'components/HealthGauge'
|
||||
import { ThreeDots } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
@ -62,8 +62,10 @@ function AccountDetails(props: Props) {
|
||||
)
|
||||
const updatedAccount = useStore((s) => s.updatedAccount)
|
||||
const accountDetailsExpanded = useStore((s) => s.accountDetailsExpanded)
|
||||
const { health } = useHealthComputer(account)
|
||||
const { health: updatedHealth } = useHealthComputer(updatedAccount || account)
|
||||
const { health, healthFactor } = useHealthComputer(account)
|
||||
const { health: updatedHealth, healthFactor: updatedHealthFactor } = useHealthComputer(
|
||||
updatedAccount || account,
|
||||
)
|
||||
const { data: prices } = usePrices()
|
||||
const accountBalanceValue = useMemo(
|
||||
() => calculateAccountBalanceValue(updatedAccount ?? account, prices),
|
||||
@ -128,7 +130,12 @@ function AccountDetails(props: Props) {
|
||||
onClick={() => useStore.setState({ accountDetailsExpanded: !accountDetailsExpanded })}
|
||||
>
|
||||
<div className='flex flex-wrap justify-center w-full py-4'>
|
||||
<HealthGauge health={health} updatedHealth={updatedHealth} />
|
||||
<HealthGauge
|
||||
health={health}
|
||||
updatedHealth={updatedHealth}
|
||||
healthFactor={healthFactor}
|
||||
updatedHealthFactor={updatedHealthFactor}
|
||||
/>
|
||||
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
||||
Health
|
||||
</Text>
|
||||
|
@ -27,7 +27,7 @@ export default function AccountStats(props: Props) {
|
||||
() => (!account ? null : calculateAccountBalanceValue(account, prices)),
|
||||
[account, prices],
|
||||
)
|
||||
const { health } = useHealthComputer(account)
|
||||
const { health, healthFactor } = useHealthComputer(account)
|
||||
const { data } = useBorrowMarketAssetsTableData(false)
|
||||
const borrowAssetsData = useMemo(() => data?.allAssets || [], [data])
|
||||
const { availableAssets: lendingAvailableAssets, accountLentAssets } =
|
||||
@ -49,7 +49,12 @@ export default function AccountStats(props: Props) {
|
||||
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
<Skeleton health={!account ? 0 : health} positionBalance={positionBalance} apr={apr} />
|
||||
<Skeleton
|
||||
health={health ?? 0}
|
||||
healthFactor={healthFactor ?? 0}
|
||||
positionBalance={positionBalance}
|
||||
apr={apr}
|
||||
/>
|
||||
{isActive && setShowMenu && (
|
||||
<div className='grid grid-flow-row grid-cols-2 gap-4 pt-4'>
|
||||
<Button
|
||||
|
@ -1,7 +1,8 @@
|
||||
import HealthBar from 'components/Account/HealthBar'
|
||||
import HealthBar from 'components/Account/Health/HealthBar'
|
||||
import HealthIcon from 'components/Account/Health/HealthIcon'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
import { ArrowChartLineUp, Heart } from 'components/Icons'
|
||||
import { ArrowChartLineUp } from 'components/Icons'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
import { ORACLE_DENOM } from 'constants/oracle'
|
||||
@ -9,12 +10,13 @@ import { BNCoin } from 'types/classes/BNCoin'
|
||||
|
||||
interface Props {
|
||||
health: number
|
||||
healthFactor: number
|
||||
positionBalance: BigNumber | null
|
||||
apr: BigNumber | null
|
||||
}
|
||||
|
||||
export default function Skeleton(props: Props) {
|
||||
const { positionBalance, apr, health } = props
|
||||
const { positionBalance, apr, health, healthFactor } = props
|
||||
return (
|
||||
<div className='flex flex-wrap w-full'>
|
||||
{positionBalance ? (
|
||||
@ -40,11 +42,16 @@ export default function Skeleton(props: Props) {
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Heart className='w-4' />
|
||||
<HealthIcon isLoading={props.healthFactor === 0} health={props.health} className='w-4' />
|
||||
<Text size='xs' className='w-auto mr-1 text-white/70'>
|
||||
Health
|
||||
</Text>
|
||||
<HealthBar health={health} className='w-[92px] h-0.5' hasLabel />
|
||||
<HealthBar
|
||||
health={health}
|
||||
healthFactor={healthFactor}
|
||||
className='w-[92px] h-0.5'
|
||||
hasLabel
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { HTMLAttributes, useCallback, useMemo } from 'react'
|
||||
import Accordion from 'components/Accordion'
|
||||
import AccountBalancesTable from 'components/Account/AccountBalancesTable'
|
||||
import AccountComposition from 'components/Account/AccountComposition'
|
||||
import HealthBar from 'components/Account/HealthBar'
|
||||
import HealthBar from 'components/Account/Health/HealthBar'
|
||||
import Card from 'components/Card'
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
import { FormattedNumber } from 'components/FormattedNumber'
|
||||
@ -50,8 +50,9 @@ export default function AccountSummary(props: Props) {
|
||||
() => [...lendingAvailableAssets, ...accountLentAssets],
|
||||
[lendingAvailableAssets, accountLentAssets],
|
||||
)
|
||||
const { health } = useHealthComputer(props.account)
|
||||
const { health: updatedHealth } = useHealthComputer(updatedAccount)
|
||||
const { health, healthFactor } = useHealthComputer(props.account)
|
||||
const { health: updatedHealth, healthFactor: updatedHealthFactor } =
|
||||
useHealthComputer(updatedAccount)
|
||||
const leverage = useMemo(
|
||||
() => (props.account ? calculateAccountLeverage(props.account, prices) : BN_ZERO),
|
||||
[props.account, prices],
|
||||
@ -109,7 +110,13 @@ export default function AccountSummary(props: Props) {
|
||||
)}
|
||||
</Item>
|
||||
<Item label='Account health'>
|
||||
<HealthBar health={health} updatedHealth={updatedHealth} className='h-1' />
|
||||
<HealthBar
|
||||
health={health}
|
||||
healthFactor={healthFactor}
|
||||
updatedHealth={updatedHealth}
|
||||
updatedHealthFactor={updatedHealthFactor}
|
||||
className='h-1'
|
||||
/>
|
||||
</Item>
|
||||
</Card>
|
||||
<Accordion
|
||||
|
@ -1,16 +1,18 @@
|
||||
import classNames from 'classnames'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import HealthTooltip from 'components/Account/Health/HealthTooltip'
|
||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
import { LocalStorageKeys } from 'constants/localStorageKeys'
|
||||
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
||||
import useHealthColor from 'hooks/useHealthColor'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import { getHealthIndicatorColors } from 'utils/healthIndicator'
|
||||
|
||||
interface Props {
|
||||
health: number
|
||||
healthFactor: number
|
||||
updatedHealth?: number
|
||||
updatedHealthFactor?: number
|
||||
hasLabel?: boolean
|
||||
className?: string
|
||||
}
|
||||
@ -28,8 +30,13 @@ function calculateHealth(health: number): number {
|
||||
return ((thirdBarEnd - thirdBarStart) / 70) * (health - 30) + thirdBarStart
|
||||
}
|
||||
|
||||
export default function HealthBar(props: Props) {
|
||||
const { health, updatedHealth } = props
|
||||
export default function HealthBar({
|
||||
health = 0,
|
||||
updatedHealth = 0,
|
||||
healthFactor = 0,
|
||||
updatedHealthFactor = 0,
|
||||
className,
|
||||
}: Props) {
|
||||
const [reduceMotion] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.REDUCE_MOTION,
|
||||
DEFAULT_SETTINGS.reduceMotion,
|
||||
@ -38,20 +45,19 @@ export default function HealthBar(props: Props) {
|
||||
const updatedWidth = calculateHealth(updatedHealth ?? 0)
|
||||
const isUpdated = updatedWidth > 0 && updatedWidth !== width
|
||||
const isIncrease = isUpdated && updatedWidth > width
|
||||
const [color, label] = useHealthColorAndLabel(health, 'fill')
|
||||
const [updatedColor, updatedLabel] = useHealthColorAndLabel(updatedHealth ?? 0, 'fill')
|
||||
const color = useHealthColor(health, 'fill')
|
||||
const updatedColor = useHealthColor(updatedHealth ?? 0, 'fill')
|
||||
const [backgroundColor, foreGroundColor] = useMemo(
|
||||
() => getHealthIndicatorColors(color, updatedColor, 'fill', isUpdated, isIncrease),
|
||||
[color, updatedColor, isUpdated, isIncrease],
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={isUpdated ? updatedLabel : label}
|
||||
type='info'
|
||||
className='flex items-center w-full'
|
||||
<HealthTooltip
|
||||
health={isUpdated ? updatedHealth : health}
|
||||
healthFactor={isUpdated ? updatedHealthFactor : healthFactor}
|
||||
>
|
||||
<div className={classNames('flex w-full', props.className)}>
|
||||
<div className={classNames('flex w-full', className)}>
|
||||
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 184 4'>
|
||||
<mask id='healthBarMask'>
|
||||
<path fill='#FFFFFF' d='M0,2c0-1.1,0.9-2,2-2h41.6v4H2C0.9,4,0,3.1,0,2z' />
|
||||
@ -119,6 +125,6 @@ export default function HealthBar(props: Props) {
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</HealthTooltip>
|
||||
)
|
||||
}
|
289
src/components/Account/Health/HealthGauge.tsx
Normal file
289
src/components/Account/Health/HealthGauge.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import classNames from 'classnames'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import HealthIcon from 'components/Account/Health/HealthIcon'
|
||||
import HealthTooltip from 'components/Account/Health/HealthTooltip'
|
||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
import { LocalStorageKeys } from 'constants/localStorageKeys'
|
||||
import useHealthColorAndLabel from 'hooks/useHealthColor'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import { computeHealthGaugePercentage } from 'utils/accounts'
|
||||
import { getHealthIndicatorColors } from 'utils/healthIndicator'
|
||||
|
||||
interface Props {
|
||||
diameter?: number
|
||||
health: number
|
||||
healthFactor: number
|
||||
updatedHealthFactor?: number
|
||||
updatedHealth?: number
|
||||
}
|
||||
|
||||
const RADIUS = 350
|
||||
const ROTATION = {
|
||||
rotate: '-126deg',
|
||||
transformOrigin: 'center',
|
||||
}
|
||||
|
||||
export const HealthGauge = ({
|
||||
diameter = 40,
|
||||
health = 0,
|
||||
updatedHealth = 0,
|
||||
healthFactor = 0,
|
||||
updatedHealthFactor = 0,
|
||||
}: Props) => {
|
||||
const color = useHealthColorAndLabel(health, 'text')
|
||||
const updatedColor = useHealthColorAndLabel(updatedHealth ?? 0, 'text')
|
||||
const [reduceMotion] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.REDUCE_MOTION,
|
||||
DEFAULT_SETTINGS.reduceMotion,
|
||||
)
|
||||
const percentage = useMemo(() => computeHealthGaugePercentage(health), [health])
|
||||
const updatedPercentage = useMemo(
|
||||
() => computeHealthGaugePercentage(updatedHealth),
|
||||
[updatedHealth],
|
||||
)
|
||||
const isUpdated = updatedHealthFactor !== 0 && updatedPercentage !== percentage
|
||||
const isIncrease = isUpdated && updatedPercentage < percentage
|
||||
const [backgroundColor, foreGroundColor] = useMemo(
|
||||
() => getHealthIndicatorColors(color, updatedColor, 'text', isUpdated, isIncrease),
|
||||
[color, updatedColor, isUpdated, isIncrease],
|
||||
)
|
||||
|
||||
const currentHealth = useMemo(
|
||||
() => (isUpdated ? updatedHealth : health),
|
||||
[isUpdated, updatedHealth, health],
|
||||
)
|
||||
|
||||
return (
|
||||
<HealthTooltip
|
||||
health={currentHealth}
|
||||
healthFactor={isUpdated ? updatedHealthFactor : healthFactor}
|
||||
>
|
||||
<div className='flex justify-center w-full'>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative grid place-items-center',
|
||||
`w-${diameter / 4} h-${diameter / 4}`,
|
||||
)}
|
||||
>
|
||||
<HealthIcon
|
||||
isLoading={healthFactor === 0}
|
||||
health={currentHealth}
|
||||
className='w-5'
|
||||
colorClass='text-white/60'
|
||||
/>
|
||||
<svg
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
x='0px'
|
||||
y='0px'
|
||||
viewBox='0 0 700 700'
|
||||
className='absolute'
|
||||
>
|
||||
<mask id='mask'>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M585.2,594.5C577.4,594.5 569.5,591.7 563.3,586.1C549.9,574 548.8,553.3 560.9,539.8C601.3,494.9 626,439.5 632.3,379.4C638.6,319.3 626,259.9 595.8,207.7C586.8,192 592.1,172 607.8,162.9C623.5,153.8 643.5,159.2 652.6,174.9C689.8,239.2 705.3,312.3 697.5,386.3C689.7,460.2 659.3,528.5 609.6,583.7C603,590.9 594.1,594.5 585.2,594.5Z'
|
||||
/>
|
||||
</g>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M561.7,147C553.9,147 546,144.2 539.8,138.6C494.9,98.2 439.5,73.5 379.4,67.2C319.4,60.9 259.9,73.5 207.6,103.7C191.9,112.8 171.9,107.4 162.8,91.7C153.7,76 159.1,56 174.8,46.9C239.2,9.7 312.3,-5.8 386.2,2C460.1,9.8 528.4,40.2 583.6,89.9C597,102 598.1,122.7 586,136.2C579.6,143.3 570.7,147 561.7,147Z'
|
||||
/>
|
||||
</g>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M349.6,699.3C328.4,699.3 307.1,697.4 285.9,693.5C204.1,678.3 130.2,634.4 77.9,569.8C25.6,505.2 -2.1,423.8 0.1,340.6C2.3,257.4 34.1,177.6 89.8,115.8C101.9,102.4 122.7,101.3 136.1,113.4C149.5,125.5 150.6,146.2 138.5,159.7C93.3,209.9 67.4,274.8 65.6,342.4C63.8,410 86.3,476.1 128.8,528.6C171.3,581.1 231.4,616.8 297.8,629.2C364.2,641.5 433.1,629.7 491.6,595.9C507.3,586.8 527.3,592.2 536.4,607.9C545.4,623.6 540.1,643.6 524.4,652.7C471,683.4 410.6,699.3 349.6,699.3Z'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<mask id='backgroundMask'>
|
||||
<g>
|
||||
<path
|
||||
fill='white'
|
||||
d='M137.1,538.3c-5.8-6.5-11.3-13.3-16.5-20.4l-57.8,31.9c7.4,10.5,15.3,20.7,23.8,30.3L137.1,538.3z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M179.4,577.1c-8.1-6-15.8-12.4-23.1-19.2L105.5,600c10.3,10,21.3,19.5,32.9,28.3L179.4,577.1z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M79.5,437.5c-5.6-17.2-24.1-26.6-41.3-21.1c-7.9,2.6-14.3,8-18.2,14.8c-4.5,7.7-5.8,17.2-2.8,26.5c0.3,1,0.6,1.9,1,2.9
|
||||
l62.7-19.4C80.3,440,79.9,438.7,79.5,437.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M105.6,495.1c-5.4-9.2-10.4-18.7-14.8-28.5l-63,19.5c5.9,14.1,12.8,27.7,20.4,40.7L105.6,495.1z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M223.5,604.3c-7.3-3.6-14.5-7.6-21.5-11.9L160.8,644c10.4,6.7,21.1,12.8,32,18.3L223.5,604.3z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M275.6,624c-9.2-2.5-18.2-5.4-27.1-8.8l-30.8,58.4c13,5.3,26.2,9.8,39.8,13.5L275.6,624z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M352.5,633.9l-6.1,65.4c1.1,0,2.1,0,3.2,0c18.1,0,32.8-14.7,32.8-32.8C382.4,649.4,369.3,635.4,352.5,633.9z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M325.3,632.7c-7.7-0.6-15.4-1.6-22.9-2.9l-18.3,63.4c11.6,2.2,23.3,3.8,35.1,4.8L325.3,632.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M519.5,577.5c-5.8,4.3-11.8,8.4-17.9,12.3l31.7,57.4c9.7-6,19.1-12.4,28.1-19.3L519.5,577.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M559.1,541.7c-5.9,6.5-12.1,12.6-18.5,18.4l41.9,50.5c9.8-8.8,19.1-18.1,27.9-28L559.1,541.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M477.9,603.2c-7.8,3.9-15.9,7.5-24.1,10.8l19.4,62.7c12.5-4.7,24.7-10.1,36.4-16.1L477.9,603.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M589,502.6c-3,4.7-6.2,9.4-9.5,14c-1,1.4-2,2.7-3,4.1l51.2,40.9c1.6-2.1,3.3-4.3,4.9-6.5c5.2-7.1,10-14.4,14.6-21.7
|
||||
L589,502.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M666.6,316.8c-18.1,0-32.8,14.7-32.8,32.8c0,10.1-0.5,20-1.6,29.9l65.3,6.1c1.2-11.9,1.8-23.9,1.8-36
|
||||
C699.4,331.4,684.7,316.7,666.6,316.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M428.4,624.4c-3.8,2.7-6.9,6.2-9.2,10.2c-4.5,7.8-5.7,17.4-2.8,26.4c4.4,13.6,16.8,22.3,30.3,22.6L428.4,624.4z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M613.8,454.5c-3.3,8.3-7,16.5-11.1,24.5l58,30.6c6.2-12,11.7-24.3,16.4-36.9L613.8,454.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M628.1,406.5c-1.5,7.5-3.3,14.9-5.5,22.2l63.1,18.2c3.3-11.3,5.9-22.7,8-34.3L628.1,406.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M458.8,87.2c10.3,4.3,20.3,9.2,30.1,14.7l30.6-58c-13.7-7.6-27.9-14.3-42.4-20L458.8,87.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M512,116.4c7.5,5.3,14.8,10.9,21.8,16.8l40.9-51.2c-10.1-8.5-20.8-16.5-31.9-23.9L512,116.4z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M343.4,65.5c11.6-0.3,23.1,0.2,34.6,1.4L384,1.6c-15.9-1.6-31.9-2.1-48-1.4L343.4,65.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M574.3,175.6l50.5-41.9c-9.2-11.8-19.2-22.9-29.9-33.4l-41.2,51.6C561,159.4,567.9,167.3,574.3,175.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M32.7,382.2c16.6,0,30.2-12.2,32.5-28.1L0,348c0,0.5,0,0.9,0,1.4C0,367.6,14.7,382.2,32.7,382.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M590,198c4.6,7.3,8.8,14.7,12.8,22.4l57.4-31.7c-5.9-11.3-12.3-22.3-19.4-32.9L590,198z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M619.8,261.7c5.6,17.2,24.1,26.6,41.3,21.1c17.2-5.6,26.6-24.1,21.1-41.3c-3.1-9.5-6.6-18.9-10.5-28L614,245.4
|
||||
C616.1,250.8,618.1,256.2,619.8,261.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M404.9,70.8c9.5,1.9,18.9,4.3,28.1,7.1L451.2,15C438,11,424.6,7.8,411,5.3L404.9,70.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M286.5,72.5c6.2-1.4,12.4-2.6,18.7-3.6c3.6-0.6,7.3-1.1,10.9-1.5l-7.3-65.1c-4.6,0.5-9.2,1.2-13.8,1.9
|
||||
c-9.4,1.5-18.7,3.4-27.8,5.6L286.5,72.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M66.5,326.8c0.8-9.8,2.1-19.4,3.8-29L7.1,279.6c-2.8,13.6-4.8,27.3-6,41.1L66.5,326.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M132.5,166.2c0.4-0.4,0.7-0.9,1.1-1.3c7.4-8.6,15.2-16.7,23.5-24.3l-41.8-50.4c-11.1,10-21.6,20.8-31.4,32.2
|
||||
c-0.8,1-1.6,2-2.5,3L132.5,166.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M100.1,213.6c4.8-8.8,10.1-17.4,15.8-25.7L64.8,147c-6.4,9-12.3,18.2-17.8,27.7c-1.6,2.8-3.1,5.6-4.7,8.4L100.1,213.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M195.5,35.8l31.7,57.4c10.7-5.1,21.8-9.6,33.1-13.3l-19.3-62.6C225.4,22.4,210.2,28.6,195.5,35.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M76.5,271.2c3.2-11.2,7.1-22.3,11.7-33l-58-30.6c-6.6,14.8-12.1,30-16.6,45.5L76.5,271.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M178,123.1c8.1-6.1,16.5-11.8,25.2-17.1l-31.7-57.3c-12.2,7.3-24.1,15.3-35.4,24L178,123.1z'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<circle
|
||||
r={RADIUS}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='white'
|
||||
fillOpacity={0.2}
|
||||
mask='url(#mask)'
|
||||
style={ROTATION}
|
||||
/>
|
||||
<circle
|
||||
r={316}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='transparent'
|
||||
strokeWidth={64}
|
||||
pathLength={100}
|
||||
strokeDasharray={100}
|
||||
strokeDashoffset={isUpdated && isIncrease ? updatedPercentage : percentage}
|
||||
strokeLinecap='round'
|
||||
style={ROTATION}
|
||||
mask={isUpdated ? 'url(#backgroundMask)' : 'url(#mask)'}
|
||||
className={classNames(
|
||||
backgroundColor,
|
||||
'stroke-current',
|
||||
!reduceMotion && 'transition-color transition-[stroke-dashoffset] duration-500',
|
||||
)}
|
||||
/>
|
||||
{isUpdated && (
|
||||
<circle
|
||||
r={316}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='transparent'
|
||||
strokeWidth={64}
|
||||
pathLength={100}
|
||||
strokeDasharray={100}
|
||||
strokeDashoffset={isUpdated && !isIncrease ? updatedPercentage : percentage}
|
||||
strokeLinecap='round'
|
||||
style={ROTATION}
|
||||
mask='url(#mask)'
|
||||
className={classNames(
|
||||
foreGroundColor,
|
||||
'stroke-current',
|
||||
!reduceMotion && 'transition-color transition-[stroke-dashoffset] duration-500',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</HealthTooltip>
|
||||
)
|
||||
}
|
31
src/components/Account/Health/HealthIcon.tsx
Normal file
31
src/components/Account/Health/HealthIcon.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { ExclamationMarkCircled, Heart } from 'components/Icons'
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean
|
||||
health: number
|
||||
className: string
|
||||
colorClass?: string
|
||||
}
|
||||
|
||||
export default function HealthIcon(props: Props) {
|
||||
const { isLoading, health, className, colorClass } = props
|
||||
const color = colorClass ?? 'text-white'
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && health === 0 ? (
|
||||
<ExclamationMarkCircled className='w-5 text-loss animate-pulse' />
|
||||
) : (
|
||||
<Heart
|
||||
className={classNames(
|
||||
!isLoading && health <= 5 ? 'text-loss' : color,
|
||||
(isLoading || health <= 5) && 'animate-pulse',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
71
src/components/Account/Health/HealthTooltip.tsx
Normal file
71
src/components/Account/Health/HealthTooltip.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { CircularProgress } from 'components/CircularProgress'
|
||||
import Text from 'components/Text'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
interface Props {
|
||||
health: number
|
||||
healthFactor: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function HealthTooltipContent({ health, healthFactor }: { health: number; healthFactor: number }) {
|
||||
const healthStatus = useMemo(() => {
|
||||
if (health > 30) return 'Healthy'
|
||||
if (health > 10) return 'Attention'
|
||||
if (health > 0 && health <= 10) return 'Close to Liquidation'
|
||||
|
||||
return 'Liquidation Risk'
|
||||
}, [health])
|
||||
|
||||
if (healthFactor === 0)
|
||||
return (
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
<Text size='xs' className='w-full mb-1 text-center'>
|
||||
Loading...
|
||||
</Text>
|
||||
<CircularProgress size={14} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap justify-center w-20'>
|
||||
<Text size='xs' className='text-center'>
|
||||
Health: {health}%
|
||||
</Text>
|
||||
<Text size='2xs' className='mb-1 text-center'>
|
||||
({healthStatus})
|
||||
</Text>
|
||||
<Text size='2xs' className='text-center text-white/70'>
|
||||
Health Factor: {BN(healthFactor).toPrecision(4)}
|
||||
</Text>
|
||||
{health > 0 && health <= 10 && (
|
||||
<Text size='2xs' className='mt-2 text-center text-info'>
|
||||
A small price movement can cause your account to be become liquidatable!
|
||||
</Text>
|
||||
)}
|
||||
{health === 0 && (
|
||||
<Text size='2xs' className='mt-2 text-center text-martian-red'>
|
||||
Your account is unhealthy and can be liquidated!
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HealthTooltip(props: Props) {
|
||||
const { health, healthFactor, children } = props
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
type='info'
|
||||
hideArrow={health <= 10}
|
||||
className='flex items-center w-full'
|
||||
content={<HealthTooltipContent health={health} healthFactor={healthFactor} />}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
@ -25,7 +25,6 @@ export const Gauge = ({
|
||||
percentage = 0,
|
||||
tooltip,
|
||||
icon,
|
||||
labelClassName,
|
||||
}: Props) => {
|
||||
const [reduceMotion] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.REDUCE_MOTION,
|
||||
@ -48,7 +47,7 @@ export const Gauge = ({
|
||||
width={diameter}
|
||||
height={diameter}
|
||||
style={{ transform: 'rotate(-90deg)' }}
|
||||
className='absolute left-0 top-0'
|
||||
className='absolute top-0 left-0'
|
||||
>
|
||||
{!strokeColor && (
|
||||
<linearGradient id='gradient'>
|
||||
|
@ -1,268 +0,0 @@
|
||||
import classNames from 'classnames'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { Heart } from 'components/Icons'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
import { LocalStorageKeys } from 'constants/localStorageKeys'
|
||||
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import { computeHealthGaugePercentage } from 'utils/accounts'
|
||||
import { getHealthIndicatorColors } from 'utils/healthIndicator'
|
||||
|
||||
interface Props {
|
||||
diameter?: number
|
||||
health: number
|
||||
updatedHealth?: number
|
||||
}
|
||||
|
||||
const RADIUS = 350
|
||||
const ROTATION = {
|
||||
rotate: '-126deg',
|
||||
transformOrigin: 'center',
|
||||
}
|
||||
|
||||
export const HealthGauge = ({ diameter = 40, health = 0, updatedHealth = 0 }: Props) => {
|
||||
const [color, label] = useHealthColorAndLabel(health, 'text')
|
||||
const [updatedColor, updatedLabel] = useHealthColorAndLabel(updatedHealth ?? 0, 'text')
|
||||
const [reduceMotion] = useLocalStorage<boolean>(
|
||||
LocalStorageKeys.REDUCE_MOTION,
|
||||
DEFAULT_SETTINGS.reduceMotion,
|
||||
)
|
||||
const percentage = useMemo(() => computeHealthGaugePercentage(health), [health])
|
||||
const updatedPercentage = useMemo(
|
||||
() => computeHealthGaugePercentage(updatedHealth),
|
||||
[updatedHealth],
|
||||
)
|
||||
const isUpdated = updatedHealth !== 0 && updatedPercentage !== percentage
|
||||
const isIncrease = isUpdated && updatedPercentage < percentage
|
||||
const [backgroundColor, foreGroundColor] = useMemo(
|
||||
() => getHealthIndicatorColors(color, updatedColor, 'text', isUpdated, isIncrease),
|
||||
[color, updatedColor, isUpdated, isIncrease],
|
||||
)
|
||||
|
||||
const tooltipContent = health === 0 ? 'loading...' : isUpdated ? updatedLabel : label
|
||||
|
||||
return (
|
||||
<Tooltip type='info' content={tooltipContent}>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative grid place-items-center',
|
||||
`w-${diameter / 4} h-${diameter / 4}`,
|
||||
)}
|
||||
>
|
||||
<Heart className='text-white/50' width={20} />
|
||||
<svg
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
x='0px'
|
||||
y='0px'
|
||||
viewBox='0 0 700 700'
|
||||
className='absolute'
|
||||
>
|
||||
<mask id='mask'>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M585.2,594.5C577.4,594.5 569.5,591.7 563.3,586.1C549.9,574 548.8,553.3 560.9,539.8C601.3,494.9 626,439.5 632.3,379.4C638.6,319.3 626,259.9 595.8,207.7C586.8,192 592.1,172 607.8,162.9C623.5,153.8 643.5,159.2 652.6,174.9C689.8,239.2 705.3,312.3 697.5,386.3C689.7,460.2 659.3,528.5 609.6,583.7C603,590.9 594.1,594.5 585.2,594.5Z'
|
||||
/>
|
||||
</g>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M561.7,147C553.9,147 546,144.2 539.8,138.6C494.9,98.2 439.5,73.5 379.4,67.2C319.4,60.9 259.9,73.5 207.6,103.7C191.9,112.8 171.9,107.4 162.8,91.7C153.7,76 159.1,56 174.8,46.9C239.2,9.7 312.3,-5.8 386.2,2C460.1,9.8 528.4,40.2 583.6,89.9C597,102 598.1,122.7 586,136.2C579.6,143.3 570.7,147 561.7,147Z'
|
||||
/>
|
||||
</g>
|
||||
<g transform='matrix(-0.5,0.866025,-0.866025,-0.5,827.404,221.632)'>
|
||||
<path
|
||||
fill='white'
|
||||
d='M349.6,699.3C328.4,699.3 307.1,697.4 285.9,693.5C204.1,678.3 130.2,634.4 77.9,569.8C25.6,505.2 -2.1,423.8 0.1,340.6C2.3,257.4 34.1,177.6 89.8,115.8C101.9,102.4 122.7,101.3 136.1,113.4C149.5,125.5 150.6,146.2 138.5,159.7C93.3,209.9 67.4,274.8 65.6,342.4C63.8,410 86.3,476.1 128.8,528.6C171.3,581.1 231.4,616.8 297.8,629.2C364.2,641.5 433.1,629.7 491.6,595.9C507.3,586.8 527.3,592.2 536.4,607.9C545.4,623.6 540.1,643.6 524.4,652.7C471,683.4 410.6,699.3 349.6,699.3Z'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<mask id='backgroundMask'>
|
||||
<g>
|
||||
<path
|
||||
fill='white'
|
||||
d='M137.1,538.3c-5.8-6.5-11.3-13.3-16.5-20.4l-57.8,31.9c7.4,10.5,15.3,20.7,23.8,30.3L137.1,538.3z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M179.4,577.1c-8.1-6-15.8-12.4-23.1-19.2L105.5,600c10.3,10,21.3,19.5,32.9,28.3L179.4,577.1z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M79.5,437.5c-5.6-17.2-24.1-26.6-41.3-21.1c-7.9,2.6-14.3,8-18.2,14.8c-4.5,7.7-5.8,17.2-2.8,26.5c0.3,1,0.6,1.9,1,2.9
|
||||
l62.7-19.4C80.3,440,79.9,438.7,79.5,437.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M105.6,495.1c-5.4-9.2-10.4-18.7-14.8-28.5l-63,19.5c5.9,14.1,12.8,27.7,20.4,40.7L105.6,495.1z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M223.5,604.3c-7.3-3.6-14.5-7.6-21.5-11.9L160.8,644c10.4,6.7,21.1,12.8,32,18.3L223.5,604.3z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M275.6,624c-9.2-2.5-18.2-5.4-27.1-8.8l-30.8,58.4c13,5.3,26.2,9.8,39.8,13.5L275.6,624z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M352.5,633.9l-6.1,65.4c1.1,0,2.1,0,3.2,0c18.1,0,32.8-14.7,32.8-32.8C382.4,649.4,369.3,635.4,352.5,633.9z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M325.3,632.7c-7.7-0.6-15.4-1.6-22.9-2.9l-18.3,63.4c11.6,2.2,23.3,3.8,35.1,4.8L325.3,632.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M519.5,577.5c-5.8,4.3-11.8,8.4-17.9,12.3l31.7,57.4c9.7-6,19.1-12.4,28.1-19.3L519.5,577.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M559.1,541.7c-5.9,6.5-12.1,12.6-18.5,18.4l41.9,50.5c9.8-8.8,19.1-18.1,27.9-28L559.1,541.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M477.9,603.2c-7.8,3.9-15.9,7.5-24.1,10.8l19.4,62.7c12.5-4.7,24.7-10.1,36.4-16.1L477.9,603.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M589,502.6c-3,4.7-6.2,9.4-9.5,14c-1,1.4-2,2.7-3,4.1l51.2,40.9c1.6-2.1,3.3-4.3,4.9-6.5c5.2-7.1,10-14.4,14.6-21.7
|
||||
L589,502.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M666.6,316.8c-18.1,0-32.8,14.7-32.8,32.8c0,10.1-0.5,20-1.6,29.9l65.3,6.1c1.2-11.9,1.8-23.9,1.8-36
|
||||
C699.4,331.4,684.7,316.7,666.6,316.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M428.4,624.4c-3.8,2.7-6.9,6.2-9.2,10.2c-4.5,7.8-5.7,17.4-2.8,26.4c4.4,13.6,16.8,22.3,30.3,22.6L428.4,624.4z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M613.8,454.5c-3.3,8.3-7,16.5-11.1,24.5l58,30.6c6.2-12,11.7-24.3,16.4-36.9L613.8,454.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M628.1,406.5c-1.5,7.5-3.3,14.9-5.5,22.2l63.1,18.2c3.3-11.3,5.9-22.7,8-34.3L628.1,406.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M458.8,87.2c10.3,4.3,20.3,9.2,30.1,14.7l30.6-58c-13.7-7.6-27.9-14.3-42.4-20L458.8,87.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M512,116.4c7.5,5.3,14.8,10.9,21.8,16.8l40.9-51.2c-10.1-8.5-20.8-16.5-31.9-23.9L512,116.4z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M343.4,65.5c11.6-0.3,23.1,0.2,34.6,1.4L384,1.6c-15.9-1.6-31.9-2.1-48-1.4L343.4,65.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M574.3,175.6l50.5-41.9c-9.2-11.8-19.2-22.9-29.9-33.4l-41.2,51.6C561,159.4,567.9,167.3,574.3,175.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M32.7,382.2c16.6,0,30.2-12.2,32.5-28.1L0,348c0,0.5,0,0.9,0,1.4C0,367.6,14.7,382.2,32.7,382.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M590,198c4.6,7.3,8.8,14.7,12.8,22.4l57.4-31.7c-5.9-11.3-12.3-22.3-19.4-32.9L590,198z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M619.8,261.7c5.6,17.2,24.1,26.6,41.3,21.1c17.2-5.6,26.6-24.1,21.1-41.3c-3.1-9.5-6.6-18.9-10.5-28L614,245.4
|
||||
C616.1,250.8,618.1,256.2,619.8,261.7z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M404.9,70.8c9.5,1.9,18.9,4.3,28.1,7.1L451.2,15C438,11,424.6,7.8,411,5.3L404.9,70.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M286.5,72.5c6.2-1.4,12.4-2.6,18.7-3.6c3.6-0.6,7.3-1.1,10.9-1.5l-7.3-65.1c-4.6,0.5-9.2,1.2-13.8,1.9
|
||||
c-9.4,1.5-18.7,3.4-27.8,5.6L286.5,72.5z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M66.5,326.8c0.8-9.8,2.1-19.4,3.8-29L7.1,279.6c-2.8,13.6-4.8,27.3-6,41.1L66.5,326.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M132.5,166.2c0.4-0.4,0.7-0.9,1.1-1.3c7.4-8.6,15.2-16.7,23.5-24.3l-41.8-50.4c-11.1,10-21.6,20.8-31.4,32.2
|
||||
c-0.8,1-1.6,2-2.5,3L132.5,166.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M100.1,213.6c4.8-8.8,10.1-17.4,15.8-25.7L64.8,147c-6.4,9-12.3,18.2-17.8,27.7c-1.6,2.8-3.1,5.6-4.7,8.4L100.1,213.6z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M195.5,35.8l31.7,57.4c10.7-5.1,21.8-9.6,33.1-13.3l-19.3-62.6C225.4,22.4,210.2,28.6,195.5,35.8z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M76.5,271.2c3.2-11.2,7.1-22.3,11.7-33l-58-30.6c-6.6,14.8-12.1,30-16.6,45.5L76.5,271.2z'
|
||||
/>
|
||||
<path
|
||||
fill='white'
|
||||
d='M178,123.1c8.1-6.1,16.5-11.8,25.2-17.1l-31.7-57.3c-12.2,7.3-24.1,15.3-35.4,24L178,123.1z'
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<circle
|
||||
r={RADIUS}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='white'
|
||||
fillOpacity={0.2}
|
||||
mask='url(#mask)'
|
||||
style={ROTATION}
|
||||
/>
|
||||
<circle
|
||||
r={316}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='transparent'
|
||||
strokeWidth={64}
|
||||
pathLength={100}
|
||||
strokeDasharray={100}
|
||||
strokeDashoffset={isUpdated && isIncrease ? updatedPercentage : percentage}
|
||||
strokeLinecap='round'
|
||||
style={ROTATION}
|
||||
mask={isUpdated ? 'url(#backgroundMask)' : 'url(#mask)'}
|
||||
className={classNames(
|
||||
backgroundColor,
|
||||
'stroke-current',
|
||||
!reduceMotion && 'transition-color transition-[stroke-dashoffset] duration-500',
|
||||
)}
|
||||
/>
|
||||
{isUpdated && (
|
||||
<circle
|
||||
r={316}
|
||||
cx={RADIUS}
|
||||
cy={RADIUS}
|
||||
fill='transparent'
|
||||
strokeWidth={64}
|
||||
pathLength={100}
|
||||
strokeDasharray={100}
|
||||
strokeDashoffset={isUpdated && !isIncrease ? updatedPercentage : percentage}
|
||||
strokeLinecap='round'
|
||||
style={ROTATION}
|
||||
mask='url(#mask)'
|
||||
className={classNames(
|
||||
foreGroundColor,
|
||||
'stroke-current',
|
||||
!reduceMotion && 'transition-color transition-[stroke-dashoffset] duration-500',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
@ -19,7 +19,7 @@ interface Props {
|
||||
function Content(props: Props) {
|
||||
const { data: account } = useAccount(props.accountId, true)
|
||||
const { data: prices } = usePrices()
|
||||
const { health } = useHealthComputer(account)
|
||||
const { health, healthFactor } = useHealthComputer(account)
|
||||
const { data } = useBorrowMarketAssetsTableData(false)
|
||||
const borrowAssets = useMemo(() => data?.allAssets || [], [data])
|
||||
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
|
||||
@ -74,12 +74,23 @@ function Content(props: Props) {
|
||||
]
|
||||
}, [account, borrowAssets, lendingAssets, prices])
|
||||
|
||||
return <Skeleton stats={stats} health={health} title={`Credit Account ${props.accountId}`} />
|
||||
return (
|
||||
<Skeleton
|
||||
stats={stats}
|
||||
health={health}
|
||||
healthFactor={healthFactor}
|
||||
title={`Credit Account ${props.accountId}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Summary(props: Props) {
|
||||
return (
|
||||
<Suspense fallback={<Skeleton health={0} title={`Credit Account ${props.accountId}`} />}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Skeleton health={0} healthFactor={0} title={`Credit Account ${props.accountId}`} />
|
||||
}
|
||||
>
|
||||
<Content {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
|
@ -1,35 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import HealthBar from 'components/Account/HealthBar'
|
||||
import HealthBar from 'components/Account/Health/HealthBar'
|
||||
import HealthIcon from 'components/Account/Health/HealthIcon'
|
||||
import Card from 'components/Card'
|
||||
import { Heart } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
|
||||
interface Props {
|
||||
stats: { title: React.ReactNode; sub: string }[]
|
||||
health: number
|
||||
healthFactor: number
|
||||
accountId: string
|
||||
isCurrent?: boolean
|
||||
}
|
||||
|
||||
export default function Skeleton(props: Props) {
|
||||
const { stats, health, healthFactor, accountId, isCurrent } = props
|
||||
return (
|
||||
<Card className='p-4 bg-white/5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Text>Credit Account {props.accountId}</Text>
|
||||
<Text>Credit Account {accountId}</Text>
|
||||
<Text size='xs' className='text-white/60'>
|
||||
{props.isCurrent && '(current)'}
|
||||
{isCurrent && '(current)'}
|
||||
</Text>
|
||||
</div>
|
||||
<div className='flex gap-4 mt-6'>
|
||||
{props.stats.map(({ title, sub }) => (
|
||||
<TitleAndSubCell key={`${props.accountId}-${sub}`} title={title} sub={sub} />
|
||||
{stats.map(({ title, sub }) => (
|
||||
<TitleAndSubCell key={`${accountId}-${sub}`} title={title} sub={sub} />
|
||||
))}
|
||||
</div>
|
||||
<div className='flex gap-1 mt-6'>
|
||||
<Heart width={20} />
|
||||
<HealthBar health={props.health} />
|
||||
<HealthIcon
|
||||
isLoading={healthFactor === 0}
|
||||
health={health}
|
||||
className='w-5'
|
||||
colorClass='text-white/60'
|
||||
/>
|
||||
<HealthBar health={health} healthFactor={healthFactor} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
|
@ -15,7 +15,6 @@ import useHealthComputer from 'hooks/useHealthComputer'
|
||||
import useLendingMarketAssetsTableData from 'hooks/useLendingMarketAssetsTableData'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
import useStore from 'store'
|
||||
import {
|
||||
calculateAccountApr,
|
||||
calculateAccountLeverage,
|
||||
@ -29,7 +28,7 @@ interface Props {
|
||||
|
||||
export default function PortfolioCard(props: Props) {
|
||||
const { data: account } = useAccount(props.accountId)
|
||||
const { health } = useHealthComputer(account)
|
||||
const { health, healthFactor } = useHealthComputer(account)
|
||||
const { address: urlAddress } = useParams()
|
||||
const { data: prices } = usePrices()
|
||||
const currentAccountId = useAccountId()
|
||||
@ -40,7 +39,6 @@ export default function PortfolioCard(props: Props) {
|
||||
LocalStorageKeys.REDUCE_MOTION,
|
||||
DEFAULT_SETTINGS.reduceMotion,
|
||||
)
|
||||
const address = useStore((s) => s.address)
|
||||
|
||||
const [deposits, lends, debts, vaults] = useMemo(() => {
|
||||
if (!prices.length || !account) return Array(4).fill(BN_ZERO)
|
||||
@ -91,7 +89,14 @@ export default function PortfolioCard(props: Props) {
|
||||
}, [account, prices.length, deposits, lends, vaults, debts, leverage, apr])
|
||||
|
||||
if (!account) {
|
||||
return <Skeleton stats={stats} health={health} accountId={props.accountId} />
|
||||
return (
|
||||
<Skeleton
|
||||
stats={stats}
|
||||
health={health}
|
||||
healthFactor={healthFactor}
|
||||
accountId={props.accountId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -102,6 +107,7 @@ export default function PortfolioCard(props: Props) {
|
||||
<Skeleton
|
||||
stats={stats}
|
||||
health={health}
|
||||
healthFactor={healthFactor}
|
||||
accountId={props.accountId}
|
||||
isCurrent={props.accountId === currentAccountId}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import DisplayCurrency from 'components/DisplayCurrency'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
import HealthBar from 'components/Account/HealthBar'
|
||||
import HealthBar from 'components/Account/Health/HealthBar'
|
||||
import HealthIcon from 'components/Account/Health/HealthIcon'
|
||||
import Card from 'components/Card'
|
||||
import { Heart } from 'components/Icons'
|
||||
import Loading from 'components/Loading'
|
||||
import Text from 'components/Text'
|
||||
import TitleAndSubCell from 'components/TitleAndSubCell'
|
||||
@ -11,19 +11,22 @@ import { DEFAULT_PORTFOLIO_STATS } from 'utils/constants'
|
||||
interface Props {
|
||||
stats?: { title: React.ReactNode | null; sub: string }[]
|
||||
health?: number
|
||||
healthFactor?: number
|
||||
title: string
|
||||
}
|
||||
|
||||
export default function SummarySkeleton(props: Props) {
|
||||
const { health, healthFactor, title } = props
|
||||
const stats = props.stats || DEFAULT_PORTFOLIO_STATS
|
||||
|
||||
return (
|
||||
<div className='flex flex-col w-full gap-8'>
|
||||
<div className='flex justify-between'>
|
||||
<Text size='2xl'>{props.title}</Text>
|
||||
{props.health !== undefined && (
|
||||
<Text size='2xl'>{title}</Text>
|
||||
{health !== undefined && healthFactor !== undefined && (
|
||||
<div className='flex gap-1 max-w-[300px] flex-grow'>
|
||||
<Heart width={20} />
|
||||
<HealthBar health={props.health} className='h-full' />
|
||||
<HealthIcon isLoading={healthFactor === 0} health={health} className='w-5' />
|
||||
<HealthBar health={health} healthFactor={healthFactor} className='h-full' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -16,6 +16,8 @@ function getBorderColor(row: AccountBalanceRow) {
|
||||
}
|
||||
|
||||
export default function Row<T>(props: Props<T>) {
|
||||
const canExpand = !!props.renderExpanded
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
@ -23,7 +25,7 @@ export default function Row<T>(props: Props<T>) {
|
||||
className={classNames(
|
||||
'group/row transition-bg',
|
||||
props.renderExpanded && 'hover:cursor-pointer',
|
||||
props.row.getIsExpanded() ? 'is-expanded bg-black/20' : 'hover:bg-white/5',
|
||||
canExpand && props.row.getIsExpanded() ? 'is-expanded bg-black/20' : 'hover:bg-white/5',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
@ -7,6 +7,7 @@ import Text from 'components/Text'
|
||||
interface Props {
|
||||
content: ReactNode | string
|
||||
type: TooltipType
|
||||
hideArrow?: boolean
|
||||
}
|
||||
|
||||
export default function TooltipContent(props: Props) {
|
||||
@ -22,7 +23,7 @@ export default function TooltipContent(props: Props) {
|
||||
>
|
||||
{typeof props.content === 'string' ? <Text size='xs'>{props.content}</Text> : props.content}
|
||||
</div>
|
||||
{
|
||||
{!props.hideArrow && (
|
||||
<div data-popper-arrow=''>
|
||||
<TooltipArrow
|
||||
width={8}
|
||||
@ -33,7 +34,7 @@ export default function TooltipContent(props: Props) {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ interface Props {
|
||||
delay?: number
|
||||
interactive?: boolean
|
||||
underline?: boolean
|
||||
hideArrow?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = (props: Props) => {
|
||||
@ -33,7 +34,9 @@ export const Tooltip = (props: Props) => {
|
||||
interactive={props.interactive}
|
||||
animation={false}
|
||||
delay={[props.delay ?? 0, 0]}
|
||||
render={() => <TooltipContent type={props.type} content={props.content} />}
|
||||
render={() => (
|
||||
<TooltipContent hideArrow={props.hideArrow} type={props.type} content={props.content} />
|
||||
)}
|
||||
>
|
||||
{props.children ? (
|
||||
<span
|
||||
|
@ -52,6 +52,7 @@ export default function SwapForm(props: Props) {
|
||||
const [estimatedFee, setEstimatedFee] = useState(defaultFee)
|
||||
const { autoLendEnabledAccountIds } = useAutoLend()
|
||||
const isAutoLendEnabled = account ? autoLendEnabledAccountIds.includes(account.id) : false
|
||||
const modal = useStore<string | null>((s) => s.fundAndWithdrawModal)
|
||||
|
||||
const throttledEstimateExactIn = useMemo(() => asyncThrottle(estimateExactIn, 250), [])
|
||||
const { simulateTrade, removedLends } = useUpdatedAccount(account)
|
||||
@ -210,7 +211,8 @@ export default function SwapForm(props: Props) {
|
||||
? sellAssetAmount.minus(sellSideMarginThreshold)
|
||||
: BN_ZERO
|
||||
|
||||
if (removeDepositAmount.isZero() && addDebtAmount.isZero() && buyAssetAmount.isZero()) return
|
||||
if (removeDepositAmount.isZero() && addDebtAmount.isZero() && buyAssetAmount.isZero() && modal)
|
||||
return
|
||||
const removeCoin = BNCoin.fromDenomAndBigNumber(sellAsset.denom, removeDepositAmount)
|
||||
const debtCoin = BNCoin.fromDenomAndBigNumber(sellAsset.denom, addDebtAmount)
|
||||
const addCoin = BNCoin.fromDenomAndBigNumber(buyAsset.denom, buyAssetAmount)
|
||||
@ -223,6 +225,7 @@ export default function SwapForm(props: Props) {
|
||||
buyAsset.denom,
|
||||
sellAsset.denom,
|
||||
debouncedUpdateAccount,
|
||||
modal,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -57,7 +57,16 @@ function Content() {
|
||||
navigate(getRoute(page, address, urlAccountId ?? accountIds[0]))
|
||||
useStore.setState({ balances: walletBalances, focusComponent: null })
|
||||
}
|
||||
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances, urlAddress])
|
||||
}, [
|
||||
accountIds,
|
||||
baseBalance,
|
||||
navigate,
|
||||
pathname,
|
||||
address,
|
||||
walletBalances,
|
||||
urlAddress,
|
||||
urlAccountId,
|
||||
])
|
||||
|
||||
if (isLoadingAccounts || isLoadingBalances) return <FetchLoading />
|
||||
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) return <WalletBridges />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import useSWR from 'swr'
|
||||
|
||||
import useBorrowEnabledMarkets from 'hooks/useBorrowEnabledMarkets'
|
||||
import useCurrentAccountDebts from 'hooks/useCurrentAccountDebts'
|
||||
import useMarketBorrowings from 'hooks/useMarketBorrowings'
|
||||
import useMarketDeposits from 'hooks/useMarketDeposits'
|
||||
@ -8,8 +9,6 @@ import { byDenom } from 'utils/array'
|
||||
import { getAssetByDenom } from 'utils/assets'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
import useBorrowEnabledMarkets from './useBorrowEnabledMarkets'
|
||||
|
||||
export default function useBorrowMarketAssetsTableData(suspense = true) {
|
||||
const markets = useBorrowEnabledMarkets()
|
||||
const accountDebts = useCurrentAccountDebts()
|
||||
|
6
src/hooks/useHealthColor.ts
Normal file
6
src/hooks/useHealthColor.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default function useHealthColor(health: number, colorPrefix: 'fill' | 'text') {
|
||||
if (health > 30) return `${colorPrefix}-violet-500`
|
||||
if (health > 10) return `${colorPrefix}-yellow-300`
|
||||
|
||||
return `${colorPrefix}-martian-red`
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default function useHealthColorAndLabel(health: number, colorPrefix: 'fill' | 'text') {
|
||||
if (health > 30) return [`${colorPrefix}-violet-500`, `${health}% (Healthy)`]
|
||||
if (health > 10) return [`${colorPrefix}-yellow-300`, `${health}% (Attention)`]
|
||||
|
||||
return [`${colorPrefix}-martian-red`, `${health}% (Liquidation risk)`]
|
||||
}
|
@ -182,11 +182,15 @@ export default function useHealthComputer(account?: Account) {
|
||||
[healthComputer],
|
||||
)
|
||||
const health = useMemo(() => {
|
||||
if (healthFactor > 10) return 100
|
||||
if (healthFactor < 0) return 0
|
||||
return BN(healthFactor * 10)
|
||||
const convertedHealth = BN(Math.log(healthFactor))
|
||||
.dividedBy(Math.log(3.5))
|
||||
.multipliedBy(100)
|
||||
.integerValue()
|
||||
.toNumber()
|
||||
|
||||
if (convertedHealth > 100) return 100
|
||||
if (convertedHealth < 0) return 0
|
||||
return convertedHealth
|
||||
}, [healthFactor])
|
||||
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user