mirror of
https://github.com/cerc-io/mars-interface.git
synced 2026-09-15 03:14:07 +00:00
initiating public sequence
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.button {
|
||||
transition: background-color 0.2s, border 0.2s;
|
||||
color: $fontColorLightPrimary;
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&.small {
|
||||
@include buttonS;
|
||||
|
||||
svg,
|
||||
.progressIndicator {
|
||||
width: rem-calc(10);
|
||||
height: rem-calc(10);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
@include buttonM;
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(3);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
width: rem-calc(12);
|
||||
margin-inline-start: space(3);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
@include buttonL;
|
||||
svg {
|
||||
width: rem-calc(18);
|
||||
height: rem-calc(18);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(4.5);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(4.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&.solid,
|
||||
&.round {
|
||||
@include buttonSolidPrimary;
|
||||
@include buttonSolidSecondary;
|
||||
@include buttonSolidTertiary;
|
||||
}
|
||||
|
||||
&.round {
|
||||
border-radius: $borderRadiusRound;
|
||||
@include padding(0);
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
@include margin(0);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: rem-calc(32);
|
||||
width: rem-calc(32);
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: rem-calc(40);
|
||||
width: rem-calc(40);
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(14);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: rem-calc(56);
|
||||
width: rem-calc(56);
|
||||
|
||||
svg {
|
||||
width: rem-calc(20);
|
||||
height: rem-calc(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: none;
|
||||
@include padding(0);
|
||||
height: unset;
|
||||
|
||||
&.primary {
|
||||
color: $colorPrimary;
|
||||
|
||||
* {
|
||||
color: $colorPrimary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $colorPrimaryHighlight;
|
||||
|
||||
* {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: $colorSecondary;
|
||||
|
||||
* {
|
||||
color: $colorSecondary;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorSecondaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($colorSecondaryDark, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CircularProgress } from '@material-ui/core'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { ButtonStyleOverride } from '../types/components'
|
||||
import styles from './Button.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: any
|
||||
color?: 'primary' | 'secondary' | 'tertiary'
|
||||
disabled?: boolean
|
||||
externalLink?: string
|
||||
id?: string
|
||||
suffix?: ReactNode
|
||||
prefix?: ReactNode
|
||||
showProgressIndicator?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
styleOverride?: ButtonStyleOverride
|
||||
text?: string | ReactNode
|
||||
variant?: 'solid' | 'transparent' | 'round'
|
||||
onClick?: (e: any) => void
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
className = '',
|
||||
color = 'primary',
|
||||
disabled,
|
||||
externalLink,
|
||||
id = '',
|
||||
suffix,
|
||||
prefix,
|
||||
showProgressIndicator,
|
||||
size = 'small',
|
||||
styleOverride,
|
||||
text,
|
||||
variant = 'solid',
|
||||
onClick,
|
||||
}: Props) => {
|
||||
const Button = () => (
|
||||
<button
|
||||
id={id}
|
||||
onClick={disabled ? () => {} : onClick}
|
||||
style={styleOverride}
|
||||
className={`${styles.button} ${styles[size]} ${styles[color]} ${
|
||||
styles[variant]
|
||||
} ${className} ${disabled ? `${styles.disabled}` : ''}`}
|
||||
>
|
||||
{prefix && !showProgressIndicator && (
|
||||
<div className={styles.prefix}>{prefix}</div>
|
||||
)}
|
||||
{text && (
|
||||
<div className={styles.text}>
|
||||
{showProgressIndicator ? (
|
||||
<CircularProgress
|
||||
color='inherit'
|
||||
size={
|
||||
size === 'small'
|
||||
? '10px'
|
||||
: size === 'medium'
|
||||
? '12px'
|
||||
: '18px'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{suffix && !showProgressIndicator && (
|
||||
<div className={styles.suffix}>{suffix}</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
return externalLink ? (
|
||||
<a
|
||||
href={externalLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={styles.link}
|
||||
>
|
||||
{Button()}
|
||||
</a>
|
||||
) : (
|
||||
Button()
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createStyles, Switch, withStyles } from '@material-ui/core'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
|
||||
interface Props {
|
||||
switchCallback: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
enabled: boolean
|
||||
) => void
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
const CollateralSwitch = ({ switchCallback, checked }: Props) => {
|
||||
const CustomSwitch = withStyles(() =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: 28,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
},
|
||||
switchBase: {
|
||||
padding: 2,
|
||||
color: colors.grey,
|
||||
'&$checked': {
|
||||
transform: 'translateX(12px)',
|
||||
color: colors.white,
|
||||
'& + $track': {
|
||||
opacity: 1,
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
thumb: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
boxShadow: 'none',
|
||||
},
|
||||
track: {
|
||||
border: `1px solid ${colors.grey}`,
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
checked: {},
|
||||
})
|
||||
)(Switch)
|
||||
|
||||
return <CustomSwitch checked={checked} onChange={switchCallback} />
|
||||
}
|
||||
|
||||
export default CollateralSwitch
|
||||
@@ -0,0 +1,70 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(184);
|
||||
box-sizing: unset;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
|
||||
.valueItem {
|
||||
margin-top: space(1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: rem-calc(8);
|
||||
border-radius: $borderRadiusRound;
|
||||
width: rem-calc(8);
|
||||
margin-inline-end: space(2);
|
||||
margin-top: space(1.5);
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(8);
|
||||
}
|
||||
|
||||
.subHeadline {
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 2);
|
||||
@include padding(0);
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
height: rem-calc(16);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1 0 rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import styles from './CollectionHover.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface HoverItem {
|
||||
color?: string
|
||||
name: string
|
||||
amount?: number
|
||||
usdValue?: number
|
||||
negative?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data?: HoverItem[]
|
||||
title?: string
|
||||
noPercent?: boolean
|
||||
}
|
||||
|
||||
const CollectionHover = ({ data, title, noPercent }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
// -----------------
|
||||
// CALCULATE
|
||||
// -----------------
|
||||
const totalUsdValue = data
|
||||
? data.reduce((total, item) => total + (item.usdValue || 0), 0)
|
||||
: 0
|
||||
|
||||
const producePercentage = (fraction: number, sum: number): string => {
|
||||
if (fraction <= 0.01 || sum <= 0.01) {
|
||||
return '0.00%'
|
||||
} else {
|
||||
return formatValue((fraction / sum) * 100, 2, 2, true, false, '%')
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// PRESENTATION
|
||||
// -----------------
|
||||
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
{item.usdValue ? (
|
||||
<div className={styles.valueItem}>
|
||||
{item.color && (
|
||||
<div
|
||||
className={styles.dot}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.amount
|
||||
? formatValue(item.amount)
|
||||
: formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{!noPercent &&
|
||||
producePercentage(
|
||||
item.usdValue,
|
||||
totalUsdValue
|
||||
)}
|
||||
</span>
|
||||
{item.amount && (
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.subHeadline}>{item.name}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return data ? (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{title ? title : t('common.summary')}</p>
|
||||
{data.map((item: HoverItem, index: number) =>
|
||||
produceItem(item, index)
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default CollectionHover
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useConnectedWallet, useWallet } from '@terra-money/wallet-provider'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import networks from '../networks'
|
||||
import useBlockHeightQuery from '../queries-new/BlockHeightQuery'
|
||||
import useStore from '../store'
|
||||
|
||||
interface CommonContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const CommonContainer = ({ children }: CommonContainerProps) => {
|
||||
/**
|
||||
* Network configurations
|
||||
*/
|
||||
const { network: extNetwork, status } = useWallet()
|
||||
const networkName = extNetwork.name
|
||||
const network = networks[networkName]
|
||||
const connectedWallet = useConnectedWallet()
|
||||
|
||||
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
|
||||
const setNetworkInfo = useStore((s) => s.setNetworkInfo)
|
||||
const setUserWalletAddress = useStore((s) => s.setUserWalletAddress)
|
||||
const setNetworkConfig = useStore((s) => s.setNetworkConfig)
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkConfig(network || extNetwork)
|
||||
}, [network, extNetwork, setNetworkConfig])
|
||||
|
||||
useEffect(() => {
|
||||
setUserWalletAddress(connectedWallet?.terraAddress ?? '')
|
||||
}, [setUserWalletAddress, connectedWallet])
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkInfo(networkName, status)
|
||||
}, [networkName, status, setNetworkInfo])
|
||||
|
||||
/**
|
||||
* Blockchain meta data
|
||||
*/
|
||||
useBlockHeightQuery()
|
||||
|
||||
return <>{isNetworkLoaded && children}</>
|
||||
}
|
||||
|
||||
export default CommonContainer
|
||||
@@ -0,0 +1,74 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.network {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
position: fixed;
|
||||
margin: -100% 0 0;
|
||||
transition: margin 0.5s;
|
||||
|
||||
&.show {
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.container {
|
||||
@include margin(0);
|
||||
@include padding(6, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
background-color: $colorAccent;
|
||||
}
|
||||
|
||||
.headline {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@include typoScaps;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
display: block;
|
||||
letter-spacing: rem-calc(1);
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $colorPrimary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: space(3);
|
||||
top: space(3);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(20);
|
||||
width: rem-calc(20);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styles from './ErrorBanner.module.scss'
|
||||
import { CloseSVG } from './Svg'
|
||||
|
||||
interface ErrorBannerProps {
|
||||
hasNetworkError: boolean
|
||||
hasQueryError: boolean
|
||||
hasServerError: boolean
|
||||
isNetworkSupported: boolean | undefined
|
||||
}
|
||||
|
||||
const ErrorBanner = memo(
|
||||
({
|
||||
hasNetworkError,
|
||||
hasQueryError,
|
||||
hasServerError,
|
||||
isNetworkSupported,
|
||||
}: ErrorBannerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [hideError, setHideError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNetworkError || hasQueryError || hasServerError) {
|
||||
setHideError(false)
|
||||
}
|
||||
}, [hasNetworkError, hasQueryError, hasServerError])
|
||||
|
||||
const getHeadline = (): string => {
|
||||
return hasNetworkError
|
||||
? t('common.appearToBeOffline')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineTitle')
|
||||
: t('error.failingRequest')
|
||||
}
|
||||
|
||||
const getBody = (): string => {
|
||||
return hasNetworkError
|
||||
? t('error.youHaveAFailingNetworkRequest')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineBody')
|
||||
: t('error.failingRequestDescription')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Error banner is disabled for GQL errors currently */}
|
||||
{(hasNetworkError || hasServerError) && isNetworkSupported && (
|
||||
<div
|
||||
className={
|
||||
!hideError
|
||||
? `${styles.network} ${styles.show}`
|
||||
: styles.network
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.close}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHideError(true)
|
||||
}}
|
||||
>
|
||||
<CloseSVG />
|
||||
</button>
|
||||
</div>
|
||||
<h3 className={styles.headline}>{getHeadline()}</h3>
|
||||
<p>{getBody()}</p>
|
||||
{!hasNetworkError && (
|
||||
<p>
|
||||
<Trans i18nKey={'error.problemPersists'}>
|
||||
text
|
||||
<a
|
||||
className={styles.link}
|
||||
href='https://discord.gg/marsprotocol'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ErrorBanner
|
||||
@@ -0,0 +1,54 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.select {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
display: inline-block;
|
||||
@include padding(0.5, 0, 0.5, 3);
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
@include typoM;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectWrapper {
|
||||
border: 1px solid $buttonBorder;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(37);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(8);
|
||||
background-color: $fontColorLightPrimary;
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
justify-self: end;
|
||||
position: absolute;
|
||||
right: rem-calc(8);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import i18n from 'i18next'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import styles from './LanguageSelect.module.scss'
|
||||
|
||||
const LanguageSelect = () => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState('en')
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const lang = i18n.language.substring(0, 2) || 'en'
|
||||
if (currentLanguage !== lang) {
|
||||
setCurrentLanguage(lang)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[i18n.language, currentLanguage]
|
||||
)
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
setCurrentLanguage(lng)
|
||||
i18n.changeLanguage(lng)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.selectWrapper}>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={currentLanguage}
|
||||
onChange={(e) => changeLanguage(e.target.value)}
|
||||
>
|
||||
<option value='de'>Deutsch</option>
|
||||
<option value='en'>English</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageSelect
|
||||
@@ -0,0 +1,65 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
min-height: rem-calc(48);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include layoutPopover;
|
||||
margin-bottom: space(8) !important;
|
||||
@include padding(2, 3);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: $colorAccent;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.closeBtnSpace {
|
||||
@include padding(0, 10, 0, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@include margin(0, 1);
|
||||
color: $colorSecondary;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.closeNotification {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@include margin(0, 4, 0, 0);
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(13);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
|
||||
import styles from './Notification.module.scss'
|
||||
import { SmallCloseSVG } from './Svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NotificationType } from '../types/enums'
|
||||
|
||||
interface Props {
|
||||
showNotification: boolean
|
||||
content: ReactNode
|
||||
type: NotificationType
|
||||
hideCloseBtn?: boolean
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
showNotification,
|
||||
content,
|
||||
type,
|
||||
hideCloseBtn = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [closeNotification, setCloseNotification] = useState(false)
|
||||
|
||||
const typeClass = useMemo(() => {
|
||||
return type === NotificationType.Warning
|
||||
? styles.warning
|
||||
: type === NotificationType.Error
|
||||
? styles.error
|
||||
: styles.info
|
||||
}, [type])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNotification && !closeNotification ? (
|
||||
<div
|
||||
className={`${styles.notification} ${typeClass} ${
|
||||
!hideCloseBtn ? styles.closeBtnSpace : null
|
||||
}`}
|
||||
>
|
||||
<p>{content}</p>
|
||||
|
||||
{!hideCloseBtn && (
|
||||
<button
|
||||
title={t('common.close')}
|
||||
className={styles.closeNotification}
|
||||
onClick={() => {
|
||||
setCloseNotification(true)
|
||||
}}
|
||||
>
|
||||
<SmallCloseSVG />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
||||
@@ -0,0 +1,78 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
min-height: rem-calc(61);
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: rem-calc(200);
|
||||
flex: 0 0 rem-calc(200);
|
||||
|
||||
.value {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
h5 {
|
||||
span {
|
||||
@include typoM;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
@include margin(2, 0, 0);
|
||||
height: rem-calc(8);
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> .fraction {
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: inline-block;
|
||||
@include margin(0, 0, 0, -1);
|
||||
@include padding(0, 0, 0, 1);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-sizing: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compact {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.compact {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.position {
|
||||
.container {
|
||||
width: rem-calc(120);
|
||||
flex: 0 0 rem-calc(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Tippy from '@tippyjs/react'
|
||||
import styles from './PositionBar.module.scss'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
import { formatValue, lookup } from '../libs/parse'
|
||||
import CollectionHover, { HoverItem } from './CollectionHover'
|
||||
import { ReactElement } from 'react'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../constants/appConstants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
const PositionBar = ({
|
||||
title,
|
||||
value,
|
||||
bars,
|
||||
total,
|
||||
compactView = false,
|
||||
}: StrategyBarProps) => {
|
||||
const { t } = useTranslation()
|
||||
const produceData = (data: StrategyBarItem[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: StrategyBarItem) => {
|
||||
if (asset.value !== 0) {
|
||||
items.push({
|
||||
color: asset.color || '',
|
||||
name: t(`strategy.${asset.name}`) || '',
|
||||
usdValue: lookup(asset.value, UST_DENOM, UST_DECIMALS),
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
const renderBars = (bars: StrategyBarItem[]): ReactElement[] => {
|
||||
const barParts: ReactElement[] = []
|
||||
bars.forEach((item: StrategyBarItem, index: number) => {
|
||||
barParts.push(
|
||||
<div
|
||||
key={index}
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width:
|
||||
item.value === 0
|
||||
? '0%'
|
||||
: ((item.value / total) * 100).toFixed(2) + '%',
|
||||
zIndex: 50 - index,
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return barParts
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
compactView
|
||||
? `${styles.position} ${styles.compact}`
|
||||
: `${styles.position}`
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.value}>
|
||||
<h4>
|
||||
<span>$</span>
|
||||
{formatValue(value)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className={'sub2'}>{title}</span>
|
||||
</div>
|
||||
{bars.length === 0 ? (
|
||||
<div className={styles.bar}>
|
||||
<div
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width: '33%',
|
||||
zIndex: 50 - 1,
|
||||
backgroundColor: colors.transparentWhite,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!(bars.length === 1 && bars[0].value === 0) && (
|
||||
<Tippy
|
||||
className={styles.box}
|
||||
content={
|
||||
<CollectionHover
|
||||
title={title}
|
||||
data={produceData(bars)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.bar}>{renderBars(bars)}</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PositionBar
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
@include margin(8, 0);
|
||||
opacity: 0.6;
|
||||
|
||||
h6 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
flex: auto;
|
||||
@include devider60;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import styles from './Title.module.scss'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
margin?: string
|
||||
}
|
||||
|
||||
const Title = ({ text, margin }: Props) => {
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
<div className={styles.horizontalLine} />
|
||||
<h6 style={{ margin: margin || '0 40px' }}>{text}</h6>
|
||||
<div className={styles.horizontalLine} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Title
|
||||
@@ -0,0 +1,22 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.txFee {
|
||||
@include padding(0, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.3;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
@include margin(0, 1.5, 0, 0);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import styles from './TxFee.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatValue } from '../libs/parse'
|
||||
|
||||
interface Props {
|
||||
txFee: string
|
||||
styleOverride?: any
|
||||
}
|
||||
|
||||
const TxFee = ({ txFee, styleOverride = {} }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={styles.txFee}>
|
||||
<div className={`overline ${styles.label}`} style={styleOverride}>
|
||||
<span>{t('common.txFee')}</span>
|
||||
</div>
|
||||
<div className={`overline ${styles.value}`} style={styleOverride}>
|
||||
{formatValue(txFee, 2, 2, true, false, ' UST', true)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFee
|
||||
@@ -0,0 +1,41 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
gasFeeFormatted: string
|
||||
taxFormatted: string
|
||||
}
|
||||
|
||||
const TxFeeToolTip = ({ gasFeeFormatted, taxFormatted }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ fontSize: '0.8rem' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.gas')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(
|
||||
gasFeeFormatted,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
' UST',
|
||||
true
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.stability')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(taxFormatted, 2, 2, true, false, ' UST', true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFeeToolTip
|
||||
@@ -0,0 +1,113 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
@include margin(12.5, 25, 4);
|
||||
width: rem-calc(230);
|
||||
height: rem-calc(265);
|
||||
border: 1px solid $graphAxis;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
|
||||
.scale {
|
||||
@include typoXXS;
|
||||
opacity: 0.6;
|
||||
line-height: rem-calc(12);
|
||||
position: absolute;
|
||||
width: rem-calc(95);
|
||||
text-align: end;
|
||||
left: rem-calc(-100);
|
||||
letter-spacing: rem-calc(2);
|
||||
|
||||
&.maxY {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.minY {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
bottom: rem-calc(-20);
|
||||
|
||||
&.position {
|
||||
left: rem-calc(6);
|
||||
width: rem-calc(120);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
right: rem-calc(-10);
|
||||
width: rem-calc(95);
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
max-height: 100%;
|
||||
width: rem-calc(36);
|
||||
border-radius: $borderRadiusXS;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
&.supply {
|
||||
left: rem-calc(30);
|
||||
}
|
||||
|
||||
&.borrow {
|
||||
left: rem-calc(75);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
left: rem-calc(172);
|
||||
transition: height 1s ease-out;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
width: rem-calc(34);
|
||||
bottom: rem-calc(12);
|
||||
left: rem-calc(2);
|
||||
text-align: center;
|
||||
@include typoXXScaps;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidation {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
width: rem-calc(105);
|
||||
margin-bottom: space(-3);
|
||||
text-align: end;
|
||||
left: rem-calc(-110);
|
||||
transition: bottom 1s ease-out;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidationLine {
|
||||
display: block;
|
||||
width: rem-calc(110);
|
||||
height: 1px;
|
||||
border-bottom: 2px dashed $graphLiquidationsLine;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transition: bottom 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './BarGraph.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
|
||||
interface barGraphData {
|
||||
bars: number[]
|
||||
labels: string[]
|
||||
classNames: string[]
|
||||
range: number[]
|
||||
liquidation: number
|
||||
legend: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: barGraphData
|
||||
}
|
||||
|
||||
const BarGraph = ({ data }: Props) => {
|
||||
const liquidationPosition = data.liquidation / (data.range[1] / 100)
|
||||
|
||||
const getBarHeightPercentage = (barIndex: number): number => {
|
||||
return Math.floor(
|
||||
(data.bars[barIndex] /
|
||||
Math.max(
|
||||
data.bars[0] || 0,
|
||||
data.bars[1] || 0,
|
||||
data.bars[2] || 0
|
||||
)) *
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span className={`${styles.scale} ${styles.maxY}`}>
|
||||
{formatValue(data.range[1], 2, 2, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.scale} ${styles.minY}`}>
|
||||
{formatValue(data.range[0], 0, 0, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.legend} ${styles.position}`}>
|
||||
{data.legend[0]}
|
||||
</span>
|
||||
{data.bars[2] > 0 && (
|
||||
<span className={`${styles.legend} ${styles.debt}`}>
|
||||
{data.legend[1]}
|
||||
</span>
|
||||
)}
|
||||
{data.bars[0] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.supply} ${data.classNames[0]}`}
|
||||
style={
|
||||
data.bars[0] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(0)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[1] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.borrow} ${data.classNames[1]}`}
|
||||
style={
|
||||
data.bars[1] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(1)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[1]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[2] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.debt} ${data.classNames[2]}`}
|
||||
style={
|
||||
data.bars[2] === 0
|
||||
? { height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(2)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[2]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.liquidation > 0 && (
|
||||
<>
|
||||
<div
|
||||
className={styles.liquidation}
|
||||
style={{
|
||||
bottom: `${
|
||||
liquidationPosition < 13
|
||||
? 13
|
||||
: liquidationPosition
|
||||
}%`,
|
||||
}}
|
||||
>
|
||||
<span>Liquidation threshold</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.liquidationLine}
|
||||
style={{
|
||||
bottom: `${liquidationPosition}%`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BarGraph
|
||||
@@ -0,0 +1,118 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.progressbarContainer {
|
||||
position: relative;
|
||||
height: rem-calc(22);
|
||||
|
||||
.progressbar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@include bgHatched;
|
||||
box-shadow: $shadowInset;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
}
|
||||
|
||||
.limitLine {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: $colorInfoWarning;
|
||||
transition: left 2s;
|
||||
box-shadow: $shadowInset;
|
||||
}
|
||||
|
||||
.limit {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: $colorGreyDark;
|
||||
box-shadow: $shadowInset;
|
||||
border-top-left-radius: $borderRadiusXXXL;
|
||||
border-bottom-left-radius: $borderRadiusXXXL;
|
||||
transition: width 2s;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: rem-calc(3);
|
||||
height: rem-calc(3);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-1.75);
|
||||
margin-inline-start: space(-0.25);
|
||||
border-radius: $borderRadiusRound;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.dotGlow {
|
||||
position: absolute;
|
||||
width: rem-calc(7);
|
||||
height: rem-calc(7);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-2.25);
|
||||
margin-inline-start: space(-1.25);
|
||||
border-radius: $borderRadiusXS;
|
||||
@include glowM;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.ltvContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.mask {
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
border-radius: $borderRadiusL;
|
||||
transition: width 2s;
|
||||
color: $fontColorLtv;
|
||||
|
||||
> span {
|
||||
transition: left 2s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
opacity: 0.4;
|
||||
transition: width 2s;
|
||||
@include glowXXL;
|
||||
@include margin(-5.5, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
margin-top: space(1.25);
|
||||
|
||||
.zero {
|
||||
text-align: start;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.limit {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
import styles from './BorrowLimit.module.scss'
|
||||
import { addDecimals } from '../../libs/math'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
|
||||
|
||||
interface Props {
|
||||
width: string
|
||||
ltv: number
|
||||
maxLtv: number
|
||||
liquidationThreshold: number
|
||||
barHeight: string
|
||||
showPercentageText: boolean
|
||||
showTitleText: boolean
|
||||
showLegend?: boolean
|
||||
top?: number
|
||||
percentageThreshold?: number
|
||||
percentageOffset?: number
|
||||
title?: string
|
||||
mode?: string
|
||||
criticalIndicator?: number
|
||||
}
|
||||
|
||||
const BorrowLimit = ({
|
||||
width,
|
||||
ltv,
|
||||
maxLtv,
|
||||
liquidationThreshold,
|
||||
barHeight = '22px',
|
||||
showPercentageText = true,
|
||||
showLegend = true,
|
||||
top = 4,
|
||||
showTitleText = true,
|
||||
percentageThreshold = 15,
|
||||
percentageOffset = 45,
|
||||
title = 'Borrowing Capacity',
|
||||
mode = 'default',
|
||||
criticalIndicator,
|
||||
}: Props) => {
|
||||
const ltvPercent =
|
||||
+(((ltv || 0) / (liquidationThreshold || 0)) * 100).toFixed(2) || 0
|
||||
|
||||
const ltvPercentRounded = +(Math.round(ltvPercent * 100) / 100).toFixed(1)
|
||||
const ltvPercentRestrained =
|
||||
ltvPercent > 100 ? 100 : ltvPercent < 0 ? 0 : ltvPercent
|
||||
const ltvPercentMargin =
|
||||
ltvPercent > percentageThreshold
|
||||
? `-${percentageOffset + 15}px`
|
||||
: '10px'
|
||||
const maxBorrowPercent =
|
||||
criticalIndicator || (maxLtv / liquidationThreshold) * 100
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div style={{ width: width }}>
|
||||
{showTitleText ? (
|
||||
<div className={`overline ${styles.title}`}>{title}</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{ height: barHeight }}
|
||||
className={styles.progressbarContainer}
|
||||
>
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.progressbar}
|
||||
>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.limitLine}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${maxBorrowPercent}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.limit}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dot}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dotGlow}
|
||||
/>
|
||||
|
||||
<div className={styles.ltvContainer}>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.mask}
|
||||
>
|
||||
{showPercentageText ? (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${ltvPercentRestrained}%`,
|
||||
top: `${top}px`,
|
||||
marginLeft: ltvPercentMargin,
|
||||
width: `${percentageOffset + 8}px`,
|
||||
textAlign:
|
||||
ltvPercent > percentageThreshold
|
||||
? 'right'
|
||||
: 'left',
|
||||
}}
|
||||
className='overline'
|
||||
>
|
||||
{ltv < 0 ? (
|
||||
'0%'
|
||||
) : (
|
||||
<>
|
||||
{`${
|
||||
mode === 'default'
|
||||
? ltvPercentRounded
|
||||
: addDecimals(ltv)
|
||||
}%`}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.indicator}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.glow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLegend && (
|
||||
<div className={`overline ${styles.values}`}>
|
||||
<div className={styles.zero}>
|
||||
{mode === 'default' ? '$0' : '0%'}
|
||||
</div>
|
||||
<div className={styles.limit}>
|
||||
{mode === 'default' ? (
|
||||
<span>
|
||||
{formatValue(
|
||||
lookup(maxLtv, UST_DENOM, UST_DECIMALS),
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'$'
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
formatValue(
|
||||
liquidationThreshold,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BorrowLimit
|
||||
@@ -0,0 +1,5 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTile;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
styleOverride?: object
|
||||
}
|
||||
|
||||
const Card = ({ children, styleOverride }: Props) => {
|
||||
return (
|
||||
<div style={styleOverride} className={styles.container}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
@@ -0,0 +1,48 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
|
||||
.innerText {
|
||||
position: absolute;
|
||||
top: 47.17%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 58.66%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: rem-calc(110);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) and (min-width: $bpMediumLow) {
|
||||
.container {
|
||||
@include padding(3, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { Doughnut, defaults } from 'react-chartjs-2'
|
||||
|
||||
import styles from './DonutGraph.module.scss'
|
||||
import DonutHover from './charts/DonutHover'
|
||||
import { producePercentData } from '../../libs/assetInfo'
|
||||
import Tippy from '@tippyjs/react'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
defaults.global.defaultFontFamily = 'Inter'
|
||||
|
||||
function hexToRgb(hex: any) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
// Create a gradient to use on our chart. This is essentially a circular border around the slice(s)
|
||||
function createGrad(
|
||||
ctx: any,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
colorOne: string,
|
||||
colorTwo = '#1b1d24',
|
||||
opacityTwo = 0.35
|
||||
) {
|
||||
let opacityOne = 1,
|
||||
colorStop = 92
|
||||
var rgbOne = hexToRgb(colorOne)
|
||||
var rgbTwo = hexToRgb(colorTwo)
|
||||
colorStop = colorStop / 100
|
||||
|
||||
var grd = ctx
|
||||
.getContext('2d')
|
||||
.createRadialGradient(
|
||||
canvasWidth / 2,
|
||||
canvasHeight / 2,
|
||||
0.0,
|
||||
canvasWidth / 2,
|
||||
canvasHeight / 2,
|
||||
canvasHeight / 2
|
||||
)
|
||||
|
||||
if (rgbOne == null || rgbTwo == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Add color stops to the gradient
|
||||
grd.addColorStop(
|
||||
0.0,
|
||||
'rgba(' +
|
||||
rgbOne.r +
|
||||
', ' +
|
||||
rgbOne.g +
|
||||
', ' +
|
||||
rgbOne.b +
|
||||
', ' +
|
||||
opacityOne +
|
||||
')'
|
||||
)
|
||||
grd.addColorStop(
|
||||
colorStop,
|
||||
'rgba(' +
|
||||
rgbOne.r +
|
||||
', ' +
|
||||
rgbOne.g +
|
||||
', ' +
|
||||
rgbOne.b +
|
||||
', ' +
|
||||
opacityOne +
|
||||
')'
|
||||
)
|
||||
grd.addColorStop(
|
||||
0.94,
|
||||
'rgba(' +
|
||||
rgbTwo.r +
|
||||
', ' +
|
||||
rgbTwo.g +
|
||||
', ' +
|
||||
rgbTwo.b +
|
||||
', ' +
|
||||
opacityTwo +
|
||||
')'
|
||||
) // required to prevent jagged edges
|
||||
grd.addColorStop(
|
||||
1.0,
|
||||
'rgba(' +
|
||||
rgbTwo.r +
|
||||
', ' +
|
||||
rgbTwo.g +
|
||||
', ' +
|
||||
rgbTwo.b +
|
||||
', ' +
|
||||
opacityTwo +
|
||||
')'
|
||||
)
|
||||
|
||||
return grd
|
||||
}
|
||||
|
||||
function generateColors(mychart: any, defaultColors: any[]) {
|
||||
if (mychart == null) {
|
||||
return defaultColors
|
||||
}
|
||||
|
||||
return defaultColors.map((color) =>
|
||||
createGrad(
|
||||
mychart.chartInstance.ctx.canvas,
|
||||
mychart.chartInstance.width,
|
||||
mychart.chartInstance.height,
|
||||
color,
|
||||
'#1b1d24',
|
||||
0.0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function generateHoverColors(mychart: any, defaultColors: any[]) {
|
||||
if (mychart == null) {
|
||||
return defaultColors
|
||||
}
|
||||
|
||||
return defaultColors.map((color) =>
|
||||
createGrad(
|
||||
mychart.chartInstance.ctx.canvas,
|
||||
mychart.chartInstance.width,
|
||||
mychart.chartInstance.height,
|
||||
color,
|
||||
color
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
placeholderTitle?: string
|
||||
value: number
|
||||
cutoutPercentage: number
|
||||
labels: string[]
|
||||
data: AssetInfo[]
|
||||
colors: string[]
|
||||
expandableOnHover?: boolean
|
||||
labelsOnHover?: boolean
|
||||
styleOverride: Object
|
||||
}
|
||||
|
||||
const DonutGraph = ({
|
||||
title,
|
||||
value,
|
||||
cutoutPercentage,
|
||||
labels,
|
||||
data,
|
||||
colors,
|
||||
expandableOnHover = true,
|
||||
labelsOnHover = true,
|
||||
styleOverride,
|
||||
}: Props) => {
|
||||
const [showToolTip, setShowToolTip] = useState(false)
|
||||
const [myChart, setMyChart] = useState()
|
||||
const _chartRef: any = React.createRef()
|
||||
|
||||
useEffect(() => {
|
||||
setMyChart(_chartRef.current)
|
||||
}, [_chartRef])
|
||||
const colorsToAdd = generateColors(myChart, colors)
|
||||
const hoverColors = generateHoverColors(myChart, colors)
|
||||
|
||||
const percentData = useMemo(() => producePercentData(data), [data])
|
||||
|
||||
// Set up our dataset based on whether we require expandable sections on hover
|
||||
const chartHasData = useMemo(() => data.some((d) => !!d), [data])
|
||||
|
||||
const datasetToAdd = chartHasData
|
||||
? [
|
||||
{
|
||||
data: percentData,
|
||||
backgroundColor: expandableOnHover ? colorsToAdd : colors,
|
||||
hoverBackgroundColor: expandableOnHover
|
||||
? hoverColors
|
||||
: colors,
|
||||
borderWidth: 0,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
data: [100],
|
||||
backgroundColor: expandableOnHover
|
||||
? generateColors(myChart, ['#3a3c49'])
|
||||
: ['#3a3c49'],
|
||||
hoverBackgroundColor: expandableOnHover
|
||||
? generateHoverColors(myChart, ['#3a3c49'])
|
||||
: ['#3a3c49'],
|
||||
borderWidth: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const chartData = {
|
||||
labels: labels,
|
||||
datasets: datasetToAdd,
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
custom: (tooltipModel: any) => {
|
||||
if (!chartHasData || !labelsOnHover) return
|
||||
// if chart is not defined, return early
|
||||
var chart: any = _chartRef.current
|
||||
if (!chart) {
|
||||
return
|
||||
}
|
||||
|
||||
// hide the tooltip when chartjs determines you've hovered out
|
||||
if (tooltipModel.opacity <= 0) {
|
||||
setShowToolTip(false)
|
||||
return
|
||||
}
|
||||
setShowToolTip(true)
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
cutoutPercentage: cutoutPercentage,
|
||||
maintainAspectRatio: true,
|
||||
circumference: Math.PI * 2,
|
||||
rotation: 0,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{showToolTip && expandableOnHover && (
|
||||
<Tippy content={<DonutHover title={title} data={data} />}>
|
||||
<div className={styles.overlay} />
|
||||
</Tippy>
|
||||
)}
|
||||
<div style={styleOverride}>
|
||||
<Doughnut
|
||||
data={chartData}
|
||||
options={chartOptions}
|
||||
ref={_chartRef}
|
||||
/>
|
||||
<div className={styles.innerText}>
|
||||
{
|
||||
<div>
|
||||
<span>$</span>
|
||||
<span className={'h4'}>
|
||||
{formatValue(Number(value), 2, 2, true, false)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
<p className={'sub2'}>{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DonutGraph
|
||||
@@ -0,0 +1,5 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { UST_DENOM, UST_DECIMALS } from '../../../constants/appConstants'
|
||||
import { lookup } from '../../../libs/parse'
|
||||
import CollectionHover, { HoverItem } from '../../CollectionHover'
|
||||
|
||||
interface Props {
|
||||
data: AssetInfo[]
|
||||
title: string
|
||||
}
|
||||
|
||||
const DonutHover = ({ data, title }: Props) => {
|
||||
const produceData = (data: AssetInfo[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: AssetInfo) => {
|
||||
if (Number(asset.uusdBalance) > 0) {
|
||||
items.push({
|
||||
color: asset.color || '',
|
||||
name: asset.symbol || '',
|
||||
amount: lookup(
|
||||
Number(asset.balance),
|
||||
asset.denom,
|
||||
asset.decimals
|
||||
),
|
||||
usdValue: lookup(
|
||||
asset.uusdBalance || 0,
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
),
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
return <CollectionHover title={title} data={produceData(data)} />
|
||||
}
|
||||
|
||||
export default DonutHover
|
||||
@@ -0,0 +1,17 @@
|
||||
interface Props {
|
||||
symbol: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const Asset = ({ symbol, name }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div>{symbol}</div>
|
||||
<div style={{ opacity: 0.6 }} className='caption'>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Asset
|
||||
@@ -0,0 +1,47 @@
|
||||
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
|
||||
interface Props {
|
||||
denom: string
|
||||
decimals: number
|
||||
amount: number
|
||||
uusdAmount: number
|
||||
}
|
||||
|
||||
const CellAmount = ({ denom, decimals, amount, uusdAmount }: Props) => {
|
||||
const usdAmount = lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
|
||||
const assetAmount = lookup(amount, denom, decimals)
|
||||
return (
|
||||
<div>
|
||||
{denom !== 'uusd'
|
||||
? formatValue(
|
||||
assetAmount > 0 && assetAmount < 0.01
|
||||
? 0.01
|
||||
: assetAmount,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
assetAmount > 0 && assetAmount < 0.01 ? '< ' : false
|
||||
)
|
||||
: null}
|
||||
<div
|
||||
style={{
|
||||
opacity: denom !== 'uusd' ? 0.6 : 1,
|
||||
}}
|
||||
className={denom !== 'uusd' ? 'caption' : ''}
|
||||
>
|
||||
<span>
|
||||
{formatValue(
|
||||
usdAmount > 0 && usdAmount < 0.01 ? 0.01 : usdAmount,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
usdAmount > 0 && usdAmount < 0.01 ? '< $' : '$'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CellAmount
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useTable, useSortBy } from 'react-table'
|
||||
import { useEffect } from 'react'
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
const Grid = ({
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
hideheader = false,
|
||||
updateSort,
|
||||
}) => {
|
||||
const {
|
||||
columns: instanceColumns,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
toggleSortBy,
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
autoResetSortBy: false,
|
||||
disableSortRemove: true,
|
||||
disableMultiSort: true,
|
||||
sortDescFirst: true,
|
||||
},
|
||||
useSortBy
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
instanceColumns.forEach((column) => {
|
||||
if (column.isSorted) {
|
||||
if (
|
||||
initialState.sortBy[0].id !== column.id ||
|
||||
initialState.sortBy[0].desc !== column.isSortedDesc
|
||||
)
|
||||
updateSort({
|
||||
sortBy: [
|
||||
{
|
||||
id: column.id,
|
||||
desc: column.isSortedDesc,
|
||||
},
|
||||
],
|
||||
})
|
||||
return false
|
||||
}
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[rows]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
instanceColumns.forEach((column) => {
|
||||
let cleared = false
|
||||
if (column.isSorted) {
|
||||
if (
|
||||
initialState.sortBy[0].id !== column.id ||
|
||||
initialState.sortBy[0].desc !== column.isSortedDesc
|
||||
) {
|
||||
cleared = true
|
||||
column.clearSortBy()
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared) {
|
||||
toggleSortBy(
|
||||
initialState.sortBy[0].id,
|
||||
initialState.sortBy[0].desc,
|
||||
false
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[initialState]
|
||||
)
|
||||
return (
|
||||
<table
|
||||
{...getTableProps()}
|
||||
style={{
|
||||
tableLayout: 'fixed',
|
||||
borderSpacing: '0px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{headerGroups[0].headers.map((column) => {
|
||||
return (
|
||||
<th
|
||||
{...column.getHeaderProps(
|
||||
column.getSortByToggleProps()
|
||||
)}
|
||||
style={{
|
||||
borderTop: hideheader
|
||||
? 'none'
|
||||
: `1px solid ${colors.tableBorder}`,
|
||||
borderBottom: `1px solid ${colors.tableBorder}`,
|
||||
cursor: column.disableSortBy
|
||||
? 'default'
|
||||
: 'pointer',
|
||||
width: column.width,
|
||||
color: colors.tableHeader, // Overwrite the .caption color to be 50% transparent
|
||||
}}
|
||||
className='caption'
|
||||
>
|
||||
{column.hideHeader || hideheader ? null : (
|
||||
<div
|
||||
style={{
|
||||
padding:
|
||||
column.headingPaddingOverride
|
||||
? column.headingPaddingOverride
|
||||
: '8px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
whiteSpace:
|
||||
column.whiteSpace ||
|
||||
'nowrap',
|
||||
textAlign:
|
||||
column.textAlign ||
|
||||
'inherit',
|
||||
justifyContent:
|
||||
column.textAlign === 'right'
|
||||
? 'flex-end'
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
{column.textAlign !== 'right' &&
|
||||
column.render('Header')}
|
||||
{!column.disableSortBy ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin:
|
||||
column.textAlign ===
|
||||
'right'
|
||||
? 'auto 7px auto 0'
|
||||
: 'auto 0 auto 7px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft:
|
||||
'4px solid transparent',
|
||||
borderRight:
|
||||
'4px solid transparent',
|
||||
borderBottom:
|
||||
!column.disableSortBy
|
||||
? !column.isSortedDesc &&
|
||||
column.isSorted
|
||||
? `4px solid ${colors.tableSortActive}`
|
||||
: `4px solid ${colors.tableSort}`
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
marginTop: '3px',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft:
|
||||
'4px solid transparent',
|
||||
borderRight:
|
||||
'4px solid transparent',
|
||||
borderTop:
|
||||
!column.disableSortBy
|
||||
? column.isSortedDesc &&
|
||||
column.isSorted
|
||||
? `4px solid ${colors.tableSortActive}`
|
||||
: `4px solid ${colors.tableSort}`
|
||||
: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{column.textAlign === 'right' &&
|
||||
column.render('Header')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<tr {...row.getRowProps()}>
|
||||
{row.cells.map((cell) => {
|
||||
return (
|
||||
<td
|
||||
{...cell.getCellProps()}
|
||||
style={{
|
||||
textAlign: cell.column.textAlign,
|
||||
height: '53px',
|
||||
width: cell.column.width,
|
||||
padding: cell.column.paddingOverride
|
||||
? cell.column.paddingOverride
|
||||
: '0 8px',
|
||||
borderBottom: `1px solid ${colors.tableBorder}`,
|
||||
overflow: cell.column.showOverflow
|
||||
? 'visable'
|
||||
: 'hidden',
|
||||
}}
|
||||
className='body2'
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export default Grid
|
||||
@@ -0,0 +1,191 @@
|
||||
import OverflowDropdown from '../../components/grid/dropdown/OverflowDropdown'
|
||||
import useActionButtonClickHandler from '../../hooks/useActionButtonClickHandler'
|
||||
import { useRedBank, useAccountBalance } from '../../hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DropdownItemProps } from '../../types/components'
|
||||
import { useHistory, useLocation } from 'react-router'
|
||||
import { ASTROPORT_URL } from '../../constants/appConstants'
|
||||
import useStore from '../../store'
|
||||
import Button from '../Button'
|
||||
import { ExternalSVG } from '../Svg'
|
||||
import { getRoute } from '../../libs/parse'
|
||||
import { ActionType } from '../../types/enums'
|
||||
|
||||
export enum GridActionType {
|
||||
BorrowAction,
|
||||
DepositAction,
|
||||
WithdrawAction,
|
||||
ManageAction,
|
||||
RepayAction,
|
||||
None,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
denom: string
|
||||
menuItems: DropdownItemProps[]
|
||||
actionType: GridActionType
|
||||
strategy?: StrategyObject
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const GridActions = ({
|
||||
denom,
|
||||
menuItems,
|
||||
actionType,
|
||||
strategy,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const newMenuItems = [...menuItems]
|
||||
const mainButton = newMenuItems[0]
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
|
||||
const { find, findDeposit } = useAccountBalance()
|
||||
const { findLiquidity, findMarketInfo } = useRedBank()
|
||||
const provideClickHandler = useActionButtonClickHandler()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
const swapUrl =
|
||||
denom === 'uusd'
|
||||
? `${ASTROPORT_URL}?from=uluna&to=uusd`
|
||||
: denom === 'uluna'
|
||||
? `${ASTROPORT_URL}?from=uusd&to=uluna`
|
||||
: `${ASTROPORT_URL}?from=uusd&to=${
|
||||
whitelistedAssets?.find((asset) => asset.denom === denom)
|
||||
?.contract_addr
|
||||
}`
|
||||
|
||||
const marketInfo = findMarketInfo(denom)
|
||||
|
||||
const buyClickHandler = provideClickHandler(
|
||||
ActionType.ExternalLink,
|
||||
denom,
|
||||
GridActionType.None,
|
||||
swapUrl
|
||||
)
|
||||
|
||||
let dropdownMenuItems = newMenuItems.slice(1, menuItems.length)
|
||||
|
||||
let useBuyButton = false
|
||||
if (actionType === GridActionType.DepositAction) {
|
||||
const assetWallet = find(denom)
|
||||
|
||||
if (Number(assetWallet?.amount || 0) <= 0) useBuyButton = true
|
||||
}
|
||||
|
||||
let disabledDepositButton: boolean = false
|
||||
// If the market has had deposits disabled then disable deposit button
|
||||
if (
|
||||
actionType === GridActionType.DepositAction &&
|
||||
!marketInfo?.deposit_enabled
|
||||
) {
|
||||
disabledDepositButton = true
|
||||
}
|
||||
// Remove deposit from submenu if it's disabled (applicable to My Station's grid)
|
||||
if (
|
||||
actionType === GridActionType.WithdrawAction &&
|
||||
!marketInfo?.deposit_enabled
|
||||
) {
|
||||
dropdownMenuItems = dropdownMenuItems.filter(
|
||||
(item) => item.gridAction !== GridActionType.DepositAction
|
||||
)
|
||||
}
|
||||
|
||||
let disableBorrowButton: boolean = false
|
||||
if (actionType === GridActionType.BorrowAction) {
|
||||
// If the market has had borrows disabled then disable borrow button
|
||||
if (!marketInfo?.borrow_enabled) {
|
||||
disableBorrowButton = true
|
||||
}
|
||||
// If the user has no collateral then disable borrow button
|
||||
if (!disableBorrowButton && whitelistedAssets?.length) {
|
||||
let depositSum: number = 0
|
||||
whitelistedAssets.forEach((asset) => {
|
||||
depositSum += Number(findDeposit(asset.denom)?.amount || 0)
|
||||
})
|
||||
|
||||
if (depositSum <= 0) disableBorrowButton = true
|
||||
}
|
||||
// if the market has no liquidity available then disable borrow button
|
||||
if (!disableBorrowButton) {
|
||||
const assetLiquidity = findLiquidity(denom)
|
||||
|
||||
if (Number(assetLiquidity?.amount || 0) <= 0)
|
||||
disableBorrowButton = true
|
||||
}
|
||||
}
|
||||
// Remove borrow from submenu if it's disabled (applicable to My Station's grid)
|
||||
if (
|
||||
actionType === GridActionType.RepayAction &&
|
||||
!marketInfo?.borrow_enabled
|
||||
) {
|
||||
dropdownMenuItems = dropdownMenuItems.filter(
|
||||
(item) => item.gridAction !== GridActionType.BorrowAction
|
||||
)
|
||||
}
|
||||
|
||||
// Disabled prop is used to disable all actions of a market on the UI, i.e. on the not connected screens where we show a dummie grid
|
||||
// The !marketInfo?.active flag determines if all actions against the market has been disabled on the SC level
|
||||
const isAllActionsDisabled: boolean = disabled || !marketInfo?.active
|
||||
const isDropdownDisabled: boolean =
|
||||
useBuyButton &&
|
||||
dropdownMenuItems.length === 1 &&
|
||||
dropdownMenuItems[0].title === 'Swap'
|
||||
const isPrimaryActionDisabled: boolean =
|
||||
disabledDepositButton || disableBorrowButton
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{useBuyButton ? (
|
||||
<Button
|
||||
text={t('common.buy')}
|
||||
suffix={<ExternalSVG />}
|
||||
color='secondary'
|
||||
onClick={buyClickHandler}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{strategy ? (
|
||||
<Button
|
||||
text={mainButton?.title || ''}
|
||||
onClick={() => {
|
||||
history.push(`/fields/${actionType}`)
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text={mainButton?.title || ''}
|
||||
disabled={
|
||||
isAllActionsDisabled || isPrimaryActionDisabled
|
||||
}
|
||||
onClick={() => {
|
||||
history.push(
|
||||
`${location.pathname}/${getRoute(
|
||||
actionType
|
||||
)}/${denom}`
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<OverflowDropdown
|
||||
menuItems={dropdownMenuItems}
|
||||
denom={denom}
|
||||
strategy={strategy}
|
||||
disabled={isAllActionsDisabled || isDropdownDisabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GridActions
|
||||
@@ -0,0 +1,13 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include padding(1, 4);
|
||||
display: flex;
|
||||
flex: auto;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: $alphaWhite10;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useHistory } from 'react-router'
|
||||
import useActionButtonClickHandler from '../../../hooks/useActionButtonClickHandler'
|
||||
import { DropdownItemProps } from '../../../types/components'
|
||||
import styles from './DropdownItem.module.scss'
|
||||
|
||||
const DropdownItem = ({
|
||||
icon,
|
||||
title,
|
||||
denom,
|
||||
actionType,
|
||||
gridAction,
|
||||
url,
|
||||
close,
|
||||
strategy,
|
||||
}: DropdownItemProps) => {
|
||||
const provideClickHandler = useActionButtonClickHandler()
|
||||
const history = useHistory()
|
||||
const clickHandler = strategy
|
||||
? () => {
|
||||
history.push(`/fields/strategy/${strategy.key}`)
|
||||
}
|
||||
: provideClickHandler(actionType, denom, gridAction, url)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
onClick={() => {
|
||||
clickHandler()
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
<span className='body2'>{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropdownItem
|
||||
@@ -0,0 +1,9 @@
|
||||
@use '../../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
margin-inline-start: space(1);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useState } from 'react'
|
||||
import Tippy, { TippyProps } from '@tippyjs/react'
|
||||
import DropdownItem from './DropdownItem'
|
||||
import MoreHorizIcon from '@material-ui/icons/MoreHoriz'
|
||||
|
||||
import styles from './OverflowDropdown.module.scss'
|
||||
import { DropdownItemProps } from '../../../types/components'
|
||||
import Button from '../../Button'
|
||||
|
||||
export const DefaultTippyProps: TippyProps = {
|
||||
animation: false,
|
||||
interactive: true,
|
||||
appendTo: document.body,
|
||||
}
|
||||
|
||||
export const DropdownTippyProps: TippyProps = {
|
||||
...DefaultTippyProps,
|
||||
placement: 'bottom-end',
|
||||
}
|
||||
|
||||
interface OverflowDropdownProps {
|
||||
menuItems: DropdownItemProps[]
|
||||
denom: string
|
||||
strategy?: StrategyObject
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const OverflowDropdown = ({
|
||||
menuItems,
|
||||
denom,
|
||||
strategy,
|
||||
disabled = false,
|
||||
}: OverflowDropdownProps) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const show = () => setVisible(true)
|
||||
const hide = () => setVisible(false)
|
||||
const renderDropdown = () => (
|
||||
<div className={styles.container}>
|
||||
{menuItems.map((menuItem, index) => (
|
||||
<DropdownItem
|
||||
key={index}
|
||||
icon={menuItem.icon}
|
||||
title={menuItem.title}
|
||||
denom={denom}
|
||||
actionType={menuItem.actionType}
|
||||
gridAction={menuItem.gridAction}
|
||||
url={menuItem.url}
|
||||
close={hide}
|
||||
strategy={strategy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (menuItems.length) {
|
||||
return (
|
||||
<div>
|
||||
<Tippy
|
||||
{...DropdownTippyProps}
|
||||
render={renderDropdown}
|
||||
visible={visible}
|
||||
onClickOutside={hide}
|
||||
>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
color='tertiary'
|
||||
variant='round'
|
||||
prefix={
|
||||
<MoreHorizIcon
|
||||
style={{
|
||||
width: '1.2rem',
|
||||
height: '1.2rem',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={visible ? hide : show}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default OverflowDropdown
|
||||
@@ -0,0 +1,156 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button,
|
||||
.disabledButton,
|
||||
.secondary {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 4, 0);
|
||||
@include typoButton;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
text-transform: uppercase;
|
||||
|
||||
> span {
|
||||
@include margin(0, 0, 0, 2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $alphaWhite60;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
@include margin(-0.5, 0, 0);
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.disabledButton:hover {
|
||||
background: $alphaWhite30;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: $colorSecondaryHighlight;
|
||||
border: none;
|
||||
@include padding(0.5, 5.5);
|
||||
height: rem-calc(36);
|
||||
|
||||
&:hover {
|
||||
background: $colorSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
@include padding(6, 6, 5.5);
|
||||
@include layoutPopover;
|
||||
width: rem-calc(360);
|
||||
max-width: 100vw;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.option {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: $colorAccent;
|
||||
border: none;
|
||||
appearance: none;
|
||||
@include typoL;
|
||||
@include padding(2, 0);
|
||||
font-weight: $fontWeightRegular;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 100%;
|
||||
text-decoration: none;
|
||||
|
||||
.image {
|
||||
opacity: 0.6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: $colorSecondaryHighlight;
|
||||
|
||||
.image {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
@include margin(0, 2, 0, 0);
|
||||
|
||||
svg,
|
||||
img {
|
||||
width: rem-calc(24);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@include margin(-1.5, 0, 0, 1);
|
||||
|
||||
svg {
|
||||
fill: $colorAccent;
|
||||
opacity: 0.5;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.dropdown {
|
||||
&:not(.centered) {
|
||||
right: 0;
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
+ .dropdown {
|
||||
right: unset;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.dropdown {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import styles from './ConnectButton.module.scss'
|
||||
import { WalletSVG } from '../Svg'
|
||||
import { ClickAwayListener } from '@material-ui/core'
|
||||
import { ConnectType, useWallet } from '@terra-money/wallet-provider'
|
||||
import * as rdd from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
textOverride?: string | ReactNode
|
||||
disabled?: boolean
|
||||
color?: string
|
||||
centered?: boolean
|
||||
}
|
||||
|
||||
const ConnectButton = ({
|
||||
textOverride,
|
||||
disabled = false,
|
||||
color,
|
||||
centered = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [openConnectList, setOpenConnectList] = useState(false)
|
||||
|
||||
const { connect, availableConnections, availableInstallations } =
|
||||
useWallet()
|
||||
|
||||
const onClickAway = useCallback(() => {
|
||||
setOpenConnectList(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<button
|
||||
className={
|
||||
disabled
|
||||
? styles.disabledButton
|
||||
: color
|
||||
? styles[color]
|
||||
: styles.button
|
||||
}
|
||||
onClick={() => {
|
||||
rdd.isMobile
|
||||
? connect(ConnectType.WALLETCONNECT)
|
||||
: setOpenConnectList(!openConnectList)
|
||||
}}
|
||||
>
|
||||
<WalletSVG />
|
||||
<span className='overline'>
|
||||
{textOverride || t('common.connectWallet')}
|
||||
</span>
|
||||
</button>
|
||||
{openConnectList && (
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div
|
||||
className={
|
||||
centered
|
||||
? `${styles.dropdown} ${styles.centered}`
|
||||
: styles.dropdown
|
||||
}
|
||||
>
|
||||
{!rdd.isMobile && (
|
||||
<>
|
||||
{availableConnections
|
||||
.filter(
|
||||
({ type }) =>
|
||||
type !== ConnectType.READONLY
|
||||
)
|
||||
.map(({ type, name, identifier }) => {
|
||||
if (
|
||||
(type === ConnectType.EXTENSION &&
|
||||
identifier === 'station') ||
|
||||
type === ConnectType.WALLETCONNECT
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
key={
|
||||
'connection' +
|
||||
type +
|
||||
identifier
|
||||
}
|
||||
className={styles.option}
|
||||
onClick={() => {
|
||||
connect(
|
||||
type,
|
||||
identifier
|
||||
)
|
||||
setOpenConnectList(
|
||||
false
|
||||
)
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
|
||||
{availableInstallations
|
||||
.filter(
|
||||
({ type }) =>
|
||||
type === ConnectType.EXTENSION
|
||||
)
|
||||
.map(({ type, identifier, name, url }) => {
|
||||
if (
|
||||
type === ConnectType.EXTENSION &&
|
||||
identifier === 'station'
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
key={
|
||||
'installation' +
|
||||
type +
|
||||
identifier
|
||||
}
|
||||
className={styles.option}
|
||||
href={url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
onClick={() => {
|
||||
setOpenConnectList(
|
||||
false
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('common.installName', {
|
||||
name: name,
|
||||
})}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectButton
|
||||
@@ -0,0 +1,227 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 2, 0, 3);
|
||||
@include typoS;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
|
||||
.walletIcon {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
svg {
|
||||
margin-top: space(-1);
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.address {
|
||||
margin-inline-start: space(1.5);
|
||||
font-weight: $fontWeightRegular;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: $fontWeightRegular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-inline-start: space(2);
|
||||
padding-inline-start: rem-calc(8);
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
border-left: 1px solid $alphaWhite60;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $colorWhite;
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
|
||||
.circularProgress {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
@include padding(6, 6, 5.5);
|
||||
@include layoutPopover;
|
||||
width: rem-calc(420);
|
||||
left: rem-calc(-233);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.detailsBalance {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
@include margin(-1, 4, 0, 0);
|
||||
height: space(6);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
@include typoH4;
|
||||
color: $colorSecondaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(116);
|
||||
width: rem-calc(116);
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
|
||||
.address,
|
||||
.addressMobile,
|
||||
.addressLabel,
|
||||
.tnsAddress {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 1;
|
||||
@include margin(0, 0, 1);
|
||||
@include typoS;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.addressMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
@include margin(-1, 1, 0, 0);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
@include padding(1, 0, 0);
|
||||
|
||||
> button {
|
||||
font-weight: $fontWeightRegular;
|
||||
|
||||
&:first-child {
|
||||
width: rem-calc(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
color: $colorSecondaryDark;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include padding(2, 0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network {
|
||||
background-color: $colorSecondaryHighlight;
|
||||
text-transform: uppercase;
|
||||
border-radius: $borderRadiusL;
|
||||
@include padding(0, 2);
|
||||
@include margin(0);
|
||||
@include typoNetwork;
|
||||
position: absolute;
|
||||
top: space(-4);
|
||||
right: space(-4);
|
||||
cursor: default;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.details {
|
||||
right: 0;
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
@include margin(4, 0, 0);
|
||||
flex: 1 0 100%;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.network {
|
||||
right: rem-calc(-12);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.details {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
.addressMobile {
|
||||
display: block;
|
||||
}
|
||||
.address {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useAccountBalance, useTNS } from '../../hooks'
|
||||
import { truncate } from '../../libs/text'
|
||||
import styles from './ConnectedButton.module.scss'
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '../Button'
|
||||
import useClipboard from 'react-use-clipboard'
|
||||
import { CheckSVG, CopySVG, ExternalSVG, WalletSVG } from '../Svg'
|
||||
import colors from '../../styles/_assets.module.scss'
|
||||
import { CircularProgress, ClickAwayListener } from '@material-ui/core'
|
||||
import { useWallet } from '@terra-money/wallet-provider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FINDER_URL,
|
||||
UST_DECIMALS,
|
||||
UST_DENOM,
|
||||
} from '../../constants/appConstants'
|
||||
import { State } from '../../types/enums'
|
||||
import useStore from '../../store'
|
||||
|
||||
interface Props {
|
||||
address: string
|
||||
}
|
||||
|
||||
const ConnectedButton = ({ address }: Props) => {
|
||||
const { find, state } = useAccountBalance()
|
||||
const chainID = useStore((s) => s.networkConfig?.chainID)
|
||||
const name = useStore((s) => s.networkConfig?.name)
|
||||
const { disconnect } = useWallet()
|
||||
const uusdAmount = Number(find(UST_DENOM)?.amount || 0)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const [isCopied, setCopied] = useClipboard(address, {
|
||||
successDuration: 1000 * 5,
|
||||
})
|
||||
|
||||
const viewOnFinder = useCallback(() => {
|
||||
window.open(`${FINDER_URL}/${chainID}/account/${address}`, '_blank')
|
||||
}, [chainID, address])
|
||||
|
||||
const onClickAway = useCallback(() => {
|
||||
setShowDetails(false)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const walletAddress = useTNS(address, true)
|
||||
const tnsAddress = useTNS(address, false)
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{name !== 'mainnet' && (
|
||||
<span className={styles.network}>{name}</span>
|
||||
)}
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
setShowDetails(!showDetails)
|
||||
}}
|
||||
>
|
||||
<span className={styles.walletIcon}>
|
||||
<WalletSVG className={styles.walletIcon} />
|
||||
</span>
|
||||
<span className={styles.address}>
|
||||
{walletAddress ? walletAddress : truncate(address, [2, 4])}
|
||||
</span>
|
||||
<div className={styles.balance}>
|
||||
{state === State.READY ? (
|
||||
`${formatValue(
|
||||
lookup(uusdAmount, UST_DENOM, UST_DECIMALS)
|
||||
)}`
|
||||
) : (
|
||||
<CircularProgress
|
||||
color='inherit'
|
||||
size={'0.9rem'}
|
||||
className={styles.circularProgress}
|
||||
/>
|
||||
)}{' '}
|
||||
UST
|
||||
</div>
|
||||
</button>
|
||||
{showDetails && (
|
||||
<ClickAwayListener onClickAway={onClickAway}>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.detailsHeader}>
|
||||
<div className={styles.detailsBalance}>
|
||||
<WalletSVG color={colors.secondaryDark} />
|
||||
<p>
|
||||
{formatValue(
|
||||
lookup(
|
||||
uusdAmount,
|
||||
UST_DENOM,
|
||||
UST_DECIMALS
|
||||
),
|
||||
0
|
||||
)}{' '}
|
||||
UST
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.detailsButton}>
|
||||
<Button
|
||||
text={t('common.disconnect')}
|
||||
color='secondary'
|
||||
onClick={disconnect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailsBody}>
|
||||
<p className={`sub2 ${styles.addressLabel}`}>
|
||||
{tnsAddress
|
||||
? tnsAddress
|
||||
: t('common.yourAddress')}
|
||||
</p>
|
||||
<p className={styles.address}>{address}</p>
|
||||
<p className={styles.addressMobile}>
|
||||
{truncate(address, [14, 14])}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.copy}
|
||||
onClick={setCopied}
|
||||
>
|
||||
<CopySVG color={colors.secondaryDark} />
|
||||
{isCopied ? (
|
||||
<>
|
||||
{t('common.copied')}{' '}
|
||||
<CheckSVG
|
||||
color={colors.secondaryDark}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>{t('common.copy')}</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className={styles.external}
|
||||
onClick={viewOnFinder}
|
||||
>
|
||||
<ExternalSVG color={colors.secondaryDark} />{' '}
|
||||
{t('common.viewOnFinder')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConnectedButton
|
||||
@@ -0,0 +1,247 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(31);
|
||||
@include padding(0.5, 3, 0);
|
||||
@include margin(0, 2, 0, 0);
|
||||
@include typoS;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
|
||||
svg {
|
||||
margin-top: space(-1);
|
||||
height: rem-calc(19);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: $fontWeightRegular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-inline-start: rem-calc(8);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $colorWhite;
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonHighlight {
|
||||
@include layoutIncentiveButton;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
width: rem-calc(390);
|
||||
left: rem-calc(-155);
|
||||
z-index: 100;
|
||||
@include layoutPopover;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
top: rem-calc(12);
|
||||
right: rem-calc(16);
|
||||
|
||||
svg {
|
||||
fill: $colorSecondaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include padding(4, 0);
|
||||
@include margin(0);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $alphaBlack20;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detailsHead {
|
||||
@include margin(0);
|
||||
@include typoScaps;
|
||||
color: $colorSecondaryDark;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
@include padding(4, 8);
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
.successContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.successTitle {
|
||||
text-align: center;
|
||||
color: $colorSecondaryDark;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.succcessTxHash {
|
||||
display: flex;
|
||||
@include margin(0, 0, 4);
|
||||
|
||||
.label {
|
||||
@include margin(0, 2, 0, 0);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container,
|
||||
.total {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
@include padding(4, 0, 0);
|
||||
border-bottom: 1px solid $colorSecondaryDark;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: nowrap;
|
||||
@include padding(0, 0, 4);
|
||||
|
||||
.head {
|
||||
@include typoScaps;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-height: rem-calc(12);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.subhead {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 0.6;
|
||||
@include margin(1, 0);
|
||||
@include typoScaps;
|
||||
}
|
||||
|
||||
.token {
|
||||
@include typoS;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
min-height: rem-calc(12);
|
||||
flex: 0 0 rem-calc(76);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@include margin(0, 0, 0, 3);
|
||||
|
||||
p {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.headline {
|
||||
@include typoXXScaps;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.tokenAmount {
|
||||
@include typoS;
|
||||
}
|
||||
|
||||
.tokenValue {
|
||||
@include margin(1, 0);
|
||||
@include typoXXS;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
border-bottom: none;
|
||||
|
||||
.position {
|
||||
.label {
|
||||
.subhead {
|
||||
opacity: 1;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 rem-calc(200);
|
||||
max-width: rem-calc(200);
|
||||
margin: 0 auto;
|
||||
@include padding(6, 0, 0);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
@include padding(2, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.details {
|
||||
right: 0;
|
||||
top: rem-calc(46);
|
||||
transform: none;
|
||||
left: unset;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include margin(2, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.details {
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveGradient {
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
width: rem-calc(550);
|
||||
@include margin(-5, -10, 0);
|
||||
|
||||
.glow {
|
||||
width: rem-calc(550);
|
||||
position: absolute;
|
||||
opacity: 0.6;
|
||||
@include glowXL;
|
||||
}
|
||||
|
||||
.dialContainer {
|
||||
width: rem-calc(550);
|
||||
display: flex;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dotContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: space(17.5);
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: rem-calc(4);
|
||||
height: rem-calc(4);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(13);
|
||||
margin-inline-start: space(59);
|
||||
border-radius: $borderRadiusXS;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.dotGlow {
|
||||
position: absolute;
|
||||
width: rem-calc(7);
|
||||
height: rem-calc(7);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(13);
|
||||
margin-inline-start: space(59);
|
||||
border-radius: $borderRadiusXS;
|
||||
@include glowM;
|
||||
transition: left 2s;
|
||||
}
|
||||
}
|
||||
|
||||
.borrowLimitTextContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.percent {
|
||||
margin-bottom: space(-1.25);
|
||||
word-break: normal;
|
||||
word-wrap: unset;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
}
|
||||
|
||||
.maxText {
|
||||
margin-top: space(19);
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.caption {
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
margin-bottom: space(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.container {
|
||||
@include margin(10, 0);
|
||||
width: rem-calc(360);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
|
||||
.glow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.maxText {
|
||||
width: 100%;
|
||||
@include margin(19, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.container {
|
||||
width: rem-calc(282);
|
||||
|
||||
.dialContainer {
|
||||
@include margin(0);
|
||||
|
||||
+ div {
|
||||
margin-top: space(6) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'create-conical-gradient'
|
||||
import ChartComponent from 'react-chartjs-2'
|
||||
import styles from './RadialGauge.module.scss'
|
||||
import Chart from 'chart.js'
|
||||
|
||||
import produceValueArc, { GradientValue } from './elements/ValueArc'
|
||||
import {
|
||||
setUpDefaults,
|
||||
RadialGaugeController,
|
||||
} from './controllers/RadialGaugeController'
|
||||
import { ReactNode } from 'react'
|
||||
import produceTrackArc from './elements/TrackArc'
|
||||
import { GAUGE_SCALE } from '../../constants/appConstants'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
|
||||
interface Props {
|
||||
currentLtv: number
|
||||
maxLtv: number
|
||||
maxValue: number
|
||||
generateText: (limit: number) => ReactNode
|
||||
showMaxValue: boolean
|
||||
colorStops: Array<GradientValue>
|
||||
showDot: boolean
|
||||
maxTextPrefix?: string
|
||||
maxTextTitle?: string
|
||||
}
|
||||
|
||||
const RadialGauge = ({
|
||||
currentLtv,
|
||||
maxLtv,
|
||||
maxValue,
|
||||
generateText,
|
||||
showMaxValue,
|
||||
colorStops,
|
||||
showDot = true,
|
||||
maxTextPrefix = '$',
|
||||
maxTextTitle = 'MAX',
|
||||
}: Props) => {
|
||||
Chart.defaults.radialGauge = Chart.defaults.doughnut
|
||||
|
||||
setUpDefaults()
|
||||
// @ts-ignore : Chart.elements is not exposed on the d.ts file for chart.js
|
||||
Chart.elements.RoundedOverArc = produceValueArc(colorStops)
|
||||
//@ts-ignore
|
||||
Chart.elements.RoundedArc = produceTrackArc(currentLtv > 0)
|
||||
var controller = RadialGaugeController()
|
||||
|
||||
// todo enable tooltips when we have the finalised specification for them
|
||||
Chart.defaults.global.tooltips.enabled = false
|
||||
Chart.controllers.radialGauge = controller
|
||||
|
||||
const chartData = {
|
||||
datasets: [
|
||||
{
|
||||
// This is the value we want to show
|
||||
data: [(currentLtv > 100 ? 100 : currentLtv) * GAUGE_SCALE],
|
||||
borderWidth: 0,
|
||||
maxLtv: maxLtv,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.glow}>
|
||||
<ChartComponent
|
||||
data={chartData}
|
||||
// We can do this because of https://github.com/jerairrest/react-chartjs-2/blob/master/src/index.js#L25
|
||||
// where it is checking the Chart.js instance for a controller that matches the type.
|
||||
// Typescript will complain however because it is not one of the preconfigured charts in react-chart-js.
|
||||
//@ts-ignore
|
||||
type='radialGauge'
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dialContainer}>
|
||||
<ChartComponent
|
||||
data={chartData}
|
||||
//@ts-ignore - see comment above
|
||||
type='radialGauge'
|
||||
/>
|
||||
</div>
|
||||
{generateText(currentLtv ?? 0)}
|
||||
|
||||
{showMaxValue ? (
|
||||
<div className={styles.maxText}>
|
||||
<span className={`caption ${styles.caption}`}>
|
||||
{maxTextTitle}
|
||||
</span>
|
||||
<span className={'sub2'} style={{ opacity: '1' }}>
|
||||
{maxTextPrefix}
|
||||
{formatValue(maxValue, 2, 2, true, false, true, true)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadialGauge
|
||||
@@ -0,0 +1,279 @@
|
||||
import Chart, { helpers } from 'chart.js'
|
||||
import { GAUGE_SCALE } from '../../../constants/appConstants'
|
||||
|
||||
const setUpDefaults = () => {
|
||||
Chart.defaults._set('radialGauge', {
|
||||
animation: {
|
||||
// Boolean - Whether we animate the rotation of the radialGauge
|
||||
animateRotate: true,
|
||||
// Boolean - Whether we animate scaling the radialGauge from the centre
|
||||
animateScale: true,
|
||||
},
|
||||
|
||||
// Todo remove these out of the table
|
||||
// The percentage of the chart that is the center area
|
||||
centerPercentage: 95.5,
|
||||
|
||||
maintainAspectRatio: true,
|
||||
circumference: Math.PI + 2,
|
||||
rotation: -Math.PI - 1,
|
||||
|
||||
// the color of the radial gauge's track
|
||||
trackColor: '#1c1c1c',
|
||||
// whether arc for the gauge should have rounded corners
|
||||
roundedCorners: true,
|
||||
|
||||
hover: {
|
||||
mode: 'disable',
|
||||
},
|
||||
options: {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
|
||||
// the domain of the metric
|
||||
domain: [0, 100],
|
||||
})
|
||||
}
|
||||
|
||||
const getCustomRadialGaugeController = () => {
|
||||
return Chart.controllers.doughnut.extend({
|
||||
// @ts-ignore
|
||||
dataElementType: Chart.elements.RoundedOverArc,
|
||||
linkScales: helpers.noop,
|
||||
|
||||
draw: function (ease: any) {
|
||||
this.drawTrack()
|
||||
Chart.controllers.doughnut.prototype.draw.call(this, ease)
|
||||
this.drawDot()
|
||||
},
|
||||
|
||||
drawTrack() {
|
||||
//@ts-ignore
|
||||
new Chart.elements.RoundedArc({
|
||||
_view: {
|
||||
backgroundColor: this.chart.options.trackColor,
|
||||
borderColor: this.chart.options.trackColor,
|
||||
borderWidth: 0,
|
||||
startAngle: this.chart.options.rotation,
|
||||
endAngle: Math.PI * 0.33,
|
||||
x: this.centerX,
|
||||
y: this.centerY,
|
||||
innerRadius: this.innerRadius,
|
||||
outerRadius: this.outerRadius,
|
||||
roundedCorners: true,
|
||||
},
|
||||
_chart: this.chart,
|
||||
}).draw()
|
||||
},
|
||||
|
||||
drawDot() {
|
||||
const maxLtv = this.getDataset()['maxLtv']
|
||||
if (!maxLtv || maxLtv <= 0) {
|
||||
return
|
||||
}
|
||||
// Calculate the angle of our borrowing capacity
|
||||
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
|
||||
const value = maxLtv * GAUGE_SCALE
|
||||
const domainSize = domainEnd - domainStart
|
||||
const limitAngle =
|
||||
domainSize > 0
|
||||
? Math.PI *
|
||||
2.0 *
|
||||
(Math.abs(value - domainStart) / domainSize)
|
||||
: 0
|
||||
|
||||
// Get the center of the arc
|
||||
const arcCentre = (this.outerRadius + this.innerRadius) / 2
|
||||
|
||||
// Calculate offset from the centre of our circle
|
||||
const x =
|
||||
arcCentre * Math.cos(limitAngle + this.chart.options.rotation)
|
||||
const y =
|
||||
arcCentre * Math.sin(limitAngle + this.chart.options.rotation)
|
||||
|
||||
// @ts-ignore
|
||||
new Chart.elements.Point({
|
||||
_view: {
|
||||
radius: 2,
|
||||
pointStyle: 'circle',
|
||||
backgroundColor: '#c83333',
|
||||
borderColor: '#c83333',
|
||||
borderWidth: 1,
|
||||
// Hover
|
||||
hitRadius: 1,
|
||||
hoverRadius: 4,
|
||||
hoverBorderWidth: 1,
|
||||
x: this.centerX + x,
|
||||
y: this.centerY + y,
|
||||
},
|
||||
_chart: this.chart,
|
||||
}).draw()
|
||||
},
|
||||
|
||||
update(reset: any) {
|
||||
const chart = this.chart
|
||||
const chartArea = chart.chartArea
|
||||
const opts = chart.options
|
||||
const arcOpts = opts.elements.arc
|
||||
const availableWidth =
|
||||
chartArea.right - chartArea.left - arcOpts.borderWidth
|
||||
const availableHeight =
|
||||
chartArea.bottom - chartArea.top - arcOpts.borderWidth
|
||||
const availableSize = Math.min(availableWidth, availableHeight)
|
||||
|
||||
const meta = this.getMeta()
|
||||
const centerPercentage = opts.centerPercentage
|
||||
|
||||
this.borderWidth = this.getMaxBorderWidth(meta.data)
|
||||
this.outerRadius = Math.max(
|
||||
(availableSize - this.borderWidth) / 2,
|
||||
0
|
||||
)
|
||||
this.innerRadius = Math.max(
|
||||
centerPercentage
|
||||
? (this.outerRadius / 100) * centerPercentage
|
||||
: 0,
|
||||
0
|
||||
)
|
||||
|
||||
meta.total = this.getMetricValue()
|
||||
this.centerX = (chartArea.left + chartArea.right) / 2
|
||||
this.centerY = (chartArea.top + chartArea.bottom) / 2
|
||||
|
||||
helpers.each(meta.data, (arc: any, index: any) => {
|
||||
this.updateElement(arc, index, reset)
|
||||
})
|
||||
},
|
||||
|
||||
updateElement(arc: any, index: any, reset: any) {
|
||||
const chart = this.chart
|
||||
const chartArea = chart.chartArea
|
||||
const opts = chart.options
|
||||
const animationOpts = opts.animation
|
||||
const centerX = (chartArea.left + chartArea.right) / 2
|
||||
const centerY = (chartArea.top + chartArea.bottom) / 2
|
||||
const startAngle = opts.rotation // non reset case handled later
|
||||
const dataset = this.getDataset()
|
||||
const arcAngle =
|
||||
reset && animationOpts.animateRotate
|
||||
? 0
|
||||
: this.calculateArcAngle(dataset.data[index])
|
||||
const value =
|
||||
reset && animationOpts.animateScale ? 0 : this.getMetricValue()
|
||||
const endAngle = startAngle + arcAngle // arcAngle
|
||||
const innerRadius = this.innerRadius
|
||||
const outerRadius = this.outerRadius
|
||||
const valueAtIndexOrDefault = helpers.valueAtIndexOrDefault
|
||||
|
||||
helpers.extend(arc, {
|
||||
// Utility
|
||||
_datasetIndex: this.index,
|
||||
_index: index,
|
||||
|
||||
// Desired view properties
|
||||
_model: {
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
startAngle,
|
||||
endAngle,
|
||||
outerRadius,
|
||||
innerRadius,
|
||||
label: valueAtIndexOrDefault(
|
||||
dataset.label,
|
||||
index,
|
||||
chart.data.labels[index]
|
||||
),
|
||||
roundedCorners: opts.roundedCorners,
|
||||
value,
|
||||
},
|
||||
})
|
||||
|
||||
const model = arc._model
|
||||
|
||||
// Resets the visual styles
|
||||
const custom = arc.custom || {}
|
||||
const valueOrDefault = helpers.valueAtIndexOrDefault
|
||||
const elementOpts = this.chart.options.elements.arc
|
||||
model.backgroundColor = custom.backgroundColor
|
||||
? custom.backgroundColor
|
||||
: valueOrDefault(
|
||||
dataset.backgroundColor,
|
||||
index,
|
||||
elementOpts.backgroundColor
|
||||
)
|
||||
model.borderColor = custom.borderColor
|
||||
? custom.borderColor
|
||||
: valueOrDefault(
|
||||
dataset.borderColor,
|
||||
index,
|
||||
elementOpts.borderColor
|
||||
)
|
||||
model.borderWidth = custom.borderWidth
|
||||
? custom.borderWidth
|
||||
: valueOrDefault(
|
||||
dataset.borderWidth,
|
||||
index,
|
||||
elementOpts.borderWidth
|
||||
)
|
||||
|
||||
arc.pivot()
|
||||
},
|
||||
|
||||
getMetricValue() {
|
||||
let value = this.getDataset().data[0]
|
||||
if (value == null) {
|
||||
value = this.chart.options.domain[0] || 0
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
|
||||
getDomain() {
|
||||
return this.chart.options.domain
|
||||
},
|
||||
|
||||
calculateArcAngle() {
|
||||
const [domainStart, domainEnd] = this.getDomain() || [0, 100]
|
||||
const value = this.getMetricValue()
|
||||
const domainSize = domainEnd - domainStart
|
||||
return domainSize > 0
|
||||
? Math.PI * 2.0 * (Math.abs(value - domainStart) / domainSize)
|
||||
: 0
|
||||
},
|
||||
|
||||
// gets the max border or hover width to properly scale pie charts
|
||||
getMaxBorderWidth(arcs: any) {
|
||||
let max = 0
|
||||
const index = this.index
|
||||
const length = arcs.length
|
||||
let borderWidth
|
||||
let hoverWidth
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0
|
||||
hoverWidth = arcs[i]._chart
|
||||
? arcs[i]._chart.config.data.datasets[index]
|
||||
.hoverBorderWidth
|
||||
: 0
|
||||
|
||||
max = borderWidth > max ? borderWidth : max
|
||||
max = hoverWidth > max ? hoverWidth : max
|
||||
}
|
||||
return max
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
getCustomRadialGaugeController as RadialGaugeController,
|
||||
setUpDefaults,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Chart from 'chart.js'
|
||||
|
||||
const produceTrackArc = (hasValue: boolean) => {
|
||||
//@ts-ignore
|
||||
return Chart.elements.Arc.extend({
|
||||
draw() {
|
||||
const noDataTrackColor = '#3a3c49'
|
||||
const ctx = this._chart.ctx
|
||||
const vm = this._view
|
||||
const { startAngle, endAngle } = vm
|
||||
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
|
||||
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
|
||||
|
||||
// translate + rotate to make drawing the corners simpler
|
||||
ctx.translate(vm.x, vm.y)
|
||||
ctx.rotate(startAngle)
|
||||
const angle = endAngle - startAngle
|
||||
ctx.beginPath()
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
|
||||
}
|
||||
ctx.arc(0, 0, vm.outerRadius, 0, angle)
|
||||
|
||||
const x = cornerX * Math.cos(angle)
|
||||
const y = cornerX * Math.sin(angle)
|
||||
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
|
||||
}
|
||||
|
||||
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
|
||||
ctx.closePath()
|
||||
ctx.rotate(-startAngle)
|
||||
ctx.translate(-vm.x, -vm.y)
|
||||
ctx.strokeStyle = hasValue ? vm.borderColor : noDataTrackColor
|
||||
ctx.lineWidth = vm.borderWidth
|
||||
ctx.fillStyle = hasValue ? vm.backgroundColor : noDataTrackColor
|
||||
|
||||
ctx.fill()
|
||||
ctx.lineJoin = 'bevel'
|
||||
|
||||
if (vm.borderWidth) {
|
||||
ctx.stroke()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default produceTrackArc
|
||||
@@ -0,0 +1,75 @@
|
||||
export interface GradientValue {
|
||||
color: string
|
||||
value: number
|
||||
}
|
||||
|
||||
const produceValueArc = (colorStops: Array<GradientValue>) => {
|
||||
//@ts-ignore
|
||||
return Chart.elements.Arc.extend({
|
||||
draw() {
|
||||
const ctx = this._chart.ctx
|
||||
const vm = this._view
|
||||
|
||||
const { startAngle, endAngle } = vm
|
||||
if (vm.value === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const cornerRadius = (vm.outerRadius - vm.innerRadius) / 2
|
||||
const cornerX = (vm.outerRadius + vm.innerRadius) / 2
|
||||
|
||||
// translate + rotate to make drawing the corners simpler
|
||||
ctx.translate(vm.x, vm.y)
|
||||
ctx.rotate(startAngle)
|
||||
const angle = endAngle - startAngle
|
||||
ctx.beginPath()
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(cornerX, 0, cornerRadius, Math.PI, 0)
|
||||
}
|
||||
ctx.arc(0, 0, vm.outerRadius, 0, angle)
|
||||
|
||||
const x = cornerX * Math.cos(angle)
|
||||
const y = cornerX * Math.sin(angle)
|
||||
|
||||
if (vm.roundedCorners) {
|
||||
ctx.arc(x, y, cornerRadius, angle, angle + Math.PI)
|
||||
}
|
||||
|
||||
ctx.arc(0, 0, vm.innerRadius, angle, 0, true)
|
||||
ctx.closePath()
|
||||
ctx.rotate(-startAngle)
|
||||
ctx.translate(-vm.x, -vm.y)
|
||||
|
||||
ctx.strokeStyle = vm.borderColor
|
||||
ctx.lineWidth = vm.borderWidth
|
||||
ctx.strokeStyle = vm.borderColor
|
||||
// create gradient and fill it
|
||||
const rotationOffSet = Math.PI * 0.5
|
||||
const gradient = ctx.createConicalGradient(
|
||||
vm.x,
|
||||
vm.y - 10,
|
||||
0 + rotationOffSet,
|
||||
Math.PI * 2 + rotationOffSet
|
||||
)
|
||||
|
||||
for (var index = 0; index < colorStops.length; index += 1) {
|
||||
const colorStop = colorStops[index]
|
||||
gradient.addColorStop(colorStop.value, colorStop.color)
|
||||
}
|
||||
|
||||
ctx.fillStyle = gradient.pattern
|
||||
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
ctx.fill()
|
||||
ctx.lineJoin = 'bevel'
|
||||
|
||||
if (vm.borderWidth) {
|
||||
ctx.stroke()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default produceValueArc
|
||||
@@ -0,0 +1,50 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(184);
|
||||
.item {
|
||||
@include margin(1, 0, 0);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: space(2);
|
||||
border-radius: $borderRadiusXXS;
|
||||
width: space(2);
|
||||
@include margin(1.5, 2, 0, 0);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
width: rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import styles from './Apr.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
data: AssetInfo
|
||||
title: string
|
||||
}
|
||||
|
||||
interface HoverItem {
|
||||
color?: string
|
||||
symbol?: string
|
||||
apr?: number
|
||||
subtitle: string
|
||||
}
|
||||
|
||||
const Apr = ({ data }: Props) => {
|
||||
const [aprData, setAprData] = useState<HoverItem[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const produceData = (data: AssetInfo[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: AssetInfo, key: number) => {
|
||||
items.push({
|
||||
color: asset.color,
|
||||
symbol: asset.symbol,
|
||||
apr: asset.apy,
|
||||
subtitle:
|
||||
key === 0 ? t('fields.baseApy') : t('common.incentiveApr'),
|
||||
})
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const baseData = data.incentive ? [data, data.incentive] : [data]
|
||||
setAprData(produceData(baseData))
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
)
|
||||
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
<div
|
||||
className={styles.dot}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.symbol}
|
||||
</span>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{formatValue(
|
||||
item?.apr || 0,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span className={`caption ${styles.subTitleText}`}>
|
||||
{item.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.apyBreakdown')}</p>
|
||||
{aprData.map((item: HoverItem, index: number) =>
|
||||
produceItem(item, index)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apr
|
||||
@@ -0,0 +1,38 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include layoutTooltip;
|
||||
min-width: rem-calc(184);
|
||||
|
||||
.item {
|
||||
margin-top: space(2);
|
||||
display: flex;
|
||||
width: rem-calc(175);
|
||||
flex-direction: row;
|
||||
@include typoXS;
|
||||
|
||||
&:last-child {
|
||||
@include devider20;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(115);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 rem-calc(60);
|
||||
}
|
||||
|
||||
&.leverage {
|
||||
@include margin(2, 0, 0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import styles from './Apy.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRedBank } from '../../hooks'
|
||||
|
||||
interface Props {
|
||||
trueApy: number
|
||||
apyData: StrategyRate
|
||||
aprData: StrategyRate
|
||||
token: string
|
||||
leverage?: string
|
||||
borrowDenom?: string
|
||||
}
|
||||
|
||||
const Apy = ({
|
||||
trueApy,
|
||||
apyData,
|
||||
aprData,
|
||||
token,
|
||||
leverage,
|
||||
borrowDenom = 'uusd',
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { findMarketInfo } = useRedBank()
|
||||
const [checkedApyData, setApyData] = useState<StrategyRate>(apyData)
|
||||
const [checkedAprData, setAprData] = useState<StrategyRate>(aprData)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (apyData?.total !== checkedApyData?.total) {
|
||||
setApyData(apyData)
|
||||
}
|
||||
if (aprData?.total !== checkedAprData?.total) {
|
||||
setAprData(aprData)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[apyData, aprData]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.apyBreakdown')}</p>
|
||||
{checkedAprData?.trading > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.tradingFeesApr')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.trading,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedAprData?.astro > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.protocolApr', { protocol: 'ASTRO' })}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.astro,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedAprData?.protocol > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.protocolApr', { protocol: token })}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedAprData.protocol,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{checkedApyData?.total > 0 && (
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.totalRewardsApy')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
checkedApyData.total,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{leverage && Number(leverage) > 0 && trueApy > 0 && (
|
||||
<>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.borrowRateApr')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(
|
||||
Number(
|
||||
findMarketInfo(borrowDenom)?.borrow_rate
|
||||
) * 100,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'-',
|
||||
'%'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles.item} ${styles.leverage}`}>
|
||||
<span className={styles.label}>
|
||||
{`${t('fields.leverage')} ${formatValue(
|
||||
leverage || 0,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
'x',
|
||||
true
|
||||
)}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{t('fields.leveragedApy')}
|
||||
</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(trueApy, 2, 2, true, false, '%')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apy
|
||||
@@ -0,0 +1,38 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include layoutTooltip;
|
||||
min-width: rem-calc(184);
|
||||
|
||||
.item {
|
||||
margin-top: space(2);
|
||||
display: flex;
|
||||
width: rem-calc(175);
|
||||
flex-direction: row;
|
||||
@include typoXS;
|
||||
|
||||
&:last-child {
|
||||
@include devider20;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(115);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 rem-calc(60);
|
||||
}
|
||||
|
||||
&.leverage {
|
||||
@include margin(2, 0, 0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import styles from './PnL.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
initial: string
|
||||
current: string
|
||||
pnlValue: number
|
||||
}
|
||||
|
||||
const PnL = ({ initial, current, pnlValue }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{t('fields.pnlBreakdown')}</p>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>{t('fields.initialValue')}</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(initial, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>{t('fields.currentValue')}</span>
|
||||
<span className={styles.value}>
|
||||
{formatValue(current, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<span className={styles.label}>
|
||||
{pnlValue >= 0 ? t('fields.profit') : t('fields.loss')}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.value} ${
|
||||
pnlValue > 0
|
||||
? 'colorInfoProfit'
|
||||
: pnlValue < 0
|
||||
? 'colorInfoLoss'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{formatValue(pnlValue, 2, 2, true, '$')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PnL
|
||||
@@ -0,0 +1,8 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.info {
|
||||
color: $tooltipIconColor;
|
||||
fill: $tooltipIconColor !important;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react'
|
||||
import Tippy from '@tippyjs/react'
|
||||
|
||||
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
|
||||
|
||||
import styles from './Tooltip.module.scss'
|
||||
|
||||
interface TooltipProps {
|
||||
content: string | ReactNode
|
||||
iconWidth: string
|
||||
}
|
||||
|
||||
const Tooltip = ({ content, iconWidth }: TooltipProps) => (
|
||||
<Tippy
|
||||
className='tippyContainer'
|
||||
content={<span className='body2'>{content}</span>}
|
||||
interactive={true}
|
||||
appendTo={() => document.body}
|
||||
>
|
||||
<HelpOutlineIcon style={{ width: iconWidth }} className={styles.info} />
|
||||
</Tippy>
|
||||
)
|
||||
|
||||
export default Tooltip
|
||||
Reference in New Issue
Block a user