Merge pull request #290 from blockworks-foundation/redesign-account-page
re-design account page
This commit is contained in:
commit
bfe4a89067
|
@ -105,13 +105,11 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
|
||||
<div className="fixed z-20 hidden h-screen md:block">
|
||||
<button
|
||||
className="absolute right-0 top-1/2 z-20 hidden h-8 w-3 -translate-y-1/2 rounded-none rounded-l bg-th-bkg-3 hover:bg-th-bkg-4 focus:outline-none focus-visible:bg-th-bkg-4 2xl:block"
|
||||
className="absolute right-0 top-1/2 z-20 hidden h-8 w-3 -translate-y-1/2 rounded-none rounded-l bg-th-bkg-3 hover:bg-th-bkg-4 focus:outline-none focus-visible:bg-th-bkg-4 2xl:flex 2xl:items-center 2xl:justify-center"
|
||||
onClick={handleToggleSidebar}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={`absolute bottom-2 h-4 w-4 shrink-0 ${
|
||||
!isCollapsed ? 'rotate-180' : ''
|
||||
}`}
|
||||
className={`h-4 w-4 shrink-0 ${!isCollapsed ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
ArchiveBoxArrowDownIcon,
|
||||
ExclamationTriangleIcon,
|
||||
DocumentTextIcon,
|
||||
SparklesIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -224,13 +223,6 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
title={t('trade')}
|
||||
pagePath="/trade"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/explore'}
|
||||
collapsed={collapsed}
|
||||
icon={<SparklesIcon className="h-5 w-5" />}
|
||||
title={t('explore')}
|
||||
pagePath="/explore"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/borrow'}
|
||||
collapsed={collapsed}
|
||||
|
|
|
@ -201,10 +201,21 @@ const TokenList = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const balancesNumber = useMemo(() => {
|
||||
if (!banks.length) return 0
|
||||
const activeBalancesNumber = banks.filter(
|
||||
(bank) => Math.abs(bank.balance) > 0,
|
||||
).length
|
||||
return activeBalancesNumber
|
||||
}, [banks])
|
||||
|
||||
return (
|
||||
<ContentBox hideBorder hidePadding>
|
||||
{mangoAccountAddress ? (
|
||||
<div className="flex w-full items-center justify-end border-b border-th-bkg-3 px-6 py-3 lg:-mt-[36px] lg:mb-4 lg:mr-12 lg:w-auto lg:border-0 lg:py-0">
|
||||
<div className="flex w-full items-center justify-between border-b border-th-bkg-3 px-6 py-3">
|
||||
<p className="text-xs text-th-fgd-2">{`${balancesNumber} ${t(
|
||||
'balances',
|
||||
)}`}</p>
|
||||
<Switch
|
||||
checked={showZeroBalances}
|
||||
disabled={!mangoAccountAddress}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
CheckCircleIcon,
|
||||
ChevronRightIcon,
|
||||
|
@ -12,17 +11,14 @@ import {
|
|||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import WalletIcon from './icons/WalletIcon'
|
||||
import Button from './shared/Button'
|
||||
import ConnectedMenu from './wallet/ConnectedMenu'
|
||||
import ConnectWalletButton from './wallet/ConnectWalletButton'
|
||||
import CreateAccountModal from './modals/CreateAccountModal'
|
||||
import { useRouter } from 'next/router'
|
||||
// import SolanaTps from './SolanaTps'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useOnlineStatus from 'hooks/useOnlineStatus'
|
||||
import { abbreviateAddress } from 'utils/formatting'
|
||||
import DepositWithdrawModal from './modals/DepositWithdrawModal'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import AccountsButton from './AccountsButton'
|
||||
import useUnownedAccount from 'hooks/useUnownedAccount'
|
||||
import NotificationsButton from './notifications/NotificationsButton'
|
||||
|
@ -41,7 +37,7 @@ import Link from 'next/link'
|
|||
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
|
||||
|
||||
export const TOPBAR_ICON_BUTTON_CLASSES =
|
||||
'relative flex h-16 w-16 items-center justify-center border-l border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2'
|
||||
'relative flex h-16 w-10 sm:w-16 items-center justify-center sm:border-l sm:border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
|
@ -63,11 +59,6 @@ const TopBar = () => {
|
|||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const isOnline = useOnlineStatus()
|
||||
|
||||
const router = useRouter()
|
||||
const { query } = router
|
||||
|
||||
const { isDesktop } = useViewport()
|
||||
|
||||
const { isUnownedAccount } = useUnownedAccount()
|
||||
const showUserSetup = mangoStore((s) => s.showUserSetup)
|
||||
const [, setIsOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
|
||||
|
@ -110,17 +101,9 @@ const TopBar = () => {
|
|||
>
|
||||
<div className="flex w-full items-center justify-between md:space-x-4">
|
||||
<span className="mb-0 flex items-center">
|
||||
{query.token || query.market ? (
|
||||
<button
|
||||
className="flex h-16 w-16 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
) : null}
|
||||
<div className="flex h-[63px] w-16 items-center justify-center bg-th-bkg-1 md:hidden">
|
||||
<img
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
className="h-8 w-8 flex-shrink-0"
|
||||
src={themeData.logoPath}
|
||||
alt="logo"
|
||||
/>
|
||||
|
@ -189,7 +172,7 @@ const TopBar = () => {
|
|||
)
|
||||
) : isWhiteListed ? (
|
||||
<Link href="/rewards" shallow={true}>
|
||||
<div className="flex h-16 items-center justify-between bg-gradient-to-br from-th-bkg-2 to-th-bkg-3 px-4 lg:pl-6">
|
||||
<div className="flex h-16 items-center justify-between border-x border-th-bkg-3 px-4 md:border-l-0 lg:pl-6">
|
||||
<div>
|
||||
<span className="whitespace-nowrap font-bold text-th-fgd-2">
|
||||
Points
|
||||
|
@ -208,7 +191,7 @@ const TopBar = () => {
|
|||
</SheenLoader>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRightIcon className="ml-2 hidden h-7 w-7 text-th-fgd-4 lg:block" />
|
||||
<ChevronRightIcon className="ml-2 hidden h-6 w-6 text-th-fgd-4 lg:block" />
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
|
@ -222,8 +205,7 @@ const TopBar = () => {
|
|||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center">
|
||||
{isUnownedAccount ||
|
||||
(!connected && !isDesktop) ? null : !isDesktop ? (
|
||||
{isUnownedAccount || !connected ? null : (
|
||||
<div className="h-[63px] bg-th-bkg-1">
|
||||
<button
|
||||
onClick={() => handleDepositWithdrawModal('deposit')}
|
||||
|
@ -232,12 +214,6 @@ const TopBar = () => {
|
|||
<DepositWithdrawIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handleDepositWithdrawModal('deposit')}
|
||||
secondary
|
||||
className="mr-4"
|
||||
>{`${t('deposit')} / ${t('withdraw')}`}</Button>
|
||||
)}
|
||||
<div className="h-[63px] bg-th-bkg-1">
|
||||
<button
|
||||
|
@ -252,10 +228,14 @@ const TopBar = () => {
|
|||
<div className="flex h-[63px] items-center bg-th-bkg-1">
|
||||
{mangoAccountAddress && <NotificationsButton />}
|
||||
<AccountsButton />
|
||||
<ConnectedMenu />
|
||||
<div className="pl-2 sm:pl-0">
|
||||
<ConnectedMenu />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ConnectWalletButton handleShowSetup={handleShowSetup} />
|
||||
<div className="pl-2 sm:pl-0">
|
||||
<ConnectWalletButton handleShowSetup={handleShowSetup} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,35 +1,19 @@
|
|||
import { useState } from 'react'
|
||||
import Button from '../shared/Button'
|
||||
import {
|
||||
ArrowDownRightIcon,
|
||||
ArrowUpLeftIcon,
|
||||
// DocumentDuplicateIcon,
|
||||
// EyeIcon,
|
||||
// EyeSlashIcon,
|
||||
// PencilIcon,
|
||||
// SquaresPlusIcon,
|
||||
// TrashIcon,
|
||||
// UserPlusIcon,
|
||||
// WrenchIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import CloseAccountModal from '../modals/CloseAccountModal'
|
||||
import AccountNameModal from '../modals/AccountNameModal'
|
||||
import { copyToClipboard } from 'utils'
|
||||
import { notify } from 'utils/notifications'
|
||||
import DelegateModal from '@components/modals/DelegateModal'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import BorrowRepayModal from '@components/modals/BorrowRepayModal'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import CreateAccountModal from '@components/modals/CreateAccountModal'
|
||||
// import { Popover, Transition } from '@headlessui/react'
|
||||
// import ActionsLinkButton from './ActionsLinkButton'
|
||||
import useUnownedAccount from 'hooks/useUnownedAccount'
|
||||
// import { useViewport } from 'hooks/useViewport'
|
||||
import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal'
|
||||
// import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts'
|
||||
// import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
// import { PRIVACY_MODE } from 'utils/constants'
|
||||
import DepositWithdrawModal from '@components/modals/DepositWithdrawModal'
|
||||
import {
|
||||
ArrowDownRightIcon,
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpLeftIcon,
|
||||
ArrowUpTrayIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
|
||||
export const handleCopyAddress = (
|
||||
mangoAccountAddress: string,
|
||||
|
@ -42,28 +26,24 @@ export const handleCopyAddress = (
|
|||
})
|
||||
}
|
||||
|
||||
type ActionType = 'deposit' | 'withdraw' | 'borrow' | 'repay'
|
||||
|
||||
const ACTION_BUTTON_CLASSES =
|
||||
'flex h-10 items-center justify-center space-x-1 sm:space-x-2 font-bold focus:outline-none md:hover:bg-th-bkg-2 text-xs sm:text-sm'
|
||||
|
||||
const ACTION_BUTTON_ICON_CLASSES = 'h-3.5 w-3.5 sm:h-5 sm:w-5'
|
||||
|
||||
const AccountActions = () => {
|
||||
const { t } = useTranslation(['common', 'close-account', 'settings'])
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
|
||||
const [showEditAccountModal, setShowEditAccountModal] = useState(false)
|
||||
const [showBorrowModal, setShowBorrowModal] = useState(false)
|
||||
const [showRepayModal, setShowRepayModal] = useState(false)
|
||||
const [showDelegateModal, setShowDelegateModal] = useState(false)
|
||||
const [modalToShow, setModalToShow] = useState('')
|
||||
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
||||
const [showAccountSizeModal, setShowAccountSizeModal] = useState(false)
|
||||
// const [privacyMode, setPrivacyMode] = useLocalStorageState(PRIVACY_MODE)
|
||||
const { connected } = useWallet()
|
||||
const {
|
||||
// isDelegatedAccount,
|
||||
isUnownedAccount,
|
||||
} = useUnownedAccount()
|
||||
// const { isMobile } = useViewport()
|
||||
// const { isAccountFull } = useMangoAccountAccounts()
|
||||
const { isUnownedAccount } = useUnownedAccount()
|
||||
|
||||
const handleBorrowModal = () => {
|
||||
const handleActionModal = (type: ActionType) => {
|
||||
if (mangoAccountAddress || !connected) {
|
||||
setShowBorrowModal(true)
|
||||
setModalToShow(type)
|
||||
} else {
|
||||
setShowCreateAccountModal(true)
|
||||
}
|
||||
|
@ -72,161 +52,63 @@ const AccountActions = () => {
|
|||
return (
|
||||
<>
|
||||
{isUnownedAccount ? null : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
className="flex w-1/2 items-center justify-center md:w-auto"
|
||||
disabled={!mangoAccountAddress}
|
||||
onClick={() => setShowRepayModal(true)}
|
||||
secondary
|
||||
<div className="grid grid-cols-4">
|
||||
<button
|
||||
className={`${ACTION_BUTTON_CLASSES} border-r border-th-bkg-3`}
|
||||
onClick={() => handleActionModal('deposit')}
|
||||
>
|
||||
<ArrowDownRightIcon className="mr-2 h-5 w-5 flex-shrink-0" />
|
||||
{t('repay')}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex w-1/2 items-center justify-center md:w-auto"
|
||||
onClick={handleBorrowModal}
|
||||
secondary
|
||||
<ArrowDownTrayIcon className={ACTION_BUTTON_ICON_CLASSES} />
|
||||
<span>{t('deposit')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${ACTION_BUTTON_CLASSES} border-r border-th-bkg-3`}
|
||||
onClick={() => handleActionModal('withdraw')}
|
||||
>
|
||||
<ArrowUpLeftIcon className="mr-2 h-5 w-5 flex-shrink-0" />
|
||||
{t('borrow')}
|
||||
</Button>
|
||||
{/* <Popover className="relative sm:w-1/3 md:w-auto">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`w-full focus:outline-none`}
|
||||
as="div"
|
||||
>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center"
|
||||
secondary
|
||||
>
|
||||
<WrenchIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
{t('actions')}
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton size="medium">
|
||||
<WrenchIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-in duration-75"
|
||||
enterFrom="opacity-0 nice scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-out duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel className="absolute right-0 top-10 mt-1 space-y-2 rounded-md bg-th-bkg-2 px-4 py-2.5">
|
||||
<ActionsLinkButton
|
||||
onClick={() =>
|
||||
handleCopyAddress(
|
||||
mangoAccountAddress,
|
||||
t('copy-address-success', {
|
||||
pk: `${mangoAccountAddress.slice(
|
||||
0,
|
||||
4,
|
||||
)}...${mangoAccountAddress.slice(-4)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<DocumentDuplicateIcon className="h-4 w-4" />
|
||||
<span className="ml-2">{t('copy-address')}</span>
|
||||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
onClick={() => setShowEditAccountModal(true)}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span className="ml-2">{t('edit-account')}</span>
|
||||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount && isUnownedAccount}
|
||||
onClick={() => setShowDelegateModal(true)}
|
||||
>
|
||||
<UserPlusIcon className="h-4 w-4" />
|
||||
<span className="ml-2">{t('delegate-account')}</span>
|
||||
</ActionsLinkButton>
|
||||
{!isAccountFull ? (
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
onClick={() => setShowAccountSizeModal(true)}
|
||||
>
|
||||
<SquaresPlusIcon className="h-4 w-4" />
|
||||
<span className="ml-2">
|
||||
{t('settings:increase-account-slots')}
|
||||
</span>
|
||||
</ActionsLinkButton>
|
||||
) : null}
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
onClick={() => setShowCloseAccountModal(true)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span className="ml-2">{t('close-account')}</span>
|
||||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
onClick={() => setPrivacyMode(!privacyMode)}
|
||||
>
|
||||
{privacyMode ? (
|
||||
<>
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
<span className="ml-2">
|
||||
{t('settings:privacy-disable')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EyeSlashIcon className="h-4 w-4" />
|
||||
<span className="ml-2">
|
||||
{t('settings:privacy-enable')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</ActionsLinkButton>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover> */}
|
||||
<ArrowUpTrayIcon className={ACTION_BUTTON_ICON_CLASSES} />
|
||||
<span>{t('withdraw')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`${ACTION_BUTTON_CLASSES} border-r border-th-bkg-3`}
|
||||
onClick={() => handleActionModal('borrow')}
|
||||
>
|
||||
<ArrowUpLeftIcon className={ACTION_BUTTON_ICON_CLASSES} />
|
||||
<span>{t('borrow')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={ACTION_BUTTON_CLASSES}
|
||||
onClick={() => handleActionModal('repay')}
|
||||
>
|
||||
<ArrowDownRightIcon className={ACTION_BUTTON_ICON_CLASSES} />
|
||||
<span>{t('repay')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showCloseAccountModal ? (
|
||||
<CloseAccountModal
|
||||
isOpen={showCloseAccountModal}
|
||||
onClose={() => setShowCloseAccountModal(false)}
|
||||
{modalToShow === 'deposit' ? (
|
||||
<DepositWithdrawModal
|
||||
action="deposit"
|
||||
isOpen={modalToShow === 'deposit'}
|
||||
onClose={() => setModalToShow('')}
|
||||
/>
|
||||
) : null}
|
||||
{showEditAccountModal ? (
|
||||
<AccountNameModal
|
||||
isOpen={showEditAccountModal}
|
||||
onClose={() => setShowEditAccountModal(false)}
|
||||
{modalToShow === 'withdraw' ? (
|
||||
<DepositWithdrawModal
|
||||
action="withdraw"
|
||||
isOpen={modalToShow === 'withdraw'}
|
||||
onClose={() => setModalToShow('')}
|
||||
/>
|
||||
) : null}
|
||||
{showBorrowModal ? (
|
||||
{modalToShow === 'borrow' ? (
|
||||
<BorrowRepayModal
|
||||
action="borrow"
|
||||
isOpen={showBorrowModal}
|
||||
onClose={() => setShowBorrowModal(false)}
|
||||
isOpen={modalToShow === 'borrow'}
|
||||
onClose={() => setModalToShow('')}
|
||||
/>
|
||||
) : null}
|
||||
{showRepayModal ? (
|
||||
{modalToShow === 'repay' ? (
|
||||
<BorrowRepayModal
|
||||
action="repay"
|
||||
isOpen={showRepayModal}
|
||||
onClose={() => setShowRepayModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
{showDelegateModal ? (
|
||||
<DelegateModal
|
||||
isOpen={showDelegateModal}
|
||||
onClose={() => setShowDelegateModal(false)}
|
||||
isOpen={modalToShow === 'repay'}
|
||||
onClose={() => setModalToShow('')}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateAccountModal ? (
|
||||
|
@ -235,12 +117,6 @@ const AccountActions = () => {
|
|||
onClose={() => setShowCreateAccountModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
{showAccountSizeModal ? (
|
||||
<MangoAccountSizeModal
|
||||
isOpen={showAccountSizeModal}
|
||||
onClose={() => setShowAccountSizeModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { formatYAxis } from 'utils/formatting'
|
||||
import { HourlyFundingChartData, PerformanceDataItem } from 'types'
|
||||
import { ContentType } from 'recharts/types/component/Tooltip'
|
||||
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const CHART_TABS: ViewToShow[] = [
|
||||
'account-value',
|
||||
'pnl',
|
||||
'cumulative-interest-value',
|
||||
]
|
||||
|
||||
const AccountChart = ({
|
||||
chartName,
|
||||
handleViewChange,
|
||||
customTooltip,
|
||||
data,
|
||||
hideChart,
|
||||
loading,
|
||||
yDecimals,
|
||||
yKey,
|
||||
}: {
|
||||
chartName: ViewToShow
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
customTooltip?: ContentType<number, string>
|
||||
data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined
|
||||
hideChart: () => void
|
||||
loading?: boolean
|
||||
yDecimals?: number
|
||||
yKey: string
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const [daysToShow, setDaysToShow] = useState<string>('1')
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data || !data.length) return []
|
||||
if (chartName === 'cumulative-interest-value') {
|
||||
return data.map((d) => ({
|
||||
interest_value:
|
||||
d.borrow_interest_cumulative_usd * -1 +
|
||||
d.deposit_interest_cumulative_usd,
|
||||
time: d.time,
|
||||
}))
|
||||
}
|
||||
return data
|
||||
}, [data, chartName])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hide-scroll mb-3 flex h-14 items-center space-x-4 overflow-x-auto border-b border-th-bkg-3">
|
||||
<button
|
||||
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={hideChart}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex space-x-2">
|
||||
{CHART_TABS.map((tab) => (
|
||||
<button
|
||||
className={`whitespace-nowrap rounded-md px-2.5 py-1.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
|
||||
chartName === tab
|
||||
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
|
||||
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
|
||||
}`}
|
||||
onClick={() => handleViewChange(tab)}
|
||||
key={tab}
|
||||
>
|
||||
{t(tab)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 pt-4">
|
||||
{chartData.length ? (
|
||||
<DetailedAreaOrBarChart
|
||||
customTooltip={customTooltip}
|
||||
data={chartData}
|
||||
daysToShow={daysToShow}
|
||||
heightClass="h-[calc(100vh-240px)]"
|
||||
loaderHeightClass="h-[calc(100vh-166px)]"
|
||||
loading={loading}
|
||||
prefix="$"
|
||||
setDaysToShow={setDaysToShow}
|
||||
tickFormat={(x) => `$${formatYAxis(x)}`}
|
||||
xKey="time"
|
||||
yDecimals={yDecimals}
|
||||
yKey={yKey}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('account:no-data')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountChart
|
|
@ -1,11 +1,8 @@
|
|||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { handleViewChange } from './AccountPage'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import {
|
||||
HealthType,
|
||||
|
@ -14,73 +11,23 @@ import {
|
|||
import HealthBar from './HealthBar'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import { IconButton } from '@components/shared/Button'
|
||||
import { CalendarIcon, ChartBarIcon } from '@heroicons/react/20/solid'
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChartBarIcon,
|
||||
ChevronRightIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import Change from '@components/shared/Change'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats'
|
||||
import PnlHistoryModal from '@components/modals/PnlHistoryModal'
|
||||
|
||||
const AccountHeroStats = ({
|
||||
accountPnl,
|
||||
accountValue,
|
||||
rollingDailyData,
|
||||
setShowPnlHistory,
|
||||
handleViewChange,
|
||||
}: {
|
||||
accountPnl: number
|
||||
accountValue: number
|
||||
rollingDailyData: PerformanceDataItem[]
|
||||
setShowPnlHistory: (show: boolean) => void
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const AccountHeroStats = ({ accountValue }: { accountValue: number }) => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const { hourlyVolumeData, loadingHourlyVolume } =
|
||||
useAccountHourlyVolumeStats()
|
||||
|
||||
const totalInterestData = mangoStore(
|
||||
(s) => s.mangoAccount.interestTotals.data,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (mangoAccountAddress) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchAccountInterestTotals(mangoAccountAddress)
|
||||
}
|
||||
}, [mangoAccountAddress])
|
||||
|
||||
const {
|
||||
data: fundingData,
|
||||
isLoading: loadingFunding,
|
||||
isFetching: fetchingFunding,
|
||||
} = useQuery(
|
||||
['funding', mangoAccountAddress],
|
||||
() => fetchFundingTotals(mangoAccountAddress),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!mangoAccountAddress,
|
||||
},
|
||||
)
|
||||
|
||||
const {
|
||||
data: volumeTotalData,
|
||||
isLoading: loadingVolumeTotalData,
|
||||
isFetching: fetchingVolumeTotalData,
|
||||
} = useQuery(
|
||||
['total-volume', mangoAccountAddress],
|
||||
() => fetchVolumeTotals(mangoAccountAddress),
|
||||
{
|
||||
cacheTime: 1000 * 60 * 10,
|
||||
staleTime: 1000 * 60,
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!mangoAccountAddress,
|
||||
},
|
||||
)
|
||||
const { rollingDailyData } = useAccountPerformanceData()
|
||||
const router = useRouter()
|
||||
const [showPnlHistory, setShowPnlHistory] = useState(false)
|
||||
|
||||
const maintHealth = useMemo(() => {
|
||||
return group && mangoAccount
|
||||
|
@ -101,397 +48,217 @@ const AccountHeroStats = ({
|
|||
}
|
||||
}, [mangoAccount, group, accountValue])
|
||||
|
||||
const accountPnl = useMemo(() => {
|
||||
if (!group || !mangoAccount) return 0
|
||||
return toUiDecimalsForQuote(mangoAccount.getPnl(group).toNumber())
|
||||
}, [group, mangoAccount])
|
||||
|
||||
const pnlChangeToday = useMemo(() => {
|
||||
if (!accountPnl || !rollingDailyData.length) return 0
|
||||
const startHour = rollingDailyData.find((item) => {
|
||||
const itemHour = new Date(item.time).getHours()
|
||||
return itemHour === 0
|
||||
})
|
||||
const startDayPnl = startHour?.pnl
|
||||
const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0
|
||||
|
||||
return pnlChangeToday
|
||||
}, [accountPnl, rollingDailyData])
|
||||
|
||||
const rollingDailyPnlChange = useMemo(() => {
|
||||
if (!accountPnl || !rollingDailyData.length) return 0
|
||||
return accountPnl - rollingDailyData[0].pnl
|
||||
}, [accountPnl, rollingDailyData])
|
||||
|
||||
const interestTotalValue = useMemo(() => {
|
||||
if (totalInterestData.length) {
|
||||
return totalInterestData.reduce(
|
||||
(a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd),
|
||||
0,
|
||||
)
|
||||
}
|
||||
return 0.0
|
||||
}, [totalInterestData])
|
||||
|
||||
const fundingTotalValue = useMemo(() => {
|
||||
if (fundingData?.length && mangoAccountAddress) {
|
||||
return fundingData.reduce(
|
||||
(a, c) => a + c.long_funding + c.short_funding,
|
||||
0,
|
||||
)
|
||||
}
|
||||
return 0.0
|
||||
}, [fundingData, mangoAccountAddress])
|
||||
|
||||
const oneDayInterestChange = useMemo(() => {
|
||||
if (rollingDailyData.length) {
|
||||
const first = rollingDailyData[0]
|
||||
const latest = rollingDailyData[rollingDailyData.length - 1]
|
||||
|
||||
const startDayInterest =
|
||||
first.borrow_interest_cumulative_usd +
|
||||
first.deposit_interest_cumulative_usd
|
||||
|
||||
const endDayInterest =
|
||||
latest.borrow_interest_cumulative_usd +
|
||||
latest.deposit_interest_cumulative_usd
|
||||
|
||||
return endDayInterest - startDayInterest
|
||||
}
|
||||
return 0.0
|
||||
}, [rollingDailyData])
|
||||
|
||||
const dailyVolume = useMemo(() => {
|
||||
if (!hourlyVolumeData || !hourlyVolumeData.length) return 0
|
||||
// Calculate the current time in milliseconds
|
||||
const currentTime = new Date().getTime()
|
||||
|
||||
// Calculate the start time for the last 24 hours in milliseconds
|
||||
const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000
|
||||
|
||||
// Filter the formatted data based on the timestamp
|
||||
const last24HoursData = hourlyVolumeData.filter((entry) => {
|
||||
const timestampMs = new Date(entry.time).getTime()
|
||||
return timestampMs >= last24HoursStartTime && timestampMs <= currentTime
|
||||
})
|
||||
|
||||
const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0)
|
||||
return volume
|
||||
}, [hourlyVolumeData])
|
||||
|
||||
const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-6 border-b border-th-bkg-3">
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-t-0 2xl:col-span-1">
|
||||
<div id="account-step-four">
|
||||
<div className="flex justify-between">
|
||||
<Tooltip
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
content={
|
||||
<div className="flex-col space-y-2 text-sm">
|
||||
<p className="text-xs">
|
||||
Health describes how close your account is to liquidation.
|
||||
The lower your account health is the more likely you are
|
||||
to get liquidated when prices fluctuate.
|
||||
</p>
|
||||
{maintHealth < 100 && mangoAccountAddress ? (
|
||||
<>
|
||||
<p className="text-xs font-bold text-th-fgd-1">
|
||||
Your account health is {maintHealth}%
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the prices of all your liabilities increase by{' '}
|
||||
{maintHealth}%, even for just a moment, some of your
|
||||
liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the value of your total collateral decreases by{' '}
|
||||
{(
|
||||
(1 - 1 / ((maintHealth || 0) / 100 + 1)) *
|
||||
100
|
||||
).toFixed(2)}
|
||||
% , some of your liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
These are examples. A combination of events can also
|
||||
lead to liquidation.
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="tooltip-underline">{t('health')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:health-contributions')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleViewChange('health-contributions')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-0.5 mt-1 flex items-center space-x-3">
|
||||
<p className="text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
{maintHealth}%
|
||||
</p>
|
||||
<HealthBar health={maintHealth} />
|
||||
</div>
|
||||
<span className="flex text-xs font-normal text-th-fgd-4">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-leverage')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<span className="tooltip-underline">{t('leverage')}</span>:
|
||||
</Tooltip>
|
||||
<span className="ml-1 font-mono text-th-fgd-2">
|
||||
<FormatNumericValue value={leverage} decimals={2} roundUp />x
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-3 md:border-l lg:col-span-2 lg:border-t-0 2xl:col-span-1">
|
||||
<div id="account-step-five">
|
||||
<div className="border-b border-th-bkg-3 px-4 pb-4 pt-3 md:px-6">
|
||||
<div id="account-step-four">
|
||||
<div className="flex justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-free-collateral')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline">{t('free-collateral')}</p>
|
||||
</Tooltip>
|
||||
<p className="mb-0.5 mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue
|
||||
value={
|
||||
group && mangoAccount
|
||||
? toUiDecimalsForQuote(
|
||||
mangoAccount.getCollateralValue(group),
|
||||
)
|
||||
: 0
|
||||
}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</p>
|
||||
<span className="text-xs font-normal text-th-fgd-4">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-total-collateral')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<span className="tooltip-underline">{t('total')}</span>:
|
||||
<span className="ml-1 font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={
|
||||
group && mangoAccount
|
||||
? toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsValue(group, HealthType.init),
|
||||
)
|
||||
: 0
|
||||
}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 lg:col-span-2 lg:border-l lg:border-t-0 2xl:col-span-1">
|
||||
<div
|
||||
id="account-step-seven"
|
||||
className="flex w-full flex-col items-start"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-pnl')}
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline">{t('pnl')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:pnl-chart')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleViewChange('pnl')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:pnl-history')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => setShowPnlHistory(true)}
|
||||
>
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
content={
|
||||
<div className="flex-col space-y-2 text-sm">
|
||||
<p className="text-xs">
|
||||
Health describes how close your account is to liquidation.
|
||||
The lower your account health is the more likely you are to
|
||||
get liquidated when prices fluctuate.
|
||||
</p>
|
||||
{maintHealth < 100 && mangoAccountAddress ? (
|
||||
<>
|
||||
<p className="text-xs font-bold text-th-fgd-1">
|
||||
Your account health is {maintHealth}%
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the prices of all your liabilities increase by{' '}
|
||||
{maintHealth}%, even for just a moment, some of your
|
||||
liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<span className="font-bold text-th-fgd-1">
|
||||
Scenario:
|
||||
</span>{' '}
|
||||
If the value of your total collateral decreases by{' '}
|
||||
{(
|
||||
(1 - 1 / ((maintHealth || 0) / 100 + 1)) *
|
||||
100
|
||||
).toFixed(2)}
|
||||
% , some of your liabilities will be liquidated.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
These are examples. A combination of events can also
|
||||
lead to liquidation.
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mb-0.5 mt-1 text-left text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue
|
||||
value={accountPnl}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex space-x-1.5">
|
||||
<Change change={rollingDailyPnlChange} prefix="$" size="small" />
|
||||
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 md:col-span-3 md:border-l lg:col-span-2 lg:border-l-0 2xl:col-span-1 2xl:border-l 2xl:border-t-0">
|
||||
<div id="account-step-six">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p>{t('account:lifetime-volume')}</p>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:volume-chart')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleViewChange('hourly-volume')}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
{loadingTotalVolume && mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-1">
|
||||
<div className="h-7 w-16 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
) : (
|
||||
<p className="mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue value={volumeTotalData || 0} isUsd />
|
||||
</p>
|
||||
)}
|
||||
<span className="flex items-center text-xs font-normal text-th-fgd-4">
|
||||
<span>{t('account:daily-volume')}</span>:
|
||||
{loadingHourlyVolume && mangoAccountAddress ? (
|
||||
<SheenLoader className="ml-1">
|
||||
<div className="h-3.5 w-10 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
) : (
|
||||
<span className="ml-1 font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={dailyVolume}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 lg:col-span-2 lg:border-l 2xl:col-span-1 2xl:border-t-0">
|
||||
<div id="account-step-eight">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-total-interest')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline">
|
||||
{t('total-interest-earned')}
|
||||
</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:cumulative-interest-chart')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() =>
|
||||
handleViewChange('cumulative-interest-value')
|
||||
}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mb-0.5 mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue
|
||||
value={interestTotalValue}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</p>
|
||||
<div className="flex space-x-1.5">
|
||||
<Change change={oneDayInterestChange} prefix="$" size="small" />
|
||||
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 border-t border-th-bkg-3 py-3 pl-6 pr-4 text-left md:col-span-3 md:border-l lg:col-span-2 2xl:col-span-1 2xl:border-t-0">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-total-funding')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
}
|
||||
>
|
||||
<p className="tooltip-underline">
|
||||
{t('account:total-funding-earned')}
|
||||
</p>
|
||||
<p className="tooltip-underline">{t('health')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:funding-chart')}
|
||||
content={t('account:health-contributions')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleViewChange('hourly-funding')}
|
||||
onClick={() =>
|
||||
handleViewChange('health-contributions', router)
|
||||
}
|
||||
>
|
||||
<ChartBarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
{(loadingFunding || fetchingFunding) && mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-2">
|
||||
<div className="h-7 w-16 bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
) : (
|
||||
<p className="mb-0.5 mt-1 text-2xl font-bold text-th-fgd-1 lg:text-xl xl:text-2xl">
|
||||
<FormatNumericValue
|
||||
value={fundingTotalValue}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
<div className="mb-0.5 mt-2 flex items-center space-x-3">
|
||||
<p className="text-2xl font-bold text-th-fgd-1 lg:text-3xl">
|
||||
{maintHealth}%
|
||||
</p>
|
||||
)}
|
||||
<HealthBar health={maintHealth} />
|
||||
</div>
|
||||
<span className="flex font-normal text-th-fgd-4">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-leverage')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<span className="tooltip-underline">{t('leverage')}</span>:
|
||||
</Tooltip>
|
||||
<span className="ml-1 font-mono text-th-fgd-2">
|
||||
<FormatNumericValue value={leverage} decimals={2} roundUp />x
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex border-b border-th-bkg-3 px-4 pb-4 pt-3 md:px-6">
|
||||
<div id="account-step-five">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-free-collateral')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline">{t('free-collateral')}</p>
|
||||
</Tooltip>
|
||||
<p className="mb-0.5 mt-2 text-2xl font-bold text-th-fgd-1 lg:text-3xl">
|
||||
<FormatNumericValue
|
||||
value={
|
||||
group && mangoAccount
|
||||
? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group))
|
||||
: 0
|
||||
}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</p>
|
||||
<span className="font-normal text-th-fgd-4">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-total-collateral')}
|
||||
maxWidth="20rem"
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<span className="tooltip-underline">{t('total')}</span>:
|
||||
<span className="ml-1 font-mono text-th-fgd-2">
|
||||
<FormatNumericValue
|
||||
value={
|
||||
group && mangoAccount
|
||||
? toUiDecimalsForQuote(
|
||||
mangoAccount.getAssetsValue(group, HealthType.init),
|
||||
)
|
||||
: 0
|
||||
}
|
||||
decimals={2}
|
||||
isUsd={true}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="account-step-seven"
|
||||
className="border-b border-th-bkg-3 px-4 pb-4 pt-3 md:px-6"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Tooltip
|
||||
content={t('account:tooltip-pnl')}
|
||||
placement="top-start"
|
||||
delay={100}
|
||||
>
|
||||
<p className="tooltip-underline">{t('pnl')}</p>
|
||||
</Tooltip>
|
||||
{mangoAccountAddress ? (
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
content={t('account:pnl-history')}
|
||||
delay={100}
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => setShowPnlHistory(true)}
|
||||
>
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mb-0.5 mt-2 text-2xl font-bold text-th-fgd-1 lg:text-3xl">
|
||||
<FormatNumericValue value={accountPnl} decimals={2} isUsd={true} />
|
||||
</p>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<Change change={rollingDailyPnlChange} prefix="$" />
|
||||
<p className="text-th-fgd-4">{t('rolling-change')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="default-transition flex h-10 w-full items-center justify-between px-4 focus:outline-none disabled:cursor-not-allowed md:px-6 md:hover:bg-th-bkg-2"
|
||||
onClick={() =>
|
||||
router.push('/?view=account-stats', undefined, { shallow: true })
|
||||
}
|
||||
disabled={!mangoAccountAddress}
|
||||
>
|
||||
<p>{t('account:more-account-stats')}</p>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</button>
|
||||
{showPnlHistory ? (
|
||||
<PnlHistoryModal
|
||||
isOpen={showPnlHistory}
|
||||
onClose={() => setShowPnlHistory(false)}
|
||||
pnlChangeToday={pnlChangeToday}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import SwapOrders from '@components/swap/SwapTriggerOrders'
|
||||
import OpenOrders from '@components/trade/OpenOrders'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const AccountOrders = () => {
|
||||
const { t } = useTranslation('trade')
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
|
||||
const [activeTab, setActiveTab] = useState('trade:limit')
|
||||
|
||||
const tabsWithCount: [string, number][] = useMemo(() => {
|
||||
const stopOrdersCount =
|
||||
mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData)
|
||||
?.length || 0
|
||||
const tabs: [string, number][] = [
|
||||
['trade:limit', Object.values(openOrders).flat().length],
|
||||
['trade:trigger-orders', stopOrdersCount],
|
||||
]
|
||||
return tabs
|
||||
}, [mangoAccount, openOrders])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex space-x-6 px-4 py-4 md:px-6">
|
||||
{tabsWithCount.map((tab) => (
|
||||
<button
|
||||
className={`flex items-center space-x-2 text-base font-bold focus:outline-none ${
|
||||
activeTab === tab[0] ? 'text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab[0])}
|
||||
key={tab[0]}
|
||||
>
|
||||
<span>{t(tab[0])}</span>
|
||||
<div className="rounded-md bg-th-bkg-3 px-1.5 py-0.5 font-body text-xs font-medium text-th-fgd-2">
|
||||
<span>{tab[1]}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col border-t border-th-bkg-3">
|
||||
{activeTab === 'trade:limit' ? <OpenOrders /> : <SwapOrders />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountOrders
|
|
@ -0,0 +1,112 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import AccountActions from './AccountActions'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import dayjs from 'dayjs'
|
||||
import AccountHeroStats from './AccountHeroStats'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Explore from '@components/explore/Explore'
|
||||
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
||||
import { formatYAxis } from 'utils/formatting'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
|
||||
import CreateAccountModal from '@components/modals/CreateAccountModal'
|
||||
import { FaceSmileIcon } from '@heroicons/react/20/solid'
|
||||
import Button from '@components/shared/Button'
|
||||
|
||||
const EMPTY_STATE_WRAPPER_CLASSES =
|
||||
'flex h-[180px] flex-col justify-center pb-4 md:h-full'
|
||||
|
||||
const AccountOverview = () => {
|
||||
const { t } = useTranslation(['common', 'governance'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount, initialLoad } = useMangoAccount()
|
||||
const { connected } = useWallet()
|
||||
const { performanceData, rollingDailyData, loadingPerformanceData } =
|
||||
useAccountPerformanceData()
|
||||
const [daysToShow, setDaysToShow] = useState('1')
|
||||
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
||||
|
||||
const accountValue = useMemo(() => {
|
||||
if (!group || !mangoAccount) return 0
|
||||
return toUiDecimalsForQuote(mangoAccount.getEquity(group).toNumber())
|
||||
}, [group, mangoAccount])
|
||||
|
||||
const latestAccountData = useMemo(() => {
|
||||
if (!accountValue || !performanceData || !performanceData.length) return []
|
||||
const latestDataItem = performanceData[performanceData.length - 1]
|
||||
return [
|
||||
{
|
||||
account_equity: accountValue,
|
||||
time: dayjs(Date.now()).toISOString(),
|
||||
borrow_interest_cumulative_usd:
|
||||
latestDataItem.borrow_interest_cumulative_usd,
|
||||
deposit_interest_cumulative_usd:
|
||||
latestDataItem.deposit_interest_cumulative_usd,
|
||||
pnl: latestDataItem.pnl,
|
||||
spot_value: latestDataItem.spot_value,
|
||||
transfer_balance: latestDataItem.transfer_balance,
|
||||
},
|
||||
]
|
||||
}, [accountValue, performanceData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 border-b border-th-bkg-3">
|
||||
<div className="col-span-12 border-b border-th-bkg-3 pt-4 md:col-span-8 md:border-b-0 md:border-r">
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
{mangoAccount || (connected && initialLoad) ? (
|
||||
<div className="px-4 pb-4 md:px-6">
|
||||
<DetailedAreaOrBarChart
|
||||
data={rollingDailyData.concat(latestAccountData)}
|
||||
daysToShow={daysToShow}
|
||||
setDaysToShow={setDaysToShow}
|
||||
loading={loadingPerformanceData || initialLoad}
|
||||
heightClass="h-[180px] lg:h-[205px]"
|
||||
hideAxis
|
||||
loaderHeightClass="h-[290px] lg:h-[315px]"
|
||||
prefix="$"
|
||||
tickFormat={(x) => `$${formatYAxis(x)}`}
|
||||
title={t('account-value')}
|
||||
xKey="time"
|
||||
yKey="account_equity"
|
||||
/>
|
||||
</div>
|
||||
) : connected ? (
|
||||
<div className={EMPTY_STATE_WRAPPER_CLASSES}>
|
||||
<div className="flex flex-col items-center">
|
||||
<FaceSmileIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p className="mb-4">Create a Mango Account to get started</p>
|
||||
<Button onClick={() => setShowCreateAccountModal(true)}>
|
||||
{t('create-account')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={EMPTY_STATE_WRAPPER_CLASSES}>
|
||||
<ConnectEmptyState text={t('governance:connect-wallet')} />
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-th-bkg-3">
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-4">
|
||||
<AccountHeroStats accountValue={accountValue} />
|
||||
</div>
|
||||
</div>
|
||||
<Explore />
|
||||
{showCreateAccountModal ? (
|
||||
<CreateAccountModal
|
||||
isOpen={showCreateAccountModal}
|
||||
onClose={() => setShowCreateAccountModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountOverview
|
|
@ -1,30 +1,15 @@
|
|||
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import AccountActions from './AccountActions'
|
||||
import { useCallback } from 'react'
|
||||
import AccountTabs from './AccountTabs'
|
||||
import AccountChart from './AccountChart'
|
||||
import useMangoAccount from '../../hooks/useMangoAccount'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import dayjs from 'dayjs'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import PnlHistoryModal from '@components/modals/PnlHistoryModal'
|
||||
import AssetsLiabilities from './AssetsLiabilities'
|
||||
import FundingChart from './FundingChart'
|
||||
import VolumeChart from './VolumeChart'
|
||||
import AccountHeroStats from './AccountHeroStats'
|
||||
import AccountValue from './AccountValue'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import HealthContributions from './HealthContributions'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
|
||||
const TABS = ['account-value', 'account:assets-liabilities']
|
||||
import AccountStats from './AccountStats'
|
||||
import PerpStatsPage from '@components/stats/perps/PerpStatsPage'
|
||||
import TokenPage from '@components/token/TokenPage'
|
||||
|
||||
export type ViewToShow =
|
||||
| ''
|
||||
| 'account-stats'
|
||||
| 'account-value'
|
||||
| 'cumulative-interest-value'
|
||||
| 'pnl'
|
||||
|
@ -32,147 +17,32 @@ export type ViewToShow =
|
|||
| 'hourly-volume'
|
||||
| 'health-contributions'
|
||||
|
||||
export const handleViewChange = (view: ViewToShow, router: NextRouter) => {
|
||||
const query = { ...router.query, ['view']: view }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
const AccountPage = () => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
|
||||
const { isTablet } = useViewport()
|
||||
const [activeTab, setActiveTab] = useLocalStorageState(
|
||||
'accountHeroKey-0.1',
|
||||
'account-value',
|
||||
)
|
||||
const { performanceData, rollingDailyData } = useAccountPerformanceData()
|
||||
const router = useRouter()
|
||||
const { view } = router.query
|
||||
const { market, token, view } = router.query
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
(view: ViewToShow) => {
|
||||
const query = { ...router.query, ['view']: view }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
},
|
||||
[router],
|
||||
)
|
||||
|
||||
const handleCloseDailyPnlModal = () => {
|
||||
setShowPnlHistory(false)
|
||||
}
|
||||
|
||||
const [accountPnl, accountValue] = useMemo(() => {
|
||||
if (!group || !mangoAccount) return [0, 0]
|
||||
return [
|
||||
toUiDecimalsForQuote(mangoAccount.getPnl(group).toNumber()),
|
||||
toUiDecimalsForQuote(mangoAccount.getEquity(group).toNumber()),
|
||||
]
|
||||
}, [group, mangoAccount])
|
||||
|
||||
const pnlChangeToday = useMemo(() => {
|
||||
if (!accountPnl || !rollingDailyData.length) return 0
|
||||
const startHour = rollingDailyData.find((item) => {
|
||||
const itemHour = new Date(item.time).getHours()
|
||||
return itemHour === 0
|
||||
})
|
||||
const startDayPnl = startHour?.pnl
|
||||
const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0
|
||||
|
||||
return pnlChangeToday
|
||||
}, [accountPnl, rollingDailyData])
|
||||
|
||||
const latestAccountData = useMemo(() => {
|
||||
if (!accountValue || !performanceData || !performanceData.length) return []
|
||||
const latestDataItem = performanceData[performanceData.length - 1]
|
||||
return [
|
||||
{
|
||||
account_equity: accountValue,
|
||||
time: dayjs(Date.now()).toISOString(),
|
||||
borrow_interest_cumulative_usd:
|
||||
latestDataItem.borrow_interest_cumulative_usd,
|
||||
deposit_interest_cumulative_usd:
|
||||
latestDataItem.deposit_interest_cumulative_usd,
|
||||
pnl: accountPnl,
|
||||
spot_value: latestDataItem.spot_value,
|
||||
transfer_balance: latestDataItem.transfer_balance,
|
||||
},
|
||||
]
|
||||
}, [accountPnl, accountValue, performanceData])
|
||||
|
||||
return !view ? (
|
||||
<>
|
||||
<div className="flex flex-col border-b-0 border-th-bkg-3 px-6 py-4 lg:flex-row lg:items-center lg:justify-between lg:border-b">
|
||||
<div>
|
||||
<div className="hide-scroll flex justify-center space-x-2 md:justify-start">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
className={`rounded-md px-2.5 py-1.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
|
||||
activeTab === tab
|
||||
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
|
||||
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
key={tab}
|
||||
>
|
||||
{t(tab)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="md:h-24">
|
||||
{activeTab === 'account-value' ? (
|
||||
<AccountValue
|
||||
accountValue={accountValue}
|
||||
latestAccountData={latestAccountData}
|
||||
rollingDailyData={rollingDailyData}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'account:assets-liabilities' ? (
|
||||
<AssetsLiabilities isMobile={isTablet} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-1 mt-6 lg:mt-0">
|
||||
<AccountActions />
|
||||
</div>
|
||||
</div>
|
||||
<AccountHeroStats
|
||||
accountPnl={accountPnl}
|
||||
accountValue={accountValue}
|
||||
rollingDailyData={rollingDailyData}
|
||||
setShowPnlHistory={setShowPnlHistory}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
<AccountTabs />
|
||||
{showPnlHistory ? (
|
||||
<PnlHistoryModal
|
||||
pnlChangeToday={pnlChangeToday}
|
||||
isOpen={showPnlHistory}
|
||||
onClose={handleCloseDailyPnlModal}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
return market ? (
|
||||
<PerpStatsPage />
|
||||
) : token ? (
|
||||
<TokenPage />
|
||||
) : view ? (
|
||||
<AccountView view={view as ViewToShow} />
|
||||
) : (
|
||||
<AccountView
|
||||
view={view as ViewToShow}
|
||||
latestAccountData={latestAccountData}
|
||||
handleViewChange={handleViewChange}
|
||||
/>
|
||||
<AccountTabs />
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
|
||||
const AccountView = ({
|
||||
view,
|
||||
handleViewChange,
|
||||
latestAccountData,
|
||||
}: {
|
||||
view: ViewToShow
|
||||
latestAccountData: PerformanceDataItem[]
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const AccountView = ({ view }: { view: ViewToShow }) => {
|
||||
const router = useRouter()
|
||||
const { connected } = useWallet()
|
||||
const { address } = router.query
|
||||
const { performanceData } = useAccountPerformanceData()
|
||||
|
||||
const handleHideChart = useCallback(() => {
|
||||
if (address && !connected) {
|
||||
|
@ -183,40 +53,8 @@ const AccountView = ({
|
|||
}, [address, router, connected])
|
||||
|
||||
switch (view) {
|
||||
case 'account-value':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="account-value"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="account_equity"
|
||||
/>
|
||||
)
|
||||
case 'pnl':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="pnl"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="pnl"
|
||||
/>
|
||||
)
|
||||
case 'cumulative-interest-value':
|
||||
return (
|
||||
<AccountChart
|
||||
chartName="cumulative-interest-value"
|
||||
handleViewChange={handleViewChange}
|
||||
data={performanceData?.concat(latestAccountData)}
|
||||
hideChart={handleHideChart}
|
||||
yKey="interest_value"
|
||||
/>
|
||||
)
|
||||
case 'hourly-funding':
|
||||
return <FundingChart hideChart={handleHideChart} />
|
||||
case 'hourly-volume':
|
||||
return <VolumeChart hideChart={handleHideChart} />
|
||||
case 'account-stats':
|
||||
return <AccountStats hideView={handleHideChart} />
|
||||
case 'health-contributions':
|
||||
return <HealthContributions hideView={handleHideChart} />
|
||||
default:
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
|
||||
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatYAxis } from 'utils/formatting'
|
||||
import FundingChart from './FundingChart'
|
||||
import VolumeChart from './VolumeChart'
|
||||
|
||||
const AccountStats = ({ hideView }: { hideView: () => void }) => {
|
||||
const { t } = useTranslation(['common', 'account'])
|
||||
const { performanceData, loadingPerformanceData } =
|
||||
useAccountPerformanceData()
|
||||
const [pnlDaysToShow, setPnlDaysToShow] = useState('1')
|
||||
const [interestDaysToShow, setInterestDaysToShow] = useState('1')
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!performanceData || !performanceData.length) return []
|
||||
const chartData = []
|
||||
for (const item of performanceData) {
|
||||
const interest =
|
||||
item.borrow_interest_cumulative_usd * -1 +
|
||||
item.deposit_interest_cumulative_usd
|
||||
chartData.push({ ...item, interest_value: interest })
|
||||
}
|
||||
return chartData
|
||||
}, [performanceData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-14 items-center space-x-4 border-b border-th-bkg-3">
|
||||
<button
|
||||
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={hideView}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<h2 className="text-lg">{t('account:account-stats')}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="col-span-2 border-b border-th-bkg-3 px-6 py-4 md:col-span-1 md:border-r">
|
||||
<DetailedAreaOrBarChart
|
||||
data={chartData}
|
||||
daysToShow={pnlDaysToShow}
|
||||
setDaysToShow={setPnlDaysToShow}
|
||||
loading={loadingPerformanceData}
|
||||
heightClass="h-64"
|
||||
loaderHeightClass="h-[350px]"
|
||||
prefix="$"
|
||||
tickFormat={(x) => `$${formatYAxis(x)}`}
|
||||
title={t('pnl')}
|
||||
xKey="time"
|
||||
yKey="pnl"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 border-b border-th-bkg-3 px-6 py-4 md:col-span-1 md:pl-6">
|
||||
<DetailedAreaOrBarChart
|
||||
data={chartData}
|
||||
daysToShow={interestDaysToShow}
|
||||
setDaysToShow={setInterestDaysToShow}
|
||||
loading={loadingPerformanceData}
|
||||
heightClass="h-64"
|
||||
loaderHeightClass="h-[350px]"
|
||||
prefix="$"
|
||||
tickFormat={(x) => `$${formatYAxis(x)}`}
|
||||
title={t('cumulative-interest-value')}
|
||||
xKey="time"
|
||||
yKey="interest_value"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 h-[400px] border-b border-th-bkg-3 md:col-span-1 md:border-r">
|
||||
<FundingChart />
|
||||
</div>
|
||||
<div className="col-span-2 h-[400px] border-b border-th-bkg-3 md:col-span-1">
|
||||
<VolumeChart />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountStats
|
|
@ -8,14 +8,15 @@ import useUnsettledPerpPositions from 'hooks/useUnsettledPerpPositions'
|
|||
import mangoStore from '@store/mangoStore'
|
||||
import PerpPositions from '@components/trade/PerpPositions'
|
||||
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
|
||||
import OpenOrders from '@components/trade/OpenOrders'
|
||||
import HistoryTabs from './HistoryTabs'
|
||||
import ManualRefresh from '@components/shared/ManualRefresh'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import SwapTriggerOrders from '@components/swap/SwapTriggerOrders'
|
||||
import AccountOverview from './AccountOverview'
|
||||
import AccountOrders from './AccountOrders'
|
||||
|
||||
const AccountTabs = () => {
|
||||
const [activeTab, setActiveTab] = useState('balances')
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { isMobile, isTablet } = useViewport()
|
||||
const unsettledSpotBalances = useUnsettledSpotBalances()
|
||||
|
@ -28,17 +29,21 @@ const AccountTabs = () => {
|
|||
Object.values(unsettledSpotBalances).flat().length +
|
||||
unsettledPerpPositions?.length
|
||||
|
||||
const tabs: [string, number][] = [
|
||||
['balances', 0],
|
||||
['trade:positions', openPerpPositions.length],
|
||||
['trade:orders', Object.values(openOrders).flat().length],
|
||||
['trade:unsettled', unsettledTradeCount],
|
||||
['history', 0],
|
||||
]
|
||||
const stopOrdersCount =
|
||||
mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData)
|
||||
?.length || 0
|
||||
tabs.splice(3, 0, ['trade:trigger-orders', stopOrdersCount])
|
||||
|
||||
const tabs: [string, number][] = [
|
||||
['overview', 0],
|
||||
['balances', 0],
|
||||
['trade:positions', openPerpPositions.length],
|
||||
[
|
||||
'trade:orders',
|
||||
Object.values(openOrders).flat().length + stopOrdersCount,
|
||||
],
|
||||
['trade:unsettled', unsettledTradeCount],
|
||||
['history', 0],
|
||||
]
|
||||
return tabs
|
||||
}, [
|
||||
mangoAccount,
|
||||
|
@ -64,7 +69,9 @@ const AccountTabs = () => {
|
|||
size={isTablet ? 'large' : 'small'}
|
||||
/>
|
||||
</div>
|
||||
<TabContent activeTab={activeTab} />
|
||||
<div className="flex min-h-[calc(100vh-140px)] flex-col">
|
||||
<TabContent activeTab={activeTab} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -73,12 +80,14 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
|
|||
const unsettledSpotBalances = useUnsettledSpotBalances()
|
||||
const unsettledPerpPositions = useUnsettledPerpPositions()
|
||||
switch (activeTab) {
|
||||
case 'overview':
|
||||
return <AccountOverview />
|
||||
case 'balances':
|
||||
return <TokenList />
|
||||
case 'trade:positions':
|
||||
return <PerpPositions />
|
||||
case 'trade:orders':
|
||||
return <OpenOrders />
|
||||
return <AccountOrders />
|
||||
case 'trade:trigger-orders':
|
||||
return <SwapTriggerOrders />
|
||||
case 'trade:unsettled':
|
||||
|
@ -91,7 +100,7 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
|
|||
case 'history':
|
||||
return <HistoryTabs />
|
||||
default:
|
||||
return <TokenList />
|
||||
return <AccountOverview />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { formatCurrencyValue } from '../../utils/numbers'
|
||||
import FlipNumbers from 'react-flip-numbers'
|
||||
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
||||
import { COLORS } from '../../styles/colors'
|
||||
import { IconButton } from '../shared/Button'
|
||||
import { ArrowsPointingOutIcon } from '@heroicons/react/20/solid'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import SheenLoader from '../shared/SheenLoader'
|
||||
import Change from '../shared/Change'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
|
@ -14,35 +8,23 @@ import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettin
|
|||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { ViewToShow } from './AccountPage'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import useThemeWrapper from 'hooks/useThemeWrapper'
|
||||
|
||||
const AccountValue = ({
|
||||
accountValue,
|
||||
latestAccountData,
|
||||
rollingDailyData,
|
||||
handleViewChange,
|
||||
}: {
|
||||
accountValue: number
|
||||
latestAccountData: PerformanceDataItem[]
|
||||
rollingDailyData: PerformanceDataItem[]
|
||||
handleViewChange: (view: ViewToShow) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { theme } = useThemeWrapper()
|
||||
const { group } = useMangoGroup()
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const [animationSettings] = useLocalStorageState(
|
||||
ANIMATION_SETTINGS_KEY,
|
||||
INITIAL_ANIMATION_SETTINGS,
|
||||
)
|
||||
const { isTablet } = useViewport()
|
||||
const { performanceLoading: loading } = useAccountPerformanceData()
|
||||
|
||||
const accountValueChange = useMemo(() => {
|
||||
if (!accountValue || !rollingDailyData.length) return 0
|
||||
|
@ -50,24 +32,10 @@ const AccountValue = ({
|
|||
return accountValueChange
|
||||
}, [accountValue, rollingDailyData])
|
||||
|
||||
const onHoverMenu = (open: boolean, action: string) => {
|
||||
if (
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
setShowExpandChart(!open)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowAccountValueChart = () => {
|
||||
handleViewChange('account-value')
|
||||
setShowExpandChart(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row md:items-end md:space-x-6">
|
||||
<div className="mx-auto mt-4 md:mx-0">
|
||||
<div className="mb-2 flex justify-start font-display text-5xl text-th-fgd-1">
|
||||
<div className="mx-auto md:mx-0">
|
||||
<div className="mb-2 flex justify-start font-display text-5xl text-th-fgd-1 xl:text-6xl">
|
||||
{animationSettings['number-scroll'] ? (
|
||||
group && mangoAccount ? (
|
||||
<FlipNumbers
|
||||
|
@ -93,52 +61,10 @@ const AccountValue = ({
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-1.5 md:justify-start">
|
||||
<Change change={accountValueChange} prefix="$" />
|
||||
<p className="text-xs text-th-fgd-4">{t('rolling-change')}</p>
|
||||
<Change change={accountValueChange} prefix="$" size="large" />
|
||||
<p className="text-base text-th-fgd-4">{t('rolling-change')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!loading ? (
|
||||
rollingDailyData.length ? (
|
||||
<div
|
||||
className="relative mt-4 flex h-40 items-end md:mt-0 md:h-20 md:w-52 lg:w-60"
|
||||
onMouseEnter={() => onHoverMenu(showExpandChart, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave')}
|
||||
>
|
||||
<SimpleAreaChart
|
||||
color={
|
||||
accountValueChange >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]
|
||||
}
|
||||
data={rollingDailyData.concat(latestAccountData)}
|
||||
name="accountValue"
|
||||
xKey="time"
|
||||
yKey="account_equity"
|
||||
/>
|
||||
<Transition
|
||||
appear={true}
|
||||
className="absolute bottom-2 right-2"
|
||||
show={showExpandChart || isTablet}
|
||||
enter="transition ease-in duration-300"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleShowAccountValueChart()}
|
||||
>
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Transition>
|
||||
</div>
|
||||
) : null
|
||||
) : mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-4 flex flex-1 md:mt-0">
|
||||
<div className="h-40 w-full rounded-md bg-th-bkg-2 md:h-20 md:w-52 lg:w-60" />
|
||||
</SheenLoader>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
||||
import { COLORS } from '../../styles/colors'
|
||||
import { IconButton } from '../shared/Button'
|
||||
import { ArrowsPointingOutIcon } from '@heroicons/react/20/solid'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import SheenLoader from '../shared/SheenLoader'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { PerformanceDataItem } from 'types'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { handleViewChange } from './AccountPage'
|
||||
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
|
||||
import useThemeWrapper from 'hooks/useThemeWrapper'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const AccountValueChart = ({
|
||||
accountValue,
|
||||
latestAccountData,
|
||||
rollingDailyData,
|
||||
}: {
|
||||
accountValue: number
|
||||
latestAccountData: PerformanceDataItem[]
|
||||
rollingDailyData: PerformanceDataItem[]
|
||||
}) => {
|
||||
const { theme } = useThemeWrapper()
|
||||
const router = useRouter()
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const [showExpandChart, setShowExpandChart] = useState<boolean>(false)
|
||||
const { isTablet } = useViewport()
|
||||
const { performanceLoading: loading } = useAccountPerformanceData()
|
||||
|
||||
const accountValueChange = useMemo(() => {
|
||||
if (!accountValue || !rollingDailyData.length) return 0
|
||||
const accountValueChange = accountValue - rollingDailyData[0].account_equity
|
||||
return accountValueChange
|
||||
}, [accountValue, rollingDailyData])
|
||||
|
||||
const onHoverMenu = (open: boolean, action: string) => {
|
||||
if (
|
||||
(!open && action === 'onMouseEnter') ||
|
||||
(open && action === 'onMouseLeave')
|
||||
) {
|
||||
setShowExpandChart(!open)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowAccountValueChart = useCallback(() => {
|
||||
handleViewChange('account-value', router)
|
||||
setShowExpandChart(false)
|
||||
}, [router])
|
||||
|
||||
return !loading ? (
|
||||
rollingDailyData.length ? (
|
||||
<div
|
||||
className="relative mt-4 flex h-40 items-end md:mt-0 md:h-28 md:w-52 lg:w-56"
|
||||
onMouseEnter={() => onHoverMenu(showExpandChart, 'onMouseEnter')}
|
||||
onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave')}
|
||||
>
|
||||
<SimpleAreaChart
|
||||
color={
|
||||
accountValueChange >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]
|
||||
}
|
||||
data={rollingDailyData.concat(latestAccountData)}
|
||||
name="accountValue"
|
||||
xKey="time"
|
||||
yKey="account_equity"
|
||||
/>
|
||||
<Transition
|
||||
appear={true}
|
||||
className="absolute bottom-2 right-2"
|
||||
show={showExpandChart || isTablet}
|
||||
enter="transition ease-in duration-300"
|
||||
enterFrom="opacity-0 scale-75"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-out duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<IconButton
|
||||
className="text-th-fgd-3"
|
||||
hideBg
|
||||
onClick={() => handleShowAccountValueChart()}
|
||||
>
|
||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
</Transition>
|
||||
</div>
|
||||
) : null
|
||||
) : mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-4 flex flex-1 md:mt-0">
|
||||
<div className="h-40 w-full rounded-md bg-th-bkg-2 md:h-20 md:w-52 lg:w-60" />
|
||||
</SheenLoader>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default AccountValueChart
|
|
@ -447,12 +447,14 @@ const ActivityFeedTable = () => {
|
|||
) : null}
|
||||
</>
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('activity:no-activity')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('activity:no-activity')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('activity:connect-activity')} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -26,8 +26,7 @@ import { formatDateAxis } from '@components/shared/DetailedAreaOrBarChart'
|
|||
import { formatYAxis } from 'utils/formatting'
|
||||
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { IconButton } from '@components/shared/Button'
|
||||
import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { FadeInFadeOut } from '@components/shared/Transitions'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
|
@ -69,7 +68,7 @@ const fetchHourlyFunding = async (mangoAccountPk: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
const FundingChart = ({ hideChart }: { hideChart: () => void }) => {
|
||||
const FundingChart = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const [daysToShow, setDaysToShow] = useState('30')
|
||||
|
@ -222,9 +221,6 @@ const FundingChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
<ContentBox className="px-6 pt-4" hideBorder hidePadding>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4 md:space-x-6">
|
||||
<IconButton onClick={hideChart} size="medium">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<h2 className="text-lg">{t('funding')}</h2>
|
||||
</div>
|
||||
<ChartRangeButtons
|
||||
|
@ -235,13 +231,11 @@ const FundingChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
/>
|
||||
</div>
|
||||
{loadingFunding || fetchingFunding ? (
|
||||
<SheenLoader className="mt-6 flex flex-1">
|
||||
<div
|
||||
className={`h-[calc(100vh-166px)] w-full rounded-lg bg-th-bkg-2`}
|
||||
/>
|
||||
<SheenLoader className="mt-4 flex flex-1">
|
||||
<div className={`h-[318px] w-full rounded-lg bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
) : filteredData.find((d) => Math.abs(d.total) > 0) ? (
|
||||
<div className="-mx-6 mt-6 h-[calc(100vh-170px)]">
|
||||
<div className="-mx-6 mt-6 h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={filteredData}>
|
||||
<Tooltip
|
||||
|
@ -335,7 +329,7 @@ const FundingChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 flex flex-col items-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<div className="mt-4 flex h-80 flex-col items-center justify-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('account:no-data')}</p>
|
||||
</div>
|
||||
|
|
|
@ -18,8 +18,7 @@ import { formatDateAxis } from '@components/shared/DetailedAreaOrBarChart'
|
|||
import { formatYAxis } from 'utils/formatting'
|
||||
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { IconButton } from '@components/shared/Button'
|
||||
import { ArrowLeftIcon, NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { NoSymbolIcon } from '@heroicons/react/20/solid'
|
||||
import { FadeInFadeOut } from '@components/shared/Transitions'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
|
@ -28,7 +27,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
|
|||
import { DAILY_MILLISECONDS } from 'utils/constants'
|
||||
import useThemeWrapper from 'hooks/useThemeWrapper'
|
||||
|
||||
const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
|
||||
const VolumeChart = () => {
|
||||
const { t } = useTranslation(['account', 'common', 'stats'])
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const { hourlyVolumeData: chartData, loadingHourlyVolume: loading } =
|
||||
|
@ -147,9 +146,6 @@ const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
<ContentBox className="px-6 pt-4" hideBorder hidePadding>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4 md:space-x-6">
|
||||
<IconButton onClick={hideChart} size="medium">
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<h2 className="text-lg">{t('stats:volume')}</h2>
|
||||
</div>
|
||||
<ChartRangeButtons
|
||||
|
@ -160,13 +156,11 @@ const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
/>
|
||||
</div>
|
||||
{loading && mangoAccountAddress ? (
|
||||
<SheenLoader className="mt-6 flex flex-1">
|
||||
<div
|
||||
className={`h-[calc(100vh-166px)] w-full rounded-lg bg-th-bkg-2`}
|
||||
/>
|
||||
<SheenLoader className="mt-4 flex flex-1">
|
||||
<div className={`h-[318px] w-full rounded-lg bg-th-bkg-2`} />
|
||||
</SheenLoader>
|
||||
) : filteredData.find((d) => d.total_volume_usd > 0) ? (
|
||||
<div className="-mx-6 mt-6 h-[calc(100vh-170px)]">
|
||||
<div className="-mx-6 mt-6 h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={filteredData}>
|
||||
<Tooltip
|
||||
|
@ -238,7 +232,7 @@ const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
|
|||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 flex flex-col items-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<div className="mt-4 flex h-80 flex-col items-center justify-center rounded-lg border border-th-bkg-3 p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('account:no-data')}</p>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import PerpMarketsTable from './PerpMarketsTable'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import RecentGainersLosers from './RecentGainersLosers'
|
||||
import Spot from './Spot'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const TABS = ['spot', 'perp']
|
||||
|
||||
const Explore = () => {
|
||||
const { t } = useTranslation(['common'])
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const [activeTab, setActiveTab] = useState('spot')
|
||||
|
||||
useEffect(() => {
|
||||
if (!perpStats || !perpStats.length) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPerpStats()
|
||||
}
|
||||
}, [perpStats])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 pt-8 md:px-6">
|
||||
<h2 className="mb-4 text-center text-lg md:text-left xl:text-xl">
|
||||
{t('explore')}
|
||||
</h2>
|
||||
</div>
|
||||
<RecentGainersLosers />
|
||||
<div className="mb-6 flex flex-col px-4 pt-8 md:mb-0 md:flex-row md:items-center md:space-x-4 md:px-6">
|
||||
<h2 className="mb-3 text-center text-lg md:mb-0 md:text-left xl:text-xl">
|
||||
{t('markets')}
|
||||
</h2>
|
||||
<div className="mx-auto max-w-[112px]">
|
||||
<ButtonGroup
|
||||
activeValue={activeTab}
|
||||
onChange={(t) => setActiveTab(t)}
|
||||
names={TABS.map((tab) => t(tab))}
|
||||
values={TABS}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TabContent activeTab={activeTab} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Explore
|
||||
|
||||
const TabContent = ({ activeTab }: { activeTab: string }) => {
|
||||
switch (activeTab) {
|
||||
case 'spot':
|
||||
return <Spot />
|
||||
case 'perp':
|
||||
return (
|
||||
<div className="mt-6 border-t border-th-bkg-3">
|
||||
<PerpMarketsTable />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return <Spot />
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import PerpMarketsTable from './PerpMarketsTable'
|
||||
import { useRouter } from 'next/router'
|
||||
import TokenPage from '@components/token/TokenPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import Spot from './Spot'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import PerpStatsPage from '@components/stats/perps/PerpStatsPage'
|
||||
import useBanks from 'hooks/useBanks'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const ExplorePage = () => {
|
||||
const { t } = useTranslation(['common'])
|
||||
const router = useRouter()
|
||||
const { token } = router.query
|
||||
const { market } = router.query
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const [activeTab, setActiveTab] = useState('spot')
|
||||
const { perpMarketsWithData } = useListedMarketsWithMarketData()
|
||||
const { banks } = useBanks()
|
||||
|
||||
const tabsWithCount: [string, number][] = useMemo(() => {
|
||||
const tabs: [string, number][] = [
|
||||
['spot', banks.length],
|
||||
['perp', perpMarketsWithData.length],
|
||||
// ['accounts', 0],
|
||||
]
|
||||
return tabs
|
||||
}, [banks, perpMarketsWithData])
|
||||
|
||||
useEffect(() => {
|
||||
if (!perpStats || !perpStats.length) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPerpStats()
|
||||
}
|
||||
}, [perpStats])
|
||||
|
||||
return (
|
||||
<div className="pb-16 md:pb-[27px]">
|
||||
{market ? (
|
||||
<PerpStatsPage />
|
||||
) : token ? (
|
||||
<TokenPage />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-col items-center py-8">
|
||||
<h1 className="mb-4">{t('explore')}</h1>
|
||||
<div className="flex justify-center">
|
||||
{tabsWithCount.map((tab) => (
|
||||
<button
|
||||
className={`flex items-center space-x-2 border-y-2 border-r-2 border-th-bkg-3 px-6 py-3 font-display text-base first:rounded-l-lg first:border-l-2 last:rounded-r-lg focus:outline-none ${
|
||||
activeTab === tab[0] ? 'bg-th-bkg-2 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab[0])}
|
||||
key={tab[0]}
|
||||
>
|
||||
<span>{t(tab[0])}</span>
|
||||
<div className="rounded-md bg-th-bkg-3 px-1 py-0.5 font-body text-xs font-medium text-th-fgd-3">
|
||||
<span>{tab[1]}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TabContent activeTab={activeTab} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExplorePage
|
||||
|
||||
const TabContent = ({ activeTab }: { activeTab: string }) => {
|
||||
switch (activeTab) {
|
||||
case 'spot':
|
||||
return <Spot />
|
||||
case 'perp':
|
||||
return (
|
||||
<div className="border-t border-th-bkg-3">
|
||||
<PerpMarketsTable />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return <Spot />
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||
|
@ -10,7 +9,7 @@ import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
|
|||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import { getDecimalCount, numberCompacter } from 'utils/numbers'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { useRouter } from 'next/router'
|
||||
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { LinkButton } from '@components/shared/Button'
|
||||
|
@ -25,14 +24,7 @@ import { useViewport } from 'hooks/useViewport'
|
|||
import { breakpoints } from 'utils/theme'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import { COLORS } from 'styles/colors'
|
||||
|
||||
export const goToPerpMarketDetails = (
|
||||
market: PerpMarket,
|
||||
router: NextRouter,
|
||||
) => {
|
||||
const query = { ...router.query, ['market']: market.name }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
import { goToPerpMarketDetails } from '@components/stats/perps/PerpMarketDetailsTable'
|
||||
|
||||
const PerpMarketsTable = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
|
@ -106,7 +98,7 @@ const PerpMarketsTable = () => {
|
|||
<TrBody
|
||||
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
|
||||
key={market.publicKey.toString()}
|
||||
onClick={() => goToPerpMarketDetails(market, router)}
|
||||
onClick={() => goToPerpMarketDetails(market.name, router)}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
|
@ -436,7 +428,7 @@ const MobilePerpMarketItem = ({
|
|||
<div className="col-span-1">
|
||||
<LinkButton
|
||||
className="flex items-center"
|
||||
onClick={() => goToPerpMarketDetails(market, router)}
|
||||
onClick={() => goToPerpMarketDetails(market.name, router)}
|
||||
>
|
||||
{t('token:token-stats', { token: market.name })}
|
||||
<ChevronRightIcon className="ml-2 h-5 w-5" />
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
import Change from '@components/shared/Change'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import useListedMarketsWithMarketData, {
|
||||
SerumMarketWithMarketData,
|
||||
} from 'hooks/useListedMarketsWithMarketData'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable'
|
||||
import {
|
||||
BoltIcon,
|
||||
ChevronRightIcon,
|
||||
FaceFrownIcon,
|
||||
RocketLaunchIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { Bank } from '@blockworks-foundation/mango-v4'
|
||||
import Link from 'next/link'
|
||||
import useBanks from 'hooks/useBanks'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { goToPerpMarketDetails } from '@components/stats/perps/PerpMarketDetailsTable'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export type BankWithMarketData = {
|
||||
bank: Bank
|
||||
market: SerumMarketWithMarketData | undefined
|
||||
}
|
||||
|
||||
const CALLOUT_TILES_WRAPPER_CLASSES =
|
||||
'col-span-12 flex flex-col rounded-lg border border-th-bkg-3 p-6 lg:col-span-4'
|
||||
|
||||
const RecentGainersLosers = () => {
|
||||
const { t } = useTranslation(['common', 'explore', 'trade'])
|
||||
const router = useRouter()
|
||||
const { group } = useMangoGroup()
|
||||
const { banks } = useBanks()
|
||||
const {
|
||||
serumMarketsWithData,
|
||||
perpMarketsWithData,
|
||||
isLoading: loadingSerumMarkets,
|
||||
} = useListedMarketsWithMarketData()
|
||||
const groupLoaded = mangoStore((s) => s.groupLoaded)
|
||||
|
||||
const banksWithMarketData = useMemo(() => {
|
||||
if (!banks.length || !group || !serumMarketsWithData.length) return []
|
||||
const banksWithMarketData = []
|
||||
const usdcQuoteMarkets = serumMarketsWithData.filter(
|
||||
(market) => market.quoteTokenIndex === 0,
|
||||
)
|
||||
for (const bank of banks) {
|
||||
const market = usdcQuoteMarkets.find(
|
||||
(market) => market.baseTokenIndex === bank.tokenIndex,
|
||||
)
|
||||
if (market) {
|
||||
banksWithMarketData.push({ bank, market })
|
||||
} else {
|
||||
banksWithMarketData.push({ bank, market: undefined })
|
||||
}
|
||||
}
|
||||
return banksWithMarketData
|
||||
}, [banks, group, serumMarketsWithData])
|
||||
|
||||
const newlyListedMintInfo = useMemo(() => {
|
||||
if (!group) return []
|
||||
const mintInfos = Array.from(group.mintInfosMapByTokenIndex).map(
|
||||
([, mintInfo]) => mintInfo,
|
||||
)
|
||||
const sortByRegistrationTime = mintInfos
|
||||
.sort((a, b) => {
|
||||
return b.registrationTime.toNumber() - a.registrationTime.toNumber()
|
||||
})
|
||||
.slice(0, 3)
|
||||
return sortByRegistrationTime
|
||||
}, [group])
|
||||
|
||||
const newlyListed = useMemo(() => {
|
||||
if (!newlyListedMintInfo.length || !banks.length) return []
|
||||
const newlyListed = []
|
||||
for (const listing of newlyListedMintInfo) {
|
||||
const bank = banks.find((bank) => bank.tokenIndex === listing.tokenIndex)
|
||||
if (bank) {
|
||||
newlyListed.push(bank)
|
||||
}
|
||||
}
|
||||
return newlyListed
|
||||
}, [newlyListedMintInfo, banks])
|
||||
|
||||
const [gainers, losers] = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!banksWithMarketData.length || !perpMarketsWithData || !group)
|
||||
return [[], []]
|
||||
const tradeableAssets = []
|
||||
for (const token of banksWithMarketData) {
|
||||
const volume = token.market?.marketData?.quote_volume_24h || 0
|
||||
if (token.market?.quoteTokenIndex === 0 && volume > 0) {
|
||||
const pastPrice = token.market?.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((token.bank.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
tradeableAssets.push({ bank: token.bank, change, type: 'spot' })
|
||||
}
|
||||
}
|
||||
for (const market of perpMarketsWithData) {
|
||||
const volume = market.marketData?.quote_volume_24h || 0
|
||||
if (volume > 0) {
|
||||
const pastPrice = market.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((market.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(
|
||||
market.perpMarketIndex,
|
||||
)
|
||||
tradeableAssets.push({ market: perpMarket, change, type: 'perp' })
|
||||
}
|
||||
}
|
||||
const sortedAssets = tradeableAssets.sort((a, b) => b.change - a.change)
|
||||
const gainers = sortedAssets.slice(0, 3).filter((item) => {
|
||||
return item.change > 0
|
||||
})
|
||||
const losers = sortedAssets
|
||||
.slice(-3)
|
||||
.filter((item) => {
|
||||
return item.change < 0
|
||||
})
|
||||
.reverse()
|
||||
return [gainers, losers]
|
||||
}, [banksWithMarketData, perpMarketsWithData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4 px-4 md:px-6">
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BoltIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:recently-listed')}</h2>
|
||||
</div>
|
||||
<Link href="/governance/list" shallow>
|
||||
<span className="default-transition font-bold text-th-active md:hover:text-th-active-dark">
|
||||
{t('governance:list-token')}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
{groupLoaded ? (
|
||||
<div className="border-t border-th-bkg-3">
|
||||
{newlyListed.map((token) => {
|
||||
const mintInfo = newlyListedMintInfo.find(
|
||||
(info) => info.tokenIndex === token.tokenIndex,
|
||||
)
|
||||
let timeSinceListing = ''
|
||||
if (mintInfo) {
|
||||
timeSinceListing = dayjs().to(
|
||||
mintInfo.registrationTime.toNumber() * 1000,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={token.tokenIndex}
|
||||
onClick={() =>
|
||||
goToTokenPage(token.name.split(' ')[0], router)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={token} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">
|
||||
{token.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3">
|
||||
<span className="text-th-fgd-3">
|
||||
{timeSinceListing}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<RocketLaunchIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:gainers')}</h2>
|
||||
</div>
|
||||
{!loadingSerumMarkets && groupLoaded ? (
|
||||
<div className="h-full border-t border-th-bkg-3">
|
||||
{gainers.length ? (
|
||||
gainers.map((gainer) => {
|
||||
const bank = gainer?.bank
|
||||
|
||||
const onClick = bank
|
||||
? () => goToTokenPage(bank.name.split(' ')[0], router)
|
||||
: () => goToPerpMarketDetails(gainer?.market?.name, router)
|
||||
|
||||
const price = bank
|
||||
? bank.uiPrice
|
||||
: gainer?.market?.uiPrice || 0
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={
|
||||
`${bank?.tokenIndex}${bank?.name}` ||
|
||||
`${gainer?.market?.perpMarketIndex}${gainer?.market?.name}`
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{bank ? (
|
||||
<div className="mr-3">
|
||||
<TokenLogo bank={bank} />
|
||||
</div>
|
||||
) : (
|
||||
<MarketLogos market={gainer?.market} size="large" />
|
||||
)}
|
||||
<p className="font-body text-th-fgd-2">
|
||||
{bank?.name || gainer?.market?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={price} isUsd />
|
||||
</span>
|
||||
<Change change={gainer.change} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<FaceFrownIcon className="mb-1.5 h-5 w-5" />
|
||||
<p>{t('explore:no-gainers')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<FaceFrownIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:losers')}</h2>
|
||||
</div>
|
||||
{!loadingSerumMarkets && groupLoaded ? (
|
||||
<div className="h-full border-t border-th-bkg-3">
|
||||
{losers.length ? (
|
||||
losers.map((loser) => {
|
||||
const bank = loser?.bank
|
||||
|
||||
const onClick = bank
|
||||
? () => goToTokenPage(bank.name.split(' ')[0], router)
|
||||
: () => goToPerpMarketDetails(loser?.market?.name, router)
|
||||
|
||||
const price = bank
|
||||
? bank.uiPrice
|
||||
: loser?.market?.uiPrice || 0
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={bank?.tokenIndex || loser?.market?.perpMarketIndex}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{bank ? (
|
||||
<TokenLogo bank={bank} />
|
||||
) : (
|
||||
<MarketLogos market={loser?.market} />
|
||||
)}
|
||||
<p className="ml-3 font-body text-th-fgd-2">
|
||||
{bank?.name || loser?.market?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={price} isUsd />
|
||||
</span>
|
||||
<Change change={loser.change} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<RocketLaunchIcon className="mb-1.5 h-5 w-5" />
|
||||
<p>{t('explore:no-losers')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentGainersLosers
|
||||
|
||||
const CalloutTilesLoader = () => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{[...Array(3)].map((x, i) => (
|
||||
<SheenLoader className="flex flex-1" key={i}>
|
||||
<div className="h-16 w-full rounded-md bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,46 +1,28 @@
|
|||
import Change from '@components/shared/Change'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import useListedMarketsWithMarketData, {
|
||||
SerumMarketWithMarketData,
|
||||
} from 'hooks/useListedMarketsWithMarketData'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChangeEvent, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SpotTable from './SpotTable'
|
||||
import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable'
|
||||
import {
|
||||
BoltIcon,
|
||||
ChevronRightIcon,
|
||||
FaceFrownIcon,
|
||||
MagnifyingGlassIcon,
|
||||
RocketLaunchIcon,
|
||||
Squares2X2Icon,
|
||||
TableCellsIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { AllowedKeys } from 'utils/markets'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import SpotCards from './SpotCards'
|
||||
import Input from '@components/forms/Input'
|
||||
import EmptyState from '@components/nftMarket/EmptyState'
|
||||
import { Bank } from '@blockworks-foundation/mango-v4'
|
||||
import Link from 'next/link'
|
||||
import useBanks from 'hooks/useBanks'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export type BankWithMarketData = {
|
||||
bank: Bank
|
||||
market: SerumMarketWithMarketData | undefined
|
||||
}
|
||||
|
||||
const CALLOUT_TILES_WRAPPER_CLASSES =
|
||||
'col-span-12 flex flex-col rounded-lg border border-th-bkg-3 p-6 lg:col-span-4'
|
||||
|
||||
const generateSearchTerm = (item: BankWithMarketData, searchValue: string) => {
|
||||
const normalizedSearchValue = searchValue.toLowerCase()
|
||||
const value = item.bank.name.toLowerCase()
|
||||
|
@ -107,12 +89,9 @@ const sortTokens = (tokens: BankWithMarketData[], sortByKey: AllowedKeys) => {
|
|||
|
||||
const Spot = () => {
|
||||
const { t } = useTranslation(['common', 'explore', 'trade'])
|
||||
const router = useRouter()
|
||||
const { group } = useMangoGroup()
|
||||
const { banks } = useBanks()
|
||||
const { serumMarketsWithData, isLoading: loadingSerumMarkets } =
|
||||
useListedMarketsWithMarketData()
|
||||
const groupLoaded = mangoStore((s) => s.groupLoaded)
|
||||
const { serumMarketsWithData } = useListedMarketsWithMarketData()
|
||||
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
|
||||
const [search, setSearch] = useState('')
|
||||
const [showTableView, setShowTableView] = useState(true)
|
||||
|
@ -136,81 +115,6 @@ const Spot = () => {
|
|||
return banksWithMarketData
|
||||
}, [banks, group, serumMarketsWithData])
|
||||
|
||||
const newlyListedMintInfo = useMemo(() => {
|
||||
if (!group) return []
|
||||
const mintInfos = Array.from(group.mintInfosMapByTokenIndex).map(
|
||||
([, mintInfo]) => mintInfo,
|
||||
)
|
||||
const sortByRegistrationTime = mintInfos
|
||||
.sort((a, b) => {
|
||||
return b.registrationTime.toNumber() - a.registrationTime.toNumber()
|
||||
})
|
||||
.slice(0, 3)
|
||||
return sortByRegistrationTime
|
||||
}, [group])
|
||||
|
||||
const newlyListed = useMemo(() => {
|
||||
if (!newlyListedMintInfo.length || !banks.length) return []
|
||||
const newlyListed = []
|
||||
for (const listing of newlyListedMintInfo) {
|
||||
const bank = banks.find((bank) => bank.tokenIndex === listing.tokenIndex)
|
||||
if (bank) {
|
||||
newlyListed.push(bank)
|
||||
}
|
||||
}
|
||||
return newlyListed
|
||||
}, [newlyListedMintInfo, banks])
|
||||
|
||||
const [gainers, losers] = useMemo(() => {
|
||||
if (!banksWithMarketData.length) return [[], []]
|
||||
const sortByChange = banksWithMarketData
|
||||
.filter((token) => {
|
||||
const volume = token.market?.marketData?.quote_volume_24h || 0
|
||||
return token.market?.quoteTokenIndex === 0 && volume > 0
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aPrice = a?.bank?.uiPrice || 0
|
||||
const bPrice = b?.bank?.uiPrice || 0
|
||||
const aPastPrice = a.market?.marketData?.price_24h
|
||||
const bPastPrice = b.market?.marketData?.price_24h
|
||||
const aValue = aPastPrice
|
||||
? ((aPrice - aPastPrice) / aPastPrice) * 100
|
||||
: undefined
|
||||
const bValue = bPastPrice
|
||||
? ((bPrice - bPastPrice) / bPastPrice) * 100
|
||||
: undefined
|
||||
if (typeof aValue === 'undefined' && typeof bValue === 'undefined') {
|
||||
return 0 // Consider them equal
|
||||
}
|
||||
if (typeof aValue === 'undefined') {
|
||||
return 1 // b should come before a
|
||||
}
|
||||
if (typeof bValue === 'undefined') {
|
||||
return -1 // a should come before b
|
||||
}
|
||||
|
||||
return bValue - aValue
|
||||
})
|
||||
const gainers = sortByChange.slice(0, 3).filter((item) => {
|
||||
const pastPrice = item.market?.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((item.bank.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
return change > 0
|
||||
})
|
||||
const losers = sortByChange
|
||||
.slice(-3)
|
||||
.reverse()
|
||||
.filter((item) => {
|
||||
const pastPrice = item.market?.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((item.bank.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
return change < 0
|
||||
})
|
||||
return [gainers, losers]
|
||||
}, [banksWithMarketData])
|
||||
|
||||
const sortedTokensToShow = useMemo(() => {
|
||||
if (!banksWithMarketData.length) return []
|
||||
return search
|
||||
|
@ -223,239 +127,63 @@ const Spot = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4 px-4 pb-8 md:px-6 2xl:px-12">
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BoltIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:recently-listed')}</h2>
|
||||
</div>
|
||||
<Link href="/governance/list" shallow>
|
||||
<span className="default-transition font-bold text-th-active md:hover:text-th-active-dark">
|
||||
{t('governance:list-token')}
|
||||
</span>
|
||||
</Link>
|
||||
<div className="md:-mt-10">
|
||||
<div className="flex flex-col px-4 sm:flex-row sm:items-center sm:justify-end md:px-6 2xl:px-12">
|
||||
<div className="flex w-full flex-col sm:flex-row sm:space-x-3 md:w-auto">
|
||||
<div className="relative mb-3 w-full sm:mb-0 md:w-40">
|
||||
<Input
|
||||
heightClass="h-10 pl-8"
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={handleUpdateSearch}
|
||||
/>
|
||||
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4" />
|
||||
</div>
|
||||
{groupLoaded ? (
|
||||
<div className="border-t border-th-bkg-3">
|
||||
{newlyListed.map((token) => {
|
||||
const mintInfo = newlyListedMintInfo.find(
|
||||
(info) => info.tokenIndex === token.tokenIndex,
|
||||
)
|
||||
let timeSinceListing = ''
|
||||
if (mintInfo) {
|
||||
timeSinceListing = dayjs().to(
|
||||
mintInfo.registrationTime.toNumber() * 1000,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={token.tokenIndex}
|
||||
onClick={() =>
|
||||
goToTokenPage(token.name.split(' ')[0], router)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={token} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">
|
||||
{token.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3">
|
||||
<span className="text-th-fgd-3">
|
||||
{timeSinceListing}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<RocketLaunchIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:gainers')}</h2>
|
||||
</div>
|
||||
{!loadingSerumMarkets && groupLoaded ? (
|
||||
<div className="h-full border-t border-th-bkg-3">
|
||||
{gainers.length ? (
|
||||
gainers.map((gainer) => {
|
||||
const bank = gainer.bank
|
||||
const pastPrice = gainer.market?.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((bank.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={bank.tokenIndex}
|
||||
onClick={() =>
|
||||
goToTokenPage(bank.name.split(' ')[0], router)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">
|
||||
{bank.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
</span>
|
||||
<Change change={change} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<FaceFrownIcon className="mb-1.5 h-5 w-5" />
|
||||
<p>{t('explore:no-gainers')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={CALLOUT_TILES_WRAPPER_CLASSES}>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<FaceFrownIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:losers')}</h2>
|
||||
</div>
|
||||
{!loadingSerumMarkets && groupLoaded ? (
|
||||
<div className="h-full border-t border-th-bkg-3">
|
||||
{losers.length ? (
|
||||
losers.map((loser) => {
|
||||
const bank = loser.bank
|
||||
const pastPrice = loser.market?.marketData?.price_24h
|
||||
const change = pastPrice
|
||||
? ((bank.uiPrice - pastPrice) / pastPrice) * 100
|
||||
: 0
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={bank.tokenIndex}
|
||||
onClick={() =>
|
||||
goToTokenPage(bank.name.split(' ')[0], router)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">
|
||||
{bank.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
</span>
|
||||
<Change change={change} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<RocketLaunchIcon className="mb-1.5 h-5 w-5" />
|
||||
<p>{t('explore:no-losers')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CalloutTilesLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-th-bkg-3 pt-4">
|
||||
<div className="flex flex-col px-4 sm:flex-row sm:items-center sm:justify-between md:px-6 2xl:px-12">
|
||||
<h2 className="mb-4 text-base sm:mb-0">{t('tokens')}</h2>
|
||||
<div className="flex flex-col sm:flex-row sm:space-x-3">
|
||||
<div className="relative mb-3 w-full sm:mb-0 sm:w-40">
|
||||
<Input
|
||||
heightClass="h-10 pl-8"
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={handleUpdateSearch}
|
||||
<div className="flex space-x-3">
|
||||
<div className="w-full sm:w-48">
|
||||
<ButtonGroup
|
||||
activeValue={sortByKey}
|
||||
onChange={(v) => setSortByKey(v)}
|
||||
names={[t('trade:24h-volume'), t('rolling-change')]}
|
||||
values={['quote_volume_24h', 'change_24h']}
|
||||
/>
|
||||
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<div className="w-full sm:w-48">
|
||||
<ButtonGroup
|
||||
activeValue={sortByKey}
|
||||
onChange={(v) => setSortByKey(v)}
|
||||
names={[t('trade:24h-volume'), t('rolling-change')]}
|
||||
values={['quote_volume_24h', 'change_24h']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-l-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<TableCellsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-r-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
!showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<Squares2X2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-l-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<TableCellsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-r-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
!showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<Squares2X2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{sortedTokensToShow.length ? (
|
||||
showTableView ? (
|
||||
<div className="mt-6 border-t border-th-bkg-3">
|
||||
<SpotTable tokens={sortedTokensToShow} />
|
||||
</div>
|
||||
) : (
|
||||
<SpotCards tokens={sortedTokensToShow} />
|
||||
)
|
||||
) : (
|
||||
<div className="px-4 pt-2 md:px-6 2xl:px-12">
|
||||
<EmptyState text="No results found..." />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
{sortedTokensToShow.length ? (
|
||||
showTableView ? (
|
||||
<div className="mt-6 border-t border-th-bkg-3">
|
||||
<SpotTable tokens={sortedTokensToShow} />
|
||||
</div>
|
||||
) : (
|
||||
<SpotCards tokens={sortedTokensToShow} />
|
||||
)
|
||||
) : (
|
||||
<div className="px-4 pt-2 md:px-6 2xl:px-12">
|
||||
<EmptyState text="No results found..." />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spot
|
||||
|
||||
const CalloutTilesLoader = () => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{[...Array(3)].map((x, i) => (
|
||||
<SheenLoader className="flex flex-1" key={i}>
|
||||
<div className="h-16 w-full rounded-md bg-th-bkg-2" />
|
||||
</SheenLoader>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
NewspaperIcon,
|
||||
ExclamationTriangleIcon,
|
||||
DocumentTextIcon,
|
||||
SparklesIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import SolanaTps from '@components/SolanaTps'
|
||||
|
@ -80,9 +79,9 @@ const BottomBar = () => {
|
|||
<ArrowTrendingUpIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('trade')}</StyledBarItemLabel>
|
||||
</BottomBarLink>
|
||||
<BottomBarLink isActive={asPath === '/explore'} pathName="/explore">
|
||||
<SparklesIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('explore')}</StyledBarItemLabel>
|
||||
<BottomBarLink isActive={asPath === '/borrow'} pathName="/borrow">
|
||||
<BanknotesIcon className="mb-1 h-4 w-4" />
|
||||
<StyledBarItemLabel>{t('borrow')}</StyledBarItemLabel>
|
||||
</BottomBarLink>
|
||||
<button
|
||||
className={`${
|
||||
|
@ -125,11 +124,6 @@ const MoreMenuPanel = ({
|
|||
className="border-b border-th-bkg-4"
|
||||
onClick={() => setShowPanel(false)}
|
||||
>
|
||||
<MoreMenuItem
|
||||
title={t('borrow')}
|
||||
path="/borrow"
|
||||
icon={<BanknotesIcon className="h-5 w-5" />}
|
||||
/>
|
||||
<MoreMenuItem
|
||||
title={t('stats')}
|
||||
path="/stats"
|
||||
|
|
|
@ -12,7 +12,7 @@ const Change = ({
|
|||
change: number | typeof NaN
|
||||
decimals?: number
|
||||
prefix?: string
|
||||
size?: 'small'
|
||||
size?: 'small' | 'large'
|
||||
suffix?: string
|
||||
}) => {
|
||||
return !isNaN(change) ? (
|
||||
|
@ -34,7 +34,11 @@ const Change = ({
|
|||
)}
|
||||
<p
|
||||
className={`font-mono font-normal ${
|
||||
size === 'small' ? 'text-xs' : 'text-sm'
|
||||
size === 'small'
|
||||
? 'text-xs'
|
||||
: size === 'large'
|
||||
? 'text-base'
|
||||
: 'text-sm'
|
||||
} ${
|
||||
change > 0
|
||||
? 'text-th-up'
|
||||
|
|
|
@ -39,12 +39,13 @@ dayjs.extend(relativeTime)
|
|||
interface DetailedAreaOrBarChartProps {
|
||||
chartType?: 'area' | 'bar'
|
||||
customTooltip?: ContentType<number, string>
|
||||
data: any[]
|
||||
data: any[] | undefined
|
||||
daysToShow?: string
|
||||
domain?: AxisDomain
|
||||
heightClass?: string
|
||||
hideChange?: boolean
|
||||
hideChart?: () => void
|
||||
hideAxis?: boolean
|
||||
loaderHeightClass?: string
|
||||
loading?: boolean
|
||||
prefix?: string
|
||||
|
@ -80,6 +81,7 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
heightClass,
|
||||
hideChange,
|
||||
hideChart,
|
||||
hideAxis,
|
||||
loaderHeightClass,
|
||||
loading,
|
||||
prefix = '',
|
||||
|
@ -114,12 +116,12 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
}
|
||||
|
||||
const flipGradientCoords = useMemo(() => {
|
||||
if (!data.length) return
|
||||
if (!data || !data.length) return
|
||||
return data[0][yKey] <= 0 && data[data.length - 1][yKey] < 0
|
||||
}, [data])
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data.length) return []
|
||||
if (!data || !data.length) return []
|
||||
const start = Number(daysToShow) * DAILY_MILLISECONDS
|
||||
const filtered = data.filter((d: any) => {
|
||||
const dataTime = new Date(d[xKey]).getTime()
|
||||
|
@ -285,7 +287,9 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
: ''}
|
||||
{prefix}
|
||||
<FormatNumericValue
|
||||
value={Math.abs(data[data.length - 1][yKey])}
|
||||
value={
|
||||
data ? Math.abs(data[data.length - 1][yKey]) : 0
|
||||
}
|
||||
decimals={yDecimals}
|
||||
/>
|
||||
{suffix}
|
||||
|
@ -386,6 +390,7 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
<XAxis
|
||||
axisLine={false}
|
||||
dataKey={xKey}
|
||||
hide={hideAxis}
|
||||
minTickGap={20}
|
||||
padding={{ left: 20, right: 20 }}
|
||||
tick={{
|
||||
|
@ -400,6 +405,7 @@ const DetailedAreaOrBarChart: FunctionComponent<
|
|||
<YAxis
|
||||
axisLine={false}
|
||||
dataKey={yKey}
|
||||
hide={hideAxis}
|
||||
minTickGap={20}
|
||||
type="number"
|
||||
domain={
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const UpTriangle = ({ size }: { size?: 'small' }) => (
|
||||
export const UpTriangle = ({ size }: { size?: 'small' | 'large' }) => (
|
||||
<div
|
||||
className={`h-0 w-0 ${
|
||||
size === 'small'
|
||||
|
@ -8,7 +8,7 @@ export const UpTriangle = ({ size }: { size?: 'small' }) => (
|
|||
/>
|
||||
)
|
||||
|
||||
export const DownTriangle = ({ size }: { size?: 'small' }) => (
|
||||
export const DownTriangle = ({ size }: { size?: 'small' | 'large' }) => (
|
||||
<div
|
||||
className={`h-0 w-0 ${
|
||||
size === 'small'
|
||||
|
|
|
@ -25,7 +25,7 @@ const TabButtons = <T extends Values>({
|
|||
{values.map(([label, count], i) => (
|
||||
<div className={fillWidth ? 'flex-1' : ''} key={`${label}` + i}>
|
||||
<button
|
||||
className={`flex h-12 w-full items-center justify-center px-4 font-normal focus-visible:bg-th-bkg-2 focus-visible:text-th-fgd-1 md:px-6 ${
|
||||
className={`flex h-12 w-full items-center justify-center px-4 focus-visible:bg-th-bkg-2 focus-visible:text-th-fgd-1 md:px-6 ${
|
||||
rounded ? 'rounded-md' : 'rounded-none'
|
||||
} ${
|
||||
showBorders
|
||||
|
@ -49,7 +49,7 @@ const TabButtons = <T extends Values>({
|
|||
className={`${
|
||||
label === 'buy' || label === 'sell'
|
||||
? 'font-display'
|
||||
: 'font-medium'
|
||||
: 'font-bold'
|
||||
} whitespace-nowrap`}
|
||||
>
|
||||
{t(label)}
|
||||
|
|
|
@ -16,14 +16,15 @@ import { LinkButton } from '@components/shared/Button'
|
|||
import SoonBadge from '@components/shared/SoonBadge'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
|
||||
export const goToPerpMarketDetails = (
|
||||
market: PerpMarket,
|
||||
market: string | undefined,
|
||||
router: NextRouter,
|
||||
) => {
|
||||
const query = { ...router.query, ['market']: market.name }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
if (market) {
|
||||
const query = { ...router.query, ['market']: market }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
}
|
||||
|
||||
const PerpMarketDetailsTable = () => {
|
||||
|
@ -95,7 +96,7 @@ const PerpMarketDetailsTable = () => {
|
|||
<TrBody
|
||||
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
|
||||
key={publicKey.toString()}
|
||||
onClick={() => goToPerpMarketDetails(market, router)}
|
||||
onClick={() => goToPerpMarketDetails(market.name, router)}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
|
@ -323,7 +324,7 @@ const PerpMarketDetailsTable = () => {
|
|||
<LinkButton
|
||||
className="flex items-center"
|
||||
onClick={() =>
|
||||
goToPerpMarketDetails(market, router)
|
||||
goToPerpMarketDetails(market.name, router)
|
||||
}
|
||||
>
|
||||
{t('stats:perp-details', { market: market.name })}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useMemo } from 'react'
|
|||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import PerpMarketDetails from './PerpMarketDetails'
|
||||
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const PerpStatsPage = () => {
|
||||
const router = useRouter()
|
||||
|
@ -33,12 +34,20 @@ const PerpStatsPage = () => {
|
|||
|
||||
return marketDetails ? (
|
||||
<>
|
||||
<div className="flex flex-col border-b border-th-bkg-3 px-6 py-5 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<MarketLogos market={marketDetails} size="large" />
|
||||
<h1 className="text-xl">{marketDetails.name}</h1>
|
||||
</div>
|
||||
<div className="flex h-14 items-center space-x-4 border-b border-th-bkg-3">
|
||||
<button
|
||||
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={() =>
|
||||
router.push(router.pathname, undefined, { shallow: true })
|
||||
}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex items-center">
|
||||
<MarketLogos market={marketDetails} size="large" />
|
||||
<span className="text-lg font-bold text-th-fgd-1">
|
||||
{marketDetails.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<PerpMarketDetails marketStats={marketStats} perpMarket={marketDetails} />
|
||||
|
|
|
@ -471,12 +471,14 @@ const SwapHistoryTable = () => {
|
|||
) : null}
|
||||
</>
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('swap:no-history')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('swap:no-history')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('swap:connect-swap')} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -516,13 +516,15 @@ const SwapOrders = () => {
|
|||
</div>
|
||||
)
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-orders')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-trigger-orders')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<ConnectEmptyState text={t('connect-orders')} />
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('trade:connect-trigger-orders')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import TopTokenAccounts from './TopTokenAccounts'
|
|||
import TokenParams from './TokenParams'
|
||||
import { formatTokenSymbol } from 'utils/tokens'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
const DEFAULT_COINGECKO_VALUES = {
|
||||
ath: 0,
|
||||
|
@ -121,6 +122,21 @@ const TokenPage = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-14 items-center space-x-4 border-b border-th-bkg-3">
|
||||
<button
|
||||
className="flex h-14 w-14 flex-shrink-0 items-center justify-center border-r border-th-bkg-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
|
||||
onClick={() =>
|
||||
router.push(router.pathname, undefined, { shallow: true })
|
||||
}
|
||||
>
|
||||
<ArrowLeftIcon className="h-5 w-5" />
|
||||
</button>
|
||||
{bank ? (
|
||||
<span className="text-lg font-bold text-th-fgd-1">
|
||||
{formatTokenSymbol(bank.name)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{bank && bankName ? (
|
||||
<>
|
||||
<div className="flex flex-col border-b border-th-bkg-3 px-6 py-5 md:flex-row md:items-center md:justify-between">
|
||||
|
@ -129,10 +145,7 @@ const TokenPage = () => {
|
|||
<TokenLogo bank={bank} />
|
||||
{coingeckoTokenInfo ? (
|
||||
<h1 className="text-base font-normal">
|
||||
{coingeckoTokenInfo.name}{' '}
|
||||
<span className="text-th-fgd-4">
|
||||
{formatTokenSymbol(bank.name)}
|
||||
</span>
|
||||
{coingeckoTokenInfo.name}
|
||||
</h1>
|
||||
) : (
|
||||
<h1 className="text-base font-normal">{bank.name}</h1>
|
||||
|
|
|
@ -78,7 +78,7 @@ const MarketLogos = ({
|
|||
: size === 'small'
|
||||
? 'mr-1.5 h-4'
|
||||
: size === 'large'
|
||||
? 'mr-2.5 h-6'
|
||||
? 'mr-3 h-6'
|
||||
: 'mr-2 h-5'
|
||||
} ${
|
||||
market instanceof Serum3Market
|
||||
|
|
|
@ -624,12 +624,14 @@ const OpenOrders = () => {
|
|||
</div>
|
||||
)
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-orders')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-orders')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('trade:connect-orders')} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -730,12 +730,14 @@ const PerpPositions = () => {
|
|||
</div>
|
||||
)
|
||||
) : mangoAccount || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-positions')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-positions')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('trade:connect-positions')} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -351,12 +351,14 @@ const TradeHistory = () => {
|
|||
) : null}
|
||||
</>
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>No trade history</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>No trade history</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('trade:connect-trade-history')} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -326,12 +326,14 @@ const UnsettledTrades = ({
|
|||
</div>
|
||||
)
|
||||
) : mangoAccountAddress || connected ? (
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-unsettled')}</p>
|
||||
<div className="flex flex-1 flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center p-8">
|
||||
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p>{t('trade:no-unsettled')}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-8">
|
||||
<ConnectEmptyState text={t('trade:connect-unsettled')} />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -10,8 +10,8 @@ export default function useAccountPerformanceData() {
|
|||
|
||||
const {
|
||||
data: performanceData,
|
||||
isLoading: loadingPerformanceData,
|
||||
isFetching: fetchingPerformanceData,
|
||||
isInitialLoading: loadingPerformanceData,
|
||||
} = useQuery(
|
||||
['performance', mangoAccountAddress],
|
||||
() => fetchAccountPerformance(mangoAccountAddress, 31),
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import ExplorePage from '@components/explore/ExplorePage'
|
||||
import type { NextPage } from 'next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, [
|
||||
'activity',
|
||||
'common',
|
||||
'explore',
|
||||
'governance',
|
||||
'notifications',
|
||||
'onboarding',
|
||||
'profile',
|
||||
'search',
|
||||
'settings',
|
||||
'stats',
|
||||
'token',
|
||||
'trade',
|
||||
])),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const Explore: NextPage = () => {
|
||||
return <ExplorePage />
|
||||
}
|
||||
|
||||
export default Explore
|
|
@ -9,6 +9,8 @@ export async function getStaticProps({ locale }: { locale: string }) {
|
|||
'account',
|
||||
'activity',
|
||||
'common',
|
||||
'explore',
|
||||
'governance',
|
||||
'notifications',
|
||||
'onboarding',
|
||||
'onboarding-tours',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"account-stats": "Account Stats",
|
||||
"assets": "Assets",
|
||||
"assets-liabilities": "Assets & Liabilities",
|
||||
"collateral-value": "Collateral Value",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"more-account-stats": "More Account Stats",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"connect-orders": "Connect to view your open orders",
|
||||
"connect-positions": "Connect to view your perp positions",
|
||||
"connect-trade-history": "Connect to view your trade history",
|
||||
"connect-trigger-orders": "Connect to view your trigger orders",
|
||||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"no-markets-found": "No markets found...",
|
||||
"no-orders": "No open orders",
|
||||
"no-positions": "No perp positions",
|
||||
"no-trigger-orders": "No trigger orders",
|
||||
"no-unsettled": "No unsettled funds",
|
||||
"notional": "Notional",
|
||||
"notional-volume": "Notional Volume ($)",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"account-stats": "Account Stats",
|
||||
"assets": "Assets",
|
||||
"assets-liabilities": "Assets & Liabilities",
|
||||
"collateral-value": "Collateral Value",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"more-account-stats": "More Account Stats",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"connect-orders": "Connect to view your open orders",
|
||||
"connect-positions": "Connect to view your perp positions",
|
||||
"connect-trade-history": "Connect to view your trade history",
|
||||
"connect-trigger-orders": "Connect to view your trigger orders",
|
||||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"no-markets-found": "No markets found...",
|
||||
"no-orders": "No open orders",
|
||||
"no-positions": "No perp positions",
|
||||
"no-trigger-orders": "No trigger orders",
|
||||
"no-unsettled": "No unsettled funds",
|
||||
"notional": "Notional",
|
||||
"notional-volume": "Notional Volume ($)",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"account-stats": "Account Stats",
|
||||
"assets": "Assets",
|
||||
"assets-liabilities": "Assets & Liabilities",
|
||||
"collateral-value": "Collateral Value",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"lifetime-volume": "Lifetime Trade Volume",
|
||||
"maint-health-contribution": "Maint Health Contribution",
|
||||
"maint-health-contributions": "Maint Health Contributions",
|
||||
"more-account-stats": "More Account Stats",
|
||||
"no-data": "No data to display",
|
||||
"no-pnl-history": "No PnL History",
|
||||
"pnl-chart": "PnL Chart",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"connect-orders": "Connect to view your open orders",
|
||||
"connect-positions": "Connect to view your perp positions",
|
||||
"connect-trade-history": "Connect to view your trade history",
|
||||
"connect-trigger-orders": "Connect to view your trigger orders",
|
||||
"connect-unsettled": "Connect to view your unsettled funds",
|
||||
"copy-and-share": "Copy Image to Clipboard",
|
||||
"current-price": "Current Price",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"no-markets-found": "No markets found...",
|
||||
"no-orders": "No open orders",
|
||||
"no-positions": "No perp positions",
|
||||
"no-trigger-orders": "No trigger orders",
|
||||
"no-unsettled": "No unsettled funds",
|
||||
"notional": "Notional",
|
||||
"notional-volume": "Notional Volume ($)",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"account-stats": "Account Stats",
|
||||
"assets": "资产",
|
||||
"assets-liabilities": "资产和债务",
|
||||
"collateral-value": "质押品价值",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"maint-health": "维持健康度",
|
||||
"maint-health-contribution": "维持健康贡献",
|
||||
"maint-health-contributions": "维持健康度",
|
||||
"more-account-stats": "More Account Stats",
|
||||
"no-data": "无数据可显示",
|
||||
"no-pnl-history": "无盈亏历史",
|
||||
"pnl-chart": "盈亏图表",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"connect-orders": "连接以查看未结订单",
|
||||
"connect-positions": "连接以查看持仓",
|
||||
"connect-trade-history": "连接以查看交易历史",
|
||||
"connect-trigger-orders": "Connect to view your trigger orders",
|
||||
"connect-unsettled": "连接以查看未结清余额",
|
||||
"copy-and-share": "将图片复制到剪贴版",
|
||||
"current-price": "目前价格",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"no-markets-found": "No markets found...",
|
||||
"no-orders": "无订单",
|
||||
"no-positions": "无持仓",
|
||||
"no-trigger-orders": "No trigger orders",
|
||||
"no-unsettled": "无未结清余额",
|
||||
"notional": "名义",
|
||||
"notional-volume": "名义交易量($)",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"account-stats": "Account Stats",
|
||||
"assets": "資產",
|
||||
"assets-liabilities": "資產和債務",
|
||||
"collateral-value": "質押品價值",
|
||||
|
@ -15,6 +16,7 @@
|
|||
"maint-health": "維持健康度",
|
||||
"maint-health-contribution": "維持健康貢獻",
|
||||
"maint-health-contributions": "維持健康度",
|
||||
"more-account-stats": "More Account Stats",
|
||||
"no-data": "無數據可顯示",
|
||||
"no-pnl-history": "無盈虧歷史",
|
||||
"pnl-chart": "盈虧圖表",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"connect-orders": "連接以查看未結訂單",
|
||||
"connect-positions": "連接以查看持倉",
|
||||
"connect-trade-history": "連接以查看交易歷史",
|
||||
"connect-trigger-orders": "Connect to view your trigger orders",
|
||||
"connect-unsettled": "連接以查看未結清餘額",
|
||||
"copy-and-share": "將圖片複製到剪貼版",
|
||||
"current-price": "目前價格",
|
||||
|
@ -57,6 +58,7 @@
|
|||
"no-markets-found": "No markets found...",
|
||||
"no-orders": "無訂單",
|
||||
"no-positions": "無持倉",
|
||||
"no-trigger-orders": "No trigger orders",
|
||||
"no-unsettled": "無未結清餘額",
|
||||
"notional": "名義",
|
||||
"notional-volume": "名義交易量($)",
|
||||
|
|
Loading…
Reference in New Issue