Merge branch 'main' into pan/prod-to-main
This commit is contained in:
commit
20f54be2ad
|
@ -8,11 +8,10 @@ import {
|
|||
} from '@blockworks-foundation/mango-client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
BellIcon,
|
||||
ExclamationIcon,
|
||||
ExternalLinkIcon,
|
||||
HeartIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { BellIcon } from '@heroicons/react/outline'
|
||||
import useMangoStore, { MNGO_INDEX } from '../stores/useMangoStore'
|
||||
import { abbreviateAddress, formatUsdValue, usdFormatter } from '../utils'
|
||||
import { notify } from '../utils/notifications'
|
||||
|
@ -31,6 +30,7 @@ import Loading from './Loading'
|
|||
import CreateAlertModal from './CreateAlertModal'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import HealthHeart from './HealthHeart'
|
||||
|
||||
const I80F48_100 = I80F48.fromString('100')
|
||||
|
||||
|
@ -131,11 +131,6 @@ export default function AccountInfo() {
|
|||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
: I80F48_100
|
||||
|
||||
const initHealthRatio =
|
||||
mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
|
||||
: I80F48_100
|
||||
|
||||
const maintHealth =
|
||||
mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Maint')
|
||||
|
@ -193,7 +188,7 @@ export default function AccountInfo() {
|
|||
) : null}
|
||||
<div>
|
||||
{mangoAccount ? (
|
||||
<div className="-mt-2 flex justify-center text-xs">
|
||||
<div className="-mt-2 mb-2 flex justify-center text-xs">
|
||||
<a
|
||||
className="flex items-center text-th-fgd-4 hover:text-th-primary"
|
||||
href={`https://explorer.solana.com/address/${mangoAccount?.publicKey}`}
|
||||
|
@ -208,12 +203,41 @@ export default function AccountInfo() {
|
|||
<div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="font-normal leading-4 text-th-fgd-3">
|
||||
{t('equity')}
|
||||
{t('value')}
|
||||
</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{initialLoad ? <DataLoader /> : formatUsdValue(+equity)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
{t('tooltip-account-liquidated')}{' '}
|
||||
<a
|
||||
href="https://docs.mango.markets/mango/health-overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('learn-more')}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="default-transition cursor-help border-b border-dashed border-th-fgd-3 border-opacity-20 font-normal leading-4 text-th-fgd-3 hover:border-th-bkg-2">
|
||||
{t('health')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="flex items-center space-x-2">
|
||||
<HealthHeart size={24} health={Number(maintHealthRatio)} />
|
||||
<div className="text-th-fgd-1">
|
||||
{maintHealthRatio.gt(I80F48_100)
|
||||
? '>100'
|
||||
: maintHealthRatio.toFixed(2)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<div className="font-normal leading-4 text-th-fgd-3">
|
||||
{t('leverage')}
|
||||
|
@ -326,54 +350,6 @@ export default function AccountInfo() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 flex items-center rounded border border-th-bkg-4 p-2.5 sm:my-1">
|
||||
<div className="flex items-center pr-2">
|
||||
<HeartIcon
|
||||
className="mr-1.5 h-5 w-5 text-th-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
{t('tooltip-account-liquidated')}{' '}
|
||||
<a
|
||||
href="https://docs.mango.markets/mango/health-overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('learn-more')}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="default-transition cursor-help border-b border-dashed border-th-fgd-3 border-opacity-20 font-normal leading-4 text-th-fgd-3 hover:border-th-bkg-2">
|
||||
{t('health')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-1.5 flex-grow rounded bg-th-bkg-4">
|
||||
<div
|
||||
style={{
|
||||
width: `${maintHealthRatio}%`,
|
||||
}}
|
||||
className={`flex rounded ${
|
||||
maintHealthRatio.toNumber() > 30
|
||||
? 'bg-th-green'
|
||||
: initHealthRatio.toNumber() > 0
|
||||
? 'bg-th-orange'
|
||||
: 'bg-th-red'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
<div className="pl-2 text-right">
|
||||
{maintHealthRatio.gt(I80F48_100)
|
||||
? '>100'
|
||||
: maintHealthRatio.toFixed(2)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
{mangoAccount && mangoAccount.beingLiquidated ? (
|
||||
<div className="flex items-center justify-center pt-0.5 text-xs">
|
||||
<ExclamationIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red" />
|
||||
|
|
|
@ -3,7 +3,7 @@ import useMangoStore from '../stores/useMangoStore'
|
|||
import {
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import Input, { Label } from './Input'
|
||||
import Button from './Button'
|
||||
import Modal from './Modal'
|
||||
|
@ -83,7 +83,7 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
|
|||
<p className="flex items-center justify-center">
|
||||
{t('edit-nickname')}
|
||||
<Tooltip content={t('tooltip-name-onchain')}>
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-primary" />
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Modal.Header>
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useMangoAccount from '../hooks/useMangoAccount'
|
||||
import {
|
||||
I80F48,
|
||||
nativeI80F48ToUi,
|
||||
QUOTE_INDEX,
|
||||
ZERO_I80F48,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import { abbreviateAddress, formatUsdValue, usdFormatter } from 'utils'
|
||||
import { DataLoader } from './MarketPosition'
|
||||
|
||||
const AccountOverviewPopover = ({
|
||||
collapsed,
|
||||
health,
|
||||
}: {
|
||||
collapsed: boolean
|
||||
health: I80F48
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const { mangoAccount, initialLoad } = useMangoAccount()
|
||||
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
|
||||
|
||||
const I80F48_100 = I80F48.fromString('100')
|
||||
|
||||
const initHealth =
|
||||
mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Init')
|
||||
: I80F48_100
|
||||
|
||||
const equity =
|
||||
mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.computeValue(mangoGroup, mangoCache)
|
||||
: ZERO_I80F48
|
||||
|
||||
return (
|
||||
<>
|
||||
{mangoAccount ? (
|
||||
<div className={`w-full ${!collapsed ? 'px-2' : ''}`}>
|
||||
{collapsed ? (
|
||||
<div className="pb-2">
|
||||
<p className="mb-0 text-xs text-th-fgd-3">{t('account')}</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{abbreviateAddress(mangoAccount.publicKey)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="pb-2">
|
||||
<p className="mb-0 text-xs leading-4">{t('value')}</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{initialLoad ? <DataLoader /> : formatUsdValue(+equity)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pb-2">
|
||||
<p className="mb-0 text-xs leading-4">{t('health')}</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{health.gt(I80F48_100) ? '>100' : health.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="pb-2">
|
||||
<p className="mb-0 text-xs leading-4">{t('leverage')}</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{initialLoad ? (
|
||||
<DataLoader />
|
||||
) : mangoAccount && mangoGroup && mangoCache ? (
|
||||
`${mangoAccount
|
||||
.getLeverage(mangoGroup, mangoCache)
|
||||
.toFixed(2)}x`
|
||||
) : (
|
||||
'0.00x'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pb-2">
|
||||
<p className="mb-0 text-xs leading-4">
|
||||
{t('collateral-available')}
|
||||
</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{initialLoad ? (
|
||||
<DataLoader />
|
||||
) : mangoAccount && mangoGroup ? (
|
||||
usdFormatter(
|
||||
nativeI80F48ToUi(
|
||||
initHealth,
|
||||
mangoGroup.tokens[QUOTE_INDEX].decimals
|
||||
).toFixed()
|
||||
)
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-0 text-xs leading-4">
|
||||
{marketConfig.name} {t('margin-available')}
|
||||
</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{mangoAccount && mangoGroup && mangoCache
|
||||
? usdFormatter(
|
||||
nativeI80F48ToUi(
|
||||
mangoAccount.getMarketMarginAvailable(
|
||||
mangoGroup,
|
||||
mangoCache,
|
||||
marketConfig.marketIndex,
|
||||
marketConfig.kind
|
||||
),
|
||||
mangoGroup.tokens[QUOTE_INDEX].decimals
|
||||
).toFixed()
|
||||
)
|
||||
: '0.00'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountOverviewPopover
|
|
@ -114,25 +114,26 @@ const AccountSelect = ({
|
|||
</Listbox.Button>
|
||||
</div>
|
||||
<Listbox.Options
|
||||
className={`thin-scroll absolute right-0 top-14 z-20 max-h-60 w-full overflow-auto rounded-md bg-th-bkg-3 p-1`}
|
||||
className={`thin-scroll absolute right-0 top-14 z-20 max-h-60 w-full overflow-auto rounded-md bg-th-bkg-2 p-1`}
|
||||
>
|
||||
{accounts.map((account) => {
|
||||
const symbolForAccount = account.config.symbol
|
||||
|
||||
return (
|
||||
<Listbox.Option
|
||||
className="mb-0"
|
||||
disabled={account.uiBalance === 0}
|
||||
key={account?.account.publicKey.toBase58()}
|
||||
value={account?.account.publicKey.toBase58()}
|
||||
>
|
||||
{({ disabled, selected }) => (
|
||||
<div
|
||||
className={`default-transition px-2 py-1 text-th-fgd-1 ${
|
||||
className={`default-transition rounded p-2 text-th-fgd-1 ${
|
||||
selected && `text-th-primary`
|
||||
} ${
|
||||
disabled
|
||||
? 'text-th-fgd-1 opacity-50 hover:cursor-not-allowed hover:text-th-fgd-1'
|
||||
: 'hover:cursor-pointer hover:text-th-primary'
|
||||
: 'hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary'
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center`}>
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
HeartIcon,
|
||||
PlusCircleIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { MangoAccount, MangoGroup } from '@blockworks-foundation/mango-client'
|
||||
import { abbreviateAddress, formatUsdValue } from '../utils'
|
||||
|
@ -29,7 +29,7 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
|
|||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { t } = useTranslation(['common', 'delegate'])
|
||||
const { publicKey } = useWallet()
|
||||
const [showNewAccountForm, setShowNewAccountForm] = useState(false)
|
||||
const [newAccPublicKey, setNewAccPublicKey] = useState(null)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useState } from 'react'
|
||||
import { CheckCircleIcon } from '@heroicons/react/outline'
|
||||
import { CheckCircleIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import Button from './Button'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Checkbox from './Checkbox'
|
||||
import { SHOW_TOUR_KEY } from './IntroTips'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
// import { SHOW_TOUR_KEY } from './IntroTips'
|
||||
// import { useViewport } from '../hooks/useViewport'
|
||||
// import { breakpoints } from './TradePageGrid'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LANGS } from './SettingsModal'
|
||||
import { RadioGroup } from '@headlessui/react'
|
||||
|
@ -24,13 +24,13 @@ const AlphaModal = ({
|
|||
const { t } = useTranslation('common')
|
||||
const [acceptRisks, setAcceptRisks] = useState(false)
|
||||
const [, setAlphaAccepted] = useLocalStorageState(ALPHA_MODAL_KEY, false)
|
||||
const [, setShowTips] = useLocalStorageState(SHOW_TOUR_KEY, false)
|
||||
// const [, setShowTips] = useLocalStorageState(SHOW_TOUR_KEY, false)
|
||||
const [savedLanguage, setSavedLanguage] = useLocalStorageState('language', '')
|
||||
const [language, setLanguage] = useState('en')
|
||||
const router = useRouter()
|
||||
const { pathname, asPath, query } = router
|
||||
const { width } = useViewport()
|
||||
const hideTips = width ? width < breakpoints.md : false
|
||||
// const { width } = useViewport()
|
||||
// const hideTips = width ? width < breakpoints.md : false
|
||||
|
||||
const handleLanguageSelect = () => {
|
||||
setSavedLanguage(language)
|
||||
|
@ -41,10 +41,10 @@ const AlphaModal = ({
|
|||
setAlphaAccepted(true)
|
||||
}
|
||||
|
||||
const handleTakeTour = () => {
|
||||
setAlphaAccepted(true)
|
||||
setShowTips(true)
|
||||
}
|
||||
// const handleTakeTour = () => {
|
||||
// setAlphaAccepted(true)
|
||||
// setShowTips(true)
|
||||
// }
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} hideClose>
|
||||
|
@ -104,7 +104,7 @@ const AlphaModal = ({
|
|||
>
|
||||
{t('get-started')}
|
||||
</Button>
|
||||
{!hideTips ? (
|
||||
{/* {!hideTips ? (
|
||||
<Button
|
||||
className="w-40"
|
||||
disabled={!acceptRisks}
|
||||
|
@ -112,14 +112,18 @@ const AlphaModal = ({
|
|||
>
|
||||
{t('show-tips')}
|
||||
</Button>
|
||||
) : null}
|
||||
) : null} */}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="pt-2">
|
||||
<RadioGroup value={language} onChange={setLanguage}>
|
||||
<div className="flex flex-col items-center pt-2">
|
||||
<RadioGroup
|
||||
className="w-full"
|
||||
value={language}
|
||||
onChange={setLanguage}
|
||||
>
|
||||
{LANGS.map((l) => (
|
||||
<RadioGroup.Option className="" key={l.locale} value={l.locale}>
|
||||
<RadioGroup.Option key={l.locale} value={l.locale}>
|
||||
{({ checked }) => (
|
||||
<div
|
||||
className={`border ${
|
||||
|
@ -137,9 +141,9 @@ const AlphaModal = ({
|
|||
</RadioGroup.Option>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button onClick={() => handleLanguageSelect()}>Save</Button>
|
||||
</div>
|
||||
<Button className="mt-4" onClick={() => handleLanguageSelect()}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useBalances } from '../hooks/useBalances'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import Button, { LinkButton } from '../components/Button'
|
||||
import { notify } from '../utils/notifications'
|
||||
import { ArrowSmDownIcon, ExclamationIcon } from '@heroicons/react/outline'
|
||||
import { ArrowSmDownIcon, ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { Market } from '@project-serum/serum'
|
||||
import { getTokenBySymbol } from '@blockworks-foundation/mango-client'
|
||||
import Loading from './Loading'
|
||||
|
@ -30,10 +29,10 @@ const BalancesTable = ({
|
|||
const [showDepositModal, setShowDepositModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
const [actionSymbol, setActionSymbol] = useState('')
|
||||
const balances = useBalances()
|
||||
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
|
||||
const { items, requestSort, sortConfig } = useSortableData(
|
||||
balances?.length > 0
|
||||
? balances
|
||||
spotBalances?.length > 0
|
||||
? spotBalances
|
||||
.filter((bal) => {
|
||||
return (
|
||||
showZeroBalances ||
|
||||
|
@ -171,7 +170,7 @@ const BalancesTable = ({
|
|||
}
|
||||
}
|
||||
|
||||
const unsettledBalances = balances.filter(
|
||||
const unsettledBalances = spotBalances.filter(
|
||||
(bal) => bal.unsettled && bal.unsettled > 0
|
||||
)
|
||||
|
||||
|
@ -209,20 +208,20 @@ const BalancesTable = ({
|
|||
{submitting ? <Loading /> : t('settle-all')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{unsettledBalances.map((bal) => {
|
||||
const tokenConfig = getTokenBySymbol(mangoGroupConfig, bal.symbol)
|
||||
return (
|
||||
<div
|
||||
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-3 px-5 py-3"
|
||||
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-2 px-5 py-3"
|
||||
key={bal.symbol}
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${bal.symbol.toLowerCase()}.svg`}
|
||||
className={`mr-3`}
|
||||
/>
|
||||
|
@ -511,9 +510,9 @@ const BalancesTable = ({
|
|||
</Td>
|
||||
{showDepositWithdraw ? (
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
className="h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
className="h-8 w-[86px] pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
onClick={() =>
|
||||
handleOpenDepositModal(balance.symbol)
|
||||
}
|
||||
|
@ -523,11 +522,12 @@ const BalancesTable = ({
|
|||
: t('deposit')}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-4 h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
className="h-8 w-[86px] pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
onClick={() =>
|
||||
handleOpenWithdrawModal(balance.symbol)
|
||||
}
|
||||
disabled={!canWithdraw}
|
||||
primary={false}
|
||||
>
|
||||
{t('withdraw')}
|
||||
</Button>
|
||||
|
@ -559,7 +559,7 @@ const BalancesTable = ({
|
|||
)}
|
||||
</Table>
|
||||
) : (
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('asset')}
|
||||
colTwoHeader={t('net-balance')}
|
||||
|
@ -652,7 +652,7 @@ const BalancesTable = ({
|
|||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
className="h-7 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
className="h-8 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
onClick={() =>
|
||||
handleOpenDepositModal(balance.symbol)
|
||||
}
|
||||
|
@ -662,7 +662,7 @@ const BalancesTable = ({
|
|||
: t('deposit')}
|
||||
</Button>
|
||||
<Button
|
||||
className="h-7 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
className="h-8 w-1/2 border border-th-fgd-4 bg-transparent pt-0 pb-0 pl-3 pr-3 text-xs"
|
||||
onClick={() =>
|
||||
handleOpenWithdrawModal(balance.symbol)
|
||||
}
|
||||
|
@ -699,7 +699,7 @@ const BalancesTable = ({
|
|||
)
|
||||
) : (
|
||||
<div
|
||||
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
|
||||
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
|
||||
>
|
||||
{t('no-balances')}
|
||||
</div>
|
||||
|
|
|
@ -12,13 +12,16 @@ const Button: FunctionComponent<ButtonProps> = ({
|
|||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
primary = true,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100 ${className}`}
|
||||
className={`whitespace-nowrap rounded-full ${
|
||||
primary ? 'bg-th-bkg-button' : 'border border-th-fgd-4'
|
||||
} px-6 py-2 font-bold text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 md:hover:brightness-[1.1] md:disabled:hover:brightness-100 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
@ -42,7 +45,7 @@ export const LinkButton: FunctionComponent<ButtonProps> = ({
|
|||
disabled={disabled}
|
||||
className={`border-0 font-bold ${
|
||||
primary ? 'text-th-primary' : 'text-th-fgd-2'
|
||||
} underline hover:no-underline hover:opacity-60 focus:outline-none disabled:cursor-not-allowed disabled:underline disabled:opacity-60 ${className}`}
|
||||
} underline focus:outline-none disabled:cursor-not-allowed disabled:underline disabled:opacity-60 md:hover:no-underline md:hover:opacity-60 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
@ -61,8 +64,8 @@ export const IconButton: FunctionComponent<ButtonProps> = ({
|
|||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`${className} flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none disabled:cursor-not-allowed
|
||||
disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:text-th-fgd-4`}
|
||||
className={`${className} flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4
|
||||
disabled:text-th-fgd-4 md:hover:text-th-primary md:disabled:hover:text-th-fgd-4`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -37,7 +37,7 @@ const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
|
|||
${
|
||||
v === activeValue
|
||||
? `text-th-primary`
|
||||
: `text-th-fgd-2 hover:text-th-primary`
|
||||
: `text-th-fgd-2 md:hover:text-th-primary`
|
||||
}
|
||||
`}
|
||||
key={`${v}${i}`}
|
||||
|
|
|
@ -30,6 +30,7 @@ interface ChartProps {
|
|||
titleValue?: number
|
||||
useMulticoloredBars?: boolean
|
||||
zeroLine?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const Chart: FunctionComponent<ChartProps> = ({
|
||||
|
@ -47,6 +48,7 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
titleValue,
|
||||
useMulticoloredBars,
|
||||
zeroLine,
|
||||
loading,
|
||||
}) => {
|
||||
const [mouseData, setMouseData] = useState<string | null>(null)
|
||||
const [daysToShow, setDaysToShow] = useState(daysRange || 30)
|
||||
|
@ -83,266 +85,292 @@ const Chart: FunctionComponent<ChartProps> = ({
|
|||
|
||||
return (
|
||||
<div className="h-52 w-full" ref={observe}>
|
||||
<div className="flex w-full items-start justify-between pb-6">
|
||||
<div className="pl-2">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">{title}</div>
|
||||
{mouseData ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{labelFormat(mouseData[yAxis])}
|
||||
{data.length > 0 ? (
|
||||
<>
|
||||
<div className="flex w-full items-start justify-between pb-6">
|
||||
<div className="pl-2">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">{title}</div>
|
||||
{mouseData ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{labelFormat(mouseData[yAxis])}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{dayjs(mouseData[xAxis]).format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{titleValue
|
||||
? labelFormat(titleValue)
|
||||
: labelFormat(data[data.length - 1][yAxis])}
|
||||
</div>
|
||||
<div className="h-4 text-xs font-normal text-th-fgd-4">
|
||||
{titleValue
|
||||
? ''
|
||||
: dayjs(data[data.length - 1][xAxis]).format(
|
||||
'ddd MMM D YYYY, h:mma'
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!hideRangeFilters ? (
|
||||
<div className="flex h-5">
|
||||
<button
|
||||
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 1 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1)}
|
||||
>
|
||||
24H
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 7 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(7)}
|
||||
>
|
||||
7D
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 30 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(30)}
|
||||
>
|
||||
30D
|
||||
</button>
|
||||
{showAll ? (
|
||||
<button
|
||||
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 1000 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1000)}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{new Date(mouseData[xAxis]).toDateString()}
|
||||
</div>
|
||||
</>
|
||||
) : data.length > 0 ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{titleValue
|
||||
? labelFormat(titleValue)
|
||||
: labelFormat(data[data.length - 1][yAxis])}
|
||||
</div>
|
||||
<div className="h-4 text-xs font-normal text-th-fgd-4">
|
||||
{titleValue
|
||||
? ''
|
||||
: new Date(data[data.length - 1][xAxis]).toDateString()}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!hideRangeFilters ? (
|
||||
<div className="flex h-5">
|
||||
<button
|
||||
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 1 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1)}
|
||||
>
|
||||
24H
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 7 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(7)}
|
||||
>
|
||||
7D
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 30 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(30)}
|
||||
>
|
||||
30D
|
||||
</button>
|
||||
{showAll ? (
|
||||
<button
|
||||
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
daysToShow === 1000 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1000)}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{width > 0 && type === 'area' ? (
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={data ? handleDaysToShow(daysToShow) : null}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Tooltip
|
||||
cursor={{
|
||||
strokeOpacity: 0,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#ffba24" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
isAnimationActive={false}
|
||||
type="monotone"
|
||||
dataKey={yAxis}
|
||||
stroke="#ffba24"
|
||||
fill="url(#gradientArea)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey={xAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={yAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dx={-10}
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={
|
||||
tickFormat
|
||||
? (v) => tickFormat(v)
|
||||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
{zeroLine ? (
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke={
|
||||
theme === 'Light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.35)'
|
||||
}
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
) : null}
|
||||
</AreaChart>
|
||||
) : null}
|
||||
{width > 0 && type === 'bar' ? (
|
||||
<BarChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={
|
||||
data
|
||||
? hideRangeFilters
|
||||
? data
|
||||
: handleDaysToShow(daysToShow)
|
||||
: null
|
||||
}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Tooltip
|
||||
cursor={{
|
||||
fill: '#fff',
|
||||
opacity: 0.2,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="defaultGradientBar" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#ffba24" stopOpacity={0.5} />
|
||||
</linearGradient>
|
||||
<linearGradient id="greenGradientBar" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={1}
|
||||
{width > 0 && type === 'area' ? (
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={data ? handleDaysToShow(daysToShow) : null}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Tooltip
|
||||
cursor={{
|
||||
strokeOpacity: 0,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme === 'Mango' ? '#91B503' : '#4BA53B'}
|
||||
stopOpacity={1}
|
||||
<defs>
|
||||
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#ffba24" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
isAnimationActive={false}
|
||||
type="monotone"
|
||||
dataKey={yAxis}
|
||||
stroke="#ffba24"
|
||||
fill="url(#gradientArea)"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="redGradientBar" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={1}
|
||||
<XAxis
|
||||
dataKey={xAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme === 'Mango' ? '#EC1809' : '#BB2525'}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Bar dataKey={yAxis}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
useMulticoloredBars
|
||||
? entry[yAxis] > 0
|
||||
? 'url(#greenGradientBar)'
|
||||
: 'url(#redGradientBar)'
|
||||
: 'url(#defaultGradientBar)'
|
||||
<YAxis
|
||||
dataKey={yAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dx={-10}
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={
|
||||
tickFormat
|
||||
? (v) => tickFormat(v)
|
||||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
<XAxis
|
||||
dataKey={xAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={yAxis}
|
||||
interval="preserveStartEnd"
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dx={-10}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={
|
||||
tickFormat
|
||||
? (v) => tickFormat(v)
|
||||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
{zeroLine ? (
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke={
|
||||
theme === 'Light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.2)'
|
||||
}
|
||||
/>
|
||||
{zeroLine ? (
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke={
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)'
|
||||
}
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
) : null}
|
||||
</AreaChart>
|
||||
) : null}
|
||||
</BarChart>
|
||||
) : null}
|
||||
{width > 0 && type === 'bar' ? (
|
||||
<BarChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={
|
||||
data
|
||||
? hideRangeFilters
|
||||
? data
|
||||
: handleDaysToShow(daysToShow)
|
||||
: null
|
||||
}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Tooltip
|
||||
cursor={{
|
||||
fill: '#fff',
|
||||
opacity: 0.2,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="defaultGradientBar"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#ffba24" stopOpacity={0.5} />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="greenGradientBar"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme === 'Mango' ? '#91B503' : '#4BA53B'}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="redGradientBar" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme === 'Mango' ? '#EC1809' : '#BB2525'}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Bar dataKey={yAxis}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
useMulticoloredBars
|
||||
? entry[yAxis] > 0
|
||||
? 'url(#greenGradientBar)'
|
||||
: 'url(#redGradientBar)'
|
||||
: 'url(#defaultGradientBar)'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
<XAxis
|
||||
dataKey={xAxis}
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey={yAxis}
|
||||
interval="preserveStartEnd"
|
||||
axisLine={false}
|
||||
hide={data.length > 0 ? false : true}
|
||||
dx={-10}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={
|
||||
tickFormat
|
||||
? (v) => tickFormat(v)
|
||||
: (v) => numberCompactFormatter.format(v)
|
||||
}
|
||||
type="number"
|
||||
width={yAxisWidth || 50}
|
||||
/>
|
||||
{zeroLine ? (
|
||||
<ReferenceLine
|
||||
y={0}
|
||||
stroke={
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.2)'
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</BarChart>
|
||||
) : null}
|
||||
</>
|
||||
) : loading ? (
|
||||
<>
|
||||
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="mb-0">Chart not available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,7 @@ import {
|
|||
useState,
|
||||
} from 'react'
|
||||
import useMangoStore, { MNGO_INDEX } from '../stores/useMangoStore'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import Button from './Button'
|
||||
import Modal from './Modal'
|
||||
import { ElementTitle } from './styles'
|
||||
|
@ -54,6 +51,11 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
|
|||
const openOrders = useMangoStore((s) => s.selectedMangoAccount.openOrders)
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
const activeAlerts = useMangoStore((s) => s.alerts.activeAlerts)
|
||||
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
|
||||
|
||||
const unsettledBalances = spotBalances.filter(
|
||||
(bal) => bal.unsettled && bal.unsettled > 0
|
||||
)
|
||||
|
||||
const fetchTotalAccountSOL = useCallback(async () => {
|
||||
if (!mangoAccount) {
|
||||
|
@ -162,7 +164,10 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
|
|||
}
|
||||
|
||||
const isDisabled =
|
||||
(openOrders && openOrders.length > 0) || hasBorrows || hasOpenPositions
|
||||
(openOrders && openOrders.length > 0) ||
|
||||
hasBorrows ||
|
||||
hasOpenPositions ||
|
||||
!!unsettledBalances.length
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} isOpen={isOpen && mangoAccount !== undefined}>
|
||||
|
@ -240,6 +245,12 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
|
|||
{t('close-account:close-open-orders')}
|
||||
</div>
|
||||
) : null}
|
||||
{unsettledBalances.length ? (
|
||||
<div className="flex items-center text-th-fgd-2">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4 text-th-red" />
|
||||
{t('close-account:settle-balances')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
@ -21,8 +21,11 @@ import { useTranslation } from 'next-i18next'
|
|||
import { WalletSelect } from 'components/WalletSelect'
|
||||
import AccountsModal from './AccountsModal'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import NftProfilePicModal from './NftProfilePicModal'
|
||||
import ProfileImage from './ProfileImage'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { breakpoints } from '../components/TradePageGrid'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
|
||||
export const handleWalletConnect = (wallet: Wallet) => {
|
||||
if (!wallet) {
|
||||
|
@ -43,14 +46,24 @@ export const handleWalletConnect = (wallet: Wallet) => {
|
|||
export const ConnectWalletButton: React.FC = () => {
|
||||
const { connected, publicKey, wallet, wallets, select } = useWallet()
|
||||
const { t } = useTranslation(['common', 'profile'])
|
||||
const pfp = useMangoStore((s) => s.wallet.pfp)
|
||||
const router = useRouter()
|
||||
const loadingTransaction = useMangoStore(
|
||||
(s) => s.wallet.nfts.loadingTransaction
|
||||
)
|
||||
const set = useMangoStore((s) => s.set)
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const [showAccountsModal, setShowAccountsModal] = useState(false)
|
||||
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
|
||||
const actions = useMangoStore((s) => s.actions)
|
||||
const profileDetails = useMangoStore((s) => s.profile.details)
|
||||
const loadProfileDetails = useMangoStore((s) => s.profile.loadDetails)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
|
||||
useEffect(() => {
|
||||
if (publicKey) {
|
||||
actions.fetchProfileDetails(publicKey.toString())
|
||||
}
|
||||
}, [publicKey])
|
||||
|
||||
const installedWallets = useMemo(() => {
|
||||
const installed: Wallet[] = []
|
||||
|
@ -80,10 +93,6 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
setShowAccountsModal(false)
|
||||
}, [])
|
||||
|
||||
const handleCloseProfilePicModal = useCallback(() => {
|
||||
setShowProfilePicModal(false)
|
||||
}, [])
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
wallet?.adapter?.disconnect()
|
||||
set((state) => {
|
||||
|
@ -115,16 +124,27 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
{({ open }) => (
|
||||
<div className="relative" id="profile-menu-tip">
|
||||
<Menu.Button
|
||||
className={`flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-button hover:bg-th-bkg-4 hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none ${
|
||||
className={`flex h-14 ${
|
||||
!isMobile ? 'w-48 border-x border-th-bkg-3 px-3' : ''
|
||||
} items-center rounded-none rounded-full hover:bg-th-bkg-2 focus:outline-none ${
|
||||
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : ''
|
||||
}`}
|
||||
>
|
||||
<ProfileImage
|
||||
thumbHeightClass="h-10"
|
||||
thumbWidthClass="w-10"
|
||||
placeholderHeightClass="h-6"
|
||||
placeholderWidthClass="w-6"
|
||||
/>
|
||||
<ProfileImage imageSize="40" placeholderSize="24" />
|
||||
{!loadProfileDetails && !isMobile ? (
|
||||
<div className="ml-2 w-32 text-left">
|
||||
<p className="mb-0.5 truncate text-xs font-bold capitalize text-th-fgd-1">
|
||||
{profileDetails.profile_name}
|
||||
</p>
|
||||
<p className="mb-0 text-xs text-th-fgd-4">
|
||||
{profileDetails.wallet_pk
|
||||
? abbreviateAddress(
|
||||
new PublicKey(profileDetails.wallet_pk)
|
||||
)
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
|
@ -137,7 +157,18 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-20 mt-1 w-48 space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5">
|
||||
<Menu.Items className="absolute right-0 z-20 mt-1 w-48 space-y-1.5 rounded-md bg-th-bkg-2 px-4 py-2.5">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => router.push('/profile')}
|
||||
>
|
||||
<UserCircleIcon className="h-4 w-4" />
|
||||
<div className="pl-2 text-left">
|
||||
{t('profile:profile')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
|
@ -149,20 +180,7 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => setShowProfilePicModal(true)}
|
||||
>
|
||||
<UserCircleIcon className="h-4 w-4" />
|
||||
<div className="pl-2 text-left">
|
||||
{pfp?.isAvailable
|
||||
? t('profile:edit-profile-pic')
|
||||
: t('profile:set-profile-pic')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer focus:outline-none md:hover:text-th-primary"
|
||||
onClick={handleDisconnect}
|
||||
>
|
||||
<LogoutIcon className="h-4 w-4" />
|
||||
|
@ -181,18 +199,18 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
</Menu>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-14 justify-between divide-x divide-th-bkg-3"
|
||||
className="flex h-14 divide-x divide-th-bkg-3"
|
||||
id="connect-wallet-tip"
|
||||
>
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={!mangoGroup}
|
||||
className="rounded-none bg-th-primary-dark text-th-bkg-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-wait disabled:text-th-bkg-2"
|
||||
className="rounded-none bg-th-primary-dark text-th-bkg-1 focus:outline-none disabled:cursor-wait disabled:text-th-bkg-2"
|
||||
>
|
||||
<div className="default-transition flex h-full flex-row items-center justify-center px-3">
|
||||
<WalletIcon className="mr-2 h-4 w-4 fill-current" />
|
||||
<div className="text-left">
|
||||
<div className="mb-0.5 whitespace-nowrap font-bold">
|
||||
<div className="mb-1 whitespace-nowrap font-bold leading-none">
|
||||
{t('connect')}
|
||||
</div>
|
||||
{wallet?.adapter?.name && (
|
||||
|
@ -214,12 +232,6 @@ export const ConnectWalletButton: React.FC = () => {
|
|||
isOpen={showAccountsModal}
|
||||
/>
|
||||
)}
|
||||
{showProfilePicModal && (
|
||||
<NftProfilePicModal
|
||||
onClose={handleCloseProfilePicModal}
|
||||
isOpen={showProfilePicModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { PlusCircleIcon, TrashIcon } from '@heroicons/react/outline'
|
||||
import { PlusCircleIcon, TrashIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import Input, { Label } from './Input'
|
||||
import { ElementTitle } from './styles'
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
ExclamationCircleIcon,
|
||||
XIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import Input, { Label } from './Input'
|
||||
import Tooltip from './Tooltip'
|
||||
import Button, { IconButton } from './Button'
|
||||
|
@ -119,7 +119,7 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-primary" />
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</ElementTitle>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react'
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/outline'
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import Input, { Label } from './Input'
|
||||
import AccountSelect from './AccountSelect'
|
||||
|
@ -14,6 +14,8 @@ import { sleep, trimDecimals } from '../utils'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import ButtonGroup from './ButtonGroup'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import MangoAccountSelect from './MangoAccountSelect'
|
||||
import { MangoAccount } from '@blockworks-foundation/mango-client'
|
||||
|
||||
interface DepositModalProps {
|
||||
onClose: () => void
|
||||
|
@ -38,6 +40,9 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
|
|||
const actions = useMangoStore((s) => s.actions)
|
||||
const [selectedAccount, setSelectedAccount] = useState(walletTokens[0])
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
|
||||
const [depositMangoAccount, setDepositMangoAccount] =
|
||||
useState<MangoAccount | null>(mangoAccount)
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenSymbol) {
|
||||
|
@ -60,14 +65,13 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
|
|||
}
|
||||
|
||||
const handleDeposit = () => {
|
||||
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
|
||||
if (!wallet) return
|
||||
if (!wallet || !depositMangoAccount) return
|
||||
|
||||
setSubmitting(true)
|
||||
deposit({
|
||||
amount: parseFloat(inputAmount),
|
||||
fromTokenAcc: selectedAccount.account,
|
||||
mangoAccount,
|
||||
mangoAccount: depositMangoAccount,
|
||||
wallet,
|
||||
})
|
||||
.then((response) => {
|
||||
|
@ -191,6 +195,15 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
|
|||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{mangoAccounts.length > 1 ? (
|
||||
<div className="mb-4">
|
||||
<Label>{t('to-account')}</Label>
|
||||
<MangoAccountSelect
|
||||
onChange={(v) => setDepositMangoAccount(v)}
|
||||
value={depositMangoAccount}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<AccountSelect
|
||||
accounts={walletTokens}
|
||||
selectedAccount={selectedAccount}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import { FAVORITE_MARKETS_KEY } from './TradeNavMenu'
|
||||
import { StarIcon } from '@heroicons/react/solid'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
|
||||
import { StarIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
import Link from 'next/link'
|
||||
|
@ -31,7 +30,7 @@ const FavoritesShortcutBar = () => {
|
|||
return !isMobile ? (
|
||||
<Transition
|
||||
appear={true}
|
||||
className="flex items-center space-x-4 bg-th-bkg-3 px-4 py-2 xl:px-6"
|
||||
className="flex items-center space-x-4 border-b border-th-bkg-3 py-1 px-6"
|
||||
show={favoriteMarkets.length > 0}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0"
|
||||
|
|
|
@ -35,14 +35,14 @@ export const FiveOhFive = ({ error }) => {
|
|||
const Icon = showDetails ? ChevronDownIcon : ChevronRightIcon
|
||||
|
||||
return (
|
||||
<div className="bg-bg-texture flex min-h-screen flex-col bg-cover bg-bottom bg-no-repeat">
|
||||
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
|
||||
<main className="my-[-2] mx-auto w-full max-w-7xl flex-grow px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-screen flex-col bg-th-bkg-1">
|
||||
<div className="absolute top-0 h-2 w-full bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
|
||||
<main className="mx-auto w-full max-w-7xl flex-grow px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex-shrink-0 pt-16">
|
||||
<img
|
||||
className="mx-auto h-12 w-auto"
|
||||
src="/assets/logotext.svg"
|
||||
alt="Workflow"
|
||||
src="/assets/icons/mngo.svg"
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-auto max-w-xl py-16 sm:py-24">
|
||||
|
@ -50,10 +50,10 @@ export const FiveOhFive = ({ error }) => {
|
|||
<p className="text-sm font-semibold uppercase tracking-wide">
|
||||
<GradientText>500 error</GradientText>
|
||||
</p>
|
||||
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
|
||||
<h1 className="mt-2 text-3xl font-extrabold tracking-tight text-white sm:text-5xl">
|
||||
Something went wrong
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-gray-500">
|
||||
<p className="mt-2 text-base text-th-fgd-3">
|
||||
The page you are looking for could not be loaded.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -85,7 +85,7 @@ export const FiveOhFive = ({ error }) => {
|
|||
<div className="flex flex-col items-center">
|
||||
<div className="mt-10 flex flex-row">
|
||||
<button
|
||||
className="mx-2 whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
|
||||
className="mx-2 whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4"
|
||||
onClick={() => location.reload()}
|
||||
>
|
||||
Refresh and try again
|
||||
|
@ -109,7 +109,7 @@ export const FiveOhFive = ({ error }) => {
|
|||
</div>
|
||||
</main>
|
||||
<footer className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="border-t border-gray-200 py-10 text-center md:flex md:justify-between">
|
||||
<div className="border-t border-th-bkg-4 py-10 text-center md:flex md:justify-between">
|
||||
<div className="mt-6 flex justify-center space-x-8 md:mt-0">
|
||||
{social.map((item, itemIdx) => (
|
||||
<a
|
||||
|
@ -124,7 +124,7 @@ export const FiveOhFive = ({ error }) => {
|
|||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
|
||||
<div className="absolute bottom-0 h-2 w-full bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { FunctionComponent, useCallback } from 'react'
|
||||
import { LinkIcon } from '@heroicons/react/outline'
|
||||
import { LinkIcon } from '@heroicons/react/solid'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { MoveIcon } from './icons'
|
||||
import EmptyState from './EmptyState'
|
||||
|
@ -7,6 +7,8 @@ import { useTranslation } from 'next-i18next'
|
|||
import { handleWalletConnect } from 'components/ConnectWalletButton'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
|
||||
interface FloatingElementProps {
|
||||
className?: string
|
||||
|
@ -24,6 +26,8 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
|
|||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const router = useRouter()
|
||||
const { pubkey } = router.query
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
if (wallet) {
|
||||
|
@ -33,7 +37,9 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`thin-scroll relative overflow-auto overflow-x-hidden rounded-lg bg-th-bkg-2 p-2.5 md:p-4 ${className}`}
|
||||
className={`thin-scroll relative overflow-auto overflow-x-hidden rounded-md ${
|
||||
!isMobile ? 'border border-th-bkg-3' : ''
|
||||
} bg-th-bkg-1 p-2.5 md:p-4 ${className}`}
|
||||
>
|
||||
{!connected && showConnect && !pubkey ? (
|
||||
<div className="absolute top-0 left-0 z-10 h-full w-full">
|
||||
|
@ -46,7 +52,7 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
|
|||
title={t('connect-wallet')}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 h-full w-full rounded-lg bg-th-bkg-2 opacity-50" />
|
||||
<div className="absolute top-0 left-0 h-full w-full rounded-lg bg-th-bkg-1 opacity-50" />
|
||||
</div>
|
||||
) : null}
|
||||
{!uiLocked ? (
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import useInterval from '../hooks/useInterval'
|
||||
import { SECONDS } from '../stores/useMangoStore'
|
||||
import { CLUSTER, SECONDS } from '../stores/useMangoStore'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ExclamationIcon } from '@heroicons/react/outline'
|
||||
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { Connection } from '@solana/web3.js'
|
||||
|
||||
const tpsAlertThreshold = 1000
|
||||
|
@ -43,7 +43,7 @@ const GlobalNotification = () => {
|
|||
getRecentPerformance(setShow, setTps)
|
||||
}, 45 * SECONDS)
|
||||
|
||||
if (show) {
|
||||
if (show && CLUSTER == 'mainnet') {
|
||||
return (
|
||||
<div className="flex items-center bg-th-bkg-4 text-th-fgd-2">
|
||||
<div className="flex w-full items-center justify-center p-1">
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
const HealthHeart = ({ health, size }: { health: number; size: number }) => {
|
||||
const styles = {
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={
|
||||
health > 15 && health < 50
|
||||
? 'text-th-orange'
|
||||
: health > 50
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
}
|
||||
style={styles}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<g transform-origin="center">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="scale"
|
||||
keyTimes="0;0.5;1"
|
||||
values="1;1.1;1"
|
||||
dur={health > 15 && health < 50 ? '1s' : health > 50 ? '2s' : '0.33s'}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="0.8;1;0.8"
|
||||
dur={health > 15 && health < 50 ? '1s' : health > 50 ? '2s' : '0.33s'}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default HealthHeart
|
|
@ -4,7 +4,7 @@ import {
|
|||
ExclamationCircleIcon,
|
||||
ExclamationIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
|
||||
interface InlineNotificationProps {
|
||||
desc?: string | (() => string)
|
||||
|
@ -38,7 +38,7 @@ const InlineNotification: FunctionComponent<InlineNotificationProps> = ({
|
|||
<ExclamationIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-orange" />
|
||||
) : null}
|
||||
{type === 'info' ? (
|
||||
<InformationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-fgd-3" />
|
||||
<InformationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-fgd-4" />
|
||||
) : null}
|
||||
<div>
|
||||
<div className="text-th-fgd-3">{title}</div>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,225 @@
|
|||
import SideNav from './SideNav'
|
||||
import { breakpoints } from '../components/TradePageGrid'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import BottomBar from './mobile/BottomBar'
|
||||
import { ConnectWalletButton } from './ConnectWalletButton'
|
||||
import GlobalNotification from './GlobalNotification'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { abbreviateAddress } from 'utils'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import AccountsModal from './AccountsModal'
|
||||
import { useRouter } from 'next/router'
|
||||
import FavoritesShortcutBar from './FavoritesShortcutBar'
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ChevronRightIcon,
|
||||
CogIcon,
|
||||
ExclamationCircleIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import Button, { IconButton } from './Button'
|
||||
import SettingsModal from './SettingsModal'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import DepositModal from './DepositModal'
|
||||
import WithdrawModal from './WithdrawModal'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
const { t } = useTranslation(['common', 'delegate'])
|
||||
const { connected, publicKey } = useWallet()
|
||||
const { mangoAccount, initialLoad } = useMangoAccount()
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [showAccountsModal, setShowAccountsModal] = useState(false)
|
||||
const [showDepositModal, setShowDepositModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
const router = useRouter()
|
||||
const { pathname } = router
|
||||
const { pubkey } = router.query
|
||||
|
||||
const canWithdraw =
|
||||
mangoAccount?.owner && publicKey
|
||||
? mangoAccount?.owner?.equals(publicKey)
|
||||
: false
|
||||
|
||||
useEffect(() => {
|
||||
const collapsed = width ? width < breakpoints.lg : false
|
||||
setIsCollapsed(collapsed)
|
||||
}, [])
|
||||
|
||||
const handleCloseAccounts = useCallback(() => {
|
||||
setShowAccountsModal(false)
|
||||
}, [])
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
setIsCollapsed(!isCollapsed)
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex-grow bg-th-bkg-1 text-th-fgd-1 transition-all`}>
|
||||
<div className="flex">
|
||||
{isMobile ? (
|
||||
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
|
||||
<BottomBar />
|
||||
</div>
|
||||
) : (
|
||||
<div className={isCollapsed ? 'mr-[64px]' : 'mr-[220px]'}>
|
||||
<div className={`fixed z-20 h-screen`}>
|
||||
<button
|
||||
className="absolute -right-4 top-1/2 z-20 h-10 w-4 -translate-y-1/2 transform rounded-none rounded-r bg-th-bkg-4 focus:outline-none"
|
||||
onClick={handleToggleSidebar}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`default-transition h-full w-full ${
|
||||
!isCollapsed ? 'rotate-180' : 'rotate-360'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={`h-full ${!isCollapsed ? 'overflow-y-auto' : ''}`}
|
||||
>
|
||||
<SideNav collapsed={isCollapsed} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full overflow-hidden">
|
||||
<GlobalNotification />
|
||||
<div className="flex h-14 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1 px-6">
|
||||
{mangoAccount && mangoAccount.beingLiquidated ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red" />
|
||||
<span className="text-th-red">{t('being-liquidated')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center text-th-fgd-3">
|
||||
<span className="mb-0 mr-2 text-base">
|
||||
{pubkey
|
||||
? '🔎'
|
||||
: connected
|
||||
? initialLoad
|
||||
? ''
|
||||
: mangoAccount
|
||||
? '🟢'
|
||||
: '👋'
|
||||
: !isMobile
|
||||
? '🔗'
|
||||
: ''}
|
||||
</span>
|
||||
{connected || pubkey ? (
|
||||
!initialLoad ? (
|
||||
mangoAccount ? (
|
||||
<div
|
||||
className="default-transition flex items-center font-bold text-th-fgd-1 hover:text-th-fgd-3"
|
||||
role="button"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
{`${
|
||||
mangoAccount.name
|
||||
? mangoAccount.name
|
||||
: abbreviateAddress(mangoAccount.publicKey)
|
||||
}`}
|
||||
{publicKey && !mangoAccount.owner.equals(publicKey) ? (
|
||||
<Tooltip content={t('delegate:delegated-account')}>
|
||||
<UsersIcon className="ml-2 h-5 w-5 text-th-fgd-3" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="flex items-center text-th-fgd-3">
|
||||
{t('create-account-helper')}
|
||||
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-th-bkg-3" />
|
||||
)
|
||||
) : !isMobile ? (
|
||||
<span className="flex items-center text-th-fgd-3">
|
||||
{t('connect-helper')}
|
||||
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-4">
|
||||
{!isMobile && connected && !initialLoad ? (
|
||||
<div className="flex space-x-2">
|
||||
{mangoAccount ? (
|
||||
<Button
|
||||
className="flex h-8 w-[86px] items-center justify-center pl-3 pr-3 text-xs"
|
||||
onClick={() => setShowDepositModal(true)}
|
||||
>
|
||||
{t('deposit')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="flex h-8 w-32 items-center justify-center pl-3 pr-3 text-xs"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
{t('create-account')}
|
||||
</Button>
|
||||
)}
|
||||
{canWithdraw ? (
|
||||
<Button
|
||||
className="flex h-8 w-[86px] items-center justify-center pl-3 pr-3 text-xs"
|
||||
onClick={() => setShowWithdrawModal(true)}
|
||||
primary={false}
|
||||
>
|
||||
{t('withdraw')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<IconButton
|
||||
className="h-8 w-8"
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<CogIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<ConnectWalletButton />
|
||||
</div>
|
||||
</div>
|
||||
{pathname === '/' ? <FavoritesShortcutBar /> : null}
|
||||
<div className={pathname === '/' ? 'px-3' : 'px-6 pb-16 md:pb-6'}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showAccountsModal && (
|
||||
<AccountsModal
|
||||
onClose={handleCloseAccounts}
|
||||
isOpen={showAccountsModal}
|
||||
/>
|
||||
)}
|
||||
{showSettingsModal ? (
|
||||
<SettingsModal
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
isOpen={showSettingsModal}
|
||||
/>
|
||||
) : null}
|
||||
{showDepositModal && (
|
||||
<DepositModal
|
||||
isOpen={showDepositModal}
|
||||
onClose={() => setShowDepositModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showWithdrawModal && (
|
||||
<WithdrawModal
|
||||
isOpen={showWithdrawModal}
|
||||
onClose={() => setShowWithdrawModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
|
@ -1,60 +1,94 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { usdFormatter } from '../utils'
|
||||
import { MedalIcon, ProfileIcon } from './icons'
|
||||
import { MedalIcon } from './icons'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import {
|
||||
ChartPieIcon,
|
||||
ExternalLinkIcon,
|
||||
TrendingUpIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { getProfilePicture } from '@solflare-wallet/pfp'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { connectionSelector } from '../stores/selectors'
|
||||
import { abbreviateAddress, usdFormatter } from '../utils'
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid'
|
||||
import ProfileImage from './ProfileImage'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { notify } from 'utils/notifications'
|
||||
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
|
||||
const LeaderboardTable = ({ range = '29' }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const [pnlLeaderboardData, setPnlLeaderboardData] = useState<any[]>([])
|
||||
const [perpPnlLeaderboardData, setPerpPnlLeaderboardData] = useState<any[]>(
|
||||
[]
|
||||
)
|
||||
const [spotPnlLeaderboardData, setSpotPnlLeaderboardData] = useState<any[]>(
|
||||
[]
|
||||
)
|
||||
const [leaderboardType, setLeaderboardType] = useState<string>('total-pnl')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const connection = useMangoStore(connectionSelector)
|
||||
|
||||
const formatLeaderboardData = async (leaderboard) => {
|
||||
const walletPks = leaderboard.map((u) => u.wallet_pk)
|
||||
const profileDetailsResponse = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/user-data/multiple-profile-details?wallet-pks=${walletPks.toString()}`
|
||||
)
|
||||
const parsedProfileDetailsResponse = await profileDetailsResponse.json()
|
||||
const leaderboardData = [] as any[]
|
||||
for (const item of leaderboard) {
|
||||
const profileDetails = parsedProfileDetailsResponse[item.wallet_pk]
|
||||
leaderboardData.push({
|
||||
...item,
|
||||
profile: profileDetails ? profileDetails : null,
|
||||
})
|
||||
}
|
||||
return leaderboardData
|
||||
}
|
||||
|
||||
const fetchPnlLeaderboard = async () => {
|
||||
setLoading(true)
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/pnl-leaderboard?start-date=${dayjs()
|
||||
.utc()
|
||||
.hour(0)
|
||||
.minute(0)
|
||||
.subtract(parseInt(range), 'day')
|
||||
.add(1, 'hour')
|
||||
.format('YYYY-MM-DDThh:00:00')}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
const leaderboardData = [] as any[]
|
||||
for (const item of parsedResponse) {
|
||||
const { isAvailable, url } = await getProfilePicture(
|
||||
connection,
|
||||
item.wallet_pk
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/pnl-leaderboard?start-date=${dayjs()
|
||||
.utc()
|
||||
.hour(0)
|
||||
.minute(0)
|
||||
.subtract(parseInt(range), 'day')
|
||||
.add(1, 'hour')
|
||||
.format('YYYY-MM-DDThh:00:00')}`
|
||||
)
|
||||
leaderboardData.push({
|
||||
...item,
|
||||
pfp: { isAvailable: isAvailable, url: url },
|
||||
})
|
||||
const parsedResponse = await response.json()
|
||||
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
||||
setPnlLeaderboardData(leaderboardData)
|
||||
setLoading(false)
|
||||
} catch {
|
||||
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
|
||||
setLoading(false)
|
||||
}
|
||||
setPnlLeaderboardData(leaderboardData)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const fetchPerpPnlLeaderboard = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/perp-pnl-leaderboard?start-date=${dayjs()
|
||||
.utc()
|
||||
.hour(0)
|
||||
.minute(0)
|
||||
.subtract(parseInt(range), 'day')
|
||||
.add(1, 'hour')
|
||||
.format('YYYY-MM-DDThh:00:00')}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
||||
setPerpPnlLeaderboardData(leaderboardData)
|
||||
setLoading(false)
|
||||
} catch {
|
||||
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSpotPnlLeaderboard = async () => {
|
||||
setLoading(true)
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/perp-pnl-leaderboard?start-date=${dayjs()
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/spot-pnl-leaderboard?start-date=${dayjs()
|
||||
.hour(0)
|
||||
.minute(0)
|
||||
.utc()
|
||||
|
@ -62,7 +96,8 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
.format('YYYY-MM-DDThh:00:00')}`
|
||||
)
|
||||
const parsedResponse = await response.json()
|
||||
setPerpPnlLeaderboardData(parsedResponse)
|
||||
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
||||
setSpotPnlLeaderboardData(leaderboardData)
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
@ -70,21 +105,31 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
useEffect(() => {
|
||||
if (leaderboardType === 'total-pnl') {
|
||||
fetchPnlLeaderboard()
|
||||
} else {
|
||||
} else if (leaderboardType === 'futures-only') {
|
||||
fetchPerpPnlLeaderboard()
|
||||
} else {
|
||||
fetchSpotPnlLeaderboard()
|
||||
}
|
||||
}, [range, leaderboardType])
|
||||
|
||||
useEffect(() => {
|
||||
fetchPerpPnlLeaderboard()
|
||||
fetchSpotPnlLeaderboard()
|
||||
}, [])
|
||||
|
||||
const leaderboardData = useMemo(
|
||||
() =>
|
||||
leaderboardType === 'total-pnl'
|
||||
? pnlLeaderboardData
|
||||
: perpPnlLeaderboardData,
|
||||
[leaderboardType, pnlLeaderboardData, perpPnlLeaderboardData]
|
||||
: leaderboardType === 'futures-only'
|
||||
? perpPnlLeaderboardData
|
||||
: spotPnlLeaderboardData,
|
||||
[
|
||||
leaderboardType,
|
||||
pnlLeaderboardData,
|
||||
perpPnlLeaderboardData,
|
||||
spotPnlLeaderboardData,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -95,14 +140,18 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
setLeaderboardType={setLeaderboardType}
|
||||
range={range}
|
||||
label="total-pnl"
|
||||
icon={<ChartPieIcon className="mr-3 hidden h-6 w-6 lg:block" />}
|
||||
/>
|
||||
<LeaderboardTypeButton
|
||||
leaderboardType={leaderboardType}
|
||||
setLeaderboardType={setLeaderboardType}
|
||||
range={range}
|
||||
label="futures-only"
|
||||
icon={<TrendingUpIcon className="mr-3 hidden h-6 w-6 lg:block" />}
|
||||
/>
|
||||
<LeaderboardTypeButton
|
||||
leaderboardType={leaderboardType}
|
||||
setLeaderboardType={setLeaderboardType}
|
||||
range={range}
|
||||
label="spot-only"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-8">
|
||||
|
@ -114,7 +163,11 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
acc={acc.mango_account}
|
||||
key={acc.mango_account}
|
||||
rawPnl={
|
||||
leaderboardType === 'total-pnl' ? acc.pnl : acc.perp_pnl
|
||||
leaderboardType === 'total-pnl'
|
||||
? acc.pnl
|
||||
: leaderboardType === 'futures-only'
|
||||
? acc.perp_pnl
|
||||
: acc.spot_pnl
|
||||
}
|
||||
pnl={
|
||||
leaderboardType === 'total-pnl'
|
||||
|
@ -123,34 +176,37 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: usdFormatter(acc.perp_pnl)
|
||||
: leaderboardType === 'futures-only'
|
||||
? usdFormatter(acc.perp_pnl)
|
||||
: usdFormatter(acc.spot_pnl)
|
||||
}
|
||||
pfp={acc.pfp}
|
||||
walletPk={acc.wallet_pk}
|
||||
profile={acc.profile}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -160,7 +216,8 @@ const LeaderboardTable = ({ range = '29' }) => {
|
|||
|
||||
export default LeaderboardTable
|
||||
|
||||
const AccountCard = ({ rank, acc, pnl, pfp, rawPnl }) => {
|
||||
const AccountCard = ({ rank, acc, rawPnl, profile, pnl, walletPk }) => {
|
||||
const router = useRouter()
|
||||
const medalColors =
|
||||
rank === 1
|
||||
? {
|
||||
|
@ -183,51 +240,63 @@ const AccountCard = ({ rank, acc, pnl, pfp, rawPnl }) => {
|
|||
lightest: '#EFBF8D',
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={`https://trade.mango.markets/account?pubkey=${acc}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="default-transition flex items-center rounded-lg p-4 ring-1 ring-inset ring-th-bkg-4 hover:bg-th-bkg-3"
|
||||
>
|
||||
<p className="mb-0 mr-4 font-bold">{rank}</p>
|
||||
<div className="relative mr-3 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-th-bkg-4">
|
||||
{rank < 4 ? (
|
||||
<MedalIcon
|
||||
className="absolute -top-2 -left-2 h-5 w-auto drop-shadow-lg"
|
||||
colors={medalColors}
|
||||
<div className="relative" key={acc}>
|
||||
{profile ? (
|
||||
<button
|
||||
className="absolute left-[118px] bottom-4 flex items-center space-x-2 rounded-full border border-th-fgd-4 px-2 py-1 hover:border-th-fgd-2 hover:filter"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/profile?name=${profile?.profile_name.replace(/\s/g, '-')}`,
|
||||
undefined,
|
||||
{
|
||||
shallow: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<p className="mb-0 text-xs capitalize text-th-fgd-3">
|
||||
{profile?.profile_name}
|
||||
</p>
|
||||
</button>
|
||||
) : null}
|
||||
<a
|
||||
className="default-transition block flex h-[112px] w-full rounded-md border border-th-bkg-4 p-4 hover:border-th-fgd-4 sm:h-[84px] sm:justify-between sm:pb-4"
|
||||
href={`/account?pubkey=${acc}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<p className="my-auto mr-4 flex w-5 justify-center font-bold">{rank}</p>
|
||||
<div className="relative my-auto">
|
||||
{rank < 4 ? (
|
||||
<MedalIcon
|
||||
className="absolute -top-1 -left-1 z-10 h-5 w-auto drop-shadow-lg"
|
||||
colors={medalColors}
|
||||
/>
|
||||
) : null}
|
||||
<ProfileImage
|
||||
imageSize="56"
|
||||
placeholderSize="32"
|
||||
publicKey={walletPk}
|
||||
/>
|
||||
) : null}
|
||||
{pfp?.isAvailable ? (
|
||||
<img
|
||||
alt=""
|
||||
src={pfp.url}
|
||||
className={`default-transition h-12 w-12 rounded-full hover:opacity-60
|
||||
`}
|
||||
/>
|
||||
) : (
|
||||
<ProfileIcon className={`h-7 w-7 text-th-fgd-3`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p className="mb-0 text-th-fgd-2">{`${acc.slice(0, 5)}...${acc.slice(
|
||||
-5
|
||||
)}`}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={`text-base font-bold text-th-fgd-2 sm:text-lg ${
|
||||
rawPnl > 0 ? 'text-th-green' : 'text-th-red'
|
||||
}`}
|
||||
>
|
||||
{pnl}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-3 flex flex-col sm:flex-grow sm:flex-row sm:justify-between">
|
||||
<p className="mb-0 font-bold text-th-fgd-2">
|
||||
{abbreviateAddress(new PublicKey(acc))}
|
||||
</p>
|
||||
|
||||
<span
|
||||
className={`flex items-center text-lg font-bold ${
|
||||
rawPnl > 0 ? 'text-th-green' : 'text-th-red'
|
||||
}`}
|
||||
>
|
||||
{pnl}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLinkIcon className="ml-3 h-4 w-4 flex-shrink-0 text-th-fgd-3" />
|
||||
</a>
|
||||
<div className="my-auto ml-auto">
|
||||
<ChevronRightIcon className="ml-2 mt-0.5 h-5 w-5 text-th-fgd-4" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -237,26 +306,34 @@ const LeaderboardTypeButton = ({
|
|||
range,
|
||||
icon,
|
||||
label,
|
||||
}: {
|
||||
leaderboardType: string
|
||||
setLeaderboardType: (x) => void
|
||||
range: string
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
return (
|
||||
<button
|
||||
className={`relative flex w-full items-center justify-center rounded-md p-4 text-center lg:h-20 lg:justify-start lg:text-left ${
|
||||
className={`relative flex w-full items-center justify-center rounded-md p-4 text-center lg:h-20 lg:justify-start lg:px-6 lg:text-left ${
|
||||
leaderboardType === label
|
||||
? 'bg-th-bkg-4 text-th-fgd-1 after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:transform after:border-l-[12px] after:border-r-[12px] after:border-t-[12px] after:border-l-transparent after:border-t-th-bkg-4 after:border-r-transparent lg:after:left-[100%] lg:after:top-1/2 lg:after:-translate-x-0 lg:after:-translate-y-1/2 lg:after:border-r-0 lg:after:border-b-[12px] lg:after:border-t-transparent lg:after:border-b-transparent lg:after:border-l-th-bkg-4'
|
||||
: 'bg-th-bkg-3 text-th-fgd-4 hover:bg-th-bkg-4'
|
||||
? 'bg-th-bkg-3 text-th-fgd-1 after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:transform after:border-l-[12px] after:border-r-[12px] after:border-t-[12px] after:border-l-transparent after:border-t-th-bkg-3 after:border-r-transparent lg:after:left-[100%] lg:after:top-1/2 lg:after:-translate-x-0 lg:after:-translate-y-1/2 lg:after:border-r-0 lg:after:border-b-[12px] lg:after:border-t-transparent lg:after:border-b-transparent lg:after:border-l-th-bkg-3'
|
||||
: 'bg-th-bkg-2 text-th-fgd-3 md:hover:bg-th-bkg-3'
|
||||
}`}
|
||||
onClick={() => setLeaderboardType(label)}
|
||||
>
|
||||
{icon}
|
||||
<div>
|
||||
<div className="font-bold sm:text-lg">{t(label)}</div>
|
||||
<span className="text-th-fgd-4">
|
||||
<span className="text-sm text-th-fgd-4">
|
||||
{range === '9999'
|
||||
? 'All-time'
|
||||
: range === '29'
|
||||
? '30-day'
|
||||
: `${range}-day`}
|
||||
? 'Last 30 days'
|
||||
: range === '1'
|
||||
? 'Last 24 hours'
|
||||
: `Last ${range} days`}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { MangoAccount, MangoGroup } from '@blockworks-foundation/mango-client'
|
||||
import {
|
||||
ArrowSmDownIcon,
|
||||
ArrowSmUpIcon,
|
||||
HeartIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoStore from 'stores/useMangoStore'
|
||||
import { abbreviateAddress } from 'utils'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
export const numberCurrencyCompacter = Intl.NumberFormat('en-us', {
|
||||
notation: 'compact',
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
|
||||
const MangoAccountCard = ({
|
||||
mangoAccount,
|
||||
pnl,
|
||||
}: {
|
||||
mangoAccount: MangoAccount
|
||||
pnl?: number
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const { publicKey } = useWallet()
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-1 flex items-center font-bold text-th-fgd-1">
|
||||
{pnl ? (
|
||||
<a
|
||||
className="default-transition text-th-fgd-1 hover:text-th-fgd-3"
|
||||
href={`https://trade.mango.markets/account?pubkey=${mangoAccount.publicKey.toString()}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{mangoAccount?.name || abbreviateAddress(mangoAccount.publicKey)}
|
||||
</a>
|
||||
) : (
|
||||
<span>
|
||||
{mangoAccount?.name || abbreviateAddress(mangoAccount.publicKey)}
|
||||
</span>
|
||||
)}
|
||||
{publicKey && !mangoAccount?.owner.equals(publicKey) ? (
|
||||
<Tooltip content={t('delegate:delegated-account')}>
|
||||
<UsersIcon className="ml-1.5 h-3 w-3 text-th-fgd-3" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</p>
|
||||
{mangoGroup && (
|
||||
<div className="text-xs text-th-fgd-3">
|
||||
<AccountInfo
|
||||
mangoGroup={mangoGroup}
|
||||
mangoAccount={mangoAccount}
|
||||
pnl={pnl}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MangoAccountCard
|
||||
|
||||
const AccountInfo = ({
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
pnl,
|
||||
}: {
|
||||
mangoGroup: MangoGroup
|
||||
mangoAccount: MangoAccount
|
||||
pnl?: number
|
||||
}) => {
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
if (!mangoCache) {
|
||||
return null
|
||||
}
|
||||
const accountEquity = mangoAccount.computeValue(mangoGroup, mangoCache)
|
||||
const health = mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-xs text-th-fgd-3">
|
||||
{numberCurrencyCompacter.format(accountEquity.toNumber())}
|
||||
<span className="pl-2 pr-1 text-th-fgd-4">|</span>
|
||||
{pnl ? (
|
||||
<span
|
||||
className={`flex items-center ${
|
||||
pnl < 0 ? 'text-th-red' : 'text-th-green'
|
||||
}`}
|
||||
>
|
||||
{pnl < 0 ? (
|
||||
<ArrowSmDownIcon className="mr-0.5 h-4 w-4" />
|
||||
) : (
|
||||
<ArrowSmUpIcon className="mr-0.5 h-4 w-4" />
|
||||
)}
|
||||
{numberCurrencyCompacter.format(pnl)}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`flex items-center ${
|
||||
Number(health) < 15
|
||||
? 'text-th-red'
|
||||
: Number(health) < 30
|
||||
? 'text-th-orange'
|
||||
: 'text-th-green'
|
||||
}`}
|
||||
>
|
||||
<HeartIcon className="mr-0.5 h-4 w-4" />
|
||||
{Number(health) > 100 ? '>100' : health.toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -48,7 +48,14 @@ const MangoAccountSelect = ({
|
|||
disabled={disabled}
|
||||
value={
|
||||
<div className="text-left">
|
||||
{abbreviateAddress(selectedMangoAccount?.publicKey)}
|
||||
<p className="mb-0 font-bold text-th-fgd-2">
|
||||
{selectedMangoAccount?.name
|
||||
? selectedMangoAccount.name
|
||||
: t('account')}
|
||||
</p>
|
||||
<p className="mb-0 text-xs">
|
||||
{abbreviateAddress(selectedMangoAccount?.publicKey)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
onChange={handleSelectMangoAccount}
|
||||
|
@ -58,7 +65,18 @@ const MangoAccountSelect = ({
|
|||
{mangoAccounts.length ? (
|
||||
mangoAccounts.map((ma, index) => (
|
||||
<Select.Option key={index} value={ma.publicKey.toString()}>
|
||||
{abbreviateAddress(ma.publicKey)}
|
||||
<div className="text-left">
|
||||
<span
|
||||
className={`mb-0 font-bold ${
|
||||
value?.publicKey.toString() === ma.publicKey.toString()
|
||||
? 'text-th-primary'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{ma?.name ? ma.name : t('account')}
|
||||
</span>
|
||||
<p className="mb-0 text-xs">{abbreviateAddress(ma?.publicKey)}</p>
|
||||
</div>
|
||||
</Select.Option>
|
||||
))
|
||||
) : (
|
||||
|
|
|
@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next'
|
|||
import SwitchMarketDropdown from './SwitchMarketDropdown'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { InformationCircleIcon } from '@heroicons/react/outline'
|
||||
import { InformationCircleIcon } from '@heroicons/react/solid'
|
||||
|
||||
const OraclePrice = () => {
|
||||
const oraclePrice = useOraclePrice()
|
||||
|
@ -61,15 +61,15 @@ const MarketDetails = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col md:px-3 md:pb-2 md:pt-3 lg:flex-row lg:items-center lg:justify-between`}
|
||||
className={`relative flex flex-col md:px-3 md:pt-3 md:pb-2 lg:flex-row lg:items-end lg:justify-between`}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center">
|
||||
<div className="hidden md:block md:pb-4 md:pr-6 lg:pb-0">
|
||||
<div className="flex flex-col lg:flex-row lg:flex-wrap">
|
||||
<div className="hidden md:block md:pr-6 lg:pb-0">
|
||||
<div className="flex items-center">
|
||||
<SwitchMarketDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-2 md:grid-cols-3 lg:grid-flow-col lg:grid-cols-none lg:grid-rows-1 lg:gap-6">
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-2 md:mt-2.5 md:grid-cols-3 md:pr-20 lg:grid-flow-col lg:grid-cols-none lg:grid-rows-1 lg:gap-6">
|
||||
<div className="flex items-center justify-between md:block">
|
||||
<div className="text-th-fgd-3 md:pb-0.5 md:text-[0.65rem]">
|
||||
{t('oracle-price')}
|
||||
|
@ -111,7 +111,7 @@ const MarketDetails = () => {
|
|||
content={t('tooltip-funding')}
|
||||
placement={'bottom'}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-th-fgd-1 md:text-xs">
|
||||
|
@ -138,7 +138,7 @@ const MarketDetails = () => {
|
|||
)} ${baseSymbol}`}
|
||||
placement={'bottom'}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
|
||||
import Link from 'next/link'
|
||||
import * as MonoIcons from './icons'
|
||||
import { initialMarket } from './SettingsModal'
|
||||
|
|
|
@ -55,7 +55,7 @@ const MarketNavItem: FunctionComponent<MarketNavItemProps> = ({
|
|||
<div className="text-th-fgd-3">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={`flex w-full items-center justify-between px-2 py-2 font-normal hover:bg-th-bkg-4 hover:text-th-primary ${
|
||||
className={`flex w-full items-center justify-between px-2 py-2 font-normal md:hover:bg-th-bkg-4 md:hover:text-th-primary ${
|
||||
asPath.includes(market.name) ||
|
||||
(asPath === '/' && initialMarket.name === market.name)
|
||||
? 'text-th-primary'
|
||||
|
|
|
@ -205,7 +205,9 @@ export default function MarketPosition() {
|
|||
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
const price = useMangoStore((s) => s.tradeForm.price)
|
||||
const perpAccounts = useMangoStore((s) => s.selectedMangoAccount.perpAccounts)
|
||||
const perpPositions = useMangoStore(
|
||||
(s) => s.selectedMangoAccount.perpPositions
|
||||
)
|
||||
const baseSymbol = marketConfig.baseSymbol
|
||||
const marketName = marketConfig.name
|
||||
const router = useRouter()
|
||||
|
@ -262,10 +264,10 @@ export default function MarketPosition() {
|
|||
breakEvenPrice = 0,
|
||||
notionalSize = 0,
|
||||
unsettledPnl = 0,
|
||||
} = perpAccounts.length
|
||||
? perpAccounts.find((pa) =>
|
||||
pa.perpMarket.publicKey.equals(selectedMarket.publicKey)
|
||||
)
|
||||
} = perpPositions.length
|
||||
? perpPositions.find((p) =>
|
||||
p?.perpMarket.publicKey.equals(selectedMarket.publicKey)
|
||||
) ?? {}
|
||||
: {}
|
||||
|
||||
function SettlePnlTooltip() {
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { MenuIcon, PlusCircleIcon } from '@heroicons/react/outline'
|
||||
import MarketMenuItem from './MarketMenuItem'
|
||||
import { LinkButton } from './Button'
|
||||
import MarketsModal from './MarketsModal'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoStore from 'stores/useMangoStore'
|
||||
|
||||
const MarketSelect = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
||||
const [showMarketsModal, setShowMarketsModal] = useState(false)
|
||||
const [hiddenMarkets] = useLocalStorageState('hiddenMarkets', [])
|
||||
const [sortedMarkets, setSortedMarkets] = useState<any[]>([])
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
|
||||
useEffect(() => {
|
||||
if (groupConfig) {
|
||||
const markets: any[] = []
|
||||
const allMarkets = [
|
||||
...groupConfig.spotMarkets,
|
||||
...groupConfig.perpMarkets,
|
||||
]
|
||||
allMarkets.forEach((market) => {
|
||||
const base = market.name.slice(0, -5)
|
||||
const found = markets.find((b) => b.baseAsset === base)
|
||||
if (!found) {
|
||||
markets.push({ baseAsset: base, markets: [market] })
|
||||
} else {
|
||||
found.markets.push(market)
|
||||
}
|
||||
})
|
||||
setSortedMarkets(markets)
|
||||
}
|
||||
}, [groupConfig])
|
||||
|
||||
return (
|
||||
<div className="hidden md:flex">
|
||||
<div className="flex h-10 w-full bg-th-bkg-3">
|
||||
<div className="flex items-center bg-th-bkg-4 pl-4 pr-1 lg:pl-9">
|
||||
{isMobile ? (
|
||||
<MenuIcon
|
||||
className="h-5 w-5 cursor-pointer text-th-fgd-1 hover:text-th-primary"
|
||||
onClick={() => setShowMarketsModal(true)}
|
||||
/>
|
||||
) : (
|
||||
<ShowMarketsButton
|
||||
onClick={() => setShowMarketsModal(true)}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderTop: '20px solid transparent',
|
||||
borderBottom: '20px solid transparent',
|
||||
paddingRight: '0.5rem',
|
||||
}}
|
||||
className="border-l-[20px] border-th-bkg-4"
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{sortedMarkets
|
||||
.filter((m) => !hiddenMarkets.includes(m.baseAsset))
|
||||
.map((s) => (
|
||||
<MarketMenuItem
|
||||
key={s.baseAsset}
|
||||
linksArray={s.markets}
|
||||
menuTitle={s.baseAsset}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showMarketsModal ? (
|
||||
<MarketsModal
|
||||
isOpen={showMarketsModal}
|
||||
onClose={() => setShowMarketsModal(false)}
|
||||
markets={sortedMarkets}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ShowMarketsButton = ({ onClick, t }) => (
|
||||
<LinkButton
|
||||
className="flex items-center whitespace-nowrap text-xs font-normal text-th-fgd-2"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusCircleIcon className="mr-1 h-4 w-4" />
|
||||
{t('markets').toUpperCase()}
|
||||
</LinkButton>
|
||||
)
|
||||
|
||||
export default MarketSelect
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { EyeIcon, EyeOffIcon } from '@heroicons/react/outline'
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid'
|
||||
import { ChevronRightIcon, EyeIcon, EyeOffIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
|
@ -83,20 +82,6 @@ const MarketsModal = ({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="bg-[rgba(255,255,255,0.1)] flex items-center justify-between px-2.5 py-0.5 text-th-fgd-3">
|
||||
<StyledColumnHeader>Markets</StyledColumnHeader>
|
||||
<div className="flex justify-between">
|
||||
<StyledColumnHeader className="pr-5 text-right w-20">
|
||||
Price
|
||||
</StyledColumnHeader>
|
||||
<StyledColumnHeader className="text-right w-20">
|
||||
24h Change
|
||||
</StyledColumnHeader>
|
||||
<StyledColumnHeader className="text-right w-20">
|
||||
24h Vol
|
||||
</StyledColumnHeader>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="divide-y divide-th-bkg-4">
|
||||
{mkt.markets.map((m) => (
|
||||
<div
|
||||
|
|
|
@ -38,98 +38,22 @@ const MarketsTable = ({
|
|||
const { items, requestSort, sortConfig } = useSortableData(markets)
|
||||
|
||||
return !isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('name')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('market')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'name'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('last')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('price')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'last'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('change24h')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('rolling-change')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'change24h'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('volumeUsd24h')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('daily-volume')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'volumeUsd24h'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
{isPerpMarket ? (
|
||||
<>
|
||||
<div className={`md:overflow-x-auto`}>
|
||||
<div className={`inline-block min-w-full align-middle`}>
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('funding1h')}
|
||||
onClick={() => requestSort('name')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('average-funding')}
|
||||
{t('market')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'funding1h'
|
||||
sortConfig?.key === 'name'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
|
@ -140,15 +64,15 @@ const MarketsTable = ({
|
|||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center no-underline"
|
||||
onClick={() => requestSort('openInterestUsd')}
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('last')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('open-interest')}
|
||||
{t('price')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'openInterestUsd'
|
||||
sortConfig?.key === 'last'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
|
@ -157,135 +81,227 @@ const MarketsTable = ({
|
|||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
</>
|
||||
) : null}
|
||||
<Th>
|
||||
<span className="flex justify-end">{t('favorite')}</span>
|
||||
</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((market) => {
|
||||
const {
|
||||
baseSymbol,
|
||||
change24h,
|
||||
funding1h,
|
||||
last,
|
||||
name,
|
||||
openInterest,
|
||||
openInterestUsd,
|
||||
volumeUsd24h,
|
||||
} = market
|
||||
const fundingApr = funding1h ? (funding1h * 24 * 365).toFixed(2) : '-'
|
||||
const coingeckoData = coingeckoPrices.find(
|
||||
(asset) => asset.symbol === baseSymbol
|
||||
)
|
||||
const chartData = coingeckoData ? coingeckoData.prices : undefined
|
||||
|
||||
return (
|
||||
<TrBody key={name} className="hover:bg-th-bkg-3">
|
||||
<Td>
|
||||
<Link href={`/?name=${name}`} shallow={true}>
|
||||
<a className="hover:cursor-pointer">
|
||||
<div className="flex h-full items-center text-th-fgd-2 hover:text-th-primary">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
<span className="default-transition">{name}</span>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td className="flex items-center">
|
||||
<div className="w-20">
|
||||
{last ? (
|
||||
formatUsdValue(last)
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="pl-6">
|
||||
{!loadingCoingeckoPrices ? (
|
||||
chartData !== undefined ? (
|
||||
<PriceChart
|
||||
name={name}
|
||||
change24h={change24h}
|
||||
data={chartData}
|
||||
height={40}
|
||||
width={104}
|
||||
/>
|
||||
) : (
|
||||
t('unavailable')
|
||||
)
|
||||
) : (
|
||||
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={change24h >= 0 ? 'text-th-green' : 'text-th-red'}
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('change24h')}
|
||||
>
|
||||
{change24h || change24h === 0 ? (
|
||||
`${(change24h * 100).toFixed(2)}%`
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{volumeUsd24h ? (
|
||||
usdFormatter(volumeUsd24h, 0)
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</Td>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('rolling-change')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'change24h'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('volumeUsd24h')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('daily-volume')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'volumeUsd24h'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
{isPerpMarket ? (
|
||||
<>
|
||||
<Td>
|
||||
{funding1h ? (
|
||||
<>
|
||||
<span>{`${funding1h.toFixed(4)}%`}</span>{' '}
|
||||
<span className="text-xs text-th-fgd-3">{`(${fundingApr}% APR)`}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{openInterestUsd ? (
|
||||
<>
|
||||
<span>{usdFormatter(openInterestUsd, 0)}</span>{' '}
|
||||
{openInterest ? (
|
||||
<div className="text-xs text-th-fgd-4">
|
||||
{openInterest.toLocaleString(undefined, {
|
||||
maximumFractionDigits:
|
||||
perpContractPrecision[baseSymbol],
|
||||
})}{' '}
|
||||
{baseSymbol}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</Td>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('funding1h')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('average-funding')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'funding1h'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center no-underline"
|
||||
onClick={() => requestSort('openInterestUsd')}
|
||||
>
|
||||
<span className="text-left font-normal text-th-fgd-3">
|
||||
{t('open-interest')}
|
||||
</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'openInterestUsd'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
</>
|
||||
) : null}
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<FavoriteMarketButton market={market} />
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Th>
|
||||
<span className="flex justify-end">{t('favorite')}</span>
|
||||
</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((market) => {
|
||||
const {
|
||||
baseSymbol,
|
||||
change24h,
|
||||
funding1h,
|
||||
last,
|
||||
name,
|
||||
openInterest,
|
||||
openInterestUsd,
|
||||
volumeUsd24h,
|
||||
} = market
|
||||
const fundingApr = funding1h
|
||||
? (funding1h * 24 * 365).toFixed(2)
|
||||
: '-'
|
||||
const coingeckoData = coingeckoPrices.find(
|
||||
(asset) => asset.symbol === baseSymbol
|
||||
)
|
||||
const chartData = coingeckoData ? coingeckoData.prices : undefined
|
||||
|
||||
return (
|
||||
<TrBody key={name}>
|
||||
<Td>
|
||||
<Link href={`/?name=${name}`} shallow={true}>
|
||||
<a className="hover:cursor-pointer">
|
||||
<div className="flex h-full items-center text-th-fgd-2 hover:text-th-primary">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
<span className="default-transition">{name}</span>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td className="flex items-center">
|
||||
<div className="w-20">
|
||||
{last ? (
|
||||
formatUsdValue(last)
|
||||
) : (
|
||||
<span className="text-th-fgd-4">
|
||||
{t('unavailable')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="pl-6">
|
||||
{!loadingCoingeckoPrices ? (
|
||||
chartData !== undefined ? (
|
||||
<PriceChart
|
||||
name={name}
|
||||
change24h={change24h}
|
||||
data={chartData}
|
||||
height={40}
|
||||
width={104}
|
||||
/>
|
||||
) : (
|
||||
t('unavailable')
|
||||
)
|
||||
) : (
|
||||
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span
|
||||
className={
|
||||
change24h >= 0 ? 'text-th-green' : 'text-th-red'
|
||||
}
|
||||
>
|
||||
{change24h || change24h === 0 ? (
|
||||
`${(change24h * 100).toFixed(2)}%`
|
||||
) : (
|
||||
<span className="text-th-fgd-4">
|
||||
{t('unavailable')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{volumeUsd24h ? (
|
||||
usdFormatter(volumeUsd24h, 0)
|
||||
) : (
|
||||
<span className="text-th-fgd-4">{t('unavailable')}</span>
|
||||
)}
|
||||
</Td>
|
||||
{isPerpMarket ? (
|
||||
<>
|
||||
<Td>
|
||||
{funding1h ? (
|
||||
<>
|
||||
<span>{`${funding1h.toFixed(4)}%`}</span>{' '}
|
||||
<span className="text-xs text-th-fgd-3">{`(${fundingApr}% APR)`}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-th-fgd-4">
|
||||
{t('unavailable')}
|
||||
</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{openInterestUsd ? (
|
||||
<>
|
||||
<span>{usdFormatter(openInterestUsd, 0)}</span>{' '}
|
||||
{openInterest ? (
|
||||
<div className="text-xs text-th-fgd-4">
|
||||
{openInterest.toLocaleString(undefined, {
|
||||
maximumFractionDigits:
|
||||
perpContractPrecision[baseSymbol],
|
||||
})}{' '}
|
||||
{baseSymbol}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-th-fgd-4">
|
||||
{t('unavailable')}
|
||||
</span>
|
||||
)}
|
||||
</Td>
|
||||
</>
|
||||
) : null}
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<FavoriteMarketButton market={market} />
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{items.map((market) => {
|
||||
|
@ -299,7 +315,7 @@ const MarketsTable = ({
|
|||
return (
|
||||
<Link href={`/?name=${name}`} shallow={true} key={name}>
|
||||
<a
|
||||
className="mb-2 block w-full rounded-lg bg-th-bkg-3 p-4 pb-2.5"
|
||||
className="default-transition mb-2 block w-full rounded-lg border border-th-bkg-3 p-4 pb-2.5 hover:bg-th-bkg-2"
|
||||
onClick={() =>
|
||||
router.push(`/?name=${name}`, undefined, {
|
||||
shallow: true,
|
||||
|
@ -379,7 +395,7 @@ const MarketsTable = ({
|
|||
}
|
||||
|
||||
const COLORS = {
|
||||
GREEN: { Mango: '#AFD803', Dark: '5EBF4d', Light: '5EBF4d' },
|
||||
GREEN: { Mango: '#AFD803', Dark: '#5EBF4d', Light: '#5EBF4d' },
|
||||
RED: { Mango: '#F84638', Dark: '#CC2929', Light: '#CC2929' },
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { Portal } from 'react-portal'
|
||||
import { XIcon } from '@heroicons/react/outline'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
|
||||
const Modal: any = React.forwardRef<any, any>((props, ref) => {
|
||||
const {
|
||||
|
@ -16,7 +16,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
|
|||
return (
|
||||
<Portal>
|
||||
<div
|
||||
className="fixed inset-0 z-40 overflow-y-auto sm:py-8"
|
||||
className="fixed inset-0 z-40 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
@ -41,7 +41,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
|
|||
|
||||
{isOpen ? (
|
||||
<div
|
||||
className={`inline-block min-h-screen bg-th-bkg-2 text-left
|
||||
className={`inline-block min-h-screen border border-th-bkg-3 bg-th-bkg-1 text-left
|
||||
sm:min-h-full sm:rounded-lg ${
|
||||
noPadding ? '' : 'px-8 pt-6 pb-6'
|
||||
} w-full transform align-middle shadow-lg transition-all sm:max-w-md ${className}`}
|
||||
|
@ -51,7 +51,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
|
|||
<div className="">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`absolute right-4 top-4 text-th-fgd-1 hover:text-th-primary focus:outline-none md:right-2 md:top-2`}
|
||||
className={`absolute right-4 top-4 text-th-fgd-1 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
|
||||
>
|
||||
<XIcon className={`h-5 w-5`} />
|
||||
</button>
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import { Fragment, useRef } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid'
|
||||
import Link from 'next/link'
|
||||
|
||||
type NavDropMenuProps = {
|
||||
menuTitle: string | React.ReactNode
|
||||
linksArray: [string, string, boolean, React.ReactNode][]
|
||||
}
|
||||
|
||||
export default function NavDropMenu({
|
||||
menuTitle = '',
|
||||
linksArray = [],
|
||||
}: NavDropMenuProps) {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const toggleMenu = () => {
|
||||
buttonRef?.current?.click()
|
||||
}
|
||||
|
||||
const onHover = (open, action) => {
|
||||
if (
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
toggleMenu()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
{({ open }) => (
|
||||
<div
|
||||
onMouseEnter={() => onHover(open, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHover(open, 'onMouseLeave')}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Popover.Button
|
||||
className={`-mr-3 rounded-none px-3 transition-none focus:bg-th-bkg-3 focus:outline-none ${
|
||||
open && 'bg-th-bkg-3'
|
||||
}`}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<div
|
||||
className={`flex h-14 items-center rounded-none font-bold hover:text-th-primary`}
|
||||
>
|
||||
<span className="font-bold">{menuTitle}</span>
|
||||
<ChevronDownIcon
|
||||
className={`default-transition ml-0.5 h-5 w-5 ${
|
||||
open ? 'rotate-180 transform' : 'rotate-360 transform'
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel className="absolute top-14 z-10">
|
||||
<div className="relative rounded-b-md bg-th-bkg-3 px-4 py-2.5">
|
||||
{linksArray.map(([name, href, isExternal, icon]) =>
|
||||
!isExternal ? (
|
||||
<Link href={href} key={href}>
|
||||
<a className="default-transition flex items-center whitespace-nowrap py-1.5 text-th-fgd-1 hover:text-th-primary">
|
||||
{icon ? <div className="mr-2">{icon}</div> : null}
|
||||
{name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
className="default-transition flex items-center whitespace-nowrap py-1.5 text-th-fgd-1 hover:text-th-primary"
|
||||
href={href}
|
||||
key={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{icon ? <div className="mr-2">{icon}</div> : null}
|
||||
{name}
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
|
@ -2,7 +2,7 @@ import React, { FunctionComponent, useState } from 'react'
|
|||
import {
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import Input, { Label } from './Input'
|
||||
import AccountSelect from './AccountSelect'
|
||||
import { ElementTitle } from './styles'
|
||||
|
@ -56,36 +56,39 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
|
|||
|
||||
const handleNewAccountDeposit = () => {
|
||||
if (!wallet) return
|
||||
setSubmitting(true)
|
||||
deposit({
|
||||
amount: parseFloat(inputAmount),
|
||||
fromTokenAcc: selectedAccount.account,
|
||||
accountName: name,
|
||||
wallet,
|
||||
})
|
||||
.then(async (response) => {
|
||||
await sleep(1000)
|
||||
actions.fetchWalletTokens(wallet)
|
||||
actions.fetchAllMangoAccounts(wallet)
|
||||
if (response && response.length > 0) {
|
||||
onAccountCreation(response[0])
|
||||
notify({
|
||||
title: 'Mango Account Created',
|
||||
txid: response[1],
|
||||
})
|
||||
}
|
||||
setSubmitting(false)
|
||||
validateAmountInput(inputAmount)
|
||||
if (inputAmount) {
|
||||
setSubmitting(true)
|
||||
deposit({
|
||||
amount: parseFloat(inputAmount),
|
||||
fromTokenAcc: selectedAccount.account,
|
||||
accountName: name,
|
||||
wallet,
|
||||
})
|
||||
.catch((e) => {
|
||||
setSubmitting(false)
|
||||
console.error(e)
|
||||
notify({
|
||||
title: t('init-error'),
|
||||
description: e.message,
|
||||
type: 'error',
|
||||
.then(async (response) => {
|
||||
await sleep(1000)
|
||||
actions.fetchWalletTokens(wallet)
|
||||
actions.fetchAllMangoAccounts(wallet)
|
||||
if (response && response.length > 0) {
|
||||
onAccountCreation(response[0])
|
||||
notify({
|
||||
title: 'Mango Account Created',
|
||||
txid: response[1],
|
||||
})
|
||||
}
|
||||
setSubmitting(false)
|
||||
})
|
||||
onAccountCreation()
|
||||
})
|
||||
.catch((e) => {
|
||||
setSubmitting(false)
|
||||
console.error(e)
|
||||
notify({
|
||||
title: t('init-error'),
|
||||
description: e.message,
|
||||
type: 'error',
|
||||
})
|
||||
onAccountCreation()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validateAmountInput = (amount) => {
|
||||
|
@ -148,7 +151,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
|
|||
{t('account-name')}{' '}
|
||||
<span className="ml-1 text-th-fgd-3">{t('optional')}</span>
|
||||
<Tooltip content={t('tooltip-name-onchain')}>
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
<Input
|
||||
|
@ -184,7 +187,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
|
|||
suffix={symbol}
|
||||
/>
|
||||
{invalidAmountMessage ? (
|
||||
<div className="flex items-center pt-1.5 text-th-red">
|
||||
<div className="flex items-center py-1.5 text-th-red">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
||||
{invalidAmountMessage}
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { PublicKey } from '@solana/web3.js'
|
|||
import { notify } from 'utils/notifications'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { PhotographIcon } from '@heroicons/react/outline'
|
||||
import { PhotographIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import { ElementTitle } from './styles'
|
||||
import {
|
||||
|
@ -211,7 +211,7 @@ const NftProfilePicModal = ({ isOpen, onClose }) => {
|
|||
{nfts.map((n) => {
|
||||
return (
|
||||
<button
|
||||
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-3 hover:bg-th-bkg-4 sm:py-4 ${
|
||||
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-3 sm:py-4 md:hover:bg-th-bkg-4 ${
|
||||
selectedProfile?.tokenAddress.toString() === n.tokenAddress
|
||||
? 'border-th-primary'
|
||||
: 'border-th-bkg-3'
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
ExternalLinkIcon,
|
||||
InformationCircleIcon,
|
||||
XCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import useMangoStore, {
|
||||
CLIENT_TX_TIMEOUT,
|
||||
CLUSTER,
|
||||
|
@ -183,7 +183,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
|
|||
<div className={`absolute right-2 top-2 flex-shrink-0`}>
|
||||
<button
|
||||
onClick={hideNotification}
|
||||
className={`text-th-fgd-4 hover:text-th-primary focus:outline-none`}
|
||||
className={`text-th-fgd-4 focus:outline-none md:hover:text-th-primary`}
|
||||
>
|
||||
<span className={`sr-only`}>{t('close')}</span>
|
||||
<svg
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { PencilIcon, TrashIcon, XIcon } from '@heroicons/react/outline'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { PencilIcon, TrashIcon, XIcon } from '@heroicons/react/solid'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import Button, { IconButton } from './Button'
|
||||
|
@ -27,6 +27,7 @@ const DesktopTable = ({
|
|||
cancelledOrderId,
|
||||
editOrderIndex,
|
||||
handleCancelOrder,
|
||||
handleCancelAllOrders,
|
||||
handleModifyOrder,
|
||||
modifiedOrderId,
|
||||
openOrders,
|
||||
|
@ -61,6 +62,7 @@ const DesktopTable = ({
|
|||
return <span>{market.name}</span>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
|
@ -162,6 +164,16 @@ const DesktopTable = ({
|
|||
<span>{t('cancel')}</span>
|
||||
)}
|
||||
</Button>
|
||||
{openOrders.filter(
|
||||
(o) => o.market.config.name === market.config.name
|
||||
).length > 1 ? (
|
||||
<Button
|
||||
onClick={() => handleCancelAllOrders(market.account)}
|
||||
className="-my-1 h-7 pt-0 pb-0 pl-3 pr-3 text-xs text-th-red"
|
||||
>
|
||||
{t('cancel-all') + ' ' + market.config.name}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -205,6 +217,7 @@ const MobileTable = ({
|
|||
cancelledOrderId,
|
||||
editOrderIndex,
|
||||
handleCancelOrder,
|
||||
handleCancelAllOrders,
|
||||
handleModifyOrder,
|
||||
modifiedOrderId,
|
||||
openOrders,
|
||||
|
@ -222,7 +235,7 @@ const MobileTable = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
{openOrders.map(({ market, order }, index) => {
|
||||
const editThisOrder = editOrderIndex === index
|
||||
return (
|
||||
|
@ -289,6 +302,15 @@ const MobileTable = ({
|
|||
<TrashIcon className="h-5 w-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
{openOrders.filter(
|
||||
(o) => o.market.config.name === market.config.name
|
||||
).length > 1 ? (
|
||||
<IconButton
|
||||
onClick={() => handleCancelAllOrders(market.account)}
|
||||
>
|
||||
<TrashIcon className="h-5 w-5 text-th-red" />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{editThisOrder ? (
|
||||
|
@ -353,6 +375,72 @@ const OpenOrdersTable = () => {
|
|||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
|
||||
const handleCancelAllOrders = async (market: PerpMarket | Market) => {
|
||||
const selectedMangoGroup =
|
||||
useMangoStore.getState().selectedMangoGroup.current
|
||||
const selectedMangoAccount =
|
||||
useMangoStore.getState().selectedMangoAccount.current
|
||||
const mangoClient = useMangoStore.getState().connection.client
|
||||
try {
|
||||
if (!selectedMangoGroup || !selectedMangoAccount || !wallet) return
|
||||
|
||||
if (market instanceof PerpMarket) {
|
||||
const txids = await mangoClient.cancelAllPerpOrders(
|
||||
selectedMangoGroup,
|
||||
[market],
|
||||
selectedMangoAccount,
|
||||
wallet.adapter
|
||||
)
|
||||
if (txids) {
|
||||
for (const txid of txids) {
|
||||
notify({
|
||||
title: t('cancel-all-success'),
|
||||
description: '',
|
||||
txid,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
notify({
|
||||
title: t('cancel-all-error'),
|
||||
description: t('transaction-failed'),
|
||||
})
|
||||
}
|
||||
actions.reloadOrders()
|
||||
} else if (market instanceof Market) {
|
||||
const txid = await mangoClient.cancelAllSpotOrders(
|
||||
selectedMangoGroup,
|
||||
selectedMangoAccount,
|
||||
market,
|
||||
wallet.adapter,
|
||||
20
|
||||
)
|
||||
if (txid) {
|
||||
notify({
|
||||
title: t('cancel-all-success'),
|
||||
description: '',
|
||||
txid,
|
||||
})
|
||||
} else {
|
||||
notify({
|
||||
title: t('cancel-all-error'),
|
||||
description: t('transaction-failed'),
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: t('cancel-all-error'),
|
||||
description: e.message,
|
||||
txid: e.txid,
|
||||
type: 'error',
|
||||
})
|
||||
console.log('error', `${e}`)
|
||||
} finally {
|
||||
actions.reloadMangoAccount()
|
||||
actions.updateOpenOrders()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelOrder = async (
|
||||
order: Order | PerpOrder | PerpTriggerOrder,
|
||||
market: Market | PerpMarket
|
||||
|
@ -493,11 +581,16 @@ const OpenOrdersTable = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const sortedOpenOrders = useMemo(() => {
|
||||
return [...openOrders].sort((a, b) => b.price - a.price)
|
||||
}, [openOrders])
|
||||
|
||||
const tableProps = {
|
||||
openOrders,
|
||||
openOrders: sortedOpenOrders,
|
||||
cancelledOrderId: cancelId,
|
||||
editOrderIndex,
|
||||
handleCancelOrder,
|
||||
handleCancelAllOrders,
|
||||
handleModifyOrder,
|
||||
modifiedOrderId: modifyId,
|
||||
setEditOrderIndex,
|
||||
|
@ -515,7 +608,7 @@ const OpenOrdersTable = () => {
|
|||
)
|
||||
) : (
|
||||
<div
|
||||
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
|
||||
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
|
||||
>
|
||||
{t('no-orders')}
|
||||
{asPath === '/account' ? (
|
||||
|
|
|
@ -282,7 +282,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
onClick={() => {
|
||||
setDisplayCumulativeSize(!displayCumulativeSize)
|
||||
}}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 hover:text-th-primary focus:outline-none"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 focus:outline-none md:hover:text-th-primary"
|
||||
>
|
||||
{displayCumulativeSize ? (
|
||||
<StepSizeIcon className="h-4 w-4" />
|
||||
|
@ -300,7 +300,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
>
|
||||
<button
|
||||
onClick={handleLayoutChange}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 hover:text-th-primary focus:outline-none"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 focus:outline-none md:hover:text-th-primary"
|
||||
>
|
||||
<SwitchHorizontalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
@ -410,7 +410,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
onClick={() => {
|
||||
setDisplayCumulativeSize(!displayCumulativeSize)
|
||||
}}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
|
||||
>
|
||||
{displayCumulativeSize ? (
|
||||
<StepSizeIcon className="h-5 w-5" />
|
||||
|
@ -428,7 +428,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
>
|
||||
<button
|
||||
onClick={handleLayoutChange}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
|
||||
>
|
||||
<SwitchHorizontalIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
@ -479,7 +479,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
/>
|
||||
)
|
||||
)}
|
||||
<div className="my-2 flex justify-between rounded-md bg-th-bkg-1 p-2 text-xs">
|
||||
<div className="my-2 flex justify-between rounded-md bg-th-bkg-2 p-2 text-xs">
|
||||
<div className="text-th-fgd-3">{t('spread')}</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{orderbookData?.spread.toFixed(2)}
|
||||
|
@ -527,7 +527,7 @@ export default function Orderbook({ depth = 8 }) {
|
|||
onClick={() => {
|
||||
setDisplayCumulativeSize(!displayCumulativeSize)
|
||||
}}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
|
||||
>
|
||||
{displayCumulativeSize ? (
|
||||
<StepSizeIcon className="h-5 w-5" />
|
||||
|
@ -603,7 +603,7 @@ const OrderbookSpread = ({ orderbookData }) => {
|
|||
}, [selectedMarket])
|
||||
|
||||
return (
|
||||
<div className="mb-0 mt-3 flex justify-between rounded-md bg-th-bkg-1 p-2 text-xs">
|
||||
<div className="my-2 flex justify-between rounded-md bg-th-bkg-2 p-2 text-xs">
|
||||
<div className="hidden text-th-fgd-3 sm:block">{t('spread')}</div>
|
||||
<div className="text-th-fgd-1">
|
||||
{orderbookData?.spread.toFixed(decimals)}
|
||||
|
|
|
@ -21,8 +21,8 @@ export default function Pagination({
|
|||
disabled={page === 1}
|
||||
className={`bg-th-bkg-4 px-1 py-1 ${
|
||||
page !== 1
|
||||
? 'hover:cursor-pointer hover:text-th-primary'
|
||||
: 'hover:cursor-not-allowed'
|
||||
? 'md:hover:cursor-pointer md:hover:text-th-primary'
|
||||
: 'md:hover:cursor-not-allowed'
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronDoubleLeftIcon className={`h-5 w-5`} />
|
||||
|
@ -32,8 +32,8 @@ export default function Pagination({
|
|||
disabled={page === 1}
|
||||
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
|
||||
page !== 1
|
||||
? 'hover:cursor-pointer hover:text-th-primary'
|
||||
: 'hover:cursor-not-allowed'
|
||||
? 'md:hover:cursor-pointer md:hover:text-th-primary'
|
||||
: 'md:hover:cursor-not-allowed'
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronLeftIcon className={`h-5 w-5`} />
|
||||
|
@ -48,8 +48,8 @@ export default function Pagination({
|
|||
disabled={page === totalPages}
|
||||
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
|
||||
page !== totalPages
|
||||
? 'hover:cursor-pointer hover:text-th-primary'
|
||||
: 'hover:cursor-not-allowed'
|
||||
? 'md:hover:cursor-pointer md:hover:text-th-primary'
|
||||
: 'md:hover:cursor-not-allowed'
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronRightIcon className={`h-5 w-5`} />
|
||||
|
@ -59,8 +59,8 @@ export default function Pagination({
|
|||
disabled={page === totalPages}
|
||||
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
|
||||
page !== totalPages
|
||||
? 'hover:cursor-pointer hover:text-th-primary'
|
||||
: 'hover:cursor-not-allowed'
|
||||
? 'md:hover:cursor-pointer md:hover:text-th-primary'
|
||||
: 'md:hover:cursor-not-allowed'
|
||||
} disabled:text-th-fgd-4`}
|
||||
>
|
||||
<ChevronDoubleRightIcon className={`h-5 w-5`} />
|
||||
|
|
|
@ -2,14 +2,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ExclamationIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { ZERO_I80F48 } from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { LinkButton } from '../components/Button'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
import { ExpandableRow, Table, Td, Th, TrBody, TrHead } from './TableElements'
|
||||
import { formatUsdValue, getPrecisionDigits, roundPerpSize } from '../utils'
|
||||
import {
|
||||
formatUsdValue,
|
||||
getPrecisionDigits,
|
||||
roundPerpSize,
|
||||
usdFormatter,
|
||||
} from '../utils'
|
||||
import Loading from './Loading'
|
||||
import MarketCloseModal from './MarketCloseModal'
|
||||
import PerpSideBadge from './PerpSideBadge'
|
||||
|
@ -21,6 +26,8 @@ import { TwitterIcon } from './icons'
|
|||
import { marketSelector } from '../stores/selectors'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import RedeemButtons from './RedeemButtons'
|
||||
import Tooltip from './Tooltip'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
|
||||
const PositionsTable: React.FC = () => {
|
||||
const { t } = useTranslation('common')
|
||||
|
@ -38,6 +45,9 @@ const PositionsTable: React.FC = () => {
|
|||
)
|
||||
const unsettledPositions =
|
||||
useMangoStore.getState().selectedMangoAccount.unsettledPerpPositions
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
const { asPath } = useRouter()
|
||||
|
@ -93,9 +103,9 @@ const PositionsTable: React.FC = () => {
|
|||
}, [unsettledPositions])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:pb-2">
|
||||
<div className="flex flex-col">
|
||||
{unsettledPositions.length > 0 ? (
|
||||
<div className="mb-6 rounded-lg border border-th-bkg-4 p-4 sm:p-6">
|
||||
<div className="mb-6 rounded-lg border border-th-bkg-3 p-4 sm:p-6">
|
||||
<div className="flex items-start justify-between pb-4">
|
||||
<div className="flex items-center">
|
||||
<ExclamationIcon className="mr-2 h-6 w-6 flex-shrink-0 text-th-primary" />
|
||||
|
@ -115,19 +125,19 @@ const PositionsTable: React.FC = () => {
|
|||
|
||||
{unsettledPositions.length > 1 ? <RedeemButtons /> : null}
|
||||
</div>
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{unsettledPositions.map((p, index) => {
|
||||
return (
|
||||
<div
|
||||
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-3 px-5 py-3"
|
||||
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-2 px-5 py-3"
|
||||
key={p.marketConfig.baseSymbol}
|
||||
>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${p.marketConfig.baseSymbol.toLowerCase()}.svg`}
|
||||
className={`mr-3`}
|
||||
/>
|
||||
|
@ -170,7 +180,9 @@ const PositionsTable: React.FC = () => {
|
|||
<Th>{t('notional-size')}</Th>
|
||||
<Th>{t('average-entry')}</Th>
|
||||
<Th>{t('break-even')}</Th>
|
||||
<Th>{t('estimated-liq-price')}</Th>
|
||||
<Th>{t('unrealized-pnl')}</Th>
|
||||
<Th>{t('unsettled-balance')}</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -186,6 +198,7 @@ const PositionsTable: React.FC = () => {
|
|||
avgEntryPrice,
|
||||
breakEvenPrice,
|
||||
unrealizedPnl,
|
||||
unsettledPnl,
|
||||
},
|
||||
index
|
||||
) => {
|
||||
|
@ -193,6 +206,18 @@ const PositionsTable: React.FC = () => {
|
|||
basePosition,
|
||||
marketConfig.baseSymbol
|
||||
)
|
||||
const liquidationPrice =
|
||||
mangoGroup &&
|
||||
mangoAccount &&
|
||||
marketConfig &&
|
||||
mangoGroup &&
|
||||
mangoCache
|
||||
? mangoAccount.getLiquidationPrice(
|
||||
mangoGroup,
|
||||
mangoCache,
|
||||
marketConfig.marketIndex
|
||||
)
|
||||
: undefined
|
||||
return (
|
||||
<TrBody key={`${marketConfig.marketIndex}`}>
|
||||
<Td>
|
||||
|
@ -255,12 +280,47 @@ const PositionsTable: React.FC = () => {
|
|||
: '--'}
|
||||
</Td>
|
||||
<Td>
|
||||
{breakEvenPrice ? (
|
||||
{liquidationPrice &&
|
||||
liquidationPrice.gt(ZERO_I80F48)
|
||||
? usdFormatter(liquidationPrice)
|
||||
: 'N/A'}
|
||||
</Td>
|
||||
<Td>
|
||||
{unrealizedPnl ? (
|
||||
<PnlText pnl={unrealizedPnl} />
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{unsettledPnl ? (
|
||||
settleSinglePos === index ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Tooltip content={t('redeem-pnl')}>
|
||||
<LinkButton
|
||||
className={
|
||||
unsettledPnl >= 0
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
}
|
||||
onClick={() =>
|
||||
handleSettlePnl(
|
||||
perpMarket,
|
||||
perpAccount,
|
||||
index
|
||||
)
|
||||
}
|
||||
disabled={unsettledPnl === 0}
|
||||
>
|
||||
{formatUsdValue(unsettledPnl)}
|
||||
</LinkButton>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<LinkButton
|
||||
onClick={() =>
|
||||
|
@ -299,7 +359,10 @@ const PositionsTable: React.FC = () => {
|
|||
notionalSize,
|
||||
avgEntryPrice,
|
||||
breakEvenPrice,
|
||||
perpAccount,
|
||||
perpMarket,
|
||||
unrealizedPnl,
|
||||
unsettledPnl,
|
||||
},
|
||||
index
|
||||
) => {
|
||||
|
@ -307,6 +370,18 @@ const PositionsTable: React.FC = () => {
|
|||
basePosition,
|
||||
marketConfig.baseSymbol
|
||||
)
|
||||
const liquidationPrice =
|
||||
mangoGroup &&
|
||||
mangoAccount &&
|
||||
marketConfig &&
|
||||
mangoGroup &&
|
||||
mangoCache
|
||||
? mangoAccount.getLiquidationPrice(
|
||||
mangoGroup,
|
||||
mangoCache,
|
||||
marketConfig.marketIndex
|
||||
)
|
||||
: undefined
|
||||
return (
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
|
@ -373,6 +448,47 @@ const PositionsTable: React.FC = () => {
|
|||
? formatUsdValue(breakEvenPrice)
|
||||
: '--'}
|
||||
</div>
|
||||
<div className="col-span-1 text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('unsettled-balance')}
|
||||
</div>
|
||||
{unsettledPnl ? (
|
||||
settleSinglePos === index ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Tooltip content={t('redeem-pnl')}>
|
||||
<LinkButton
|
||||
className={`font-bold ${
|
||||
unsettledPnl >= 0
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleSettlePnl(
|
||||
perpMarket,
|
||||
perpAccount,
|
||||
index
|
||||
)
|
||||
}
|
||||
disabled={unsettledPnl === 0}
|
||||
>
|
||||
{formatUsdValue(unsettledPnl)}
|
||||
</LinkButton>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('estimated-liq-price')}
|
||||
</div>
|
||||
{liquidationPrice &&
|
||||
liquidationPrice.gt(ZERO_I80F48)
|
||||
? usdFormatter(liquidationPrice)
|
||||
: 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -383,7 +499,7 @@ const PositionsTable: React.FC = () => {
|
|||
)
|
||||
) : (
|
||||
<div
|
||||
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
|
||||
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
|
||||
>
|
||||
{t('no-perp')}
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@ const PnlText = ({ className, pnl }: { className?: string; pnl?: number }) => (
|
|||
<>
|
||||
{pnl ? (
|
||||
<p
|
||||
className={`mb-0 ${className} ${
|
||||
className={`mb-0 ${className} text-xs ${
|
||||
pnl > 0 ? 'text-th-green' : 'text-th-red'
|
||||
}`}
|
||||
>
|
||||
|
|
|
@ -1,28 +1,74 @@
|
|||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
import { getProfilePicture } from '@solflare-wallet/pfp'
|
||||
import { useEffect, useState } from 'react'
|
||||
import useMangoStore from 'stores/useMangoStore'
|
||||
import { ProfileIcon } from './icons'
|
||||
|
||||
const ProfileImage = ({
|
||||
thumbHeightClass,
|
||||
thumbWidthClass,
|
||||
placeholderHeightClass,
|
||||
placeholderWidthClass,
|
||||
imageSize,
|
||||
placeholderSize,
|
||||
publicKey,
|
||||
}: {
|
||||
imageSize: string
|
||||
placeholderSize: string
|
||||
publicKey?: string
|
||||
}) => {
|
||||
const pfp = useMangoStore((s) => s.wallet.pfp)
|
||||
const loadPfp = useMangoStore((s) => s.wallet.loadPfp)
|
||||
const loadingTransaction = useMangoStore(
|
||||
(s) => s.wallet.nfts.loadingTransaction
|
||||
)
|
||||
return pfp?.isAvailable ? (
|
||||
const connection = useMangoStore((s) => s.connection.current)
|
||||
const [unownedPfp, setUnownedPfp] = useState<any>(null)
|
||||
const [loadUnownedPfp, setLoadUnownedPfp] = useState<boolean>(false)
|
||||
const { connected } = useWallet()
|
||||
|
||||
useEffect(() => {
|
||||
if (publicKey) {
|
||||
setLoadUnownedPfp(true)
|
||||
const getProfilePic = async () => {
|
||||
const pfp = await getProfilePicture(
|
||||
connection,
|
||||
new PublicKey(publicKey)
|
||||
)
|
||||
setUnownedPfp(pfp)
|
||||
setLoadUnownedPfp(false)
|
||||
}
|
||||
getProfilePic()
|
||||
}
|
||||
}, [publicKey])
|
||||
|
||||
const isLoading =
|
||||
(connected && loadingTransaction && !publicKey) ||
|
||||
(connected && loadPfp && !publicKey) ||
|
||||
loadUnownedPfp
|
||||
|
||||
return (pfp?.isAvailable && !publicKey) || unownedPfp?.isAvailable ? (
|
||||
<img
|
||||
alt=""
|
||||
src={pfp.url}
|
||||
className={`default-transition rounded-full hover:opacity-60 ${thumbHeightClass} ${thumbWidthClass} ${
|
||||
src={publicKey ? unownedPfp?.url : pfp?.url}
|
||||
className={`default-transition rounded-full ${
|
||||
loadingTransaction ? 'opacity-40' : ''
|
||||
}`}
|
||||
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
|
||||
/>
|
||||
) : (
|
||||
<ProfileIcon
|
||||
className={`text-th-fgd-3 ${placeholderHeightClass} ${placeholderWidthClass}`}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-shrink-0 items-center justify-center rounded-full ${
|
||||
isLoading ? 'animate-pulse bg-th-bkg-4' : 'bg-th-bkg-4'
|
||||
}`}
|
||||
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${placeholderSize}px`,
|
||||
height: `${placeholderSize}px`,
|
||||
}}
|
||||
>
|
||||
<ProfileIcon className={`h-full w-full text-th-fgd-3`} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { PencilIcon } from '@heroicons/react/outline'
|
||||
import { useCallback, useState } from 'react'
|
||||
import useMangoStore from 'stores/useMangoStore'
|
||||
import NftProfilePicModal from './NftProfilePicModal'
|
||||
import ProfileImage from './ProfileImage'
|
||||
|
||||
const ProfileImageButton = ({
|
||||
disabled,
|
||||
imageSize,
|
||||
placeholderSize,
|
||||
publicKey,
|
||||
}: {
|
||||
disabled: boolean
|
||||
imageSize: string
|
||||
placeholderSize: string
|
||||
publicKey?: string
|
||||
}) => {
|
||||
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
|
||||
const loadingTransaction = useMangoStore(
|
||||
(s) => s.wallet.nfts.loadingTransaction
|
||||
)
|
||||
|
||||
const handleCloseProfilePicModal = useCallback(() => {
|
||||
setShowProfilePicModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
disabled={disabled}
|
||||
className={`relative mb-2 mr-4 flex items-center justify-center rounded-full sm:mb-0 ${
|
||||
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : 'bg-th-bkg-button'
|
||||
}`}
|
||||
onClick={() => setShowProfilePicModal(true)}
|
||||
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
|
||||
>
|
||||
<ProfileImage
|
||||
imageSize={imageSize}
|
||||
placeholderSize={placeholderSize}
|
||||
publicKey={publicKey || ''}
|
||||
/>
|
||||
{!disabled ? (
|
||||
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
|
||||
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
{showProfilePicModal ? (
|
||||
<NftProfilePicModal
|
||||
isOpen={showProfilePicModal}
|
||||
onClose={handleCloseProfilePicModal}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileImageButton
|
|
@ -93,7 +93,7 @@ export default function RecentMarketTrades() {
|
|||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="my-3 border-b border-th-bkg-4">
|
||||
<div className="my-3 border-b border-th-bkg-3">
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
<div className="flex w-full justify-between text-left">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TemplateIcon } from '@heroicons/react/outline'
|
||||
import { TemplateIcon } from '@heroicons/react/solid'
|
||||
import { defaultLayouts, GRID_LAYOUT_KEY } from './TradePageGrid'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import Tooltip from './Tooltip'
|
||||
|
|
|
@ -33,7 +33,7 @@ const Select = ({
|
|||
{open ? (
|
||||
<Listbox.Options
|
||||
static
|
||||
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-3 p-2 text-th-fgd-1 outline-none ${dropdownPanelClassName}`}
|
||||
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-2 p-2 text-th-fgd-1 outline-none ${dropdownPanelClassName}`}
|
||||
>
|
||||
{children}
|
||||
</Listbox.Options>
|
||||
|
@ -47,11 +47,11 @@ const Select = ({
|
|||
|
||||
const Option = ({ value, children, className = '' }) => {
|
||||
return (
|
||||
<Listbox.Option value={value}>
|
||||
<Listbox.Option className="mb-0" value={value}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`default-transition text-th-fgd-1 hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary ${
|
||||
selected && `text-th-primary`
|
||||
className={`default-transition rounded p-2 text-th-fgd-1 hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary ${
|
||||
selected ? 'text-th-primary' : ''
|
||||
} ${className}`}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { MangoAccount } from '@blockworks-foundation/mango-client'
|
||||
import { RadioGroup } from '@headlessui/react'
|
||||
import { CheckCircleIcon } from '@heroicons/react/outline'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import useMangoStore, { LAST_ACCOUNT_KEY } from 'stores/useMangoStore'
|
||||
import MangoAccountCard from './MangoAccountCard'
|
||||
|
||||
const SelectMangoAccount = ({
|
||||
onClose,
|
||||
className,
|
||||
}: {
|
||||
onClose?: () => void
|
||||
className?: string
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const selectedMangoAccount = useMangoStore(
|
||||
(s) => s.selectedMangoAccount.current
|
||||
)
|
||||
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
|
||||
const actions = useMangoStore((s) => s.actions)
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
const [, setLastAccountViewed] = useLocalStorageState(LAST_ACCOUNT_KEY)
|
||||
|
||||
const handleMangoAccountChange = (mangoAccount: MangoAccount) => {
|
||||
setLastAccountViewed(mangoAccount.publicKey.toString())
|
||||
setMangoStore((state) => {
|
||||
state.selectedMangoAccount.current = mangoAccount
|
||||
})
|
||||
|
||||
actions.fetchTradeHistory()
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
value={selectedMangoAccount}
|
||||
onChange={(acc) => {
|
||||
if (acc) {
|
||||
handleMangoAccountChange(acc)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RadioGroup.Label className="sr-only">
|
||||
{t('select-account')}
|
||||
</RadioGroup.Label>
|
||||
<div className={`${className} space-y-2`}>
|
||||
{mangoAccounts.map((account) => (
|
||||
<RadioGroup.Option
|
||||
key={account.publicKey.toString()}
|
||||
value={account}
|
||||
className={({ checked }) =>
|
||||
`${
|
||||
checked
|
||||
? 'ring-1 ring-inset ring-th-green'
|
||||
: 'ring-1 ring-inset ring-th-fgd-4 hover:ring-th-fgd-2'
|
||||
}
|
||||
default-transition relative flex w-full cursor-pointer rounded-md px-3 py-3 focus:outline-none`
|
||||
}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="text-sm">
|
||||
<RadioGroup.Label className="flex cursor-pointer items-center text-th-fgd-1">
|
||||
<MangoAccountCard mangoAccount={account} />
|
||||
</RadioGroup.Label>
|
||||
</div>
|
||||
</div>
|
||||
{checked && (
|
||||
<div className="flex-shrink-0 text-th-green">
|
||||
<CheckCircleIcon className="h-5 w-5" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectMangoAccount
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { CogIcon } from '@heroicons/react/outline'
|
||||
import { CogIcon } from '@heroicons/react/solid'
|
||||
import SettingsModal from './SettingsModal'
|
||||
|
||||
const Settings = () => {
|
||||
|
@ -12,7 +12,7 @@ const Settings = () => {
|
|||
return mounted ? (
|
||||
<>
|
||||
<button
|
||||
className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none"
|
||||
className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
|
||||
onClick={() => setShowSettingsModal(true)}
|
||||
>
|
||||
<CogIcon className="h-5 w-5" />
|
||||
|
|
|
@ -107,7 +107,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
|
|||
{!settingsView ? (
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<button
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
|
||||
onClick={() => setSettingsView('Default Market')}
|
||||
>
|
||||
<span>{t('default-market')}</span>
|
||||
|
@ -117,7 +117,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
|
|||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
|
||||
onClick={() => setSettingsView('Theme')}
|
||||
>
|
||||
<span>{t('theme')}</span>
|
||||
|
@ -127,7 +127,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
|
|||
</div>
|
||||
</button>
|
||||
<button
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
|
||||
onClick={() => setSettingsView('Language')}
|
||||
>
|
||||
<span>{t('language')}</span>
|
||||
|
@ -139,7 +139,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
|
|||
) : null}
|
||||
</button>
|
||||
<button
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
|
||||
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
|
||||
onClick={() => setSettingsView('RPC Endpoint')}
|
||||
>
|
||||
<span>{t('rpc-endpoint')}</span>
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
import Link from 'next/link'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
|
||||
import { BtcMonoIcon, TradeIcon, TrophyIcon } from './icons'
|
||||
import {
|
||||
CashIcon,
|
||||
ChartBarIcon,
|
||||
CurrencyDollarIcon,
|
||||
DotsHorizontalIcon,
|
||||
SwitchHorizontalIcon,
|
||||
CalculatorIcon,
|
||||
LibraryIcon,
|
||||
LightBulbIcon,
|
||||
UserAddIcon,
|
||||
ExternalLinkIcon,
|
||||
ChevronDownIcon,
|
||||
ReceiptTaxIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import AccountOverviewPopover from './AccountOverviewPopover'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Fragment, ReactNode, useEffect, useState } from 'react'
|
||||
import { Disclosure, Popover, Transition } from '@headlessui/react'
|
||||
import HealthHeart from './HealthHeart'
|
||||
import { abbreviateAddress } from 'utils'
|
||||
import { I80F48 } from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from 'stores/useMangoStore'
|
||||
|
||||
const SideNav = ({ collapsed }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const [defaultMarket] = useLocalStorageState(
|
||||
DEFAULT_MARKET_KEY,
|
||||
initialMarket
|
||||
)
|
||||
const router = useRouter()
|
||||
const { pathname } = router
|
||||
|
||||
const I80F48_100 = I80F48.fromString('100')
|
||||
|
||||
const maintHealthRatio =
|
||||
mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
: I80F48_100
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col justify-between transition-all duration-500 ease-in-out ${
|
||||
collapsed ? 'w-[64px]' : 'w-[220px]'
|
||||
} min-h-screen border-r border-th-bkg-3 bg-th-bkg-1`}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<Link href={defaultMarket.path} shallow={true}>
|
||||
<div
|
||||
className={`flex h-14 w-full items-center justify-start border-b border-th-bkg-3 px-4`}
|
||||
>
|
||||
<div className={`flex flex-shrink-0 cursor-pointer items-center`}>
|
||||
<img
|
||||
className={`h-8 w-auto`}
|
||||
src="/assets/icons/logo.svg"
|
||||
alt="next"
|
||||
/>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={!collapsed}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-300"
|
||||
enterFrom="opacity-50"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<span className="ml-2 text-lg font-bold text-th-fgd-1">
|
||||
Mango
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className={`flex flex-col items-start space-y-3.5 px-4 pt-4`}>
|
||||
<MenuItem
|
||||
active={pathname === '/'}
|
||||
collapsed={collapsed}
|
||||
icon={<TradeIcon className="h-5 w-5" />}
|
||||
title={t('trade')}
|
||||
pagePath="/"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/account'}
|
||||
collapsed={collapsed}
|
||||
icon={<CurrencyDollarIcon className="h-5 w-5" />}
|
||||
title={t('account')}
|
||||
pagePath="/account"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/markets'}
|
||||
collapsed={collapsed}
|
||||
icon={<BtcMonoIcon className="h-4 w-4" />}
|
||||
title={t('markets')}
|
||||
pagePath="/markets"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/borrow'}
|
||||
collapsed={collapsed}
|
||||
icon={<CashIcon className="h-5 w-5" />}
|
||||
title={t('borrow')}
|
||||
pagePath="/borrow"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/swap'}
|
||||
collapsed={collapsed}
|
||||
icon={<SwitchHorizontalIcon className="h-5 w-5" />}
|
||||
title={t('swap')}
|
||||
pagePath="/swap"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/stats'}
|
||||
collapsed={collapsed}
|
||||
icon={<ChartBarIcon className="h-5 w-5" />}
|
||||
title={t('stats')}
|
||||
pagePath="/stats"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/leaderboard'}
|
||||
collapsed={collapsed}
|
||||
icon={<TrophyIcon className="h-[18px] w-[18px]" />}
|
||||
title={t('leaderboard')}
|
||||
pagePath="/leaderboard"
|
||||
/>
|
||||
<ExpandableMenuItem
|
||||
collapsed={collapsed}
|
||||
icon={<DotsHorizontalIcon className="h-5 w-5" />}
|
||||
title={t('more')}
|
||||
>
|
||||
<MenuItem
|
||||
active={pathname === '/referral'}
|
||||
collapsed={false}
|
||||
icon={<UserAddIcon className="h-4 w-4" />}
|
||||
title={t('referrals')}
|
||||
pagePath="/referral"
|
||||
hideIconBg
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/risk-calculator'}
|
||||
collapsed={false}
|
||||
icon={<CalculatorIcon className="h-4 w-4" />}
|
||||
title={t('calculator')}
|
||||
pagePath="/risk-calculator"
|
||||
hideIconBg
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/fees'}
|
||||
collapsed={false}
|
||||
icon={<ReceiptTaxIcon className="h-4 w-4" />}
|
||||
title={t('fees')}
|
||||
pagePath="/fees"
|
||||
hideIconBg
|
||||
/>
|
||||
<MenuItem
|
||||
collapsed={false}
|
||||
icon={<LightBulbIcon className="h-4 w-4" />}
|
||||
title={t('learn')}
|
||||
pagePath="https://docs.mango.markets"
|
||||
hideIconBg
|
||||
isExternal
|
||||
/>
|
||||
<MenuItem
|
||||
collapsed={false}
|
||||
icon={<LibraryIcon className="h-4 w-4" />}
|
||||
title={t('governance')}
|
||||
pagePath="https://dao.mango.markets"
|
||||
hideIconBg
|
||||
isExternal
|
||||
/>
|
||||
</ExpandableMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
{mangoAccount ? (
|
||||
<div className="flex min-h-[64px] w-full items-center border-t border-th-bkg-3 ">
|
||||
<ExpandableMenuItem
|
||||
collapsed={collapsed}
|
||||
icon={<HealthHeart health={Number(maintHealthRatio)} size={32} />}
|
||||
title={
|
||||
<div className="py-3 text-left">
|
||||
<p className="mb-0 whitespace-nowrap text-xs text-th-fgd-3">
|
||||
{t('account-summary')}
|
||||
</p>
|
||||
<p className="mb-0 font-bold text-th-fgd-1">
|
||||
{abbreviateAddress(mangoAccount.publicKey)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
hideIconBg
|
||||
alignBottom
|
||||
>
|
||||
<AccountOverviewPopover
|
||||
collapsed={collapsed}
|
||||
health={maintHealthRatio}
|
||||
/>
|
||||
</ExpandableMenuItem>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideNav
|
||||
|
||||
const MenuItem = ({
|
||||
active,
|
||||
collapsed,
|
||||
icon,
|
||||
title,
|
||||
pagePath,
|
||||
hideIconBg,
|
||||
isExternal,
|
||||
}: {
|
||||
active?: boolean
|
||||
collapsed: boolean
|
||||
icon: ReactNode
|
||||
title: string
|
||||
pagePath: string
|
||||
hideIconBg?: boolean
|
||||
isExternal?: boolean
|
||||
}) => {
|
||||
return !isExternal ? (
|
||||
<Link href={pagePath} shallow={true}>
|
||||
<a
|
||||
className={`default-transition flex items-center hover:brightness-[1.1] ${
|
||||
active ? 'text-th-primary' : 'text-th-fgd-1'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
hideIconBg
|
||||
? ''
|
||||
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={!collapsed}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-300"
|
||||
enterFrom="opacity-50"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<span className="ml-2">{title}</span>
|
||||
</Transition>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<a
|
||||
href={pagePath}
|
||||
className={`default-transition flex items-center justify-between hover:brightness-[1.1] ${
|
||||
active ? 'text-th-primary' : 'text-th-fgd-1'
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={
|
||||
hideIconBg
|
||||
? ''
|
||||
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
{!collapsed ? <span className="ml-2">{title}</span> : null}
|
||||
</div>
|
||||
<ExternalLinkIcon className="h-4 w-4" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpandableMenuItem = ({
|
||||
children,
|
||||
collapsed,
|
||||
icon,
|
||||
title,
|
||||
hideIconBg,
|
||||
alignBottom,
|
||||
}: {
|
||||
children: ReactNode
|
||||
collapsed: boolean
|
||||
icon: ReactNode
|
||||
title: string | ReactNode
|
||||
hideIconBg?: boolean
|
||||
alignBottom?: boolean
|
||||
}) => {
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
|
||||
const onHoverMenu = (open, action) => {
|
||||
if (
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
setShowMenu(!showMenu)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (collapsed) {
|
||||
setShowMenu(false)
|
||||
}
|
||||
}, [collapsed])
|
||||
|
||||
return collapsed ? (
|
||||
<Popover>
|
||||
<div
|
||||
onMouseEnter={() => onHoverMenu(showMenu, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHoverMenu(showMenu, 'onMouseLeave')}
|
||||
className="relative z-30"
|
||||
>
|
||||
<Popover.Button className="hover:text-th-primary">
|
||||
<div
|
||||
className={` ${
|
||||
hideIconBg
|
||||
? ''
|
||||
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
|
||||
} ${
|
||||
alignBottom
|
||||
? 'default-transition flex h-16 w-16 items-center justify-center hover:bg-th-bkg-2'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={showMenu}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-300"
|
||||
enterFrom="opacity-0 transform scale-90"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`absolute w-56 space-y-2 rounded-md bg-th-bkg-2 p-4 ${
|
||||
alignBottom
|
||||
? 'bottom-0 left-14'
|
||||
: 'left-10 top-1/2 -translate-y-1/2 transform'
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</Popover>
|
||||
) : (
|
||||
<Disclosure>
|
||||
<div
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
role="button"
|
||||
className={`w-full `}
|
||||
>
|
||||
<Disclosure.Button
|
||||
className={`flex w-full items-center justify-between rounded-none hover:text-th-primary ${
|
||||
alignBottom ? 'h-[64px] px-4 hover:bg-th-bkg-2' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={
|
||||
hideIconBg
|
||||
? ''
|
||||
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={!collapsed}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-300"
|
||||
enterFrom="opacity-50"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<span className="ml-2">{title}</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
showMenu ? 'rotate-180 transform' : 'rotate-360 transform'
|
||||
} default-transition h-5 w-5 flex-shrink-0`}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={showMenu}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-500"
|
||||
enterFrom="opacity-100 max-h-0"
|
||||
enterTo="opacity-100 max-h-64"
|
||||
leave="transition-all ease-out duration-500"
|
||||
leaveFrom="opacity-100 max-h-64"
|
||||
leaveTo="opacity-0 max-h-0"
|
||||
>
|
||||
<Disclosure.Panel className="overflow-hidden">
|
||||
<div className="space-y-2 p-2">{children}</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
</Disclosure>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { FunctionComponent, useEffect, useMemo, useState } from 'react'
|
||||
import { ExternalLinkIcon, EyeOffIcon } from '@heroicons/react/outline'
|
||||
import { ExternalLinkIcon, EyeOffIcon } from '@heroicons/react/solid'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { AreaChart, Area, XAxis, YAxis, Tooltip } from 'recharts'
|
||||
|
@ -295,7 +295,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
|
|||
</AreaChart>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 1 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(1)}
|
||||
|
@ -303,7 +303,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
|
|||
24H
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 7 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(7)}
|
||||
|
@ -311,7 +311,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
|
|||
7D
|
||||
</button>
|
||||
<button
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
|
||||
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
|
||||
daysToShow === 30 && 'text-th-primary'
|
||||
}`}
|
||||
onClick={() => setDaysToShow(30)}
|
||||
|
|
|
@ -6,7 +6,7 @@ import ButtonGroup from './ButtonGroup'
|
|||
import { numberCompacter, numberFormatter } from './SwapTokenInfo'
|
||||
import Button, { IconButton } from './Button'
|
||||
import Input from './Input'
|
||||
import { SearchIcon, XIcon } from '@heroicons/react/outline'
|
||||
import { SearchIcon, XIcon } from '@heroicons/react/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ExpandableRow } from './TableElements'
|
||||
|
||||
|
@ -36,8 +36,9 @@ const SwapTokenInsights = ({ formState, jupiterTokens, setOutputToken }) => {
|
|||
`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${ids.toString()}&order=market_cap_desc&sparkline=false&price_change_percentage=24h,7d,30d`
|
||||
)
|
||||
const data = await response.json()
|
||||
const filterMicroVolume = data.filter((token) => token.total_volume > 10000)
|
||||
setLoading(false)
|
||||
setTokenInsights(data)
|
||||
setTokenInsights(filterMicroVolume)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { memo, useMemo, useState, PureComponent, useEffect } from 'react'
|
||||
import { SearchIcon } from '@heroicons/react/outline'
|
||||
import { SearchIcon } from '@heroicons/react/solid'
|
||||
import Modal from './Modal'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Token } from '../@types/types'
|
||||
|
@ -45,7 +45,7 @@ class ItemRenderer extends PureComponent<ItemRendererProps> {
|
|||
<div style={this.props.style}>
|
||||
<button
|
||||
key={tokenInfo?.address}
|
||||
className="flex w-full cursor-pointer items-center justify-between rounded-none py-4 px-6 font-normal hover:bg-th-bkg-4 focus:bg-th-bkg-3 focus:outline-none"
|
||||
className="flex w-full cursor-pointer items-center justify-between rounded-none py-4 px-6 font-normal focus:bg-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-4"
|
||||
onClick={() => this.props.data.onSubmit(tokenInfo)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
|
@ -75,14 +75,36 @@ const SwapTokenSelect = ({
|
|||
sortedTokenMints,
|
||||
onClose,
|
||||
onTokenSelect,
|
||||
walletTokens,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
sortedTokenMints: Token[]
|
||||
onClose?: (x?) => void
|
||||
onTokenSelect?: (x?) => void
|
||||
walletTokens?: Array<any>
|
||||
}) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const popularTokenSymbols = ['USDC', 'SOL', 'USDT', 'MNGO', 'BTC', 'ETH']
|
||||
|
||||
const popularTokens = useMemo(() => {
|
||||
return walletTokens?.length
|
||||
? sortedTokenMints.filter((token) => {
|
||||
const walletMints = walletTokens.map((tok) =>
|
||||
tok.account.mint.toString()
|
||||
)
|
||||
return !token?.name || !token?.symbol
|
||||
? false
|
||||
: popularTokenSymbols.includes(token.symbol) &&
|
||||
walletMints.includes(token.address)
|
||||
})
|
||||
: sortedTokenMints.filter((token) => {
|
||||
return !token?.name || !token?.symbol
|
||||
? false
|
||||
: popularTokenSymbols.includes(token.symbol)
|
||||
})
|
||||
}, [walletTokens])
|
||||
|
||||
useEffect(() => {
|
||||
function onEscape(e) {
|
||||
if (e.keyCode === 27) {
|
||||
|
@ -95,9 +117,20 @@ const SwapTokenSelect = ({
|
|||
|
||||
const tokenInfos = useMemo(() => {
|
||||
if (sortedTokenMints?.length) {
|
||||
return sortedTokenMints.filter((token) => {
|
||||
const filteredTokens = sortedTokenMints.filter((token) => {
|
||||
return !token?.name || !token?.symbol ? false : true
|
||||
})
|
||||
if (walletTokens?.length) {
|
||||
const walletMints = walletTokens.map((tok) =>
|
||||
tok.account.mint.toString()
|
||||
)
|
||||
return filteredTokens.sort(
|
||||
(a, b) =>
|
||||
walletMints.indexOf(b.address) - walletMints.indexOf(a.address)
|
||||
)
|
||||
} else {
|
||||
return filteredTokens
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
@ -116,13 +149,33 @@ const SwapTokenSelect = ({
|
|||
<SearchIcon className="h-8 w-8" />
|
||||
<input
|
||||
type="text"
|
||||
className="ml-4 flex-1 bg-th-bkg-2 focus:outline-none"
|
||||
className="ml-4 flex-1 bg-th-bkg-1 focus:outline-none"
|
||||
placeholder="Search by token or paste address"
|
||||
autoFocus
|
||||
value={search}
|
||||
onChange={handleUpdateSearch}
|
||||
/>
|
||||
</div>
|
||||
{popularTokens.length && onTokenSelect ? (
|
||||
<div className="flex flex-wrap px-4">
|
||||
{popularTokens.map((token) => (
|
||||
<button
|
||||
className="mx-1 mb-2 flex items-center rounded-md border border-th-fgd-4 bg-th-bkg-1 py-1 px-3 hover:border-th-fgd-3 focus:border-th-fgd-2"
|
||||
onClick={() => onTokenSelect(token)}
|
||||
key={token.address}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
width="16"
|
||||
height="16"
|
||||
src={`/assets/icons/${token.symbol.toLowerCase()}.svg`}
|
||||
className={`mr-1.5`}
|
||||
/>
|
||||
<span className="text-th-fgd-1">{token.symbol}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={403}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Fragment, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Popover, Transition } from '@headlessui/react'
|
||||
import { SearchIcon } from '@heroicons/react/outline'
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid'
|
||||
import { ChevronDownIcon, SearchIcon } from '@heroicons/react/solid'
|
||||
import Input from './Input'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import MarketNavItem from './MarketNavItem'
|
||||
|
@ -61,7 +60,7 @@ const SwitchMarketDropdown = () => {
|
|||
{({ open }) => (
|
||||
<div className="relative flex flex-col">
|
||||
<Popover.Button
|
||||
className={`border border-th-bkg-3 p-0.5 transition-none hover:border-th-bkg-4 focus:border-th-bkg-4 focus:outline-none ${
|
||||
className={`default-transition border border-th-fgd-4 p-0.5 transition-none hover:border-th-fgd-3 focus:border-th-fgd-4 focus:outline-none ${
|
||||
open && 'border-th-fgd-4'
|
||||
}`}
|
||||
ref={buttonRef}
|
||||
|
@ -103,7 +102,7 @@ const SwitchMarketDropdown = () => {
|
|||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel
|
||||
className="thin-scroll absolute left-0 top-14 z-10 max-h-[50vh] w-72 transform overflow-y-auto rounded-b-md rounded-tl-md bg-th-bkg-3 p-4 sm:max-h-[75vh]"
|
||||
className="thin-scroll absolute left-0 top-14 z-10 max-h-[50vh] w-72 transform overflow-y-auto rounded-b-md rounded-tl-md bg-th-bkg-2 p-4 sm:max-h-[75vh]"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="hidden pb-2.5 sm:block">
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import * as MonoIcons from './icons'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const TabButtons = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
showSymbolIcon,
|
||||
onClick,
|
||||
}: {
|
||||
tabs: Array<{ label: string; key: string }>
|
||||
activeTab: string
|
||||
showSymbolIcon?: boolean
|
||||
onClick: (x) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const renderSymbolIcon = (s) => {
|
||||
const iconName = `${s.slice(0, 1)}${s.slice(1, 4).toLowerCase()}MonoIcon`
|
||||
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
|
||||
return <SymbolIcon className="mr-1.5 h-3.5 w-auto" />
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
className={`default-transition mb-2 mr-2 flex cursor-pointer items-center rounded-full px-3 py-2 font-bold leading-none ring-1 ring-inset ${
|
||||
tab.key === activeTab
|
||||
? `text-th-primary ring-th-primary`
|
||||
: `text-th-fgd-4 ring-th-fgd-4 hover:text-th-fgd-3 hover:ring-th-fgd-3`
|
||||
} ${showSymbolIcon ? 'uppercase' : ''}
|
||||
`}
|
||||
onClick={() => onClick(tab.key)}
|
||||
role="button"
|
||||
key={tab.key}
|
||||
>
|
||||
{showSymbolIcon ? renderSymbolIcon(tab.label) : null}
|
||||
{t(tab.label.toLowerCase().replace(/\s/g, '-'))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabButtons
|
|
@ -8,7 +8,7 @@ export const Table = ({ children }) => (
|
|||
)
|
||||
|
||||
export const TrHead = ({ children }) => (
|
||||
<tr className="text-xs text-th-fgd-3">{children}</tr>
|
||||
<tr className="text-xxs leading-tight text-th-fgd-2">{children}</tr>
|
||||
)
|
||||
|
||||
export const Th = ({ children }) => (
|
||||
|
@ -18,7 +18,7 @@ export const Th = ({ children }) => (
|
|||
)
|
||||
|
||||
export const TrBody = ({ children, className = '' }) => (
|
||||
<tr className={`border-b border-th-bkg-4 ${className}`}>{children}</tr>
|
||||
<tr className={`border-b border-th-bkg-3 ${className}`}>{children}</tr>
|
||||
)
|
||||
|
||||
export const Td = ({
|
||||
|
@ -28,7 +28,7 @@ export const Td = ({
|
|||
children: ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
<td className={`h-16 px-4 text-sm text-th-fgd-2 ${className}`}>{children}</td>
|
||||
<td className={`h-14 px-4 text-xs text-th-fgd-2 ${className}`}>{children}</td>
|
||||
)
|
||||
|
||||
type ExpandableRowProps = {
|
||||
|
@ -47,7 +47,7 @@ export const ExpandableRow = ({
|
|||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={`default-transition flex w-full items-center justify-between border-t border-th-bkg-4 p-4 font-normal text-th-fgd-1 hover:bg-th-bkg-4 focus:outline-none ${
|
||||
className={`default-transition flex w-full items-center justify-between border-t border-th-bkg-3 p-4 text-left text-xs font-normal text-th-fgd-1 hover:bg-th-bkg-4 focus:outline-none ${
|
||||
rounded
|
||||
? open
|
||||
? 'rounded-b-none'
|
||||
|
@ -76,7 +76,7 @@ export const ExpandableRow = ({
|
|||
leaveTo="opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="px-4 pb-4 pt-2">{panelTemplate}</div>
|
||||
<div className="px-4 pb-4 pt-2 text-xs">{panelTemplate}</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
|
@ -92,7 +92,7 @@ type RowProps = {
|
|||
export const Row = ({ children }: RowProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`default-transition w-full rounded-none border-t border-th-bkg-4 p-4 font-normal text-th-fgd-1`}
|
||||
className={`default-transition w-full rounded-none border-t border-th-bkg-3 p-4 font-normal text-th-fgd-1`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -107,7 +107,9 @@ export const TableDateDisplay = ({
|
|||
showSeconds?: boolean
|
||||
}) => (
|
||||
<>
|
||||
<p className="mb-0 text-th-fgd-2">{dayjs(date).format('DD MMM YYYY')}</p>
|
||||
<p className="mb-0 text-xs text-th-fgd-2">
|
||||
{dayjs(date).format('DD MMM YYYY')}
|
||||
</p>
|
||||
<p className="mb-0 text-xs">
|
||||
{dayjs(date).format(showSeconds ? 'h:mm:ssa' : 'h:mma')}
|
||||
</p>
|
||||
|
|
|
@ -22,7 +22,7 @@ const Tabs: FunctionComponent<TabsProps> = ({
|
|||
const { t } = useTranslation('common')
|
||||
|
||||
return (
|
||||
<div className={`relative mb-4 border-b border-th-fgd-4`}>
|
||||
<div className={`relative mb-6 border-b border-th-bkg-4`}>
|
||||
<div
|
||||
className={`default-transition absolute bottom-[-1px] left-0 h-0.5 bg-th-primary`}
|
||||
style={{
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { abbreviateAddress } from '../utils/index'
|
||||
import useLocalStorageState from '../hooks/useLocalStorageState'
|
||||
import MenuItem from './MenuItem'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { ConnectWalletButton } from 'components'
|
||||
import NavDropMenu from './NavDropMenu'
|
||||
import AccountsModal from './AccountsModal'
|
||||
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Settings from './Settings'
|
||||
import TradeNavMenu from './TradeNavMenu'
|
||||
import {
|
||||
CalculatorIcon,
|
||||
CurrencyDollarIcon,
|
||||
LibraryIcon,
|
||||
LightBulbIcon,
|
||||
UserAddIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { MangoIcon, TrophyIcon } from './icons'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
|
||||
const StyledNewLabel = ({ children, ...props }) => (
|
||||
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const TopBar = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { connected, publicKey } = useWallet()
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
|
||||
const cluster = useMangoStore((s) => s.connection.cluster)
|
||||
const [showAccountsModal, setShowAccountsModal] = useState(false)
|
||||
const [defaultMarket] = useLocalStorageState(
|
||||
DEFAULT_MARKET_KEY,
|
||||
initialMarket
|
||||
)
|
||||
const isDevnet = cluster === 'devnet'
|
||||
|
||||
const handleCloseAccounts = useCallback(() => {
|
||||
setShowAccountsModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className={`bg-th-bkg-2`}>
|
||||
<div className={`px-4 xl:px-6`}>
|
||||
<div className={`flex h-14 justify-between`}>
|
||||
<div className={`flex`}>
|
||||
<Link href={defaultMarket.path} shallow={true}>
|
||||
<div
|
||||
className={`flex flex-shrink-0 cursor-pointer items-center`}
|
||||
>
|
||||
<img
|
||||
className={`h-8 w-auto`}
|
||||
src="/assets/icons/logo.svg"
|
||||
alt="next"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div
|
||||
className={`hidden md:ml-4 md:flex md:items-center md:space-x-2 lg:space-x-3`}
|
||||
>
|
||||
<TradeNavMenu />
|
||||
<MenuItem href="/account">{t('account')}</MenuItem>
|
||||
<MenuItem href="/markets">{t('markets')}</MenuItem>
|
||||
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
|
||||
<MenuItem href="/swap">{t('swap')}</MenuItem>
|
||||
<div className="relative">
|
||||
<MenuItem href="/leaderboard">
|
||||
{t('leaderboard')}
|
||||
<div className="absolute -right-3 -top-3 flex h-4 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-yellow-500 px-1.5">
|
||||
<StyledNewLabel className="uppercase text-white">
|
||||
new
|
||||
</StyledNewLabel>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<MenuItem href="/stats">{t('stats')}</MenuItem>
|
||||
<NavDropMenu
|
||||
menuTitle={t('more')}
|
||||
// linksArray: [name: string, href: string, isExternal: boolean]
|
||||
linksArray={[
|
||||
[
|
||||
t('referrals'),
|
||||
'/referral',
|
||||
false,
|
||||
<UserAddIcon className="h-4 w-4" key="referrals" />,
|
||||
],
|
||||
[
|
||||
t('leaderboard'),
|
||||
'/leaderboard',
|
||||
false,
|
||||
<TrophyIcon className="h-4 w-4" key="leaderboard" />,
|
||||
],
|
||||
[
|
||||
t('calculator'),
|
||||
'/risk-calculator',
|
||||
false,
|
||||
<CalculatorIcon className="h-4 w-4" key="calculator" />,
|
||||
],
|
||||
[
|
||||
t('fees'),
|
||||
'/fees',
|
||||
false,
|
||||
<CurrencyDollarIcon className="h-4 w-4" key="fees" />,
|
||||
],
|
||||
[
|
||||
t('learn'),
|
||||
'https://docs.mango.markets/',
|
||||
true,
|
||||
<LightBulbIcon className="h-4 w-4" key="learn" />,
|
||||
],
|
||||
[
|
||||
t('governance'),
|
||||
'https://dao.mango.markets/',
|
||||
true,
|
||||
<LibraryIcon className="h-4 w-4" key="governance" />,
|
||||
],
|
||||
[
|
||||
'Mango v2',
|
||||
'https://v2.mango.markets',
|
||||
true,
|
||||
<MangoIcon
|
||||
className="h-4 w-4 stroke-current"
|
||||
key="mango-v2"
|
||||
/>,
|
||||
],
|
||||
[
|
||||
'Mango v1',
|
||||
'https://v1.mango.markets',
|
||||
true,
|
||||
<MangoIcon
|
||||
className="h-4 w-4 stroke-current"
|
||||
key="mango-v1"
|
||||
/>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
{isDevnet ? <div className="pl-2 text-xxs">Devnet</div> : null}
|
||||
<div className="pl-2">
|
||||
<Settings />
|
||||
</div>
|
||||
{mangoAccount &&
|
||||
mangoAccount.owner.toBase58() === publicKey?.toBase58() ? (
|
||||
<button
|
||||
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
<div className="text-xs font-normal text-th-primary">
|
||||
{mangoAccounts
|
||||
? mangoAccounts.length === 1
|
||||
? `1 ${t('account')}`
|
||||
: `${mangoAccounts.length} ${t('accounts')}`
|
||||
: t('account')}
|
||||
</div>
|
||||
{mangoAccount.name
|
||||
? mangoAccount.name
|
||||
: abbreviateAddress(mangoAccount.publicKey)}
|
||||
</button>
|
||||
) : connected && !mangoAccount ? (
|
||||
<button
|
||||
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
<div className="text-xs font-normal text-th-primary">
|
||||
{`0 ${t('accounts')}`}
|
||||
</div>
|
||||
{t('get-started')} 😎
|
||||
</button>
|
||||
) : null}
|
||||
<ConnectWalletButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{showAccountsModal && (
|
||||
<AccountsModal
|
||||
onClose={handleCloseAccounts}
|
||||
isOpen={showAccountsModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopBar
|
|
@ -1,5 +1,5 @@
|
|||
import { FunctionComponent, useEffect, useMemo, useState } from 'react'
|
||||
import { RefreshIcon } from '@heroicons/react/outline'
|
||||
import { RefreshIcon } from '@heroicons/react/solid'
|
||||
import Input, { Label } from './Input'
|
||||
import Button, { LinkButton } from './Button'
|
||||
import Modal from './Modal'
|
||||
|
@ -89,8 +89,7 @@ const TradeHistoryFilterModal: FunctionComponent<
|
|||
return {
|
||||
...prevSelected,
|
||||
size: {
|
||||
condition: (size) =>
|
||||
parseFloat(size) >= from && parseFloat(size) <= to,
|
||||
condition: (size) => size >= from && size <= to,
|
||||
values: { from: from, to: to },
|
||||
},
|
||||
}
|
||||
|
@ -301,8 +300,8 @@ const FilterButton = ({ filters, filterKey, value, onClick }) => {
|
|||
<button
|
||||
className={`default-transitions rounded-full border border-th-fgd-3 px-3 py-1 text-xs text-th-fgd-1 ${
|
||||
filters[filterKey]?.includes(value) &&
|
||||
'border-th-primary bg-th-primary text-th-bkg-1 hover:text-th-bkg-1'
|
||||
} hover:border-th-primary hover:text-th-primary`}
|
||||
'border-th-primary bg-th-primary text-th-bkg-1 md:hover:text-th-bkg-1'
|
||||
} md:hover:border-th-primary md:hover:text-th-primary`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{t(value.toLowerCase())}
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
InformationCircleIcon,
|
||||
RefreshIcon,
|
||||
SaveIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import { fetchHourlyPerformanceStats } from './account_page/AccountOverview'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import Loading from './Loading'
|
||||
|
@ -190,7 +190,7 @@ const TradeHistoryTable = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -245,7 +245,7 @@ const TradeHistoryTable = ({
|
|||
<SaveIcon className={`mr-1.5 h-4 w-4`} />
|
||||
{t('export-trades-csv')}
|
||||
<Tooltip content={t('trade-export-disclaimer')}>
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -494,7 +494,7 @@ const TradeHistoryTable = ({
|
|||
</>
|
||||
) : (
|
||||
<div className="mb-6">
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
{paginatedData.map((trade: any, index) => (
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
|
@ -598,11 +598,11 @@ const TradeHistoryTable = ({
|
|||
</div>
|
||||
)
|
||||
) : hasActiveFilter ? (
|
||||
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
|
||||
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
|
||||
{t('no-trades-found')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
|
||||
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
|
||||
{t('no-history')}
|
||||
{asPath === '/account' ? (
|
||||
<Link href={'/'} shallow={true}>
|
||||
|
|
|
@ -205,10 +205,10 @@ const MenuCategories: FunctionComponent<MenuCategoriesProps> = ({
|
|||
key={cat.name}
|
||||
onClick={() => onChange(cat.name)}
|
||||
onMouseEnter={() => onChange(cat.name)}
|
||||
className={`default-transition relative flex h-14 w-full cursor-pointer flex-col justify-center whitespace-nowrap rounded-none px-4 font-bold hover:bg-th-bkg-3 ${
|
||||
className={`default-transition relative flex h-14 w-full cursor-pointer flex-col justify-center whitespace-nowrap rounded-none px-4 font-bold md:hover:bg-th-bkg-3 ${
|
||||
activeCategory === cat.name
|
||||
? `bg-th-bkg-3 text-th-primary`
|
||||
: `text-th-fgd-2 hover:text-th-primary`
|
||||
: `text-th-fgd-2 md:hover:text-th-primary`
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
@ -240,14 +240,14 @@ export const FavoriteMarketButton = ({ market }) => {
|
|||
|
||||
return favoriteMarkets.find((mkt) => mkt === market.name) ? (
|
||||
<button
|
||||
className="default-transition flex items-center justify-center text-th-primary hover:text-th-fgd-3"
|
||||
className="default-transition flex items-center justify-center text-th-primary md:hover:text-th-fgd-3"
|
||||
onClick={() => removeFromFavorites(market.name)}
|
||||
>
|
||||
<FilledStarIcon className="h-5 w-5" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="default-transition flex items-center justify-center text-th-fgd-4 hover:text-th-primary"
|
||||
className="default-transition flex items-center justify-center text-th-fgd-4 md:hover:text-th-primary"
|
||||
onClick={() => addToFavorites(market.name)}
|
||||
>
|
||||
<StarIcon className="h-5 w-5" />
|
||||
|
|
|
@ -11,8 +11,6 @@ const TVChartContainer = dynamic(
|
|||
import { useEffect, useState } from 'react'
|
||||
import FloatingElement from '../components/FloatingElement'
|
||||
import Orderbook from '../components/Orderbook'
|
||||
import AccountInfo from './AccountInfo'
|
||||
import UserMarketInfo from './UserMarketInfo'
|
||||
import TradeForm from './trade_form/TradeForm'
|
||||
import UserInfo from './UserInfo'
|
||||
import RecentMarketTrades from './RecentMarketTrades'
|
||||
|
@ -24,46 +22,45 @@ import MarketDetails from './MarketDetails'
|
|||
const ResponsiveGridLayout = WidthProvider(Responsive)
|
||||
|
||||
export const defaultLayouts = {
|
||||
xxl: [
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 19 },
|
||||
{ i: 'tradeForm', x: 8, y: 0, w: 2, h: 19 },
|
||||
{ i: 'orderbook', x: 10, y: 0, w: 2, h: 25 },
|
||||
{ i: 'marketTrades', x: 10, y: 1, w: 2, h: 13 },
|
||||
{ i: 'userInfo', x: 0, y: 1, w: 10, h: 19 },
|
||||
],
|
||||
xl: [
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 27 },
|
||||
{ i: 'marketPosition', x: 9, y: 4, w: 3, h: 13 },
|
||||
{ i: 'accountInfo', x: 9, y: 3, w: 3, h: 14 },
|
||||
{ i: 'orderbook', x: 6, y: 0, w: 3, h: 17 },
|
||||
{ i: 'tradeForm', x: 9, y: 1, w: 3, h: 17 },
|
||||
{ i: 'marketTrades', x: 6, y: 1, w: 3, h: 10 },
|
||||
{ i: 'userInfo', x: 0, y: 2, w: 9, h: 19 },
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 19, minW: 2 },
|
||||
{ i: 'tradeForm', x: 6, y: 0, w: 3, h: 19, minW: 3 },
|
||||
{ i: 'orderbook', x: 9, y: 0, w: 3, h: 25, minW: 2 },
|
||||
{ i: 'marketTrades', x: 9, y: 0, w: 3, h: 13, minW: 2 },
|
||||
{ i: 'userInfo', x: 0, y: 1, w: 9, h: 19, minW: 6 },
|
||||
],
|
||||
lg: [
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 27, minW: 2 },
|
||||
{ i: 'marketPosition', x: 9, y: 2, w: 3, h: 13, minW: 2 },
|
||||
{ i: 'accountInfo', x: 9, y: 1, w: 3, h: 14, minW: 2 },
|
||||
{ i: 'orderbook', x: 6, y: 2, w: 3, h: 17, minW: 2 },
|
||||
{ i: 'tradeForm', x: 9, y: 0, w: 3, h: 17, minW: 3 },
|
||||
{ i: 'marketTrades', x: 6, y: 2, w: 3, h: 10, minW: 2 },
|
||||
{ i: 'userInfo', x: 0, y: 3, w: 9, h: 19, minW: 6 },
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 19, minW: 2 },
|
||||
{ i: 'tradeForm', x: 6, y: 0, w: 3, h: 19, minW: 2 },
|
||||
{ i: 'orderbook', x: 9, y: 0, w: 3, h: 25, minW: 2 },
|
||||
{ i: 'marketTrades', x: 9, y: 1, w: 3, h: 13, minW: 2 },
|
||||
{ i: 'userInfo', x: 0, y: 1, w: 9, h: 19, minW: 6 },
|
||||
],
|
||||
md: [
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 25, minW: 2 },
|
||||
{ i: 'marketPosition', x: 8, y: 1, w: 4, h: 11, minW: 2 },
|
||||
{ i: 'accountInfo', x: 8, y: 0, w: 4, h: 14, minW: 2 },
|
||||
{ i: 'orderbook', x: 0, y: 2, w: 4, h: 19, minW: 2 },
|
||||
{ i: 'tradeForm', x: 4, y: 2, w: 4, h: 19, minW: 3 },
|
||||
{ i: 'marketTrades', x: 8, y: 2, w: 4, h: 19, minW: 2 },
|
||||
{ i: 'userInfo', x: 0, y: 3, w: 12, h: 19, minW: 6 },
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 16, minW: 2 },
|
||||
{ i: 'tradeForm', x: 0, y: 1, w: 4, h: 22, minW: 3 },
|
||||
{ i: 'orderbook', x: 4, y: 1, w: 4, h: 22, minW: 2 },
|
||||
{ i: 'marketTrades', x: 8, y: 1, w: 4, h: 22, minW: 2 },
|
||||
{ i: 'userInfo', x: 0, y: 2, w: 12, h: 19, minW: 6 },
|
||||
],
|
||||
sm: [
|
||||
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 20, minW: 6 },
|
||||
{ i: 'marketPosition', x: 0, y: 1, w: 6, h: 14, minW: 6 },
|
||||
{ i: 'accountInfo', x: 6, y: 1, w: 6, h: 14, minW: 6 },
|
||||
{ i: 'tradeForm', x: 0, y: 2, w: 12, h: 17, minW: 6 },
|
||||
{ i: 'orderbook', x: 0, y: 3, w: 6, h: 17, minW: 6 },
|
||||
{ i: 'marketTrades', x: 6, y: 3, w: 6, h: 17, minW: 6 },
|
||||
{ i: 'userInfo', x: 0, y: 4, w: 12, h: 19, minW: 6 },
|
||||
{ i: 'tradeForm', x: 0, y: 1, w: 12, h: 17, minW: 6 },
|
||||
{ i: 'orderbook', x: 0, y: 2, w: 6, h: 22, minW: 3 },
|
||||
{ i: 'marketTrades', x: 6, y: 2, w: 6, h: 22, minW: 3 },
|
||||
{ i: 'userInfo', x: 0, y: 3, w: 12, h: 19, minW: 6 },
|
||||
],
|
||||
}
|
||||
|
||||
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.1.6'
|
||||
export const breakpoints = { xl: 1600, lg: 1280, md: 1024, sm: 768 }
|
||||
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.2.0'
|
||||
export const breakpoints = { xxl: 1600, xl: 1440, lg: 1170, md: 960, sm: 768 }
|
||||
|
||||
const getCurrentBreakpoint = () => {
|
||||
return Responsive.utils.getBreakpointFromWidth(
|
||||
|
@ -103,7 +100,7 @@ const TradePageGrid: React.FC = () => {
|
|||
const orderbookLayout = layouts[bp].find((obj) => {
|
||||
return obj.i === 'orderbook'
|
||||
})
|
||||
let depth = orderbookLayout.h * 0.891 - 5
|
||||
let depth = orderbookLayout.h * 0.921 - 5
|
||||
const maxNum = max([1, depth])
|
||||
if (typeof maxNum === 'number') {
|
||||
depth = round(maxNum)
|
||||
|
@ -126,7 +123,7 @@ const TradePageGrid: React.FC = () => {
|
|||
<ResponsiveGridLayout
|
||||
layouts={savedLayouts ? savedLayouts : defaultLayouts}
|
||||
breakpoints={breakpoints}
|
||||
cols={{ xl: 12, lg: 12, md: 12, sm: 12 }}
|
||||
cols={{ xxl: 12, xl: 12, lg: 12, md: 12, sm: 12 }}
|
||||
rowHeight={15}
|
||||
isDraggable={!uiLocked}
|
||||
isResizable={!uiLocked}
|
||||
|
@ -147,16 +144,6 @@ const TradePageGrid: React.FC = () => {
|
|||
<div key="tradeForm">
|
||||
<TradeForm />
|
||||
</div>
|
||||
<div key="accountInfo">
|
||||
<FloatingElement className="h-full" showConnect>
|
||||
<AccountInfo />
|
||||
</FloatingElement>
|
||||
</div>
|
||||
<div key="marketPosition">
|
||||
<FloatingElement className="h-full" showConnect>
|
||||
<UserMarketInfo />
|
||||
</FloatingElement>
|
||||
</div>
|
||||
<div key="marketTrades">
|
||||
<FloatingElement className="h-full">
|
||||
<RecentMarketTrades />
|
||||
|
|
|
@ -177,12 +177,20 @@ const TVChartContainer = () => {
|
|||
custom_css_url: '/tradingview-chart.css',
|
||||
loading_screen: {
|
||||
backgroundColor:
|
||||
theme === 'Dark' ? '#1B1B1F' : theme === 'Light' ? '#fff' : '#1D1832',
|
||||
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
|
||||
},
|
||||
overrides: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
'paneProperties.background':
|
||||
theme === 'Dark' ? '#1B1B1F' : theme === 'Light' ? '#fff' : '#1D1832',
|
||||
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
|
||||
'paneProperties.vertGridProperties.color':
|
||||
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
|
||||
'paneProperties.horzGridProperties.color':
|
||||
theme === 'Dark'
|
||||
? '#1B1B1F'
|
||||
: theme === 'Light'
|
||||
? '#f7f7f7'
|
||||
: '#1D1832',
|
||||
...chartStyleOverrides,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/outline'
|
||||
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/solid'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import ResetLayout from './ResetLayout'
|
||||
|
|
|
@ -38,7 +38,7 @@ export const WalletSelect: React.FC<{ wallets: Wallet[] }> = ({ wallets }) => {
|
|||
{wallets?.map((wallet, index) => (
|
||||
<Menu.Item key={index}>
|
||||
<button
|
||||
className="flex w-full flex-row items-center justify-between rounded-none py-1.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
className="flex w-full flex-row items-center justify-between rounded-none py-1.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
|
||||
onClick={() => {
|
||||
select(wallet.adapter.name)
|
||||
}}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Tooltip from './Tooltip'
|
|||
import {
|
||||
ExclamationCircleIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import Select from './Select'
|
||||
import { withdraw } from '../utils/mango'
|
||||
import {
|
||||
|
@ -23,8 +23,9 @@ import {
|
|||
} from '@blockworks-foundation/mango-client'
|
||||
import { notify } from '../utils/notifications'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ExpandableRow } from './TableElements'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import MangoAccountSelect from './MangoAccountSelect'
|
||||
import InlineNotification from './InlineNotification'
|
||||
|
||||
interface WithdrawModalProps {
|
||||
onClose: () => void
|
||||
|
@ -56,8 +57,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
const actions = useMangoStore((s) => s.actions)
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
||||
const [withdrawMangoAccount, setWithdrawMangoAccount] =
|
||||
useState<MangoAccount | null>(mangoAccount)
|
||||
|
||||
const tokens = useMemo(() => mangoGroupConfig.tokens, [mangoGroupConfig])
|
||||
const token = useMemo(
|
||||
|
@ -74,29 +78,43 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
marketMode == MarketMode.CloseOnly ||
|
||||
marketMode == MarketMode.ForceCloseOnly
|
||||
|
||||
const initHealthRatio = useMemo(() => {
|
||||
if (mangoAccount && mangoGroup && mangoCache) {
|
||||
return mangoAccount
|
||||
.getHealthRatio(mangoGroup, mangoCache, 'Init')
|
||||
.toNumber()
|
||||
}
|
||||
return -1
|
||||
}, [mangoAccount])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mangoGroup || !mangoAccount || !withdrawTokenSymbol || !mangoCache)
|
||||
if (
|
||||
!mangoGroup ||
|
||||
!withdrawMangoAccount ||
|
||||
!withdrawTokenSymbol ||
|
||||
!mangoCache
|
||||
)
|
||||
return
|
||||
|
||||
const mintDecimals = mangoGroup.tokens[tokenIndex].decimals
|
||||
const tokenDeposits = mangoAccount.getUiDeposit(
|
||||
const tokenDeposits = withdrawMangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
const tokenBorrows = mangoAccount.getUiBorrow(
|
||||
const tokenBorrows = withdrawMangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
|
||||
const maxWithoutBorrows = nativeI80F48ToUi(
|
||||
mangoAccount
|
||||
withdrawMangoAccount
|
||||
.getAvailableBalance(mangoGroup, mangoCache, tokenIndex)
|
||||
.floor(),
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
)
|
||||
const maxWithBorrows = mangoAccount
|
||||
const maxWithBorrows = withdrawMangoAccount
|
||||
.getMaxWithBorrowForToken(mangoGroup, mangoCache, tokenIndex)
|
||||
.add(maxWithoutBorrows)
|
||||
.mul(I80F48.fromString('0.995')) // handle rounding errors when borrowing
|
||||
|
@ -114,7 +132,8 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
|
||||
if (maxWithdraw.gt(I80F48.fromNumber(0)) && token) {
|
||||
setMaxAmount(
|
||||
floorToDecimal(parseFloat(maxWithdraw.toFixed()), token.decimals)
|
||||
floorToDecimal(parseFloat(maxWithdraw.toFixed()), token.decimals) -
|
||||
1 / Math.pow(10, token.decimals)
|
||||
)
|
||||
} else {
|
||||
setMaxAmount(0)
|
||||
|
@ -134,9 +153,9 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
// clone MangoAccount and arrays to not modify selectedMangoAccount
|
||||
// FIXME: MangoAccount needs type updated to accept null for pubKey
|
||||
// @ts-ignore
|
||||
const simulation = new MangoAccount(null, mangoAccount)
|
||||
simulation.deposits = [...mangoAccount.deposits]
|
||||
simulation.borrows = [...mangoAccount.borrows]
|
||||
const simulation = new MangoAccount(null, withdrawMangoAccount)
|
||||
simulation.deposits = [...withdrawMangoAccount.deposits]
|
||||
simulation.borrows = [...withdrawMangoAccount.borrows]
|
||||
|
||||
// update with simulated values
|
||||
simulation.deposits[tokenIndex] = newDeposit
|
||||
|
@ -149,23 +168,29 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
const liabsVal = simulation
|
||||
.getLiabsVal(mangoGroup, mangoCache, 'Init')
|
||||
.toNumber()
|
||||
const leverage = simulation.getLeverage(mangoGroup, mangoCache).toNumber()
|
||||
const equity = simulation.computeValue(mangoGroup, mangoCache).toNumber()
|
||||
const initHealthRatio = simulation
|
||||
.getHealthRatio(mangoGroup, mangoCache, 'Init')
|
||||
.toNumber()
|
||||
|
||||
const maintHealthRatio = simulation
|
||||
.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
.toNumber()
|
||||
|
||||
const leverage = simulation.getLeverage(mangoGroup, mangoCache).toNumber()
|
||||
|
||||
setSimulation({
|
||||
initHealthRatio,
|
||||
liabsVal,
|
||||
leverage,
|
||||
equity,
|
||||
initHealthRatio,
|
||||
leverage,
|
||||
liabsVal,
|
||||
maintHealthRatio,
|
||||
})
|
||||
}, [
|
||||
includeBorrow,
|
||||
inputAmount,
|
||||
tokenIndex,
|
||||
mangoAccount,
|
||||
withdrawMangoAccount,
|
||||
mangoGroup,
|
||||
mangoCache,
|
||||
])
|
||||
|
@ -181,10 +206,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
token: mangoGroup.tokens[tokenIndex].mint,
|
||||
allowBorrow: includeBorrow,
|
||||
wallet,
|
||||
mangoAccount: withdrawMangoAccount,
|
||||
})
|
||||
.then((txid: string) => {
|
||||
setSubmitting(false)
|
||||
actions.reloadMangoAccount()
|
||||
actions.fetchAllMangoAccounts(wallet)
|
||||
actions.fetchWalletTokens(wallet)
|
||||
notify({
|
||||
title: t('withdraw-success'),
|
||||
|
@ -212,8 +238,8 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
}
|
||||
|
||||
const getDepositsForSelectedAsset = (): I80F48 => {
|
||||
return mangoAccount && mangoCache && mangoGroup
|
||||
? mangoAccount.getUiDeposit(
|
||||
return withdrawMangoAccount && mangoCache && mangoGroup
|
||||
? withdrawMangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
|
@ -228,11 +254,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
}
|
||||
|
||||
const getAccountStatusColor = (
|
||||
initHealthRatio: number,
|
||||
health: number,
|
||||
isRisk?: boolean,
|
||||
isStatus?: boolean
|
||||
) => {
|
||||
if (initHealthRatio < 1) {
|
||||
if (health < 15) {
|
||||
return isRisk ? (
|
||||
<div className="text-th-red">{t('high')}</div>
|
||||
) : isStatus ? (
|
||||
|
@ -240,7 +266,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
) : (
|
||||
'ring-th-red text-th-red'
|
||||
)
|
||||
} else if (initHealthRatio > 1 && initHealthRatio < 10) {
|
||||
} else if (health >= 15 && health < 50) {
|
||||
return isRisk ? (
|
||||
<div className="text-th-orange">{t('moderate')}</div>
|
||||
) : isStatus ? (
|
||||
|
@ -296,7 +322,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
|
||||
return {
|
||||
symbol: token.symbol,
|
||||
balance: mangoAccount
|
||||
balance: withdrawMangoAccount
|
||||
?.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
|
@ -320,10 +346,24 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
{title ? title : t('withdraw-funds')}
|
||||
</ElementTitle>
|
||||
</Modal.Header>
|
||||
{initHealthRatio < 0 ? (
|
||||
<div className="pb-2">
|
||||
<InlineNotification type="error" desc={t('no-new-positions')} />
|
||||
</div>
|
||||
) : null}
|
||||
{mangoAccounts.length > 1 ? (
|
||||
<div className="mb-4">
|
||||
<Label>{t('from-account')}</Label>
|
||||
<MangoAccountSelect
|
||||
onChange={(v) => setWithdrawMangoAccount(v)}
|
||||
value={withdrawMangoAccount}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Label>{t('asset')}</Label>
|
||||
<Select
|
||||
value={
|
||||
withdrawTokenSymbol && mangoAccount ? (
|
||||
withdrawTokenSymbol && withdrawMangoAccount ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
|
@ -335,7 +375,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
/>
|
||||
{withdrawTokenSymbol}
|
||||
</div>
|
||||
{mangoAccount
|
||||
{withdrawMangoAccount
|
||||
?.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
|
@ -373,7 +413,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
<span>{t('borrow-funds')}</span>
|
||||
<Tooltip content={t('tooltip-interest-charged')}>
|
||||
<InformationCircleIcon
|
||||
className={`ml-2 h-5 w-5 cursor-help text-th-primary`}
|
||||
className={`ml-2 h-5 w-5 cursor-help text-th-fgd-4`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -407,22 +447,34 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
onChange={(e) => onChangeAmountInput(e.target.value)}
|
||||
suffix={withdrawTokenSymbol}
|
||||
/>
|
||||
{simulation ? (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
content={t('tooltip-projected-leverage')}
|
||||
className="py-1"
|
||||
>
|
||||
<span
|
||||
className={`${getAccountStatusColor(
|
||||
simulation.initHealthRatio
|
||||
)} ml-1 flex h-10 items-center justify-center rounded bg-th-bkg-1 px-2 ring-1 ring-inset`}
|
||||
</div>
|
||||
{simulation ? (
|
||||
<div className="mt-4 space-y-2 bg-th-bkg-2 p-4">
|
||||
<div className="flex justify-between">
|
||||
<p className="mb-0">{t('tooltip-projected-health')}</p>
|
||||
<p
|
||||
className={`mb-0 font-bold text-th-fgd-1 ${getAccountStatusColor(
|
||||
simulation.maintHealthRatio
|
||||
)}`}
|
||||
>
|
||||
{simulation.maintHealthRatio > 100
|
||||
? '>100'
|
||||
: simulation.maintHealthRatio.toFixed(2)}
|
||||
%
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<p className="mb-0">{t('tooltip-projected-leverage')}</p>
|
||||
<p
|
||||
className={`mb-0 font-bold text-th-fgd-1 ${getAccountStatusColor(
|
||||
simulation.maintHealthRatio
|
||||
)}`}
|
||||
>
|
||||
{simulation.leverage.toFixed(2)}x
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{invalidAmountMessage ? (
|
||||
<div className="flex items-center pt-1.5 text-th-red">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
||||
|
@ -432,7 +484,12 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
<div className={`flex justify-center pt-6`}>
|
||||
<Button
|
||||
onClick={() => setShowSimulation(true)}
|
||||
disabled={Number(inputAmount) <= 0}
|
||||
disabled={
|
||||
!inputAmount ||
|
||||
Number(inputAmount) <= 0 ||
|
||||
!!invalidAmountMessage ||
|
||||
initHealthRatio < 0
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{t('next')}
|
||||
|
@ -448,18 +505,17 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
</ElementTitle>
|
||||
</Modal.Header>
|
||||
{simulation.initHealthRatio < 0 ? (
|
||||
<div className="mb-4 rounded border border-th-red p-2">
|
||||
<div className="flex items-center text-th-red">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||
{t('prices-changed')}
|
||||
</div>
|
||||
<div className="pb-2">
|
||||
<InlineNotification type="error" desc={t('prices-changed')} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="rounded-lg bg-th-bkg-1 p-4 text-center text-th-fgd-1">
|
||||
<div className="pb-1 text-th-fgd-3">{t('about-to-withdraw')}</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="relative text-xl font-semibold">
|
||||
{inputAmount}
|
||||
{Number(inputAmount).toLocaleString(undefined, {
|
||||
maximumFractionDigits: token?.decimals,
|
||||
})}
|
||||
<span className="absolute bottom-0.5 ml-1.5 text-xs font-normal text-th-fgd-4">
|
||||
{withdrawTokenSymbol}
|
||||
</span>
|
||||
|
@ -473,79 +529,6 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
|
|||
)} ${withdrawTokenSymbol}`}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="border-b border-th-bkg-4 pt-4">
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="relative mr-2.5 flex h-2 w-2">
|
||||
<span
|
||||
className={`absolute inline-flex h-full w-full animate-ping rounded-full ${getAccountStatusColor(
|
||||
simulation.initHealthRatio,
|
||||
false,
|
||||
true
|
||||
)} opacity-75`}
|
||||
></span>
|
||||
<span
|
||||
className={`relative inline-flex h-2 w-2 rounded-full ${getAccountStatusColor(
|
||||
simulation.initHealthRatio,
|
||||
false,
|
||||
true
|
||||
)}`}
|
||||
></span>
|
||||
</span>
|
||||
{t('health-check')}
|
||||
<Tooltip content={t('tooltip-after-withdrawal')}>
|
||||
<InformationCircleIcon
|
||||
className={`ml-2 h-5 w-5 cursor-help text-th-primary`}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
panelTemplate={
|
||||
simulation ? (
|
||||
<div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<p className="mb-0">{t('account-value')}</p>
|
||||
<div className="text-th-fgd-1">
|
||||
$
|
||||
{simulation.equity.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<p className="mb-0">{t('account-risk')}</p>
|
||||
<div className="text-th-fgd-1">
|
||||
{getAccountStatusColor(
|
||||
simulation.initHealthRatio,
|
||||
true
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-2">
|
||||
<p className="mb-0">{t('leverage')}</p>
|
||||
<div className="text-th-fgd-1">
|
||||
{simulation.leverage.toFixed(2)}x
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<p className="mb-0">{t('borrow-value')}</p>
|
||||
<div className="text-th-fgd-1">
|
||||
$
|
||||
{simulation.liabsVal.toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 flex flex-col items-center`}>
|
||||
<Button
|
||||
onClick={handleWithdraw}
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
I80F48,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { useBalances } from '../../hooks/useBalances'
|
||||
import {
|
||||
formatUsdValue,
|
||||
i80f48ToPercent,
|
||||
|
@ -25,7 +24,6 @@ import { useWallet } from '@solana/wallet-adapter-react'
|
|||
|
||||
export default function AccountBorrows() {
|
||||
const { t } = useTranslation('common')
|
||||
const balances = useBalances()
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const mangoConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
||||
|
@ -34,6 +32,7 @@ export default function AccountBorrows() {
|
|||
const loadingMangoAccount = useMangoStore(
|
||||
(s) => s.selectedMangoAccount.initialLoad
|
||||
)
|
||||
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
|
||||
|
||||
const [borrowSymbol, setBorrowSymbol] = useState('')
|
||||
const [depositToSettle, setDepositToSettle] = useState<any | null>(null)
|
||||
|
@ -77,7 +76,7 @@ export default function AccountBorrows() {
|
|||
<div className="flex flex-col pb-8 pt-4">
|
||||
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full align-middle sm:px-6 lg:px-8">
|
||||
{balances.find((b) => b?.borrows?.gt(ZERO_I80F48)) ? (
|
||||
{spotBalances.find((b) => b?.borrows?.gt(ZERO_I80F48)) ? (
|
||||
!isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
|
@ -89,7 +88,7 @@ export default function AccountBorrows() {
|
|||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{balances
|
||||
{spotBalances
|
||||
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
|
||||
.map((asset) => {
|
||||
const token = getTokenBySymbol(
|
||||
|
@ -175,12 +174,12 @@ export default function AccountBorrows() {
|
|||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('asset')}
|
||||
colTwoHeader={t('balance')}
|
||||
/>
|
||||
{balances
|
||||
{spotBalances
|
||||
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
|
||||
.map((asset, i) => {
|
||||
const token = getTokenBySymbol(
|
||||
|
@ -286,7 +285,7 @@ export default function AccountBorrows() {
|
|||
)
|
||||
) : (
|
||||
<div
|
||||
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
|
||||
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
|
||||
>
|
||||
{t('no-borrows')}
|
||||
</div>
|
||||
|
@ -425,7 +424,7 @@ export default function AccountBorrows() {
|
|||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="border-b border-th-bkg-4">
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('asset')}
|
||||
colTwoHeader={`${t('deposit')}/${t('borrow-rate')}`}
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '../TableElements'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Select from '../Select'
|
||||
import Pagination from '../Pagination'
|
||||
import usePagination from '../../hooks/usePagination'
|
||||
import { roundToDecimal } from '../../utils'
|
||||
|
@ -24,7 +23,8 @@ const utc = require('dayjs/plugin/utc')
|
|||
dayjs.extend(utc)
|
||||
import { exportDataToCSV } from '../../utils/export'
|
||||
import Button from '../Button'
|
||||
import { SaveIcon } from '@heroicons/react/outline'
|
||||
import { SaveIcon } from '@heroicons/react/solid'
|
||||
import TabButtons from 'components/TabButtons'
|
||||
|
||||
const QUOTE_DECIMALS = 6
|
||||
|
||||
|
@ -305,50 +305,23 @@ const AccountFunding = () => {
|
|||
<>
|
||||
{!isEmpty(hourlyFunding) && !loadHourlyStats ? (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between pb-4 pt-6">
|
||||
<h2>{t('history')}</h2>
|
||||
<Select
|
||||
value={selectedAsset}
|
||||
onChange={(a) => setSelectedAsset(a)}
|
||||
className="w-24 sm:hidden"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{Object.keys(hourlyFunding).map((token: string) => (
|
||||
<Select.Option
|
||||
key={token}
|
||||
value={token}
|
||||
className={`default-transition relative flex w-full cursor-pointer rounded-md bg-th-bkg-1 px-3 py-3 hover:bg-th-bkg-3 focus:outline-none`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{token}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</div>
|
||||
</Select>
|
||||
<div className="hidden pb-4 sm:flex sm:pb-0">
|
||||
{Object.keys(hourlyFunding).map((token: string) => (
|
||||
<div
|
||||
className={`default-transition ml-2 cursor-pointer rounded-md bg-th-bkg-3 px-2 py-1
|
||||
${
|
||||
selectedAsset === token
|
||||
? `text-th-primary ring-1 ring-inset ring-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedAsset(token)}
|
||||
key={token}
|
||||
>
|
||||
{token}-PERP
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pb-2 pt-6">
|
||||
<h2 className="mb-4">{t('history')}</h2>
|
||||
<TabButtons
|
||||
activeTab={selectedAsset}
|
||||
tabs={Object.keys(hourlyFunding).map((token: string) => ({
|
||||
label: token,
|
||||
key: token,
|
||||
}))}
|
||||
onClick={setSelectedAsset}
|
||||
showSymbolIcon
|
||||
/>
|
||||
</div>
|
||||
{selectedAsset && chartData.length > 0 ? (
|
||||
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
|
||||
{chartData.find((d) => d.funding !== 0) ? (
|
||||
<div
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4"
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
|
|
|
@ -5,9 +5,10 @@ import {
|
|||
ExternalLinkIcon,
|
||||
InformationCircleIcon,
|
||||
SaveIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
import {
|
||||
getMarketByBaseSymbolAndKind,
|
||||
getTokenBySymbol,
|
||||
PerpMarket,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
Td,
|
||||
TableDateDisplay,
|
||||
Row,
|
||||
ExpandableRow,
|
||||
} from '../TableElements'
|
||||
import { LinkButton } from '../Button'
|
||||
import { useSortableData } from '../../hooks/useSortableData'
|
||||
|
@ -32,16 +34,20 @@ import Button from '../Button'
|
|||
import { useViewport } from '../../hooks/useViewport'
|
||||
import { breakpoints } from '.././TradePageGrid'
|
||||
import MobileTableHeader from 'components/mobile/MobileTableHeader'
|
||||
import AccountInterest from './AccountInterest'
|
||||
import AccountFunding from './AccountFunding'
|
||||
import TabButtons from 'components/TabButtons'
|
||||
|
||||
const historyViews = [
|
||||
{ label: 'Trades', key: 'Trades' },
|
||||
{ label: 'Deposits', key: 'Deposit' },
|
||||
{ label: 'Withdrawals', key: 'Withdraw' },
|
||||
{ label: 'Interest', key: 'Interest' },
|
||||
{ label: 'Funding', key: 'Funding' },
|
||||
{ label: 'Liquidations', key: 'Liquidation' },
|
||||
]
|
||||
|
||||
export default function AccountHistory() {
|
||||
const { t } = useTranslation('common')
|
||||
const [view, setView] = useState('Trades')
|
||||
const [history, setHistory] = useState(null)
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
|
@ -68,24 +74,8 @@ export default function AccountHistory() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:px-4">
|
||||
{historyViews.map(({ label, key }, index) => (
|
||||
<div
|
||||
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
|
||||
index > 0 ? 'ml-4 md:ml-2' : null
|
||||
} default-transition cursor-pointer rounded-md
|
||||
${
|
||||
view === key
|
||||
? `text-th-primary`
|
||||
: `text-th-fgd-3 hover:text-th-fgd-1`
|
||||
}
|
||||
`}
|
||||
onClick={() => setView(key)}
|
||||
key={key as string}
|
||||
>
|
||||
{t(label.toLowerCase())}
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-2">
|
||||
<TabButtons activeTab={view} tabs={historyViews} onClick={setView} />
|
||||
</div>
|
||||
<ViewContent view={view} history={history} />
|
||||
</>
|
||||
|
@ -100,6 +90,10 @@ const ViewContent = ({ view, history }) => {
|
|||
return <HistoryTable history={history} view={view} />
|
||||
case 'Withdraw':
|
||||
return <HistoryTable history={history} view={view} />
|
||||
case 'Interest':
|
||||
return <AccountInterest />
|
||||
case 'Funding':
|
||||
return <AccountFunding />
|
||||
case 'Liquidation':
|
||||
return <LiquidationHistoryTable history={history} view={view} />
|
||||
default:
|
||||
|
@ -108,23 +102,40 @@ const ViewContent = ({ view, history }) => {
|
|||
}
|
||||
|
||||
const parseActivityDetails = (activity_details, activity_type, perpMarket) => {
|
||||
const groupConfig = useMangoStore.getState().selectedMangoGroup.config
|
||||
let assetGained, assetLost
|
||||
|
||||
const assetSymbol =
|
||||
activity_type === 'liquidate_perp_market'
|
||||
? 'USD (PERP)'
|
||||
? 'USDC'
|
||||
: activity_details.asset_symbol
|
||||
|
||||
const assetDecimals = activity_type.includes('perp')
|
||||
? getMarketByBaseSymbolAndKind(
|
||||
groupConfig,
|
||||
assetSymbol.split('-')[0],
|
||||
'perp'
|
||||
).baseDecimals
|
||||
: getTokenBySymbol(groupConfig, assetSymbol.split('-')[0]).decimals
|
||||
|
||||
const liabSymbol =
|
||||
activity_type === 'liquidate_perp_market' ||
|
||||
activity_details.liab_type === 'Perp'
|
||||
? activity_details.liab_symbol.includes('USDC')
|
||||
? 'USD (PERP)'
|
||||
? 'USDC'
|
||||
: `${activity_details.liab_symbol}-PERP`
|
||||
: activity_details.liab_symbol
|
||||
|
||||
const liabDecimals = activity_type.includes('perp')
|
||||
? getMarketByBaseSymbolAndKind(
|
||||
groupConfig,
|
||||
liabSymbol.split('-')[0],
|
||||
'perp'
|
||||
).baseDecimals
|
||||
: getTokenBySymbol(groupConfig, liabSymbol.split('-')[0]).decimals
|
||||
|
||||
const liabAmount =
|
||||
perpMarket && liabSymbol !== 'USD (PERP)'
|
||||
perpMarket && liabSymbol !== 'USDC'
|
||||
? perpMarket.baseLotsToNumber(activity_details.liab_amount)
|
||||
: activity_details.liab_amount
|
||||
|
||||
|
@ -132,12 +143,14 @@ const parseActivityDetails = (activity_details, activity_type, perpMarket) => {
|
|||
|
||||
const asset_amount = {
|
||||
amount: parseFloat(assetAmount),
|
||||
decimals: assetDecimals,
|
||||
symbol: assetSymbol,
|
||||
price: parseFloat(activity_details.asset_price),
|
||||
}
|
||||
|
||||
const liab_amount = {
|
||||
amount: parseFloat(liabAmount),
|
||||
decimals: liabDecimals,
|
||||
symbol: liabSymbol,
|
||||
price: parseFloat(activity_details.liab_price),
|
||||
}
|
||||
|
@ -176,6 +189,8 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
: []
|
||||
}, [history, view])
|
||||
const { items, requestSort, sortConfig } = useSortableData(filteredHistory)
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.md : false
|
||||
|
||||
const exportHistoryToCSV = () => {
|
||||
const dataToExport = history
|
||||
|
@ -234,7 +249,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
|
@ -248,7 +263,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
</Button>
|
||||
</div>
|
||||
{items.length ? (
|
||||
<>
|
||||
!isMobile ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
|
@ -275,7 +290,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('asset_amount')}
|
||||
>
|
||||
<span className="font-normal">Asset Lost</span>
|
||||
<span className="font-normal">{t('asset-liquidated')}</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'asset_amount'
|
||||
|
@ -287,30 +302,12 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('asset_price')}
|
||||
>
|
||||
<span className="font-normal">Price</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'asset_price'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Th>
|
||||
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('liab_amount')}
|
||||
>
|
||||
<span className="font-normal">Asset Gained</span>
|
||||
<span className="font-normal">{t('asset-returned')}</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'liab_amount'
|
||||
|
@ -323,21 +320,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
</LinkButton>
|
||||
</Th>
|
||||
<Th>
|
||||
<LinkButton
|
||||
className="flex items-center font-normal no-underline"
|
||||
onClick={() => requestSort('liab_price')}
|
||||
>
|
||||
<span className="font-normal">Price</span>
|
||||
<ArrowSmDownIcon
|
||||
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
|
||||
sortConfig?.key === 'liab_price'
|
||||
? sortConfig.direction === 'ascending'
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
: null
|
||||
}`}
|
||||
/>
|
||||
</LinkButton>
|
||||
<span className="font-normal">{t('liquidation-fee')}</span>
|
||||
</Th>
|
||||
<Th>
|
||||
<span></span>
|
||||
|
@ -371,8 +354,10 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
perpMarket
|
||||
)
|
||||
|
||||
const lostDecimals = assetLost.symbol === 'SOL' ? 9 : 6
|
||||
const gainedDecimals = assetGained.symbol === 'SOL' ? 9 : 6
|
||||
const valueLost = Math.abs(assetLost.amount * assetLost.price)
|
||||
const valueGained = assetGained.amount * assetGained.price
|
||||
const liquidationFee = valueGained - valueLost
|
||||
|
||||
return (
|
||||
<TrBody key={activity_details.signature}>
|
||||
<Td>
|
||||
|
@ -380,35 +365,43 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
date={activity_details.block_datetime}
|
||||
/>
|
||||
</Td>
|
||||
|
||||
<Td>
|
||||
<span className="text-th-red">
|
||||
<span>
|
||||
{Math.abs(assetLost.amount).toLocaleString(undefined, {
|
||||
maximumFractionDigits: lostDecimals,
|
||||
maximumFractionDigits: assetLost.decimals,
|
||||
})}{' '}
|
||||
</span>
|
||||
{assetLost.symbol}
|
||||
{`${assetLost.symbol} at ${formatUsdValue(
|
||||
assetLost.price
|
||||
)}`}
|
||||
<p className="mb-0 text-xs text-th-fgd-3">
|
||||
{formatUsdValue(valueLost)}
|
||||
</p>
|
||||
</Td>
|
||||
<Td>
|
||||
{assetLost.price.toLocaleString(undefined, {
|
||||
maximumFractionDigits: lostDecimals,
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-th-green">
|
||||
<span>
|
||||
{Math.abs(assetGained.amount).toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
maximumFractionDigits: gainedDecimals,
|
||||
maximumFractionDigits: assetGained.decimals,
|
||||
}
|
||||
)}{' '}
|
||||
</span>
|
||||
{assetGained.symbol}
|
||||
{`${assetGained.symbol} at ${formatUsdValue(
|
||||
assetGained.price
|
||||
)}`}
|
||||
<p className="mb-0 text-xs text-th-fgd-3">
|
||||
{formatUsdValue(valueGained)}
|
||||
</p>
|
||||
</Td>
|
||||
<Td>
|
||||
{assetGained.price.toLocaleString(undefined, {
|
||||
maximumFractionDigits: gainedDecimals,
|
||||
})}
|
||||
<span
|
||||
className={
|
||||
liquidationFee >= 0 ? 'text-th-green' : 'text-th-red'
|
||||
}
|
||||
>
|
||||
{formatUsdValue(liquidationFee)}
|
||||
</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<a
|
||||
|
@ -417,7 +410,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>View Transaction</span>
|
||||
<span>{t('view-transaction')}</span>
|
||||
<ExternalLinkIcon className={`ml-1.5 h-4 w-4`} />
|
||||
</a>
|
||||
</Td>
|
||||
|
@ -426,9 +419,118 @@ const LiquidationHistoryTable = ({ history, view }) => {
|
|||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
) : (
|
||||
<div className="border-b border-th-bkg-3">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('date')}
|
||||
colTwoHeader={t('liquidation-fee')}
|
||||
/>
|
||||
{items.map(({ activity_details, activity_type }) => {
|
||||
let perpMarket: PerpMarket | null = null
|
||||
if (activity_type.includes('perp')) {
|
||||
const symbol = activity_details.perp_market.split('-')[0]
|
||||
const marketConfig = getMarketByBaseSymbolAndKind(
|
||||
groupConfig,
|
||||
symbol,
|
||||
'perp'
|
||||
)
|
||||
perpMarket = markets[
|
||||
marketConfig.publicKey.toString()
|
||||
] as PerpMarket
|
||||
}
|
||||
|
||||
const [assetGained, assetLost] = parseActivityDetails(
|
||||
activity_details,
|
||||
activity_type,
|
||||
perpMarket
|
||||
)
|
||||
|
||||
const valueLost = Math.abs(assetLost.amount * assetLost.price)
|
||||
const valueGained = assetGained.amount * assetGained.price
|
||||
const liquidationFee = valueGained - valueLost
|
||||
return (
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
<div className="flex w-full items-center justify-between text-th-fgd-1">
|
||||
<div className="text-left">
|
||||
<TableDateDisplay
|
||||
date={activity_details.block_datetime}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right text-th-fgd-1">
|
||||
<span
|
||||
className={
|
||||
liquidationFee >= 0
|
||||
? 'text-th-green'
|
||||
: 'text-th-red'
|
||||
}
|
||||
>
|
||||
{formatUsdValue(liquidationFee)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={`${activity_details.signature}`}
|
||||
panelTemplate={
|
||||
<div className="grid grid-flow-row grid-cols-2 gap-4 pb-4">
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('asset-liquidated')}
|
||||
</div>
|
||||
<span>
|
||||
{Math.abs(assetLost.amount).toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
maximumFractionDigits: assetLost.decimals,
|
||||
}
|
||||
)}{' '}
|
||||
</span>
|
||||
{`${assetLost.symbol} at ${formatUsdValue(
|
||||
assetLost.price
|
||||
)}`}
|
||||
<p className="mb-0 text-xs text-th-fgd-3">
|
||||
{formatUsdValue(valueLost)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('asset-returned')}
|
||||
</div>
|
||||
<span>
|
||||
{Math.abs(assetGained.amount).toLocaleString(
|
||||
undefined,
|
||||
{
|
||||
maximumFractionDigits: assetGained.decimals,
|
||||
}
|
||||
)}{' '}
|
||||
</span>
|
||||
{`${assetGained.symbol} at ${formatUsdValue(
|
||||
assetGained.price
|
||||
)}`}
|
||||
<p className="mb-0 text-xs text-th-fgd-3">
|
||||
{formatUsdValue(valueGained)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<a
|
||||
className="default-transition flex h-8 w-full items-center justify-center rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-2 text-xs font-bold text-th-fgd-2"
|
||||
href={`https://explorer.solana.com/tx/${activity_details.signature}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>{t('view-transaction')}</span>
|
||||
<ExternalLinkIcon className={`ml-1.5 h-4 w-4`} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
|
||||
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
|
||||
{t('history-empty')}
|
||||
</div>
|
||||
)}
|
||||
|
@ -511,7 +613,7 @@ const HistoryTable = ({ history, view }) => {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
|
@ -647,7 +749,7 @@ const HistoryTable = ({ history, view }) => {
|
|||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="mb-4 border-b border-th-bkg-4">
|
||||
<div className="mb-4 border-b border-th-bkg-3">
|
||||
<MobileTableHeader
|
||||
colOneHeader={t('date')}
|
||||
colTwoHeader={t('asset')}
|
||||
|
@ -691,7 +793,7 @@ const HistoryTable = ({ history, view }) => {
|
|||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
|
||||
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
|
||||
{t('history-empty')}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { getTokenBySymbol } from '@blockworks-foundation/mango-client'
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import Select from '../Select'
|
||||
import {
|
||||
Table,
|
||||
TableDateDisplay,
|
||||
|
@ -27,8 +26,9 @@ import useLocalStorageState from '../../hooks/useLocalStorageState'
|
|||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
import { exportDataToCSV } from '../../utils/export'
|
||||
import { SaveIcon } from '@heroicons/react/outline'
|
||||
import { SaveIcon } from '@heroicons/react/solid'
|
||||
import Button from '../Button'
|
||||
import TabButtons from 'components/TabButtons'
|
||||
|
||||
interface InterestStats {
|
||||
[key: string]: {
|
||||
|
@ -458,50 +458,22 @@ const AccountInterest = () => {
|
|||
<>
|
||||
{!isEmpty(hourlyInterestStats) && !loadHourlyStats ? (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between pb-4 pt-8">
|
||||
<h2>{t('history')}</h2>
|
||||
<Select
|
||||
value={selectedAsset}
|
||||
onChange={(a) => setSelectedAsset(a)}
|
||||
className="w-24 md:hidden"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{Object.keys(hourlyInterestStats).map((token: string) => (
|
||||
<Select.Option
|
||||
key={token}
|
||||
value={token}
|
||||
className={`default-transition relative flex w-full cursor-pointer rounded-md bg-th-bkg-1 px-3 py-3 hover:bg-th-bkg-3 focus:outline-none`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{token}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</div>
|
||||
</Select>
|
||||
<div className="hidden pb-4 sm:pb-0 md:flex">
|
||||
{Object.keys(hourlyInterestStats).map((token: string) => (
|
||||
<div
|
||||
className={`default-transition ml-2 cursor-pointer rounded-md bg-th-bkg-3 px-2 py-1
|
||||
${
|
||||
selectedAsset === token
|
||||
? `text-th-primary ring-1 ring-inset ring-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedAsset(token)}
|
||||
key={token}
|
||||
>
|
||||
{token}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pb-2 pt-8">
|
||||
<h2 className="mb-4">{t('history')}</h2>
|
||||
<TabButtons
|
||||
activeTab={selectedAsset}
|
||||
tabs={Object.keys(hourlyInterestStats).map(
|
||||
(token: string) => ({ label: token, key: token })
|
||||
)}
|
||||
onClick={setSelectedAsset}
|
||||
showSymbolIcon
|
||||
/>
|
||||
</div>
|
||||
{selectedAsset && chartData.length > 0 ? (
|
||||
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
|
||||
{chartData.find((d) => d.interest !== 0) ? (
|
||||
<div
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
|
@ -533,7 +505,7 @@ const AccountInterest = () => {
|
|||
) : null}
|
||||
{chartData.find((d) => d.value !== 0) ? (
|
||||
<div
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
{token ? (
|
||||
|
|
|
@ -1,30 +1,19 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { ExclamationIcon } from '@heroicons/react/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { formatUsdValue } from '../../utils'
|
||||
import BalancesTable from '../BalancesTable'
|
||||
import Switch from '../Switch'
|
||||
import useLocalStorageState from '../../hooks/useLocalStorageState'
|
||||
import ButtonGroup from '../ButtonGroup'
|
||||
import PerformanceChart from './PerformanceChart'
|
||||
import PositionsTable from '../PerpPositionsTable'
|
||||
import AccountOverviewStats from './AccountOverviewStats'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
const SHOW_ZERO_BALANCE_KEY = 'showZeroAccountBalances-0.2'
|
||||
|
||||
const performanceRangePresets = [
|
||||
{ label: '24h', value: 1 },
|
||||
{ label: '7d', value: 7 },
|
||||
{ label: '30d', value: 30 },
|
||||
{ label: '3m', value: 90 },
|
||||
]
|
||||
const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label)
|
||||
|
||||
export const fetchHourlyPerformanceStats = async (
|
||||
mangoAccountPk: string,
|
||||
range: number
|
||||
|
@ -58,8 +47,6 @@ export default function AccountOverview() {
|
|||
true
|
||||
)
|
||||
|
||||
const [pnl, setPnl] = useState(0)
|
||||
const [performanceRange, setPerformanceRange] = useState('30d')
|
||||
const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -68,12 +55,7 @@ export default function AccountOverview() {
|
|||
if (!pubKey) {
|
||||
return
|
||||
}
|
||||
const stats = await fetchHourlyPerformanceStats(
|
||||
pubKey,
|
||||
performanceRangePresets[performanceRangePresets.length - 1].value
|
||||
)
|
||||
|
||||
setPnl(stats?.length ? stats?.[0]?.['pnl'] : 0)
|
||||
const stats = await fetchHourlyPerformanceStats(pubKey, 30)
|
||||
setHourlyPerformanceStats(stats)
|
||||
}
|
||||
if (pubKey) {
|
||||
|
@ -81,143 +63,26 @@ export default function AccountOverview() {
|
|||
}
|
||||
}, [mangoAccount?.publicKey])
|
||||
|
||||
const maintHealthRatio = useMemo(() => {
|
||||
return mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
: 100
|
||||
}, [mangoAccount, mangoGroup, mangoCache])
|
||||
|
||||
const initHealthRatio = useMemo(() => {
|
||||
return mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
|
||||
: 100
|
||||
}, [mangoAccount, mangoGroup, mangoCache])
|
||||
|
||||
const mangoAccountValue = useMemo(() => {
|
||||
return mangoAccount && mangoGroup && mangoCache
|
||||
? +mangoAccount.computeValue(mangoGroup, mangoCache)
|
||||
: 0
|
||||
}, [mangoAccount])
|
||||
}, [mangoAccount, mangoGroup, mangoCache])
|
||||
|
||||
return mangoAccount ? (
|
||||
<>
|
||||
<div className="flex flex-col pb-8 md:flex-row md:space-x-6 md:pb-12">
|
||||
<div className="w-full pb-8 md:w-1/3 md:pb-0 lg:w-1/4">
|
||||
<h2 className="mb-4">{t('summary')}</h2>
|
||||
<div className="border-y border-th-bkg-4 p-3 sm:p-4">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
|
||||
{t('account-value')}
|
||||
</div>
|
||||
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
|
||||
{formatUsdValue(mangoAccountValue)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
|
||||
{t('pnl')}{' '}
|
||||
{hourlyPerformanceStats?.length ? (
|
||||
<div className="text-xs text-th-fgd-4">
|
||||
{dayjs(hourlyPerformanceStats[0]['time']).format(
|
||||
'MMM D YYYY, h:mma'
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
|
||||
{formatUsdValue(pnl)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
|
||||
{t('leverage')}
|
||||
</div>
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
|
||||
{mangoAccount.getLeverage(mangoGroup, mangoCache).toFixed(2)}x
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="p-3 sm:p-4">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
|
||||
{t('health-ratio')}
|
||||
</div>
|
||||
<div className={`text-xl font-bold text-th-fgd-1 sm:text-2xl`}>
|
||||
{maintHealthRatio < 1000 ? maintHealthRatio.toFixed(2) : '>100'}%
|
||||
</div>
|
||||
{mangoAccount.beingLiquidated ? (
|
||||
<div className="flex items-center pt-0.5 text-xs sm:pt-2 sm:text-sm">
|
||||
<ExclamationIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red sm:h-7 sm:w-7" />
|
||||
<span className="text-th-red">{t('being-liquidated')}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex h-1 rounded bg-th-bkg-3">
|
||||
<div
|
||||
style={{
|
||||
width: `${maintHealthRatio}%`,
|
||||
}}
|
||||
className={`flex rounded ${
|
||||
maintHealthRatio > 30
|
||||
? 'bg-th-green'
|
||||
: initHealthRatio > 0
|
||||
? 'bg-th-orange'
|
||||
: 'bg-th-red'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-80 w-full md:h-auto md:w-2/3 lg:w-3/4">
|
||||
<div className="mb-4 ml-auto md:w-56">
|
||||
<ButtonGroup
|
||||
activeValue={performanceRange}
|
||||
className="h-8"
|
||||
onChange={(p) => setPerformanceRange(p)}
|
||||
values={performanceRangePresetLabels}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:border-t md:border-th-bkg-4">
|
||||
<PerformanceChart
|
||||
hourlyPerformanceStats={hourlyPerformanceStats}
|
||||
performanceRange={performanceRange}
|
||||
accountValue={mangoAccountValue}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-12 md:gap-x-6">
|
||||
<div className="relative col-span-12 h-[700px] md:h-[615px] lg:h-[430px] xl:h-[320px]">
|
||||
<AccountOverviewStats
|
||||
hourlyPerformanceStats={hourlyPerformanceStats}
|
||||
accountValue={mangoAccountValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-8 pt-20 md:pt-0">
|
||||
<div className="pb-8">
|
||||
<h2 className="mb-4">{t('perp-positions')}</h2>
|
||||
<PositionsTable />
|
||||
</div>
|
||||
<h2 className="mb-4">{t('assets-liabilities')}</h2>
|
||||
|
||||
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 pb-8 md:grid-cols-2 md:grid-rows-1 md:gap-4 md:pb-12">
|
||||
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
|
||||
<div className="pb-0.5 text-th-fgd-3">{t('total-assets')}</div>
|
||||
<div className="flex items-center">
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
|
||||
{formatUsdValue(
|
||||
+mangoAccount.getAssetsVal(mangoGroup, mangoCache)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
|
||||
<div className="pb-0.5 text-th-fgd-3">{t('total-liabilities')}</div>
|
||||
<div className="flex items-center">
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
|
||||
{formatUsdValue(
|
||||
+mangoAccount.getLiabsVal(mangoGroup, mangoCache)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between pb-4">
|
||||
<h2>{t('balances')}</h2>
|
||||
<Switch
|
||||
|
|
|
@ -0,0 +1,581 @@
|
|||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip as ChartTooltip,
|
||||
} from 'recharts'
|
||||
import { InformationCircleIcon, ScaleIcon } from '@heroicons/react/solid'
|
||||
import useDimensions from 'react-cool-dimensions'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ZERO_BN } from '@blockworks-foundation/mango-client'
|
||||
import ButtonGroup from '../ButtonGroup'
|
||||
import { formatUsdValue } from '../../utils'
|
||||
import { numberCompacter } from '../SwapTokenInfo'
|
||||
import Checkbox from '../Checkbox'
|
||||
import Tooltip from '../Tooltip'
|
||||
import useMangoStore, { PerpPosition } from 'stores/useMangoStore'
|
||||
import LongShortChart from './LongShortChart'
|
||||
import HealthHeart from 'components/HealthHeart'
|
||||
|
||||
type AccountOverviewStats = {
|
||||
hourlyPerformanceStats: any[]
|
||||
performanceRange: '24hr' | '7d' | '30d' | '3m'
|
||||
}
|
||||
|
||||
const defaultData = [
|
||||
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
|
||||
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
|
||||
]
|
||||
|
||||
const performanceRangePresets = [
|
||||
{ label: '24h', value: 1 },
|
||||
{ label: '7d', value: 7 },
|
||||
{ label: '30d', value: 30 },
|
||||
{ label: '3m', value: 90 },
|
||||
]
|
||||
const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label)
|
||||
|
||||
const AccountOverviewStats = ({ hourlyPerformanceStats, accountValue }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('common')
|
||||
const { observe, width, height } = useDimensions()
|
||||
const [chartToShow, setChartToShow] = useState<string>('Value')
|
||||
const [chartData, setChartData] = useState<any[]>([])
|
||||
const [mouseData, setMouseData] = useState<string | null>(null)
|
||||
const [performanceRange, setPerformanceRange] = useState('30d')
|
||||
const [showSpotPnl, setShowSpotPnl] = useState(true)
|
||||
const [showPerpPnl, setShowPerpPnl] = useState(true)
|
||||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
|
||||
const perpPositions = useMangoStore(
|
||||
(s) => s.selectedMangoAccount.perpPositions
|
||||
)
|
||||
|
||||
const maintHealthRatio = useMemo(() => {
|
||||
return mangoAccount && mangoGroup && mangoCache
|
||||
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
|
||||
: 100
|
||||
}, [mangoAccount, mangoGroup, mangoCache])
|
||||
|
||||
const { longData, shortData, longExposure, shortExposure } = useMemo(() => {
|
||||
const longData: any = []
|
||||
const shortData: any = []
|
||||
|
||||
if (!spotBalances || !perpPositions) {
|
||||
longData.push({ symbol: 'spacer', value: 1 })
|
||||
shortData.push({ symbol: 'spacer', value: 1 })
|
||||
// return {}
|
||||
}
|
||||
|
||||
const DUST_THRESHOLD = 0.05
|
||||
const netUnsettledPositionsValue = perpPositions.reduce(
|
||||
(a, c) => a + (c?.unsettledPnl ?? 0),
|
||||
0
|
||||
)
|
||||
|
||||
for (const { net, symbol, value } of spotBalances) {
|
||||
let amount = Number(net)
|
||||
let totValue = Number(value)
|
||||
if (symbol === 'USDC') {
|
||||
amount += netUnsettledPositionsValue
|
||||
totValue += netUnsettledPositionsValue
|
||||
}
|
||||
if (totValue > DUST_THRESHOLD) {
|
||||
longData.push({
|
||||
asset: symbol,
|
||||
amount: amount,
|
||||
symbol: symbol,
|
||||
value: totValue,
|
||||
})
|
||||
}
|
||||
if (-totValue > DUST_THRESHOLD) {
|
||||
shortData.push({
|
||||
asset: symbol,
|
||||
amount: Math.abs(amount),
|
||||
symbol: symbol,
|
||||
value: Math.abs(totValue),
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const {
|
||||
marketConfig,
|
||||
basePosition,
|
||||
notionalSize,
|
||||
perpAccount,
|
||||
} of perpPositions.filter((p) => !!p) as PerpPosition[]) {
|
||||
if (notionalSize < DUST_THRESHOLD) continue
|
||||
|
||||
if (perpAccount.basePosition.gt(ZERO_BN)) {
|
||||
longData.push({
|
||||
asset: marketConfig.name,
|
||||
amount: basePosition,
|
||||
symbol: marketConfig.baseSymbol,
|
||||
value: notionalSize,
|
||||
})
|
||||
} else {
|
||||
shortData.push({
|
||||
asset: marketConfig.name,
|
||||
amount: Math.abs(basePosition),
|
||||
symbol: marketConfig.baseSymbol,
|
||||
value: notionalSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
const longExposure = longData.reduce((a, c) => a + c.value, 0)
|
||||
const shortExposure = shortData.reduce((a, c) => a + c.value, 0)
|
||||
if (shortExposure === 0) {
|
||||
shortData.push({ symbol: 'spacer', value: 1 })
|
||||
}
|
||||
if (longExposure === 0) {
|
||||
longData.push({ symbol: 'spacer', value: 1 })
|
||||
}
|
||||
const dif = longExposure - shortExposure
|
||||
if (dif > 0) {
|
||||
shortData.push({ symbol: 'spacer', value: dif })
|
||||
}
|
||||
|
||||
return { longData, shortData, longExposure, shortExposure }
|
||||
}, [spotBalances, perpPositions])
|
||||
|
||||
useEffect(() => {
|
||||
if (hourlyPerformanceStats.length > 0) {
|
||||
if (performanceRange === '3m') {
|
||||
setChartData(hourlyPerformanceStats.slice().reverse())
|
||||
}
|
||||
if (performanceRange === '30d') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
if (performanceRange === '7d') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
if (performanceRange === '24h') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
} else {
|
||||
setChartData([])
|
||||
}
|
||||
}, [hourlyPerformanceStats, performanceRange])
|
||||
|
||||
useEffect(() => {
|
||||
if (chartData.length > 0) {
|
||||
for (const stat of chartData) {
|
||||
stat.spot_pnl = stat.pnl - stat.perp_pnl
|
||||
}
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
const handleMouseMove = (coords) => {
|
||||
if (coords.activePayload) {
|
||||
setMouseData(coords.activePayload[0].payload)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseData(null)
|
||||
}
|
||||
|
||||
const renderPnlChartTitle = () => {
|
||||
if (showPerpPnl && showSpotPnl) {
|
||||
return t('pnl')
|
||||
}
|
||||
if (!showSpotPnl) {
|
||||
return `${t('perp')} ${t('pnl')}`
|
||||
}
|
||||
if (!showPerpPnl) {
|
||||
return `${t('spot')} ${t('pnl')}`
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateAxis = (date) => {
|
||||
if (['3m', '30d'].includes(performanceRange)) {
|
||||
return dayjs(date).format('D MMM')
|
||||
} else if (performanceRange === '7d') {
|
||||
return dayjs(date).format('ddd, h:mma')
|
||||
} else {
|
||||
return dayjs(date).format('h:mma')
|
||||
}
|
||||
}
|
||||
|
||||
const pnlChartDataKey = () => {
|
||||
if (!showPerpPnl && showSpotPnl) {
|
||||
return 'spot_pnl'
|
||||
} else if (!showSpotPnl && showPerpPnl) {
|
||||
return 'perp_pnl'
|
||||
} else {
|
||||
return 'pnl'
|
||||
}
|
||||
}
|
||||
|
||||
const pnlChartColor =
|
||||
chartToShow === 'PnL' &&
|
||||
chartData.length > 0 &&
|
||||
chartData[chartData.length - 1][pnlChartDataKey()] > 0
|
||||
? theme === 'Mango'
|
||||
? '#AFD803'
|
||||
: '#5EBF4D'
|
||||
: theme === 'Mango'
|
||||
? '#F84638'
|
||||
: '#CC2929'
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 lg:gap-6">
|
||||
<div className="order-2 col-span-12 lg:order-1 lg:col-span-4">
|
||||
<div className="px-3 pb-4 xl:pb-6">
|
||||
<div className="flex items-center pb-1.5">
|
||||
<div className="text-sm text-th-fgd-3">
|
||||
{chartToShow === 'Value'
|
||||
? t('account-value')
|
||||
: renderPnlChartTitle()}{' '}
|
||||
</div>
|
||||
</div>
|
||||
{mouseData ? (
|
||||
<>
|
||||
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
|
||||
{formatUsdValue(
|
||||
mouseData[
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : chartData.length === 0 ? (
|
||||
<>
|
||||
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
|
||||
{chartToShow === 'PnL' ? '--' : formatUsdValue(accountValue)}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{dayjs().format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : chartData.length > 0 ? (
|
||||
<>
|
||||
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
|
||||
{chartToShow === 'PnL'
|
||||
? formatUsdValue(
|
||||
chartData[chartData.length - 1][pnlChartDataKey()]
|
||||
)
|
||||
: formatUsdValue(accountValue)}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{chartToShow === 'PnL'
|
||||
? dayjs(chartData[chartData.length - 1]['time']).format(
|
||||
'ddd MMM D YYYY, h:mma'
|
||||
)
|
||||
: dayjs().format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-th-bkg-3 border-y border-th-bkg-3 md:flex-row md:divide-y-0 md:p-3 lg:flex-col lg:divide-y lg:p-0 xl:flex-row xl:divide-y-0 xl:p-5">
|
||||
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
|
||||
<HealthHeart size={40} health={Number(maintHealthRatio)} />
|
||||
<div>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
{t('tooltip-account-liquidated')}{' '}
|
||||
<a
|
||||
href="https://docs.mango.markets/mango/health-overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('learn-more')}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-1.5 pb-0.5">
|
||||
<div className="text-th-fgd-3">{t('health')}</div>
|
||||
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className={`text-lg font-bold text-th-fgd-1`}>
|
||||
{maintHealthRatio < 100 ? maintHealthRatio.toFixed(2) : '>100'}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 md:pl-4 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
|
||||
<ScaleIcon className="h-10 w-10 text-th-fgd-4" />
|
||||
<div>
|
||||
<div className="pb-0.5 text-th-fgd-3">{t('leverage')}</div>
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className={`text-lg font-bold text-th-fgd-1`}>
|
||||
{mangoAccount?.getLeverage(mangoGroup, mangoCache).toFixed(2)}
|
||||
x
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-th-bkg-3 border-b border-th-bkg-3 md:flex-row md:divide-y-0 md:p-3 lg:flex-col lg:divide-y lg:p-0 xl:flex-row xl:divide-y-0 xl:p-5">
|
||||
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
|
||||
<LongShortChart chartData={longData} />
|
||||
<div>
|
||||
<Tooltip content={t('total-long-tooltip')}>
|
||||
<div className="flex items-center space-x-1.5 pb-0.5">
|
||||
<div className="text-th-fgd-3">{t('long-exposure')}</div>
|
||||
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className="text-lg font-bold text-th-fgd-1">
|
||||
{formatUsdValue(+longExposure)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 md:pl-4 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
|
||||
<LongShortChart chartData={shortData} />
|
||||
<div>
|
||||
<Tooltip content={t('total-short-tooltip')}>
|
||||
<div className="flex items-center space-x-1.5 pb-0.5">
|
||||
<div className="whitespace-nowrap text-th-fgd-3">
|
||||
{t('short-exposure')}
|
||||
</div>
|
||||
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{mangoGroup && mangoCache ? (
|
||||
<div className="text-lg font-bold text-th-fgd-1">
|
||||
{formatUsdValue(+shortExposure)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="order-1 col-span-12 px-4 pb-6 lg:order-2 lg:col-span-8 lg:pb-0 xl:px-6">
|
||||
<div className="mb-4 flex justify-between space-x-2 sm:mb-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-3">
|
||||
<div className="mb-3 w-28 sm:mb-0">
|
||||
<ButtonGroup
|
||||
activeValue={chartToShow}
|
||||
className="h-8"
|
||||
onChange={(c) => setChartToShow(c)}
|
||||
values={['Value', 'PnL']}
|
||||
/>
|
||||
</div>
|
||||
{chartToShow === 'PnL' && chartData.length ? (
|
||||
<div className="flex space-x-3">
|
||||
<Checkbox
|
||||
checked={showSpotPnl}
|
||||
disabled={!showPerpPnl}
|
||||
onChange={(e) => setShowSpotPnl(e.target.checked)}
|
||||
>
|
||||
{t('include-spot')}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={showPerpPnl}
|
||||
disabled={!showSpotPnl}
|
||||
onChange={(e) => setShowPerpPnl(e.target.checked)}
|
||||
>
|
||||
{t('include-perp')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="w-40">
|
||||
<ButtonGroup
|
||||
activeValue={performanceRange}
|
||||
className="h-8"
|
||||
onChange={(p) => setPerformanceRange(p)}
|
||||
values={performanceRangePresetLabels}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{chartData.length > 0 ? (
|
||||
<div className="h-48 md:h-64 lg:h-[340px] xl:h-[225px]" ref={observe}>
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height + 12}
|
||||
data={chartData?.length ? chartData : defaultData}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={{
|
||||
strokeOpacity: 0,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="defaultGradientArea"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
|
||||
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="greenGradientArea"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={0.9}
|
||||
/>
|
||||
<stop
|
||||
offset="80%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="redGradientArea"
|
||||
x1="0"
|
||||
y1="1"
|
||||
x2="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={0.9}
|
||||
/>
|
||||
<stop
|
||||
offset="80%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
isAnimationActive={true}
|
||||
type="monotone"
|
||||
dataKey={
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
}
|
||||
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
|
||||
fill={
|
||||
chartToShow === 'PnL'
|
||||
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
|
||||
? 'url(#greenGradientArea)'
|
||||
: 'url(#redGradientArea)'
|
||||
: 'url(#defaultGradientArea)'
|
||||
}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
}
|
||||
type="number"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
axisLine={false}
|
||||
dx={-10}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => numberCompacter.format(v)}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
</AreaChart>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-48 w-full items-center justify-center rounded-md bg-th-bkg-3 md:h-64 lg:h-[410px] xl:h-[270px]">
|
||||
<p className="mb-0">{t('no-chart')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountOverviewStats
|
|
@ -12,7 +12,7 @@ import Chart from '../Chart'
|
|||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
import { exportDataToCSV } from '../../utils/export'
|
||||
import { SaveIcon } from '@heroicons/react/outline'
|
||||
import { SaveIcon } from '@heroicons/react/solid'
|
||||
import Button from '../Button'
|
||||
|
||||
export const handleDustTicks = (v) =>
|
||||
|
@ -125,7 +125,7 @@ const AccountPerformance = () => {
|
|||
{chartData.length > 0 ? (
|
||||
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
|
||||
<div
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
|
@ -141,7 +141,7 @@ const AccountPerformance = () => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
|
||||
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
|
||||
style={{ height: '330px' }}
|
||||
>
|
||||
<Chart
|
||||
|
|
|
@ -3,8 +3,6 @@ import dayjs from 'dayjs'
|
|||
import isEmpty from 'lodash/isEmpty'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts'
|
||||
import { SaveIcon } from '@heroicons/react/outline'
|
||||
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { numberCompactFormatter } from '../../utils/'
|
||||
import { exportDataToCSV } from '../../utils/export'
|
||||
|
@ -14,8 +12,9 @@ import Select from 'components/Select'
|
|||
import Checkbox from 'components/Checkbox'
|
||||
import ButtonGroup from 'components/ButtonGroup'
|
||||
import * as MonoIcons from '../icons'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
|
||||
import { SaveIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { CHART_COLORS } from './LongShortChart'
|
||||
|
||||
const utc = require('dayjs/plugin/utc')
|
||||
dayjs.extend(utc)
|
||||
|
@ -28,25 +27,6 @@ export const handleDustTicks = (v) => {
|
|||
: numberCompactFormatter.format(v)
|
||||
}
|
||||
|
||||
// Each line added to the graph will use one of these colors
|
||||
const COLORS = {
|
||||
All: '#ff7c43',
|
||||
USDC: '#ffa600',
|
||||
SRM: '#8dd3c7',
|
||||
SOL: '#A288E3',
|
||||
RAY: '#4AB839',
|
||||
MSOL: '#fb8072',
|
||||
MNGO: '#80b1d3',
|
||||
LUNA: '#fdb462',
|
||||
AVAX: '#b3de69',
|
||||
BNB: '#FF47A6',
|
||||
FTT: '#A38560',
|
||||
BTC: '#bc80bd',
|
||||
ETH: '#05C793',
|
||||
ADA: '#3F8EFC',
|
||||
GMT: '#CBA74A',
|
||||
}
|
||||
|
||||
const HEADERS = [
|
||||
'time',
|
||||
'symbol',
|
||||
|
@ -185,6 +165,10 @@ const AccountPerformance = () => {
|
|||
// Normalise chart to start from 0 (except for account value)
|
||||
if (parseInt(performanceRange) !== 90 && chartToShow !== 'account-value') {
|
||||
const startValues = Object.assign({}, stats[0])
|
||||
// Initialize symbol not present at the start to 0
|
||||
uniqueSymbols
|
||||
.filter((e) => !(e in startValues))
|
||||
.map((f) => (startValues[f] = 0))
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
for (const key in stats[i]) {
|
||||
if (key !== 'time') {
|
||||
|
@ -349,7 +333,7 @@ const AccountPerformance = () => {
|
|||
{mangoAccount ? (
|
||||
<>
|
||||
<div
|
||||
className="h-[540px] w-full rounded-lg rounded-b-none border border-th-bkg-4 p-6 pb-24 sm:pb-16"
|
||||
className="h-[540px] w-full rounded-lg rounded-b-none border border-th-bkg-3 p-6 pb-24 sm:pb-16"
|
||||
ref={observe}
|
||||
>
|
||||
<div className="flex flex-col pb-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
@ -411,7 +395,7 @@ const AccountPerformance = () => {
|
|||
key={`${v}${i}`}
|
||||
type="monotone"
|
||||
dataKey={`${v}`}
|
||||
stroke={`${COLORS[v]}`}
|
||||
stroke={`${CHART_COLORS(theme)[v]}`}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
|
@ -430,7 +414,7 @@ const AccountPerformance = () => {
|
|||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="-mt-[1px] rounded-b-lg border border-th-bkg-4 py-3 px-6">
|
||||
<div className="-mt-[1px] rounded-b-lg border border-th-bkg-3 py-3 px-6">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="mb-0 font-bold">{t('assets')}</p>
|
||||
<Checkbox
|
||||
|
@ -450,18 +434,21 @@ const AccountPerformance = () => {
|
|||
className={`default-transition m-1 flex items-center rounded-full border py-1 px-2 text-xs font-bold ${
|
||||
selectedSymbols.includes(s)
|
||||
? ''
|
||||
: 'border-th-fgd-4 text-th-fgd-4 hover:border-th-fgd-3 hover:text-th-fgd-3 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none'
|
||||
: 'border-th-fgd-4 text-th-fgd-4 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none md:hover:border-th-fgd-3 md:hover:text-th-fgd-3'
|
||||
}`}
|
||||
onClick={() => toggleOption(s)}
|
||||
style={
|
||||
selectedSymbols.includes(s)
|
||||
? { borderColor: COLORS[s], color: COLORS[s] }
|
||||
? {
|
||||
borderColor: CHART_COLORS(theme)[s],
|
||||
color: CHART_COLORS(theme)[s],
|
||||
}
|
||||
: {}
|
||||
}
|
||||
key={s}
|
||||
>
|
||||
{renderSymbolIcon(s)}
|
||||
{s}
|
||||
{s == 'All' ? t(`account-performance:all`) : s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import { PieChart, Pie, Cell, Tooltip } from 'recharts'
|
||||
import { formatUsdValue, tokenPrecision } from 'utils'
|
||||
import * as MonoIcons from '../icons'
|
||||
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
export const CHART_COLORS = (theme) => {
|
||||
return {
|
||||
All: '#ff7c43',
|
||||
spacer: theme === 'Light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)',
|
||||
ADA: '#335CBE',
|
||||
AVAX: '#E84142',
|
||||
BNB: '#F3BA2F',
|
||||
BTC: '#F7931A',
|
||||
COPE: '#EEEEEE',
|
||||
ETH: '#627EEA',
|
||||
FTT: '#02A6C2',
|
||||
GMT: '#CBA74A',
|
||||
LUNA: '#FFD83D',
|
||||
MNGO: '#FBB31F',
|
||||
MSOL: '#8562CF',
|
||||
RAY: '#4CA2DA',
|
||||
SOL: '#916CE0',
|
||||
SRM: '#58D4E3',
|
||||
USDC: '#2775CA',
|
||||
USDT: '#50AF95',
|
||||
}
|
||||
}
|
||||
|
||||
const LongShortChart = ({ chartData }: { chartData: any[] }) => {
|
||||
const { theme } = useTheme()
|
||||
const CustomToolTip = () => {
|
||||
const renderIcon = (symbol) => {
|
||||
const iconName = `${symbol.slice(0, 1)}${symbol
|
||||
.slice(1, 4)
|
||||
.toLowerCase()}MonoIcon`
|
||||
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
|
||||
return (
|
||||
<div style={{ color: CHART_COLORS(theme)[symbol] }}>
|
||||
<SymbolIcon className={`mr-1.5 h-3.5 w-auto`} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const showTooltip = chartData.find((d) => d.symbol !== 'spacer')
|
||||
|
||||
return chartData.length && showTooltip ? (
|
||||
<div className="space-y-1.5 rounded-md bg-th-bkg-2 p-3 pb-2">
|
||||
{chartData
|
||||
.filter((d) => d.symbol !== 'spacer')
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.map((entry, index) => {
|
||||
const { amount, asset, symbol, value } = entry
|
||||
return (
|
||||
<div
|
||||
className="flex w-48 items-center justify-between border-b border-th-bkg-4 pb-1 text-xs last:border-b-0 last:pb-0"
|
||||
key={`item-${index}-${symbol}`}
|
||||
>
|
||||
<div className="mb-0.5 flex items-center">
|
||||
{renderIcon(symbol)}
|
||||
<p
|
||||
className="mb-0 text-xs leading-none"
|
||||
style={{ color: CHART_COLORS(theme)[symbol] }}
|
||||
>
|
||||
{asset}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p
|
||||
className="mb-0 text-xs leading-none"
|
||||
style={{ color: CHART_COLORS(theme)[symbol] }}
|
||||
>
|
||||
{amount.toLocaleString(undefined, {
|
||||
maximumFractionDigits: tokenPrecision[symbol],
|
||||
})}
|
||||
</p>
|
||||
<p className="mb-0 text-xxs text-th-fgd-4">
|
||||
{formatUsdValue(value)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
return chartData.length ? (
|
||||
<PieChart width={40} height={40}>
|
||||
<Pie
|
||||
cursor="pointer"
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={20}
|
||||
innerRadius={10}
|
||||
minAngle={2}
|
||||
startAngle={90}
|
||||
endAngle={450}
|
||||
>
|
||||
{chartData
|
||||
.sort((a, b) => a.symbol.localeCompare(b.symbol))
|
||||
.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={CHART_COLORS(theme)[entry.symbol]}
|
||||
stroke="rgba(0,0,0,0.1)"
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
content={<CustomToolTip />}
|
||||
position={{ x: 48, y: 0 }}
|
||||
wrapperStyle={{ zIndex: 10 }}
|
||||
/>
|
||||
</PieChart>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default LongShortChart
|
|
@ -3,7 +3,7 @@ import Button from 'components/Button'
|
|||
import Input from 'components/Input'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useState } from 'react'
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/outline'
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const MangoAccountLookup = () => {
|
||||
|
@ -34,14 +34,13 @@ export const MangoAccountLookup = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center rounded-lg px-4 text-th-fgd-1">
|
||||
<div className="flex w-full flex-col items-center text-th-fgd-1">
|
||||
<h2 className="mb-1 text-base">{t('mango-account-lookup-title')}</h2>
|
||||
<p className="mb-2 text-center">{t('mango-account-lookup-desc')}</p>
|
||||
<div className="w-[350px] p-1 md:w-[400px]">
|
||||
<div className="w-full max-w-[360px] pt-2">
|
||||
<Input
|
||||
type="text"
|
||||
error={isInvalid}
|
||||
placeholder="Address"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
|
@ -52,7 +51,7 @@ export const MangoAccountLookup = () => {
|
|||
{t('invalid-address')}
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-3 pb-2">
|
||||
<div className="pt-4">
|
||||
<Button onClick={onClickSearch}>{t('view')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,382 +0,0 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip as ChartTooltip,
|
||||
} from 'recharts'
|
||||
import { InformationCircleIcon } from '@heroicons/react/outline'
|
||||
import useDimensions from 'react-cool-dimensions'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import ButtonGroup from '../ButtonGroup'
|
||||
import { formatUsdValue } from '../../utils'
|
||||
import { numberCompacter } from '../SwapTokenInfo'
|
||||
import Checkbox from '../Checkbox'
|
||||
import Tooltip from '../Tooltip'
|
||||
|
||||
type PerformanceChart = {
|
||||
hourlyPerformanceStats: any[]
|
||||
performanceRange: '24hr' | '7d' | '30d' | '3m'
|
||||
}
|
||||
|
||||
const defaultData = [
|
||||
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
|
||||
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
|
||||
]
|
||||
|
||||
const PerformanceChart = ({
|
||||
hourlyPerformanceStats,
|
||||
performanceRange,
|
||||
accountValue,
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('common')
|
||||
const { observe, width, height } = useDimensions()
|
||||
|
||||
const [chartData, setChartData] = useState<any[]>([])
|
||||
const [mouseData, setMouseData] = useState<string | null>(null)
|
||||
const [chartToShow, setChartToShow] = useState('Value')
|
||||
const [showSpotPnl, setShowSpotPnl] = useState(true)
|
||||
const [showPerpPnl, setShowPerpPnl] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (hourlyPerformanceStats.length > 0) {
|
||||
if (performanceRange === '3m') {
|
||||
setChartData(hourlyPerformanceStats.slice().reverse())
|
||||
}
|
||||
if (performanceRange === '30d') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
if (performanceRange === '7d') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
if (performanceRange === '24h') {
|
||||
const start = new Date(
|
||||
// @ts-ignore
|
||||
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
|
||||
).getTime()
|
||||
const chartData = cloneDeep(hourlyPerformanceStats).filter(
|
||||
(d) => new Date(d.time).getTime() > start
|
||||
)
|
||||
const pnlStart = chartData[chartData.length - 1].pnl
|
||||
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
if (i === chartData.length - 1) {
|
||||
chartData[i].pnl = 0
|
||||
chartData[i].perp_pnl = 0
|
||||
} else {
|
||||
chartData[i].pnl = chartData[i].pnl - pnlStart
|
||||
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
|
||||
}
|
||||
}
|
||||
setChartData(chartData.reverse())
|
||||
}
|
||||
} else {
|
||||
setChartData([])
|
||||
}
|
||||
}, [hourlyPerformanceStats, performanceRange])
|
||||
|
||||
useEffect(() => {
|
||||
if (chartData.length > 0) {
|
||||
for (const stat of chartData) {
|
||||
stat.spot_pnl = stat.pnl - stat.perp_pnl
|
||||
}
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
const handleMouseMove = (coords) => {
|
||||
if (coords.activePayload) {
|
||||
setMouseData(coords.activePayload[0].payload)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseData(null)
|
||||
}
|
||||
|
||||
const renderPnlChartTitle = () => {
|
||||
if (showPerpPnl && showSpotPnl) {
|
||||
return t('pnl')
|
||||
}
|
||||
if (!showSpotPnl) {
|
||||
return `${t('perp')} ${t('pnl')}`
|
||||
}
|
||||
if (!showPerpPnl) {
|
||||
return `${t('spot')} ${t('pnl')}`
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateAxis = (date) => {
|
||||
if (['3m', '30d'].includes(performanceRange)) {
|
||||
return dayjs(date).format('D MMM')
|
||||
} else if (performanceRange === '7d') {
|
||||
return dayjs(date).format('ddd, h:mma')
|
||||
} else {
|
||||
return dayjs(date).format('h:mma')
|
||||
}
|
||||
}
|
||||
|
||||
const pnlChartDataKey = () => {
|
||||
if (!showPerpPnl && showSpotPnl) {
|
||||
return 'spot_pnl'
|
||||
} else if (!showSpotPnl && showPerpPnl) {
|
||||
return 'perp_pnl'
|
||||
} else {
|
||||
return 'pnl'
|
||||
}
|
||||
}
|
||||
|
||||
const pnlChartColor =
|
||||
chartToShow === 'PnL' &&
|
||||
chartData.length > 0 &&
|
||||
chartData[chartData.length - 1][pnlChartDataKey()] > 0
|
||||
? theme === 'Mango'
|
||||
? '#AFD803'
|
||||
: '#5EBF4D'
|
||||
: theme === 'Mango'
|
||||
? '#F84638'
|
||||
: '#CC2929'
|
||||
|
||||
return (
|
||||
<div className="mt-4 h-64 w-full" ref={observe}>
|
||||
<div className="flex justify-between pb-9">
|
||||
<div>
|
||||
<div className="flex items-center pb-0.5">
|
||||
<div className="text-sm text-th-fgd-3">
|
||||
{chartToShow === 'Value'
|
||||
? t('account-value')
|
||||
: renderPnlChartTitle()}{' '}
|
||||
<span className="text-th-fgd-4">
|
||||
{`(${t('timeframe-desc', {
|
||||
timeframe: performanceRange,
|
||||
})})`}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip content={t('delayed-info')}>
|
||||
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{mouseData ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{formatUsdValue(
|
||||
mouseData[
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : chartData.length === 0 ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">--</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{dayjs().format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : chartData.length > 0 ? (
|
||||
<>
|
||||
<div className="pb-1 text-xl font-bold text-th-fgd-1">
|
||||
{chartToShow === 'PnL'
|
||||
? formatUsdValue(
|
||||
chartData[chartData.length - 1][pnlChartDataKey()]
|
||||
)
|
||||
: formatUsdValue(accountValue)}
|
||||
</div>
|
||||
<div className="text-xs font-normal text-th-fgd-4">
|
||||
{chartToShow === 'PnL'
|
||||
? dayjs(chartData[chartData.length - 1]['time']).format(
|
||||
'ddd MMM D YYYY, h:mma'
|
||||
)
|
||||
: dayjs().format('ddd MMM D YYYY, h:mma')}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="w-36">
|
||||
<ButtonGroup
|
||||
activeValue={chartToShow}
|
||||
className="pb-2 pt-2 text-sm"
|
||||
onChange={(v) => setChartToShow(v)}
|
||||
values={['Value', 'PnL']}
|
||||
names={[t('value'), t('pnl')]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{chartToShow === 'PnL' ? (
|
||||
<div className="flex space-x-3 pt-4">
|
||||
<Checkbox
|
||||
checked={showSpotPnl}
|
||||
disabled={!showPerpPnl}
|
||||
onChange={(e) => setShowSpotPnl(e.target.checked)}
|
||||
>
|
||||
{t('include-spot')}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={showPerpPnl}
|
||||
disabled={!showSpotPnl}
|
||||
onChange={(e) => setShowPerpPnl(e.target.checked)}
|
||||
>
|
||||
{t('include-perp')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{chartData.length > 0 ? (
|
||||
<AreaChart
|
||||
width={width}
|
||||
height={height}
|
||||
data={chartData?.length ? chartData : defaultData}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={{
|
||||
strokeOpacity: 0,
|
||||
}}
|
||||
content={<></>}
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="defaultGradientArea"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
|
||||
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="greenGradientArea" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={0.9}
|
||||
/>
|
||||
<stop
|
||||
offset="80%"
|
||||
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="redGradientArea" x1="0" y1="1" x2="0" y2="0">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={0.9}
|
||||
/>
|
||||
<stop
|
||||
offset="80%"
|
||||
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
isAnimationActive={true}
|
||||
type="monotone"
|
||||
dataKey={
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
}
|
||||
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
|
||||
fill={
|
||||
chartToShow === 'PnL'
|
||||
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
|
||||
? 'url(#greenGradientArea)'
|
||||
: 'url(#redGradientArea)'
|
||||
: 'url(#defaultGradientArea)'
|
||||
}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={
|
||||
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
|
||||
}
|
||||
type="number"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
axisLine={false}
|
||||
dx={-10}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => numberCompacter.format(v)}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
axisLine={false}
|
||||
dy={10}
|
||||
minTickGap={20}
|
||||
tick={{
|
||||
fill:
|
||||
theme === 'Light'
|
||||
? 'rgba(0,0,0,0.4)'
|
||||
: 'rgba(255,255,255,0.35)',
|
||||
fontSize: 10,
|
||||
}}
|
||||
tickLine={false}
|
||||
tickFormatter={(v) => formatDateAxis(v)}
|
||||
/>
|
||||
</AreaChart>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PerformanceChart
|
File diff suppressed because one or more lines are too long
|
@ -39,11 +39,9 @@ export * from './MarketDetails'
|
|||
export * from './MarketFee'
|
||||
export * from './MarketMenuItem'
|
||||
export * from './MarketNavItem'
|
||||
export * from './MarketSelect'
|
||||
export * from './MarketsModal'
|
||||
export * from './MenuItem'
|
||||
export * from './Modal'
|
||||
export * from './NavDropMenu'
|
||||
export * from './NewAccount'
|
||||
export * from './Notification'
|
||||
export * from './OpenOrdersTable'
|
||||
|
@ -70,7 +68,6 @@ export * from './SwitchMarketDropdown'
|
|||
export * from './TableElements'
|
||||
export * from './Tabs'
|
||||
export * from './Tooltip'
|
||||
export * from './TopBar'
|
||||
export * from './TradeHistoryTable'
|
||||
export * from './TradeNavMenu'
|
||||
export * from './TradePageGrid'
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
MenuIcon,
|
||||
XIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
import { BtcMonoIcon, TradeIcon } from '../icons'
|
||||
import { BtcMonoIcon, TradeIcon, TrophyIcon } from '../icons'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { IconButton } from '../Button'
|
||||
import {
|
||||
|
@ -18,7 +18,7 @@ import {
|
|||
LightBulbIcon,
|
||||
SwitchHorizontalIcon,
|
||||
UserAddIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/solid'
|
||||
|
||||
const StyledBarItemLabel = ({ children, ...props }) => (
|
||||
<div style={{ fontSize: '0.6rem', lineHeight: 1 }} {...props}>
|
||||
|
@ -39,14 +39,14 @@ const BottomBar = () => {
|
|||
pathname: '/markets',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<a
|
||||
className={`${
|
||||
asPath === '/markets' ? 'text-th-primary' : 'text-th-fgd-3'
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
|
||||
>
|
||||
<BtcMonoIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('markets')}</StyledBarItemLabel>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
||||
|
@ -55,46 +55,46 @@ const BottomBar = () => {
|
|||
}}
|
||||
shallow={true}
|
||||
>
|
||||
<div
|
||||
<a
|
||||
className={`${
|
||||
asPath === '/' || asPath.startsWith('/?name')
|
||||
? 'text-th-primary'
|
||||
: 'text-th-fgd-3'
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
|
||||
>
|
||||
<TradeIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('trade')}</StyledBarItemLabel>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/account" shallow={true}>
|
||||
<div
|
||||
<a
|
||||
className={`${
|
||||
asPath === '/account' ? 'text-th-primary' : 'text-th-fgd-3'
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
|
||||
>
|
||||
<CurrencyDollarIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('account')}</StyledBarItemLabel>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/stats" shallow={true}>
|
||||
<div
|
||||
<a
|
||||
className={`${
|
||||
asPath === '/stats' ? 'text-th-primary' : 'text-th-fgd-3'
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
|
||||
>
|
||||
<ChartBarIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('stats')}</StyledBarItemLabel>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
<div
|
||||
<a
|
||||
className={`${
|
||||
showPanel ? 'text-th-primary' : 'text-th-fgd-3'
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
|
||||
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
|
||||
onClick={() => setShowPanel(!showPanel)}
|
||||
>
|
||||
<MenuIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('more')}</StyledBarItemLabel>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<MoreMenuPanel showPanel={showPanel} setShowPanel={setShowPanel} />
|
||||
</>
|
||||
|
@ -141,6 +141,11 @@ const MoreMenuPanel = ({
|
|||
path="/swap"
|
||||
icon={<SwitchHorizontalIcon className="h-5 w-5" />}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
title={t('leaderboard')}
|
||||
path="/leaderboard"
|
||||
icon={<TrophyIcon className="h-5 w-5" />}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
title={t('referrals')}
|
||||
path="/referral"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { XIcon } from '@heroicons/react/outline'
|
||||
import { XIcon } from '@heroicons/react/solid'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import { getWeights, PerpMarket } from '@blockworks-foundation/mango-client'
|
||||
import { CandlesIcon } from '../icons'
|
||||
|
@ -84,6 +84,7 @@ const MobileTradePage = () => {
|
|||
onChange={handleChangeViewIndex}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
width="w-40 sm:w-full"
|
||||
/>
|
||||
<Swipeable index={viewIndex} onChangeIndex={handleChangeViewIndex}>
|
||||
<div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
|
||||
import useDrag from 'hooks/useDrag'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
|
@ -44,7 +44,7 @@ function SwipeableTabs({
|
|||
}
|
||||
|
||||
return (
|
||||
<div onMouseLeave={dragStop} className="thin-scroll relative mb-4">
|
||||
<div onMouseLeave={dragStop} className="thin-scroll relative mb-6">
|
||||
<ScrollMenu
|
||||
LeftArrow={LeftArrow}
|
||||
RightArrow={RightArrow}
|
||||
|
@ -148,10 +148,10 @@ function Tab({
|
|||
} items-center justify-center font-bold focus:text-th-primary focus:outline-none ${
|
||||
selected
|
||||
? 'border-b-2 border-th-primary text-th-primary'
|
||||
: 'border-b border-th-fgd-4 text-th-fgd-3'
|
||||
: 'border-b border-th-bkg-4 text-th-fgd-3'
|
||||
}`}
|
||||
>
|
||||
{t(title)}
|
||||
{t(title.toLowerCase().replace(/\s/g, '-'))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,46 +1,33 @@
|
|||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import Chart from '../Chart'
|
||||
import Select from '../Select'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import TabButtons from 'components/TabButtons'
|
||||
|
||||
export default function StatsAssets({ latestStats, stats }) {
|
||||
export default function StatsAssets({
|
||||
latestStats,
|
||||
stats,
|
||||
loadHistoricalStats,
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const [selectedAsset, setSelectedAsset] = useState<string>('BTC')
|
||||
|
||||
const selectedStatsData = stats.filter((stat) => stat.name === selectedAsset)
|
||||
const selectedStatsData = useMemo(() => {
|
||||
if (stats.length) {
|
||||
return stats.filter((stat) => stat.name === selectedAsset)
|
||||
}
|
||||
return []
|
||||
}, [stats, selectedAsset])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex flex-row-reverse items-center justify-between md:flex-col md:items-stretch">
|
||||
<Select
|
||||
value={selectedAsset}
|
||||
onChange={(a) => setSelectedAsset(a)}
|
||||
className="w-24 md:hidden"
|
||||
>
|
||||
{latestStats.map((stat) => (
|
||||
<Select.Option key={stat.name} value={stat.name}>
|
||||
{stat.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="mb-4 hidden rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:flex md:px-4">
|
||||
{latestStats.map((stat, index) => (
|
||||
<div
|
||||
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
|
||||
index > 0 ? 'ml-4 md:ml-2' : null
|
||||
} default-transition cursor-pointer rounded-md
|
||||
${
|
||||
selectedAsset === stat.name
|
||||
? `text-th-primary`
|
||||
: `text-th-fgd-3 hover:text-th-fgd-1`
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedAsset(stat.name)}
|
||||
key={stat.name as string}
|
||||
>
|
||||
{stat.name}
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-2">
|
||||
<TabButtons
|
||||
activeTab={selectedAsset}
|
||||
tabs={latestStats.map((s) => ({ label: s.name, key: s.name }))}
|
||||
onClick={setSelectedAsset}
|
||||
showSymbolIcon
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
|
@ -68,6 +55,7 @@ export default function StatsAssets({ latestStats, stats }) {
|
|||
x.toLocaleString(undefined, { maximumFractionDigits: 2 })
|
||||
}
|
||||
type="area"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -84,6 +72,7 @@ export default function StatsAssets({ latestStats, stats }) {
|
|||
(x * 100).toLocaleString(undefined, { maximumFractionDigits: 4 })
|
||||
}
|
||||
type="bar"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -99,6 +88,7 @@ export default function StatsAssets({ latestStats, stats }) {
|
|||
x.toLocaleString(undefined, { maximumFractionDigits: 2 })
|
||||
}
|
||||
type="area"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -115,6 +105,7 @@ export default function StatsAssets({ latestStats, stats }) {
|
|||
(x * 100).toLocaleString(undefined, { maximumFractionDigits: 4 })
|
||||
}
|
||||
type="bar"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,9 +5,9 @@ import Chart from '../Chart'
|
|||
import BN from 'bn.js'
|
||||
import { perpContractPrecision } from '../../utils'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Select from '../Select'
|
||||
import { marketsSelector } from '../../stores/selectors'
|
||||
import dayjs from 'dayjs'
|
||||
import TabButtons from 'components/TabButtons'
|
||||
|
||||
function calculateFundingRate(
|
||||
oldestLongFunding,
|
||||
|
@ -36,7 +36,7 @@ function calculateFundingRate(
|
|||
return (fundingInQuoteDecimals / basePriceInBaseLots) * 100
|
||||
}
|
||||
|
||||
export default function StatsPerps({ perpStats }) {
|
||||
export default function StatsPerps({ perpStats, loadPerpStats }) {
|
||||
const { t } = useTranslation('common')
|
||||
const [selectedAsset, setSelectedAsset] = useState<string>('BTC-PERP')
|
||||
const marketConfigs = useMangoStore(
|
||||
|
@ -61,7 +61,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
}, [selectedMarketConfig, perpMarkets])
|
||||
|
||||
const perpsData = useMemo(() => {
|
||||
if (perpStats.length === 0 || !selectedMarket) return []
|
||||
if (!perpStats.length || !selectedMarket) return []
|
||||
|
||||
let selectedStatsData = perpStats.filter(
|
||||
(stat) => stat.name === selectedAsset
|
||||
|
@ -119,35 +119,16 @@ export default function StatsPerps({ perpStats }) {
|
|||
return (
|
||||
<>
|
||||
<div className="mb-4 flex flex-row-reverse items-center justify-between md:flex-col md:items-stretch">
|
||||
<Select
|
||||
value={selectedAsset}
|
||||
onChange={(a) => setSelectedAsset(a)}
|
||||
className="ml-4 w-36 flex-shrink-0 md:hidden"
|
||||
>
|
||||
{marketConfigs?.map((market) => (
|
||||
<Select.Option key={market.name} value={market.name}>
|
||||
{market.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="mb-4 hidden rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:flex md:px-4">
|
||||
{marketConfigs?.map((market, index) => (
|
||||
<div
|
||||
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
|
||||
index > 0 ? 'ml-4 md:ml-2' : null
|
||||
} default-transition cursor-pointer rounded-md
|
||||
${
|
||||
selectedAsset === market.name
|
||||
? `text-th-primary`
|
||||
: `text-th-fgd-3 hover:text-th-fgd-1`
|
||||
}
|
||||
`}
|
||||
onClick={() => setSelectedAsset(market.name)}
|
||||
key={market.name as string}
|
||||
>
|
||||
{market.baseSymbol}
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-2">
|
||||
<TabButtons
|
||||
activeTab={selectedAsset}
|
||||
tabs={marketConfigs?.map((m) => ({
|
||||
label: m.baseSymbol,
|
||||
key: m.name,
|
||||
}))}
|
||||
onClick={setSelectedAsset}
|
||||
showSymbolIcon
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center text-xl text-th-fgd-1">
|
||||
<img
|
||||
|
@ -179,6 +160,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
}
|
||||
type="area"
|
||||
yAxisWidth={70}
|
||||
loading={loadPerpStats}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -201,6 +183,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
selectedMarketConfig.baseSymbol
|
||||
}
|
||||
type="area"
|
||||
loading={loadPerpStats}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -208,7 +191,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
<div className="mb-4">
|
||||
<h2 className="mb-4">{t('liquidity-mining')}</h2>
|
||||
<div className="grid grid-cols-2 gap-x-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<div className="col-span-1 border-y border-th-bkg-4 py-3">
|
||||
<div className="col-span-1 border-y border-th-bkg-3 py-3">
|
||||
<p className="mb-0">{t('depth-rewarded')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{maxDepthUi.toLocaleString() + ' '}
|
||||
|
@ -219,7 +202,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 border-y border-th-bkg-4 py-3">
|
||||
<div className="col-span-1 border-y border-th-bkg-3 py-3">
|
||||
<p className="mb-0">{t('target-period-length')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{(
|
||||
|
@ -229,7 +212,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
{t('minutes')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 border-b border-th-bkg-4 py-3 md:border-y">
|
||||
<div className="col-span-1 border-b border-th-bkg-3 py-3 md:border-y">
|
||||
<p className="mb-0">{t('mngo-per-period')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{(
|
||||
|
@ -238,7 +221,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
|
||||
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
|
||||
<p className="mb-0">{t('mngo-left-period')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{(
|
||||
|
@ -248,7 +231,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
|
||||
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
|
||||
<p className="mb-0">{t('est-period-end')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{dayjs(est * 1000).format('DD MMM YYYY')}
|
||||
|
@ -257,7 +240,7 @@ export default function StatsPerps({ perpStats }) {
|
|||
{dayjs(est * 1000).format('h:mma')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
|
||||
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
|
||||
<p className="mb-0">{t('period-progress')}</p>
|
||||
<div className="text-lg font-bold">
|
||||
{(progress * 100).toFixed(2)}%
|
||||
|
|
|
@ -32,30 +32,38 @@ const getAverageStats = (
|
|||
symbol: string,
|
||||
type: string
|
||||
): string => {
|
||||
if (stats?.length) {
|
||||
if (stats.length > 0) {
|
||||
const priorDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
|
||||
const selectedStatsData = stats.filter((s) => s.name === symbol)
|
||||
const timeFilteredStats = selectedStatsData.filter(
|
||||
(d) => new Date(d.time).getTime() >= priorDate.getTime()
|
||||
)
|
||||
|
||||
const oldestStat = timeFilteredStats[0]
|
||||
const latestStat = timeFilteredStats[timeFilteredStats.length - 1]
|
||||
const avg =
|
||||
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
|
||||
let avg
|
||||
if (latestStat && oldestStat && type in latestStat && type in oldestStat) {
|
||||
avg =
|
||||
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
|
||||
}
|
||||
|
||||
priorDate.setHours(priorDate.getHours() + 1)
|
||||
|
||||
if (new Date(oldestStat.hourly).getDate() > priorDate.getDate()) {
|
||||
if (new Date(oldestStat?.hourly).getDate() > priorDate.getDate()) {
|
||||
return '-'
|
||||
} else {
|
||||
}
|
||||
if (avg) {
|
||||
return `${avg.toFixed(4)}%`
|
||||
}
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
export default function StatsTotals({ latestStats, stats }) {
|
||||
export default function StatsTotals({
|
||||
latestStats,
|
||||
stats,
|
||||
loadHistoricalStats,
|
||||
loadLatestStats,
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
@ -64,7 +72,7 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
const [depositValues, borrowValues]: [Values[], Values[]] = useMemo(() => {
|
||||
const depositValues: Values[] = []
|
||||
const borrowValues: Values[] = []
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
for (let i = 0; i < stats?.length; i++) {
|
||||
const time = stats[i].hourly
|
||||
const name = stats[i].name
|
||||
const depositValue =
|
||||
|
@ -149,6 +157,7 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
|
||||
}
|
||||
type="area"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -165,6 +174,7 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
|
||||
}
|
||||
type="area"
|
||||
loading={loadHistoricalStats}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -242,17 +252,17 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
) : loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="pb-8">
|
||||
<h2 className="mb-4">{t('average-deposit')}</h2>
|
||||
{stats.length > 1 ? (
|
||||
{stats.length && latestStats.length ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
|
@ -290,70 +300,158 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
) : loadHistoricalStats || loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<h2 className="mb-4">{t('average-borrow')}</h2>
|
||||
{stats.length > 1 ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th>{t('asset')}</Th>
|
||||
<Th>24h</Th>
|
||||
<Th>7d</Th>
|
||||
<Th>30d</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{latestStats.map((stat) => (
|
||||
<TrBody key={stat.name}>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
</TrBody>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<h2 className="mb-4">{t('average-borrow')}</h2>
|
||||
{stats.length && latestStats.length ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th>{t('asset')}</Th>
|
||||
<Th>24h</Th>
|
||||
<Th>7d</Th>
|
||||
<Th>30d</Th>
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{latestStats.map((stat) => (
|
||||
<TrBody key={stat.name}>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
<Td>
|
||||
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
|
||||
</Td>
|
||||
</TrBody>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : loadHistoricalStats || loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-8 border-b border-th-bkg-4">
|
||||
<div className="mb-8 border-b border-th-bkg-3">
|
||||
<h2 className="mb-4">{t('current-stats')}</h2>
|
||||
{latestStats.map((stat) => (
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
<div className="grid w-full grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 sm:col-span-6">
|
||||
{latestStats.length ? (
|
||||
latestStats.map((stat) => (
|
||||
<ExpandableRow
|
||||
buttonTemplate={
|
||||
<div className="grid w-full grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 sm:col-span-6">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name
|
||||
.split(/-|\//)[0]
|
||||
.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('total-deposits')}
|
||||
</div>
|
||||
{formatNumberString(stat.totalDeposits, 0)}
|
||||
</div>
|
||||
<div className="col-span-6 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('total-borrows')}
|
||||
</div>
|
||||
{formatNumberString(stat.totalBorrows, 0)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={stat.name}
|
||||
panelTemplate={
|
||||
<div className="grid grid-flow-row grid-cols-2 gap-4">
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('deposit-rate')}
|
||||
</div>
|
||||
<span className="text-th-green">
|
||||
{formatNumberString(
|
||||
stat.depositInterest.toNumber(),
|
||||
2
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('borrow-rate')}
|
||||
</div>
|
||||
<span className="text-th-red">
|
||||
{formatNumberString(
|
||||
stat.borrowInterest.toNumber(),
|
||||
2
|
||||
)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('utilization')}
|
||||
</div>
|
||||
{formatNumberString(
|
||||
stat.utilization
|
||||
.mul(I80F48.fromNumber(100))
|
||||
.toNumber(),
|
||||
2
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{stats.length && latestStats.length ? (
|
||||
<div className="mb-8 border-b border-th-bkg-4">
|
||||
<h2 className="mb-4">{t('average-deposit')}</h2>
|
||||
{latestStats.map((stat) => (
|
||||
<Row key={stat.name}>
|
||||
<div className="grid grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
|
@ -367,129 +465,72 @@ export default function StatsTotals({ latestStats, stats }) {
|
|||
{stat.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('total-deposits')}
|
||||
</div>
|
||||
{formatNumberString(stat.totalDeposits, 0)}
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
|
||||
{getAverageStats(stats, 1, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
<div className="col-span-6 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('total-borrows')}
|
||||
</div>
|
||||
{formatNumberString(stat.totalBorrows, 0)}
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
|
||||
{getAverageStats(stats, 7, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
|
||||
{getAverageStats(stats, 30, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={stat.name}
|
||||
panelTemplate={
|
||||
<div className="grid grid-flow-row grid-cols-2 gap-4">
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('deposit-rate')}
|
||||
</Row>
|
||||
))}
|
||||
</div>
|
||||
) : loadHistoricalStats || loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
) : null}
|
||||
{stats.length && latestStats.length ? (
|
||||
<div className="mb-4 border-b border-th-bkg-4">
|
||||
<h2 className="mb-4">{t('average-borrow')}</h2>
|
||||
{latestStats.map((stat) => (
|
||||
<Row key={stat.name}>
|
||||
<div className="grid grid-cols-12 grid-rows-2 gap-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 flex items-center sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name
|
||||
.split(/-|\//)[0]
|
||||
.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
<span className="text-th-green">
|
||||
{formatNumberString(stat.depositInterest.toNumber(), 2)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('borrow-rate')}
|
||||
</div>
|
||||
<span className="text-th-red">
|
||||
{formatNumberString(stat.borrowInterest.toNumber(), 2)}%
|
||||
</span>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
|
||||
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">
|
||||
{t('utilization')}
|
||||
</div>
|
||||
{formatNumberString(
|
||||
stat.utilization.mul(I80F48.fromNumber(100)).toNumber(),
|
||||
2
|
||||
)}
|
||||
%
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
|
||||
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
|
||||
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mb-8 border-b border-th-bkg-4">
|
||||
<h2 className="mb-4">{t('average-deposit')}</h2>
|
||||
{stats.length > 1
|
||||
? latestStats.map((stat) => (
|
||||
<Row key={stat.name}>
|
||||
<div className="grid grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name
|
||||
.split(/-|\//)[0]
|
||||
.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
|
||||
{getAverageStats(stats, 1, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
|
||||
{getAverageStats(stats, 7, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
|
||||
{getAverageStats(stats, 30, stat.name, 'depositIndex')}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<div className="mb-4 border-b border-th-bkg-4">
|
||||
<h2 className="mb-4">{t('average-borrow')}</h2>
|
||||
{stats.length > 1
|
||||
? latestStats.map((stat) => (
|
||||
<Row key={stat.name}>
|
||||
<div className="grid grid-cols-12 grid-rows-2 gap-2 text-left sm:grid-rows-1 sm:text-right">
|
||||
<div className="text-fgd-1 col-span-12 flex items-center sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
alt=""
|
||||
width="20"
|
||||
height="20"
|
||||
src={`/assets/icons/${stat.name
|
||||
.split(/-|\//)[0]
|
||||
.toLowerCase()}.svg`}
|
||||
className={`mr-2.5`}
|
||||
/>
|
||||
{stat.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
|
||||
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
|
||||
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
<div className="col-span-4 sm:col-span-3">
|
||||
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
|
||||
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</Row>
|
||||
))}
|
||||
</div>
|
||||
) : loadHistoricalStats || loadLatestStats ? (
|
||||
<>
|
||||
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -11,12 +11,13 @@ import {
|
|||
PerpOrderType,
|
||||
ZERO_I80F48,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import {
|
||||
ExclamationIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { ExclamationIcon, InformationCircleIcon } from '@heroicons/react/solid'
|
||||
import { notify } from '../../utils/notifications'
|
||||
import { calculateTradePrice, getDecimalCount } from '../../utils'
|
||||
import {
|
||||
calculateTradePrice,
|
||||
getDecimalCount,
|
||||
tokenPrecision,
|
||||
} from '../../utils'
|
||||
import { floorToDecimal } from '../../utils/index'
|
||||
import useMangoStore, { Orderbook } from '../../stores/useMangoStore'
|
||||
import Button, { LinkButton } from '../Button'
|
||||
|
@ -41,6 +42,7 @@ import useLocalStorageState, {
|
|||
import InlineNotification from '../InlineNotification'
|
||||
import { DEFAULT_SPOT_MARGIN_KEY } from '../SettingsModal'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
|
||||
const MAX_SLIPPAGE_KEY = 'maxSlippage'
|
||||
|
||||
|
@ -116,6 +118,7 @@ export default function AdvancedTradeForm({
|
|||
const [postOnly, setPostOnly] = useState(false)
|
||||
const [ioc, setIoc] = useState(false)
|
||||
const [isCloseOnly, setIsCloseOnly] = useState(false)
|
||||
const [updateBaseSize, setUpdateBaseSize] = useState(false)
|
||||
|
||||
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
|
||||
const orderbook = orderBookRef.current
|
||||
|
@ -342,6 +345,43 @@ export default function AdvancedTradeForm({
|
|||
),
|
||||
[]
|
||||
)
|
||||
const previousMarkPrice: number = usePrevious(markPrice)
|
||||
|
||||
useEffect(() => {
|
||||
if (tradeType === 'Limit' && price) {
|
||||
if (updateBaseSize) {
|
||||
if (quoteSize) {
|
||||
setBaseSize(
|
||||
(Number(quoteSize) / price).toFixed(
|
||||
tokenPrecision[marketConfig.baseSymbol]
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (baseSize) {
|
||||
setQuoteSize((Number(baseSize) * price).toFixed(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [tradeType, price])
|
||||
|
||||
useEffect(() => {
|
||||
if (markPrice !== previousMarkPrice && tradeType === 'Market') {
|
||||
if (updateBaseSize) {
|
||||
if (quoteSize) {
|
||||
setBaseSize(
|
||||
(Number(quoteSize) / markPrice).toFixed(
|
||||
tokenPrecision[marketConfig.baseSymbol]
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (baseSize) {
|
||||
setQuoteSize((Number(baseSize) * markPrice).toFixed(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [markPrice, previousMarkPrice, tradeType, updateBaseSize])
|
||||
|
||||
let minOrderSize = '0'
|
||||
if (market instanceof Market && market.minOrderSize) {
|
||||
|
@ -392,6 +432,9 @@ export default function AdvancedTradeForm({
|
|||
const rawQuoteSize = baseSize * usePrice
|
||||
setQuoteSize(rawQuoteSize.toFixed(6))
|
||||
setPositionSizePercent('')
|
||||
if (updateBaseSize) {
|
||||
setUpdateBaseSize(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onSetQuoteSize = (quoteSize: number | '') => {
|
||||
|
@ -410,6 +453,9 @@ export default function AdvancedTradeForm({
|
|||
const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount)
|
||||
setBaseSize(baseSize)
|
||||
setPositionSizePercent('')
|
||||
if (!updateBaseSize) {
|
||||
setUpdateBaseSize(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onTradeTypeChange = (tradeType) => {
|
||||
|
@ -1068,7 +1114,7 @@ export default function AdvancedTradeForm({
|
|||
<button
|
||||
disabled={disabledTradeButton}
|
||||
onClick={onSubmit}
|
||||
className={`flex-grow rounded-full px-6 py-2 font-bold text-white hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100 ${
|
||||
className={`flex-grow rounded-full px-6 py-2 font-bold text-white focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 ${
|
||||
side === 'buy' ? 'bg-th-green-dark' : 'bg-th-red'
|
||||
}`}
|
||||
>
|
||||
|
@ -1174,7 +1220,7 @@ export default function AdvancedTradeForm({
|
|||
{t('max-slippage')}
|
||||
<Tooltip content={t('tooltip-slippage')}>
|
||||
<div className="outline-none focus:outline-none">
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3" />
|
||||
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
|
|||
const { t } = useTranslation('common')
|
||||
const market = useMangoStore((s) => s.selectedMarket.current)
|
||||
return (
|
||||
<div className={`relative mb-3 md:-mt-2.5 md:border-b md:border-th-fgd-4`}>
|
||||
<div className={`relative mb-3 md:-mt-2.5 md:border-b md:border-th-bkg-3`}>
|
||||
<div
|
||||
className={`absolute hidden md:block ${
|
||||
side === 'buy'
|
||||
|
@ -30,11 +30,11 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
|
|||
<button
|
||||
onClick={() => onChange('buy')}
|
||||
className={`default-transition relative flex w-1/2 cursor-pointer
|
||||
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold hover:opacity-100 md:text-base
|
||||
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold md:text-base md:hover:opacity-100
|
||||
${
|
||||
side === 'buy'
|
||||
? `border border-th-green text-th-green md:border-0`
|
||||
: `border border-th-fgd-4 text-th-fgd-4 hover:border-th-green hover:text-th-green md:border-0`
|
||||
: `border border-th-fgd-4 text-th-fgd-4 md:border-0 md:hover:border-th-green md:hover:text-th-green`
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
@ -43,11 +43,11 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
|
|||
<button
|
||||
onClick={() => onChange('sell')}
|
||||
className={`default-transition relative flex w-1/2 cursor-pointer
|
||||
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold hover:opacity-100 md:text-base
|
||||
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold md:text-base md:hover:opacity-100
|
||||
${
|
||||
side === 'sell'
|
||||
? `border border-th-red text-th-red md:border-0`
|
||||
: `border border-th-fgd-4 text-th-fgd-4 hover:border-th-red hover:text-th-red md:border-0`
|
||||
: `border border-th-fgd-4 text-th-fgd-4 md:border-0 md:hover:border-th-red md:hover:text-th-red`
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { SwitchHorizontalIcon } from '@heroicons/react/outline'
|
||||
import { SwitchHorizontalIcon } from '@heroicons/react/solid'
|
||||
import { getWeights } from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from '../../stores/useMangoStore'
|
||||
import AdvancedTradeForm from './AdvancedTradeForm'
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
import { Balances } from '../@types/types'
|
||||
import {
|
||||
getTokenBySymbol,
|
||||
nativeI80F48ToUi,
|
||||
nativeToUi,
|
||||
QUOTE_INDEX,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore from '../stores/useMangoStore'
|
||||
import { i80f48ToPercent } from '../utils/index'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import { I80F48 } from '@blockworks-foundation/mango-client'
|
||||
import useMangoAccount from './useMangoAccount'
|
||||
|
||||
export function useBalances(): Balances[] {
|
||||
const balances: any[] = []
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
|
||||
for (const {
|
||||
marketIndex,
|
||||
baseSymbol,
|
||||
name,
|
||||
} of mangoGroupConfig.spotMarkets) {
|
||||
if (!mangoAccount || !mangoGroup || !mangoCache) {
|
||||
return []
|
||||
}
|
||||
|
||||
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex]
|
||||
const quoteCurrencyIndex = QUOTE_INDEX
|
||||
|
||||
let nativeBaseFree = 0
|
||||
let nativeQuoteFree = 0
|
||||
let nativeBaseLocked = 0
|
||||
let nativeQuoteLocked = 0
|
||||
if (openOrders) {
|
||||
nativeBaseFree = openOrders.baseTokenFree.toNumber()
|
||||
nativeQuoteFree = openOrders.quoteTokenFree
|
||||
.add(openOrders['referrerRebatesAccrued'])
|
||||
.toNumber()
|
||||
nativeBaseLocked = openOrders.baseTokenTotal
|
||||
.sub(openOrders.baseTokenFree)
|
||||
.toNumber()
|
||||
nativeQuoteLocked = openOrders.quoteTokenTotal
|
||||
.sub(openOrders.quoteTokenFree)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const tokenIndex = marketIndex
|
||||
|
||||
const net = (nativeBaseLocked, tokenIndex) => {
|
||||
const amount = mangoAccount
|
||||
.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
.add(
|
||||
nativeI80F48ToUi(
|
||||
I80F48.fromNumber(nativeBaseLocked),
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
).sub(
|
||||
mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
const value = (nativeBaseLocked, tokenIndex) => {
|
||||
const amount = mangoGroup
|
||||
.getPrice(tokenIndex, mangoCache)
|
||||
.mul(net(nativeBaseLocked, tokenIndex))
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
const marketPair = [
|
||||
{
|
||||
market: null,
|
||||
key: `${baseSymbol}${name}`,
|
||||
symbol: baseSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeBaseLocked,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeBaseFree,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
net: net(nativeBaseLocked, tokenIndex),
|
||||
value: value(nativeBaseLocked, tokenIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
decimals: mangoGroup.tokens[tokenIndex].decimals,
|
||||
},
|
||||
{
|
||||
market: null,
|
||||
key: `${name}`,
|
||||
symbol: mangoGroupConfig.quoteSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeQuoteLocked,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeQuoteFree,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
net: net(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
value: value(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
decimals: mangoGroup.tokens[quoteCurrencyIndex].decimals,
|
||||
},
|
||||
]
|
||||
balances.push(marketPair)
|
||||
}
|
||||
|
||||
const baseBalances = balances.map((b) => b[0])
|
||||
const quoteBalances = balances.map((b) => b[1])
|
||||
const quoteMeta = quoteBalances[0]
|
||||
const quoteInOrders = sumBy(quoteBalances, 'orders')
|
||||
const unsettled = sumBy(quoteBalances, 'unsettled')
|
||||
|
||||
if (!mangoGroup || !mangoCache) {
|
||||
return []
|
||||
}
|
||||
|
||||
const net: I80F48 = quoteMeta.deposits
|
||||
.add(I80F48.fromNumber(unsettled))
|
||||
.sub(quoteMeta.borrows)
|
||||
.add(I80F48.fromNumber(quoteInOrders))
|
||||
|
||||
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol)
|
||||
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
|
||||
|
||||
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache))
|
||||
|
||||
const depositRate = i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex))
|
||||
|
||||
const borrowRate = i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex))
|
||||
|
||||
return [
|
||||
{
|
||||
market: null,
|
||||
key: `${quoteMeta.symbol}${quoteMeta.symbol}`,
|
||||
symbol: quoteMeta.symbol,
|
||||
deposits: quoteMeta.deposits,
|
||||
borrows: quoteMeta.borrows,
|
||||
orders: quoteInOrders,
|
||||
unsettled,
|
||||
net,
|
||||
value,
|
||||
depositRate,
|
||||
borrowRate,
|
||||
decimals: mangoGroup.tokens[QUOTE_INDEX].decimals,
|
||||
},
|
||||
].concat(baseBalances)
|
||||
}
|
|
@ -29,6 +29,9 @@ const useMangoStats = () => {
|
|||
},
|
||||
])
|
||||
const [latestStats, setLatestStats] = useState<any[]>([])
|
||||
const [loadLatestStats, setLoadLatestStats] = useState<boolean>(false)
|
||||
const [loadHistoricalStats, setLoadHistoricalStats] = useState<boolean>(false)
|
||||
const [loadPerpStats, setLoadPerpStats] = useState<boolean>(false)
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
|
||||
const connection = useMangoStore((s) => s.connection.current)
|
||||
|
@ -36,22 +39,34 @@ const useMangoStats = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchHistoricalStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/spot_stats_hourly?mango-group=${mangoGroupName}`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setStats(stats)
|
||||
setLoadHistoricalStats(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/spot_stats_hourly?mango-group=${mangoGroupName}`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setStats(stats)
|
||||
setLoadHistoricalStats(false)
|
||||
} catch {
|
||||
setLoadHistoricalStats(false)
|
||||
}
|
||||
}
|
||||
fetchHistoricalStats()
|
||||
}, [mangoGroupName])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHistoricalPerpStats = async () => {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/perp_stats_hourly?mango-group=${mangoGroupName}`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setPerpStats(stats)
|
||||
setLoadPerpStats(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/stats/perp_stats_hourly?mango-group=${mangoGroupName}`
|
||||
)
|
||||
const stats = await response.json()
|
||||
setPerpStats(stats)
|
||||
setLoadPerpStats(false)
|
||||
} catch {
|
||||
setLoadPerpStats(false)
|
||||
}
|
||||
}
|
||||
fetchHistoricalPerpStats()
|
||||
}, [mangoGroupName])
|
||||
|
@ -59,6 +74,7 @@ const useMangoStats = () => {
|
|||
useEffect(() => {
|
||||
const getLatestStats = async () => {
|
||||
if (mangoGroup) {
|
||||
setLoadLatestStats(true)
|
||||
const rootBanks = await mangoGroup.loadRootBanks(connection)
|
||||
if (!config) return
|
||||
const latestStats = config.tokens.map((token) => {
|
||||
|
@ -95,13 +111,21 @@ const useMangoStats = () => {
|
|||
}
|
||||
})
|
||||
setLatestStats(latestStats)
|
||||
setLoadLatestStats(false)
|
||||
}
|
||||
}
|
||||
|
||||
getLatestStats()
|
||||
}, [mangoGroup])
|
||||
|
||||
return { latestStats, stats, perpStats }
|
||||
return {
|
||||
latestStats,
|
||||
loadLatestStats,
|
||||
stats,
|
||||
perpStats,
|
||||
loadHistoricalStats,
|
||||
loadPerpStats,
|
||||
}
|
||||
}
|
||||
|
||||
export default useMangoStats
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import useMangoStore from '../stores/useMangoStore'
|
||||
import useMangoStore, { PerpPosition } from '../stores/useMangoStore'
|
||||
import BN from 'bn.js'
|
||||
import {
|
||||
MangoAccount,
|
||||
|
@ -24,7 +24,7 @@ export const collectPerpPosition = (
|
|||
marketConfig: PerpMarketConfig,
|
||||
perpMarket: PerpMarket,
|
||||
tradeHistory: any
|
||||
) => {
|
||||
): PerpPosition | undefined => {
|
||||
if (
|
||||
!mangoAccount ||
|
||||
!mangoGroup ||
|
||||
|
@ -32,7 +32,7 @@ export const collectPerpPosition = (
|
|||
!perpMarket ||
|
||||
!tradeHistory
|
||||
)
|
||||
return {}
|
||||
return
|
||||
|
||||
const perpMarketInfo = mangoGroup.perpMarkets[marketConfig.marketIndex]
|
||||
const perpAccount = mangoAccount.perpAccounts[marketConfig.marketIndex]
|
||||
|
@ -104,7 +104,7 @@ const usePerpPositions = () => {
|
|||
mangoCache &&
|
||||
Object.keys(allMarkets).length
|
||||
) {
|
||||
const perpAccounts = mangoAccount
|
||||
const perpPositions = mangoAccount
|
||||
? groupConfig.perpMarkets.map((m) =>
|
||||
collectPerpPosition(
|
||||
mangoAccount,
|
||||
|
@ -117,13 +117,14 @@ const usePerpPositions = () => {
|
|||
)
|
||||
: []
|
||||
|
||||
const openPerpPositions = perpAccounts.filter(
|
||||
({ perpAccount }) =>
|
||||
perpAccount?.basePosition && !perpAccount.basePosition.eq(new BN(0))
|
||||
)
|
||||
const openPerpPositions = perpPositions.filter(
|
||||
(p) =>
|
||||
p?.perpAccount?.basePosition &&
|
||||
!p.perpAccount.basePosition.eq(new BN(0))
|
||||
) as PerpPosition[]
|
||||
|
||||
setMangoStore((state) => {
|
||||
state.selectedMangoAccount.perpAccounts = perpAccounts
|
||||
state.selectedMangoAccount.perpPositions = perpPositions
|
||||
state.selectedMangoAccount.openPerpPositions = openPerpPositions
|
||||
if (
|
||||
openPerpPositions.length !==
|
||||
|
@ -132,10 +133,12 @@ const usePerpPositions = () => {
|
|||
state.selectedMangoAccount.totalOpenPerpPositions =
|
||||
openPerpPositions.length
|
||||
}
|
||||
state.selectedMangoAccount.unsettledPerpPositions = perpAccounts.filter(
|
||||
({ perpAccount, unsettledPnl }) =>
|
||||
perpAccount?.basePosition?.eq(new BN(0)) && unsettledPnl != 0
|
||||
)
|
||||
state.selectedMangoAccount.unsettledPerpPositions =
|
||||
perpPositions.filter(
|
||||
(p) =>
|
||||
p?.perpAccount?.basePosition?.eq(new BN(0)) &&
|
||||
p?.unsettledPnl != 0
|
||||
) as PerpPosition[]
|
||||
})
|
||||
}
|
||||
}, [mangoAccount, mangoCache, tradeHistory])
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import {
|
||||
getTokenBySymbol,
|
||||
nativeI80F48ToUi,
|
||||
nativeToUi,
|
||||
QUOTE_INDEX,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore, { SpotBalance } from '../stores/useMangoStore'
|
||||
import { i80f48ToPercent } from '../utils/index'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import { I80F48 } from '@blockworks-foundation/mango-client'
|
||||
import useMangoAccount from './useMangoAccount'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const useSpotBalances = () => {
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
|
||||
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
|
||||
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
|
||||
const setMangoStore = useMangoStore((s) => s.set)
|
||||
|
||||
useEffect(() => {
|
||||
if (!mangoAccount || !mangoGroup || !mangoCache) {
|
||||
return
|
||||
}
|
||||
|
||||
const balances: SpotBalance[][] = []
|
||||
|
||||
for (const {
|
||||
marketIndex,
|
||||
baseSymbol,
|
||||
name,
|
||||
} of mangoGroupConfig.spotMarkets) {
|
||||
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex]
|
||||
const quoteCurrencyIndex = QUOTE_INDEX
|
||||
|
||||
let nativeBaseFree = 0
|
||||
let nativeQuoteFree = 0
|
||||
let nativeBaseLocked = 0
|
||||
let nativeQuoteLocked = 0
|
||||
if (openOrders) {
|
||||
nativeBaseFree = openOrders.baseTokenFree.toNumber()
|
||||
nativeQuoteFree = openOrders.quoteTokenFree
|
||||
.add(openOrders['referrerRebatesAccrued'])
|
||||
.toNumber()
|
||||
nativeBaseLocked = openOrders.baseTokenTotal
|
||||
.sub(openOrders.baseTokenFree)
|
||||
.toNumber()
|
||||
nativeQuoteLocked = openOrders.quoteTokenTotal
|
||||
.sub(openOrders.quoteTokenFree)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const tokenIndex = marketIndex
|
||||
|
||||
const net = (nativeBaseLocked, tokenIndex) => {
|
||||
const amount = mangoAccount
|
||||
.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
.add(
|
||||
nativeI80F48ToUi(
|
||||
I80F48.fromNumber(nativeBaseLocked),
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
).sub(
|
||||
mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
const value = (nativeBaseLocked, tokenIndex) => {
|
||||
const amount = mangoGroup
|
||||
.getPrice(tokenIndex, mangoCache)
|
||||
.mul(net(nativeBaseLocked, tokenIndex))
|
||||
|
||||
return amount
|
||||
}
|
||||
|
||||
const marketPair: SpotBalance[] = [
|
||||
{
|
||||
market: null,
|
||||
key: `${baseSymbol}${name}`,
|
||||
symbol: baseSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[tokenIndex],
|
||||
mangoGroup,
|
||||
tokenIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeBaseLocked,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeBaseFree,
|
||||
mangoGroup.tokens[tokenIndex].decimals
|
||||
),
|
||||
net: net(nativeBaseLocked, tokenIndex),
|
||||
value: value(nativeBaseLocked, tokenIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
decimals: mangoGroup.tokens[tokenIndex].decimals,
|
||||
},
|
||||
{
|
||||
market: null,
|
||||
key: `${name}`,
|
||||
symbol: mangoGroupConfig.quoteSymbol,
|
||||
deposits: mangoAccount.getUiDeposit(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
borrows: mangoAccount.getUiBorrow(
|
||||
mangoCache.rootBankCache[quoteCurrencyIndex],
|
||||
mangoGroup,
|
||||
quoteCurrencyIndex
|
||||
),
|
||||
orders: nativeToUi(
|
||||
nativeQuoteLocked,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
unsettled: nativeToUi(
|
||||
nativeQuoteFree,
|
||||
mangoGroup.tokens[quoteCurrencyIndex].decimals
|
||||
),
|
||||
net: net(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
value: value(nativeQuoteLocked, quoteCurrencyIndex),
|
||||
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
|
||||
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
|
||||
decimals: mangoGroup.tokens[quoteCurrencyIndex].decimals,
|
||||
},
|
||||
]
|
||||
balances.push(marketPair)
|
||||
}
|
||||
|
||||
const baseBalances = balances.map((b) => b[0])
|
||||
const quoteBalances = balances.map((b) => b[1])
|
||||
const quoteMeta = quoteBalances[0]
|
||||
const quoteInOrders = sumBy(quoteBalances, 'orders')
|
||||
const unsettled = sumBy(quoteBalances, 'unsettled')
|
||||
|
||||
const net: I80F48 = quoteMeta.deposits
|
||||
.add(I80F48.fromNumber(unsettled))
|
||||
.sub(quoteMeta.borrows)
|
||||
.add(I80F48.fromNumber(quoteInOrders))
|
||||
|
||||
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol)
|
||||
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
|
||||
|
||||
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache))
|
||||
|
||||
const depositRate = i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex))
|
||||
|
||||
const borrowRate = i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex))
|
||||
|
||||
setMangoStore((s) => {
|
||||
s.selectedMangoAccount.spotBalances = baseBalances.concat([
|
||||
{
|
||||
market: null,
|
||||
key: `${quoteMeta.symbol}${quoteMeta.symbol}`,
|
||||
symbol: quoteMeta.symbol,
|
||||
deposits: quoteMeta.deposits,
|
||||
borrows: quoteMeta.borrows,
|
||||
orders: quoteInOrders,
|
||||
unsettled,
|
||||
net,
|
||||
value,
|
||||
depositRate,
|
||||
borrowRate,
|
||||
decimals: mangoGroup.tokens[QUOTE_INDEX].decimals,
|
||||
},
|
||||
])
|
||||
})
|
||||
}, [mangoGroup, mangoCache, mangoAccount])
|
||||
}
|
||||
|
||||
export default useSpotBalances
|
|
@ -3,6 +3,7 @@ import useMangoStore from '../stores/useMangoStore'
|
|||
import {
|
||||
getMultipleAccounts,
|
||||
nativeToUi,
|
||||
zeroKey,
|
||||
} from '@blockworks-foundation/mango-client'
|
||||
import {
|
||||
MSRM_DECIMALS,
|
||||
|
@ -66,7 +67,9 @@ const useSrmAccount = () => {
|
|||
)
|
||||
|
||||
setSrmAccount(srmAccountInfo.accountInfo)
|
||||
setMsrmAccount(msrmAccountInfo.accountInfo)
|
||||
if (!msrmPk.equals(zeroKey)) {
|
||||
setMsrmAccount(msrmAccountInfo.accountInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fetchAccounts()
|
||||
|
|
|
@ -22,8 +22,7 @@ const reverseSide = (side) => (side === 'buy' ? 'sell' : 'buy')
|
|||
|
||||
function getMarketName(event) {
|
||||
const mangoGroupConfig = useMangoStore.getState().selectedMangoGroup.config
|
||||
|
||||
let marketName
|
||||
let marketName = ''
|
||||
if (!event.marketName && event.address) {
|
||||
const marketInfo = getMarketByPublicKey(mangoGroupConfig, event.address)
|
||||
if (marketInfo) {
|
||||
|
|
14
package.json
14
package.json
|
@ -4,8 +4,9 @@
|
|||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"devnet": "NEXT_PUBLIC_CLUSTER=devnet NEXT_PUBLIC_ENDPOINT=https://mango.devnet.rpcpool.com/ NEXT_PUBLIC_GROUP=devnet.2 next dev",
|
||||
"dev": "NODE_OPTIONS=\"--max_old_space_size=4096\" next dev",
|
||||
"devnet": "NEXT_PUBLIC_CLUSTER=devnet NEXT_PUBLIC_ENDPOINT=https://mango.devnet.rpcpool.com/ NEXT_PUBLIC_GROUP=devnet.4 next dev",
|
||||
"testnet": "NEXT_PUBLIC_CLUSTER=testnet NEXT_PUBLIC_ENDPOINT=https://api.testnet.solana.com NEXT_PUBLIC_GROUP=testnet.0 next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"prepare": "husky install",
|
||||
|
@ -17,7 +18,7 @@
|
|||
"analyze": "ANALYZE=true yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^3.6.8",
|
||||
"@blockworks-foundation/mango-client": "^3.6.9",
|
||||
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
|
||||
"@heroicons/react": "^1.0.0",
|
||||
"@jup-ag/react-hook": "^1.0.0-beta.22",
|
||||
|
@ -66,7 +67,7 @@
|
|||
"borsh": "^0.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^12.1.0",
|
||||
"@next/bundle-analyzer": "^12.2.0",
|
||||
"@svgr/webpack": "^6.1.2",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@types/node": "^14.14.25",
|
||||
|
@ -91,8 +92,13 @@
|
|||
"typescript": "^4.6.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"big.js": "6.1.1",
|
||||
"bn.js": "5.1.3",
|
||||
"@solana/buffer-layout": "4.0.0",
|
||||
"@solana/web3.js": "1.47.3",
|
||||
"@project-serum/serum": "0.13.65",
|
||||
"@project-serum/anchor": "0.23.0",
|
||||
"@project-serum/sol-wallet-adapter": "0.2.6",
|
||||
"@types/bn.js": "5.1.0"
|
||||
},
|
||||
"nextBundleAnalysis": {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { RektIcon } from '../components/icons'
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'delegate'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function Custom404() {
|
||||
const { t } = useTranslation('common')
|
||||
return (
|
||||
<div
|
||||
className="mx-auto flex max-w-xl flex-col items-center justify-center text-center"
|
||||
style={{ height: 'calc(100vh - 80px)' }}
|
||||
>
|
||||
<RektIcon className="mb-6 h-14 w-auto -rotate-6 transform text-th-red" />
|
||||
<span className="text-lg font-bold text-th-fgd-4">404</span>
|
||||
<h1 className="mt-1 text-3xl text-th-fgd-1 sm:text-4xl">
|
||||
{t('404-heading')}
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-th-fgd-4">{t('404-description')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -13,10 +13,8 @@ import useOraclePrice from '../hooks/useOraclePrice'
|
|||
import { getDecimalCount } from '../utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ViewportProvider } from '../hooks/useViewport'
|
||||
import BottomBar from '../components/mobile/BottomBar'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import GlobalNotification from '../components/GlobalNotification'
|
||||
import { useOpenOrders } from '../hooks/useOpenOrders'
|
||||
import usePerpPositions from '../hooks/usePerpPositions'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
|
@ -41,6 +39,8 @@ import {
|
|||
GlowWalletAdapter,
|
||||
} from '@solana/wallet-adapter-wallets'
|
||||
import { HuobiWalletAdapter } from '@solana/wallet-adapter-huobi'
|
||||
import useSpotBalances from 'hooks/useSpotBalances'
|
||||
import Layout from 'components/Layout'
|
||||
|
||||
const SENTRY_URL = process.env.NEXT_PUBLIC_SENTRY_URL
|
||||
if (SENTRY_URL) {
|
||||
|
@ -60,6 +60,11 @@ const OpenOrdersStoreUpdater = () => {
|
|||
return null
|
||||
}
|
||||
|
||||
const SpotBalancesStoreUpdater = () => {
|
||||
useSpotBalances()
|
||||
return null
|
||||
}
|
||||
|
||||
const PerpPositionsStoreUpdater = () => {
|
||||
usePerpPositions()
|
||||
return null
|
||||
|
@ -188,33 +193,31 @@ function App({ Component, pageProps }) {
|
|||
<meta name="google" content="notranslate" />
|
||||
<link rel="manifest" href="/manifest.json"></link>
|
||||
</Head>
|
||||
<ErrorBoundary>
|
||||
<WalletProvider wallets={wallets}>
|
||||
<PageTitle />
|
||||
<MangoStoreUpdater />
|
||||
<OpenOrdersStoreUpdater />
|
||||
<PerpPositionsStoreUpdater />
|
||||
<TradeHistoryStoreUpdater />
|
||||
<FetchReferrer />
|
||||
|
||||
<ThemeProvider defaultTheme="Mango">
|
||||
<ThemeProvider defaultTheme="Mango">
|
||||
<ErrorBoundary>
|
||||
<WalletProvider wallets={wallets}>
|
||||
<PageTitle />
|
||||
<MangoStoreUpdater />
|
||||
<OpenOrdersStoreUpdater />
|
||||
<SpotBalancesStoreUpdater />
|
||||
<PerpPositionsStoreUpdater />
|
||||
<TradeHistoryStoreUpdater />
|
||||
<FetchReferrer />
|
||||
<WalletListener />
|
||||
<ViewportProvider>
|
||||
<div className="min-h-screen bg-th-bkg-1">
|
||||
<ErrorBoundary>
|
||||
<GlobalNotification />
|
||||
<Component {...pageProps} />
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
|
||||
<BottomBar />
|
||||
</div>
|
||||
|
||||
<Notifications />
|
||||
</ViewportProvider>
|
||||
</ThemeProvider>
|
||||
</WalletProvider>
|
||||
</ErrorBoundary>
|
||||
</WalletProvider>
|
||||
</ErrorBoundary>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,30 +7,25 @@ import React, {
|
|||
} from 'react'
|
||||
import {
|
||||
BellIcon,
|
||||
ChevronDownIcon,
|
||||
CurrencyDollarIcon,
|
||||
DuplicateIcon,
|
||||
ExclamationCircleIcon,
|
||||
GiftIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid'
|
||||
} from '@heroicons/react/solid'
|
||||
import { nativeToUi, ZERO_BN } from '@blockworks-foundation/mango-client'
|
||||
import useMangoStore, { serumProgramId, MNGO_INDEX } from 'stores/useMangoStore'
|
||||
import PageBodyContainer from 'components/PageBodyContainer'
|
||||
import TopBar from 'components/TopBar'
|
||||
import AccountOrders from 'components/account_page/AccountOrders'
|
||||
import AccountHistory from 'components/account_page/AccountHistory'
|
||||
import AccountsModal from 'components/AccountsModal'
|
||||
import AccountOverview from 'components/account_page/AccountOverview'
|
||||
import AccountInterest from 'components/account_page/AccountInterest'
|
||||
import AccountFunding from 'components/account_page/AccountFunding'
|
||||
import AccountPerformancePerToken from 'components/account_page/AccountPerformancePerToken'
|
||||
import AccountNameModal from 'components/AccountNameModal'
|
||||
import { IconButton, LinkButton } from 'components/Button'
|
||||
import Button, { LinkButton } from 'components/Button'
|
||||
import EmptyState from 'components/EmptyState'
|
||||
import Loading from 'components/Loading'
|
||||
import Swipeable from 'components/mobile/Swipeable'
|
||||
|
@ -49,15 +44,23 @@ import {
|
|||
mangoGroupSelector,
|
||||
} from 'stores/selectors'
|
||||
import CreateAlertModal from 'components/CreateAlertModal'
|
||||
import { copyToClipboard } from 'utils'
|
||||
import {
|
||||
abbreviateAddress,
|
||||
// abbreviateAddress,
|
||||
copyToClipboard,
|
||||
} from 'utils'
|
||||
import DelegateModal from 'components/DelegateModal'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { handleWalletConnect } from 'components/ConnectWalletButton'
|
||||
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
|
||||
import NftProfilePicModal from 'components/NftProfilePicModal'
|
||||
import ProfileImage from 'components/ProfileImage'
|
||||
import SwipeableTabs from 'components/mobile/SwipeableTabs'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import ProfileImage from 'components/ProfileImage'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
|
@ -76,14 +79,7 @@ export async function getStaticProps({ locale }) {
|
|||
}
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
'Portfolio',
|
||||
'Orders',
|
||||
'History',
|
||||
'Interest',
|
||||
'Funding',
|
||||
'Performance',
|
||||
]
|
||||
const TABS = ['Overview', 'Orders', 'History', 'Performance']
|
||||
|
||||
export default function Account() {
|
||||
const { t } = useTranslation(['common', 'close-account', 'delegate'])
|
||||
|
@ -106,9 +102,10 @@ export default function Account() {
|
|||
const [viewIndex, setViewIndex] = useState(0)
|
||||
const [activeTab, setActiveTab] = useState(TABS[0])
|
||||
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
|
||||
const loadingTransaction = useMangoStore(
|
||||
(s) => s.wallet.nfts.loadingTransaction
|
||||
)
|
||||
const [savedLanguage] = useLocalStorageState('language', '')
|
||||
|
||||
const [profileData, setProfileData] = useState<any>(null)
|
||||
const [loadProfileDetails, setLoadProfileDetails] = useState(false)
|
||||
|
||||
const connecting = wallet?.adapter?.connecting
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
@ -147,6 +144,10 @@ export default function Account() {
|
|||
}
|
||||
}, [wallet])
|
||||
|
||||
useEffect(() => {
|
||||
dayjs.locale(savedLanguage == 'zh_tw' ? 'zh-tw' : savedLanguage)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
async function loadUnownedMangoAccount() {
|
||||
try {
|
||||
|
@ -279,233 +280,244 @@ export default function Account() {
|
|||
}
|
||||
}
|
||||
|
||||
const fetchProfileDetails = async (walletPk: string) => {
|
||||
setLoadProfileDetails(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://mango-transaction-log.herokuapp.com/v3/user-data/profile-details?wallet-pk=${walletPk}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setProfileData(data)
|
||||
setLoadProfileDetails(false)
|
||||
} catch (e) {
|
||||
notify({ type: 'error', title: t('profile:profile-fetch-fail') })
|
||||
console.log(e)
|
||||
setLoadProfileDetails(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (mangoAccount && pubkey) {
|
||||
fetchProfileDetails(mangoAccount.owner.toString())
|
||||
}
|
||||
}, [mangoAccount, pubkey])
|
||||
|
||||
return (
|
||||
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
|
||||
<TopBar />
|
||||
<PageBodyContainer>
|
||||
<div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10">
|
||||
{mangoAccount ? (
|
||||
<>
|
||||
<div className="flex flex-col pb-3 sm:flex-row sm:items-center md:pb-0">
|
||||
<button
|
||||
disabled={!!pubkey}
|
||||
className={`relative mb-2 mr-4 flex h-20 w-20 items-center justify-center rounded-full sm:mb-0 ${
|
||||
loadingTransaction
|
||||
? 'animate-pulse bg-th-bkg-4'
|
||||
: 'bg-th-bkg-button'
|
||||
}`}
|
||||
onClick={() => setShowProfilePicModal(true)}
|
||||
>
|
||||
<ProfileImage
|
||||
thumbHeightClass="h-20"
|
||||
thumbWidthClass="w-20"
|
||||
placeholderHeightClass="h-12"
|
||||
placeholderWidthClass="w-12"
|
||||
/>
|
||||
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
|
||||
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
|
||||
</div>
|
||||
</button>
|
||||
<div>
|
||||
<div className="mb-1 flex items-center">
|
||||
<h1 className={`mr-3`}>
|
||||
{mangoAccount?.name || t('account')}
|
||||
</h1>
|
||||
{!pubkey ? (
|
||||
<IconButton
|
||||
className="h-7 w-7"
|
||||
onClick={() => setShowNameModal(true)}
|
||||
>
|
||||
<PencilIcon className="h-3.5 w-3.5" />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex h-4 items-center">
|
||||
<div className="pt-6">
|
||||
<div className="flex flex-col pb-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
{mangoAccount ? (
|
||||
<>
|
||||
<div className="flex flex-col pb-3 sm:flex-row sm:items-center lg:pb-0">
|
||||
<div>
|
||||
<div className="flex h-8 items-center">
|
||||
<Tooltip content="Copy account address">
|
||||
<LinkButton
|
||||
className="flex items-center text-th-fgd-4 no-underline"
|
||||
onClick={() =>
|
||||
handleCopyAddress(mangoAccount.publicKey.toString())
|
||||
}
|
||||
>
|
||||
<span className="text-xxs font-normal sm:text-xs">
|
||||
{mangoAccount.publicKey.toBase58()}
|
||||
</span>
|
||||
<DuplicateIcon className="ml-1.5 h-4 w-4" />
|
||||
<h1>
|
||||
{mangoAccount?.name ||
|
||||
abbreviateAddress(mangoAccount.publicKey)}
|
||||
</h1>
|
||||
<DuplicateIcon className="ml-1.5 h-5 w-5" />
|
||||
</LinkButton>
|
||||
{isCopied ? (
|
||||
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
|
||||
Copied
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center text-xxs text-th-fgd-4">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('account-address-warning')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isCopied ? (
|
||||
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
|
||||
Copied
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center text-xxs text-th-fgd-4">
|
||||
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('account-address-warning')}
|
||||
</div>
|
||||
{pubkey && mangoAccount ? (
|
||||
profileData && !loadProfileDetails ? (
|
||||
<Link
|
||||
href={`/profile?name=${profileData?.profile_name.replace(
|
||||
/\s/g,
|
||||
'-'
|
||||
)}`}
|
||||
shallow={true}
|
||||
>
|
||||
<a className="default-transition mt-2 flex items-center space-x-2 text-th-fgd-3 hover:text-th-fgd-2">
|
||||
<ProfileImage
|
||||
imageSize="24"
|
||||
placeholderSize="12"
|
||||
publicKey={mangoAccount.owner.toString()}
|
||||
/>
|
||||
<span className="mb-0 capitalize">
|
||||
{profileData?.profile_name}
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="mt-2 h-7 w-40 animate-pulse rounded bg-th-bkg-3" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
{!pubkey ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
className="flex h-8 w-full items-center justify-center rounded-full bg-th-primary px-3 py-0 text-xs font-bold text-th-bkg-1 hover:brightness-[1.15] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
|
||||
disabled={mngoAccrued.eq(ZERO_BN)}
|
||||
onClick={handleRedeemMngo}
|
||||
>
|
||||
<div className="flex items-center whitespace-nowrap">
|
||||
<GiftIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||
{!mngoAccrued.eq(ZERO_BN) && mangoGroup
|
||||
? t('claim-x-mngo', {
|
||||
amount: nativeToUi(
|
||||
mngoAccrued.toNumber(),
|
||||
mangoGroup.tokens[MNGO_INDEX].decimals
|
||||
).toLocaleString(undefined, {
|
||||
minimumSignificantDigits: 1,
|
||||
}),
|
||||
})
|
||||
: t('zero-mngo-rewards')}
|
||||
</div>
|
||||
</button>
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<div className="relative sm:w-full">
|
||||
<Menu.Button className="flex h-8 items-center justify-center rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-2 text-xs font-bold hover:brightness-[1.1] hover:filter sm:w-full">
|
||||
{t('more')}
|
||||
<ChevronDownIcon
|
||||
className={`default-transition h-5 w-5 ${
|
||||
open
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
}`}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-20 mt-1 w-full space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5 sm:w-48">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => setShowAlertsModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<BellIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('alerts')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{!isDelegatedAccount ? (
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => setShowDelegateModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<UsersIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('delegate:set-delegate')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SwitchHorizontalIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('change-account')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{!isDelegatedAccount ? (
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
|
||||
onClick={() => setShowCloseAccountModal(true)}
|
||||
>
|
||||
<div className="flex items-center whitespace-nowrap">
|
||||
<TrashIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||
{t('close-account:close-account')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="md:rounded-lg md:bg-th-bkg-2 md:p-6">
|
||||
{mangoAccount ? (
|
||||
!isMobile ? (
|
||||
<>
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onChange={handleTabChange}
|
||||
tabs={TABS}
|
||||
/>
|
||||
<TabContent activeTab={activeTab} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
/>
|
||||
<Swipeable
|
||||
index={viewIndex}
|
||||
onChangeIndex={handleChangeViewIndex}
|
||||
</div>
|
||||
{!pubkey ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
className="flex h-8 w-full items-center justify-center rounded-full px-3 py-0 text-xs"
|
||||
disabled={mngoAccrued.eq(ZERO_BN)}
|
||||
onClick={handleRedeemMngo}
|
||||
>
|
||||
<div>
|
||||
<AccountOverview />
|
||||
<div className="flex items-center whitespace-nowrap">
|
||||
<GiftIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||
{!mngoAccrued.eq(ZERO_BN) && mangoGroup
|
||||
? t('claim-x-mngo', {
|
||||
amount: nativeToUi(
|
||||
mngoAccrued.toNumber(),
|
||||
mangoGroup.tokens[MNGO_INDEX].decimals
|
||||
).toLocaleString(undefined, {
|
||||
minimumSignificantDigits: 1,
|
||||
}),
|
||||
})
|
||||
: t('zero-mngo-rewards')}
|
||||
</div>
|
||||
<div>
|
||||
<AccountOrders />
|
||||
</div>
|
||||
<div>
|
||||
<AccountHistory />
|
||||
</div>
|
||||
<div>
|
||||
<AccountInterest />
|
||||
</div>
|
||||
<div>
|
||||
<AccountFunding />
|
||||
</div>
|
||||
<div>
|
||||
<AccountPerformancePerToken />
|
||||
</div>
|
||||
</Swipeable>
|
||||
</>
|
||||
)
|
||||
) : connected ? (
|
||||
isLoading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<Loading />
|
||||
</Button>
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<div className="relative sm:w-full">
|
||||
<Menu.Button className="flex h-8 items-center justify-center rounded-full border border-th-fgd-4 bg-transparent pt-0 pb-0 pl-3 pr-2 text-xs font-bold text-th-fgd-2 hover:brightness-[1.1] hover:filter sm:w-full">
|
||||
{t('more')}
|
||||
<ChevronDownIcon
|
||||
className={`default-transition h-5 w-5 ${
|
||||
open
|
||||
? 'rotate-180 transform'
|
||||
: 'rotate-360 transform'
|
||||
}`}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in duration-200"
|
||||
enterFrom="opacity-0 transform scale-75"
|
||||
enterTo="opacity-100 transform scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-20 mt-1 w-full space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5 sm:w-48">
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
|
||||
onClick={() => setShowAlertsModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<BellIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('alerts')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{!isDelegatedAccount ? (
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
|
||||
onClick={() => setShowDelegateModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<UsersIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('delegate:set-delegate')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
|
||||
onClick={() => setShowAccountsModal(true)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SwitchHorizontalIcon className="mr-1.5 h-4 w-4" />
|
||||
{t('change-account')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
{!isDelegatedAccount ? (
|
||||
<Menu.Item>
|
||||
<button
|
||||
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
|
||||
onClick={() => setShowCloseAccountModal(true)}
|
||||
>
|
||||
<div className="flex items-center whitespace-nowrap">
|
||||
<TrashIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
|
||||
{t('close-account:close-account')}
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
buttonText={t('create-account')}
|
||||
icon={<CurrencyDollarIcon />}
|
||||
onClickButton={() => setShowAccountsModal(true)}
|
||||
title={t('no-account-found')}
|
||||
disabled={!wallet || !mangoGroup}
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
{mangoAccount ? (
|
||||
!isMobile ? (
|
||||
<div className="mt-2">
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
onChange={handleTabChange}
|
||||
tabs={TABS}
|
||||
/>
|
||||
)
|
||||
<TabContent activeTab={activeTab} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2">
|
||||
<SwipeableTabs
|
||||
onChange={handleChangeViewIndex}
|
||||
items={TABS}
|
||||
tabIndex={viewIndex}
|
||||
width="w-40 sm:w-48"
|
||||
/>
|
||||
<Swipeable
|
||||
index={viewIndex}
|
||||
onChangeIndex={handleChangeViewIndex}
|
||||
>
|
||||
<div>
|
||||
<AccountOverview />
|
||||
</div>
|
||||
<div>
|
||||
<AccountOrders />
|
||||
</div>
|
||||
<div>
|
||||
<AccountHistory />
|
||||
</div>
|
||||
<div>
|
||||
<AccountPerformancePerToken />
|
||||
</div>
|
||||
</Swipeable>
|
||||
</div>
|
||||
)
|
||||
) : isLoading && (connected || pubkey) ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<Loading />
|
||||
</div>
|
||||
) : connected ? (
|
||||
<div className="-mt-4 rounded-lg border border-th-bkg-3 p-4 md:p-6">
|
||||
<EmptyState
|
||||
buttonText={t('create-account')}
|
||||
icon={<CurrencyDollarIcon />}
|
||||
onClickButton={() => setShowAccountsModal(true)}
|
||||
title={t('no-account-found')}
|
||||
disabled={!wallet || !mangoGroup}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="-mt-4 rounded-lg border border-th-bkg-3 p-4 md:p-6">
|
||||
<EmptyState
|
||||
buttonText={t('connect')}
|
||||
desc={t('connect-view')}
|
||||
|
@ -514,14 +526,16 @@ export default function Account() {
|
|||
onClickButton={handleConnect}
|
||||
title={t('connect-wallet')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!connected && (
|
||||
<div className="mt-6 md:mt-3 md:rounded-lg md:bg-th-bkg-2 md:p-6">
|
||||
<MangoAccountLookup />
|
||||
{!connected && !pubkey ? (
|
||||
<div className="flex flex-col items-center pt-2">
|
||||
<p>OR</p>
|
||||
<MangoAccountLookup />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</PageBodyContainer>
|
||||
</div>
|
||||
|
||||
{showAccountsModal ? (
|
||||
<AccountsModal
|
||||
onClose={handleCloseAccounts}
|
||||
|
@ -566,16 +580,12 @@ export default function Account() {
|
|||
|
||||
const TabContent = ({ activeTab }) => {
|
||||
switch (activeTab) {
|
||||
case 'Portfolio':
|
||||
case 'Overview':
|
||||
return <AccountOverview />
|
||||
case 'Orders':
|
||||
return <AccountOrders />
|
||||
case 'History':
|
||||
return <AccountHistory />
|
||||
case 'Interest':
|
||||
return <AccountInterest />
|
||||
case 'Funding':
|
||||
return <AccountFunding />
|
||||
case 'Performance':
|
||||
return <AccountPerformancePerToken />
|
||||
default:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue