initiating public sequence

This commit is contained in:
Linkie Link
2022-04-29 23:45:14 +02:00
commit 2ecadb938f
384 changed files with 79043 additions and 0 deletions
+191
View File
@@ -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;
}
+88
View File
@@ -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
+52
View File
@@ -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;
}
}
}
}
}
+112
View File
@@ -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
+45
View File
@@ -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
+74
View File
@@ -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);
}
}
}
}
+91
View File
@@ -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
+54
View File
@@ -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;
}
}
+39
View File
@@ -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
+65
View File
@@ -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);
}
}
}
+59
View File
@@ -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
+78
View File
@@ -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);
}
}
}
+103
View File
@@ -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
+17
View File
@@ -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);
}
}
+18
View File
@@ -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
+22
View File
@@ -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;
}
}
}
+24
View File
@@ -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
+41
View File
@@ -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;
}
}
+112
View File
@@ -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;
}
}
}
+172
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
@use '../../styles/master' as *;
.container {
@include layoutTile;
}
+17
View File
@@ -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);
}
}
+266
View File
@@ -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
+17
View File
@@ -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
+47
View File
@@ -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
+232
View File
@@ -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
+191
View File
@@ -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);
}
}
+146
View File
@@ -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;
}
}
}
+150
View File
@@ -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
+50
View File
@@ -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;
}
}
}
}
}
+90
View File
@@ -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
+38
View File
@@ -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;
}
}
}
+159
View File
@@ -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
+38
View File
@@ -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;
}
}
}
+52
View File
@@ -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;
}
+24
View File
@@ -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