re-design account page

This commit is contained in:
saml33 2023-10-17 22:28:43 +11:00
parent 93923f937f
commit 6362bd3cc1
50 changed files with 1261 additions and 1582 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,8 @@ export default function useAccountPerformanceData() {
const {
data: performanceData,
isLoading: loadingPerformanceData,
isFetching: fetchingPerformanceData,
isInitialLoading: loadingPerformanceData,
} = useQuery(
['performance', mangoAccountAddress],
() => fetchAccountPerformance(mangoAccountAddress, 31),

View File

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

View File

@ -9,6 +9,8 @@ export async function getStaticProps({ locale }: { locale: string }) {
'account',
'activity',
'common',
'explore',
'governance',
'notifications',
'onboarding',
'onboarding-tours',

View File

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

View File

@ -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 ($)",

View File

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

View File

@ -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 ($)",

View File

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

View File

@ -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 ($)",

View File

@ -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": "盈亏图表",

View File

@ -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": "名义交易量($)",

View File

@ -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": "盈虧圖表",

View File

@ -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": "名義交易量($)",