Merge branch 'main' into pan/prod-to-main

This commit is contained in:
Riordan Panayides 2022-07-17 16:41:08 +01:00
commit 20f54be2ad
137 changed files with 8519 additions and 5647 deletions

View File

@ -8,11 +8,10 @@ import {
} from '@blockworks-foundation/mango-client'
import { useCallback, useState } from 'react'
import {
BellIcon,
ExclamationIcon,
ExternalLinkIcon,
HeartIcon,
} from '@heroicons/react/solid'
import { BellIcon } from '@heroicons/react/outline'
import useMangoStore, { MNGO_INDEX } from '../stores/useMangoStore'
import { abbreviateAddress, formatUsdValue, usdFormatter } from '../utils'
import { notify } from '../utils/notifications'
@ -31,6 +30,7 @@ import Loading from './Loading'
import CreateAlertModal from './CreateAlertModal'
import { useWallet } from '@solana/wallet-adapter-react'
import { useRouter } from 'next/router'
import HealthHeart from './HealthHeart'
const I80F48_100 = I80F48.fromString('100')
@ -131,11 +131,6 @@ export default function AccountInfo() {
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: I80F48_100
const initHealthRatio =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
: I80F48_100
const maintHealth =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Maint')
@ -193,7 +188,7 @@ export default function AccountInfo() {
) : null}
<div>
{mangoAccount ? (
<div className="-mt-2 flex justify-center text-xs">
<div className="-mt-2 mb-2 flex justify-center text-xs">
<a
className="flex items-center text-th-fgd-4 hover:text-th-primary"
href={`https://explorer.solana.com/address/${mangoAccount?.publicKey}`}
@ -208,12 +203,41 @@ export default function AccountInfo() {
<div>
<div className="flex justify-between pb-2">
<div className="font-normal leading-4 text-th-fgd-3">
{t('equity')}
{t('value')}
</div>
<div className="text-th-fgd-1">
{initialLoad ? <DataLoader /> : formatUsdValue(+equity)}
</div>
</div>
<div className="flex justify-between pb-2">
<Tooltip
content={
<div>
{t('tooltip-account-liquidated')}{' '}
<a
href="https://docs.mango.markets/mango/health-overview"
target="_blank"
rel="noopener noreferrer"
>
{t('learn-more')}
</a>
</div>
}
>
<div className="default-transition cursor-help border-b border-dashed border-th-fgd-3 border-opacity-20 font-normal leading-4 text-th-fgd-3 hover:border-th-bkg-2">
{t('health')}
</div>
</Tooltip>
<div className="flex items-center space-x-2">
<HealthHeart size={24} health={Number(maintHealthRatio)} />
<div className="text-th-fgd-1">
{maintHealthRatio.gt(I80F48_100)
? '>100'
: maintHealthRatio.toFixed(2)}
%
</div>
</div>
</div>
<div className="flex justify-between pb-2">
<div className="font-normal leading-4 text-th-fgd-3">
{t('leverage')}
@ -326,54 +350,6 @@ export default function AccountInfo() {
</div>
</div>
</div>
<div className="my-2 flex items-center rounded border border-th-bkg-4 p-2.5 sm:my-1">
<div className="flex items-center pr-2">
<HeartIcon
className="mr-1.5 h-5 w-5 text-th-primary"
aria-hidden="true"
/>
<span>
<Tooltip
content={
<div>
{t('tooltip-account-liquidated')}{' '}
<a
href="https://docs.mango.markets/mango/health-overview"
target="_blank"
rel="noopener noreferrer"
>
{t('learn-more')}
</a>
</div>
}
>
<div className="default-transition cursor-help border-b border-dashed border-th-fgd-3 border-opacity-20 font-normal leading-4 text-th-fgd-3 hover:border-th-bkg-2">
{t('health')}
</div>
</Tooltip>
</span>
</div>
<div className="flex h-1.5 flex-grow rounded bg-th-bkg-4">
<div
style={{
width: `${maintHealthRatio}%`,
}}
className={`flex rounded ${
maintHealthRatio.toNumber() > 30
? 'bg-th-green'
: initHealthRatio.toNumber() > 0
? 'bg-th-orange'
: 'bg-th-red'
}`}
></div>
</div>
<div className="pl-2 text-right">
{maintHealthRatio.gt(I80F48_100)
? '>100'
: maintHealthRatio.toFixed(2)}
%
</div>
</div>
{mangoAccount && mangoAccount.beingLiquidated ? (
<div className="flex items-center justify-center pt-0.5 text-xs">
<ExclamationIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red" />

View File

@ -3,7 +3,7 @@ import useMangoStore from '../stores/useMangoStore'
import {
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import Input, { Label } from './Input'
import Button from './Button'
import Modal from './Modal'
@ -83,7 +83,7 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
<p className="flex items-center justify-center">
{t('edit-nickname')}
<Tooltip content={t('tooltip-name-onchain')}>
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-primary" />
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
</Tooltip>
</p>
</Modal.Header>

View File

@ -0,0 +1,121 @@
import { useTranslation } from 'next-i18next'
import useMangoStore from '../stores/useMangoStore'
import useMangoAccount from '../hooks/useMangoAccount'
import {
I80F48,
nativeI80F48ToUi,
QUOTE_INDEX,
ZERO_I80F48,
} from '@blockworks-foundation/mango-client'
import { abbreviateAddress, formatUsdValue, usdFormatter } from 'utils'
import { DataLoader } from './MarketPosition'
const AccountOverviewPopover = ({
collapsed,
health,
}: {
collapsed: boolean
health: I80F48
}) => {
const { t } = useTranslation('common')
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const { mangoAccount, initialLoad } = useMangoAccount()
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const I80F48_100 = I80F48.fromString('100')
const initHealth =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Init')
: I80F48_100
const equity =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.computeValue(mangoGroup, mangoCache)
: ZERO_I80F48
return (
<>
{mangoAccount ? (
<div className={`w-full ${!collapsed ? 'px-2' : ''}`}>
{collapsed ? (
<div className="pb-2">
<p className="mb-0 text-xs text-th-fgd-3">{t('account')}</p>
<p className="mb-0 font-bold text-th-fgd-1">
{abbreviateAddress(mangoAccount.publicKey)}
</p>
</div>
) : null}
<div className="pb-2">
<p className="mb-0 text-xs leading-4">{t('value')}</p>
<p className="mb-0 font-bold text-th-fgd-1">
{initialLoad ? <DataLoader /> : formatUsdValue(+equity)}
</p>
</div>
<div className="pb-2">
<p className="mb-0 text-xs leading-4">{t('health')}</p>
<p className="mb-0 font-bold text-th-fgd-1">
{health.gt(I80F48_100) ? '>100' : health.toFixed(2)}%
</p>
</div>
<div className="pb-2">
<p className="mb-0 text-xs leading-4">{t('leverage')}</p>
<p className="mb-0 font-bold text-th-fgd-1">
{initialLoad ? (
<DataLoader />
) : mangoAccount && mangoGroup && mangoCache ? (
`${mangoAccount
.getLeverage(mangoGroup, mangoCache)
.toFixed(2)}x`
) : (
'0.00x'
)}
</p>
</div>
<div className="pb-2">
<p className="mb-0 text-xs leading-4">
{t('collateral-available')}
</p>
<p className="mb-0 font-bold text-th-fgd-1">
{initialLoad ? (
<DataLoader />
) : mangoAccount && mangoGroup ? (
usdFormatter(
nativeI80F48ToUi(
initHealth,
mangoGroup.tokens[QUOTE_INDEX].decimals
).toFixed()
)
) : (
'--'
)}
</p>
</div>
<div>
<p className="mb-0 text-xs leading-4">
{marketConfig.name} {t('margin-available')}
</p>
<p className="mb-0 font-bold text-th-fgd-1">
{mangoAccount && mangoGroup && mangoCache
? usdFormatter(
nativeI80F48ToUi(
mangoAccount.getMarketMarginAvailable(
mangoGroup,
mangoCache,
marketConfig.marketIndex,
marketConfig.kind
),
mangoGroup.tokens[QUOTE_INDEX].decimals
).toFixed()
)
: '0.00'}
</p>
</div>
</div>
) : null}
</>
)
}
export default AccountOverviewPopover

View File

@ -114,25 +114,26 @@ const AccountSelect = ({
</Listbox.Button>
</div>
<Listbox.Options
className={`thin-scroll absolute right-0 top-14 z-20 max-h-60 w-full overflow-auto rounded-md bg-th-bkg-3 p-1`}
className={`thin-scroll absolute right-0 top-14 z-20 max-h-60 w-full overflow-auto rounded-md bg-th-bkg-2 p-1`}
>
{accounts.map((account) => {
const symbolForAccount = account.config.symbol
return (
<Listbox.Option
className="mb-0"
disabled={account.uiBalance === 0}
key={account?.account.publicKey.toBase58()}
value={account?.account.publicKey.toBase58()}
>
{({ disabled, selected }) => (
<div
className={`default-transition px-2 py-1 text-th-fgd-1 ${
className={`default-transition rounded p-2 text-th-fgd-1 ${
selected && `text-th-primary`
} ${
disabled
? 'text-th-fgd-1 opacity-50 hover:cursor-not-allowed hover:text-th-fgd-1'
: 'hover:cursor-pointer hover:text-th-primary'
: 'hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary'
}`}
>
<div className={`flex items-center`}>

View File

@ -5,7 +5,7 @@ import {
HeartIcon,
PlusCircleIcon,
UsersIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import useMangoStore from '../stores/useMangoStore'
import { MangoAccount, MangoGroup } from '@blockworks-foundation/mango-client'
import { abbreviateAddress, formatUsdValue } from '../utils'
@ -29,7 +29,7 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
isOpen,
onClose,
}) => {
const { t } = useTranslation('common')
const { t } = useTranslation(['common', 'delegate'])
const { publicKey } = useWallet()
const [showNewAccountForm, setShowNewAccountForm] = useState(false)
const [newAccPublicKey, setNewAccPublicKey] = useState(null)

View File

@ -1,13 +1,13 @@
import React, { useState } from 'react'
import { CheckCircleIcon } from '@heroicons/react/outline'
import { CheckCircleIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import Button from './Button'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
import Checkbox from './Checkbox'
import { SHOW_TOUR_KEY } from './IntroTips'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
// import { SHOW_TOUR_KEY } from './IntroTips'
// import { useViewport } from '../hooks/useViewport'
// import { breakpoints } from './TradePageGrid'
import { useRouter } from 'next/router'
import { LANGS } from './SettingsModal'
import { RadioGroup } from '@headlessui/react'
@ -24,13 +24,13 @@ const AlphaModal = ({
const { t } = useTranslation('common')
const [acceptRisks, setAcceptRisks] = useState(false)
const [, setAlphaAccepted] = useLocalStorageState(ALPHA_MODAL_KEY, false)
const [, setShowTips] = useLocalStorageState(SHOW_TOUR_KEY, false)
// const [, setShowTips] = useLocalStorageState(SHOW_TOUR_KEY, false)
const [savedLanguage, setSavedLanguage] = useLocalStorageState('language', '')
const [language, setLanguage] = useState('en')
const router = useRouter()
const { pathname, asPath, query } = router
const { width } = useViewport()
const hideTips = width ? width < breakpoints.md : false
// const { width } = useViewport()
// const hideTips = width ? width < breakpoints.md : false
const handleLanguageSelect = () => {
setSavedLanguage(language)
@ -41,10 +41,10 @@ const AlphaModal = ({
setAlphaAccepted(true)
}
const handleTakeTour = () => {
setAlphaAccepted(true)
setShowTips(true)
}
// const handleTakeTour = () => {
// setAlphaAccepted(true)
// setShowTips(true)
// }
return (
<Modal isOpen={isOpen} onClose={onClose} hideClose>
@ -104,7 +104,7 @@ const AlphaModal = ({
>
{t('get-started')}
</Button>
{!hideTips ? (
{/* {!hideTips ? (
<Button
className="w-40"
disabled={!acceptRisks}
@ -112,14 +112,18 @@ const AlphaModal = ({
>
{t('show-tips')}
</Button>
) : null}
) : null} */}
</div>
</>
) : (
<div className="pt-2">
<RadioGroup value={language} onChange={setLanguage}>
<div className="flex flex-col items-center pt-2">
<RadioGroup
className="w-full"
value={language}
onChange={setLanguage}
>
{LANGS.map((l) => (
<RadioGroup.Option className="" key={l.locale} value={l.locale}>
<RadioGroup.Option key={l.locale} value={l.locale}>
{({ checked }) => (
<div
className={`border ${
@ -137,9 +141,9 @@ const AlphaModal = ({
</RadioGroup.Option>
))}
</RadioGroup>
<div className="flex justify-center pt-4">
<Button onClick={() => handleLanguageSelect()}>Save</Button>
</div>
<Button className="mt-4" onClick={() => handleLanguageSelect()}>
Save
</Button>
</div>
)}
</Modal>

View File

@ -1,9 +1,8 @@
import { useCallback, useState } from 'react'
import { useBalances } from '../hooks/useBalances'
import useMangoStore from '../stores/useMangoStore'
import Button, { LinkButton } from '../components/Button'
import { notify } from '../utils/notifications'
import { ArrowSmDownIcon, ExclamationIcon } from '@heroicons/react/outline'
import { ArrowSmDownIcon, ExclamationIcon } from '@heroicons/react/solid'
import { Market } from '@project-serum/serum'
import { getTokenBySymbol } from '@blockworks-foundation/mango-client'
import Loading from './Loading'
@ -30,10 +29,10 @@ const BalancesTable = ({
const [showDepositModal, setShowDepositModal] = useState(false)
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
const [actionSymbol, setActionSymbol] = useState('')
const balances = useBalances()
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
const { items, requestSort, sortConfig } = useSortableData(
balances?.length > 0
? balances
spotBalances?.length > 0
? spotBalances
.filter((bal) => {
return (
showZeroBalances ||
@ -171,7 +170,7 @@ const BalancesTable = ({
}
}
const unsettledBalances = balances.filter(
const unsettledBalances = spotBalances.filter(
(bal) => bal.unsettled && bal.unsettled > 0
)
@ -209,20 +208,20 @@ const BalancesTable = ({
{submitting ? <Loading /> : t('settle-all')}
</Button>
</div>
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{unsettledBalances.map((bal) => {
const tokenConfig = getTokenBySymbol(mangoGroupConfig, bal.symbol)
return (
<div
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-3 px-5 py-3"
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-2 px-5 py-3"
key={bal.symbol}
>
<div className="flex space-x-2">
<div className="flex items-center">
<img
alt=""
width="24"
height="24"
width="20"
height="20"
src={`/assets/icons/${bal.symbol.toLowerCase()}.svg`}
className={`mr-3`}
/>
@ -511,9 +510,9 @@ const BalancesTable = ({
</Td>
{showDepositWithdraw ? (
<Td>
<div className="flex justify-end">
<div className="flex justify-end space-x-2">
<Button
className="h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
className="h-8 w-[86px] pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenDepositModal(balance.symbol)
}
@ -523,11 +522,12 @@ const BalancesTable = ({
: t('deposit')}
</Button>
<Button
className="ml-4 h-7 pt-0 pb-0 pl-3 pr-3 text-xs"
className="h-8 w-[86px] pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenWithdrawModal(balance.symbol)
}
disabled={!canWithdraw}
primary={false}
>
{t('withdraw')}
</Button>
@ -559,7 +559,7 @@ const BalancesTable = ({
)}
</Table>
) : (
<div className="border-b border-th-bkg-4">
<div className="border-b border-th-bkg-3">
<MobileTableHeader
colOneHeader={t('asset')}
colTwoHeader={t('net-balance')}
@ -652,7 +652,7 @@ const BalancesTable = ({
</div>
<div className="flex space-x-4">
<Button
className="h-7 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
className="h-8 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenDepositModal(balance.symbol)
}
@ -662,7 +662,7 @@ const BalancesTable = ({
: t('deposit')}
</Button>
<Button
className="h-7 w-1/2 pt-0 pb-0 pl-3 pr-3 text-xs"
className="h-8 w-1/2 border border-th-fgd-4 bg-transparent pt-0 pb-0 pl-3 pr-3 text-xs"
onClick={() =>
handleOpenWithdrawModal(balance.symbol)
}
@ -699,7 +699,7 @@ const BalancesTable = ({
)
) : (
<div
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
>
{t('no-balances')}
</div>

View File

@ -12,13 +12,16 @@ const Button: FunctionComponent<ButtonProps> = ({
onClick,
disabled = false,
className,
primary = true,
...props
}) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100 ${className}`}
className={`whitespace-nowrap rounded-full ${
primary ? 'bg-th-bkg-button' : 'border border-th-fgd-4'
} px-6 py-2 font-bold text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 md:hover:brightness-[1.1] md:disabled:hover:brightness-100 ${className}`}
{...props}
>
{children}
@ -42,7 +45,7 @@ export const LinkButton: FunctionComponent<ButtonProps> = ({
disabled={disabled}
className={`border-0 font-bold ${
primary ? 'text-th-primary' : 'text-th-fgd-2'
} underline hover:no-underline hover:opacity-60 focus:outline-none disabled:cursor-not-allowed disabled:underline disabled:opacity-60 ${className}`}
} underline focus:outline-none disabled:cursor-not-allowed disabled:underline disabled:opacity-60 md:hover:no-underline md:hover:opacity-60 ${className}`}
{...props}
>
{children}
@ -61,8 +64,8 @@ export const IconButton: FunctionComponent<ButtonProps> = ({
<button
onClick={onClick}
disabled={disabled}
className={`${className} flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none disabled:cursor-not-allowed
disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:text-th-fgd-4`}
className={`${className} flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4
disabled:text-th-fgd-4 md:hover:text-th-primary md:disabled:hover:text-th-fgd-4`}
{...props}
>
{children}

View File

@ -37,7 +37,7 @@ const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
${
v === activeValue
? `text-th-primary`
: `text-th-fgd-2 hover:text-th-primary`
: `text-th-fgd-2 md:hover:text-th-primary`
}
`}
key={`${v}${i}`}

View File

@ -30,6 +30,7 @@ interface ChartProps {
titleValue?: number
useMulticoloredBars?: boolean
zeroLine?: boolean
loading?: boolean
}
const Chart: FunctionComponent<ChartProps> = ({
@ -47,6 +48,7 @@ const Chart: FunctionComponent<ChartProps> = ({
titleValue,
useMulticoloredBars,
zeroLine,
loading,
}) => {
const [mouseData, setMouseData] = useState<string | null>(null)
const [daysToShow, setDaysToShow] = useState(daysRange || 30)
@ -83,266 +85,292 @@ const Chart: FunctionComponent<ChartProps> = ({
return (
<div className="h-52 w-full" ref={observe}>
<div className="flex w-full items-start justify-between pb-6">
<div className="pl-2">
<div className="pb-0.5 text-xs text-th-fgd-3">{title}</div>
{mouseData ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{labelFormat(mouseData[yAxis])}
{data.length > 0 ? (
<>
<div className="flex w-full items-start justify-between pb-6">
<div className="pl-2">
<div className="pb-0.5 text-xs text-th-fgd-3">{title}</div>
{mouseData ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{labelFormat(mouseData[yAxis])}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(mouseData[xAxis]).format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{titleValue
? labelFormat(titleValue)
: labelFormat(data[data.length - 1][yAxis])}
</div>
<div className="h-4 text-xs font-normal text-th-fgd-4">
{titleValue
? ''
: dayjs(data[data.length - 1][xAxis]).format(
'ddd MMM D YYYY, h:mma'
)}
</div>
</>
)}
</div>
{!hideRangeFilters ? (
<div className="flex h-5">
<button
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 1 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(1)}
>
24H
</button>
<button
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 7 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(7)}
>
7D
</button>
<button
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 30 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(30)}
>
30D
</button>
{showAll ? (
<button
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 1000 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(1000)}
>
All
</button>
) : null}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{new Date(mouseData[xAxis]).toDateString()}
</div>
</>
) : data.length > 0 ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{titleValue
? labelFormat(titleValue)
: labelFormat(data[data.length - 1][yAxis])}
</div>
<div className="h-4 text-xs font-normal text-th-fgd-4">
{titleValue
? ''
: new Date(data[data.length - 1][xAxis]).toDateString()}
</div>
</>
) : (
<>
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
</>
)}
</div>
{!hideRangeFilters ? (
<div className="flex h-5">
<button
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
daysToShow === 1 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(1)}
>
24H
</button>
<button
className={`default-transition mx-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
daysToShow === 7 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(7)}
>
7D
</button>
<button
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
daysToShow === 30 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(30)}
>
30D
</button>
{showAll ? (
<button
className={`default-transition ml-3 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
daysToShow === 1000 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(1000)}
>
All
</button>
) : null}
</div>
) : null}
</div>
{width > 0 && type === 'area' ? (
<AreaChart
width={width}
height={height}
data={data ? handleDaysToShow(daysToShow) : null}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
<stop offset="100%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yAxis}
stroke="#ffba24"
fill="url(#gradientArea)"
/>
<XAxis
dataKey={xAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
<YAxis
dataKey={yAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dx={-10}
domain={['dataMin', 'dataMax']}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat
? (v) => tickFormat(v)
: (v) => numberCompactFormatter.format(v)
}
type="number"
width={yAxisWidth || 50}
/>
{zeroLine ? (
<ReferenceLine
y={0}
stroke={
theme === 'Light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.35)'
}
strokeDasharray="3 3"
/>
) : null}
</AreaChart>
) : null}
{width > 0 && type === 'bar' ? (
<BarChart
width={width}
height={height}
data={
data
? hideRangeFilters
? data
: handleDaysToShow(daysToShow)
: null
}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
fill: '#fff',
opacity: 0.2,
}}
content={<></>}
/>
<defs>
<linearGradient id="defaultGradientBar" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
<stop offset="100%" stopColor="#ffba24" stopOpacity={0.5} />
</linearGradient>
<linearGradient id="greenGradientBar" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={1}
{width > 0 && type === 'area' ? (
<AreaChart
width={width}
height={height}
data={data ? handleDaysToShow(daysToShow) : null}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<stop
offset="100%"
stopColor={theme === 'Mango' ? '#91B503' : '#4BA53B'}
stopOpacity={1}
<defs>
<linearGradient id="gradientArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
<stop offset="100%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yAxis}
stroke="#ffba24"
fill="url(#gradientArea)"
/>
</linearGradient>
<linearGradient id="redGradientBar" x1="0" y1="1" x2="0" y2="0">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={1}
<XAxis
dataKey={xAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
<stop
offset="100%"
stopColor={theme === 'Mango' ? '#EC1809' : '#BB2525'}
stopOpacity={1}
/>
</linearGradient>
</defs>
<Bar dataKey={yAxis}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
useMulticoloredBars
? entry[yAxis] > 0
? 'url(#greenGradientBar)'
: 'url(#redGradientBar)'
: 'url(#defaultGradientBar)'
<YAxis
dataKey={yAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dx={-10}
domain={['dataMin', 'dataMax']}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat
? (v) => tickFormat(v)
: (v) => numberCompactFormatter.format(v)
}
type="number"
width={yAxisWidth || 50}
/>
))}
</Bar>
<XAxis
dataKey={xAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
<YAxis
dataKey={yAxis}
interval="preserveStartEnd"
axisLine={false}
hide={data.length > 0 ? false : true}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat
? (v) => tickFormat(v)
: (v) => numberCompactFormatter.format(v)
}
type="number"
width={yAxisWidth || 50}
/>
{zeroLine ? (
<ReferenceLine
y={0}
stroke={
theme === 'Light' ? 'rgba(0,0,0,0.4)' : 'rgba(255,255,255,0.2)'
}
/>
{zeroLine ? (
<ReferenceLine
y={0}
stroke={
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)'
}
strokeDasharray="3 3"
/>
) : null}
</AreaChart>
) : null}
</BarChart>
) : null}
{width > 0 && type === 'bar' ? (
<BarChart
width={width}
height={height}
data={
data
? hideRangeFilters
? data
: handleDaysToShow(daysToShow)
: null
}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Tooltip
cursor={{
fill: '#fff',
opacity: 0.2,
}}
content={<></>}
/>
<defs>
<linearGradient
id="defaultGradientBar"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#ffba24" stopOpacity={1} />
<stop offset="100%" stopColor="#ffba24" stopOpacity={0.5} />
</linearGradient>
<linearGradient
id="greenGradientBar"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={1}
/>
<stop
offset="100%"
stopColor={theme === 'Mango' ? '#91B503' : '#4BA53B'}
stopOpacity={1}
/>
</linearGradient>
<linearGradient id="redGradientBar" x1="0" y1="1" x2="0" y2="0">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={1}
/>
<stop
offset="100%"
stopColor={theme === 'Mango' ? '#EC1809' : '#BB2525'}
stopOpacity={1}
/>
</linearGradient>
</defs>
<Bar dataKey={yAxis}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
useMulticoloredBars
? entry[yAxis] > 0
? 'url(#greenGradientBar)'
: 'url(#redGradientBar)'
: 'url(#defaultGradientBar)'
}
/>
))}
</Bar>
<XAxis
dataKey={xAxis}
axisLine={false}
hide={data.length > 0 ? false : true}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
<YAxis
dataKey={yAxis}
interval="preserveStartEnd"
axisLine={false}
hide={data.length > 0 ? false : true}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat
? (v) => tickFormat(v)
: (v) => numberCompactFormatter.format(v)
}
type="number"
width={yAxisWidth || 50}
/>
{zeroLine ? (
<ReferenceLine
y={0}
stroke={
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.2)'
}
/>
) : null}
</BarChart>
) : null}
</>
) : loading ? (
<>
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
</>
) : (
<div className="flex h-full w-full items-center justify-center">
<p className="mb-0">Chart not available</p>
</div>
)}
</div>
)
}

View File

@ -6,10 +6,7 @@ import {
useState,
} from 'react'
import useMangoStore, { MNGO_INDEX } from '../stores/useMangoStore'
import {
CheckCircleIcon,
ExclamationCircleIcon,
} from '@heroicons/react/outline'
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
import Button from './Button'
import Modal from './Modal'
import { ElementTitle } from './styles'
@ -54,6 +51,11 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
const openOrders = useMangoStore((s) => s.selectedMangoAccount.openOrders)
const setMangoStore = useMangoStore((s) => s.set)
const activeAlerts = useMangoStore((s) => s.alerts.activeAlerts)
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
const unsettledBalances = spotBalances.filter(
(bal) => bal.unsettled && bal.unsettled > 0
)
const fetchTotalAccountSOL = useCallback(async () => {
if (!mangoAccount) {
@ -162,7 +164,10 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
}
const isDisabled =
(openOrders && openOrders.length > 0) || hasBorrows || hasOpenPositions
(openOrders && openOrders.length > 0) ||
hasBorrows ||
hasOpenPositions ||
!!unsettledBalances.length
return (
<Modal onClose={onClose} isOpen={isOpen && mangoAccount !== undefined}>
@ -240,6 +245,12 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
{t('close-account:close-open-orders')}
</div>
) : null}
{unsettledBalances.length ? (
<div className="flex items-center text-th-fgd-2">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4 text-th-red" />
{t('close-account:settle-balances')}
</div>
) : null}
</div>
</>
) : null}

View File

@ -21,8 +21,11 @@ import { useTranslation } from 'next-i18next'
import { WalletSelect } from 'components/WalletSelect'
import AccountsModal from './AccountsModal'
import uniqBy from 'lodash/uniqBy'
import NftProfilePicModal from './NftProfilePicModal'
import ProfileImage from './ProfileImage'
import { useRouter } from 'next/router'
import { PublicKey } from '@solana/web3.js'
import { breakpoints } from '../components/TradePageGrid'
import { useViewport } from 'hooks/useViewport'
export const handleWalletConnect = (wallet: Wallet) => {
if (!wallet) {
@ -43,14 +46,24 @@ export const handleWalletConnect = (wallet: Wallet) => {
export const ConnectWalletButton: React.FC = () => {
const { connected, publicKey, wallet, wallets, select } = useWallet()
const { t } = useTranslation(['common', 'profile'])
const pfp = useMangoStore((s) => s.wallet.pfp)
const router = useRouter()
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
const set = useMangoStore((s) => s.set)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
const actions = useMangoStore((s) => s.actions)
const profileDetails = useMangoStore((s) => s.profile.details)
const loadProfileDetails = useMangoStore((s) => s.profile.loadDetails)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
useEffect(() => {
if (publicKey) {
actions.fetchProfileDetails(publicKey.toString())
}
}, [publicKey])
const installedWallets = useMemo(() => {
const installed: Wallet[] = []
@ -80,10 +93,6 @@ export const ConnectWalletButton: React.FC = () => {
setShowAccountsModal(false)
}, [])
const handleCloseProfilePicModal = useCallback(() => {
setShowProfilePicModal(false)
}, [])
const handleDisconnect = useCallback(() => {
wallet?.adapter?.disconnect()
set((state) => {
@ -115,16 +124,27 @@ export const ConnectWalletButton: React.FC = () => {
{({ open }) => (
<div className="relative" id="profile-menu-tip">
<Menu.Button
className={`flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-button hover:bg-th-bkg-4 hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none ${
className={`flex h-14 ${
!isMobile ? 'w-48 border-x border-th-bkg-3 px-3' : ''
} items-center rounded-none rounded-full hover:bg-th-bkg-2 focus:outline-none ${
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : ''
}`}
>
<ProfileImage
thumbHeightClass="h-10"
thumbWidthClass="w-10"
placeholderHeightClass="h-6"
placeholderWidthClass="w-6"
/>
<ProfileImage imageSize="40" placeholderSize="24" />
{!loadProfileDetails && !isMobile ? (
<div className="ml-2 w-32 text-left">
<p className="mb-0.5 truncate text-xs font-bold capitalize text-th-fgd-1">
{profileDetails.profile_name}
</p>
<p className="mb-0 text-xs text-th-fgd-4">
{profileDetails.wallet_pk
? abbreviateAddress(
new PublicKey(profileDetails.wallet_pk)
)
: ''}
</p>
</div>
) : null}
</Menu.Button>
<Transition
appear={true}
@ -137,7 +157,18 @@ export const ConnectWalletButton: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 z-20 mt-1 w-48 space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5">
<Menu.Items className="absolute right-0 z-20 mt-1 w-48 space-y-1.5 rounded-md bg-th-bkg-2 px-4 py-2.5">
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => router.push('/profile')}
>
<UserCircleIcon className="h-4 w-4" />
<div className="pl-2 text-left">
{t('profile:profile')}
</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
@ -149,20 +180,7 @@ export const ConnectWalletButton: React.FC = () => {
</Menu.Item>
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowProfilePicModal(true)}
>
<UserCircleIcon className="h-4 w-4" />
<div className="pl-2 text-left">
{pfp?.isAvailable
? t('profile:edit-profile-pic')
: t('profile:set-profile-pic')}
</div>
</button>
</Menu.Item>
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer focus:outline-none md:hover:text-th-primary"
onClick={handleDisconnect}
>
<LogoutIcon className="h-4 w-4" />
@ -181,18 +199,18 @@ export const ConnectWalletButton: React.FC = () => {
</Menu>
) : (
<div
className="flex h-14 justify-between divide-x divide-th-bkg-3"
className="flex h-14 divide-x divide-th-bkg-3"
id="connect-wallet-tip"
>
<button
onClick={handleConnect}
disabled={!mangoGroup}
className="rounded-none bg-th-primary-dark text-th-bkg-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-wait disabled:text-th-bkg-2"
className="rounded-none bg-th-primary-dark text-th-bkg-1 focus:outline-none disabled:cursor-wait disabled:text-th-bkg-2"
>
<div className="default-transition flex h-full flex-row items-center justify-center px-3">
<WalletIcon className="mr-2 h-4 w-4 fill-current" />
<div className="text-left">
<div className="mb-0.5 whitespace-nowrap font-bold">
<div className="mb-1 whitespace-nowrap font-bold leading-none">
{t('connect')}
</div>
{wallet?.adapter?.name && (
@ -214,12 +232,6 @@ export const ConnectWalletButton: React.FC = () => {
isOpen={showAccountsModal}
/>
)}
{showProfilePicModal && (
<NftProfilePicModal
onClose={handleCloseProfilePicModal}
isOpen={showProfilePicModal}
/>
)}
</>
)
}

View File

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react'
import { PlusCircleIcon, TrashIcon } from '@heroicons/react/outline'
import { PlusCircleIcon, TrashIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import Input, { Label } from './Input'
import { ElementTitle } from './styles'

View File

@ -4,7 +4,7 @@ import {
ExclamationCircleIcon,
XIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import Input, { Label } from './Input'
import Tooltip from './Tooltip'
import Button, { IconButton } from './Button'
@ -119,7 +119,7 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
</div>
}
>
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-primary" />
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
</Tooltip>
</ElementTitle>
</div>

View File

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/outline'
import { ExclamationCircleIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import Input, { Label } from './Input'
import AccountSelect from './AccountSelect'
@ -14,6 +14,8 @@ import { sleep, trimDecimals } from '../utils'
import { useTranslation } from 'next-i18next'
import ButtonGroup from './ButtonGroup'
import { useWallet } from '@solana/wallet-adapter-react'
import MangoAccountSelect from './MangoAccountSelect'
import { MangoAccount } from '@blockworks-foundation/mango-client'
interface DepositModalProps {
onClose: () => void
@ -38,6 +40,9 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
const actions = useMangoStore((s) => s.actions)
const [selectedAccount, setSelectedAccount] = useState(walletTokens[0])
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
const [depositMangoAccount, setDepositMangoAccount] =
useState<MangoAccount | null>(mangoAccount)
useEffect(() => {
if (tokenSymbol) {
@ -60,14 +65,13 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
}
const handleDeposit = () => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
if (!wallet) return
if (!wallet || !depositMangoAccount) return
setSubmitting(true)
deposit({
amount: parseFloat(inputAmount),
fromTokenAcc: selectedAccount.account,
mangoAccount,
mangoAccount: depositMangoAccount,
wallet,
})
.then((response) => {
@ -191,6 +195,15 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
/>
</div>
) : null}
{mangoAccounts.length > 1 ? (
<div className="mb-4">
<Label>{t('to-account')}</Label>
<MangoAccountSelect
onChange={(v) => setDepositMangoAccount(v)}
value={depositMangoAccount}
/>
</div>
) : null}
<AccountSelect
accounts={walletTokens}
selectedAccount={selectedAccount}

View File

@ -1,7 +1,6 @@
import useLocalStorageState from '../hooks/useLocalStorageState'
import { FAVORITE_MARKETS_KEY } from './TradeNavMenu'
import { StarIcon } from '@heroicons/react/solid'
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
import { StarIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import Link from 'next/link'
@ -31,7 +30,7 @@ const FavoritesShortcutBar = () => {
return !isMobile ? (
<Transition
appear={true}
className="flex items-center space-x-4 bg-th-bkg-3 px-4 py-2 xl:px-6"
className="flex items-center space-x-4 border-b border-th-bkg-3 py-1 px-6"
show={favoriteMarkets.length > 0}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0"

View File

@ -35,14 +35,14 @@ export const FiveOhFive = ({ error }) => {
const Icon = showDetails ? ChevronDownIcon : ChevronRightIcon
return (
<div className="bg-bg-texture flex min-h-screen flex-col bg-cover bg-bottom bg-no-repeat">
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
<main className="my-[-2] mx-auto w-full max-w-7xl flex-grow px-4 sm:px-6 lg:px-8">
<div className="flex h-screen flex-col bg-th-bkg-1">
<div className="absolute top-0 h-2 w-full bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
<main className="mx-auto w-full max-w-7xl flex-grow px-4 sm:px-6 lg:px-8">
<div className="flex-shrink-0 pt-16">
<img
className="mx-auto h-12 w-auto"
src="/assets/logotext.svg"
alt="Workflow"
src="/assets/icons/mngo.svg"
alt="Logo"
/>
</div>
<div className="mx-auto max-w-xl py-16 sm:py-24">
@ -50,10 +50,10 @@ export const FiveOhFive = ({ error }) => {
<p className="text-sm font-semibold uppercase tracking-wide">
<GradientText>500 error</GradientText>
</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
<h1 className="mt-2 text-3xl font-extrabold tracking-tight text-white sm:text-5xl">
Something went wrong
</h1>
<p className="mt-2 text-lg text-gray-500">
<p className="mt-2 text-base text-th-fgd-3">
The page you are looking for could not be loaded.
</p>
</div>
@ -85,7 +85,7 @@ export const FiveOhFive = ({ error }) => {
<div className="flex flex-col items-center">
<div className="mt-10 flex flex-row">
<button
className="mx-2 whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
className="mx-2 whitespace-nowrap rounded-full bg-th-bkg-button px-6 py-2 font-bold text-th-fgd-1 focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4"
onClick={() => location.reload()}
>
Refresh and try again
@ -109,7 +109,7 @@ export const FiveOhFive = ({ error }) => {
</div>
</main>
<footer className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="border-t border-gray-200 py-10 text-center md:flex md:justify-between">
<div className="border-t border-th-bkg-4 py-10 text-center md:flex md:justify-between">
<div className="mt-6 flex justify-center space-x-8 md:mt-0">
{social.map((item, itemIdx) => (
<a
@ -124,7 +124,7 @@ export const FiveOhFive = ({ error }) => {
</div>
</div>
</footer>
<div className="h-2 w-screen bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
<div className="absolute bottom-0 h-2 w-full bg-gradient-to-r from-mango-theme-green via-mango-theme-yellow-dark to-mango-theme-red-dark"></div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import React, { FunctionComponent, useCallback } from 'react'
import { LinkIcon } from '@heroicons/react/outline'
import { LinkIcon } from '@heroicons/react/solid'
import useMangoStore from '../stores/useMangoStore'
import { MoveIcon } from './icons'
import EmptyState from './EmptyState'
@ -7,6 +7,8 @@ import { useTranslation } from 'next-i18next'
import { handleWalletConnect } from 'components/ConnectWalletButton'
import { useWallet } from '@solana/wallet-adapter-react'
import { useRouter } from 'next/router'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
interface FloatingElementProps {
className?: string
@ -24,6 +26,8 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const router = useRouter()
const { pubkey } = router.query
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const handleConnect = useCallback(() => {
if (wallet) {
@ -33,7 +37,9 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
return (
<div
className={`thin-scroll relative overflow-auto overflow-x-hidden rounded-lg bg-th-bkg-2 p-2.5 md:p-4 ${className}`}
className={`thin-scroll relative overflow-auto overflow-x-hidden rounded-md ${
!isMobile ? 'border border-th-bkg-3' : ''
} bg-th-bkg-1 p-2.5 md:p-4 ${className}`}
>
{!connected && showConnect && !pubkey ? (
<div className="absolute top-0 left-0 z-10 h-full w-full">
@ -46,7 +52,7 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
title={t('connect-wallet')}
/>
</div>
<div className="absolute top-0 left-0 h-full w-full rounded-lg bg-th-bkg-2 opacity-50" />
<div className="absolute top-0 left-0 h-full w-full rounded-lg bg-th-bkg-1 opacity-50" />
</div>
) : null}
{!uiLocked ? (

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
import sumBy from 'lodash/sumBy'
import useInterval from '../hooks/useInterval'
import { SECONDS } from '../stores/useMangoStore'
import { CLUSTER, SECONDS } from '../stores/useMangoStore'
import { useTranslation } from 'next-i18next'
import { ExclamationIcon } from '@heroicons/react/outline'
import { ExclamationIcon } from '@heroicons/react/solid'
import { Connection } from '@solana/web3.js'
const tpsAlertThreshold = 1000
@ -43,7 +43,7 @@ const GlobalNotification = () => {
getRecentPerformance(setShow, setTps)
}, 45 * SECONDS)
if (show) {
if (show && CLUSTER == 'mainnet') {
return (
<div className="flex items-center bg-th-bkg-4 text-th-fgd-2">
<div className="flex w-full items-center justify-center p-1">

View File

@ -0,0 +1,46 @@
const HealthHeart = ({ health, size }: { health: number; size: number }) => {
const styles = {
height: `${size}px`,
width: `${size}px`,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={
health > 15 && health < 50
? 'text-th-orange'
: health > 50
? 'text-th-green'
: 'text-th-red'
}
style={styles}
viewBox="0 0 20 20"
fill="currentColor"
>
<g transform-origin="center">
<path
fillRule="evenodd"
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
clipRule="evenodd"
/>
<animateTransform
attributeName="transform"
type="scale"
keyTimes="0;0.5;1"
values="1;1.1;1"
dur={health > 15 && health < 50 ? '1s' : health > 50 ? '2s' : '0.33s'}
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
values="0.8;1;0.8"
dur={health > 15 && health < 50 ? '1s' : health > 50 ? '2s' : '0.33s'}
repeatCount="indefinite"
/>
</g>
</svg>
)
}
export default HealthHeart

View File

@ -4,7 +4,7 @@ import {
ExclamationCircleIcon,
ExclamationIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
interface InlineNotificationProps {
desc?: string | (() => string)
@ -38,7 +38,7 @@ const InlineNotification: FunctionComponent<InlineNotificationProps> = ({
<ExclamationIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-orange" />
) : null}
{type === 'info' ? (
<InformationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-fgd-3" />
<InformationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0 text-th-fgd-4" />
) : null}
<div>
<div className="text-th-fgd-3">{title}</div>

File diff suppressed because it is too large Load Diff

225
components/Layout.tsx Normal file
View File

@ -0,0 +1,225 @@
import SideNav from './SideNav'
import { breakpoints } from '../components/TradePageGrid'
import { useViewport } from 'hooks/useViewport'
import BottomBar from './mobile/BottomBar'
import { ConnectWalletButton } from './ConnectWalletButton'
import GlobalNotification from './GlobalNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { abbreviateAddress } from 'utils'
import { useCallback, useEffect, useState } from 'react'
import AccountsModal from './AccountsModal'
import { useRouter } from 'next/router'
import FavoritesShortcutBar from './FavoritesShortcutBar'
import {
ArrowRightIcon,
ChevronRightIcon,
CogIcon,
ExclamationCircleIcon,
UsersIcon,
} from '@heroicons/react/solid'
import Button, { IconButton } from './Button'
import SettingsModal from './SettingsModal'
import { useTranslation } from 'next-i18next'
import { useWallet } from '@solana/wallet-adapter-react'
import DepositModal from './DepositModal'
import WithdrawModal from './WithdrawModal'
import Tooltip from './Tooltip'
const Layout = ({ children }) => {
const { t } = useTranslation(['common', 'delegate'])
const { connected, publicKey } = useWallet()
const { mangoAccount, initialLoad } = useMangoAccount()
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showDepositModal, setShowDepositModal] = useState(false)
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
const [isCollapsed, setIsCollapsed] = useState(false)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const router = useRouter()
const { pathname } = router
const { pubkey } = router.query
const canWithdraw =
mangoAccount?.owner && publicKey
? mangoAccount?.owner?.equals(publicKey)
: false
useEffect(() => {
const collapsed = width ? width < breakpoints.lg : false
setIsCollapsed(collapsed)
}, [])
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
const handleToggleSidebar = () => {
setIsCollapsed(!isCollapsed)
setTimeout(() => {
window.dispatchEvent(new Event('resize'))
}, 100)
}
return (
<div className={`flex-grow bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<div className="flex">
{isMobile ? (
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<BottomBar />
</div>
) : (
<div className={isCollapsed ? 'mr-[64px]' : 'mr-[220px]'}>
<div className={`fixed z-20 h-screen`}>
<button
className="absolute -right-4 top-1/2 z-20 h-10 w-4 -translate-y-1/2 transform rounded-none rounded-r bg-th-bkg-4 focus:outline-none"
onClick={handleToggleSidebar}
>
<ChevronRightIcon
className={`default-transition h-full w-full ${
!isCollapsed ? 'rotate-180' : 'rotate-360'
}`}
/>
</button>
<div
className={`h-full ${!isCollapsed ? 'overflow-y-auto' : ''}`}
>
<SideNav collapsed={isCollapsed} />
</div>
</div>
</div>
)}
<div className="w-full overflow-hidden">
<GlobalNotification />
<div className="flex h-14 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1 px-6">
{mangoAccount && mangoAccount.beingLiquidated ? (
<div className="flex items-center justify-center">
<ExclamationCircleIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red" />
<span className="text-th-red">{t('being-liquidated')}</span>
</div>
) : (
<div className="flex items-center text-th-fgd-3">
<span className="mb-0 mr-2 text-base">
{pubkey
? '🔎'
: connected
? initialLoad
? ''
: mangoAccount
? '🟢'
: '👋'
: !isMobile
? '🔗'
: ''}
</span>
{connected || pubkey ? (
!initialLoad ? (
mangoAccount ? (
<div
className="default-transition flex items-center font-bold text-th-fgd-1 hover:text-th-fgd-3"
role="button"
onClick={() => setShowAccountsModal(true)}
>
{`${
mangoAccount.name
? mangoAccount.name
: abbreviateAddress(mangoAccount.publicKey)
}`}
{publicKey && !mangoAccount.owner.equals(publicKey) ? (
<Tooltip content={t('delegate:delegated-account')}>
<UsersIcon className="ml-2 h-5 w-5 text-th-fgd-3" />
</Tooltip>
) : (
''
)}
</div>
) : (
<span className="flex items-center text-th-fgd-3">
{t('create-account-helper')}
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
</span>
)
) : (
<div className="h-4 w-32 animate-pulse rounded bg-th-bkg-3" />
)
) : !isMobile ? (
<span className="flex items-center text-th-fgd-3">
{t('connect-helper')}
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
</span>
) : null}
</div>
)}
<div className="flex items-center space-x-4">
{!isMobile && connected && !initialLoad ? (
<div className="flex space-x-2">
{mangoAccount ? (
<Button
className="flex h-8 w-[86px] items-center justify-center pl-3 pr-3 text-xs"
onClick={() => setShowDepositModal(true)}
>
{t('deposit')}
</Button>
) : (
<Button
className="flex h-8 w-32 items-center justify-center pl-3 pr-3 text-xs"
onClick={() => setShowAccountsModal(true)}
>
{t('create-account')}
</Button>
)}
{canWithdraw ? (
<Button
className="flex h-8 w-[86px] items-center justify-center pl-3 pr-3 text-xs"
onClick={() => setShowWithdrawModal(true)}
primary={false}
>
{t('withdraw')}
</Button>
) : null}
</div>
) : null}
<IconButton
className="h-8 w-8"
onClick={() => setShowSettingsModal(true)}
>
<CogIcon className="h-5 w-5" />
</IconButton>
<ConnectWalletButton />
</div>
</div>
{pathname === '/' ? <FavoritesShortcutBar /> : null}
<div className={pathname === '/' ? 'px-3' : 'px-6 pb-16 md:pb-6'}>
{children}
</div>
</div>
</div>
{showAccountsModal && (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
)}
{showSettingsModal ? (
<SettingsModal
onClose={() => setShowSettingsModal(false)}
isOpen={showSettingsModal}
/>
) : null}
{showDepositModal && (
<DepositModal
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
/>
)}
{showWithdrawModal && (
<WithdrawModal
isOpen={showWithdrawModal}
onClose={() => setShowWithdrawModal(false)}
/>
)}
</div>
)
}
export default Layout

View File

@ -1,60 +1,94 @@
import { useEffect, useMemo, useState } from 'react'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import { usdFormatter } from '../utils'
import { MedalIcon, ProfileIcon } from './icons'
import { MedalIcon } from './icons'
import { useTranslation } from 'next-i18next'
import {
ChartPieIcon,
ExternalLinkIcon,
TrendingUpIcon,
} from '@heroicons/react/outline'
import { getProfilePicture } from '@solflare-wallet/pfp'
import useMangoStore from '../stores/useMangoStore'
import { connectionSelector } from '../stores/selectors'
import { abbreviateAddress, usdFormatter } from '../utils'
import { ChevronRightIcon } from '@heroicons/react/solid'
import ProfileImage from './ProfileImage'
import { useRouter } from 'next/router'
import { PublicKey } from '@solana/web3.js'
import { notify } from 'utils/notifications'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
const LeaderboardTable = ({ range = '29' }) => {
const { t } = useTranslation('common')
const [pnlLeaderboardData, setPnlLeaderboardData] = useState<any[]>([])
const [perpPnlLeaderboardData, setPerpPnlLeaderboardData] = useState<any[]>(
[]
)
const [spotPnlLeaderboardData, setSpotPnlLeaderboardData] = useState<any[]>(
[]
)
const [leaderboardType, setLeaderboardType] = useState<string>('total-pnl')
const [loading, setLoading] = useState(false)
const connection = useMangoStore(connectionSelector)
const formatLeaderboardData = async (leaderboard) => {
const walletPks = leaderboard.map((u) => u.wallet_pk)
const profileDetailsResponse = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/user-data/multiple-profile-details?wallet-pks=${walletPks.toString()}`
)
const parsedProfileDetailsResponse = await profileDetailsResponse.json()
const leaderboardData = [] as any[]
for (const item of leaderboard) {
const profileDetails = parsedProfileDetailsResponse[item.wallet_pk]
leaderboardData.push({
...item,
profile: profileDetails ? profileDetails : null,
})
}
return leaderboardData
}
const fetchPnlLeaderboard = async () => {
setLoading(true)
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/pnl-leaderboard?start-date=${dayjs()
.utc()
.hour(0)
.minute(0)
.subtract(parseInt(range), 'day')
.add(1, 'hour')
.format('YYYY-MM-DDThh:00:00')}`
)
const parsedResponse = await response.json()
const leaderboardData = [] as any[]
for (const item of parsedResponse) {
const { isAvailable, url } = await getProfilePicture(
connection,
item.wallet_pk
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/pnl-leaderboard?start-date=${dayjs()
.utc()
.hour(0)
.minute(0)
.subtract(parseInt(range), 'day')
.add(1, 'hour')
.format('YYYY-MM-DDThh:00:00')}`
)
leaderboardData.push({
...item,
pfp: { isAvailable: isAvailable, url: url },
})
const parsedResponse = await response.json()
const leaderboardData = await formatLeaderboardData(parsedResponse)
setPnlLeaderboardData(leaderboardData)
setLoading(false)
} catch {
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
setLoading(false)
}
setPnlLeaderboardData(leaderboardData)
setLoading(false)
}
const fetchPerpPnlLeaderboard = async () => {
setLoading(true)
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/perp-pnl-leaderboard?start-date=${dayjs()
.utc()
.hour(0)
.minute(0)
.subtract(parseInt(range), 'day')
.add(1, 'hour')
.format('YYYY-MM-DDThh:00:00')}`
)
const parsedResponse = await response.json()
const leaderboardData = await formatLeaderboardData(parsedResponse)
setPerpPnlLeaderboardData(leaderboardData)
setLoading(false)
} catch {
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
setLoading(false)
}
}
const fetchSpotPnlLeaderboard = async () => {
setLoading(true)
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/perp-pnl-leaderboard?start-date=${dayjs()
`https://mango-transaction-log.herokuapp.com/v3/stats/spot-pnl-leaderboard?start-date=${dayjs()
.hour(0)
.minute(0)
.utc()
@ -62,7 +96,8 @@ const LeaderboardTable = ({ range = '29' }) => {
.format('YYYY-MM-DDThh:00:00')}`
)
const parsedResponse = await response.json()
setPerpPnlLeaderboardData(parsedResponse)
const leaderboardData = await formatLeaderboardData(parsedResponse)
setSpotPnlLeaderboardData(leaderboardData)
setLoading(false)
}
@ -70,21 +105,31 @@ const LeaderboardTable = ({ range = '29' }) => {
useEffect(() => {
if (leaderboardType === 'total-pnl') {
fetchPnlLeaderboard()
} else {
} else if (leaderboardType === 'futures-only') {
fetchPerpPnlLeaderboard()
} else {
fetchSpotPnlLeaderboard()
}
}, [range, leaderboardType])
useEffect(() => {
fetchPerpPnlLeaderboard()
fetchSpotPnlLeaderboard()
}, [])
const leaderboardData = useMemo(
() =>
leaderboardType === 'total-pnl'
? pnlLeaderboardData
: perpPnlLeaderboardData,
[leaderboardType, pnlLeaderboardData, perpPnlLeaderboardData]
: leaderboardType === 'futures-only'
? perpPnlLeaderboardData
: spotPnlLeaderboardData,
[
leaderboardType,
pnlLeaderboardData,
perpPnlLeaderboardData,
spotPnlLeaderboardData,
]
)
return (
@ -95,14 +140,18 @@ const LeaderboardTable = ({ range = '29' }) => {
setLeaderboardType={setLeaderboardType}
range={range}
label="total-pnl"
icon={<ChartPieIcon className="mr-3 hidden h-6 w-6 lg:block" />}
/>
<LeaderboardTypeButton
leaderboardType={leaderboardType}
setLeaderboardType={setLeaderboardType}
range={range}
label="futures-only"
icon={<TrendingUpIcon className="mr-3 hidden h-6 w-6 lg:block" />}
/>
<LeaderboardTypeButton
leaderboardType={leaderboardType}
setLeaderboardType={setLeaderboardType}
range={range}
label="spot-only"
/>
</div>
<div className="col-span-12 lg:col-span-8">
@ -114,7 +163,11 @@ const LeaderboardTable = ({ range = '29' }) => {
acc={acc.mango_account}
key={acc.mango_account}
rawPnl={
leaderboardType === 'total-pnl' ? acc.pnl : acc.perp_pnl
leaderboardType === 'total-pnl'
? acc.pnl
: leaderboardType === 'futures-only'
? acc.perp_pnl
: acc.spot_pnl
}
pnl={
leaderboardType === 'total-pnl'
@ -123,34 +176,37 @@ const LeaderboardTable = ({ range = '29' }) => {
currency: 'USD',
maximumFractionDigits: 0,
})
: usdFormatter(acc.perp_pnl)
: leaderboardType === 'futures-only'
? usdFormatter(acc.perp_pnl)
: usdFormatter(acc.spot_pnl)
}
pfp={acc.pfp}
walletPk={acc.wallet_pk}
profile={acc.profile}
/>
))}
</div>
) : (
<div className="space-y-2">
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-20 w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
</div>
)}
</div>
@ -160,7 +216,8 @@ const LeaderboardTable = ({ range = '29' }) => {
export default LeaderboardTable
const AccountCard = ({ rank, acc, pnl, pfp, rawPnl }) => {
const AccountCard = ({ rank, acc, rawPnl, profile, pnl, walletPk }) => {
const router = useRouter()
const medalColors =
rank === 1
? {
@ -183,51 +240,63 @@ const AccountCard = ({ rank, acc, pnl, pfp, rawPnl }) => {
lightest: '#EFBF8D',
}
return (
<a
href={`https://trade.mango.markets/account?pubkey=${acc}`}
target="_blank"
rel="noopener noreferrer"
className="default-transition flex items-center rounded-lg p-4 ring-1 ring-inset ring-th-bkg-4 hover:bg-th-bkg-3"
>
<p className="mb-0 mr-4 font-bold">{rank}</p>
<div className="relative mr-3 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-th-bkg-4">
{rank < 4 ? (
<MedalIcon
className="absolute -top-2 -left-2 h-5 w-auto drop-shadow-lg"
colors={medalColors}
<div className="relative" key={acc}>
{profile ? (
<button
className="absolute left-[118px] bottom-4 flex items-center space-x-2 rounded-full border border-th-fgd-4 px-2 py-1 hover:border-th-fgd-2 hover:filter"
onClick={() =>
router.push(
`/profile?name=${profile?.profile_name.replace(/\s/g, '-')}`,
undefined,
{
shallow: true,
}
)
}
>
<p className="mb-0 text-xs capitalize text-th-fgd-3">
{profile?.profile_name}
</p>
</button>
) : null}
<a
className="default-transition block flex h-[112px] w-full rounded-md border border-th-bkg-4 p-4 hover:border-th-fgd-4 sm:h-[84px] sm:justify-between sm:pb-4"
href={`/account?pubkey=${acc}`}
target="_blank"
rel="noopener noreferrer"
>
<p className="my-auto mr-4 flex w-5 justify-center font-bold">{rank}</p>
<div className="relative my-auto">
{rank < 4 ? (
<MedalIcon
className="absolute -top-1 -left-1 z-10 h-5 w-auto drop-shadow-lg"
colors={medalColors}
/>
) : null}
<ProfileImage
imageSize="56"
placeholderSize="32"
publicKey={walletPk}
/>
) : null}
{pfp?.isAvailable ? (
<img
alt=""
src={pfp.url}
className={`default-transition h-12 w-12 rounded-full hover:opacity-60
`}
/>
) : (
<ProfileIcon className={`h-7 w-7 text-th-fgd-3`} />
)}
</div>
<div className="flex w-full flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="mb-0 text-th-fgd-2">{`${acc.slice(0, 5)}...${acc.slice(
-5
)}`}</p>
</div>
<div>
<div className="flex items-center">
<span
className={`text-base font-bold text-th-fgd-2 sm:text-lg ${
rawPnl > 0 ? 'text-th-green' : 'text-th-red'
}`}
>
{pnl}
</span>
</div>
<div className="ml-3 flex flex-col sm:flex-grow sm:flex-row sm:justify-between">
<p className="mb-0 font-bold text-th-fgd-2">
{abbreviateAddress(new PublicKey(acc))}
</p>
<span
className={`flex items-center text-lg font-bold ${
rawPnl > 0 ? 'text-th-green' : 'text-th-red'
}`}
>
{pnl}
</span>
</div>
</div>
<ExternalLinkIcon className="ml-3 h-4 w-4 flex-shrink-0 text-th-fgd-3" />
</a>
<div className="my-auto ml-auto">
<ChevronRightIcon className="ml-2 mt-0.5 h-5 w-5 text-th-fgd-4" />
</div>
</a>
</div>
)
}
@ -237,26 +306,34 @@ const LeaderboardTypeButton = ({
range,
icon,
label,
}: {
leaderboardType: string
setLeaderboardType: (x) => void
range: string
icon?: ReactNode
label: string
}) => {
const { t } = useTranslation('common')
return (
<button
className={`relative flex w-full items-center justify-center rounded-md p-4 text-center lg:h-20 lg:justify-start lg:text-left ${
className={`relative flex w-full items-center justify-center rounded-md p-4 text-center lg:h-20 lg:justify-start lg:px-6 lg:text-left ${
leaderboardType === label
? 'bg-th-bkg-4 text-th-fgd-1 after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:transform after:border-l-[12px] after:border-r-[12px] after:border-t-[12px] after:border-l-transparent after:border-t-th-bkg-4 after:border-r-transparent lg:after:left-[100%] lg:after:top-1/2 lg:after:-translate-x-0 lg:after:-translate-y-1/2 lg:after:border-r-0 lg:after:border-b-[12px] lg:after:border-t-transparent lg:after:border-b-transparent lg:after:border-l-th-bkg-4'
: 'bg-th-bkg-3 text-th-fgd-4 hover:bg-th-bkg-4'
? 'bg-th-bkg-3 text-th-fgd-1 after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:transform after:border-l-[12px] after:border-r-[12px] after:border-t-[12px] after:border-l-transparent after:border-t-th-bkg-3 after:border-r-transparent lg:after:left-[100%] lg:after:top-1/2 lg:after:-translate-x-0 lg:after:-translate-y-1/2 lg:after:border-r-0 lg:after:border-b-[12px] lg:after:border-t-transparent lg:after:border-b-transparent lg:after:border-l-th-bkg-3'
: 'bg-th-bkg-2 text-th-fgd-3 md:hover:bg-th-bkg-3'
}`}
onClick={() => setLeaderboardType(label)}
>
{icon}
<div>
<div className="font-bold sm:text-lg">{t(label)}</div>
<span className="text-th-fgd-4">
<span className="text-sm text-th-fgd-4">
{range === '9999'
? 'All-time'
: range === '29'
? '30-day'
: `${range}-day`}
? 'Last 30 days'
: range === '1'
? 'Last 24 hours'
: `Last ${range} days`}
</span>
</div>
</button>

View File

@ -0,0 +1,120 @@
import { MangoAccount, MangoGroup } from '@blockworks-foundation/mango-client'
import {
ArrowSmDownIcon,
ArrowSmUpIcon,
HeartIcon,
UsersIcon,
} from '@heroicons/react/outline'
import { useWallet } from '@solana/wallet-adapter-react'
import { useTranslation } from 'next-i18next'
import useMangoStore from 'stores/useMangoStore'
import { abbreviateAddress } from 'utils'
import Tooltip from './Tooltip'
export const numberCurrencyCompacter = Intl.NumberFormat('en-us', {
notation: 'compact',
style: 'currency',
currency: 'USD',
maximumFractionDigits: 2,
})
const MangoAccountCard = ({
mangoAccount,
pnl,
}: {
mangoAccount: MangoAccount
pnl?: number
}) => {
const { t } = useTranslation('common')
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const { publicKey } = useWallet()
return (
<div>
<p className="mb-1 flex items-center font-bold text-th-fgd-1">
{pnl ? (
<a
className="default-transition text-th-fgd-1 hover:text-th-fgd-3"
href={`https://trade.mango.markets/account?pubkey=${mangoAccount.publicKey.toString()}`}
target="_blank"
rel="noopener noreferrer"
>
{mangoAccount?.name || abbreviateAddress(mangoAccount.publicKey)}
</a>
) : (
<span>
{mangoAccount?.name || abbreviateAddress(mangoAccount.publicKey)}
</span>
)}
{publicKey && !mangoAccount?.owner.equals(publicKey) ? (
<Tooltip content={t('delegate:delegated-account')}>
<UsersIcon className="ml-1.5 h-3 w-3 text-th-fgd-3" />
</Tooltip>
) : (
''
)}
</p>
{mangoGroup && (
<div className="text-xs text-th-fgd-3">
<AccountInfo
mangoGroup={mangoGroup}
mangoAccount={mangoAccount}
pnl={pnl}
/>
</div>
)}
</div>
)
}
export default MangoAccountCard
const AccountInfo = ({
mangoGroup,
mangoAccount,
pnl,
}: {
mangoGroup: MangoGroup
mangoAccount: MangoAccount
pnl?: number
}) => {
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
if (!mangoCache) {
return null
}
const accountEquity = mangoAccount.computeValue(mangoGroup, mangoCache)
const health = mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
return (
<div className="flex items-center text-xs text-th-fgd-3">
{numberCurrencyCompacter.format(accountEquity.toNumber())}
<span className="pl-2 pr-1 text-th-fgd-4">|</span>
{pnl ? (
<span
className={`flex items-center ${
pnl < 0 ? 'text-th-red' : 'text-th-green'
}`}
>
{pnl < 0 ? (
<ArrowSmDownIcon className="mr-0.5 h-4 w-4" />
) : (
<ArrowSmUpIcon className="mr-0.5 h-4 w-4" />
)}
{numberCurrencyCompacter.format(pnl)}
</span>
) : (
<span
className={`flex items-center ${
Number(health) < 15
? 'text-th-red'
: Number(health) < 30
? 'text-th-orange'
: 'text-th-green'
}`}
>
<HeartIcon className="mr-0.5 h-4 w-4" />
{Number(health) > 100 ? '>100' : health.toFixed(0)}%
</span>
)}
</div>
)
}

View File

@ -48,7 +48,14 @@ const MangoAccountSelect = ({
disabled={disabled}
value={
<div className="text-left">
{abbreviateAddress(selectedMangoAccount?.publicKey)}
<p className="mb-0 font-bold text-th-fgd-2">
{selectedMangoAccount?.name
? selectedMangoAccount.name
: t('account')}
</p>
<p className="mb-0 text-xs">
{abbreviateAddress(selectedMangoAccount?.publicKey)}
</p>
</div>
}
onChange={handleSelectMangoAccount}
@ -58,7 +65,18 @@ const MangoAccountSelect = ({
{mangoAccounts.length ? (
mangoAccounts.map((ma, index) => (
<Select.Option key={index} value={ma.publicKey.toString()}>
{abbreviateAddress(ma.publicKey)}
<div className="text-left">
<span
className={`mb-0 font-bold ${
value?.publicKey.toString() === ma.publicKey.toString()
? 'text-th-primary'
: ''
}`}
>
{ma?.name ? ma.name : t('account')}
</span>
<p className="mb-0 text-xs">{abbreviateAddress(ma?.publicKey)}</p>
</div>
</Select.Option>
))
) : (

View File

@ -15,7 +15,7 @@ import { useTranslation } from 'next-i18next'
import SwitchMarketDropdown from './SwitchMarketDropdown'
import Tooltip from './Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
import { InformationCircleIcon } from '@heroicons/react/outline'
import { InformationCircleIcon } from '@heroicons/react/solid'
const OraclePrice = () => {
const oraclePrice = useOraclePrice()
@ -61,15 +61,15 @@ const MarketDetails = () => {
return (
<div
className={`relative flex flex-col md:px-3 md:pb-2 md:pt-3 lg:flex-row lg:items-center lg:justify-between`}
className={`relative flex flex-col md:px-3 md:pt-3 md:pb-2 lg:flex-row lg:items-end lg:justify-between`}
>
<div className="flex flex-col lg:flex-row lg:items-center">
<div className="hidden md:block md:pb-4 md:pr-6 lg:pb-0">
<div className="flex flex-col lg:flex-row lg:flex-wrap">
<div className="hidden md:block md:pr-6 lg:pb-0">
<div className="flex items-center">
<SwitchMarketDropdown />
</div>
</div>
<div className="grid grid-flow-row grid-cols-1 gap-2 md:grid-cols-3 lg:grid-flow-col lg:grid-cols-none lg:grid-rows-1 lg:gap-6">
<div className="grid grid-flow-row grid-cols-1 gap-2 md:mt-2.5 md:grid-cols-3 md:pr-20 lg:grid-flow-col lg:grid-cols-none lg:grid-rows-1 lg:gap-6">
<div className="flex items-center justify-between md:block">
<div className="text-th-fgd-3 md:pb-0.5 md:text-[0.65rem]">
{t('oracle-price')}
@ -111,7 +111,7 @@ const MarketDetails = () => {
content={t('tooltip-funding')}
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
</Tooltip>
</div>
<div className="text-th-fgd-1 md:text-xs">
@ -138,7 +138,7 @@ const MarketDetails = () => {
)} ${baseSymbol}`}
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
</Tooltip>
</div>
</div>

View File

@ -1,6 +1,6 @@
import { useState } from 'react'
import { useRouter } from 'next/router'
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
import Link from 'next/link'
import * as MonoIcons from './icons'
import { initialMarket } from './SettingsModal'

View File

@ -55,7 +55,7 @@ const MarketNavItem: FunctionComponent<MarketNavItemProps> = ({
<div className="text-th-fgd-3">
<div className="flex items-center">
<button
className={`flex w-full items-center justify-between px-2 py-2 font-normal hover:bg-th-bkg-4 hover:text-th-primary ${
className={`flex w-full items-center justify-between px-2 py-2 font-normal md:hover:bg-th-bkg-4 md:hover:text-th-primary ${
asPath.includes(market.name) ||
(asPath === '/' && initialMarket.name === market.name)
? 'text-th-primary'

View File

@ -205,7 +205,9 @@ export default function MarketPosition() {
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const setMangoStore = useMangoStore((s) => s.set)
const price = useMangoStore((s) => s.tradeForm.price)
const perpAccounts = useMangoStore((s) => s.selectedMangoAccount.perpAccounts)
const perpPositions = useMangoStore(
(s) => s.selectedMangoAccount.perpPositions
)
const baseSymbol = marketConfig.baseSymbol
const marketName = marketConfig.name
const router = useRouter()
@ -262,10 +264,10 @@ export default function MarketPosition() {
breakEvenPrice = 0,
notionalSize = 0,
unsettledPnl = 0,
} = perpAccounts.length
? perpAccounts.find((pa) =>
pa.perpMarket.publicKey.equals(selectedMarket.publicKey)
)
} = perpPositions.length
? perpPositions.find((p) =>
p?.perpMarket.publicKey.equals(selectedMarket.publicKey)
) ?? {}
: {}
function SettlePnlTooltip() {

View File

@ -1,102 +0,0 @@
import { useEffect, useState } from 'react'
import { MenuIcon, PlusCircleIcon } from '@heroicons/react/outline'
import MarketMenuItem from './MarketMenuItem'
import { LinkButton } from './Button'
import MarketsModal from './MarketsModal'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { useTranslation } from 'next-i18next'
import useMangoStore from 'stores/useMangoStore'
const MarketSelect = () => {
const { t } = useTranslation('common')
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const [showMarketsModal, setShowMarketsModal] = useState(false)
const [hiddenMarkets] = useLocalStorageState('hiddenMarkets', [])
const [sortedMarkets, setSortedMarkets] = useState<any[]>([])
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
useEffect(() => {
if (groupConfig) {
const markets: any[] = []
const allMarkets = [
...groupConfig.spotMarkets,
...groupConfig.perpMarkets,
]
allMarkets.forEach((market) => {
const base = market.name.slice(0, -5)
const found = markets.find((b) => b.baseAsset === base)
if (!found) {
markets.push({ baseAsset: base, markets: [market] })
} else {
found.markets.push(market)
}
})
setSortedMarkets(markets)
}
}, [groupConfig])
return (
<div className="hidden md:flex">
<div className="flex h-10 w-full bg-th-bkg-3">
<div className="flex items-center bg-th-bkg-4 pl-4 pr-1 lg:pl-9">
{isMobile ? (
<MenuIcon
className="h-5 w-5 cursor-pointer text-th-fgd-1 hover:text-th-primary"
onClick={() => setShowMarketsModal(true)}
/>
) : (
<ShowMarketsButton
onClick={() => setShowMarketsModal(true)}
t={t}
/>
)}
</div>
<div
style={{
width: '0',
height: '0',
borderTop: '20px solid transparent',
borderBottom: '20px solid transparent',
paddingRight: '0.5rem',
}}
className="border-l-[20px] border-th-bkg-4"
/>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
{sortedMarkets
.filter((m) => !hiddenMarkets.includes(m.baseAsset))
.map((s) => (
<MarketMenuItem
key={s.baseAsset}
linksArray={s.markets}
menuTitle={s.baseAsset}
/>
))}
</div>
</div>
</div>
{showMarketsModal ? (
<MarketsModal
isOpen={showMarketsModal}
onClose={() => setShowMarketsModal(false)}
markets={sortedMarkets}
/>
) : null}
</div>
)
}
const ShowMarketsButton = ({ onClick, t }) => (
<LinkButton
className="flex items-center whitespace-nowrap text-xs font-normal text-th-fgd-2"
onClick={onClick}
>
<PlusCircleIcon className="mr-1 h-4 w-4" />
{t('markets').toUpperCase()}
</LinkButton>
)
export default MarketSelect

View File

@ -1,7 +1,6 @@
import React from 'react'
import Link from 'next/link'
import { EyeIcon, EyeOffIcon } from '@heroicons/react/outline'
import { ChevronRightIcon } from '@heroicons/react/solid'
import { ChevronRightIcon, EyeIcon, EyeOffIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import useLocalStorageState from '../hooks/useLocalStorageState'
import useMangoStore from '../stores/useMangoStore'
@ -83,20 +82,6 @@ const MarketsModal = ({
)}
</div>
</div>
{/* <div className="bg-[rgba(255,255,255,0.1)] flex items-center justify-between px-2.5 py-0.5 text-th-fgd-3">
<StyledColumnHeader>Markets</StyledColumnHeader>
<div className="flex justify-between">
<StyledColumnHeader className="pr-5 text-right w-20">
Price
</StyledColumnHeader>
<StyledColumnHeader className="text-right w-20">
24h Change
</StyledColumnHeader>
<StyledColumnHeader className="text-right w-20">
24h Vol
</StyledColumnHeader>
</div>
</div> */}
<div className="divide-y divide-th-bkg-4">
{mkt.markets.map((m) => (
<div

View File

@ -38,98 +38,22 @@ const MarketsTable = ({
const { items, requestSort, sortConfig } = useSortableData(markets)
return !isMobile ? (
<Table>
<thead>
<TrHead>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('name')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('market')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'name'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('last')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('price')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'last'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('change24h')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('rolling-change')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'change24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('volumeUsd24h')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('daily-volume')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'volumeUsd24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
{isPerpMarket ? (
<>
<div className={`md:overflow-x-auto`}>
<div className={`inline-block min-w-full align-middle`}>
<Table>
<thead>
<TrHead>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('funding1h')}
onClick={() => requestSort('name')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('average-funding')}
{t('market')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'funding1h'
sortConfig?.key === 'name'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
@ -140,15 +64,15 @@ const MarketsTable = ({
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('openInterestUsd')}
className="flex items-center font-normal no-underline"
onClick={() => requestSort('last')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('open-interest')}
{t('price')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'openInterestUsd'
sortConfig?.key === 'last'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
@ -157,135 +81,227 @@ const MarketsTable = ({
/>
</LinkButton>
</Th>
</>
) : null}
<Th>
<span className="flex justify-end">{t('favorite')}</span>
</Th>
</TrHead>
</thead>
<tbody>
{items.map((market) => {
const {
baseSymbol,
change24h,
funding1h,
last,
name,
openInterest,
openInterestUsd,
volumeUsd24h,
} = market
const fundingApr = funding1h ? (funding1h * 24 * 365).toFixed(2) : '-'
const coingeckoData = coingeckoPrices.find(
(asset) => asset.symbol === baseSymbol
)
const chartData = coingeckoData ? coingeckoData.prices : undefined
return (
<TrBody key={name} className="hover:bg-th-bkg-3">
<Td>
<Link href={`/?name=${name}`} shallow={true}>
<a className="hover:cursor-pointer">
<div className="flex h-full items-center text-th-fgd-2 hover:text-th-primary">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<span className="default-transition">{name}</span>
</div>
</a>
</Link>
</Td>
<Td className="flex items-center">
<div className="w-20">
{last ? (
formatUsdValue(last)
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</div>
<div className="pl-6">
{!loadingCoingeckoPrices ? (
chartData !== undefined ? (
<PriceChart
name={name}
change24h={change24h}
data={chartData}
height={40}
width={104}
/>
) : (
t('unavailable')
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
</Td>
<Td>
<span
className={change24h >= 0 ? 'text-th-green' : 'text-th-red'}
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('change24h')}
>
{change24h || change24h === 0 ? (
`${(change24h * 100).toFixed(2)}%`
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</span>
</Td>
<Td>
{volumeUsd24h ? (
usdFormatter(volumeUsd24h, 0)
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</Td>
<span className="text-left font-normal text-th-fgd-3">
{t('rolling-change')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'change24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('volumeUsd24h')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('daily-volume')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'volumeUsd24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
{isPerpMarket ? (
<>
<Td>
{funding1h ? (
<>
<span>{`${funding1h.toFixed(4)}%`}</span>{' '}
<span className="text-xs text-th-fgd-3">{`(${fundingApr}% APR)`}</span>
</>
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</Td>
<Td>
{openInterestUsd ? (
<>
<span>{usdFormatter(openInterestUsd, 0)}</span>{' '}
{openInterest ? (
<div className="text-xs text-th-fgd-4">
{openInterest.toLocaleString(undefined, {
maximumFractionDigits:
perpContractPrecision[baseSymbol],
})}{' '}
{baseSymbol}
</div>
) : null}
</>
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</Td>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('funding1h')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('average-funding')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'funding1h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('openInterestUsd')}
>
<span className="text-left font-normal text-th-fgd-3">
{t('open-interest')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'openInterestUsd'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
</>
) : null}
<Td>
<div className="flex justify-end">
<FavoriteMarketButton market={market} />
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
<Th>
<span className="flex justify-end">{t('favorite')}</span>
</Th>
</TrHead>
</thead>
<tbody>
{items.map((market) => {
const {
baseSymbol,
change24h,
funding1h,
last,
name,
openInterest,
openInterestUsd,
volumeUsd24h,
} = market
const fundingApr = funding1h
? (funding1h * 24 * 365).toFixed(2)
: '-'
const coingeckoData = coingeckoPrices.find(
(asset) => asset.symbol === baseSymbol
)
const chartData = coingeckoData ? coingeckoData.prices : undefined
return (
<TrBody key={name}>
<Td>
<Link href={`/?name=${name}`} shallow={true}>
<a className="hover:cursor-pointer">
<div className="flex h-full items-center text-th-fgd-2 hover:text-th-primary">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<span className="default-transition">{name}</span>
</div>
</a>
</Link>
</Td>
<Td className="flex items-center">
<div className="w-20">
{last ? (
formatUsdValue(last)
) : (
<span className="text-th-fgd-4">
{t('unavailable')}
</span>
)}
</div>
<div className="pl-6">
{!loadingCoingeckoPrices ? (
chartData !== undefined ? (
<PriceChart
name={name}
change24h={change24h}
data={chartData}
height={40}
width={104}
/>
) : (
t('unavailable')
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
</Td>
<Td>
<span
className={
change24h >= 0 ? 'text-th-green' : 'text-th-red'
}
>
{change24h || change24h === 0 ? (
`${(change24h * 100).toFixed(2)}%`
) : (
<span className="text-th-fgd-4">
{t('unavailable')}
</span>
)}
</span>
</Td>
<Td>
{volumeUsd24h ? (
usdFormatter(volumeUsd24h, 0)
) : (
<span className="text-th-fgd-4">{t('unavailable')}</span>
)}
</Td>
{isPerpMarket ? (
<>
<Td>
{funding1h ? (
<>
<span>{`${funding1h.toFixed(4)}%`}</span>{' '}
<span className="text-xs text-th-fgd-3">{`(${fundingApr}% APR)`}</span>
</>
) : (
<span className="text-th-fgd-4">
{t('unavailable')}
</span>
)}
</Td>
<Td>
{openInterestUsd ? (
<>
<span>{usdFormatter(openInterestUsd, 0)}</span>{' '}
{openInterest ? (
<div className="text-xs text-th-fgd-4">
{openInterest.toLocaleString(undefined, {
maximumFractionDigits:
perpContractPrecision[baseSymbol],
})}{' '}
{baseSymbol}
</div>
) : null}
</>
) : (
<span className="text-th-fgd-4">
{t('unavailable')}
</span>
)}
</Td>
</>
) : null}
<Td>
<div className="flex justify-end">
<FavoriteMarketButton market={market} />
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
</div>
) : (
<>
{items.map((market) => {
@ -299,7 +315,7 @@ const MarketsTable = ({
return (
<Link href={`/?name=${name}`} shallow={true} key={name}>
<a
className="mb-2 block w-full rounded-lg bg-th-bkg-3 p-4 pb-2.5"
className="default-transition mb-2 block w-full rounded-lg border border-th-bkg-3 p-4 pb-2.5 hover:bg-th-bkg-2"
onClick={() =>
router.push(`/?name=${name}`, undefined, {
shallow: true,
@ -379,7 +395,7 @@ const MarketsTable = ({
}
const COLORS = {
GREEN: { Mango: '#AFD803', Dark: '5EBF4d', Light: '5EBF4d' },
GREEN: { Mango: '#AFD803', Dark: '#5EBF4d', Light: '#5EBF4d' },
RED: { Mango: '#F84638', Dark: '#CC2929', Light: '#CC2929' },
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Portal } from 'react-portal'
import { XIcon } from '@heroicons/react/outline'
import { XIcon } from '@heroicons/react/solid'
const Modal: any = React.forwardRef<any, any>((props, ref) => {
const {
@ -16,7 +16,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
return (
<Portal>
<div
className="fixed inset-0 z-40 overflow-y-auto sm:py-8"
className="fixed inset-0 z-40 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
@ -41,7 +41,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
{isOpen ? (
<div
className={`inline-block min-h-screen bg-th-bkg-2 text-left
className={`inline-block min-h-screen border border-th-bkg-3 bg-th-bkg-1 text-left
sm:min-h-full sm:rounded-lg ${
noPadding ? '' : 'px-8 pt-6 pb-6'
} w-full transform align-middle shadow-lg transition-all sm:max-w-md ${className}`}
@ -51,7 +51,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
<div className="">
<button
onClick={onClose}
className={`absolute right-4 top-4 text-th-fgd-1 hover:text-th-primary focus:outline-none md:right-2 md:top-2`}
className={`absolute right-4 top-4 text-th-fgd-1 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XIcon className={`h-5 w-5`} />
</button>

View File

@ -1,97 +0,0 @@
import { Fragment, useRef } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/solid'
import Link from 'next/link'
type NavDropMenuProps = {
menuTitle: string | React.ReactNode
linksArray: [string, string, boolean, React.ReactNode][]
}
export default function NavDropMenu({
menuTitle = '',
linksArray = [],
}: NavDropMenuProps) {
const buttonRef = useRef<HTMLButtonElement>(null)
const toggleMenu = () => {
buttonRef?.current?.click()
}
const onHover = (open, action) => {
if (
(!open && action === 'onMouseEnter') ||
(open && action === 'onMouseLeave')
) {
toggleMenu()
}
}
return (
<Popover className="relative">
{({ open }) => (
<div
onMouseEnter={() => onHover(open, 'onMouseEnter')}
onMouseLeave={() => onHover(open, 'onMouseLeave')}
className="flex flex-col"
>
<Popover.Button
className={`-mr-3 rounded-none px-3 transition-none focus:bg-th-bkg-3 focus:outline-none ${
open && 'bg-th-bkg-3'
}`}
ref={buttonRef}
>
<div
className={`flex h-14 items-center rounded-none font-bold hover:text-th-primary`}
>
<span className="font-bold">{menuTitle}</span>
<ChevronDownIcon
className={`default-transition ml-0.5 h-5 w-5 ${
open ? 'rotate-180 transform' : 'rotate-360 transform'
}`}
aria-hidden="true"
/>
</div>
</Popover.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 transform scale-75"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel className="absolute top-14 z-10">
<div className="relative rounded-b-md bg-th-bkg-3 px-4 py-2.5">
{linksArray.map(([name, href, isExternal, icon]) =>
!isExternal ? (
<Link href={href} key={href}>
<a className="default-transition flex items-center whitespace-nowrap py-1.5 text-th-fgd-1 hover:text-th-primary">
{icon ? <div className="mr-2">{icon}</div> : null}
{name}
</a>
</Link>
) : (
<a
className="default-transition flex items-center whitespace-nowrap py-1.5 text-th-fgd-1 hover:text-th-primary"
href={href}
key={href}
target="_blank"
rel="noopener noreferrer"
>
{icon ? <div className="mr-2">{icon}</div> : null}
{name}
</a>
)
)}
</div>
</Popover.Panel>
</Transition>
</div>
)}
</Popover>
)
}

View File

@ -2,7 +2,7 @@ import React, { FunctionComponent, useState } from 'react'
import {
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import Input, { Label } from './Input'
import AccountSelect from './AccountSelect'
import { ElementTitle } from './styles'
@ -56,36 +56,39 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
const handleNewAccountDeposit = () => {
if (!wallet) return
setSubmitting(true)
deposit({
amount: parseFloat(inputAmount),
fromTokenAcc: selectedAccount.account,
accountName: name,
wallet,
})
.then(async (response) => {
await sleep(1000)
actions.fetchWalletTokens(wallet)
actions.fetchAllMangoAccounts(wallet)
if (response && response.length > 0) {
onAccountCreation(response[0])
notify({
title: 'Mango Account Created',
txid: response[1],
})
}
setSubmitting(false)
validateAmountInput(inputAmount)
if (inputAmount) {
setSubmitting(true)
deposit({
amount: parseFloat(inputAmount),
fromTokenAcc: selectedAccount.account,
accountName: name,
wallet,
})
.catch((e) => {
setSubmitting(false)
console.error(e)
notify({
title: t('init-error'),
description: e.message,
type: 'error',
.then(async (response) => {
await sleep(1000)
actions.fetchWalletTokens(wallet)
actions.fetchAllMangoAccounts(wallet)
if (response && response.length > 0) {
onAccountCreation(response[0])
notify({
title: 'Mango Account Created',
txid: response[1],
})
}
setSubmitting(false)
})
onAccountCreation()
})
.catch((e) => {
setSubmitting(false)
console.error(e)
notify({
title: t('init-error'),
description: e.message,
type: 'error',
})
onAccountCreation()
})
}
}
const validateAmountInput = (amount) => {
@ -148,7 +151,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
{t('account-name')}{' '}
<span className="ml-1 text-th-fgd-3">{t('optional')}</span>
<Tooltip content={t('tooltip-name-onchain')}>
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-3" />
<InformationCircleIcon className="ml-2 h-5 w-5 text-th-fgd-4" />
</Tooltip>
</Label>
<Input
@ -184,7 +187,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
suffix={symbol}
/>
{invalidAmountMessage ? (
<div className="flex items-center pt-1.5 text-th-red">
<div className="flex items-center py-1.5 text-th-red">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
{invalidAmountMessage}
</div>

View File

@ -3,7 +3,7 @@ import { PublicKey } from '@solana/web3.js'
import { notify } from 'utils/notifications'
import useMangoStore from '../stores/useMangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
import { PhotographIcon } from '@heroicons/react/outline'
import { PhotographIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import { ElementTitle } from './styles'
import {
@ -211,7 +211,7 @@ const NftProfilePicModal = ({ isOpen, onClose }) => {
{nfts.map((n) => {
return (
<button
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-3 hover:bg-th-bkg-4 sm:py-4 ${
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-3 sm:py-4 md:hover:bg-th-bkg-4 ${
selectedProfile?.tokenAddress.toString() === n.tokenAddress
? 'border-th-primary'
: 'border-th-bkg-3'

View File

@ -4,7 +4,7 @@ import {
ExternalLinkIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import useMangoStore, {
CLIENT_TX_TIMEOUT,
CLUSTER,
@ -183,7 +183,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
<div className={`absolute right-2 top-2 flex-shrink-0`}>
<button
onClick={hideNotification}
className={`text-th-fgd-4 hover:text-th-primary focus:outline-none`}
className={`text-th-fgd-4 focus:outline-none md:hover:text-th-primary`}
>
<span className={`sr-only`}>{t('close')}</span>
<svg

View File

@ -1,5 +1,5 @@
import { useState } from 'react'
import { PencilIcon, TrashIcon, XIcon } from '@heroicons/react/outline'
import { useMemo, useState } from 'react'
import { PencilIcon, TrashIcon, XIcon } from '@heroicons/react/solid'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Button, { IconButton } from './Button'
@ -27,6 +27,7 @@ const DesktopTable = ({
cancelledOrderId,
editOrderIndex,
handleCancelOrder,
handleCancelAllOrders,
handleModifyOrder,
modifiedOrderId,
openOrders,
@ -61,6 +62,7 @@ const DesktopTable = ({
return <span>{market.name}</span>
}
}
return (
<Table>
<thead>
@ -162,6 +164,16 @@ const DesktopTable = ({
<span>{t('cancel')}</span>
)}
</Button>
{openOrders.filter(
(o) => o.market.config.name === market.config.name
).length > 1 ? (
<Button
onClick={() => handleCancelAllOrders(market.account)}
className="-my-1 h-7 pt-0 pb-0 pl-3 pr-3 text-xs text-th-red"
>
{t('cancel-all') + ' ' + market.config.name}
</Button>
) : null}
</>
) : (
<>
@ -205,6 +217,7 @@ const MobileTable = ({
cancelledOrderId,
editOrderIndex,
handleCancelOrder,
handleCancelAllOrders,
handleModifyOrder,
modifiedOrderId,
openOrders,
@ -222,7 +235,7 @@ const MobileTable = ({
}
return (
<div className="border-b border-th-bkg-4">
<div className="border-b border-th-bkg-3">
{openOrders.map(({ market, order }, index) => {
const editThisOrder = editOrderIndex === index
return (
@ -289,6 +302,15 @@ const MobileTable = ({
<TrashIcon className="h-5 w-5" />
)}
</IconButton>
{openOrders.filter(
(o) => o.market.config.name === market.config.name
).length > 1 ? (
<IconButton
onClick={() => handleCancelAllOrders(market.account)}
>
<TrashIcon className="h-5 w-5 text-th-red" />
</IconButton>
) : null}
</div>
</div>
{editThisOrder ? (
@ -353,6 +375,72 @@ const OpenOrdersTable = () => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const handleCancelAllOrders = async (market: PerpMarket | Market) => {
const selectedMangoGroup =
useMangoStore.getState().selectedMangoGroup.current
const selectedMangoAccount =
useMangoStore.getState().selectedMangoAccount.current
const mangoClient = useMangoStore.getState().connection.client
try {
if (!selectedMangoGroup || !selectedMangoAccount || !wallet) return
if (market instanceof PerpMarket) {
const txids = await mangoClient.cancelAllPerpOrders(
selectedMangoGroup,
[market],
selectedMangoAccount,
wallet.adapter
)
if (txids) {
for (const txid of txids) {
notify({
title: t('cancel-all-success'),
description: '',
txid,
})
}
} else {
notify({
title: t('cancel-all-error'),
description: t('transaction-failed'),
})
}
actions.reloadOrders()
} else if (market instanceof Market) {
const txid = await mangoClient.cancelAllSpotOrders(
selectedMangoGroup,
selectedMangoAccount,
market,
wallet.adapter,
20
)
if (txid) {
notify({
title: t('cancel-all-success'),
description: '',
txid,
})
} else {
notify({
title: t('cancel-all-error'),
description: t('transaction-failed'),
})
}
}
} catch (e) {
notify({
title: t('cancel-all-error'),
description: e.message,
txid: e.txid,
type: 'error',
})
console.log('error', `${e}`)
} finally {
actions.reloadMangoAccount()
actions.updateOpenOrders()
}
}
const handleCancelOrder = async (
order: Order | PerpOrder | PerpTriggerOrder,
market: Market | PerpMarket
@ -493,11 +581,16 @@ const OpenOrdersTable = () => {
}
}
const sortedOpenOrders = useMemo(() => {
return [...openOrders].sort((a, b) => b.price - a.price)
}, [openOrders])
const tableProps = {
openOrders,
openOrders: sortedOpenOrders,
cancelledOrderId: cancelId,
editOrderIndex,
handleCancelOrder,
handleCancelAllOrders,
handleModifyOrder,
modifiedOrderId: modifyId,
setEditOrderIndex,
@ -515,7 +608,7 @@ const OpenOrdersTable = () => {
)
) : (
<div
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
>
{t('no-orders')}
{asPath === '/account' ? (

View File

@ -282,7 +282,7 @@ export default function Orderbook({ depth = 8 }) {
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 hover:text-th-primary focus:outline-none"
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 focus:outline-none md:hover:text-th-primary"
>
{displayCumulativeSize ? (
<StepSizeIcon className="h-4 w-4" />
@ -300,7 +300,7 @@ export default function Orderbook({ depth = 8 }) {
>
<button
onClick={handleLayoutChange}
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 hover:text-th-primary focus:outline-none"
className="flex h-7 w-7 items-center justify-center rounded-full bg-th-bkg-4 focus:outline-none md:hover:text-th-primary"
>
<SwitchHorizontalIcon className="h-4 w-4" />
</button>
@ -410,7 +410,7 @@ export default function Orderbook({ depth = 8 }) {
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
>
{displayCumulativeSize ? (
<StepSizeIcon className="h-5 w-5" />
@ -428,7 +428,7 @@ export default function Orderbook({ depth = 8 }) {
>
<button
onClick={handleLayoutChange}
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
>
<SwitchHorizontalIcon className="h-5 w-5" />
</button>
@ -479,7 +479,7 @@ export default function Orderbook({ depth = 8 }) {
/>
)
)}
<div className="my-2 flex justify-between rounded-md bg-th-bkg-1 p-2 text-xs">
<div className="my-2 flex justify-between rounded-md bg-th-bkg-2 p-2 text-xs">
<div className="text-th-fgd-3">{t('spread')}</div>
<div className="text-th-fgd-1">
{orderbookData?.spread.toFixed(2)}
@ -527,7 +527,7 @@ export default function Orderbook({ depth = 8 }) {
onClick={() => {
setDisplayCumulativeSize(!displayCumulativeSize)
}}
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none"
className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 focus:outline-none md:hover:text-th-primary"
>
{displayCumulativeSize ? (
<StepSizeIcon className="h-5 w-5" />
@ -603,7 +603,7 @@ const OrderbookSpread = ({ orderbookData }) => {
}, [selectedMarket])
return (
<div className="mb-0 mt-3 flex justify-between rounded-md bg-th-bkg-1 p-2 text-xs">
<div className="my-2 flex justify-between rounded-md bg-th-bkg-2 p-2 text-xs">
<div className="hidden text-th-fgd-3 sm:block">{t('spread')}</div>
<div className="text-th-fgd-1">
{orderbookData?.spread.toFixed(decimals)}

View File

@ -21,8 +21,8 @@ export default function Pagination({
disabled={page === 1}
className={`bg-th-bkg-4 px-1 py-1 ${
page !== 1
? 'hover:cursor-pointer hover:text-th-primary'
: 'hover:cursor-not-allowed'
? 'md:hover:cursor-pointer md:hover:text-th-primary'
: 'md:hover:cursor-not-allowed'
} disabled:text-th-fgd-4`}
>
<ChevronDoubleLeftIcon className={`h-5 w-5`} />
@ -32,8 +32,8 @@ export default function Pagination({
disabled={page === 1}
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
page !== 1
? 'hover:cursor-pointer hover:text-th-primary'
: 'hover:cursor-not-allowed'
? 'md:hover:cursor-pointer md:hover:text-th-primary'
: 'md:hover:cursor-not-allowed'
} disabled:text-th-fgd-4`}
>
<ChevronLeftIcon className={`h-5 w-5`} />
@ -48,8 +48,8 @@ export default function Pagination({
disabled={page === totalPages}
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
page !== totalPages
? 'hover:cursor-pointer hover:text-th-primary'
: 'hover:cursor-not-allowed'
? 'md:hover:cursor-pointer md:hover:text-th-primary'
: 'md:hover:cursor-not-allowed'
} disabled:text-th-fgd-4`}
>
<ChevronRightIcon className={`h-5 w-5`} />
@ -59,8 +59,8 @@ export default function Pagination({
disabled={page === totalPages}
className={`ml-2 bg-th-bkg-4 px-1 py-1 ${
page !== totalPages
? 'hover:cursor-pointer hover:text-th-primary'
: 'hover:cursor-not-allowed'
? 'md:hover:cursor-pointer md:hover:text-th-primary'
: 'md:hover:cursor-not-allowed'
} disabled:text-th-fgd-4`}
>
<ChevronDoubleRightIcon className={`h-5 w-5`} />

View File

@ -2,14 +2,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import { ExclamationIcon } from '@heroicons/react/outline'
import { ExclamationIcon } from '@heroicons/react/solid'
import { ZERO_I80F48 } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import { LinkButton } from '../components/Button'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { ExpandableRow, Table, Td, Th, TrBody, TrHead } from './TableElements'
import { formatUsdValue, getPrecisionDigits, roundPerpSize } from '../utils'
import {
formatUsdValue,
getPrecisionDigits,
roundPerpSize,
usdFormatter,
} from '../utils'
import Loading from './Loading'
import MarketCloseModal from './MarketCloseModal'
import PerpSideBadge from './PerpSideBadge'
@ -21,6 +26,8 @@ import { TwitterIcon } from './icons'
import { marketSelector } from '../stores/selectors'
import { useWallet } from '@solana/wallet-adapter-react'
import RedeemButtons from './RedeemButtons'
import Tooltip from './Tooltip'
import useMangoAccount from 'hooks/useMangoAccount'
const PositionsTable: React.FC = () => {
const { t } = useTranslation('common')
@ -38,6 +45,9 @@ const PositionsTable: React.FC = () => {
)
const unsettledPositions =
useMangoStore.getState().selectedMangoAccount.unsettledPerpPositions
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { asPath } = useRouter()
@ -93,9 +103,9 @@ const PositionsTable: React.FC = () => {
}, [unsettledPositions])
return (
<div className="flex flex-col md:pb-2">
<div className="flex flex-col">
{unsettledPositions.length > 0 ? (
<div className="mb-6 rounded-lg border border-th-bkg-4 p-4 sm:p-6">
<div className="mb-6 rounded-lg border border-th-bkg-3 p-4 sm:p-6">
<div className="flex items-start justify-between pb-4">
<div className="flex items-center">
<ExclamationIcon className="mr-2 h-6 w-6 flex-shrink-0 text-th-primary" />
@ -115,19 +125,19 @@ const PositionsTable: React.FC = () => {
{unsettledPositions.length > 1 ? <RedeemButtons /> : null}
</div>
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid grid-flow-row grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{unsettledPositions.map((p, index) => {
return (
<div
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-3 px-5 py-3"
className="col-span-1 flex items-center justify-between rounded-full bg-th-bkg-2 px-5 py-3"
key={p.marketConfig.baseSymbol}
>
<div className="flex space-x-2">
<div className="flex items-center">
<img
alt=""
width="24"
height="24"
width="20"
height="20"
src={`/assets/icons/${p.marketConfig.baseSymbol.toLowerCase()}.svg`}
className={`mr-3`}
/>
@ -170,7 +180,9 @@ const PositionsTable: React.FC = () => {
<Th>{t('notional-size')}</Th>
<Th>{t('average-entry')}</Th>
<Th>{t('break-even')}</Th>
<Th>{t('estimated-liq-price')}</Th>
<Th>{t('unrealized-pnl')}</Th>
<Th>{t('unsettled-balance')}</Th>
</TrHead>
</thead>
<tbody>
@ -186,6 +198,7 @@ const PositionsTable: React.FC = () => {
avgEntryPrice,
breakEvenPrice,
unrealizedPnl,
unsettledPnl,
},
index
) => {
@ -193,6 +206,18 @@ const PositionsTable: React.FC = () => {
basePosition,
marketConfig.baseSymbol
)
const liquidationPrice =
mangoGroup &&
mangoAccount &&
marketConfig &&
mangoGroup &&
mangoCache
? mangoAccount.getLiquidationPrice(
mangoGroup,
mangoCache,
marketConfig.marketIndex
)
: undefined
return (
<TrBody key={`${marketConfig.marketIndex}`}>
<Td>
@ -255,12 +280,47 @@ const PositionsTable: React.FC = () => {
: '--'}
</Td>
<Td>
{breakEvenPrice ? (
{liquidationPrice &&
liquidationPrice.gt(ZERO_I80F48)
? usdFormatter(liquidationPrice)
: 'N/A'}
</Td>
<Td>
{unrealizedPnl ? (
<PnlText pnl={unrealizedPnl} />
) : (
'--'
)}
</Td>
<Td>
{unsettledPnl ? (
settleSinglePos === index ? (
<Loading />
) : (
<Tooltip content={t('redeem-pnl')}>
<LinkButton
className={
unsettledPnl >= 0
? 'text-th-green'
: 'text-th-red'
}
onClick={() =>
handleSettlePnl(
perpMarket,
perpAccount,
index
)
}
disabled={unsettledPnl === 0}
>
{formatUsdValue(unsettledPnl)}
</LinkButton>
</Tooltip>
)
) : (
'--'
)}
</Td>
<Td>
<LinkButton
onClick={() =>
@ -299,7 +359,10 @@ const PositionsTable: React.FC = () => {
notionalSize,
avgEntryPrice,
breakEvenPrice,
perpAccount,
perpMarket,
unrealizedPnl,
unsettledPnl,
},
index
) => {
@ -307,6 +370,18 @@ const PositionsTable: React.FC = () => {
basePosition,
marketConfig.baseSymbol
)
const liquidationPrice =
mangoGroup &&
mangoAccount &&
marketConfig &&
mangoGroup &&
mangoCache
? mangoAccount.getLiquidationPrice(
mangoGroup,
mangoCache,
marketConfig.marketIndex
)
: undefined
return (
<ExpandableRow
buttonTemplate={
@ -373,6 +448,47 @@ const PositionsTable: React.FC = () => {
? formatUsdValue(breakEvenPrice)
: '--'}
</div>
<div className="col-span-1 text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('unsettled-balance')}
</div>
{unsettledPnl ? (
settleSinglePos === index ? (
<Loading />
) : (
<Tooltip content={t('redeem-pnl')}>
<LinkButton
className={`font-bold ${
unsettledPnl >= 0
? 'text-th-green'
: 'text-th-red'
}`}
onClick={() =>
handleSettlePnl(
perpMarket,
perpAccount,
index
)
}
disabled={unsettledPnl === 0}
>
{formatUsdValue(unsettledPnl)}
</LinkButton>
</Tooltip>
)
) : (
'--'
)}
</div>
<div className="col-span-1 text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('estimated-liq-price')}
</div>
{liquidationPrice &&
liquidationPrice.gt(ZERO_I80F48)
? usdFormatter(liquidationPrice)
: 'N/A'}
</div>
</div>
}
/>
@ -383,7 +499,7 @@ const PositionsTable: React.FC = () => {
)
) : (
<div
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
>
{t('no-perp')}
</div>

View File

@ -4,7 +4,7 @@ const PnlText = ({ className, pnl }: { className?: string; pnl?: number }) => (
<>
{pnl ? (
<p
className={`mb-0 ${className} ${
className={`mb-0 ${className} text-xs ${
pnl > 0 ? 'text-th-green' : 'text-th-red'
}`}
>

View File

@ -1,28 +1,74 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { PublicKey } from '@solana/web3.js'
import { getProfilePicture } from '@solflare-wallet/pfp'
import { useEffect, useState } from 'react'
import useMangoStore from 'stores/useMangoStore'
import { ProfileIcon } from './icons'
const ProfileImage = ({
thumbHeightClass,
thumbWidthClass,
placeholderHeightClass,
placeholderWidthClass,
imageSize,
placeholderSize,
publicKey,
}: {
imageSize: string
placeholderSize: string
publicKey?: string
}) => {
const pfp = useMangoStore((s) => s.wallet.pfp)
const loadPfp = useMangoStore((s) => s.wallet.loadPfp)
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
return pfp?.isAvailable ? (
const connection = useMangoStore((s) => s.connection.current)
const [unownedPfp, setUnownedPfp] = useState<any>(null)
const [loadUnownedPfp, setLoadUnownedPfp] = useState<boolean>(false)
const { connected } = useWallet()
useEffect(() => {
if (publicKey) {
setLoadUnownedPfp(true)
const getProfilePic = async () => {
const pfp = await getProfilePicture(
connection,
new PublicKey(publicKey)
)
setUnownedPfp(pfp)
setLoadUnownedPfp(false)
}
getProfilePic()
}
}, [publicKey])
const isLoading =
(connected && loadingTransaction && !publicKey) ||
(connected && loadPfp && !publicKey) ||
loadUnownedPfp
return (pfp?.isAvailable && !publicKey) || unownedPfp?.isAvailable ? (
<img
alt=""
src={pfp.url}
className={`default-transition rounded-full hover:opacity-60 ${thumbHeightClass} ${thumbWidthClass} ${
src={publicKey ? unownedPfp?.url : pfp?.url}
className={`default-transition rounded-full ${
loadingTransaction ? 'opacity-40' : ''
}`}
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
/>
) : (
<ProfileIcon
className={`text-th-fgd-3 ${placeholderHeightClass} ${placeholderWidthClass}`}
/>
<div
className={`flex flex-shrink-0 items-center justify-center rounded-full ${
isLoading ? 'animate-pulse bg-th-bkg-4' : 'bg-th-bkg-4'
}`}
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
>
<div
style={{
width: `${placeholderSize}px`,
height: `${placeholderSize}px`,
}}
>
<ProfileIcon className={`h-full w-full text-th-fgd-3`} />
</div>
</div>
)
}

View File

@ -0,0 +1,58 @@
import { PencilIcon } from '@heroicons/react/outline'
import { useCallback, useState } from 'react'
import useMangoStore from 'stores/useMangoStore'
import NftProfilePicModal from './NftProfilePicModal'
import ProfileImage from './ProfileImage'
const ProfileImageButton = ({
disabled,
imageSize,
placeholderSize,
publicKey,
}: {
disabled: boolean
imageSize: string
placeholderSize: string
publicKey?: string
}) => {
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
const handleCloseProfilePicModal = useCallback(() => {
setShowProfilePicModal(false)
}, [])
return (
<>
<button
disabled={disabled}
className={`relative mb-2 mr-4 flex items-center justify-center rounded-full sm:mb-0 ${
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : 'bg-th-bkg-button'
}`}
onClick={() => setShowProfilePicModal(true)}
style={{ width: `${imageSize}px`, height: `${imageSize}px` }}
>
<ProfileImage
imageSize={imageSize}
placeholderSize={placeholderSize}
publicKey={publicKey || ''}
/>
{!disabled ? (
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
</div>
) : null}
</button>
{showProfilePicModal ? (
<NftProfilePicModal
isOpen={showProfilePicModal}
onClose={handleCloseProfilePicModal}
/>
) : null}
</>
)
}
export default ProfileImageButton

View File

@ -93,7 +93,7 @@ export default function RecentMarketTrades() {
)}
</>
) : (
<div className="my-3 border-b border-th-bkg-4">
<div className="my-3 border-b border-th-bkg-3">
<ExpandableRow
buttonTemplate={
<div className="flex w-full justify-between text-left">

View File

@ -1,4 +1,4 @@
import { TemplateIcon } from '@heroicons/react/outline'
import { TemplateIcon } from '@heroicons/react/solid'
import { defaultLayouts, GRID_LAYOUT_KEY } from './TradePageGrid'
import useLocalStorageState from '../hooks/useLocalStorageState'
import Tooltip from './Tooltip'

View File

@ -33,7 +33,7 @@ const Select = ({
{open ? (
<Listbox.Options
static
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-3 p-2 text-th-fgd-1 outline-none ${dropdownPanelClassName}`}
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-2 p-2 text-th-fgd-1 outline-none ${dropdownPanelClassName}`}
>
{children}
</Listbox.Options>
@ -47,11 +47,11 @@ const Select = ({
const Option = ({ value, children, className = '' }) => {
return (
<Listbox.Option value={value}>
<Listbox.Option className="mb-0" value={value}>
{({ selected }) => (
<div
className={`default-transition text-th-fgd-1 hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary ${
selected && `text-th-primary`
className={`default-transition rounded p-2 text-th-fgd-1 hover:cursor-pointer hover:bg-th-bkg-3 hover:text-th-primary ${
selected ? 'text-th-primary' : ''
} ${className}`}
>
{children}

View File

@ -0,0 +1,88 @@
import { MangoAccount } from '@blockworks-foundation/mango-client'
import { RadioGroup } from '@headlessui/react'
import { CheckCircleIcon } from '@heroicons/react/outline'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
import useMangoStore, { LAST_ACCOUNT_KEY } from 'stores/useMangoStore'
import MangoAccountCard from './MangoAccountCard'
const SelectMangoAccount = ({
onClose,
className,
}: {
onClose?: () => void
className?: string
}) => {
const { t } = useTranslation('common')
const selectedMangoAccount = useMangoStore(
(s) => s.selectedMangoAccount.current
)
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
const actions = useMangoStore((s) => s.actions)
const setMangoStore = useMangoStore((s) => s.set)
const [, setLastAccountViewed] = useLocalStorageState(LAST_ACCOUNT_KEY)
const handleMangoAccountChange = (mangoAccount: MangoAccount) => {
setLastAccountViewed(mangoAccount.publicKey.toString())
setMangoStore((state) => {
state.selectedMangoAccount.current = mangoAccount
})
actions.fetchTradeHistory()
if (onClose) {
onClose()
}
}
return (
<RadioGroup
value={selectedMangoAccount}
onChange={(acc) => {
if (acc) {
handleMangoAccountChange(acc)
}
}}
>
<RadioGroup.Label className="sr-only">
{t('select-account')}
</RadioGroup.Label>
<div className={`${className} space-y-2`}>
{mangoAccounts.map((account) => (
<RadioGroup.Option
key={account.publicKey.toString()}
value={account}
className={({ checked }) =>
`${
checked
? 'ring-1 ring-inset ring-th-green'
: 'ring-1 ring-inset ring-th-fgd-4 hover:ring-th-fgd-2'
}
default-transition relative flex w-full cursor-pointer rounded-md px-3 py-3 focus:outline-none`
}
>
{({ checked }) => (
<>
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-sm">
<RadioGroup.Label className="flex cursor-pointer items-center text-th-fgd-1">
<MangoAccountCard mangoAccount={account} />
</RadioGroup.Label>
</div>
</div>
{checked && (
<div className="flex-shrink-0 text-th-green">
<CheckCircleIcon className="h-5 w-5" />
</div>
)}
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
)
}
export default SelectMangoAccount

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { CogIcon } from '@heroicons/react/outline'
import { CogIcon } from '@heroicons/react/solid'
import SettingsModal from './SettingsModal'
const Settings = () => {
@ -12,7 +12,7 @@ const Settings = () => {
return mounted ? (
<>
<button
className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-4 text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
onClick={() => setShowSettingsModal(true)}
>
<CogIcon className="h-5 w-5" />

View File

@ -107,7 +107,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
{!settingsView ? (
<div className="border-b border-th-bkg-4">
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
onClick={() => setSettingsView('Default Market')}
>
<span>{t('default-market')}</span>
@ -117,7 +117,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
</div>
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
onClick={() => setSettingsView('Theme')}
>
<span>{t('theme')}</span>
@ -127,7 +127,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
</div>
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
onClick={() => setSettingsView('Language')}
>
<span>{t('language')}</span>
@ -139,7 +139,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
) : null}
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 focus:outline-none md:hover:text-th-primary"
onClick={() => setSettingsView('RPC Endpoint')}
>
<span>{t('rpc-endpoint')}</span>

424
components/SideNav.tsx Normal file
View File

@ -0,0 +1,424 @@
import Link from 'next/link'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
import { BtcMonoIcon, TradeIcon, TrophyIcon } from './icons'
import {
CashIcon,
ChartBarIcon,
CurrencyDollarIcon,
DotsHorizontalIcon,
SwitchHorizontalIcon,
CalculatorIcon,
LibraryIcon,
LightBulbIcon,
UserAddIcon,
ExternalLinkIcon,
ChevronDownIcon,
ReceiptTaxIcon,
} from '@heroicons/react/solid'
import { useRouter } from 'next/router'
import AccountOverviewPopover from './AccountOverviewPopover'
import useMangoAccount from 'hooks/useMangoAccount'
import { useTranslation } from 'next-i18next'
import { Fragment, ReactNode, useEffect, useState } from 'react'
import { Disclosure, Popover, Transition } from '@headlessui/react'
import HealthHeart from './HealthHeart'
import { abbreviateAddress } from 'utils'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useMangoStore from 'stores/useMangoStore'
const SideNav = ({ collapsed }) => {
const { t } = useTranslation('common')
const { mangoAccount } = useMangoAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const [defaultMarket] = useLocalStorageState(
DEFAULT_MARKET_KEY,
initialMarket
)
const router = useRouter()
const { pathname } = router
const I80F48_100 = I80F48.fromString('100')
const maintHealthRatio =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: I80F48_100
return (
<div
className={`flex flex-col justify-between transition-all duration-500 ease-in-out ${
collapsed ? 'w-[64px]' : 'w-[220px]'
} min-h-screen border-r border-th-bkg-3 bg-th-bkg-1`}
>
<div className="mb-4">
<Link href={defaultMarket.path} shallow={true}>
<div
className={`flex h-14 w-full items-center justify-start border-b border-th-bkg-3 px-4`}
>
<div className={`flex flex-shrink-0 cursor-pointer items-center`}>
<img
className={`h-8 w-auto`}
src="/assets/icons/logo.svg"
alt="next"
/>
<Transition
appear={true}
show={!collapsed}
as={Fragment}
enter="transition-all ease-in duration-300"
enterFrom="opacity-50"
enterTo="opacity-100"
leave="transition ease-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<span className="ml-2 text-lg font-bold text-th-fgd-1">
Mango
</span>
</Transition>
</div>
</div>
</Link>
<div className={`flex flex-col items-start space-y-3.5 px-4 pt-4`}>
<MenuItem
active={pathname === '/'}
collapsed={collapsed}
icon={<TradeIcon className="h-5 w-5" />}
title={t('trade')}
pagePath="/"
/>
<MenuItem
active={pathname === '/account'}
collapsed={collapsed}
icon={<CurrencyDollarIcon className="h-5 w-5" />}
title={t('account')}
pagePath="/account"
/>
<MenuItem
active={pathname === '/markets'}
collapsed={collapsed}
icon={<BtcMonoIcon className="h-4 w-4" />}
title={t('markets')}
pagePath="/markets"
/>
<MenuItem
active={pathname === '/borrow'}
collapsed={collapsed}
icon={<CashIcon className="h-5 w-5" />}
title={t('borrow')}
pagePath="/borrow"
/>
<MenuItem
active={pathname === '/swap'}
collapsed={collapsed}
icon={<SwitchHorizontalIcon className="h-5 w-5" />}
title={t('swap')}
pagePath="/swap"
/>
<MenuItem
active={pathname === '/stats'}
collapsed={collapsed}
icon={<ChartBarIcon className="h-5 w-5" />}
title={t('stats')}
pagePath="/stats"
/>
<MenuItem
active={pathname === '/leaderboard'}
collapsed={collapsed}
icon={<TrophyIcon className="h-[18px] w-[18px]" />}
title={t('leaderboard')}
pagePath="/leaderboard"
/>
<ExpandableMenuItem
collapsed={collapsed}
icon={<DotsHorizontalIcon className="h-5 w-5" />}
title={t('more')}
>
<MenuItem
active={pathname === '/referral'}
collapsed={false}
icon={<UserAddIcon className="h-4 w-4" />}
title={t('referrals')}
pagePath="/referral"
hideIconBg
/>
<MenuItem
active={pathname === '/risk-calculator'}
collapsed={false}
icon={<CalculatorIcon className="h-4 w-4" />}
title={t('calculator')}
pagePath="/risk-calculator"
hideIconBg
/>
<MenuItem
active={pathname === '/fees'}
collapsed={false}
icon={<ReceiptTaxIcon className="h-4 w-4" />}
title={t('fees')}
pagePath="/fees"
hideIconBg
/>
<MenuItem
collapsed={false}
icon={<LightBulbIcon className="h-4 w-4" />}
title={t('learn')}
pagePath="https://docs.mango.markets"
hideIconBg
isExternal
/>
<MenuItem
collapsed={false}
icon={<LibraryIcon className="h-4 w-4" />}
title={t('governance')}
pagePath="https://dao.mango.markets"
hideIconBg
isExternal
/>
</ExpandableMenuItem>
</div>
</div>
{mangoAccount ? (
<div className="flex min-h-[64px] w-full items-center border-t border-th-bkg-3 ">
<ExpandableMenuItem
collapsed={collapsed}
icon={<HealthHeart health={Number(maintHealthRatio)} size={32} />}
title={
<div className="py-3 text-left">
<p className="mb-0 whitespace-nowrap text-xs text-th-fgd-3">
{t('account-summary')}
</p>
<p className="mb-0 font-bold text-th-fgd-1">
{abbreviateAddress(mangoAccount.publicKey)}
</p>
</div>
}
hideIconBg
alignBottom
>
<AccountOverviewPopover
collapsed={collapsed}
health={maintHealthRatio}
/>
</ExpandableMenuItem>
</div>
) : null}
</div>
)
}
export default SideNav
const MenuItem = ({
active,
collapsed,
icon,
title,
pagePath,
hideIconBg,
isExternal,
}: {
active?: boolean
collapsed: boolean
icon: ReactNode
title: string
pagePath: string
hideIconBg?: boolean
isExternal?: boolean
}) => {
return !isExternal ? (
<Link href={pagePath} shallow={true}>
<a
className={`default-transition flex items-center hover:brightness-[1.1] ${
active ? 'text-th-primary' : 'text-th-fgd-1'
}`}
>
<div
className={
hideIconBg
? ''
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
}
>
{icon}
</div>
<Transition
appear={true}
show={!collapsed}
as={Fragment}
enter="transition-all ease-in duration-300"
enterFrom="opacity-50"
enterTo="opacity-100"
leave="transition ease-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<span className="ml-2">{title}</span>
</Transition>
</a>
</Link>
) : (
<a
href={pagePath}
className={`default-transition flex items-center justify-between hover:brightness-[1.1] ${
active ? 'text-th-primary' : 'text-th-fgd-1'
}`}
target="_blank"
rel="noopener noreferrer"
>
<div className="flex items-center">
<div
className={
hideIconBg
? ''
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
}
>
{icon}
</div>
{!collapsed ? <span className="ml-2">{title}</span> : null}
</div>
<ExternalLinkIcon className="h-4 w-4" />
</a>
)
}
const ExpandableMenuItem = ({
children,
collapsed,
icon,
title,
hideIconBg,
alignBottom,
}: {
children: ReactNode
collapsed: boolean
icon: ReactNode
title: string | ReactNode
hideIconBg?: boolean
alignBottom?: boolean
}) => {
const [showMenu, setShowMenu] = useState(false)
const onHoverMenu = (open, action) => {
if (
(!open && action === 'onMouseEnter') ||
(open && action === 'onMouseLeave')
) {
setShowMenu(!showMenu)
}
}
useEffect(() => {
if (collapsed) {
setShowMenu(false)
}
}, [collapsed])
return collapsed ? (
<Popover>
<div
onMouseEnter={() => onHoverMenu(showMenu, 'onMouseEnter')}
onMouseLeave={() => onHoverMenu(showMenu, 'onMouseLeave')}
className="relative z-30"
>
<Popover.Button className="hover:text-th-primary">
<div
className={` ${
hideIconBg
? ''
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
} ${
alignBottom
? 'default-transition flex h-16 w-16 items-center justify-center hover:bg-th-bkg-2'
: ''
}`}
>
{icon}
</div>
</Popover.Button>
<Transition
appear={true}
show={showMenu}
as={Fragment}
enter="transition-all ease-in duration-300"
enterFrom="opacity-0 transform scale-90"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel
className={`absolute w-56 space-y-2 rounded-md bg-th-bkg-2 p-4 ${
alignBottom
? 'bottom-0 left-14'
: 'left-10 top-1/2 -translate-y-1/2 transform'
}`}
>
{children}
</Popover.Panel>
</Transition>
</div>
</Popover>
) : (
<Disclosure>
<div
onClick={() => setShowMenu(!showMenu)}
role="button"
className={`w-full `}
>
<Disclosure.Button
className={`flex w-full items-center justify-between rounded-none hover:text-th-primary ${
alignBottom ? 'h-[64px] px-4 hover:bg-th-bkg-2' : ''
}`}
>
<div className="flex items-center">
<div
className={
hideIconBg
? ''
: 'flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3'
}
>
{icon}
</div>
<Transition
appear={true}
show={!collapsed}
as={Fragment}
enter="transition-all ease-in duration-300"
enterFrom="opacity-50"
enterTo="opacity-100"
leave="transition ease-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<span className="ml-2">{title}</span>
</Transition>
</div>
<ChevronDownIcon
className={`${
showMenu ? 'rotate-180 transform' : 'rotate-360 transform'
} default-transition h-5 w-5 flex-shrink-0`}
/>
</Disclosure.Button>
<Transition
appear={true}
show={showMenu}
as={Fragment}
enter="transition-all ease-in duration-500"
enterFrom="opacity-100 max-h-0"
enterTo="opacity-100 max-h-64"
leave="transition-all ease-out duration-500"
leaveFrom="opacity-100 max-h-64"
leaveTo="opacity-0 max-h-0"
>
<Disclosure.Panel className="overflow-hidden">
<div className="space-y-2 p-2">{children}</div>
</Disclosure.Panel>
</Transition>
</div>
</Disclosure>
)
}

View File

@ -1,5 +1,5 @@
import { FunctionComponent, useEffect, useMemo, useState } from 'react'
import { ExternalLinkIcon, EyeOffIcon } from '@heroicons/react/outline'
import { ExternalLinkIcon, EyeOffIcon } from '@heroicons/react/solid'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { AreaChart, Area, XAxis, YAxis, Tooltip } from 'recharts'
@ -295,7 +295,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
</AreaChart>
<div className="flex justify-end">
<button
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 1 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(1)}
@ -303,7 +303,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
24H
</button>
<button
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 7 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(7)}
@ -311,7 +311,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
7D
</button>
<button
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 hover:text-th-primary focus:outline-none ${
className={`default-transition px-3 py-2 text-xs font-bold text-th-fgd-1 focus:outline-none md:hover:text-th-primary ${
daysToShow === 30 && 'text-th-primary'
}`}
onClick={() => setDaysToShow(30)}

View File

@ -6,7 +6,7 @@ import ButtonGroup from './ButtonGroup'
import { numberCompacter, numberFormatter } from './SwapTokenInfo'
import Button, { IconButton } from './Button'
import Input from './Input'
import { SearchIcon, XIcon } from '@heroicons/react/outline'
import { SearchIcon, XIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
import { ExpandableRow } from './TableElements'
@ -36,8 +36,9 @@ const SwapTokenInsights = ({ formState, jupiterTokens, setOutputToken }) => {
`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${ids.toString()}&order=market_cap_desc&sparkline=false&price_change_percentage=24h,7d,30d`
)
const data = await response.json()
const filterMicroVolume = data.filter((token) => token.total_volume > 10000)
setLoading(false)
setTokenInsights(data)
setTokenInsights(filterMicroVolume)
}
useEffect(() => {

View File

@ -1,5 +1,5 @@
import { memo, useMemo, useState, PureComponent, useEffect } from 'react'
import { SearchIcon } from '@heroicons/react/outline'
import { SearchIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import { FixedSizeList } from 'react-window'
import { Token } from '../@types/types'
@ -45,7 +45,7 @@ class ItemRenderer extends PureComponent<ItemRendererProps> {
<div style={this.props.style}>
<button
key={tokenInfo?.address}
className="flex w-full cursor-pointer items-center justify-between rounded-none py-4 px-6 font-normal hover:bg-th-bkg-4 focus:bg-th-bkg-3 focus:outline-none"
className="flex w-full cursor-pointer items-center justify-between rounded-none py-4 px-6 font-normal focus:bg-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-4"
onClick={() => this.props.data.onSubmit(tokenInfo)}
>
<div className="flex items-center">
@ -75,14 +75,36 @@ const SwapTokenSelect = ({
sortedTokenMints,
onClose,
onTokenSelect,
walletTokens,
}: {
isOpen: boolean
sortedTokenMints: Token[]
onClose?: (x?) => void
onTokenSelect?: (x?) => void
walletTokens?: Array<any>
}) => {
const [search, setSearch] = useState('')
const popularTokenSymbols = ['USDC', 'SOL', 'USDT', 'MNGO', 'BTC', 'ETH']
const popularTokens = useMemo(() => {
return walletTokens?.length
? sortedTokenMints.filter((token) => {
const walletMints = walletTokens.map((tok) =>
tok.account.mint.toString()
)
return !token?.name || !token?.symbol
? false
: popularTokenSymbols.includes(token.symbol) &&
walletMints.includes(token.address)
})
: sortedTokenMints.filter((token) => {
return !token?.name || !token?.symbol
? false
: popularTokenSymbols.includes(token.symbol)
})
}, [walletTokens])
useEffect(() => {
function onEscape(e) {
if (e.keyCode === 27) {
@ -95,9 +117,20 @@ const SwapTokenSelect = ({
const tokenInfos = useMemo(() => {
if (sortedTokenMints?.length) {
return sortedTokenMints.filter((token) => {
const filteredTokens = sortedTokenMints.filter((token) => {
return !token?.name || !token?.symbol ? false : true
})
if (walletTokens?.length) {
const walletMints = walletTokens.map((tok) =>
tok.account.mint.toString()
)
return filteredTokens.sort(
(a, b) =>
walletMints.indexOf(b.address) - walletMints.indexOf(a.address)
)
} else {
return filteredTokens
}
} else {
return []
}
@ -116,13 +149,33 @@ const SwapTokenSelect = ({
<SearchIcon className="h-8 w-8" />
<input
type="text"
className="ml-4 flex-1 bg-th-bkg-2 focus:outline-none"
className="ml-4 flex-1 bg-th-bkg-1 focus:outline-none"
placeholder="Search by token or paste address"
autoFocus
value={search}
onChange={handleUpdateSearch}
/>
</div>
{popularTokens.length && onTokenSelect ? (
<div className="flex flex-wrap px-4">
{popularTokens.map((token) => (
<button
className="mx-1 mb-2 flex items-center rounded-md border border-th-fgd-4 bg-th-bkg-1 py-1 px-3 hover:border-th-fgd-3 focus:border-th-fgd-2"
onClick={() => onTokenSelect(token)}
key={token.address}
>
<img
alt=""
width="16"
height="16"
src={`/assets/icons/${token.symbol.toLowerCase()}.svg`}
className={`mr-1.5`}
/>
<span className="text-th-fgd-1">{token.symbol}</span>
</button>
))}
</div>
) : null}
<FixedSizeList
width="100%"
height={403}

View File

@ -1,7 +1,6 @@
import { Fragment, useCallback, useMemo, useRef, useState } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { SearchIcon } from '@heroicons/react/outline'
import { ChevronDownIcon } from '@heroicons/react/solid'
import { ChevronDownIcon, SearchIcon } from '@heroicons/react/solid'
import Input from './Input'
import { useTranslation } from 'next-i18next'
import MarketNavItem from './MarketNavItem'
@ -61,7 +60,7 @@ const SwitchMarketDropdown = () => {
{({ open }) => (
<div className="relative flex flex-col">
<Popover.Button
className={`border border-th-bkg-3 p-0.5 transition-none hover:border-th-bkg-4 focus:border-th-bkg-4 focus:outline-none ${
className={`default-transition border border-th-fgd-4 p-0.5 transition-none hover:border-th-fgd-3 focus:border-th-fgd-4 focus:outline-none ${
open && 'border-th-fgd-4'
}`}
ref={buttonRef}
@ -103,7 +102,7 @@ const SwitchMarketDropdown = () => {
leaveTo="opacity-0"
>
<Popover.Panel
className="thin-scroll absolute left-0 top-14 z-10 max-h-[50vh] w-72 transform overflow-y-auto rounded-b-md rounded-tl-md bg-th-bkg-3 p-4 sm:max-h-[75vh]"
className="thin-scroll absolute left-0 top-14 z-10 max-h-[50vh] w-72 transform overflow-y-auto rounded-b-md rounded-tl-md bg-th-bkg-2 p-4 sm:max-h-[75vh]"
tabIndex={-1}
>
<div className="hidden pb-2.5 sm:block">

44
components/TabButtons.tsx Normal file
View File

@ -0,0 +1,44 @@
import * as MonoIcons from './icons'
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
const TabButtons = ({
tabs,
activeTab,
showSymbolIcon,
onClick,
}: {
tabs: Array<{ label: string; key: string }>
activeTab: string
showSymbolIcon?: boolean
onClick: (x) => void
}) => {
const { t } = useTranslation('common')
const renderSymbolIcon = (s) => {
const iconName = `${s.slice(0, 1)}${s.slice(1, 4).toLowerCase()}MonoIcon`
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
return <SymbolIcon className="mr-1.5 h-3.5 w-auto" />
}
return (
<div className="flex flex-wrap">
{tabs.map((tab) => (
<div
className={`default-transition mb-2 mr-2 flex cursor-pointer items-center rounded-full px-3 py-2 font-bold leading-none ring-1 ring-inset ${
tab.key === activeTab
? `text-th-primary ring-th-primary`
: `text-th-fgd-4 ring-th-fgd-4 hover:text-th-fgd-3 hover:ring-th-fgd-3`
} ${showSymbolIcon ? 'uppercase' : ''}
`}
onClick={() => onClick(tab.key)}
role="button"
key={tab.key}
>
{showSymbolIcon ? renderSymbolIcon(tab.label) : null}
{t(tab.label.toLowerCase().replace(/\s/g, '-'))}
</div>
))}
</div>
)
}
export default TabButtons

View File

@ -8,7 +8,7 @@ export const Table = ({ children }) => (
)
export const TrHead = ({ children }) => (
<tr className="text-xs text-th-fgd-3">{children}</tr>
<tr className="text-xxs leading-tight text-th-fgd-2">{children}</tr>
)
export const Th = ({ children }) => (
@ -18,7 +18,7 @@ export const Th = ({ children }) => (
)
export const TrBody = ({ children, className = '' }) => (
<tr className={`border-b border-th-bkg-4 ${className}`}>{children}</tr>
<tr className={`border-b border-th-bkg-3 ${className}`}>{children}</tr>
)
export const Td = ({
@ -28,7 +28,7 @@ export const Td = ({
children: ReactNode
className?: string
}) => (
<td className={`h-16 px-4 text-sm text-th-fgd-2 ${className}`}>{children}</td>
<td className={`h-14 px-4 text-xs text-th-fgd-2 ${className}`}>{children}</td>
)
type ExpandableRowProps = {
@ -47,7 +47,7 @@ export const ExpandableRow = ({
{({ open }) => (
<>
<Disclosure.Button
className={`default-transition flex w-full items-center justify-between border-t border-th-bkg-4 p-4 font-normal text-th-fgd-1 hover:bg-th-bkg-4 focus:outline-none ${
className={`default-transition flex w-full items-center justify-between border-t border-th-bkg-3 p-4 text-left text-xs font-normal text-th-fgd-1 hover:bg-th-bkg-4 focus:outline-none ${
rounded
? open
? 'rounded-b-none'
@ -76,7 +76,7 @@ export const ExpandableRow = ({
leaveTo="opacity-0"
>
<Disclosure.Panel>
<div className="px-4 pb-4 pt-2">{panelTemplate}</div>
<div className="px-4 pb-4 pt-2 text-xs">{panelTemplate}</div>
</Disclosure.Panel>
</Transition>
</>
@ -92,7 +92,7 @@ type RowProps = {
export const Row = ({ children }: RowProps) => {
return (
<div
className={`default-transition w-full rounded-none border-t border-th-bkg-4 p-4 font-normal text-th-fgd-1`}
className={`default-transition w-full rounded-none border-t border-th-bkg-3 p-4 font-normal text-th-fgd-1`}
>
{children}
</div>
@ -107,7 +107,9 @@ export const TableDateDisplay = ({
showSeconds?: boolean
}) => (
<>
<p className="mb-0 text-th-fgd-2">{dayjs(date).format('DD MMM YYYY')}</p>
<p className="mb-0 text-xs text-th-fgd-2">
{dayjs(date).format('DD MMM YYYY')}
</p>
<p className="mb-0 text-xs">
{dayjs(date).format(showSeconds ? 'h:mm:ssa' : 'h:mma')}
</p>

View File

@ -22,7 +22,7 @@ const Tabs: FunctionComponent<TabsProps> = ({
const { t } = useTranslation('common')
return (
<div className={`relative mb-4 border-b border-th-fgd-4`}>
<div className={`relative mb-6 border-b border-th-bkg-4`}>
<div
className={`default-transition absolute bottom-[-1px] left-0 h-0.5 bg-th-primary`}
style={{

View File

@ -1,193 +0,0 @@
import { useCallback, useState } from 'react'
import Link from 'next/link'
import { abbreviateAddress } from '../utils/index'
import useLocalStorageState from '../hooks/useLocalStorageState'
import MenuItem from './MenuItem'
import useMangoStore from '../stores/useMangoStore'
import { ConnectWalletButton } from 'components'
import NavDropMenu from './NavDropMenu'
import AccountsModal from './AccountsModal'
import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
import { useTranslation } from 'next-i18next'
import Settings from './Settings'
import TradeNavMenu from './TradeNavMenu'
import {
CalculatorIcon,
CurrencyDollarIcon,
LibraryIcon,
LightBulbIcon,
UserAddIcon,
} from '@heroicons/react/outline'
import { MangoIcon, TrophyIcon } from './icons'
import { useWallet } from '@solana/wallet-adapter-react'
const StyledNewLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
{children}
</div>
)
const TopBar = () => {
const { t } = useTranslation('common')
const { connected, publicKey } = useWallet()
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
const cluster = useMangoStore((s) => s.connection.cluster)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [defaultMarket] = useLocalStorageState(
DEFAULT_MARKET_KEY,
initialMarket
)
const isDevnet = cluster === 'devnet'
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
}, [])
return (
<>
<nav className={`bg-th-bkg-2`}>
<div className={`px-4 xl:px-6`}>
<div className={`flex h-14 justify-between`}>
<div className={`flex`}>
<Link href={defaultMarket.path} shallow={true}>
<div
className={`flex flex-shrink-0 cursor-pointer items-center`}
>
<img
className={`h-8 w-auto`}
src="/assets/icons/logo.svg"
alt="next"
/>
</div>
</Link>
<div
className={`hidden md:ml-4 md:flex md:items-center md:space-x-2 lg:space-x-3`}
>
<TradeNavMenu />
<MenuItem href="/account">{t('account')}</MenuItem>
<MenuItem href="/markets">{t('markets')}</MenuItem>
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
<MenuItem href="/swap">{t('swap')}</MenuItem>
<div className="relative">
<MenuItem href="/leaderboard">
{t('leaderboard')}
<div className="absolute -right-3 -top-3 flex h-4 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-yellow-500 px-1.5">
<StyledNewLabel className="uppercase text-white">
new
</StyledNewLabel>
</div>
</MenuItem>
</div>
<MenuItem href="/stats">{t('stats')}</MenuItem>
<NavDropMenu
menuTitle={t('more')}
// linksArray: [name: string, href: string, isExternal: boolean]
linksArray={[
[
t('referrals'),
'/referral',
false,
<UserAddIcon className="h-4 w-4" key="referrals" />,
],
[
t('leaderboard'),
'/leaderboard',
false,
<TrophyIcon className="h-4 w-4" key="leaderboard" />,
],
[
t('calculator'),
'/risk-calculator',
false,
<CalculatorIcon className="h-4 w-4" key="calculator" />,
],
[
t('fees'),
'/fees',
false,
<CurrencyDollarIcon className="h-4 w-4" key="fees" />,
],
[
t('learn'),
'https://docs.mango.markets/',
true,
<LightBulbIcon className="h-4 w-4" key="learn" />,
],
[
t('governance'),
'https://dao.mango.markets/',
true,
<LibraryIcon className="h-4 w-4" key="governance" />,
],
[
'Mango v2',
'https://v2.mango.markets',
true,
<MangoIcon
className="h-4 w-4 stroke-current"
key="mango-v2"
/>,
],
[
'Mango v1',
'https://v1.mango.markets',
true,
<MangoIcon
className="h-4 w-4 stroke-current"
key="mango-v1"
/>,
],
]}
/>
</div>
</div>
<div className="flex items-center space-x-2.5">
{isDevnet ? <div className="pl-2 text-xxs">Devnet</div> : null}
<div className="pl-2">
<Settings />
</div>
{mangoAccount &&
mangoAccount.owner.toBase58() === publicKey?.toBase58() ? (
<button
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="text-xs font-normal text-th-primary">
{mangoAccounts
? mangoAccounts.length === 1
? `1 ${t('account')}`
: `${mangoAccounts.length} ${t('accounts')}`
: t('account')}
</div>
{mangoAccount.name
? mangoAccount.name
: abbreviateAddress(mangoAccount.publicKey)}
</button>
) : connected && !mangoAccount ? (
<button
className="rounded border border-th-bkg-4 py-1 px-2 text-xs hover:border-th-fgd-4 focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="text-xs font-normal text-th-primary">
{`0 ${t('accounts')}`}
</div>
{t('get-started')} 😎
</button>
) : null}
<ConnectWalletButton />
</div>
</div>
</div>
</nav>
{showAccountsModal && (
<AccountsModal
onClose={handleCloseAccounts}
isOpen={showAccountsModal}
/>
)}
</>
)
}
export default TopBar

View File

@ -1,5 +1,5 @@
import { FunctionComponent, useEffect, useMemo, useState } from 'react'
import { RefreshIcon } from '@heroicons/react/outline'
import { RefreshIcon } from '@heroicons/react/solid'
import Input, { Label } from './Input'
import Button, { LinkButton } from './Button'
import Modal from './Modal'
@ -89,8 +89,7 @@ const TradeHistoryFilterModal: FunctionComponent<
return {
...prevSelected,
size: {
condition: (size) =>
parseFloat(size) >= from && parseFloat(size) <= to,
condition: (size) => size >= from && size <= to,
values: { from: from, to: to },
},
}
@ -301,8 +300,8 @@ const FilterButton = ({ filters, filterKey, value, onClick }) => {
<button
className={`default-transitions rounded-full border border-th-fgd-3 px-3 py-1 text-xs text-th-fgd-1 ${
filters[filterKey]?.includes(value) &&
'border-th-primary bg-th-primary text-th-bkg-1 hover:text-th-bkg-1'
} hover:border-th-primary hover:text-th-primary`}
'border-th-primary bg-th-primary text-th-bkg-1 md:hover:text-th-bkg-1'
} md:hover:border-th-primary md:hover:text-th-primary`}
onClick={onClick}
>
{t(value.toLowerCase())}

View File

@ -28,7 +28,7 @@ import {
InformationCircleIcon,
RefreshIcon,
SaveIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import { fetchHourlyPerformanceStats } from './account_page/AccountOverview'
import useMangoStore from '../stores/useMangoStore'
import Loading from './Loading'
@ -190,7 +190,7 @@ const TradeHistoryTable = ({
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-4" />
</Tooltip>
) : null}
</div>
@ -245,7 +245,7 @@ const TradeHistoryTable = ({
<SaveIcon className={`mr-1.5 h-4 w-4`} />
{t('export-trades-csv')}
<Tooltip content={t('trade-export-disclaimer')}>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
</Tooltip>
</a>
</div>
@ -494,7 +494,7 @@ const TradeHistoryTable = ({
</>
) : (
<div className="mb-6">
<div className="border-b border-th-bkg-4">
<div className="border-b border-th-bkg-3">
{paginatedData.map((trade: any, index) => (
<ExpandableRow
buttonTemplate={
@ -598,11 +598,11 @@ const TradeHistoryTable = ({
</div>
)
) : hasActiveFilter ? (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
{t('no-trades-found')}
</div>
) : (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
{t('no-history')}
{asPath === '/account' ? (
<Link href={'/'} shallow={true}>

View File

@ -205,10 +205,10 @@ const MenuCategories: FunctionComponent<MenuCategoriesProps> = ({
key={cat.name}
onClick={() => onChange(cat.name)}
onMouseEnter={() => onChange(cat.name)}
className={`default-transition relative flex h-14 w-full cursor-pointer flex-col justify-center whitespace-nowrap rounded-none px-4 font-bold hover:bg-th-bkg-3 ${
className={`default-transition relative flex h-14 w-full cursor-pointer flex-col justify-center whitespace-nowrap rounded-none px-4 font-bold md:hover:bg-th-bkg-3 ${
activeCategory === cat.name
? `bg-th-bkg-3 text-th-primary`
: `text-th-fgd-2 hover:text-th-primary`
: `text-th-fgd-2 md:hover:text-th-primary`
}
`}
>
@ -240,14 +240,14 @@ export const FavoriteMarketButton = ({ market }) => {
return favoriteMarkets.find((mkt) => mkt === market.name) ? (
<button
className="default-transition flex items-center justify-center text-th-primary hover:text-th-fgd-3"
className="default-transition flex items-center justify-center text-th-primary md:hover:text-th-fgd-3"
onClick={() => removeFromFavorites(market.name)}
>
<FilledStarIcon className="h-5 w-5" />
</button>
) : (
<button
className="default-transition flex items-center justify-center text-th-fgd-4 hover:text-th-primary"
className="default-transition flex items-center justify-center text-th-fgd-4 md:hover:text-th-primary"
onClick={() => addToFavorites(market.name)}
>
<StarIcon className="h-5 w-5" />

View File

@ -11,8 +11,6 @@ const TVChartContainer = dynamic(
import { useEffect, useState } from 'react'
import FloatingElement from '../components/FloatingElement'
import Orderbook from '../components/Orderbook'
import AccountInfo from './AccountInfo'
import UserMarketInfo from './UserMarketInfo'
import TradeForm from './trade_form/TradeForm'
import UserInfo from './UserInfo'
import RecentMarketTrades from './RecentMarketTrades'
@ -24,46 +22,45 @@ import MarketDetails from './MarketDetails'
const ResponsiveGridLayout = WidthProvider(Responsive)
export const defaultLayouts = {
xxl: [
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 19 },
{ i: 'tradeForm', x: 8, y: 0, w: 2, h: 19 },
{ i: 'orderbook', x: 10, y: 0, w: 2, h: 25 },
{ i: 'marketTrades', x: 10, y: 1, w: 2, h: 13 },
{ i: 'userInfo', x: 0, y: 1, w: 10, h: 19 },
],
xl: [
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 27 },
{ i: 'marketPosition', x: 9, y: 4, w: 3, h: 13 },
{ i: 'accountInfo', x: 9, y: 3, w: 3, h: 14 },
{ i: 'orderbook', x: 6, y: 0, w: 3, h: 17 },
{ i: 'tradeForm', x: 9, y: 1, w: 3, h: 17 },
{ i: 'marketTrades', x: 6, y: 1, w: 3, h: 10 },
{ i: 'userInfo', x: 0, y: 2, w: 9, h: 19 },
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 19, minW: 2 },
{ i: 'tradeForm', x: 6, y: 0, w: 3, h: 19, minW: 3 },
{ i: 'orderbook', x: 9, y: 0, w: 3, h: 25, minW: 2 },
{ i: 'marketTrades', x: 9, y: 0, w: 3, h: 13, minW: 2 },
{ i: 'userInfo', x: 0, y: 1, w: 9, h: 19, minW: 6 },
],
lg: [
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 27, minW: 2 },
{ i: 'marketPosition', x: 9, y: 2, w: 3, h: 13, minW: 2 },
{ i: 'accountInfo', x: 9, y: 1, w: 3, h: 14, minW: 2 },
{ i: 'orderbook', x: 6, y: 2, w: 3, h: 17, minW: 2 },
{ i: 'tradeForm', x: 9, y: 0, w: 3, h: 17, minW: 3 },
{ i: 'marketTrades', x: 6, y: 2, w: 3, h: 10, minW: 2 },
{ i: 'userInfo', x: 0, y: 3, w: 9, h: 19, minW: 6 },
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 19, minW: 2 },
{ i: 'tradeForm', x: 6, y: 0, w: 3, h: 19, minW: 2 },
{ i: 'orderbook', x: 9, y: 0, w: 3, h: 25, minW: 2 },
{ i: 'marketTrades', x: 9, y: 1, w: 3, h: 13, minW: 2 },
{ i: 'userInfo', x: 0, y: 1, w: 9, h: 19, minW: 6 },
],
md: [
{ i: 'tvChart', x: 0, y: 0, w: 8, h: 25, minW: 2 },
{ i: 'marketPosition', x: 8, y: 1, w: 4, h: 11, minW: 2 },
{ i: 'accountInfo', x: 8, y: 0, w: 4, h: 14, minW: 2 },
{ i: 'orderbook', x: 0, y: 2, w: 4, h: 19, minW: 2 },
{ i: 'tradeForm', x: 4, y: 2, w: 4, h: 19, minW: 3 },
{ i: 'marketTrades', x: 8, y: 2, w: 4, h: 19, minW: 2 },
{ i: 'userInfo', x: 0, y: 3, w: 12, h: 19, minW: 6 },
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 16, minW: 2 },
{ i: 'tradeForm', x: 0, y: 1, w: 4, h: 22, minW: 3 },
{ i: 'orderbook', x: 4, y: 1, w: 4, h: 22, minW: 2 },
{ i: 'marketTrades', x: 8, y: 1, w: 4, h: 22, minW: 2 },
{ i: 'userInfo', x: 0, y: 2, w: 12, h: 19, minW: 6 },
],
sm: [
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 20, minW: 6 },
{ i: 'marketPosition', x: 0, y: 1, w: 6, h: 14, minW: 6 },
{ i: 'accountInfo', x: 6, y: 1, w: 6, h: 14, minW: 6 },
{ i: 'tradeForm', x: 0, y: 2, w: 12, h: 17, minW: 6 },
{ i: 'orderbook', x: 0, y: 3, w: 6, h: 17, minW: 6 },
{ i: 'marketTrades', x: 6, y: 3, w: 6, h: 17, minW: 6 },
{ i: 'userInfo', x: 0, y: 4, w: 12, h: 19, minW: 6 },
{ i: 'tradeForm', x: 0, y: 1, w: 12, h: 17, minW: 6 },
{ i: 'orderbook', x: 0, y: 2, w: 6, h: 22, minW: 3 },
{ i: 'marketTrades', x: 6, y: 2, w: 6, h: 22, minW: 3 },
{ i: 'userInfo', x: 0, y: 3, w: 12, h: 19, minW: 6 },
],
}
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.1.6'
export const breakpoints = { xl: 1600, lg: 1280, md: 1024, sm: 768 }
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.2.0'
export const breakpoints = { xxl: 1600, xl: 1440, lg: 1170, md: 960, sm: 768 }
const getCurrentBreakpoint = () => {
return Responsive.utils.getBreakpointFromWidth(
@ -103,7 +100,7 @@ const TradePageGrid: React.FC = () => {
const orderbookLayout = layouts[bp].find((obj) => {
return obj.i === 'orderbook'
})
let depth = orderbookLayout.h * 0.891 - 5
let depth = orderbookLayout.h * 0.921 - 5
const maxNum = max([1, depth])
if (typeof maxNum === 'number') {
depth = round(maxNum)
@ -126,7 +123,7 @@ const TradePageGrid: React.FC = () => {
<ResponsiveGridLayout
layouts={savedLayouts ? savedLayouts : defaultLayouts}
breakpoints={breakpoints}
cols={{ xl: 12, lg: 12, md: 12, sm: 12 }}
cols={{ xxl: 12, xl: 12, lg: 12, md: 12, sm: 12 }}
rowHeight={15}
isDraggable={!uiLocked}
isResizable={!uiLocked}
@ -147,16 +144,6 @@ const TradePageGrid: React.FC = () => {
<div key="tradeForm">
<TradeForm />
</div>
<div key="accountInfo">
<FloatingElement className="h-full" showConnect>
<AccountInfo />
</FloatingElement>
</div>
<div key="marketPosition">
<FloatingElement className="h-full" showConnect>
<UserMarketInfo />
</FloatingElement>
</div>
<div key="marketTrades">
<FloatingElement className="h-full">
<RecentMarketTrades />

View File

@ -177,12 +177,20 @@ const TVChartContainer = () => {
custom_css_url: '/tradingview-chart.css',
loading_screen: {
backgroundColor:
theme === 'Dark' ? '#1B1B1F' : theme === 'Light' ? '#fff' : '#1D1832',
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
},
overrides: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
'paneProperties.background':
theme === 'Dark' ? '#1B1B1F' : theme === 'Light' ? '#fff' : '#1D1832',
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
'paneProperties.vertGridProperties.color':
theme === 'Dark' ? '#101012' : theme === 'Light' ? '#fff' : '#141026',
'paneProperties.horzGridProperties.color':
theme === 'Dark'
? '#1B1B1F'
: theme === 'Light'
? '#f7f7f7'
: '#1D1832',
...chartStyleOverrides,
},
}

View File

@ -1,4 +1,4 @@
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/outline'
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/solid'
import { Transition } from '@headlessui/react'
import useMangoStore from '../stores/useMangoStore'
import ResetLayout from './ResetLayout'

View File

@ -38,7 +38,7 @@ export const WalletSelect: React.FC<{ wallets: Wallet[] }> = ({ wallets }) => {
{wallets?.map((wallet, index) => (
<Menu.Item key={index}>
<button
className="flex w-full flex-row items-center justify-between rounded-none py-1.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
className="flex w-full flex-row items-center justify-between rounded-none py-1.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
onClick={() => {
select(wallet.adapter.name)
}}

View File

@ -11,7 +11,7 @@ import Tooltip from './Tooltip'
import {
ExclamationCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import Select from './Select'
import { withdraw } from '../utils/mango'
import {
@ -23,8 +23,9 @@ import {
} from '@blockworks-foundation/mango-client'
import { notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import { ExpandableRow } from './TableElements'
import { useWallet } from '@solana/wallet-adapter-react'
import MangoAccountSelect from './MangoAccountSelect'
import InlineNotification from './InlineNotification'
interface WithdrawModalProps {
onClose: () => void
@ -56,8 +57,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const actions = useMangoStore((s) => s.actions)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoAccounts = useMangoStore((s) => s.mangoAccounts)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const [withdrawMangoAccount, setWithdrawMangoAccount] =
useState<MangoAccount | null>(mangoAccount)
const tokens = useMemo(() => mangoGroupConfig.tokens, [mangoGroupConfig])
const token = useMemo(
@ -74,29 +78,43 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
marketMode == MarketMode.CloseOnly ||
marketMode == MarketMode.ForceCloseOnly
const initHealthRatio = useMemo(() => {
if (mangoAccount && mangoGroup && mangoCache) {
return mangoAccount
.getHealthRatio(mangoGroup, mangoCache, 'Init')
.toNumber()
}
return -1
}, [mangoAccount])
useEffect(() => {
if (!mangoGroup || !mangoAccount || !withdrawTokenSymbol || !mangoCache)
if (
!mangoGroup ||
!withdrawMangoAccount ||
!withdrawTokenSymbol ||
!mangoCache
)
return
const mintDecimals = mangoGroup.tokens[tokenIndex].decimals
const tokenDeposits = mangoAccount.getUiDeposit(
const tokenDeposits = withdrawMangoAccount.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
const tokenBorrows = mangoAccount.getUiBorrow(
const tokenBorrows = withdrawMangoAccount.getUiBorrow(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
const maxWithoutBorrows = nativeI80F48ToUi(
mangoAccount
withdrawMangoAccount
.getAvailableBalance(mangoGroup, mangoCache, tokenIndex)
.floor(),
mangoGroup.tokens[tokenIndex].decimals
)
const maxWithBorrows = mangoAccount
const maxWithBorrows = withdrawMangoAccount
.getMaxWithBorrowForToken(mangoGroup, mangoCache, tokenIndex)
.add(maxWithoutBorrows)
.mul(I80F48.fromString('0.995')) // handle rounding errors when borrowing
@ -114,7 +132,8 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
if (maxWithdraw.gt(I80F48.fromNumber(0)) && token) {
setMaxAmount(
floorToDecimal(parseFloat(maxWithdraw.toFixed()), token.decimals)
floorToDecimal(parseFloat(maxWithdraw.toFixed()), token.decimals) -
1 / Math.pow(10, token.decimals)
)
} else {
setMaxAmount(0)
@ -134,9 +153,9 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
// clone MangoAccount and arrays to not modify selectedMangoAccount
// FIXME: MangoAccount needs type updated to accept null for pubKey
// @ts-ignore
const simulation = new MangoAccount(null, mangoAccount)
simulation.deposits = [...mangoAccount.deposits]
simulation.borrows = [...mangoAccount.borrows]
const simulation = new MangoAccount(null, withdrawMangoAccount)
simulation.deposits = [...withdrawMangoAccount.deposits]
simulation.borrows = [...withdrawMangoAccount.borrows]
// update with simulated values
simulation.deposits[tokenIndex] = newDeposit
@ -149,23 +168,29 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const liabsVal = simulation
.getLiabsVal(mangoGroup, mangoCache, 'Init')
.toNumber()
const leverage = simulation.getLeverage(mangoGroup, mangoCache).toNumber()
const equity = simulation.computeValue(mangoGroup, mangoCache).toNumber()
const initHealthRatio = simulation
.getHealthRatio(mangoGroup, mangoCache, 'Init')
.toNumber()
const maintHealthRatio = simulation
.getHealthRatio(mangoGroup, mangoCache, 'Maint')
.toNumber()
const leverage = simulation.getLeverage(mangoGroup, mangoCache).toNumber()
setSimulation({
initHealthRatio,
liabsVal,
leverage,
equity,
initHealthRatio,
leverage,
liabsVal,
maintHealthRatio,
})
}, [
includeBorrow,
inputAmount,
tokenIndex,
mangoAccount,
withdrawMangoAccount,
mangoGroup,
mangoCache,
])
@ -181,10 +206,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
token: mangoGroup.tokens[tokenIndex].mint,
allowBorrow: includeBorrow,
wallet,
mangoAccount: withdrawMangoAccount,
})
.then((txid: string) => {
setSubmitting(false)
actions.reloadMangoAccount()
actions.fetchAllMangoAccounts(wallet)
actions.fetchWalletTokens(wallet)
notify({
title: t('withdraw-success'),
@ -212,8 +238,8 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
}
const getDepositsForSelectedAsset = (): I80F48 => {
return mangoAccount && mangoCache && mangoGroup
? mangoAccount.getUiDeposit(
return withdrawMangoAccount && mangoCache && mangoGroup
? withdrawMangoAccount.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
@ -228,11 +254,11 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
}
const getAccountStatusColor = (
initHealthRatio: number,
health: number,
isRisk?: boolean,
isStatus?: boolean
) => {
if (initHealthRatio < 1) {
if (health < 15) {
return isRisk ? (
<div className="text-th-red">{t('high')}</div>
) : isStatus ? (
@ -240,7 +266,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
) : (
'ring-th-red text-th-red'
)
} else if (initHealthRatio > 1 && initHealthRatio < 10) {
} else if (health >= 15 && health < 50) {
return isRisk ? (
<div className="text-th-orange">{t('moderate')}</div>
) : isStatus ? (
@ -296,7 +322,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
return {
symbol: token.symbol,
balance: mangoAccount
balance: withdrawMangoAccount
?.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
@ -320,10 +346,24 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
{title ? title : t('withdraw-funds')}
</ElementTitle>
</Modal.Header>
{initHealthRatio < 0 ? (
<div className="pb-2">
<InlineNotification type="error" desc={t('no-new-positions')} />
</div>
) : null}
{mangoAccounts.length > 1 ? (
<div className="mb-4">
<Label>{t('from-account')}</Label>
<MangoAccountSelect
onChange={(v) => setWithdrawMangoAccount(v)}
value={withdrawMangoAccount}
/>
</div>
) : null}
<Label>{t('asset')}</Label>
<Select
value={
withdrawTokenSymbol && mangoAccount ? (
withdrawTokenSymbol && withdrawMangoAccount ? (
<div className="flex w-full items-center justify-between">
<div className="flex items-center">
<img
@ -335,7 +375,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
/>
{withdrawTokenSymbol}
</div>
{mangoAccount
{withdrawMangoAccount
?.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
@ -373,7 +413,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
<span>{t('borrow-funds')}</span>
<Tooltip content={t('tooltip-interest-charged')}>
<InformationCircleIcon
className={`ml-2 h-5 w-5 cursor-help text-th-primary`}
className={`ml-2 h-5 w-5 cursor-help text-th-fgd-4`}
/>
</Tooltip>
</div>
@ -407,22 +447,34 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
onChange={(e) => onChangeAmountInput(e.target.value)}
suffix={withdrawTokenSymbol}
/>
{simulation ? (
<Tooltip
placement="right"
content={t('tooltip-projected-leverage')}
className="py-1"
>
<span
className={`${getAccountStatusColor(
simulation.initHealthRatio
)} ml-1 flex h-10 items-center justify-center rounded bg-th-bkg-1 px-2 ring-1 ring-inset`}
</div>
{simulation ? (
<div className="mt-4 space-y-2 bg-th-bkg-2 p-4">
<div className="flex justify-between">
<p className="mb-0">{t('tooltip-projected-health')}</p>
<p
className={`mb-0 font-bold text-th-fgd-1 ${getAccountStatusColor(
simulation.maintHealthRatio
)}`}
>
{simulation.maintHealthRatio > 100
? '>100'
: simulation.maintHealthRatio.toFixed(2)}
%
</p>
</div>
<div className="flex justify-between">
<p className="mb-0">{t('tooltip-projected-leverage')}</p>
<p
className={`mb-0 font-bold text-th-fgd-1 ${getAccountStatusColor(
simulation.maintHealthRatio
)}`}
>
{simulation.leverage.toFixed(2)}x
</span>
</Tooltip>
) : null}
</div>
</p>
</div>
</div>
) : null}
{invalidAmountMessage ? (
<div className="flex items-center pt-1.5 text-th-red">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
@ -432,7 +484,12 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
<div className={`flex justify-center pt-6`}>
<Button
onClick={() => setShowSimulation(true)}
disabled={Number(inputAmount) <= 0}
disabled={
!inputAmount ||
Number(inputAmount) <= 0 ||
!!invalidAmountMessage ||
initHealthRatio < 0
}
className="w-full"
>
{t('next')}
@ -448,18 +505,17 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
</ElementTitle>
</Modal.Header>
{simulation.initHealthRatio < 0 ? (
<div className="mb-4 rounded border border-th-red p-2">
<div className="flex items-center text-th-red">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{t('prices-changed')}
</div>
<div className="pb-2">
<InlineNotification type="error" desc={t('prices-changed')} />
</div>
) : null}
<div className="rounded-lg bg-th-bkg-1 p-4 text-center text-th-fgd-1">
<div className="pb-1 text-th-fgd-3">{t('about-to-withdraw')}</div>
<div className="flex items-center justify-center">
<div className="relative text-xl font-semibold">
{inputAmount}
{Number(inputAmount).toLocaleString(undefined, {
maximumFractionDigits: token?.decimals,
})}
<span className="absolute bottom-0.5 ml-1.5 text-xs font-normal text-th-fgd-4">
{withdrawTokenSymbol}
</span>
@ -473,79 +529,6 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
)} ${withdrawTokenSymbol}`}</div>
) : null}
</div>
<div className="border-b border-th-bkg-4 pt-4">
<ExpandableRow
buttonTemplate={
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="relative mr-2.5 flex h-2 w-2">
<span
className={`absolute inline-flex h-full w-full animate-ping rounded-full ${getAccountStatusColor(
simulation.initHealthRatio,
false,
true
)} opacity-75`}
></span>
<span
className={`relative inline-flex h-2 w-2 rounded-full ${getAccountStatusColor(
simulation.initHealthRatio,
false,
true
)}`}
></span>
</span>
{t('health-check')}
<Tooltip content={t('tooltip-after-withdrawal')}>
<InformationCircleIcon
className={`ml-2 h-5 w-5 cursor-help text-th-primary`}
/>
</Tooltip>
</div>
</div>
}
panelTemplate={
simulation ? (
<div>
<div className="flex justify-between pb-2">
<p className="mb-0">{t('account-value')}</p>
<div className="text-th-fgd-1">
$
{simulation.equity.toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</div>
</div>
<div className="flex justify-between pb-2">
<p className="mb-0">{t('account-risk')}</p>
<div className="text-th-fgd-1">
{getAccountStatusColor(
simulation.initHealthRatio,
true
)}
</div>
</div>
<div className="flex justify-between pb-2">
<p className="mb-0">{t('leverage')}</p>
<div className="text-th-fgd-1">
{simulation.leverage.toFixed(2)}x
</div>
</div>
<div className="flex justify-between">
<p className="mb-0">{t('borrow-value')}</p>
<div className="text-th-fgd-1">
$
{simulation.liabsVal.toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</div>
</div>
</div>
) : null
}
/>
</div>
<div className={`mt-6 flex flex-col items-center`}>
<Button
onClick={handleWithdraw}

View File

@ -5,7 +5,6 @@ import {
I80F48,
} from '@blockworks-foundation/mango-client'
import useMangoStore from '../../stores/useMangoStore'
import { useBalances } from '../../hooks/useBalances'
import {
formatUsdValue,
i80f48ToPercent,
@ -25,7 +24,6 @@ import { useWallet } from '@solana/wallet-adapter-react'
export default function AccountBorrows() {
const { t } = useTranslation('common')
const balances = useBalances()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const mangoConfig = useMangoStore((s) => s.selectedMangoGroup.config)
@ -34,6 +32,7 @@ export default function AccountBorrows() {
const loadingMangoAccount = useMangoStore(
(s) => s.selectedMangoAccount.initialLoad
)
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
const [borrowSymbol, setBorrowSymbol] = useState('')
const [depositToSettle, setDepositToSettle] = useState<any | null>(null)
@ -77,7 +76,7 @@ export default function AccountBorrows() {
<div className="flex flex-col pb-8 pt-4">
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full align-middle sm:px-6 lg:px-8">
{balances.find((b) => b?.borrows?.gt(ZERO_I80F48)) ? (
{spotBalances.find((b) => b?.borrows?.gt(ZERO_I80F48)) ? (
!isMobile ? (
<Table>
<thead>
@ -89,7 +88,7 @@ export default function AccountBorrows() {
</TrHead>
</thead>
<tbody>
{balances
{spotBalances
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
.map((asset) => {
const token = getTokenBySymbol(
@ -175,12 +174,12 @@ export default function AccountBorrows() {
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-4">
<div className="border-b border-th-bkg-3">
<MobileTableHeader
colOneHeader={t('asset')}
colTwoHeader={t('balance')}
/>
{balances
{spotBalances
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
.map((asset, i) => {
const token = getTokenBySymbol(
@ -286,7 +285,7 @@ export default function AccountBorrows() {
)
) : (
<div
className={`w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3`}
className={`w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3`}
>
{t('no-borrows')}
</div>
@ -425,7 +424,7 @@ export default function AccountBorrows() {
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-4">
<div className="border-b border-th-bkg-3">
<MobileTableHeader
colOneHeader={t('asset')}
colTwoHeader={`${t('deposit')}/${t('borrow-rate')}`}

View File

@ -11,7 +11,6 @@ import {
} from '../TableElements'
import isEmpty from 'lodash/isEmpty'
import { useTranslation } from 'next-i18next'
import Select from '../Select'
import Pagination from '../Pagination'
import usePagination from '../../hooks/usePagination'
import { roundToDecimal } from '../../utils'
@ -24,7 +23,8 @@ const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
import { exportDataToCSV } from '../../utils/export'
import Button from '../Button'
import { SaveIcon } from '@heroicons/react/outline'
import { SaveIcon } from '@heroicons/react/solid'
import TabButtons from 'components/TabButtons'
const QUOTE_DECIMALS = 6
@ -305,50 +305,23 @@ const AccountFunding = () => {
<>
{!isEmpty(hourlyFunding) && !loadHourlyStats ? (
<>
<div className="flex w-full items-center justify-between pb-4 pt-6">
<h2>{t('history')}</h2>
<Select
value={selectedAsset}
onChange={(a) => setSelectedAsset(a)}
className="w-24 sm:hidden"
>
<div className="space-y-2">
{Object.keys(hourlyFunding).map((token: string) => (
<Select.Option
key={token}
value={token}
className={`default-transition relative flex w-full cursor-pointer rounded-md bg-th-bkg-1 px-3 py-3 hover:bg-th-bkg-3 focus:outline-none`}
>
<div className="flex w-full items-center justify-between">
{token}
</div>
</Select.Option>
))}
</div>
</Select>
<div className="hidden pb-4 sm:flex sm:pb-0">
{Object.keys(hourlyFunding).map((token: string) => (
<div
className={`default-transition ml-2 cursor-pointer rounded-md bg-th-bkg-3 px-2 py-1
${
selectedAsset === token
? `text-th-primary ring-1 ring-inset ring-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => setSelectedAsset(token)}
key={token}
>
{token}-PERP
</div>
))}
</div>
<div className="pb-2 pt-6">
<h2 className="mb-4">{t('history')}</h2>
<TabButtons
activeTab={selectedAsset}
tabs={Object.keys(hourlyFunding).map((token: string) => ({
label: token,
key: token,
}))}
onClick={setSelectedAsset}
showSymbolIcon
/>
</div>
{selectedAsset && chartData.length > 0 ? (
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
{chartData.find((d) => d.funding !== 0) ? (
<div
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4"
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4"
style={{ height: '330px' }}
>
<Chart

View File

@ -5,9 +5,10 @@ import {
ExternalLinkIcon,
InformationCircleIcon,
SaveIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
import {
getMarketByBaseSymbolAndKind,
getTokenBySymbol,
PerpMarket,
} from '@blockworks-foundation/mango-client'
@ -21,6 +22,7 @@ import {
Td,
TableDateDisplay,
Row,
ExpandableRow,
} from '../TableElements'
import { LinkButton } from '../Button'
import { useSortableData } from '../../hooks/useSortableData'
@ -32,16 +34,20 @@ import Button from '../Button'
import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from '.././TradePageGrid'
import MobileTableHeader from 'components/mobile/MobileTableHeader'
import AccountInterest from './AccountInterest'
import AccountFunding from './AccountFunding'
import TabButtons from 'components/TabButtons'
const historyViews = [
{ label: 'Trades', key: 'Trades' },
{ label: 'Deposits', key: 'Deposit' },
{ label: 'Withdrawals', key: 'Withdraw' },
{ label: 'Interest', key: 'Interest' },
{ label: 'Funding', key: 'Funding' },
{ label: 'Liquidations', key: 'Liquidation' },
]
export default function AccountHistory() {
const { t } = useTranslation('common')
const [view, setView] = useState('Trades')
const [history, setHistory] = useState(null)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
@ -68,24 +74,8 @@ export default function AccountHistory() {
return (
<>
<div className="mb-4 flex rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:px-4">
{historyViews.map(({ label, key }, index) => (
<div
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
index > 0 ? 'ml-4 md:ml-2' : null
} default-transition cursor-pointer rounded-md
${
view === key
? `text-th-primary`
: `text-th-fgd-3 hover:text-th-fgd-1`
}
`}
onClick={() => setView(key)}
key={key as string}
>
{t(label.toLowerCase())}
</div>
))}
<div className="mb-2">
<TabButtons activeTab={view} tabs={historyViews} onClick={setView} />
</div>
<ViewContent view={view} history={history} />
</>
@ -100,6 +90,10 @@ const ViewContent = ({ view, history }) => {
return <HistoryTable history={history} view={view} />
case 'Withdraw':
return <HistoryTable history={history} view={view} />
case 'Interest':
return <AccountInterest />
case 'Funding':
return <AccountFunding />
case 'Liquidation':
return <LiquidationHistoryTable history={history} view={view} />
default:
@ -108,23 +102,40 @@ const ViewContent = ({ view, history }) => {
}
const parseActivityDetails = (activity_details, activity_type, perpMarket) => {
const groupConfig = useMangoStore.getState().selectedMangoGroup.config
let assetGained, assetLost
const assetSymbol =
activity_type === 'liquidate_perp_market'
? 'USD (PERP)'
? 'USDC'
: activity_details.asset_symbol
const assetDecimals = activity_type.includes('perp')
? getMarketByBaseSymbolAndKind(
groupConfig,
assetSymbol.split('-')[0],
'perp'
).baseDecimals
: getTokenBySymbol(groupConfig, assetSymbol.split('-')[0]).decimals
const liabSymbol =
activity_type === 'liquidate_perp_market' ||
activity_details.liab_type === 'Perp'
? activity_details.liab_symbol.includes('USDC')
? 'USD (PERP)'
? 'USDC'
: `${activity_details.liab_symbol}-PERP`
: activity_details.liab_symbol
const liabDecimals = activity_type.includes('perp')
? getMarketByBaseSymbolAndKind(
groupConfig,
liabSymbol.split('-')[0],
'perp'
).baseDecimals
: getTokenBySymbol(groupConfig, liabSymbol.split('-')[0]).decimals
const liabAmount =
perpMarket && liabSymbol !== 'USD (PERP)'
perpMarket && liabSymbol !== 'USDC'
? perpMarket.baseLotsToNumber(activity_details.liab_amount)
: activity_details.liab_amount
@ -132,12 +143,14 @@ const parseActivityDetails = (activity_details, activity_type, perpMarket) => {
const asset_amount = {
amount: parseFloat(assetAmount),
decimals: assetDecimals,
symbol: assetSymbol,
price: parseFloat(activity_details.asset_price),
}
const liab_amount = {
amount: parseFloat(liabAmount),
decimals: liabDecimals,
symbol: liabSymbol,
price: parseFloat(activity_details.liab_price),
}
@ -176,6 +189,8 @@ const LiquidationHistoryTable = ({ history, view }) => {
: []
}, [history, view])
const { items, requestSort, sortConfig } = useSortableData(filteredHistory)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const exportHistoryToCSV = () => {
const dataToExport = history
@ -234,7 +249,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
</Tooltip>
</div>
<Button
@ -248,7 +263,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
</Button>
</div>
{items.length ? (
<>
!isMobile ? (
<Table>
<thead>
<TrHead>
@ -275,7 +290,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
className="flex items-center font-normal no-underline"
onClick={() => requestSort('asset_amount')}
>
<span className="font-normal">Asset Lost</span>
<span className="font-normal">{t('asset-liquidated')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'asset_amount'
@ -287,30 +302,12 @@ const LiquidationHistoryTable = ({ history, view }) => {
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('asset_price')}
>
<span className="font-normal">Price</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'asset_price'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('liab_amount')}
>
<span className="font-normal">Asset Gained</span>
<span className="font-normal">{t('asset-returned')}</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liab_amount'
@ -323,21 +320,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('liab_price')}
>
<span className="font-normal">Price</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'liab_price'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
<span className="font-normal">{t('liquidation-fee')}</span>
</Th>
<Th>
<span></span>
@ -371,8 +354,10 @@ const LiquidationHistoryTable = ({ history, view }) => {
perpMarket
)
const lostDecimals = assetLost.symbol === 'SOL' ? 9 : 6
const gainedDecimals = assetGained.symbol === 'SOL' ? 9 : 6
const valueLost = Math.abs(assetLost.amount * assetLost.price)
const valueGained = assetGained.amount * assetGained.price
const liquidationFee = valueGained - valueLost
return (
<TrBody key={activity_details.signature}>
<Td>
@ -380,35 +365,43 @@ const LiquidationHistoryTable = ({ history, view }) => {
date={activity_details.block_datetime}
/>
</Td>
<Td>
<span className="text-th-red">
<span>
{Math.abs(assetLost.amount).toLocaleString(undefined, {
maximumFractionDigits: lostDecimals,
maximumFractionDigits: assetLost.decimals,
})}{' '}
</span>
{assetLost.symbol}
{`${assetLost.symbol} at ${formatUsdValue(
assetLost.price
)}`}
<p className="mb-0 text-xs text-th-fgd-3">
{formatUsdValue(valueLost)}
</p>
</Td>
<Td>
{assetLost.price.toLocaleString(undefined, {
maximumFractionDigits: lostDecimals,
})}
</Td>
<Td>
<span className="text-th-green">
<span>
{Math.abs(assetGained.amount).toLocaleString(
undefined,
{
maximumFractionDigits: gainedDecimals,
maximumFractionDigits: assetGained.decimals,
}
)}{' '}
</span>
{assetGained.symbol}
{`${assetGained.symbol} at ${formatUsdValue(
assetGained.price
)}`}
<p className="mb-0 text-xs text-th-fgd-3">
{formatUsdValue(valueGained)}
</p>
</Td>
<Td>
{assetGained.price.toLocaleString(undefined, {
maximumFractionDigits: gainedDecimals,
})}
<span
className={
liquidationFee >= 0 ? 'text-th-green' : 'text-th-red'
}
>
{formatUsdValue(liquidationFee)}
</span>
</Td>
<Td>
<a
@ -417,7 +410,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
target="_blank"
rel="noopener noreferrer"
>
<span>View Transaction</span>
<span>{t('view-transaction')}</span>
<ExternalLinkIcon className={`ml-1.5 h-4 w-4`} />
</a>
</Td>
@ -426,9 +419,118 @@ const LiquidationHistoryTable = ({ history, view }) => {
})}
</tbody>
</Table>
</>
) : (
<div className="border-b border-th-bkg-3">
<MobileTableHeader
colOneHeader={t('date')}
colTwoHeader={t('liquidation-fee')}
/>
{items.map(({ activity_details, activity_type }) => {
let perpMarket: PerpMarket | null = null
if (activity_type.includes('perp')) {
const symbol = activity_details.perp_market.split('-')[0]
const marketConfig = getMarketByBaseSymbolAndKind(
groupConfig,
symbol,
'perp'
)
perpMarket = markets[
marketConfig.publicKey.toString()
] as PerpMarket
}
const [assetGained, assetLost] = parseActivityDetails(
activity_details,
activity_type,
perpMarket
)
const valueLost = Math.abs(assetLost.amount * assetLost.price)
const valueGained = assetGained.amount * assetGained.price
const liquidationFee = valueGained - valueLost
return (
<ExpandableRow
buttonTemplate={
<div className="flex w-full items-center justify-between text-th-fgd-1">
<div className="text-left">
<TableDateDisplay
date={activity_details.block_datetime}
/>
</div>
<div className="text-right text-th-fgd-1">
<span
className={
liquidationFee >= 0
? 'text-th-green'
: 'text-th-red'
}
>
{formatUsdValue(liquidationFee)}
</span>
</div>
</div>
}
key={`${activity_details.signature}`}
panelTemplate={
<div className="grid grid-flow-row grid-cols-2 gap-4 pb-4">
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('asset-liquidated')}
</div>
<span>
{Math.abs(assetLost.amount).toLocaleString(
undefined,
{
maximumFractionDigits: assetLost.decimals,
}
)}{' '}
</span>
{`${assetLost.symbol} at ${formatUsdValue(
assetLost.price
)}`}
<p className="mb-0 text-xs text-th-fgd-3">
{formatUsdValue(valueLost)}
</p>
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('asset-returned')}
</div>
<span>
{Math.abs(assetGained.amount).toLocaleString(
undefined,
{
maximumFractionDigits: assetGained.decimals,
}
)}{' '}
</span>
{`${assetGained.symbol} at ${formatUsdValue(
assetGained.price
)}`}
<p className="mb-0 text-xs text-th-fgd-3">
{formatUsdValue(valueGained)}
</p>
</div>
<div className="col-span-2">
<a
className="default-transition flex h-8 w-full items-center justify-center rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-2 text-xs font-bold text-th-fgd-2"
href={`https://explorer.solana.com/tx/${activity_details.signature}`}
target="_blank"
rel="noopener noreferrer"
>
<span>{t('view-transaction')}</span>
<ExternalLinkIcon className={`ml-1.5 h-4 w-4`} />
</a>
</div>
</div>
}
/>
)
})}
</div>
)
) : (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
{t('history-empty')}
</div>
)}
@ -511,7 +613,7 @@ const HistoryTable = ({ history, view }) => {
</div>
}
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-4" />
</Tooltip>
</div>
<Button
@ -647,7 +749,7 @@ const HistoryTable = ({ history, view }) => {
</tbody>
</Table>
) : (
<div className="mb-4 border-b border-th-bkg-4">
<div className="mb-4 border-b border-th-bkg-3">
<MobileTableHeader
colOneHeader={t('date')}
colTwoHeader={t('asset')}
@ -691,7 +793,7 @@ const HistoryTable = ({ history, view }) => {
</div>
)
) : (
<div className="w-full rounded-md bg-th-bkg-1 py-6 text-center text-th-fgd-3">
<div className="w-full rounded-md border border-th-bkg-3 py-6 text-center text-th-fgd-3">
{t('history-empty')}
</div>
)}

View File

@ -2,7 +2,6 @@ import { getTokenBySymbol } from '@blockworks-foundation/mango-client'
import { useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import useMangoStore from '../../stores/useMangoStore'
import Select from '../Select'
import {
Table,
TableDateDisplay,
@ -27,8 +26,9 @@ import useLocalStorageState from '../../hooks/useLocalStorageState'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
import { exportDataToCSV } from '../../utils/export'
import { SaveIcon } from '@heroicons/react/outline'
import { SaveIcon } from '@heroicons/react/solid'
import Button from '../Button'
import TabButtons from 'components/TabButtons'
interface InterestStats {
[key: string]: {
@ -458,50 +458,22 @@ const AccountInterest = () => {
<>
{!isEmpty(hourlyInterestStats) && !loadHourlyStats ? (
<>
<div className="flex w-full items-center justify-between pb-4 pt-8">
<h2>{t('history')}</h2>
<Select
value={selectedAsset}
onChange={(a) => setSelectedAsset(a)}
className="w-24 md:hidden"
>
<div className="space-y-2">
{Object.keys(hourlyInterestStats).map((token: string) => (
<Select.Option
key={token}
value={token}
className={`default-transition relative flex w-full cursor-pointer rounded-md bg-th-bkg-1 px-3 py-3 hover:bg-th-bkg-3 focus:outline-none`}
>
<div className="flex w-full items-center justify-between">
{token}
</div>
</Select.Option>
))}
</div>
</Select>
<div className="hidden pb-4 sm:pb-0 md:flex">
{Object.keys(hourlyInterestStats).map((token: string) => (
<div
className={`default-transition ml-2 cursor-pointer rounded-md bg-th-bkg-3 px-2 py-1
${
selectedAsset === token
? `text-th-primary ring-1 ring-inset ring-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => setSelectedAsset(token)}
key={token}
>
{token}
</div>
))}
</div>
<div className="pb-2 pt-8">
<h2 className="mb-4">{t('history')}</h2>
<TabButtons
activeTab={selectedAsset}
tabs={Object.keys(hourlyInterestStats).map(
(token: string) => ({ label: token, key: token })
)}
onClick={setSelectedAsset}
showSymbolIcon
/>
</div>
{selectedAsset && chartData.length > 0 ? (
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
{chartData.find((d) => d.interest !== 0) ? (
<div
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
style={{ height: '330px' }}
>
<Chart
@ -533,7 +505,7 @@ const AccountInterest = () => {
) : null}
{chartData.find((d) => d.value !== 0) ? (
<div
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
style={{ height: '330px' }}
>
{token ? (

View File

@ -1,30 +1,19 @@
import { useEffect, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { ExclamationIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
import useMangoStore from '../../stores/useMangoStore'
import { formatUsdValue } from '../../utils'
import BalancesTable from '../BalancesTable'
import Switch from '../Switch'
import useLocalStorageState from '../../hooks/useLocalStorageState'
import ButtonGroup from '../ButtonGroup'
import PerformanceChart from './PerformanceChart'
import PositionsTable from '../PerpPositionsTable'
import AccountOverviewStats from './AccountOverviewStats'
dayjs.extend(utc)
const SHOW_ZERO_BALANCE_KEY = 'showZeroAccountBalances-0.2'
const performanceRangePresets = [
{ label: '24h', value: 1 },
{ label: '7d', value: 7 },
{ label: '30d', value: 30 },
{ label: '3m', value: 90 },
]
const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label)
export const fetchHourlyPerformanceStats = async (
mangoAccountPk: string,
range: number
@ -58,8 +47,6 @@ export default function AccountOverview() {
true
)
const [pnl, setPnl] = useState(0)
const [performanceRange, setPerformanceRange] = useState('30d')
const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState([])
useEffect(() => {
@ -68,12 +55,7 @@ export default function AccountOverview() {
if (!pubKey) {
return
}
const stats = await fetchHourlyPerformanceStats(
pubKey,
performanceRangePresets[performanceRangePresets.length - 1].value
)
setPnl(stats?.length ? stats?.[0]?.['pnl'] : 0)
const stats = await fetchHourlyPerformanceStats(pubKey, 30)
setHourlyPerformanceStats(stats)
}
if (pubKey) {
@ -81,143 +63,26 @@ export default function AccountOverview() {
}
}, [mangoAccount?.publicKey])
const maintHealthRatio = useMemo(() => {
return mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const initHealthRatio = useMemo(() => {
return mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const mangoAccountValue = useMemo(() => {
return mangoAccount && mangoGroup && mangoCache
? +mangoAccount.computeValue(mangoGroup, mangoCache)
: 0
}, [mangoAccount])
}, [mangoAccount, mangoGroup, mangoCache])
return mangoAccount ? (
<>
<div className="flex flex-col pb-8 md:flex-row md:space-x-6 md:pb-12">
<div className="w-full pb-8 md:w-1/3 md:pb-0 lg:w-1/4">
<h2 className="mb-4">{t('summary')}</h2>
<div className="border-y border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
{t('account-value')}
</div>
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
{formatUsdValue(mangoAccountValue)}
</div>
</div>
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
<div className="flex items-center justify-between">
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
{t('pnl')}{' '}
{hourlyPerformanceStats?.length ? (
<div className="text-xs text-th-fgd-4">
{dayjs(hourlyPerformanceStats[0]['time']).format(
'MMM D YYYY, h:mma'
)}
</div>
) : null}
</div>
</div>
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
{formatUsdValue(pnl)}
</div>
</div>
<div className="border-b border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
{t('leverage')}
</div>
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
{mangoAccount.getLeverage(mangoGroup, mangoCache).toFixed(2)}x
</div>
) : null}
</div>
<div className="p-3 sm:p-4">
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
{t('health-ratio')}
</div>
<div className={`text-xl font-bold text-th-fgd-1 sm:text-2xl`}>
{maintHealthRatio < 1000 ? maintHealthRatio.toFixed(2) : '>100'}%
</div>
{mangoAccount.beingLiquidated ? (
<div className="flex items-center pt-0.5 text-xs sm:pt-2 sm:text-sm">
<ExclamationIcon className="mr-1.5 h-5 w-5 flex-shrink-0 text-th-red sm:h-7 sm:w-7" />
<span className="text-th-red">{t('being-liquidated')}</span>
</div>
) : null}
</div>
<div className="flex h-1 rounded bg-th-bkg-3">
<div
style={{
width: `${maintHealthRatio}%`,
}}
className={`flex rounded ${
maintHealthRatio > 30
? 'bg-th-green'
: initHealthRatio > 0
? 'bg-th-orange'
: 'bg-th-red'
}`}
></div>
</div>
</div>
<div className="h-80 w-full md:h-auto md:w-2/3 lg:w-3/4">
<div className="mb-4 ml-auto md:w-56">
<ButtonGroup
activeValue={performanceRange}
className="h-8"
onChange={(p) => setPerformanceRange(p)}
values={performanceRangePresetLabels}
/>
</div>
<div className="md:border-t md:border-th-bkg-4">
<PerformanceChart
hourlyPerformanceStats={hourlyPerformanceStats}
performanceRange={performanceRange}
accountValue={mangoAccountValue}
/>
</div>
<div className="grid grid-cols-12 md:gap-x-6">
<div className="relative col-span-12 h-[700px] md:h-[615px] lg:h-[430px] xl:h-[320px]">
<AccountOverviewStats
hourlyPerformanceStats={hourlyPerformanceStats}
accountValue={mangoAccountValue}
/>
</div>
</div>
<div className="pb-8 pt-20 md:pt-0">
<div className="pb-8">
<h2 className="mb-4">{t('perp-positions')}</h2>
<PositionsTable />
</div>
<h2 className="mb-4">{t('assets-liabilities')}</h2>
<div className="grid grid-flow-col grid-cols-1 grid-rows-2 pb-8 md:grid-cols-2 md:grid-rows-1 md:gap-4 md:pb-12">
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('total-assets')}</div>
<div className="flex items-center">
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{formatUsdValue(
+mangoAccount.getAssetsVal(mangoGroup, mangoCache)
)}
</div>
) : null}
</div>
</div>
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3">{t('total-liabilities')}</div>
<div className="flex items-center">
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{formatUsdValue(
+mangoAccount.getLiabsVal(mangoGroup, mangoCache)
)}
</div>
) : null}
</div>
</div>
</div>
<div className="flex justify-between pb-4">
<h2>{t('balances')}</h2>
<Switch

View File

@ -0,0 +1,581 @@
import { useState, useEffect, useMemo } from 'react'
import { useTheme } from 'next-themes'
import cloneDeep from 'lodash/cloneDeep'
import dayjs from 'dayjs'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as ChartTooltip,
} from 'recharts'
import { InformationCircleIcon, ScaleIcon } from '@heroicons/react/solid'
import useDimensions from 'react-cool-dimensions'
import { useTranslation } from 'next-i18next'
import { ZERO_BN } from '@blockworks-foundation/mango-client'
import ButtonGroup from '../ButtonGroup'
import { formatUsdValue } from '../../utils'
import { numberCompacter } from '../SwapTokenInfo'
import Checkbox from '../Checkbox'
import Tooltip from '../Tooltip'
import useMangoStore, { PerpPosition } from 'stores/useMangoStore'
import LongShortChart from './LongShortChart'
import HealthHeart from 'components/HealthHeart'
type AccountOverviewStats = {
hourlyPerformanceStats: any[]
performanceRange: '24hr' | '7d' | '30d' | '3m'
}
const defaultData = [
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
]
const performanceRangePresets = [
{ label: '24h', value: 1 },
{ label: '7d', value: 7 },
{ label: '30d', value: 30 },
{ label: '3m', value: 90 },
]
const performanceRangePresetLabels = performanceRangePresets.map((x) => x.label)
const AccountOverviewStats = ({ hourlyPerformanceStats, accountValue }) => {
const { theme } = useTheme()
const { t } = useTranslation('common')
const { observe, width, height } = useDimensions()
const [chartToShow, setChartToShow] = useState<string>('Value')
const [chartData, setChartData] = useState<any[]>([])
const [mouseData, setMouseData] = useState<string | null>(null)
const [performanceRange, setPerformanceRange] = useState('30d')
const [showSpotPnl, setShowSpotPnl] = useState(true)
const [showPerpPnl, setShowPerpPnl] = useState(true)
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const spotBalances = useMangoStore((s) => s.selectedMangoAccount.spotBalances)
const perpPositions = useMangoStore(
(s) => s.selectedMangoAccount.perpPositions
)
const maintHealthRatio = useMemo(() => {
return mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const { longData, shortData, longExposure, shortExposure } = useMemo(() => {
const longData: any = []
const shortData: any = []
if (!spotBalances || !perpPositions) {
longData.push({ symbol: 'spacer', value: 1 })
shortData.push({ symbol: 'spacer', value: 1 })
// return {}
}
const DUST_THRESHOLD = 0.05
const netUnsettledPositionsValue = perpPositions.reduce(
(a, c) => a + (c?.unsettledPnl ?? 0),
0
)
for (const { net, symbol, value } of spotBalances) {
let amount = Number(net)
let totValue = Number(value)
if (symbol === 'USDC') {
amount += netUnsettledPositionsValue
totValue += netUnsettledPositionsValue
}
if (totValue > DUST_THRESHOLD) {
longData.push({
asset: symbol,
amount: amount,
symbol: symbol,
value: totValue,
})
}
if (-totValue > DUST_THRESHOLD) {
shortData.push({
asset: symbol,
amount: Math.abs(amount),
symbol: symbol,
value: Math.abs(totValue),
})
}
}
for (const {
marketConfig,
basePosition,
notionalSize,
perpAccount,
} of perpPositions.filter((p) => !!p) as PerpPosition[]) {
if (notionalSize < DUST_THRESHOLD) continue
if (perpAccount.basePosition.gt(ZERO_BN)) {
longData.push({
asset: marketConfig.name,
amount: basePosition,
symbol: marketConfig.baseSymbol,
value: notionalSize,
})
} else {
shortData.push({
asset: marketConfig.name,
amount: Math.abs(basePosition),
symbol: marketConfig.baseSymbol,
value: notionalSize,
})
}
}
const longExposure = longData.reduce((a, c) => a + c.value, 0)
const shortExposure = shortData.reduce((a, c) => a + c.value, 0)
if (shortExposure === 0) {
shortData.push({ symbol: 'spacer', value: 1 })
}
if (longExposure === 0) {
longData.push({ symbol: 'spacer', value: 1 })
}
const dif = longExposure - shortExposure
if (dif > 0) {
shortData.push({ symbol: 'spacer', value: dif })
}
return { longData, shortData, longExposure, shortExposure }
}, [spotBalances, perpPositions])
useEffect(() => {
if (hourlyPerformanceStats.length > 0) {
if (performanceRange === '3m') {
setChartData(hourlyPerformanceStats.slice().reverse())
}
if (performanceRange === '30d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '7d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '24h') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
} else {
setChartData([])
}
}, [hourlyPerformanceStats, performanceRange])
useEffect(() => {
if (chartData.length > 0) {
for (const stat of chartData) {
stat.spot_pnl = stat.pnl - stat.perp_pnl
}
}
}, [chartData])
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const renderPnlChartTitle = () => {
if (showPerpPnl && showSpotPnl) {
return t('pnl')
}
if (!showSpotPnl) {
return `${t('perp')} ${t('pnl')}`
}
if (!showPerpPnl) {
return `${t('spot')} ${t('pnl')}`
}
}
const formatDateAxis = (date) => {
if (['3m', '30d'].includes(performanceRange)) {
return dayjs(date).format('D MMM')
} else if (performanceRange === '7d') {
return dayjs(date).format('ddd, h:mma')
} else {
return dayjs(date).format('h:mma')
}
}
const pnlChartDataKey = () => {
if (!showPerpPnl && showSpotPnl) {
return 'spot_pnl'
} else if (!showSpotPnl && showPerpPnl) {
return 'perp_pnl'
} else {
return 'pnl'
}
}
const pnlChartColor =
chartToShow === 'PnL' &&
chartData.length > 0 &&
chartData[chartData.length - 1][pnlChartDataKey()] > 0
? theme === 'Mango'
? '#AFD803'
: '#5EBF4D'
: theme === 'Mango'
? '#F84638'
: '#CC2929'
return (
<div className="grid grid-cols-12 lg:gap-6">
<div className="order-2 col-span-12 lg:order-1 lg:col-span-4">
<div className="px-3 pb-4 xl:pb-6">
<div className="flex items-center pb-1.5">
<div className="text-sm text-th-fgd-3">
{chartToShow === 'Value'
? t('account-value')
: renderPnlChartTitle()}{' '}
</div>
</div>
{mouseData ? (
<>
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
{formatUsdValue(
mouseData[
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
]
)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length === 0 ? (
<>
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
{chartToShow === 'PnL' ? '--' : formatUsdValue(accountValue)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length > 0 ? (
<>
<div className="pb-1 text-2xl font-bold text-th-fgd-1 sm:text-3xl">
{chartToShow === 'PnL'
? formatUsdValue(
chartData[chartData.length - 1][pnlChartDataKey()]
)
: formatUsdValue(accountValue)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{chartToShow === 'PnL'
? dayjs(chartData[chartData.length - 1]['time']).format(
'ddd MMM D YYYY, h:mma'
)
: dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : (
<>
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
</>
)}
</div>
<div className="flex flex-col divide-y divide-th-bkg-3 border-y border-th-bkg-3 md:flex-row md:divide-y-0 md:p-3 lg:flex-col lg:divide-y lg:p-0 xl:flex-row xl:divide-y-0 xl:p-5">
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
<HealthHeart size={40} health={Number(maintHealthRatio)} />
<div>
<Tooltip
content={
<div>
{t('tooltip-account-liquidated')}{' '}
<a
href="https://docs.mango.markets/mango/health-overview"
target="_blank"
rel="noopener noreferrer"
>
{t('learn-more')}
</a>
</div>
}
>
<div className="flex items-center space-x-1.5 pb-0.5">
<div className="text-th-fgd-3">{t('health')}</div>
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
</div>
</Tooltip>
<div className={`text-lg font-bold text-th-fgd-1`}>
{maintHealthRatio < 100 ? maintHealthRatio.toFixed(2) : '>100'}%
</div>
</div>
</div>
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 md:pl-4 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
<ScaleIcon className="h-10 w-10 text-th-fgd-4" />
<div>
<div className="pb-0.5 text-th-fgd-3">{t('leverage')}</div>
{mangoGroup && mangoCache ? (
<div className={`text-lg font-bold text-th-fgd-1`}>
{mangoAccount?.getLeverage(mangoGroup, mangoCache).toFixed(2)}
x
</div>
) : null}
</div>
</div>
</div>
<div className="flex flex-col divide-y divide-th-bkg-3 border-b border-th-bkg-3 md:flex-row md:divide-y-0 md:p-3 lg:flex-col lg:divide-y lg:p-0 xl:flex-row xl:divide-y-0 xl:p-5">
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
<LongShortChart chartData={longData} />
<div>
<Tooltip content={t('total-long-tooltip')}>
<div className="flex items-center space-x-1.5 pb-0.5">
<div className="text-th-fgd-3">{t('long-exposure')}</div>
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
</div>
</Tooltip>
{mangoGroup && mangoCache ? (
<div className="text-lg font-bold text-th-fgd-1">
{formatUsdValue(+longExposure)}
</div>
) : null}
</div>
</div>
<div className="flex w-full items-center space-x-3 p-3 md:w-1/2 md:p-0 md:pl-4 lg:w-full lg:p-3 xl:w-1/2 xl:p-0">
<LongShortChart chartData={shortData} />
<div>
<Tooltip content={t('total-short-tooltip')}>
<div className="flex items-center space-x-1.5 pb-0.5">
<div className="whitespace-nowrap text-th-fgd-3">
{t('short-exposure')}
</div>
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 cursor-help text-th-fgd-4" />
</div>
</Tooltip>
{mangoGroup && mangoCache ? (
<div className="text-lg font-bold text-th-fgd-1">
{formatUsdValue(+shortExposure)}
</div>
) : null}
</div>
</div>
</div>
</div>
<div className="order-1 col-span-12 px-4 pb-6 lg:order-2 lg:col-span-8 lg:pb-0 xl:px-6">
<div className="mb-4 flex justify-between space-x-2 sm:mb-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:space-x-3">
<div className="mb-3 w-28 sm:mb-0">
<ButtonGroup
activeValue={chartToShow}
className="h-8"
onChange={(c) => setChartToShow(c)}
values={['Value', 'PnL']}
/>
</div>
{chartToShow === 'PnL' && chartData.length ? (
<div className="flex space-x-3">
<Checkbox
checked={showSpotPnl}
disabled={!showPerpPnl}
onChange={(e) => setShowSpotPnl(e.target.checked)}
>
{t('include-spot')}
</Checkbox>
<Checkbox
checked={showPerpPnl}
disabled={!showSpotPnl}
onChange={(e) => setShowPerpPnl(e.target.checked)}
>
{t('include-perp')}
</Checkbox>
</div>
) : null}
</div>
<div className="w-40">
<ButtonGroup
activeValue={performanceRange}
className="h-8"
onChange={(p) => setPerformanceRange(p)}
values={performanceRangePresetLabels}
/>
</div>
</div>
{chartData.length > 0 ? (
<div className="h-48 md:h-64 lg:h-[340px] xl:h-[225px]" ref={observe}>
<AreaChart
width={width}
height={height + 12}
data={chartData?.length ? chartData : defaultData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<ChartTooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient
id="defaultGradientArea"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
<linearGradient
id="greenGradientArea"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient
id="redGradientArea"
x1="0"
y1="1"
x2="0"
y2="0"
>
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={true}
type="monotone"
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
fill={
chartToShow === 'PnL'
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
? 'url(#greenGradientArea)'
: 'url(#redGradientArea)'
: 'url(#defaultGradientArea)'
}
fillOpacity={0.3}
/>
<YAxis
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
type="number"
domain={['dataMin', 'dataMax']}
axisLine={false}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => numberCompacter.format(v)}
/>
<XAxis
dataKey="time"
axisLine={false}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
</AreaChart>
</div>
) : (
<div className="flex h-48 w-full items-center justify-center rounded-md bg-th-bkg-3 md:h-64 lg:h-[410px] xl:h-[270px]">
<p className="mb-0">{t('no-chart')}</p>
</div>
)}
</div>
</div>
)
}
export default AccountOverviewStats

View File

@ -12,7 +12,7 @@ import Chart from '../Chart'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
import { exportDataToCSV } from '../../utils/export'
import { SaveIcon } from '@heroicons/react/outline'
import { SaveIcon } from '@heroicons/react/solid'
import Button from '../Button'
export const handleDustTicks = (v) =>
@ -125,7 +125,7 @@ const AccountPerformance = () => {
{chartData.length > 0 ? (
<div className="flex w-full flex-col space-x-0 sm:flex-row sm:space-x-4">
<div
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
style={{ height: '330px' }}
>
<Chart
@ -141,7 +141,7 @@ const AccountPerformance = () => {
/>
</div>
<div
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
className="relative mb-6 w-full rounded-md border border-th-bkg-3 p-4 sm:w-1/2"
style={{ height: '330px' }}
>
<Chart

View File

@ -3,8 +3,6 @@ import dayjs from 'dayjs'
import isEmpty from 'lodash/isEmpty'
import { useTranslation } from 'next-i18next'
import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts'
import { SaveIcon } from '@heroicons/react/outline'
import useMangoStore from '../../stores/useMangoStore'
import { numberCompactFormatter } from '../../utils/'
import { exportDataToCSV } from '../../utils/export'
@ -14,8 +12,9 @@ import Select from 'components/Select'
import Checkbox from 'components/Checkbox'
import ButtonGroup from 'components/ButtonGroup'
import * as MonoIcons from '../icons'
import { QuestionMarkCircleIcon } from '@heroicons/react/outline'
import { SaveIcon, QuestionMarkCircleIcon } from '@heroicons/react/solid'
import { useTheme } from 'next-themes'
import { CHART_COLORS } from './LongShortChart'
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
@ -28,25 +27,6 @@ export const handleDustTicks = (v) => {
: numberCompactFormatter.format(v)
}
// Each line added to the graph will use one of these colors
const COLORS = {
All: '#ff7c43',
USDC: '#ffa600',
SRM: '#8dd3c7',
SOL: '#A288E3',
RAY: '#4AB839',
MSOL: '#fb8072',
MNGO: '#80b1d3',
LUNA: '#fdb462',
AVAX: '#b3de69',
BNB: '#FF47A6',
FTT: '#A38560',
BTC: '#bc80bd',
ETH: '#05C793',
ADA: '#3F8EFC',
GMT: '#CBA74A',
}
const HEADERS = [
'time',
'symbol',
@ -185,6 +165,10 @@ const AccountPerformance = () => {
// Normalise chart to start from 0 (except for account value)
if (parseInt(performanceRange) !== 90 && chartToShow !== 'account-value') {
const startValues = Object.assign({}, stats[0])
// Initialize symbol not present at the start to 0
uniqueSymbols
.filter((e) => !(e in startValues))
.map((f) => (startValues[f] = 0))
for (let i = 0; i < stats.length; i++) {
for (const key in stats[i]) {
if (key !== 'time') {
@ -349,7 +333,7 @@ const AccountPerformance = () => {
{mangoAccount ? (
<>
<div
className="h-[540px] w-full rounded-lg rounded-b-none border border-th-bkg-4 p-6 pb-24 sm:pb-16"
className="h-[540px] w-full rounded-lg rounded-b-none border border-th-bkg-3 p-6 pb-24 sm:pb-16"
ref={observe}
>
<div className="flex flex-col pb-4 sm:flex-row sm:items-center sm:justify-between">
@ -411,7 +395,7 @@ const AccountPerformance = () => {
key={`${v}${i}`}
type="monotone"
dataKey={`${v}`}
stroke={`${COLORS[v]}`}
stroke={`${CHART_COLORS(theme)[v]}`}
dot={false}
/>
))}
@ -430,7 +414,7 @@ const AccountPerformance = () => {
/>
) : null}
</div>
<div className="-mt-[1px] rounded-b-lg border border-th-bkg-4 py-3 px-6">
<div className="-mt-[1px] rounded-b-lg border border-th-bkg-3 py-3 px-6">
<div className="mb-2 flex items-center justify-between">
<p className="mb-0 font-bold">{t('assets')}</p>
<Checkbox
@ -450,18 +434,21 @@ const AccountPerformance = () => {
className={`default-transition m-1 flex items-center rounded-full border py-1 px-2 text-xs font-bold ${
selectedSymbols.includes(s)
? ''
: 'border-th-fgd-4 text-th-fgd-4 hover:border-th-fgd-3 hover:text-th-fgd-3 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none'
: 'border-th-fgd-4 text-th-fgd-4 focus:border-th-fgd-3 focus:text-th-fgd-3 focus:outline-none md:hover:border-th-fgd-3 md:hover:text-th-fgd-3'
}`}
onClick={() => toggleOption(s)}
style={
selectedSymbols.includes(s)
? { borderColor: COLORS[s], color: COLORS[s] }
? {
borderColor: CHART_COLORS(theme)[s],
color: CHART_COLORS(theme)[s],
}
: {}
}
key={s}
>
{renderSymbolIcon(s)}
{s}
{s == 'All' ? t(`account-performance:all`) : s}
</button>
))}
</div>

View File

@ -0,0 +1,121 @@
import { PieChart, Pie, Cell, Tooltip } from 'recharts'
import { formatUsdValue, tokenPrecision } from 'utils'
import * as MonoIcons from '../icons'
import { QuestionMarkCircleIcon } from '@heroicons/react/solid'
import { useTheme } from 'next-themes'
export const CHART_COLORS = (theme) => {
return {
All: '#ff7c43',
spacer: theme === 'Light' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)',
ADA: '#335CBE',
AVAX: '#E84142',
BNB: '#F3BA2F',
BTC: '#F7931A',
COPE: '#EEEEEE',
ETH: '#627EEA',
FTT: '#02A6C2',
GMT: '#CBA74A',
LUNA: '#FFD83D',
MNGO: '#FBB31F',
MSOL: '#8562CF',
RAY: '#4CA2DA',
SOL: '#916CE0',
SRM: '#58D4E3',
USDC: '#2775CA',
USDT: '#50AF95',
}
}
const LongShortChart = ({ chartData }: { chartData: any[] }) => {
const { theme } = useTheme()
const CustomToolTip = () => {
const renderIcon = (symbol) => {
const iconName = `${symbol.slice(0, 1)}${symbol
.slice(1, 4)
.toLowerCase()}MonoIcon`
const SymbolIcon = MonoIcons[iconName] || QuestionMarkCircleIcon
return (
<div style={{ color: CHART_COLORS(theme)[symbol] }}>
<SymbolIcon className={`mr-1.5 h-3.5 w-auto`} />
</div>
)
}
const showTooltip = chartData.find((d) => d.symbol !== 'spacer')
return chartData.length && showTooltip ? (
<div className="space-y-1.5 rounded-md bg-th-bkg-2 p-3 pb-2">
{chartData
.filter((d) => d.symbol !== 'spacer')
.sort((a, b) => b.value - a.value)
.map((entry, index) => {
const { amount, asset, symbol, value } = entry
return (
<div
className="flex w-48 items-center justify-between border-b border-th-bkg-4 pb-1 text-xs last:border-b-0 last:pb-0"
key={`item-${index}-${symbol}`}
>
<div className="mb-0.5 flex items-center">
{renderIcon(symbol)}
<p
className="mb-0 text-xs leading-none"
style={{ color: CHART_COLORS(theme)[symbol] }}
>
{asset}
</p>
</div>
<div className="text-right">
<p
className="mb-0 text-xs leading-none"
style={{ color: CHART_COLORS(theme)[symbol] }}
>
{amount.toLocaleString(undefined, {
maximumFractionDigits: tokenPrecision[symbol],
})}
</p>
<p className="mb-0 text-xxs text-th-fgd-4">
{formatUsdValue(value)}
</p>
</div>
</div>
)
})}
</div>
) : null
}
return chartData.length ? (
<PieChart width={40} height={40}>
<Pie
cursor="pointer"
data={chartData}
dataKey="value"
cx="50%"
cy="50%"
outerRadius={20}
innerRadius={10}
minAngle={2}
startAngle={90}
endAngle={450}
>
{chartData
.sort((a, b) => a.symbol.localeCompare(b.symbol))
.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={CHART_COLORS(theme)[entry.symbol]}
stroke="rgba(0,0,0,0.1)"
/>
))}
</Pie>
<Tooltip
content={<CustomToolTip />}
position={{ x: 48, y: 0 }}
wrapperStyle={{ zIndex: 10 }}
/>
</PieChart>
) : null
}
export default LongShortChart

View File

@ -3,7 +3,7 @@ import Button from 'components/Button'
import Input from 'components/Input'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { ExclamationCircleIcon } from '@heroicons/react/outline'
import { ExclamationCircleIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
export const MangoAccountLookup = () => {
@ -34,14 +34,13 @@ export const MangoAccountLookup = () => {
}
return (
<div className="flex flex-col items-center rounded-lg px-4 text-th-fgd-1">
<div className="flex w-full flex-col items-center text-th-fgd-1">
<h2 className="mb-1 text-base">{t('mango-account-lookup-title')}</h2>
<p className="mb-2 text-center">{t('mango-account-lookup-desc')}</p>
<div className="w-[350px] p-1 md:w-[400px]">
<div className="w-full max-w-[360px] pt-2">
<Input
type="text"
error={isInvalid}
placeholder="Address"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
@ -52,7 +51,7 @@ export const MangoAccountLookup = () => {
{t('invalid-address')}
</div>
)}
<div className="pt-3 pb-2">
<div className="pt-4">
<Button onClick={onClickSearch}>{t('view')}</Button>
</div>
</div>

View File

@ -1,382 +0,0 @@
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import cloneDeep from 'lodash/cloneDeep'
import dayjs from 'dayjs'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as ChartTooltip,
} from 'recharts'
import { InformationCircleIcon } from '@heroicons/react/outline'
import useDimensions from 'react-cool-dimensions'
import { useTranslation } from 'next-i18next'
import ButtonGroup from '../ButtonGroup'
import { formatUsdValue } from '../../utils'
import { numberCompacter } from '../SwapTokenInfo'
import Checkbox from '../Checkbox'
import Tooltip from '../Tooltip'
type PerformanceChart = {
hourlyPerformanceStats: any[]
performanceRange: '24hr' | '7d' | '30d' | '3m'
}
const defaultData = [
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
]
const PerformanceChart = ({
hourlyPerformanceStats,
performanceRange,
accountValue,
}) => {
const { theme } = useTheme()
const { t } = useTranslation('common')
const { observe, width, height } = useDimensions()
const [chartData, setChartData] = useState<any[]>([])
const [mouseData, setMouseData] = useState<string | null>(null)
const [chartToShow, setChartToShow] = useState('Value')
const [showSpotPnl, setShowSpotPnl] = useState(true)
const [showPerpPnl, setShowPerpPnl] = useState(true)
useEffect(() => {
if (hourlyPerformanceStats.length > 0) {
if (performanceRange === '3m') {
setChartData(hourlyPerformanceStats.slice().reverse())
}
if (performanceRange === '30d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '7d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '24h') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
} else {
setChartData([])
}
}, [hourlyPerformanceStats, performanceRange])
useEffect(() => {
if (chartData.length > 0) {
for (const stat of chartData) {
stat.spot_pnl = stat.pnl - stat.perp_pnl
}
}
}, [chartData])
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const renderPnlChartTitle = () => {
if (showPerpPnl && showSpotPnl) {
return t('pnl')
}
if (!showSpotPnl) {
return `${t('perp')} ${t('pnl')}`
}
if (!showPerpPnl) {
return `${t('spot')} ${t('pnl')}`
}
}
const formatDateAxis = (date) => {
if (['3m', '30d'].includes(performanceRange)) {
return dayjs(date).format('D MMM')
} else if (performanceRange === '7d') {
return dayjs(date).format('ddd, h:mma')
} else {
return dayjs(date).format('h:mma')
}
}
const pnlChartDataKey = () => {
if (!showPerpPnl && showSpotPnl) {
return 'spot_pnl'
} else if (!showSpotPnl && showPerpPnl) {
return 'perp_pnl'
} else {
return 'pnl'
}
}
const pnlChartColor =
chartToShow === 'PnL' &&
chartData.length > 0 &&
chartData[chartData.length - 1][pnlChartDataKey()] > 0
? theme === 'Mango'
? '#AFD803'
: '#5EBF4D'
: theme === 'Mango'
? '#F84638'
: '#CC2929'
return (
<div className="mt-4 h-64 w-full" ref={observe}>
<div className="flex justify-between pb-9">
<div>
<div className="flex items-center pb-0.5">
<div className="text-sm text-th-fgd-3">
{chartToShow === 'Value'
? t('account-value')
: renderPnlChartTitle()}{' '}
<span className="text-th-fgd-4">
{`(${t('timeframe-desc', {
timeframe: performanceRange,
})})`}
</span>
</div>
<Tooltip content={t('delayed-info')}>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
</Tooltip>
</div>
{mouseData ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{formatUsdValue(
mouseData[
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
]
)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length === 0 ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">--</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length > 0 ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{chartToShow === 'PnL'
? formatUsdValue(
chartData[chartData.length - 1][pnlChartDataKey()]
)
: formatUsdValue(accountValue)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{chartToShow === 'PnL'
? dayjs(chartData[chartData.length - 1]['time']).format(
'ddd MMM D YYYY, h:mma'
)
: dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : (
<>
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
</>
)}
</div>
<div className="flex flex-col items-end">
<div className="w-36">
<ButtonGroup
activeValue={chartToShow}
className="pb-2 pt-2 text-sm"
onChange={(v) => setChartToShow(v)}
values={['Value', 'PnL']}
names={[t('value'), t('pnl')]}
/>
</div>
{chartToShow === 'PnL' ? (
<div className="flex space-x-3 pt-4">
<Checkbox
checked={showSpotPnl}
disabled={!showPerpPnl}
onChange={(e) => setShowSpotPnl(e.target.checked)}
>
{t('include-spot')}
</Checkbox>
<Checkbox
checked={showPerpPnl}
disabled={!showSpotPnl}
onChange={(e) => setShowPerpPnl(e.target.checked)}
>
{t('include-perp')}
</Checkbox>
</div>
) : null}
</div>
</div>
{chartData.length > 0 ? (
<AreaChart
width={width}
height={height}
data={chartData?.length ? chartData : defaultData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<ChartTooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient
id="defaultGradientArea"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
<linearGradient id="greenGradientArea" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient id="redGradientArea" x1="0" y1="1" x2="0" y2="0">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={true}
type="monotone"
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
fill={
chartToShow === 'PnL'
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
? 'url(#greenGradientArea)'
: 'url(#redGradientArea)'
: 'url(#defaultGradientArea)'
}
fillOpacity={0.3}
/>
<YAxis
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
type="number"
domain={['dataMin', 'dataMax']}
axisLine={false}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => numberCompacter.format(v)}
/>
<XAxis
dataKey="time"
axisLine={false}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
</AreaChart>
) : null}
</div>
)
}
export default PerformanceChart

File diff suppressed because one or more lines are too long

View File

@ -39,11 +39,9 @@ export * from './MarketDetails'
export * from './MarketFee'
export * from './MarketMenuItem'
export * from './MarketNavItem'
export * from './MarketSelect'
export * from './MarketsModal'
export * from './MenuItem'
export * from './Modal'
export * from './NavDropMenu'
export * from './NewAccount'
export * from './Notification'
export * from './OpenOrdersTable'
@ -70,7 +68,6 @@ export * from './SwitchMarketDropdown'
export * from './TableElements'
export * from './Tabs'
export * from './Tooltip'
export * from './TopBar'
export * from './TradeHistoryTable'
export * from './TradeNavMenu'
export * from './TradePageGrid'

View File

@ -7,7 +7,7 @@ import {
MenuIcon,
XIcon,
} from '@heroicons/react/solid'
import { BtcMonoIcon, TradeIcon } from '../icons'
import { BtcMonoIcon, TradeIcon, TrophyIcon } from '../icons'
import { useTranslation } from 'next-i18next'
import { IconButton } from '../Button'
import {
@ -18,7 +18,7 @@ import {
LightBulbIcon,
SwitchHorizontalIcon,
UserAddIcon,
} from '@heroicons/react/outline'
} from '@heroicons/react/solid'
const StyledBarItemLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.6rem', lineHeight: 1 }} {...props}>
@ -39,14 +39,14 @@ const BottomBar = () => {
pathname: '/markets',
}}
>
<div
<a
className={`${
asPath === '/markets' ? 'text-th-primary' : 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
>
<BtcMonoIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('markets')}</StyledBarItemLabel>
</div>
</a>
</Link>
<Link
href={{
@ -55,46 +55,46 @@ const BottomBar = () => {
}}
shallow={true}
>
<div
<a
className={`${
asPath === '/' || asPath.startsWith('/?name')
? 'text-th-primary'
: 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
>
<TradeIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('trade')}</StyledBarItemLabel>
</div>
</a>
</Link>
<Link href="/account" shallow={true}>
<div
<a
className={`${
asPath === '/account' ? 'text-th-primary' : 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
>
<CurrencyDollarIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('account')}</StyledBarItemLabel>
</div>
</a>
</Link>
<Link href="/stats" shallow={true}>
<div
<a
className={`${
asPath === '/stats' ? 'text-th-primary' : 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
>
<ChartBarIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('stats')}</StyledBarItemLabel>
</div>
</a>
</Link>
<div
<a
className={`${
showPanel ? 'text-th-primary' : 'text-th-fgd-3'
} default-transition col-span-1 flex cursor-pointer flex-col items-center hover:text-th-primary`}
} default-transition col-span-1 flex cursor-pointer flex-col items-center`}
onClick={() => setShowPanel(!showPanel)}
>
<MenuIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('more')}</StyledBarItemLabel>
</div>
</a>
</div>
<MoreMenuPanel showPanel={showPanel} setShowPanel={setShowPanel} />
</>
@ -141,6 +141,11 @@ const MoreMenuPanel = ({
path="/swap"
icon={<SwitchHorizontalIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('leaderboard')}
path="/leaderboard"
icon={<TrophyIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('referrals')}
path="/referral"

View File

@ -1,7 +1,7 @@
import { useMemo, useState } from 'react'
import { Disclosure } from '@headlessui/react'
import dynamic from 'next/dynamic'
import { XIcon } from '@heroicons/react/outline'
import { XIcon } from '@heroicons/react/solid'
import useMangoStore from '../../stores/useMangoStore'
import { getWeights, PerpMarket } from '@blockworks-foundation/mango-client'
import { CandlesIcon } from '../icons'
@ -84,6 +84,7 @@ const MobileTradePage = () => {
onChange={handleChangeViewIndex}
items={TABS}
tabIndex={viewIndex}
width="w-40 sm:w-full"
/>
<Swipeable index={viewIndex} onChangeIndex={handleChangeViewIndex}>
<div>

View File

@ -1,4 +1,4 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline'
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import useDrag from 'hooks/useDrag'
import { useTranslation } from 'next-i18next'
import React, { useContext, useEffect, useState } from 'react'
@ -44,7 +44,7 @@ function SwipeableTabs({
}
return (
<div onMouseLeave={dragStop} className="thin-scroll relative mb-4">
<div onMouseLeave={dragStop} className="thin-scroll relative mb-6">
<ScrollMenu
LeftArrow={LeftArrow}
RightArrow={RightArrow}
@ -148,10 +148,10 @@ function Tab({
} items-center justify-center font-bold focus:text-th-primary focus:outline-none ${
selected
? 'border-b-2 border-th-primary text-th-primary'
: 'border-b border-th-fgd-4 text-th-fgd-3'
: 'border-b border-th-bkg-4 text-th-fgd-3'
}`}
>
{t(title)}
{t(title.toLowerCase().replace(/\s/g, '-'))}
</div>
)
}

View File

@ -1,46 +1,33 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import Chart from '../Chart'
import Select from '../Select'
import { useTranslation } from 'next-i18next'
import TabButtons from 'components/TabButtons'
export default function StatsAssets({ latestStats, stats }) {
export default function StatsAssets({
latestStats,
stats,
loadHistoricalStats,
}) {
const { t } = useTranslation('common')
const [selectedAsset, setSelectedAsset] = useState<string>('BTC')
const selectedStatsData = stats.filter((stat) => stat.name === selectedAsset)
const selectedStatsData = useMemo(() => {
if (stats.length) {
return stats.filter((stat) => stat.name === selectedAsset)
}
return []
}, [stats, selectedAsset])
return (
<>
<div className="mb-4 flex flex-row-reverse items-center justify-between md:flex-col md:items-stretch">
<Select
value={selectedAsset}
onChange={(a) => setSelectedAsset(a)}
className="w-24 md:hidden"
>
{latestStats.map((stat) => (
<Select.Option key={stat.name} value={stat.name}>
{stat.name}
</Select.Option>
))}
</Select>
<div className="mb-4 hidden rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:flex md:px-4">
{latestStats.map((stat, index) => (
<div
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
index > 0 ? 'ml-4 md:ml-2' : null
} default-transition cursor-pointer rounded-md
${
selectedAsset === stat.name
? `text-th-primary`
: `text-th-fgd-3 hover:text-th-fgd-1`
}
`}
onClick={() => setSelectedAsset(stat.name)}
key={stat.name as string}
>
{stat.name}
</div>
))}
<div className="mb-2">
<TabButtons
activeTab={selectedAsset}
tabs={latestStats.map((s) => ({ label: s.name, key: s.name }))}
onClick={setSelectedAsset}
showSymbolIcon
/>
</div>
<div className="flex items-center text-xl text-th-fgd-1">
<img
@ -68,6 +55,7 @@ export default function StatsAssets({ latestStats, stats }) {
x.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
type="area"
loading={loadHistoricalStats}
/>
</div>
<div
@ -84,6 +72,7 @@ export default function StatsAssets({ latestStats, stats }) {
(x * 100).toLocaleString(undefined, { maximumFractionDigits: 4 })
}
type="bar"
loading={loadHistoricalStats}
/>
</div>
<div
@ -99,6 +88,7 @@ export default function StatsAssets({ latestStats, stats }) {
x.toLocaleString(undefined, { maximumFractionDigits: 2 })
}
type="area"
loading={loadHistoricalStats}
/>
</div>
<div
@ -115,6 +105,7 @@ export default function StatsAssets({ latestStats, stats }) {
(x * 100).toLocaleString(undefined, { maximumFractionDigits: 4 })
}
type="bar"
loading={loadHistoricalStats}
/>
</div>
</div>

View File

@ -5,9 +5,9 @@ import Chart from '../Chart'
import BN from 'bn.js'
import { perpContractPrecision } from '../../utils'
import { useTranslation } from 'next-i18next'
import Select from '../Select'
import { marketsSelector } from '../../stores/selectors'
import dayjs from 'dayjs'
import TabButtons from 'components/TabButtons'
function calculateFundingRate(
oldestLongFunding,
@ -36,7 +36,7 @@ function calculateFundingRate(
return (fundingInQuoteDecimals / basePriceInBaseLots) * 100
}
export default function StatsPerps({ perpStats }) {
export default function StatsPerps({ perpStats, loadPerpStats }) {
const { t } = useTranslation('common')
const [selectedAsset, setSelectedAsset] = useState<string>('BTC-PERP')
const marketConfigs = useMangoStore(
@ -61,7 +61,7 @@ export default function StatsPerps({ perpStats }) {
}, [selectedMarketConfig, perpMarkets])
const perpsData = useMemo(() => {
if (perpStats.length === 0 || !selectedMarket) return []
if (!perpStats.length || !selectedMarket) return []
let selectedStatsData = perpStats.filter(
(stat) => stat.name === selectedAsset
@ -119,35 +119,16 @@ export default function StatsPerps({ perpStats }) {
return (
<>
<div className="mb-4 flex flex-row-reverse items-center justify-between md:flex-col md:items-stretch">
<Select
value={selectedAsset}
onChange={(a) => setSelectedAsset(a)}
className="ml-4 w-36 flex-shrink-0 md:hidden"
>
{marketConfigs?.map((market) => (
<Select.Option key={market.name} value={market.name}>
{market.name}
</Select.Option>
))}
</Select>
<div className="mb-4 hidden rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:flex md:px-4">
{marketConfigs?.map((market, index) => (
<div
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
index > 0 ? 'ml-4 md:ml-2' : null
} default-transition cursor-pointer rounded-md
${
selectedAsset === market.name
? `text-th-primary`
: `text-th-fgd-3 hover:text-th-fgd-1`
}
`}
onClick={() => setSelectedAsset(market.name)}
key={market.name as string}
>
{market.baseSymbol}
</div>
))}
<div className="mb-2">
<TabButtons
activeTab={selectedAsset}
tabs={marketConfigs?.map((m) => ({
label: m.baseSymbol,
key: m.name,
}))}
onClick={setSelectedAsset}
showSymbolIcon
/>
</div>
<div className="flex items-center text-xl text-th-fgd-1">
<img
@ -179,6 +160,7 @@ export default function StatsPerps({ perpStats }) {
}
type="area"
yAxisWidth={70}
loading={loadPerpStats}
/>
</div>
<div
@ -201,6 +183,7 @@ export default function StatsPerps({ perpStats }) {
selectedMarketConfig.baseSymbol
}
type="area"
loading={loadPerpStats}
/>
) : null}
</div>
@ -208,7 +191,7 @@ export default function StatsPerps({ perpStats }) {
<div className="mb-4">
<h2 className="mb-4">{t('liquidity-mining')}</h2>
<div className="grid grid-cols-2 gap-x-3 md:grid-cols-3 lg:grid-cols-6">
<div className="col-span-1 border-y border-th-bkg-4 py-3">
<div className="col-span-1 border-y border-th-bkg-3 py-3">
<p className="mb-0">{t('depth-rewarded')}</p>
<div className="text-lg font-bold">
{maxDepthUi.toLocaleString() + ' '}
@ -219,7 +202,7 @@ export default function StatsPerps({ perpStats }) {
) : null}
</div>
</div>
<div className="col-span-1 border-y border-th-bkg-4 py-3">
<div className="col-span-1 border-y border-th-bkg-3 py-3">
<p className="mb-0">{t('target-period-length')}</p>
<div className="text-lg font-bold">
{(
@ -229,7 +212,7 @@ export default function StatsPerps({ perpStats }) {
{t('minutes')}
</div>
</div>
<div className="col-span-1 border-b border-th-bkg-4 py-3 md:border-y">
<div className="col-span-1 border-b border-th-bkg-3 py-3 md:border-y">
<p className="mb-0">{t('mngo-per-period')}</p>
<div className="text-lg font-bold">
{(
@ -238,7 +221,7 @@ export default function StatsPerps({ perpStats }) {
).toFixed(2)}
</div>
</div>
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
<p className="mb-0">{t('mngo-left-period')}</p>
<div className="text-lg font-bold">
{(
@ -248,7 +231,7 @@ export default function StatsPerps({ perpStats }) {
</div>
</div>
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
<p className="mb-0">{t('est-period-end')}</p>
<div className="text-lg font-bold">
{dayjs(est * 1000).format('DD MMM YYYY')}
@ -257,7 +240,7 @@ export default function StatsPerps({ perpStats }) {
{dayjs(est * 1000).format('h:mma')}
</div>
</div>
<div className="col-span-1 border-b border-th-bkg-4 py-3 lg:border-y">
<div className="col-span-1 border-b border-th-bkg-3 py-3 lg:border-y">
<p className="mb-0">{t('period-progress')}</p>
<div className="text-lg font-bold">
{(progress * 100).toFixed(2)}%

View File

@ -32,30 +32,38 @@ const getAverageStats = (
symbol: string,
type: string
): string => {
if (stats?.length) {
if (stats.length > 0) {
const priorDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
const selectedStatsData = stats.filter((s) => s.name === symbol)
const timeFilteredStats = selectedStatsData.filter(
(d) => new Date(d.time).getTime() >= priorDate.getTime()
)
const oldestStat = timeFilteredStats[0]
const latestStat = timeFilteredStats[timeFilteredStats.length - 1]
const avg =
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
let avg
if (latestStat && oldestStat && type in latestStat && type in oldestStat) {
avg =
Math.pow(latestStat[type] / oldestStat[type], 365 / daysAgo) * 100 - 100
}
priorDate.setHours(priorDate.getHours() + 1)
if (new Date(oldestStat.hourly).getDate() > priorDate.getDate()) {
if (new Date(oldestStat?.hourly).getDate() > priorDate.getDate()) {
return '-'
} else {
}
if (avg) {
return `${avg.toFixed(4)}%`
}
}
return '-'
}
export default function StatsTotals({ latestStats, stats }) {
export default function StatsTotals({
latestStats,
stats,
loadHistoricalStats,
loadLatestStats,
}) {
const { t } = useTranslation('common')
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
@ -64,7 +72,7 @@ export default function StatsTotals({ latestStats, stats }) {
const [depositValues, borrowValues]: [Values[], Values[]] = useMemo(() => {
const depositValues: Values[] = []
const borrowValues: Values[] = []
for (let i = 0; i < stats.length; i++) {
for (let i = 0; i < stats?.length; i++) {
const time = stats[i].hourly
const name = stats[i].name
const depositValue =
@ -149,6 +157,7 @@ export default function StatsTotals({ latestStats, stats }) {
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
}
type="area"
loading={loadHistoricalStats}
/>
</div>
<div
@ -165,6 +174,7 @@ export default function StatsTotals({ latestStats, stats }) {
'$' + x.toLocaleString(undefined, { maximumFractionDigits: 0 })
}
type="area"
loading={loadHistoricalStats}
/>
</div>
</div>
@ -242,17 +252,17 @@ export default function StatsTotals({ latestStats, stats }) {
))}
</tbody>
</Table>
) : (
) : loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
)}
) : null}
</div>
<div className="pb-8">
<h2 className="mb-4">{t('average-deposit')}</h2>
{stats.length > 1 ? (
{stats.length && latestStats.length ? (
<Table>
<thead>
<TrHead>
@ -290,70 +300,158 @@ export default function StatsTotals({ latestStats, stats }) {
))}
</tbody>
</Table>
) : (
) : loadHistoricalStats || loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
)}
) : null}
</div>
<h2 className="mb-4">{t('average-borrow')}</h2>
{stats.length > 1 ? (
<Table>
<thead>
<TrHead>
<Th>{t('asset')}</Th>
<Th>24h</Th>
<Th>7d</Th>
<Th>30d</Th>
</TrHead>
</thead>
<tbody>
{latestStats.map((stat) => (
<TrBody key={stat.name}>
<Td>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
</Td>
<Td>
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
</Td>
<Td>
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
</Td>
<Td>
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
</Td>
</TrBody>
))}
</tbody>
</Table>
) : (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
)}
<>
<h2 className="mb-4">{t('average-borrow')}</h2>
{stats.length && latestStats.length ? (
<Table>
<thead>
<TrHead>
<Th>{t('asset')}</Th>
<Th>24h</Th>
<Th>7d</Th>
<Th>30d</Th>
</TrHead>
</thead>
<tbody>
{latestStats.map((stat) => (
<TrBody key={stat.name}>
<Td>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
</Td>
<Td>
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
</Td>
<Td>
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
</Td>
<Td>
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
</Td>
</TrBody>
))}
</tbody>
</Table>
) : loadHistoricalStats || loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
) : null}
</>
</>
) : (
<>
<div className="mb-8 border-b border-th-bkg-4">
<div className="mb-8 border-b border-th-bkg-3">
<h2 className="mb-4">{t('current-stats')}</h2>
{latestStats.map((stat) => (
<ExpandableRow
buttonTemplate={
<div className="grid w-full grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 sm:col-span-6">
{latestStats.length ? (
latestStats.map((stat) => (
<ExpandableRow
buttonTemplate={
<div className="grid w-full grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 sm:col-span-6">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
</div>
<div className="col-span-6 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('total-deposits')}
</div>
{formatNumberString(stat.totalDeposits, 0)}
</div>
<div className="col-span-6 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('total-borrows')}
</div>
{formatNumberString(stat.totalBorrows, 0)}
</div>
</div>
}
key={stat.name}
panelTemplate={
<div className="grid grid-flow-row grid-cols-2 gap-4">
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('deposit-rate')}
</div>
<span className="text-th-green">
{formatNumberString(
stat.depositInterest.toNumber(),
2
)}
%
</span>
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('borrow-rate')}
</div>
<span className="text-th-red">
{formatNumberString(
stat.borrowInterest.toNumber(),
2
)}
%
</span>
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('utilization')}
</div>
{formatNumberString(
stat.utilization
.mul(I80F48.fromNumber(100))
.toNumber(),
2
)}
%
</div>
</div>
}
/>
))
) : loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
) : null}
</div>
{stats.length && latestStats.length ? (
<div className="mb-8 border-b border-th-bkg-4">
<h2 className="mb-4">{t('average-deposit')}</h2>
{latestStats.map((stat) => (
<Row key={stat.name}>
<div className="grid grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 sm:col-span-3">
<div className="flex items-center">
<img
alt=""
@ -367,129 +465,72 @@ export default function StatsTotals({ latestStats, stats }) {
{stat.name}
</div>
</div>
<div className="col-span-6 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('total-deposits')}
</div>
{formatNumberString(stat.totalDeposits, 0)}
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
{getAverageStats(stats, 1, stat.name, 'depositIndex')}
</div>
<div className="col-span-6 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('total-borrows')}
</div>
{formatNumberString(stat.totalBorrows, 0)}
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
{getAverageStats(stats, 7, stat.name, 'depositIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
{getAverageStats(stats, 30, stat.name, 'depositIndex')}
</div>
</div>
}
key={stat.name}
panelTemplate={
<div className="grid grid-flow-row grid-cols-2 gap-4">
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('deposit-rate')}
</Row>
))}
</div>
) : loadHistoricalStats || loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
) : null}
{stats.length && latestStats.length ? (
<div className="mb-4 border-b border-th-bkg-4">
<h2 className="mb-4">{t('average-borrow')}</h2>
{latestStats.map((stat) => (
<Row key={stat.name}>
<div className="grid grid-cols-12 grid-rows-2 gap-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 flex items-center sm:col-span-3">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
<span className="text-th-green">
{formatNumberString(stat.depositInterest.toNumber(), 2)}
%
</span>
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('borrow-rate')}
</div>
<span className="text-th-red">
{formatNumberString(stat.borrowInterest.toNumber(), 2)}%
</span>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
</div>
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('utilization')}
</div>
{formatNumberString(
stat.utilization.mul(I80F48.fromNumber(100)).toNumber(),
2
)}
%
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
</div>
</div>
}
/>
))}
</div>
<div className="mb-8 border-b border-th-bkg-4">
<h2 className="mb-4">{t('average-deposit')}</h2>
{stats.length > 1
? latestStats.map((stat) => (
<Row key={stat.name}>
<div className="grid grid-cols-12 grid-rows-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 sm:col-span-3">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
{getAverageStats(stats, 1, stat.name, 'depositIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
{getAverageStats(stats, 7, stat.name, 'depositIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
{getAverageStats(stats, 30, stat.name, 'depositIndex')}
</div>
</div>
</Row>
))
: null}
</div>
<div className="mb-4 border-b border-th-bkg-4">
<h2 className="mb-4">{t('average-borrow')}</h2>
{stats.length > 1
? latestStats.map((stat) => (
<Row key={stat.name}>
<div className="grid grid-cols-12 grid-rows-2 gap-2 text-left sm:grid-rows-1 sm:text-right">
<div className="text-fgd-1 col-span-12 flex items-center sm:col-span-3">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${stat.name
.split(/-|\//)[0]
.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
{stat.name}
</div>
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">24h</div>
{getAverageStats(stats, 1, stat.name, 'borrowIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">7d</div>
{getAverageStats(stats, 7, stat.name, 'borrowIndex')}
</div>
<div className="col-span-4 sm:col-span-3">
<div className="pb-0.5 text-xs text-th-fgd-3">30d</div>
{getAverageStats(stats, 30, stat.name, 'borrowIndex')}
</div>
</div>
</Row>
))
: null}
</div>
</Row>
))}
</div>
) : loadHistoricalStats || loadLatestStats ? (
<>
<div className="h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-8 w-full animate-pulse rounded bg-th-bkg-3" />
</>
) : null}
</>
)}
</>

View File

@ -11,12 +11,13 @@ import {
PerpOrderType,
ZERO_I80F48,
} from '@blockworks-foundation/mango-client'
import {
ExclamationIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
import { ExclamationIcon, InformationCircleIcon } from '@heroicons/react/solid'
import { notify } from '../../utils/notifications'
import { calculateTradePrice, getDecimalCount } from '../../utils'
import {
calculateTradePrice,
getDecimalCount,
tokenPrecision,
} from '../../utils'
import { floorToDecimal } from '../../utils/index'
import useMangoStore, { Orderbook } from '../../stores/useMangoStore'
import Button, { LinkButton } from '../Button'
@ -41,6 +42,7 @@ import useLocalStorageState, {
import InlineNotification from '../InlineNotification'
import { DEFAULT_SPOT_MARGIN_KEY } from '../SettingsModal'
import { useWallet } from '@solana/wallet-adapter-react'
import usePrevious from 'hooks/usePrevious'
const MAX_SLIPPAGE_KEY = 'maxSlippage'
@ -116,6 +118,7 @@ export default function AdvancedTradeForm({
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
const [isCloseOnly, setIsCloseOnly] = useState(false)
const [updateBaseSize, setUpdateBaseSize] = useState(false)
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
const orderbook = orderBookRef.current
@ -342,6 +345,43 @@ export default function AdvancedTradeForm({
),
[]
)
const previousMarkPrice: number = usePrevious(markPrice)
useEffect(() => {
if (tradeType === 'Limit' && price) {
if (updateBaseSize) {
if (quoteSize) {
setBaseSize(
(Number(quoteSize) / price).toFixed(
tokenPrecision[marketConfig.baseSymbol]
)
)
}
} else {
if (baseSize) {
setQuoteSize((Number(baseSize) * price).toFixed(2))
}
}
}
}, [tradeType, price])
useEffect(() => {
if (markPrice !== previousMarkPrice && tradeType === 'Market') {
if (updateBaseSize) {
if (quoteSize) {
setBaseSize(
(Number(quoteSize) / markPrice).toFixed(
tokenPrecision[marketConfig.baseSymbol]
)
)
}
} else {
if (baseSize) {
setQuoteSize((Number(baseSize) * markPrice).toFixed(2))
}
}
}
}, [markPrice, previousMarkPrice, tradeType, updateBaseSize])
let minOrderSize = '0'
if (market instanceof Market && market.minOrderSize) {
@ -392,6 +432,9 @@ export default function AdvancedTradeForm({
const rawQuoteSize = baseSize * usePrice
setQuoteSize(rawQuoteSize.toFixed(6))
setPositionSizePercent('')
if (updateBaseSize) {
setUpdateBaseSize(false)
}
}
const onSetQuoteSize = (quoteSize: number | '') => {
@ -410,6 +453,9 @@ export default function AdvancedTradeForm({
const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount)
setBaseSize(baseSize)
setPositionSizePercent('')
if (!updateBaseSize) {
setUpdateBaseSize(true)
}
}
const onTradeTypeChange = (tradeType) => {
@ -1068,7 +1114,7 @@ export default function AdvancedTradeForm({
<button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`flex-grow rounded-full px-6 py-2 font-bold text-white hover:brightness-[1.1] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100 ${
className={`flex-grow rounded-full px-6 py-2 font-bold text-white focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 ${
side === 'buy' ? 'bg-th-green-dark' : 'bg-th-red'
}`}
>
@ -1174,7 +1220,7 @@ export default function AdvancedTradeForm({
{t('max-slippage')}
<Tooltip content={t('tooltip-slippage')}>
<div className="outline-none focus:outline-none">
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3" />
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4" />
</div>
</Tooltip>
</div>

View File

@ -18,7 +18,7 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
const { t } = useTranslation('common')
const market = useMangoStore((s) => s.selectedMarket.current)
return (
<div className={`relative mb-3 md:-mt-2.5 md:border-b md:border-th-fgd-4`}>
<div className={`relative mb-3 md:-mt-2.5 md:border-b md:border-th-bkg-3`}>
<div
className={`absolute hidden md:block ${
side === 'buy'
@ -30,11 +30,11 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
<button
onClick={() => onChange('buy')}
className={`default-transition relative flex w-1/2 cursor-pointer
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold hover:opacity-100 md:text-base
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold md:text-base md:hover:opacity-100
${
side === 'buy'
? `border border-th-green text-th-green md:border-0`
: `border border-th-fgd-4 text-th-fgd-4 hover:border-th-green hover:text-th-green md:border-0`
: `border border-th-fgd-4 text-th-fgd-4 md:border-0 md:hover:border-th-green md:hover:text-th-green`
}
`}
>
@ -43,11 +43,11 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
<button
onClick={() => onChange('sell')}
className={`default-transition relative flex w-1/2 cursor-pointer
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold hover:opacity-100 md:text-base
items-center justify-center whitespace-nowrap py-1 text-sm font-semibold md:text-base md:hover:opacity-100
${
side === 'sell'
? `border border-th-red text-th-red md:border-0`
: `border border-th-fgd-4 text-th-fgd-4 hover:border-th-red hover:text-th-red md:border-0`
: `border border-th-fgd-4 text-th-fgd-4 md:border-0 md:hover:border-th-red md:hover:text-th-red`
}
`}
>

View File

@ -1,5 +1,5 @@
import { useMemo, useState } from 'react'
import { SwitchHorizontalIcon } from '@heroicons/react/outline'
import { SwitchHorizontalIcon } from '@heroicons/react/solid'
import { getWeights } from '@blockworks-foundation/mango-client'
import useMangoStore from '../../stores/useMangoStore'
import AdvancedTradeForm from './AdvancedTradeForm'

View File

@ -1,184 +0,0 @@
import { Balances } from '../@types/types'
import {
getTokenBySymbol,
nativeI80F48ToUi,
nativeToUi,
QUOTE_INDEX,
} from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import { i80f48ToPercent } from '../utils/index'
import sumBy from 'lodash/sumBy'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useMangoAccount from './useMangoAccount'
export function useBalances(): Balances[] {
const balances: any[] = []
const { mangoAccount } = useMangoAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
for (const {
marketIndex,
baseSymbol,
name,
} of mangoGroupConfig.spotMarkets) {
if (!mangoAccount || !mangoGroup || !mangoCache) {
return []
}
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex]
const quoteCurrencyIndex = QUOTE_INDEX
let nativeBaseFree = 0
let nativeQuoteFree = 0
let nativeBaseLocked = 0
let nativeQuoteLocked = 0
if (openOrders) {
nativeBaseFree = openOrders.baseTokenFree.toNumber()
nativeQuoteFree = openOrders.quoteTokenFree
.add(openOrders['referrerRebatesAccrued'])
.toNumber()
nativeBaseLocked = openOrders.baseTokenTotal
.sub(openOrders.baseTokenFree)
.toNumber()
nativeQuoteLocked = openOrders.quoteTokenTotal
.sub(openOrders.quoteTokenFree)
.toNumber()
}
const tokenIndex = marketIndex
const net = (nativeBaseLocked, tokenIndex) => {
const amount = mangoAccount
.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
.add(
nativeI80F48ToUi(
I80F48.fromNumber(nativeBaseLocked),
mangoGroup.tokens[tokenIndex].decimals
).sub(
mangoAccount.getUiBorrow(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
)
)
return amount
}
const value = (nativeBaseLocked, tokenIndex) => {
const amount = mangoGroup
.getPrice(tokenIndex, mangoCache)
.mul(net(nativeBaseLocked, tokenIndex))
return amount
}
const marketPair = [
{
market: null,
key: `${baseSymbol}${name}`,
symbol: baseSymbol,
deposits: mangoAccount.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
),
borrows: mangoAccount.getUiBorrow(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
),
orders: nativeToUi(
nativeBaseLocked,
mangoGroup.tokens[tokenIndex].decimals
),
unsettled: nativeToUi(
nativeBaseFree,
mangoGroup.tokens[tokenIndex].decimals
),
net: net(nativeBaseLocked, tokenIndex),
value: value(nativeBaseLocked, tokenIndex),
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
decimals: mangoGroup.tokens[tokenIndex].decimals,
},
{
market: null,
key: `${name}`,
symbol: mangoGroupConfig.quoteSymbol,
deposits: mangoAccount.getUiDeposit(
mangoCache.rootBankCache[quoteCurrencyIndex],
mangoGroup,
quoteCurrencyIndex
),
borrows: mangoAccount.getUiBorrow(
mangoCache.rootBankCache[quoteCurrencyIndex],
mangoGroup,
quoteCurrencyIndex
),
orders: nativeToUi(
nativeQuoteLocked,
mangoGroup.tokens[quoteCurrencyIndex].decimals
),
unsettled: nativeToUi(
nativeQuoteFree,
mangoGroup.tokens[quoteCurrencyIndex].decimals
),
net: net(nativeQuoteLocked, quoteCurrencyIndex),
value: value(nativeQuoteLocked, quoteCurrencyIndex),
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
decimals: mangoGroup.tokens[quoteCurrencyIndex].decimals,
},
]
balances.push(marketPair)
}
const baseBalances = balances.map((b) => b[0])
const quoteBalances = balances.map((b) => b[1])
const quoteMeta = quoteBalances[0]
const quoteInOrders = sumBy(quoteBalances, 'orders')
const unsettled = sumBy(quoteBalances, 'unsettled')
if (!mangoGroup || !mangoCache) {
return []
}
const net: I80F48 = quoteMeta.deposits
.add(I80F48.fromNumber(unsettled))
.sub(quoteMeta.borrows)
.add(I80F48.fromNumber(quoteInOrders))
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol)
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache))
const depositRate = i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex))
const borrowRate = i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex))
return [
{
market: null,
key: `${quoteMeta.symbol}${quoteMeta.symbol}`,
symbol: quoteMeta.symbol,
deposits: quoteMeta.deposits,
borrows: quoteMeta.borrows,
orders: quoteInOrders,
unsettled,
net,
value,
depositRate,
borrowRate,
decimals: mangoGroup.tokens[QUOTE_INDEX].decimals,
},
].concat(baseBalances)
}

View File

@ -29,6 +29,9 @@ const useMangoStats = () => {
},
])
const [latestStats, setLatestStats] = useState<any[]>([])
const [loadLatestStats, setLoadLatestStats] = useState<boolean>(false)
const [loadHistoricalStats, setLoadHistoricalStats] = useState<boolean>(false)
const [loadPerpStats, setLoadPerpStats] = useState<boolean>(false)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
const connection = useMangoStore((s) => s.connection.current)
@ -36,22 +39,34 @@ const useMangoStats = () => {
useEffect(() => {
const fetchHistoricalStats = async () => {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/spot_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setStats(stats)
setLoadHistoricalStats(true)
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/spot_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setStats(stats)
setLoadHistoricalStats(false)
} catch {
setLoadHistoricalStats(false)
}
}
fetchHistoricalStats()
}, [mangoGroupName])
useEffect(() => {
const fetchHistoricalPerpStats = async () => {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/perp_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setPerpStats(stats)
setLoadPerpStats(true)
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/perp_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setPerpStats(stats)
setLoadPerpStats(false)
} catch {
setLoadPerpStats(false)
}
}
fetchHistoricalPerpStats()
}, [mangoGroupName])
@ -59,6 +74,7 @@ const useMangoStats = () => {
useEffect(() => {
const getLatestStats = async () => {
if (mangoGroup) {
setLoadLatestStats(true)
const rootBanks = await mangoGroup.loadRootBanks(connection)
if (!config) return
const latestStats = config.tokens.map((token) => {
@ -95,13 +111,21 @@ const useMangoStats = () => {
}
})
setLatestStats(latestStats)
setLoadLatestStats(false)
}
}
getLatestStats()
}, [mangoGroup])
return { latestStats, stats, perpStats }
return {
latestStats,
loadLatestStats,
stats,
perpStats,
loadHistoricalStats,
loadPerpStats,
}
}
export default useMangoStats

View File

@ -1,4 +1,4 @@
import useMangoStore from '../stores/useMangoStore'
import useMangoStore, { PerpPosition } from '../stores/useMangoStore'
import BN from 'bn.js'
import {
MangoAccount,
@ -24,7 +24,7 @@ export const collectPerpPosition = (
marketConfig: PerpMarketConfig,
perpMarket: PerpMarket,
tradeHistory: any
) => {
): PerpPosition | undefined => {
if (
!mangoAccount ||
!mangoGroup ||
@ -32,7 +32,7 @@ export const collectPerpPosition = (
!perpMarket ||
!tradeHistory
)
return {}
return
const perpMarketInfo = mangoGroup.perpMarkets[marketConfig.marketIndex]
const perpAccount = mangoAccount.perpAccounts[marketConfig.marketIndex]
@ -104,7 +104,7 @@ const usePerpPositions = () => {
mangoCache &&
Object.keys(allMarkets).length
) {
const perpAccounts = mangoAccount
const perpPositions = mangoAccount
? groupConfig.perpMarkets.map((m) =>
collectPerpPosition(
mangoAccount,
@ -117,13 +117,14 @@ const usePerpPositions = () => {
)
: []
const openPerpPositions = perpAccounts.filter(
({ perpAccount }) =>
perpAccount?.basePosition && !perpAccount.basePosition.eq(new BN(0))
)
const openPerpPositions = perpPositions.filter(
(p) =>
p?.perpAccount?.basePosition &&
!p.perpAccount.basePosition.eq(new BN(0))
) as PerpPosition[]
setMangoStore((state) => {
state.selectedMangoAccount.perpAccounts = perpAccounts
state.selectedMangoAccount.perpPositions = perpPositions
state.selectedMangoAccount.openPerpPositions = openPerpPositions
if (
openPerpPositions.length !==
@ -132,10 +133,12 @@ const usePerpPositions = () => {
state.selectedMangoAccount.totalOpenPerpPositions =
openPerpPositions.length
}
state.selectedMangoAccount.unsettledPerpPositions = perpAccounts.filter(
({ perpAccount, unsettledPnl }) =>
perpAccount?.basePosition?.eq(new BN(0)) && unsettledPnl != 0
)
state.selectedMangoAccount.unsettledPerpPositions =
perpPositions.filter(
(p) =>
p?.perpAccount?.basePosition?.eq(new BN(0)) &&
p?.unsettledPnl != 0
) as PerpPosition[]
})
}
}, [mangoAccount, mangoCache, tradeHistory])

188
hooks/useSpotBalances.tsx Normal file
View File

@ -0,0 +1,188 @@
import {
getTokenBySymbol,
nativeI80F48ToUi,
nativeToUi,
QUOTE_INDEX,
} from '@blockworks-foundation/mango-client'
import useMangoStore, { SpotBalance } from '../stores/useMangoStore'
import { i80f48ToPercent } from '../utils/index'
import sumBy from 'lodash/sumBy'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useMangoAccount from './useMangoAccount'
import { useEffect } from 'react'
const useSpotBalances = () => {
const { mangoAccount } = useMangoAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const setMangoStore = useMangoStore((s) => s.set)
useEffect(() => {
if (!mangoAccount || !mangoGroup || !mangoCache) {
return
}
const balances: SpotBalance[][] = []
for (const {
marketIndex,
baseSymbol,
name,
} of mangoGroupConfig.spotMarkets) {
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex]
const quoteCurrencyIndex = QUOTE_INDEX
let nativeBaseFree = 0
let nativeQuoteFree = 0
let nativeBaseLocked = 0
let nativeQuoteLocked = 0
if (openOrders) {
nativeBaseFree = openOrders.baseTokenFree.toNumber()
nativeQuoteFree = openOrders.quoteTokenFree
.add(openOrders['referrerRebatesAccrued'])
.toNumber()
nativeBaseLocked = openOrders.baseTokenTotal
.sub(openOrders.baseTokenFree)
.toNumber()
nativeQuoteLocked = openOrders.quoteTokenTotal
.sub(openOrders.quoteTokenFree)
.toNumber()
}
const tokenIndex = marketIndex
const net = (nativeBaseLocked, tokenIndex) => {
const amount = mangoAccount
.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
.add(
nativeI80F48ToUi(
I80F48.fromNumber(nativeBaseLocked),
mangoGroup.tokens[tokenIndex].decimals
).sub(
mangoAccount.getUiBorrow(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
)
)
)
return amount
}
const value = (nativeBaseLocked, tokenIndex) => {
const amount = mangoGroup
.getPrice(tokenIndex, mangoCache)
.mul(net(nativeBaseLocked, tokenIndex))
return amount
}
const marketPair: SpotBalance[] = [
{
market: null,
key: `${baseSymbol}${name}`,
symbol: baseSymbol,
deposits: mangoAccount.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
),
borrows: mangoAccount.getUiBorrow(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
tokenIndex
),
orders: nativeToUi(
nativeBaseLocked,
mangoGroup.tokens[tokenIndex].decimals
),
unsettled: nativeToUi(
nativeBaseFree,
mangoGroup.tokens[tokenIndex].decimals
),
net: net(nativeBaseLocked, tokenIndex),
value: value(nativeBaseLocked, tokenIndex),
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
decimals: mangoGroup.tokens[tokenIndex].decimals,
},
{
market: null,
key: `${name}`,
symbol: mangoGroupConfig.quoteSymbol,
deposits: mangoAccount.getUiDeposit(
mangoCache.rootBankCache[quoteCurrencyIndex],
mangoGroup,
quoteCurrencyIndex
),
borrows: mangoAccount.getUiBorrow(
mangoCache.rootBankCache[quoteCurrencyIndex],
mangoGroup,
quoteCurrencyIndex
),
orders: nativeToUi(
nativeQuoteLocked,
mangoGroup.tokens[quoteCurrencyIndex].decimals
),
unsettled: nativeToUi(
nativeQuoteFree,
mangoGroup.tokens[quoteCurrencyIndex].decimals
),
net: net(nativeQuoteLocked, quoteCurrencyIndex),
value: value(nativeQuoteLocked, quoteCurrencyIndex),
depositRate: i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex)),
borrowRate: i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex)),
decimals: mangoGroup.tokens[quoteCurrencyIndex].decimals,
},
]
balances.push(marketPair)
}
const baseBalances = balances.map((b) => b[0])
const quoteBalances = balances.map((b) => b[1])
const quoteMeta = quoteBalances[0]
const quoteInOrders = sumBy(quoteBalances, 'orders')
const unsettled = sumBy(quoteBalances, 'unsettled')
const net: I80F48 = quoteMeta.deposits
.add(I80F48.fromNumber(unsettled))
.sub(quoteMeta.borrows)
.add(I80F48.fromNumber(quoteInOrders))
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol)
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache))
const depositRate = i80f48ToPercent(mangoGroup.getDepositRate(tokenIndex))
const borrowRate = i80f48ToPercent(mangoGroup.getBorrowRate(tokenIndex))
setMangoStore((s) => {
s.selectedMangoAccount.spotBalances = baseBalances.concat([
{
market: null,
key: `${quoteMeta.symbol}${quoteMeta.symbol}`,
symbol: quoteMeta.symbol,
deposits: quoteMeta.deposits,
borrows: quoteMeta.borrows,
orders: quoteInOrders,
unsettled,
net,
value,
depositRate,
borrowRate,
decimals: mangoGroup.tokens[QUOTE_INDEX].decimals,
},
])
})
}, [mangoGroup, mangoCache, mangoAccount])
}
export default useSpotBalances

View File

@ -3,6 +3,7 @@ import useMangoStore from '../stores/useMangoStore'
import {
getMultipleAccounts,
nativeToUi,
zeroKey,
} from '@blockworks-foundation/mango-client'
import {
MSRM_DECIMALS,
@ -66,7 +67,9 @@ const useSrmAccount = () => {
)
setSrmAccount(srmAccountInfo.accountInfo)
setMsrmAccount(msrmAccountInfo.accountInfo)
if (!msrmPk.equals(zeroKey)) {
setMsrmAccount(msrmAccountInfo.accountInfo)
}
}
fetchAccounts()

View File

@ -22,8 +22,7 @@ const reverseSide = (side) => (side === 'buy' ? 'sell' : 'buy')
function getMarketName(event) {
const mangoGroupConfig = useMangoStore.getState().selectedMangoGroup.config
let marketName
let marketName = ''
if (!event.marketName && event.address) {
const marketInfo = getMarketByPublicKey(mangoGroupConfig, event.address)
if (marketInfo) {

View File

@ -4,8 +4,9 @@
"license": "MIT",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"devnet": "NEXT_PUBLIC_CLUSTER=devnet NEXT_PUBLIC_ENDPOINT=https://mango.devnet.rpcpool.com/ NEXT_PUBLIC_GROUP=devnet.2 next dev",
"dev": "NODE_OPTIONS=\"--max_old_space_size=4096\" next dev",
"devnet": "NEXT_PUBLIC_CLUSTER=devnet NEXT_PUBLIC_ENDPOINT=https://mango.devnet.rpcpool.com/ NEXT_PUBLIC_GROUP=devnet.4 next dev",
"testnet": "NEXT_PUBLIC_CLUSTER=testnet NEXT_PUBLIC_ENDPOINT=https://api.testnet.solana.com NEXT_PUBLIC_GROUP=testnet.0 next dev",
"build": "next build",
"start": "next start",
"prepare": "husky install",
@ -17,7 +18,7 @@
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
"@blockworks-foundation/mango-client": "^3.6.8",
"@blockworks-foundation/mango-client": "^3.6.9",
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
"@heroicons/react": "^1.0.0",
"@jup-ag/react-hook": "^1.0.0-beta.22",
@ -66,7 +67,7 @@
"borsh": "^0.7.0"
},
"devDependencies": {
"@next/bundle-analyzer": "^12.1.0",
"@next/bundle-analyzer": "^12.2.0",
"@svgr/webpack": "^6.1.2",
"@testing-library/react": "^11.2.5",
"@types/node": "^14.14.25",
@ -91,8 +92,13 @@
"typescript": "^4.6.3"
},
"resolutions": {
"big.js": "6.1.1",
"bn.js": "5.1.3",
"@solana/buffer-layout": "4.0.0",
"@solana/web3.js": "1.47.3",
"@project-serum/serum": "0.13.65",
"@project-serum/anchor": "0.23.0",
"@project-serum/sol-wallet-adapter": "0.2.6",
"@types/bn.js": "5.1.0"
},
"nextBundleAnalysis": {

29
pages/404.tsx Normal file
View File

@ -0,0 +1,29 @@
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
import { RektIcon } from '../components/icons'
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['common', 'delegate'])),
// Will be passed to the page component as props
},
}
}
export default function Custom404() {
const { t } = useTranslation('common')
return (
<div
className="mx-auto flex max-w-xl flex-col items-center justify-center text-center"
style={{ height: 'calc(100vh - 80px)' }}
>
<RektIcon className="mb-6 h-14 w-auto -rotate-6 transform text-th-red" />
<span className="text-lg font-bold text-th-fgd-4">404</span>
<h1 className="mt-1 text-3xl text-th-fgd-1 sm:text-4xl">
{t('404-heading')}
</h1>
<p className="mt-2 text-lg text-th-fgd-4">{t('404-description')}</p>
</div>
)
}

View File

@ -13,10 +13,8 @@ import useOraclePrice from '../hooks/useOraclePrice'
import { getDecimalCount } from '../utils'
import { useRouter } from 'next/router'
import { ViewportProvider } from '../hooks/useViewport'
import BottomBar from '../components/mobile/BottomBar'
import { appWithTranslation } from 'next-i18next'
import ErrorBoundary from '../components/ErrorBoundary'
import GlobalNotification from '../components/GlobalNotification'
import { useOpenOrders } from '../hooks/useOpenOrders'
import usePerpPositions from '../hooks/usePerpPositions'
import { useEffect, useMemo } from 'react'
@ -41,6 +39,8 @@ import {
GlowWalletAdapter,
} from '@solana/wallet-adapter-wallets'
import { HuobiWalletAdapter } from '@solana/wallet-adapter-huobi'
import useSpotBalances from 'hooks/useSpotBalances'
import Layout from 'components/Layout'
const SENTRY_URL = process.env.NEXT_PUBLIC_SENTRY_URL
if (SENTRY_URL) {
@ -60,6 +60,11 @@ const OpenOrdersStoreUpdater = () => {
return null
}
const SpotBalancesStoreUpdater = () => {
useSpotBalances()
return null
}
const PerpPositionsStoreUpdater = () => {
usePerpPositions()
return null
@ -188,33 +193,31 @@ function App({ Component, pageProps }) {
<meta name="google" content="notranslate" />
<link rel="manifest" href="/manifest.json"></link>
</Head>
<ErrorBoundary>
<WalletProvider wallets={wallets}>
<PageTitle />
<MangoStoreUpdater />
<OpenOrdersStoreUpdater />
<PerpPositionsStoreUpdater />
<TradeHistoryStoreUpdater />
<FetchReferrer />
<ThemeProvider defaultTheme="Mango">
<ThemeProvider defaultTheme="Mango">
<ErrorBoundary>
<WalletProvider wallets={wallets}>
<PageTitle />
<MangoStoreUpdater />
<OpenOrdersStoreUpdater />
<SpotBalancesStoreUpdater />
<PerpPositionsStoreUpdater />
<TradeHistoryStoreUpdater />
<FetchReferrer />
<WalletListener />
<ViewportProvider>
<div className="min-h-screen bg-th-bkg-1">
<ErrorBoundary>
<GlobalNotification />
<Component {...pageProps} />
<Layout>
<Component {...pageProps} />
</Layout>
</ErrorBoundary>
</div>
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<BottomBar />
</div>
<Notifications />
</ViewportProvider>
</ThemeProvider>
</WalletProvider>
</ErrorBoundary>
</WalletProvider>
</ErrorBoundary>
</ThemeProvider>
</>
)
}

View File

@ -7,30 +7,25 @@ import React, {
} from 'react'
import {
BellIcon,
ChevronDownIcon,
CurrencyDollarIcon,
DuplicateIcon,
ExclamationCircleIcon,
GiftIcon,
LinkIcon,
PencilIcon,
SwitchHorizontalIcon,
TrashIcon,
UsersIcon,
} from '@heroicons/react/outline'
import { ChevronDownIcon } from '@heroicons/react/solid'
} from '@heroicons/react/solid'
import { nativeToUi, ZERO_BN } from '@blockworks-foundation/mango-client'
import useMangoStore, { serumProgramId, MNGO_INDEX } from 'stores/useMangoStore'
import PageBodyContainer from 'components/PageBodyContainer'
import TopBar from 'components/TopBar'
import AccountOrders from 'components/account_page/AccountOrders'
import AccountHistory from 'components/account_page/AccountHistory'
import AccountsModal from 'components/AccountsModal'
import AccountOverview from 'components/account_page/AccountOverview'
import AccountInterest from 'components/account_page/AccountInterest'
import AccountFunding from 'components/account_page/AccountFunding'
import AccountPerformancePerToken from 'components/account_page/AccountPerformancePerToken'
import AccountNameModal from 'components/AccountNameModal'
import { IconButton, LinkButton } from 'components/Button'
import Button, { LinkButton } from 'components/Button'
import EmptyState from 'components/EmptyState'
import Loading from 'components/Loading'
import Swipeable from 'components/mobile/Swipeable'
@ -49,15 +44,23 @@ import {
mangoGroupSelector,
} from 'stores/selectors'
import CreateAlertModal from 'components/CreateAlertModal'
import { copyToClipboard } from 'utils'
import {
abbreviateAddress,
// abbreviateAddress,
copyToClipboard,
} from 'utils'
import DelegateModal from 'components/DelegateModal'
import { Menu, Transition } from '@headlessui/react'
import { useWallet } from '@solana/wallet-adapter-react'
import { handleWalletConnect } from 'components/ConnectWalletButton'
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
import NftProfilePicModal from 'components/NftProfilePicModal'
import ProfileImage from 'components/ProfileImage'
import SwipeableTabs from 'components/mobile/SwipeableTabs'
import useLocalStorageState from 'hooks/useLocalStorageState'
import dayjs from 'dayjs'
import Link from 'next/link'
import ProfileImage from 'components/ProfileImage'
import Tooltip from 'components/Tooltip'
export async function getStaticProps({ locale }) {
return {
@ -76,14 +79,7 @@ export async function getStaticProps({ locale }) {
}
}
const TABS = [
'Portfolio',
'Orders',
'History',
'Interest',
'Funding',
'Performance',
]
const TABS = ['Overview', 'Orders', 'History', 'Performance']
export default function Account() {
const { t } = useTranslation(['common', 'close-account', 'delegate'])
@ -106,9 +102,10 @@ export default function Account() {
const [viewIndex, setViewIndex] = useState(0)
const [activeTab, setActiveTab] = useState(TABS[0])
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
const [savedLanguage] = useLocalStorageState('language', '')
const [profileData, setProfileData] = useState<any>(null)
const [loadProfileDetails, setLoadProfileDetails] = useState(false)
const connecting = wallet?.adapter?.connecting
const isMobile = width ? width < breakpoints.sm : false
@ -147,6 +144,10 @@ export default function Account() {
}
}, [wallet])
useEffect(() => {
dayjs.locale(savedLanguage == 'zh_tw' ? 'zh-tw' : savedLanguage)
})
useEffect(() => {
async function loadUnownedMangoAccount() {
try {
@ -279,233 +280,244 @@ export default function Account() {
}
}
const fetchProfileDetails = async (walletPk: string) => {
setLoadProfileDetails(true)
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/user-data/profile-details?wallet-pk=${walletPk}`
)
const data = await response.json()
setProfileData(data)
setLoadProfileDetails(false)
} catch (e) {
notify({ type: 'error', title: t('profile:profile-fetch-fail') })
console.log(e)
setLoadProfileDetails(false)
}
}
useEffect(() => {
if (mangoAccount && pubkey) {
fetchProfileDetails(mangoAccount.owner.toString())
}
}, [mangoAccount, pubkey])
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
<PageBodyContainer>
<div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10">
{mangoAccount ? (
<>
<div className="flex flex-col pb-3 sm:flex-row sm:items-center md:pb-0">
<button
disabled={!!pubkey}
className={`relative mb-2 mr-4 flex h-20 w-20 items-center justify-center rounded-full sm:mb-0 ${
loadingTransaction
? 'animate-pulse bg-th-bkg-4'
: 'bg-th-bkg-button'
}`}
onClick={() => setShowProfilePicModal(true)}
>
<ProfileImage
thumbHeightClass="h-20"
thumbWidthClass="w-20"
placeholderHeightClass="h-12"
placeholderWidthClass="w-12"
/>
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
</div>
</button>
<div>
<div className="mb-1 flex items-center">
<h1 className={`mr-3`}>
{mangoAccount?.name || t('account')}
</h1>
{!pubkey ? (
<IconButton
className="h-7 w-7"
onClick={() => setShowNameModal(true)}
>
<PencilIcon className="h-3.5 w-3.5" />
</IconButton>
) : null}
</div>
<div className="flex h-4 items-center">
<div className="pt-6">
<div className="flex flex-col pb-4 lg:flex-row lg:items-end lg:justify-between">
{mangoAccount ? (
<>
<div className="flex flex-col pb-3 sm:flex-row sm:items-center lg:pb-0">
<div>
<div className="flex h-8 items-center">
<Tooltip content="Copy account address">
<LinkButton
className="flex items-center text-th-fgd-4 no-underline"
onClick={() =>
handleCopyAddress(mangoAccount.publicKey.toString())
}
>
<span className="text-xxs font-normal sm:text-xs">
{mangoAccount.publicKey.toBase58()}
</span>
<DuplicateIcon className="ml-1.5 h-4 w-4" />
<h1>
{mangoAccount?.name ||
abbreviateAddress(mangoAccount.publicKey)}
</h1>
<DuplicateIcon className="ml-1.5 h-5 w-5" />
</LinkButton>
{isCopied ? (
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
Copied
</span>
) : null}
</div>
<div className="flex items-center text-xxs text-th-fgd-4">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
{t('account-address-warning')}
</div>
</Tooltip>
{isCopied ? (
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
Copied
</span>
) : null}
</div>
<div className="flex items-center text-xxs text-th-fgd-4">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
{t('account-address-warning')}
</div>
{pubkey && mangoAccount ? (
profileData && !loadProfileDetails ? (
<Link
href={`/profile?name=${profileData?.profile_name.replace(
/\s/g,
'-'
)}`}
shallow={true}
>
<a className="default-transition mt-2 flex items-center space-x-2 text-th-fgd-3 hover:text-th-fgd-2">
<ProfileImage
imageSize="24"
placeholderSize="12"
publicKey={mangoAccount.owner.toString()}
/>
<span className="mb-0 capitalize">
{profileData?.profile_name}
</span>
</a>
</Link>
) : (
<div className="mt-2 h-7 w-40 animate-pulse rounded bg-th-bkg-3" />
)
) : null}
</div>
{!pubkey ? (
<div className="flex items-center space-x-2">
<button
className="flex h-8 w-full items-center justify-center rounded-full bg-th-primary px-3 py-0 text-xs font-bold text-th-bkg-1 hover:brightness-[1.15] focus:outline-none disabled:cursor-not-allowed disabled:bg-th-bkg-4 disabled:text-th-fgd-4 disabled:hover:brightness-100"
disabled={mngoAccrued.eq(ZERO_BN)}
onClick={handleRedeemMngo}
>
<div className="flex items-center whitespace-nowrap">
<GiftIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{!mngoAccrued.eq(ZERO_BN) && mangoGroup
? t('claim-x-mngo', {
amount: nativeToUi(
mngoAccrued.toNumber(),
mangoGroup.tokens[MNGO_INDEX].decimals
).toLocaleString(undefined, {
minimumSignificantDigits: 1,
}),
})
: t('zero-mngo-rewards')}
</div>
</button>
<Menu>
{({ open }) => (
<div className="relative sm:w-full">
<Menu.Button className="flex h-8 items-center justify-center rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-2 text-xs font-bold hover:brightness-[1.1] hover:filter sm:w-full">
{t('more')}
<ChevronDownIcon
className={`default-transition h-5 w-5 ${
open
? 'rotate-180 transform'
: 'rotate-360 transform'
}`}
/>
</Menu.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 transform scale-75"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 z-20 mt-1 w-full space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5 sm:w-48">
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowAlertsModal(true)}
>
<div className="flex items-center">
<BellIcon className="mr-1.5 h-4 w-4" />
{t('alerts')}
</div>
</button>
</Menu.Item>
{!isDelegatedAccount ? (
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowDelegateModal(true)}
>
<div className="flex items-center">
<UsersIcon className="mr-1.5 h-4 w-4" />
{t('delegate:set-delegate')}
</div>
</button>
</Menu.Item>
) : null}
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowAccountsModal(true)}
>
<div className="flex items-center">
<SwitchHorizontalIcon className="mr-1.5 h-4 w-4" />
{t('change-account')}
</div>
</button>
</Menu.Item>
{!isDelegatedAccount ? (
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowCloseAccountModal(true)}
>
<div className="flex items-center whitespace-nowrap">
<TrashIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{t('close-account:close-account')}
</div>
</button>
</Menu.Item>
) : null}
</Menu.Items>
</Transition>
</div>
)}
</Menu>
</div>
) : null}
</>
) : null}
</div>
<div className="md:rounded-lg md:bg-th-bkg-2 md:p-6">
{mangoAccount ? (
!isMobile ? (
<>
<Tabs
activeTab={activeTab}
onChange={handleTabChange}
tabs={TABS}
/>
<TabContent activeTab={activeTab} />
</>
) : (
<>
<SwipeableTabs
onChange={handleChangeViewIndex}
items={TABS}
tabIndex={viewIndex}
/>
<Swipeable
index={viewIndex}
onChangeIndex={handleChangeViewIndex}
</div>
{!pubkey ? (
<div className="flex items-center space-x-2">
<Button
className="flex h-8 w-full items-center justify-center rounded-full px-3 py-0 text-xs"
disabled={mngoAccrued.eq(ZERO_BN)}
onClick={handleRedeemMngo}
>
<div>
<AccountOverview />
<div className="flex items-center whitespace-nowrap">
<GiftIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{!mngoAccrued.eq(ZERO_BN) && mangoGroup
? t('claim-x-mngo', {
amount: nativeToUi(
mngoAccrued.toNumber(),
mangoGroup.tokens[MNGO_INDEX].decimals
).toLocaleString(undefined, {
minimumSignificantDigits: 1,
}),
})
: t('zero-mngo-rewards')}
</div>
<div>
<AccountOrders />
</div>
<div>
<AccountHistory />
</div>
<div>
<AccountInterest />
</div>
<div>
<AccountFunding />
</div>
<div>
<AccountPerformancePerToken />
</div>
</Swipeable>
</>
)
) : connected ? (
isLoading ? (
<div className="flex justify-center py-10">
<Loading />
</Button>
<Menu>
{({ open }) => (
<div className="relative sm:w-full">
<Menu.Button className="flex h-8 items-center justify-center rounded-full border border-th-fgd-4 bg-transparent pt-0 pb-0 pl-3 pr-2 text-xs font-bold text-th-fgd-2 hover:brightness-[1.1] hover:filter sm:w-full">
{t('more')}
<ChevronDownIcon
className={`default-transition h-5 w-5 ${
open
? 'rotate-180 transform'
: 'rotate-360 transform'
}`}
/>
</Menu.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 transform scale-75"
enterTo="opacity-100 transform scale-100"
leave="transition ease-out duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Menu.Items className="absolute right-0 z-20 mt-1 w-full space-y-1.5 rounded-md bg-th-bkg-3 px-4 py-2.5 sm:w-48">
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
onClick={() => setShowAlertsModal(true)}
>
<div className="flex items-center">
<BellIcon className="mr-1.5 h-4 w-4" />
{t('alerts')}
</div>
</button>
</Menu.Item>
{!isDelegatedAccount ? (
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
onClick={() => setShowDelegateModal(true)}
>
<div className="flex items-center">
<UsersIcon className="mr-1.5 h-4 w-4" />
{t('delegate:set-delegate')}
</div>
</button>
</Menu.Item>
) : null}
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
onClick={() => setShowAccountsModal(true)}
>
<div className="flex items-center">
<SwitchHorizontalIcon className="mr-1.5 h-4 w-4" />
{t('change-account')}
</div>
</button>
</Menu.Item>
{!isDelegatedAccount ? (
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-primary"
onClick={() => setShowCloseAccountModal(true)}
>
<div className="flex items-center whitespace-nowrap">
<TrashIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{t('close-account:close-account')}
</div>
</button>
</Menu.Item>
) : null}
</Menu.Items>
</Transition>
</div>
)}
</Menu>
</div>
) : (
<EmptyState
buttonText={t('create-account')}
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title={t('no-account-found')}
disabled={!wallet || !mangoGroup}
) : null}
</>
) : null}
</div>
<div>
{mangoAccount ? (
!isMobile ? (
<div className="mt-2">
<Tabs
activeTab={activeTab}
onChange={handleTabChange}
tabs={TABS}
/>
)
<TabContent activeTab={activeTab} />
</div>
) : (
<div className="mt-2">
<SwipeableTabs
onChange={handleChangeViewIndex}
items={TABS}
tabIndex={viewIndex}
width="w-40 sm:w-48"
/>
<Swipeable
index={viewIndex}
onChangeIndex={handleChangeViewIndex}
>
<div>
<AccountOverview />
</div>
<div>
<AccountOrders />
</div>
<div>
<AccountHistory />
</div>
<div>
<AccountPerformancePerToken />
</div>
</Swipeable>
</div>
)
) : isLoading && (connected || pubkey) ? (
<div className="flex justify-center py-10">
<Loading />
</div>
) : connected ? (
<div className="-mt-4 rounded-lg border border-th-bkg-3 p-4 md:p-6">
<EmptyState
buttonText={t('create-account')}
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title={t('no-account-found')}
disabled={!wallet || !mangoGroup}
/>
</div>
) : (
<div className="-mt-4 rounded-lg border border-th-bkg-3 p-4 md:p-6">
<EmptyState
buttonText={t('connect')}
desc={t('connect-view')}
@ -514,14 +526,16 @@ export default function Account() {
onClickButton={handleConnect}
title={t('connect-wallet')}
/>
)}
</div>
{!connected && (
<div className="mt-6 md:mt-3 md:rounded-lg md:bg-th-bkg-2 md:p-6">
<MangoAccountLookup />
{!connected && !pubkey ? (
<div className="flex flex-col items-center pt-2">
<p>OR</p>
<MangoAccountLookup />
</div>
) : null}
</div>
)}
</PageBodyContainer>
</div>
{showAccountsModal ? (
<AccountsModal
onClose={handleCloseAccounts}
@ -566,16 +580,12 @@ export default function Account() {
const TabContent = ({ activeTab }) => {
switch (activeTab) {
case 'Portfolio':
case 'Overview':
return <AccountOverview />
case 'Orders':
return <AccountOrders />
case 'History':
return <AccountHistory />
case 'Interest':
return <AccountInterest />
case 'Funding':
return <AccountFunding />
case 'Performance':
return <AccountPerformancePerToken />
default:

Some files were not shown because too many files have changed in this diff Show More