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:
Linkie Link 2023-09-01 11:04:40 +02:00 committed by GitHub
parent 23fe839229
commit fbb4207f93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 113 additions and 30 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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}
> >

View File

@ -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>

View File

@ -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>

View File

@ -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']
} }

View 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]
}

View File

@ -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',
}, },