Health indicator change preview (#410)
* fix: adjusted the AccountDetail width * feat: added health indicator updates * Update src/components/Account/AccountDetails.tsx Co-authored-by: Yusuf Seyrek <yusufseyrek@users.noreply.github.com> * fix: created a function for the back- and foregroundColors * fix: updated tailwind conf * fix: fixed the useHealthColorAndLabel function --------- Co-authored-by: Yusuf Seyrek <yusufseyrek@users.noreply.github.com> Co-authored-by: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com>
This commit is contained in:
parent
23fe839229
commit
fbb4207f93
@ -48,6 +48,7 @@ function AccountDetails(props: Props) {
|
|||||||
const updatedAccount = useStore((s) => s.updatedAccount)
|
const updatedAccount = useStore((s) => s.updatedAccount)
|
||||||
const [isExpanded, setIsExpanded] = useToggle()
|
const [isExpanded, setIsExpanded] = useToggle()
|
||||||
const { health } = useHealthComputer(account)
|
const { health } = useHealthComputer(account)
|
||||||
|
const { health: updatedHealth } = useHealthComputer(updatedAccount || account)
|
||||||
const { data: prices } = usePrices()
|
const { data: prices } = usePrices()
|
||||||
const accountBalanceValue = useMemo(
|
const accountBalanceValue = useMemo(
|
||||||
() => calculateAccountBalanceValue(updatedAccount ? updatedAccount : account, prices),
|
() => calculateAccountBalanceValue(updatedAccount ? updatedAccount : account, prices),
|
||||||
@ -78,8 +79,8 @@ function AccountDetails(props: Props) {
|
|||||||
<div
|
<div
|
||||||
data-testid='account-details'
|
data-testid='account-details'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isExpanded ? 'right-6' : '-right-80',
|
isExpanded ? 'right-4' : '-right-82.5',
|
||||||
'w-100 flex items-start gap-4 absolute top-6',
|
'w-100 flex items-start gap-6 absolute top-6',
|
||||||
!reduceMotion && 'transition-all duration-300',
|
!reduceMotion && 'transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -94,7 +95,7 @@ function AccountDetails(props: Props) {
|
|||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
>
|
>
|
||||||
<div className='flex flex-wrap justify-center w-full py-4'>
|
<div className='flex flex-wrap justify-center w-full py-4'>
|
||||||
<HealthGauge health={health} />
|
<HealthGauge health={health} updatedHealth={updatedHealth} />
|
||||||
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
<Text size='2xs' className='mb-0.5 mt-1 w-full text-center text-white/50'>
|
||||||
Account Health
|
Account Health
|
||||||
</Text>
|
</Text>
|
||||||
@ -140,7 +141,7 @@ function AccountDetails(props: Props) {
|
|||||||
|
|
||||||
{glowElement(!reduceMotion)}
|
{glowElement(!reduceMotion)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex w-80 backdrop-blur-sticky'>
|
<div className='flex w-90 backdrop-blur-sticky'>
|
||||||
<Card className='w-full bg-white/5' title={`Credit Account ${account.id}`}>
|
<Card className='w-full bg-white/5' title={`Credit Account ${account.id}`}>
|
||||||
<AccountComposition account={account} />
|
<AccountComposition account={account} />
|
||||||
<Text className='w-full px-4 py-2 text-white bg-white/10'>Balances</Text>
|
<Text className='w-full px-4 py-2 text-white bg-white/10'>Balances</Text>
|
||||||
|
@ -60,7 +60,7 @@ export default function AccountStats(props: Props) {
|
|||||||
<Text size='xs' className='w-auto mr-1 text-white/70'>
|
<Text size='xs' className='w-auto mr-1 text-white/70'>
|
||||||
Health
|
Health
|
||||||
</Text>
|
</Text>
|
||||||
<HealthBar health={health} classNames='w-[92px] h-0.5' hasLabel />
|
<HealthBar health={health} className='w-[92px] h-0.5' hasLabel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,25 +45,26 @@ export default function AccountSummary(props: Props) {
|
|||||||
[lendingAvailableAssets, accountLentAssets],
|
[lendingAvailableAssets, accountLentAssets],
|
||||||
)
|
)
|
||||||
const { health } = useHealthComputer(props.account)
|
const { health } = useHealthComputer(props.account)
|
||||||
|
const { health: updatedHealth } = useHealthComputer(updatedAccount)
|
||||||
const leverage = useMemo(
|
const leverage = useMemo(
|
||||||
() => (props.account ? calculateAccountLeverage(props.account, prices) : BN_ZERO),
|
() => (props.account ? calculateAccountLeverage(props.account, prices) : BN_ZERO),
|
||||||
[props.account, prices],
|
[props.account, prices],
|
||||||
)
|
)
|
||||||
if (!props.account) return null
|
if (!props.account) return null
|
||||||
return (
|
return (
|
||||||
<div className='h-[546px] min-w-[370px] basis-[370px] overflow-y-scroll scrollbar-hide'>
|
<div className='h-[546px] min-w-92.5 basis-92.5 max-w-screen overflow-y-scroll scrollbar-hide'>
|
||||||
<Card className='mb-4 h-min min-w-fit bg-white/10' contentClassName='flex'>
|
<Card className='mb-4 h-min min-w-fit bg-white/10' contentClassName='flex'>
|
||||||
<Item label='Net worth' classNames='flex-1'>
|
<Item label='Net worth' classes='flex-1'>
|
||||||
<DisplayCurrency
|
<DisplayCurrency
|
||||||
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, accountBalance)}
|
coin={BNCoin.fromDenomAndBigNumber(ORACLE_DENOM, accountBalance)}
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
<Item label='Leverage' classNames='flex-1'>
|
<Item label='Leverage' classes='flex-1'>
|
||||||
<Text size='sm'>{formatLeverage(leverage.toNumber())}</Text>
|
<Text size='sm'>{formatLeverage(leverage.toNumber())}</Text>
|
||||||
</Item>
|
</Item>
|
||||||
<Item label='Account health'>
|
<Item label='Account health'>
|
||||||
<HealthBar health={health} classNames='w-[184px] h-1' />
|
<HealthBar health={health} updatedHealth={updatedHealth} className='w-[184px] h-1' />
|
||||||
</Item>
|
</Item>
|
||||||
</Card>
|
</Card>
|
||||||
<Accordion
|
<Accordion
|
||||||
@ -99,7 +100,7 @@ export default function AccountSummary(props: Props) {
|
|||||||
|
|
||||||
interface ItemProps extends HTMLAttributes<HTMLDivElement> {
|
interface ItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
label: string
|
label: string
|
||||||
classNames?: string
|
classes?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item(props: ItemProps) {
|
function Item(props: ItemProps) {
|
||||||
@ -107,7 +108,7 @@ function Item(props: ItemProps) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex flex-col justify-around px-3 py-1 border-r border-r-white/10',
|
'flex flex-col justify-around px-3 py-1 border-r border-r-white/10',
|
||||||
props.classNames,
|
props.classes,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -1,38 +1,54 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import { Tooltip } from 'components/Tooltip'
|
import { Tooltip } from 'components/Tooltip'
|
||||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||||
import { REDUCE_MOTION_KEY } from 'constants/localStore'
|
import { REDUCE_MOTION_KEY } from 'constants/localStore'
|
||||||
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
||||||
import useLocalStorage from 'hooks/useLocalStorage'
|
import useLocalStorage from 'hooks/useLocalStorage'
|
||||||
|
import { getHealthIndicatorColors } from 'utils/healthIndicator'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
health: number
|
health: number
|
||||||
|
updatedHealth?: number
|
||||||
hasLabel?: boolean
|
hasLabel?: boolean
|
||||||
classNames?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateHealth(health: number) {
|
function calculateHealth(health: number): number {
|
||||||
const firstBarEnd = 43
|
const firstBarEnd = 43
|
||||||
const secondBarStart = 46
|
const secondBarStart = 46
|
||||||
const secondBarEnd = 93
|
const secondBarEnd = 93
|
||||||
const thirdBarStart = 96
|
const thirdBarStart = 96
|
||||||
const thirdBarEnd = 184
|
const thirdBarEnd = 184
|
||||||
|
|
||||||
|
if (health <= 0) return 0
|
||||||
if (health <= 10) return (firstBarEnd / 10) * health
|
if (health <= 10) return (firstBarEnd / 10) * health
|
||||||
if (health <= 30) return ((secondBarEnd - secondBarStart) / 20) * (health - 10) + secondBarStart
|
if (health <= 30) return ((secondBarEnd - secondBarStart) / 20) * (health - 10) + secondBarStart
|
||||||
if (health <= 100) return ((thirdBarEnd - thirdBarStart) / 70) * (health - 30) + thirdBarStart
|
return ((thirdBarEnd - thirdBarStart) / 70) * (health - 30) + thirdBarStart
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HealthBar(props: Props) {
|
export default function HealthBar(props: Props) {
|
||||||
const { health } = props
|
const { health, updatedHealth } = props
|
||||||
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
|
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
|
||||||
const [color, label] = useHealthColorAndLabel(health, 'fill-')
|
|
||||||
const width = calculateHealth(health)
|
const width = calculateHealth(health)
|
||||||
|
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 [backgroundColor, foreGroundColor] = useMemo(
|
||||||
|
() => getHealthIndicatorColors(color, updatedColor, 'fill', isUpdated, isIncrease),
|
||||||
|
[color, updatedColor, isUpdated, isIncrease],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content={label} type='info' className='flex items-center w-full'>
|
<Tooltip
|
||||||
<div className={classNames('flex max-w-[184px] max-h-1', props.classNames)}>
|
content={isUpdated ? updatedLabel : label}
|
||||||
|
type='info'
|
||||||
|
className='flex items-center w-full'
|
||||||
|
>
|
||||||
|
<div className={classNames('flex max-w-[184px] max-h-1', props.className)}>
|
||||||
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 184 4'>
|
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 184 4'>
|
||||||
<mask id='healthBarMask'>
|
<mask id='healthBarMask'>
|
||||||
<path fill='#FFFFFF' d='M0,2c0-1.1,0.9-2,2-2h41.6v4H2C0.9,4,0,3.1,0,2z' />
|
<path fill='#FFFFFF' d='M0,2c0-1.1,0.9-2,2-2h41.6v4H2C0.9,4,0,3.1,0,2z' />
|
||||||
@ -41,11 +57,22 @@ export default function HealthBar(props: Props) {
|
|||||||
</mask>
|
</mask>
|
||||||
<rect className='fill-white/10' width='184' height='4' mask='url(#healthBarMask)' />
|
<rect className='fill-white/10' width='184' height='4' mask='url(#healthBarMask)' />
|
||||||
<rect
|
<rect
|
||||||
className={classNames(color, !reduceMotion && 'transition-all duration-500')}
|
className={classNames(backgroundColor, !reduceMotion && 'transition-all duration-500')}
|
||||||
width={width}
|
width={isUpdated && isIncrease ? updatedWidth : width}
|
||||||
height='4'
|
height='4'
|
||||||
mask='url(#healthBarMask)'
|
mask='url(#healthBarMask)'
|
||||||
/>
|
/>
|
||||||
|
{isUpdated && (
|
||||||
|
<rect
|
||||||
|
className={classNames(
|
||||||
|
foreGroundColor,
|
||||||
|
!reduceMotion && 'transition-all duration-500',
|
||||||
|
)}
|
||||||
|
width={isUpdated && !isIncrease ? updatedWidth : width}
|
||||||
|
height='4'
|
||||||
|
mask='url(#healthBarMask)'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -8,10 +8,12 @@ import { REDUCE_MOTION_KEY } from 'constants/localStore'
|
|||||||
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
import useHealthColorAndLabel from 'hooks/useHealthColorAndLabel'
|
||||||
import useLocalStorage from 'hooks/useLocalStorage'
|
import useLocalStorage from 'hooks/useLocalStorage'
|
||||||
import { computeHealthGaugePercentage } from 'utils/accounts'
|
import { computeHealthGaugePercentage } from 'utils/accounts'
|
||||||
|
import { getHealthIndicatorColors } from 'utils/healthIndicator'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
diameter?: number
|
diameter?: number
|
||||||
health: number
|
health: number
|
||||||
|
updatedHealth?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const RADIUS = 350
|
const RADIUS = 350
|
||||||
@ -20,13 +22,24 @@ const ROTATION = {
|
|||||||
transformOrigin: 'center',
|
transformOrigin: 'center',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthGauge = ({ diameter = 40, health = 0 }: Props) => {
|
export const HealthGauge = ({ diameter = 40, health = 0, updatedHealth = 0 }: Props) => {
|
||||||
const [color, label] = useHealthColorAndLabel(health, 'text-')
|
const [color, label] = useHealthColorAndLabel(health, 'text')
|
||||||
|
const [updatedColor, updatedLabel] = useHealthColorAndLabel(updatedHealth ?? 0, 'text')
|
||||||
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
|
const [reduceMotion] = useLocalStorage<boolean>(REDUCE_MOTION_KEY, DEFAULT_SETTINGS.reduceMotion)
|
||||||
const percentage = useMemo(() => computeHealthGaugePercentage(health), [health])
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip type='info' content={label}>
|
<Tooltip type='info' content={isUpdated ? updatedLabel : label}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'relative grid place-items-center',
|
'relative grid place-items-center',
|
||||||
@ -79,16 +92,36 @@ export const HealthGauge = ({ diameter = 40, health = 0 }: Props) => {
|
|||||||
strokeWidth={64}
|
strokeWidth={64}
|
||||||
pathLength={100}
|
pathLength={100}
|
||||||
strokeDasharray={100}
|
strokeDasharray={100}
|
||||||
strokeDashoffset={percentage}
|
strokeDashoffset={isUpdated && isIncrease ? updatedPercentage : percentage}
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
style={ROTATION}
|
style={ROTATION}
|
||||||
mask='url(#mask)'
|
mask='url(#mask)'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
color,
|
backgroundColor,
|
||||||
'stroke-current',
|
'stroke-current',
|
||||||
!reduceMotion && 'transition-color transition-[stroke-dashoffset] duration-500',
|
!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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export default function useHealthColorAndLabel(health: number, colorPrefix: string) {
|
export default function useHealthColorAndLabel(health: number, colorPrefix: 'fill' | 'text') {
|
||||||
if (health > 30) return [`${colorPrefix}violet-500`, 'Healthy']
|
if (health > 30) return [`${colorPrefix}-violet-500`, 'Healthy']
|
||||||
if (health > 10) return [`${colorPrefix}yellow-300`, 'Attention']
|
if (health > 10) return [`${colorPrefix}-yellow-300`, 'Attention']
|
||||||
return [`${colorPrefix}martian-red`, 'Liquidation risk']
|
|
||||||
|
return [`${colorPrefix}-martian-red`, 'Liquidation risk']
|
||||||
}
|
}
|
||||||
|
15
src/utils/healthIndicator.ts
Normal file
15
src/utils/healthIndicator.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export function getHealthIndicatorColors(
|
||||||
|
color: string,
|
||||||
|
updatedColor: string,
|
||||||
|
colorPrefix: 'fill' | 'text',
|
||||||
|
isUpdated: boolean,
|
||||||
|
isIncrease: boolean,
|
||||||
|
): [backgroundColor: string, foregroundColor: string] {
|
||||||
|
let backgroundColor = color
|
||||||
|
if (isUpdated && isIncrease) backgroundColor = updatedColor
|
||||||
|
if (isUpdated && !isIncrease) backgroundColor = `${colorPrefix}-grey`
|
||||||
|
|
||||||
|
const foreGroundColor = isIncrease ? `${colorPrefix}-grey` : updatedColor
|
||||||
|
|
||||||
|
return [backgroundColor, foreGroundColor]
|
||||||
|
}
|
@ -29,9 +29,11 @@ module.exports = {
|
|||||||
'text-5xl',
|
'text-5xl',
|
||||||
'text-yellow-300',
|
'text-yellow-300',
|
||||||
'text-violet-500',
|
'text-violet-500',
|
||||||
|
'text-grey',
|
||||||
'fill-yellow-300',
|
'fill-yellow-300',
|
||||||
'fill-violet-500',
|
'fill-violet-500',
|
||||||
'fill-martian-red',
|
'fill-martian-red',
|
||||||
|
'fill-grey',
|
||||||
'w-2',
|
'w-2',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
@ -82,7 +84,7 @@ module.exports = {
|
|||||||
error: '#F04438',
|
error: '#F04438',
|
||||||
'error-bg': '#FDA29B',
|
'error-bg': '#FDA29B',
|
||||||
green: '#039855',
|
green: '#039855',
|
||||||
grey: '#3a3c49',
|
grey: '#908e91',
|
||||||
'grey-dark': '#1a1c25',
|
'grey-dark': '#1a1c25',
|
||||||
'grey-highlight': '#4c4c4c',
|
'grey-highlight': '#4c4c4c',
|
||||||
'grey-light': '#bfbfbf',
|
'grey-light': '#bfbfbf',
|
||||||
@ -176,6 +178,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
minWidth: {
|
minWidth: {
|
||||||
15: '60px',
|
15: '60px',
|
||||||
|
92.5: '370px',
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
5.5: '22px',
|
5.5: '22px',
|
||||||
@ -191,6 +194,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
35: '140px',
|
35: '140px',
|
||||||
|
82.5: '330px',
|
||||||
},
|
},
|
||||||
transitionDuration: {
|
transitionDuration: {
|
||||||
3000: '3000ms',
|
3000: '3000ms',
|
||||||
@ -210,6 +214,7 @@ module.exports = {
|
|||||||
35: '140px',
|
35: '140px',
|
||||||
50: '200px',
|
50: '200px',
|
||||||
60: '240px',
|
60: '240px',
|
||||||
|
92.5: '370px',
|
||||||
100: '400px',
|
100: '400px',
|
||||||
120: '480px',
|
120: '480px',
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user