add explore page
This commit is contained in:
parent
3f8f9f0877
commit
372876f0de
|
@ -15,7 +15,7 @@ import {
|
|||
ArchiveBoxArrowDownIcon,
|
||||
ExclamationTriangleIcon,
|
||||
DocumentTextIcon,
|
||||
// ClipboardDocumentIcon,
|
||||
SparklesIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -224,6 +224,13 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
|
|||
title={t('trade')}
|
||||
pagePath="/trade"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/explore'}
|
||||
collapsed={collapsed}
|
||||
icon={<SparklesIcon className="h-5 w-5" />}
|
||||
title={t('explore')}
|
||||
pagePath="/explore"
|
||||
/>
|
||||
<MenuItem
|
||||
active={pathname === '/borrow'}
|
||||
collapsed={collapsed}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Bank, MangoAccount } from '@blockworks-foundation/mango-v4'
|
||||
import { Bank } from '@blockworks-foundation/mango-v4'
|
||||
import { Disclosure, Popover, Transition } from '@headlessui/react'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
|
@ -63,7 +63,7 @@ const TokenList = () => {
|
|||
SHOW_ZERO_BALANCES_KEY,
|
||||
true,
|
||||
)
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const { initContributions } = useHealthContributions()
|
||||
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
|
||||
const totalInterestData = mangoStore(
|
||||
|
@ -156,7 +156,7 @@ const TokenList = () => {
|
|||
<div className="flex w-full items-center justify-end border-b border-th-bkg-3 px-6 py-3 lg:-mt-[36px] lg:mb-4 lg:mr-12 lg:w-auto lg:border-0 lg:py-0">
|
||||
<Switch
|
||||
checked={showZeroBalances}
|
||||
disabled={!mangoAccount}
|
||||
disabled={!mangoAccountAddress}
|
||||
onChange={() => setShowZeroBalances(!showZeroBalances)}
|
||||
>
|
||||
{t('account:zero-balances')}
|
||||
|
@ -347,7 +347,7 @@ const TokenList = () => {
|
|||
</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end">
|
||||
<ActionsMenu bank={bank} mangoAccount={mangoAccount} />
|
||||
<ActionsMenu bank={bank} />
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
|
@ -492,7 +492,7 @@ const MobileTokenListItem = ({ data }: { data: TableData }) => {
|
|||
</p>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<ActionsMenu bank={bank} mangoAccount={mangoAccount} />
|
||||
<ActionsMenu bank={bank} />
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
|
@ -503,14 +503,15 @@ const MobileTokenListItem = ({ data }: { data: TableData }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const ActionsMenu = ({
|
||||
export const ActionsMenu = ({
|
||||
bank,
|
||||
mangoAccount,
|
||||
showText,
|
||||
}: {
|
||||
bank: Bank
|
||||
mangoAccount: MangoAccount | undefined
|
||||
showText?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const [showDepositModal, setShowDepositModal] = useState(false)
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false)
|
||||
const [showBorrowModal, setShowBorrowModal] = useState(false)
|
||||
|
@ -521,6 +522,7 @@ const ActionsMenu = ({
|
|||
const { mangoTokens } = useJupiterMints()
|
||||
const spotMarkets = mangoStore((s) => s.serumMarkets)
|
||||
const { isUnownedAccount } = useUnownedAccount()
|
||||
const { isDesktop } = useViewport()
|
||||
|
||||
const spotMarket = useMemo(() => {
|
||||
return spotMarkets.find((m) => {
|
||||
|
@ -544,9 +546,12 @@ const ActionsMenu = ({
|
|||
)
|
||||
|
||||
const balance = useMemo(() => {
|
||||
if (!mangoAccount || !bank) return 0
|
||||
return mangoAccount.getTokenBalanceUi(bank)
|
||||
}, [bank, mangoAccount])
|
||||
if (!mangoAccountAddress || !bank) return 0
|
||||
const mangoAccount = mangoStore.getState().mangoAccount.current
|
||||
if (mangoAccount) {
|
||||
return mangoAccount.getTokenBalanceUi(bank)
|
||||
} else return 0
|
||||
}, [bank, mangoAccountAddress])
|
||||
|
||||
const handleSwap = useCallback(() => {
|
||||
const tokenInfo = mangoTokens.find(
|
||||
|
@ -583,7 +588,7 @@ const ActionsMenu = ({
|
|||
})
|
||||
}
|
||||
router.push('/swap', undefined, { shallow: true })
|
||||
}, [bank, router, set, mangoTokens, mangoAccount])
|
||||
}, [bank, router, set, mangoTokens, mangoAccountAddress])
|
||||
|
||||
const handleTrade = useCallback(() => {
|
||||
router.push(`/trade?name=${spotMarket?.name}`, undefined, { shallow: true })
|
||||
|
@ -596,16 +601,20 @@ const ActionsMenu = ({
|
|||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Popover.Button
|
||||
className={`flex h-10 w-28 items-center justify-center rounded-full border border-th-button text-th-fgd-1 md:h-8 md:w-8 ${
|
||||
!open ? 'focus-visible:border-th-fgd-2' : ''
|
||||
} md:hover:border-th-button-hover md:hover:text-th-fgd-1`}
|
||||
className={`flex items-center justify-center border border-th-button text-th-fgd-1 md:hover:border-th-button-hover md:hover:text-th-fgd-1 ${
|
||||
showText || !isDesktop
|
||||
? 'h-10 w-full rounded-md'
|
||||
: 'h-8 w-8 rounded-full'
|
||||
}`}
|
||||
>
|
||||
{showText || !isDesktop ? (
|
||||
<span className="mr-2 font-display">{t('actions')}</span>
|
||||
) : null}
|
||||
{open ? (
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EllipsisHorizontalIcon className="h-5 w-5" />
|
||||
)}
|
||||
<span className="ml-2 md:hidden">{t('actions')}</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
appear={true}
|
||||
|
@ -619,23 +628,29 @@ const ActionsMenu = ({
|
|||
leaveTo="opacity-0"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`thin-scroll absolute bottom-12 left-0 z-20 max-h-60 w-32 space-y-2 overflow-auto rounded-md bg-th-bkg-2 p-4 pt-2 md:bottom-0 md:left-auto md:right-12 md:pt-4`}
|
||||
className={`thin-scroll absolute z-20 max-h-60 w-32 space-y-2 overflow-auto rounded-md bg-th-bkg-2 p-4 ${
|
||||
isDesktop && !showText
|
||||
? 'bottom-0 left-auto right-12 pt-2'
|
||||
: 'bottom-12 left-0'
|
||||
}`}
|
||||
>
|
||||
<div className="hidden items-center justify-center border-b border-th-bkg-3 pb-2 md:flex">
|
||||
<div className="mr-2 flex flex-shrink-0 items-center">
|
||||
<TokenLogo bank={bank} size={20} />
|
||||
{!showText && isDesktop ? (
|
||||
<div className="flex items-center justify-center border-b border-th-bkg-3 pb-2">
|
||||
<div className="mr-2 flex flex-shrink-0 items-center">
|
||||
<TokenLogo bank={bank} size={20} />
|
||||
</div>
|
||||
<p className="font-body">
|
||||
{formatTokenSymbol(bank.name)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-body">{formatTokenSymbol(bank.name)}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => handleShowActionModals(bank.name, 'deposit')}
|
||||
>
|
||||
{t('deposit')}
|
||||
</ActionsLinkButton>
|
||||
{balance < 0 ? (
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => handleShowActionModals(bank.name, 'repay')}
|
||||
>
|
||||
{t('repay')}
|
||||
|
@ -643,7 +658,6 @@ const ActionsMenu = ({
|
|||
) : null}
|
||||
{balance && balance > 0 ? (
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() =>
|
||||
handleShowActionModals(bank.name, 'withdraw')
|
||||
}
|
||||
|
@ -652,22 +666,15 @@ const ActionsMenu = ({
|
|||
</ActionsLinkButton>
|
||||
) : null}
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => handleShowActionModals(bank.name, 'borrow')}
|
||||
>
|
||||
{t('borrow')}
|
||||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={handleSwap}
|
||||
>
|
||||
<ActionsLinkButton onClick={handleSwap}>
|
||||
{t('swap')}
|
||||
</ActionsLinkButton>
|
||||
{spotMarket ? (
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={handleTrade}
|
||||
>
|
||||
<ActionsLinkButton onClick={handleTrade}>
|
||||
{t('trade')}
|
||||
</ActionsLinkButton>
|
||||
) : null}
|
||||
|
|
|
@ -17,8 +17,6 @@ import CloseAccountModal from '../modals/CloseAccountModal'
|
|||
import AccountNameModal from '../modals/AccountNameModal'
|
||||
import { copyToClipboard } from 'utils'
|
||||
import { notify } from 'utils/notifications'
|
||||
import { abbreviateAddress } from 'utils/formatting'
|
||||
import { MangoAccount } from '@blockworks-foundation/mango-v4'
|
||||
import DelegateModal from '@components/modals/DelegateModal'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import BorrowRepayModal from '@components/modals/BorrowRepayModal'
|
||||
|
@ -34,10 +32,10 @@ import useLocalStorageState from 'hooks/useLocalStorageState'
|
|||
import { PRIVACY_MODE } from 'utils/constants'
|
||||
|
||||
export const handleCopyAddress = (
|
||||
mangoAccount: MangoAccount,
|
||||
mangoAccountAddress: string,
|
||||
successMessage: string,
|
||||
) => {
|
||||
copyToClipboard(mangoAccount.publicKey.toString())
|
||||
copyToClipboard(mangoAccountAddress)
|
||||
notify({
|
||||
title: successMessage,
|
||||
type: 'success',
|
||||
|
@ -46,7 +44,7 @@ export const handleCopyAddress = (
|
|||
|
||||
const AccountActions = () => {
|
||||
const { t } = useTranslation(['common', 'close-account', 'settings'])
|
||||
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
|
||||
const [showEditAccountModal, setShowEditAccountModal] = useState(false)
|
||||
const [showBorrowModal, setShowBorrowModal] = useState(false)
|
||||
|
@ -123,12 +121,14 @@ const AccountActions = () => {
|
|||
>
|
||||
<Popover.Panel className="absolute right-0 top-10 mt-1 space-y-2 rounded-md bg-th-bkg-2 px-4 py-2.5">
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() =>
|
||||
handleCopyAddress(
|
||||
mangoAccount!,
|
||||
mangoAccountAddress,
|
||||
t('copy-address-success', {
|
||||
pk: abbreviateAddress(mangoAccount!.publicKey),
|
||||
pk: `${mangoAccountAddress.slice(
|
||||
0,
|
||||
4,
|
||||
)}...${mangoAccountAddress.slice(-4)}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -138,7 +138,6 @@ const AccountActions = () => {
|
|||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => setShowEditAccountModal(true)}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
|
@ -146,7 +145,6 @@ const AccountActions = () => {
|
|||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount && isUnownedAccount}
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => setShowDelegateModal(true)}
|
||||
>
|
||||
<UserPlusIcon className="h-4 w-4" />
|
||||
|
@ -155,7 +153,6 @@ const AccountActions = () => {
|
|||
{!isAccountFull ? (
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => setShowAccountSizeModal(true)}
|
||||
>
|
||||
<SquaresPlusIcon className="h-4 w-4" />
|
||||
|
@ -166,14 +163,12 @@ const AccountActions = () => {
|
|||
) : null}
|
||||
<ActionsLinkButton
|
||||
disabled={isDelegatedAccount}
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => setShowCloseAccountModal(true)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span className="ml-2">{t('close-account')}</span>
|
||||
</ActionsLinkButton>
|
||||
<ActionsLinkButton
|
||||
mangoAccount={mangoAccount!}
|
||||
onClick={() => setPrivacyMode(!privacyMode)}
|
||||
>
|
||||
{privacyMode ? (
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import { MangoAccount } from '@blockworks-foundation/mango-v4'
|
||||
import { LinkButton } from '@components/shared/Button'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
const ActionsLinkButton = ({
|
||||
children,
|
||||
disabled,
|
||||
mangoAccount,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode
|
||||
disabled?: boolean
|
||||
mangoAccount: MangoAccount
|
||||
onClick: () => void
|
||||
}) => {
|
||||
const { mangoAccountAddress } = useMangoAccount()
|
||||
return (
|
||||
<LinkButton
|
||||
className="w-full whitespace-nowrap text-left font-normal"
|
||||
disabled={!mangoAccount || disabled}
|
||||
disabled={!mangoAccountAddress || disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import PerpMarketsTable from './PerpMarketsTable'
|
||||
import { useRouter } from 'next/router'
|
||||
import TokenPage from '@components/token/TokenPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import Spot from './Spot'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import PerpStatsPage from '@components/stats/perps/PerpStatsPage'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const ExplorePage = () => {
|
||||
const { t } = useTranslation(['common'])
|
||||
const router = useRouter()
|
||||
const { token } = router.query
|
||||
const { market } = router.query
|
||||
const perpStats = mangoStore((s) => s.perpStats.data)
|
||||
const initialStatsLoad = mangoStore((s) => s.tokenStats.initialLoad)
|
||||
const [activeTab, setActiveTab] = useState('spot')
|
||||
const { perpMarketsWithData, serumMarketsWithData } =
|
||||
useListedMarketsWithMarketData()
|
||||
|
||||
const tabsWithCount: [string, number][] = useMemo(() => {
|
||||
const tabs: [string, number][] = [
|
||||
['spot', serumMarketsWithData.length],
|
||||
['perp', perpMarketsWithData.length],
|
||||
// ['accounts', 0],
|
||||
]
|
||||
return tabs
|
||||
}, [perpMarketsWithData, serumMarketsWithData])
|
||||
|
||||
useEffect(() => {
|
||||
if (!perpStats || !perpStats.length) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPerpStats()
|
||||
}
|
||||
}, [perpStats])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialStatsLoad) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchTokenStats()
|
||||
}
|
||||
}, [initialStatsLoad])
|
||||
|
||||
return market ? (
|
||||
<PerpStatsPage />
|
||||
) : token ? (
|
||||
<TokenPage />
|
||||
) : (
|
||||
<div className="pb-[27px]">
|
||||
<div>
|
||||
<div className="flex flex-col items-center py-8">
|
||||
<h1 className="mb-4">{t('explore')}</h1>
|
||||
<div className="flex justify-center">
|
||||
{tabsWithCount.map((tab) => (
|
||||
<button
|
||||
className={`flex items-center space-x-2 border-y-2 border-r-2 border-th-bkg-3 px-6 py-3 font-display text-base first:rounded-l-lg first:border-l-2 last:rounded-r-lg focus:outline-none ${
|
||||
activeTab === tab[0] ? 'bg-th-bkg-2 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab[0])}
|
||||
key={tab[0]}
|
||||
>
|
||||
<span>{t(tab[0])}</span>
|
||||
<div className="rounded-md bg-th-bkg-3 px-1 py-0.5 font-body text-xs font-medium text-th-fgd-3">
|
||||
<span>{tab[1]}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TabContent activeTab={activeTab} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExplorePage
|
||||
|
||||
const TabContent = ({ activeTab }: { activeTab: string }) => {
|
||||
switch (activeTab) {
|
||||
case 'spot':
|
||||
return <Spot />
|
||||
case 'perp':
|
||||
return (
|
||||
<div className="border-t border-th-bkg-3">
|
||||
<PerpMarketsTable />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return <Spot />
|
||||
}
|
||||
}
|
|
@ -1,9 +1,5 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useViewport } from '../../../hooks/useViewport'
|
||||
import { COLORS } from '../../../styles/colors'
|
||||
import { breakpoints } from '../../../utils/theme'
|
||||
import ContentBox from '../../shared/ContentBox'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||
import {
|
||||
|
@ -25,6 +21,10 @@ import useListedMarketsWithMarketData, {
|
|||
PerpMarketWithMarketData,
|
||||
} from 'hooks/useListedMarketsWithMarketData'
|
||||
import { sortPerpMarkets } from 'utils/markets'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import { COLORS } from 'styles/colors'
|
||||
|
||||
export const goToPerpMarketDetails = (
|
||||
market: PerpMarket,
|
||||
|
@ -34,7 +34,7 @@ export const goToPerpMarketDetails = (
|
|||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
const PerpMarketsOverviewTable = () => {
|
||||
const PerpMarketsTable = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const { theme } = useThemeWrapper()
|
||||
const { width } = useViewport()
|
||||
|
@ -256,7 +256,7 @@ const PerpMarketsOverviewTable = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export default PerpMarketsOverviewTable
|
||||
export default PerpMarketsTable
|
||||
|
||||
const MobilePerpMarketItem = ({
|
||||
market,
|
|
@ -0,0 +1,275 @@
|
|||
import Change from '@components/shared/Change'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChangeEvent, Fragment, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SpotMarketsTable from './SpotMarketsTable'
|
||||
import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable'
|
||||
import {
|
||||
BoltIcon,
|
||||
ChevronRightIcon,
|
||||
FaceFrownIcon,
|
||||
MagnifyingGlassIcon,
|
||||
RocketLaunchIcon,
|
||||
Squares2X2Icon,
|
||||
TableCellsIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { AllowedKeys, sortSpotMarkets, startSearch } from 'utils/markets'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import SpotMarketCards from './SpotMarketCards'
|
||||
import Input from '@components/forms/Input'
|
||||
import EmptyState from '@components/nftMarket/EmptyState'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const Spot = () => {
|
||||
const { t } = useTranslation(['common', 'explore', 'trade'])
|
||||
const router = useRouter()
|
||||
const { group } = useMangoGroup()
|
||||
const { serumMarketsWithData } = useListedMarketsWithMarketData()
|
||||
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
|
||||
const [search, setSearch] = useState('')
|
||||
const [showTableView, setShowTableView] = useState(false)
|
||||
|
||||
const newlyListedMintInfo = useMemo(() => {
|
||||
if (!group) return []
|
||||
const mintInfos = Array.from(group.mintInfosMapByTokenIndex).map(
|
||||
([, mintInfo]) => mintInfo,
|
||||
)
|
||||
const sortByRegistrationTime = mintInfos
|
||||
.sort((a, b) => {
|
||||
return b.registrationTime.toNumber() - a.registrationTime.toNumber()
|
||||
})
|
||||
.slice(0, 3)
|
||||
return sortByRegistrationTime
|
||||
}, [group])
|
||||
|
||||
const newlyListed = useMemo(() => {
|
||||
if (!newlyListedMintInfo.length || !serumMarketsWithData.length) return []
|
||||
const newlyListed = []
|
||||
for (const listing of newlyListedMintInfo) {
|
||||
const market = serumMarketsWithData.find(
|
||||
(market) => market.baseTokenIndex === listing.tokenIndex,
|
||||
)
|
||||
if (market) {
|
||||
newlyListed.push(market)
|
||||
}
|
||||
}
|
||||
return newlyListed
|
||||
}, [newlyListedMintInfo, serumMarketsWithData])
|
||||
|
||||
const [gainers, losers] = useMemo(() => {
|
||||
if (!serumMarketsWithData.length) return [[], []]
|
||||
const sortByChange = serumMarketsWithData
|
||||
.filter((market) => market.quoteTokenIndex === 0)
|
||||
.sort((a, b) => {
|
||||
const rollingChangeA = a.rollingChange || 0
|
||||
const rollingChangeB = b.rollingChange || 0
|
||||
return rollingChangeB - rollingChangeA
|
||||
})
|
||||
const gainers = sortByChange.slice(0, 3).filter((item) => {
|
||||
const change = item.rollingChange || 0
|
||||
return change > 0
|
||||
})
|
||||
const losers = sortByChange
|
||||
.slice(-3)
|
||||
.reverse()
|
||||
.filter((item) => {
|
||||
const change = item.rollingChange || 0
|
||||
return change < 0
|
||||
})
|
||||
return [gainers, losers]
|
||||
}, [serumMarketsWithData])
|
||||
|
||||
const sortedSerumMarketsToShow = useMemo(() => {
|
||||
if (!serumMarketsWithData.length) return []
|
||||
return search
|
||||
? startSearch(serumMarketsWithData, search)
|
||||
: sortSpotMarkets(serumMarketsWithData, sortByKey)
|
||||
}, [search, serumMarketsWithData, sortByKey, showTableView])
|
||||
|
||||
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4 px-4 pb-8 md:px-6 2xl:px-12">
|
||||
<div className="col-span-12 rounded-lg border border-th-bkg-3 p-6 lg:col-span-4">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<BoltIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:recently-listed')}</h2>
|
||||
</div>
|
||||
<div className="border-t border-th-bkg-3">
|
||||
{newlyListed.map((listing) => {
|
||||
const bank = group?.getFirstBankByTokenIndex(
|
||||
listing.baseTokenIndex,
|
||||
)
|
||||
const mintInfo = newlyListedMintInfo.find(
|
||||
(info) => info.tokenIndex === listing.baseTokenIndex,
|
||||
)
|
||||
let timeSinceListing = ''
|
||||
if (mintInfo) {
|
||||
timeSinceListing = dayjs().to(
|
||||
mintInfo.registrationTime.toNumber() * 1000,
|
||||
)
|
||||
}
|
||||
if (!bank) return null
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={listing.baseTokenIndex}
|
||||
onClick={() => goToTokenPage(bank.name.split(' ')[0], router)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">{bank.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3">
|
||||
<span className="text-th-fgd-3">{timeSinceListing}</span>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 rounded-lg border border-th-bkg-3 p-6 lg:col-span-4">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<RocketLaunchIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:gainers')}</h2>
|
||||
</div>
|
||||
<div className="border-t border-th-bkg-3">
|
||||
{gainers.map((gainer) => {
|
||||
const bank = group?.getFirstBankByTokenIndex(
|
||||
gainer.baseTokenIndex,
|
||||
)
|
||||
if (!bank) return null
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={gainer.baseTokenIndex}
|
||||
onClick={() => goToTokenPage(bank.name.split(' ')[0], router)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">{bank.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
</span>
|
||||
<Change change={gainer.rollingChange || 0} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 rounded-lg border border-th-bkg-3 p-6 lg:col-span-4">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<FaceFrownIcon className="h-5 w-5" />
|
||||
<h2 className="text-base">{t('explore:losers')}</h2>
|
||||
</div>
|
||||
<div className="border-t border-th-bkg-3">
|
||||
{losers.map((loser) => {
|
||||
const bank = group?.getFirstBankByTokenIndex(loser.baseTokenIndex)
|
||||
if (!bank) return null
|
||||
return (
|
||||
<div
|
||||
className="default-transition flex h-16 cursor-pointer items-center justify-between border-b border-th-bkg-3 px-4 md:hover:bg-th-bkg-2"
|
||||
key={loser.baseTokenIndex}
|
||||
onClick={() => goToTokenPage(bank.name.split(' ')[0], router)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TokenLogo bank={bank} />
|
||||
<p className="ml-3 font-body text-th-fgd-2">{bank.name}</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-3 flex flex-col items-end">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
</span>
|
||||
<Change change={loser.rollingChange || 0} suffix="%" />
|
||||
</div>
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-th-bkg-3 pt-4">
|
||||
<div className="flex flex-col px-4 sm:flex-row sm:items-end sm:justify-between md:px-6 2xl:px-12">
|
||||
<h2 className="mb-4 text-base sm:mb-0">{t('tokens')}</h2>
|
||||
<div className="flex flex-col sm:flex-row sm:space-x-3">
|
||||
<div className="relative mb-3 w-full sm:mb-0 sm:w-40">
|
||||
<Input
|
||||
heightClass="h-10 pl-8"
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={handleUpdateSearch}
|
||||
/>
|
||||
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<div className="w-full md:w-48">
|
||||
<ButtonGroup
|
||||
activeValue={sortByKey}
|
||||
onChange={(v) => setSortByKey(v)}
|
||||
names={[t('trade:24h-volume'), t('rolling-change')]}
|
||||
values={['quote_volume_24h', 'change_24h']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-l-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
!showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<Squares2X2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className={`flex w-10 items-center justify-center rounded-r-md border border-th-bkg-3 focus:outline-none md:hover:bg-th-bkg-3 ${
|
||||
showTableView ? 'bg-th-bkg-3 text-th-active' : ''
|
||||
}`}
|
||||
onClick={() => setShowTableView(!showTableView)}
|
||||
>
|
||||
<TableCellsIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{sortedSerumMarketsToShow.length ? (
|
||||
showTableView ? (
|
||||
<div className="mt-6 border-t border-th-bkg-3">
|
||||
<SpotMarketsTable markets={sortedSerumMarketsToShow} />
|
||||
</div>
|
||||
) : (
|
||||
<SpotMarketCards markets={sortedSerumMarketsToShow} />
|
||||
)
|
||||
) : (
|
||||
<div className="px-4 pt-2 md:px-6 2xl:px-12">
|
||||
<EmptyState text="No results found..." />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spot
|
|
@ -0,0 +1,118 @@
|
|||
import { ActionsMenu } from '@components/TokenList'
|
||||
import Button from '@components/shared/Button'
|
||||
import Change from '@components/shared/Change'
|
||||
import FormatNumericValue from '@components/shared/FormatNumericValue'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable'
|
||||
import Decimal from 'decimal.js'
|
||||
import { SerumMarketWithMarketData } from 'hooks/useListedMarketsWithMarketData'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { numberCompacter } from 'utils/numbers'
|
||||
|
||||
const SpotMarketCards = ({
|
||||
markets,
|
||||
}: {
|
||||
markets: SerumMarketWithMarketData[]
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'explore', 'trade'])
|
||||
const { group } = useMangoGroup()
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 px-4 py-6 md:px-6 2xl:px-12">
|
||||
{markets
|
||||
.filter((m) => m.quoteTokenIndex === 0)
|
||||
.map((market) => {
|
||||
const { baseTokenIndex, rollingChange, marketData } = market
|
||||
const bank = group?.getFirstBankByTokenIndex(market.baseTokenIndex)
|
||||
|
||||
if (!bank) return null
|
||||
|
||||
const availableVaultBalance = group
|
||||
? group.getTokenVaultBalanceByMintUi(bank.mint) -
|
||||
bank.uiDeposits() * bank.minVaultToDepositsRatio
|
||||
: 0
|
||||
const available = Decimal.max(
|
||||
0,
|
||||
availableVaultBalance.toFixed(bank.mintDecimals),
|
||||
)
|
||||
const depositRate = bank.getDepositRateUi()
|
||||
const borrowRate = bank.getBorrowRateUi()
|
||||
const assetWeight = bank.scaledInitAssetWeight(bank.price).toFixed(2)
|
||||
return (
|
||||
<div
|
||||
className="col-span-12 rounded-lg border border-th-bkg-3 p-6 md:col-span-6 xl:col-span-4 2xl:col-span-3"
|
||||
key={baseTokenIndex}
|
||||
>
|
||||
<div className="mb-4 flex items-center space-x-3 border-b border-th-bkg-3 pb-4">
|
||||
<TokenLogo bank={bank} size={32} />
|
||||
<div>
|
||||
<h3 className="mb-1 text-base leading-none">{bank.name}</h3>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-mono">
|
||||
<FormatNumericValue value={bank.uiPrice} isUsd />
|
||||
</span>
|
||||
<Change change={rollingChange || 0} suffix="%" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p>{t('trade:24h-volume')}</p>
|
||||
<p className="font-mono text-th-fgd-2">
|
||||
{marketData?.quote_volume_24h ? (
|
||||
<span>
|
||||
{numberCompacter.format(marketData.quote_volume_24h)}{' '}
|
||||
<span className="font-body text-th-fgd-4">USDC</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
0 <span className="font-body text-th-fgd-4">USDC</span>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>{t('available')}</p>
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
<FormatNumericValue value={available} />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{t('collateral-weight')}</p>
|
||||
<span className="font-mono text-th-fgd-2">
|
||||
{assetWeight}x
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>{t('rates')}</p>
|
||||
<div className="flex space-x-1.5 font-mono">
|
||||
<p className="text-th-up">
|
||||
<FormatNumericValue value={depositRate} decimals={2} />%
|
||||
</p>
|
||||
<span className="text-th-fgd-4">|</span>
|
||||
<p className="text-th-down">
|
||||
<FormatNumericValue value={borrowRate} decimals={2} />%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={() => goToTokenPage(bank.name.split(' ')[0], router)}
|
||||
secondary
|
||||
>
|
||||
{t('details')}
|
||||
</Button>
|
||||
<div className="relative mt-3">
|
||||
<ActionsMenu bank={bank} showText />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpotMarketCards
|
|
@ -1,9 +1,5 @@
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import { useCallback } from 'react'
|
||||
import { useViewport } from '../../../hooks/useViewport'
|
||||
import { COLORS } from '../../../styles/colors'
|
||||
import { breakpoints } from '../../../utils/theme'
|
||||
import ContentBox from '../../shared/ContentBox'
|
||||
import MarketLogos from '@components/trade/MarketLogos'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import {
|
||||
|
@ -18,21 +14,27 @@ import FormatNumericValue from '@components/shared/FormatNumericValue'
|
|||
import { floorToDecimal, getDecimalCount, numberCompacter } from 'utils/numbers'
|
||||
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
|
||||
import useThemeWrapper from 'hooks/useThemeWrapper'
|
||||
import useListedMarketsWithMarketData, {
|
||||
SerumMarketWithMarketData,
|
||||
} from 'hooks/useListedMarketsWithMarketData'
|
||||
import { sortSpotMarkets } from 'utils/markets'
|
||||
import { SerumMarketWithMarketData } from 'hooks/useListedMarketsWithMarketData'
|
||||
import { useSortableData } from 'hooks/useSortableData'
|
||||
import Change from '@components/shared/Change'
|
||||
import { Bank } from '@blockworks-foundation/mango-v4'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import ContentBox from '@components/shared/ContentBox'
|
||||
import { COLORS } from 'styles/colors'
|
||||
import TokenLogo from '@components/shared/TokenLogo'
|
||||
import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable'
|
||||
import { useRouter } from 'next/router'
|
||||
import Decimal from 'decimal.js'
|
||||
import BankAmountWithValue from '@components/shared/BankAmountWithValue'
|
||||
|
||||
type TableData = {
|
||||
baseBank: Bank | undefined
|
||||
change: number
|
||||
market: SerumMarketWithMarketData
|
||||
marketName: string
|
||||
tokenName: string | undefined
|
||||
price: number
|
||||
priceHistory:
|
||||
| {
|
||||
|
@ -45,14 +47,17 @@ type TableData = {
|
|||
isUp: boolean
|
||||
}
|
||||
|
||||
const SpotMarketsTable = () => {
|
||||
const SpotMarketsTable = ({
|
||||
markets,
|
||||
}: {
|
||||
markets: SerumMarketWithMarketData[]
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { group } = useMangoGroup()
|
||||
const { theme } = useThemeWrapper()
|
||||
const { width } = useViewport()
|
||||
const router = useRouter()
|
||||
const showTableView = width ? width > breakpoints.md : false
|
||||
const { serumMarketsWithData, isLoading, isFetching } =
|
||||
useListedMarketsWithMarketData()
|
||||
|
||||
const formattedTableData = useCallback(
|
||||
(markets: SerumMarketWithMarketData[]) => {
|
||||
|
@ -77,7 +82,29 @@ const SpotMarketsTable = () => {
|
|||
|
||||
const change = volume > 0 ? ((price - pastPrice) / pastPrice) * 100 : 0
|
||||
|
||||
const marketName = m.name
|
||||
const tokenName = baseBank?.name
|
||||
|
||||
let availableVaultBalance = 0
|
||||
let available = new Decimal(0)
|
||||
let depositRate = 0
|
||||
let borrowRate = 0
|
||||
let assetWeight = '0'
|
||||
|
||||
if (baseBank) {
|
||||
availableVaultBalance = group
|
||||
? group.getTokenVaultBalanceByMintUi(baseBank.mint) -
|
||||
baseBank.uiDeposits() * baseBank.minVaultToDepositsRatio
|
||||
: 0
|
||||
available = Decimal.max(
|
||||
0,
|
||||
availableVaultBalance.toFixed(baseBank.mintDecimals),
|
||||
)
|
||||
depositRate = baseBank.getDepositRateUi()
|
||||
borrowRate = baseBank.getBorrowRateUi()
|
||||
assetWeight = baseBank
|
||||
.scaledInitAssetWeight(baseBank.price)
|
||||
.toFixed(2)
|
||||
}
|
||||
|
||||
const isUp =
|
||||
price && priceHistory && priceHistory.length
|
||||
|
@ -85,10 +112,14 @@ const SpotMarketsTable = () => {
|
|||
: false
|
||||
|
||||
const data = {
|
||||
available,
|
||||
assetWeight,
|
||||
borrowRate,
|
||||
baseBank,
|
||||
change,
|
||||
depositRate,
|
||||
market: m,
|
||||
marketName,
|
||||
tokenName,
|
||||
price,
|
||||
priceHistory,
|
||||
quoteBank,
|
||||
|
@ -106,13 +137,7 @@ const SpotMarketsTable = () => {
|
|||
items: tableData,
|
||||
requestSort,
|
||||
sortConfig,
|
||||
} = useSortableData(
|
||||
formattedTableData(
|
||||
sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h'),
|
||||
),
|
||||
)
|
||||
|
||||
const loadingMarketData = isLoading || isFetching
|
||||
} = useSortableData(formattedTableData(markets))
|
||||
|
||||
return (
|
||||
<ContentBox hideBorder hidePadding>
|
||||
|
@ -122,10 +147,10 @@ const SpotMarketsTable = () => {
|
|||
<TrHead>
|
||||
<Th className="text-left">
|
||||
<SortableColumnHeader
|
||||
sortKey="marketName"
|
||||
sort={() => requestSort('marketName')}
|
||||
sortKey="tokenName"
|
||||
sort={() => requestSort('tokenName')}
|
||||
sortConfig={sortConfig}
|
||||
title={t('market')}
|
||||
title={t('token')}
|
||||
/>
|
||||
</Th>
|
||||
<Th>
|
||||
|
@ -159,15 +184,50 @@ const SpotMarketsTable = () => {
|
|||
/>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<SortableColumnHeader
|
||||
sortKey="available"
|
||||
sort={() => requestSort('available')}
|
||||
sortConfig={sortConfig}
|
||||
title={t('available')}
|
||||
/>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<SortableColumnHeader
|
||||
sortKey="assetWeight"
|
||||
sort={() => requestSort('assetWeight')}
|
||||
sortConfig={sortConfig}
|
||||
title={t('collateral-weight')}
|
||||
/>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex justify-end">
|
||||
<SortableColumnHeader
|
||||
sortKey="depositRate"
|
||||
sort={() => requestSort('depositRate')}
|
||||
sortConfig={sortConfig}
|
||||
title={t('rates')}
|
||||
/>
|
||||
</div>
|
||||
</Th>
|
||||
<Th />
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableData.map((data) => {
|
||||
const {
|
||||
available,
|
||||
assetWeight,
|
||||
baseBank,
|
||||
borrowRate,
|
||||
change,
|
||||
depositRate,
|
||||
market,
|
||||
marketName,
|
||||
tokenName,
|
||||
price,
|
||||
priceHistory,
|
||||
quoteBank,
|
||||
|
@ -175,12 +235,20 @@ const SpotMarketsTable = () => {
|
|||
isUp,
|
||||
} = data
|
||||
|
||||
if (!baseBank) return null
|
||||
|
||||
return (
|
||||
<TrBody key={market.publicKey.toString()}>
|
||||
<TrBody
|
||||
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
|
||||
key={market.publicKey.toString()}
|
||||
onClick={() =>
|
||||
goToTokenPage(baseBank.name.split(' ')[0], router)
|
||||
}
|
||||
>
|
||||
<Td>
|
||||
<div className="flex items-center">
|
||||
<MarketLogos market={market} size="large" />
|
||||
<p className="font-body">{marketName}</p>
|
||||
<TokenLogo bank={baseBank} />
|
||||
<p className="ml-3 font-body">{tokenName}</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
|
@ -188,15 +256,7 @@ const SpotMarketsTable = () => {
|
|||
<p>
|
||||
{price ? (
|
||||
<>
|
||||
<FormatNumericValue
|
||||
value={price}
|
||||
isUsd={quoteBank?.name === 'USDC'}
|
||||
/>{' '}
|
||||
{quoteBank?.name !== 'USDC' ? (
|
||||
<span className="font-body text-th-fgd-4">
|
||||
{quoteBank?.name}
|
||||
</span>
|
||||
) : null}
|
||||
<FormatNumericValue value={price} isUsd />
|
||||
</>
|
||||
) : (
|
||||
'–'
|
||||
|
@ -210,23 +270,19 @@ const SpotMarketsTable = () => {
|
|||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{!loadingMarketData ? (
|
||||
priceHistory && priceHistory.length ? (
|
||||
<div className="h-10 w-24">
|
||||
<SimpleAreaChart
|
||||
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
data={priceHistory}
|
||||
name={baseBank!.name + quoteBank!.name}
|
||||
xKey="time"
|
||||
yKey="price"
|
||||
/>
|
||||
</div>
|
||||
) : baseBank?.name === 'USDC' ||
|
||||
baseBank?.name === 'USDT' ? null : (
|
||||
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
|
||||
)
|
||||
) : (
|
||||
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
|
||||
{priceHistory && priceHistory.length ? (
|
||||
<div className="h-10 w-24">
|
||||
<SimpleAreaChart
|
||||
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
data={priceHistory}
|
||||
name={baseBank!.name + quoteBank!.name}
|
||||
xKey="time"
|
||||
yKey="price"
|
||||
/>
|
||||
</div>
|
||||
) : baseBank?.name === 'USDC' ||
|
||||
baseBank?.name === 'USDT' ? null : (
|
||||
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
|
@ -250,6 +306,33 @@ const SpotMarketsTable = () => {
|
|||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col text-right">
|
||||
<BankAmountWithValue
|
||||
amount={available}
|
||||
bank={baseBank}
|
||||
fixDecimals={false}
|
||||
stacked
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-right font-mono">{assetWeight}x</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end space-x-1.5">
|
||||
<p className="text-th-up">
|
||||
<FormatNumericValue value={depositRate} decimals={2} />%
|
||||
</p>
|
||||
<span className="text-th-fgd-4">|</span>
|
||||
<p className="text-th-down">
|
||||
<FormatNumericValue value={borrowRate} decimals={2} />%
|
||||
</p>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
|
@ -261,7 +344,6 @@ const SpotMarketsTable = () => {
|
|||
return (
|
||||
<MobileSpotMarketItem
|
||||
key={data.market.publicKey.toString()}
|
||||
loadingMarketData={loadingMarketData}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
|
@ -274,13 +356,7 @@ const SpotMarketsTable = () => {
|
|||
|
||||
export default SpotMarketsTable
|
||||
|
||||
const MobileSpotMarketItem = ({
|
||||
data,
|
||||
loadingMarketData,
|
||||
}: {
|
||||
data: TableData
|
||||
loadingMarketData: boolean
|
||||
}) => {
|
||||
const MobileSpotMarketItem = ({ data }: { data: TableData }) => {
|
||||
const { t } = useTranslation('common')
|
||||
const { theme } = useThemeWrapper()
|
||||
|
||||
|
@ -288,7 +364,7 @@ const MobileSpotMarketItem = ({
|
|||
baseBank,
|
||||
change,
|
||||
market,
|
||||
marketName,
|
||||
tokenName,
|
||||
price,
|
||||
priceHistory,
|
||||
quoteBank,
|
||||
|
@ -308,25 +384,21 @@ const MobileSpotMarketItem = ({
|
|||
<div className="flex flex-shrink-0 items-center">
|
||||
<MarketLogos market={market} />
|
||||
</div>
|
||||
<p className="leading-none text-th-fgd-1">{marketName}</p>
|
||||
<p className="leading-none text-th-fgd-1">{tokenName}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{!loadingMarketData ? (
|
||||
priceHistory && priceHistory.length ? (
|
||||
<div className="h-10 w-20">
|
||||
<SimpleAreaChart
|
||||
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
data={priceHistory}
|
||||
name={baseBank!.name + quoteBank!.name}
|
||||
xKey="time"
|
||||
yKey="price"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
|
||||
)
|
||||
{priceHistory && priceHistory.length ? (
|
||||
<div className="h-10 w-20">
|
||||
<SimpleAreaChart
|
||||
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
|
||||
data={priceHistory}
|
||||
name={baseBank!.name + quoteBank!.name}
|
||||
xKey="time"
|
||||
yKey="price"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
|
||||
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
|
||||
)}
|
||||
<Change change={change} suffix="%" />
|
||||
<ChevronDownIcon
|
|
@ -7,6 +7,7 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|||
maxLength?: number
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
heightClass?: string
|
||||
prefixClassname?: string
|
||||
wrapperClassName?: string
|
||||
hasError?: boolean
|
||||
|
@ -22,6 +23,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
|||
onChange,
|
||||
maxLength,
|
||||
className,
|
||||
heightClass,
|
||||
hasError,
|
||||
wrapperClassName = 'w-full',
|
||||
disabled,
|
||||
|
@ -40,7 +42,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
|||
</div>
|
||||
) : null}
|
||||
<input
|
||||
className={`${className} h-12 w-full flex-1 rounded-md border bg-th-input-bkg px-3 text-base
|
||||
className={`${className} ${
|
||||
heightClass ? heightClass : 'h-12'
|
||||
} w-full flex-1 rounded-md border bg-th-input-bkg px-3 text-base
|
||||
text-th-fgd-1 ${
|
||||
hasError ? 'border-th-down' : 'border-th-input-border'
|
||||
} focus:outline-none
|
||||
|
|
|
@ -204,7 +204,7 @@ const MangoAccountsListModal = ({
|
|||
className="text-th-fgd-3"
|
||||
onClick={() =>
|
||||
handleCopyAddress(
|
||||
acc,
|
||||
acc.publicKey.toString(),
|
||||
t('copy-address-success', {
|
||||
pk: abbreviateAddress(acc.publicKey),
|
||||
}),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import TabButtons from '@components/shared/TabButtons'
|
||||
import TokenPage from '@components/token/TokenPage'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
@ -9,11 +8,9 @@ import { breakpoints } from 'utils/theme'
|
|||
import MangoStats from './mango/MangoStats'
|
||||
import PerpStats from './perps/PerpStats'
|
||||
import PerpStatsPage from './perps/PerpStatsPage'
|
||||
import SpotMarketsTable from './spot/SpotMarketsTable'
|
||||
import TokenStats from './tokens/TokenStats'
|
||||
|
||||
const TABS = ['tokens', 'perp-markets', 'spot-markets', 'mango-stats']
|
||||
const actions = mangoStore.getState().actions
|
||||
const TABS = ['tokens', 'perp-markets', 'mango-stats']
|
||||
|
||||
const StatsPage = () => {
|
||||
const [activeTab, setActiveTab] = useState('tokens')
|
||||
|
@ -22,7 +19,6 @@ const StatsPage = () => {
|
|||
const perpPositionsStatsNotLoaded = mangoStore(
|
||||
(s) => s.perpStats.positions.initialLoad,
|
||||
)
|
||||
const { group } = useMangoGroup()
|
||||
const { width } = useViewport()
|
||||
const fullWidthTabs = width ? width < breakpoints.lg : false
|
||||
const router = useRouter()
|
||||
|
@ -30,23 +26,25 @@ const StatsPage = () => {
|
|||
const { token } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
if (group && (!perpStats || !perpStats.length)) {
|
||||
if (!perpStats || !perpStats.length) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPerpStats()
|
||||
}
|
||||
}, [group, perpStats])
|
||||
}, [perpStats])
|
||||
|
||||
useEffect(() => {
|
||||
if (group && perpPositionsStatsNotLoaded) {
|
||||
if (perpPositionsStatsNotLoaded) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchPositionsStats()
|
||||
}
|
||||
}, [group, perpPositionsStatsNotLoaded])
|
||||
}, [perpPositionsStatsNotLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
if (group && !initialStatsLoad) {
|
||||
if (!initialStatsLoad) {
|
||||
const actions = mangoStore.getState().actions
|
||||
actions.fetchTokenStats()
|
||||
}
|
||||
}, [group, initialStatsLoad])
|
||||
}, [initialStatsLoad])
|
||||
|
||||
const tabsWithCount: [string, number][] = useMemo(() => {
|
||||
return TABS.map((t) => [t, 0])
|
||||
|
@ -83,8 +81,6 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
|
|||
return <TokenStats />
|
||||
case 'perp-markets':
|
||||
return <PerpStats />
|
||||
case 'spot-markets':
|
||||
return <SpotMarketsTable />
|
||||
case 'mango-stats':
|
||||
return <MangoStats />
|
||||
default:
|
||||
|
|
|
@ -11,12 +11,20 @@ import {
|
|||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { getOracleProvider } from 'hooks/useOracleProvider'
|
||||
import { goToPerpMarketDetails } from './PerpMarketsOverviewTable'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextRouter, useRouter } from 'next/router'
|
||||
import { LinkButton } from '@components/shared/Button'
|
||||
import SoonBadge from '@components/shared/SoonBadge'
|
||||
import { useViewport } from 'hooks/useViewport'
|
||||
import { breakpoints } from 'utils/theme'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
|
||||
export const goToPerpMarketDetails = (
|
||||
market: PerpMarket,
|
||||
router: NextRouter,
|
||||
) => {
|
||||
const query = { ...router.query, ['market']: market.name }
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
const PerpMarketDetailsTable = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
|
|
|
@ -3,10 +3,9 @@ import mangoStore from '@store/mangoStore'
|
|||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import SecondaryTabBar from '@components/shared/SecondaryTabBar'
|
||||
import PerpMarketsDetailsTable from './PerpMarketDetailsTable'
|
||||
import PerpMarketsOverviewTable from './PerpMarketsOverviewTable'
|
||||
import PerpMarketsPositions from './PerpMarketsPositions'
|
||||
|
||||
const TABS = ['overview', 'details', 'trade:positions']
|
||||
const TABS = ['details', 'trade:positions']
|
||||
|
||||
const PerpStats = () => {
|
||||
const [activeTab, setActiveTab] = useState(TABS[0])
|
||||
|
@ -34,13 +33,11 @@ const PerpStats = () => {
|
|||
const TabContent = ({ activeTab }: { activeTab: string }) => {
|
||||
switch (activeTab) {
|
||||
case TABS[0]:
|
||||
return <PerpMarketsOverviewTable />
|
||||
case TABS[1]:
|
||||
return <PerpMarketsDetailsTable />
|
||||
case TABS[2]:
|
||||
case TABS[1]:
|
||||
return <PerpMarketsPositions />
|
||||
default:
|
||||
return <PerpMarketsOverviewTable />
|
||||
return <PerpMarketsDetailsTable />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,10 +24,13 @@ import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
|||
import Loading from '@components/shared/Loading'
|
||||
import MarketChange from '@components/shared/MarketChange'
|
||||
import SheenLoader from '@components/shared/SheenLoader'
|
||||
import useListedMarketsWithMarketData, {
|
||||
SerumMarketWithMarketData,
|
||||
} from 'hooks/useListedMarketsWithMarketData'
|
||||
import { AllowedKeys, sortPerpMarkets, sortSpotMarkets } from 'utils/markets'
|
||||
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
|
||||
import {
|
||||
AllowedKeys,
|
||||
sortPerpMarkets,
|
||||
sortSpotMarkets,
|
||||
startSearch,
|
||||
} from 'utils/markets'
|
||||
import Input from '@components/forms/Input'
|
||||
import { useSortableData } from 'hooks/useSortableData'
|
||||
import { SortableColumnHeader } from '@components/shared/TableElements'
|
||||
|
@ -38,38 +41,6 @@ const MARKET_LINK_CLASSES =
|
|||
const MARKET_LINK_DISABLED_CLASSES =
|
||||
'flex w-full items-center justify-between py-2 px-4 md:hover:cursor-not-allowed'
|
||||
|
||||
const generateSearchTerm = (
|
||||
item: SerumMarketWithMarketData,
|
||||
searchValue: string,
|
||||
) => {
|
||||
const normalizedSearchValue = searchValue.toLowerCase()
|
||||
const value = item.name.toLowerCase()
|
||||
|
||||
const isMatchingWithName =
|
||||
item.name.toLowerCase().indexOf(normalizedSearchValue) >= 0
|
||||
const matchingSymbolPercent = isMatchingWithName
|
||||
? normalizedSearchValue.length / item.name.length
|
||||
: 0
|
||||
|
||||
return {
|
||||
token: item,
|
||||
matchingIdx: value.indexOf(normalizedSearchValue),
|
||||
matchingSymbolPercent,
|
||||
}
|
||||
}
|
||||
|
||||
const startSearch = (
|
||||
items: SerumMarketWithMarketData[],
|
||||
searchValue: string,
|
||||
) => {
|
||||
return items
|
||||
.map((item) => generateSearchTerm(item, searchValue))
|
||||
.filter((item) => item.matchingIdx >= 0)
|
||||
.sort((i1, i2) => i1.matchingIdx - i2.matchingIdx)
|
||||
.sort((i1, i2) => i2.matchingSymbolPercent - i1.matchingSymbolPercent)
|
||||
.map((item) => item.token)
|
||||
}
|
||||
|
||||
const MarketSelectDropdown = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
|
|
|
@ -27,8 +27,7 @@
|
|||
"@blockworks-foundation/mango-v4-settings": "0.2.13",
|
||||
"@blockworks-foundation/mangolana": "0.0.1-beta.15",
|
||||
"@headlessui/react": "1.6.6",
|
||||
"@heroicons/react": "2.0.10",
|
||||
"bignumber.js": "9.1.2",
|
||||
"@heroicons/react": "2.0.18",
|
||||
"@metaplex-foundation/js": "0.19.4",
|
||||
"@project-serum/anchor": "0.25.0",
|
||||
"@pythnetwork/client": "2.15.0",
|
||||
|
@ -44,6 +43,7 @@
|
|||
"@tippyjs/react": "4.2.6",
|
||||
"@web3auth/sign-in-with-solana": "1.0.0",
|
||||
"big.js": "6.2.1",
|
||||
"bignumber.js": "9.1.2",
|
||||
"clsx": "1.2.1",
|
||||
"csv-stringify": "6.3.2",
|
||||
"d3-interpolate": "3.0.1",
|
||||
|
@ -72,10 +72,10 @@
|
|||
"react-number-format": "4.9.2",
|
||||
"react-tsparticles": "2.2.4",
|
||||
"recharts": "2.5.0",
|
||||
"three": "^0.155.0",
|
||||
"tsparticles": "2.2.4",
|
||||
"walktour": "5.1.1",
|
||||
"zustand": "4.1.3",
|
||||
"three": "^0.155.0"
|
||||
"zustand": "4.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@project-serum/anchor": "0.25.0",
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import ExplorePage from '@components/explore/ExplorePage'
|
||||
import type { NextPage } from 'next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
export async function getStaticProps({ locale }: { locale: string }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, [
|
||||
'activity',
|
||||
'common',
|
||||
'explore',
|
||||
'notifications',
|
||||
'onboarding',
|
||||
'profile',
|
||||
'search',
|
||||
'settings',
|
||||
'stats',
|
||||
'token',
|
||||
'trade',
|
||||
])),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const Explore: NextPage = () => {
|
||||
return <ExplorePage />
|
||||
}
|
||||
|
||||
export default Explore
|
|
@ -79,6 +79,7 @@
|
|||
"enable-notifications": "Enable Notifications",
|
||||
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
|
||||
"error-token-positions-full": "Not enough token positions available in your account.",
|
||||
"explore": "Explore",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"gainers": "Gainers",
|
||||
"losers": "Losers",
|
||||
"recently-listed": "Recently Listed"
|
||||
}
|
|
@ -79,6 +79,7 @@
|
|||
"enable-notifications": "Enable Notifications",
|
||||
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
|
||||
"error-token-positions-full": "Not enough token positions available in your account.",
|
||||
"explore": "Explore",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"gainers": "Gainers",
|
||||
"losers": "Losers",
|
||||
"recently-listed": "Recently Listed"
|
||||
}
|
|
@ -79,6 +79,7 @@
|
|||
"enable-notifications": "Enable Notifications",
|
||||
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
|
||||
"error-token-positions-full": "Not enough token positions available in your account.",
|
||||
"explore": "Explore",
|
||||
"explorer": "Explorer",
|
||||
"fee": "Fee",
|
||||
"feedback-survey": "Feedback Survey",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"gainers": "Gainers",
|
||||
"losers": "Losers",
|
||||
"recently-listed": "Recently Listed"
|
||||
}
|
|
@ -79,6 +79,7 @@
|
|||
"enable-notifications": "Enable Notifications",
|
||||
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
|
||||
"error-token-positions-full": "你帐户的币位已占满",
|
||||
"explore": "Explore",
|
||||
"explorer": "浏览器",
|
||||
"fee": "费用",
|
||||
"feedback-survey": "反馈调查",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"gainers": "Gainers",
|
||||
"losers": "Losers",
|
||||
"recently-listed": "Recently Listed"
|
||||
}
|
|
@ -79,6 +79,7 @@
|
|||
"enable-notifications": "Enable Notifications",
|
||||
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
|
||||
"error-token-positions-full": "你帳戶的幣位已占滿",
|
||||
"explore": "Explore",
|
||||
"explorer": "瀏覽器",
|
||||
"fee": "費用",
|
||||
"feedback-survey": "反饋調查",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"gainers": "Gainers",
|
||||
"losers": "Losers",
|
||||
"recently-listed": "Recently Listed"
|
||||
}
|
|
@ -58,3 +58,35 @@ export const sortPerpMarkets = (
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
const generateSearchTerm = (
|
||||
item: SerumMarketWithMarketData,
|
||||
searchValue: string,
|
||||
) => {
|
||||
const normalizedSearchValue = searchValue.toLowerCase()
|
||||
const value = item.name.toLowerCase()
|
||||
|
||||
const isMatchingWithName =
|
||||
item.name.toLowerCase().indexOf(normalizedSearchValue) >= 0
|
||||
const matchingSymbolPercent = isMatchingWithName
|
||||
? normalizedSearchValue.length / item.name.length
|
||||
: 0
|
||||
|
||||
return {
|
||||
token: item,
|
||||
matchingIdx: value.indexOf(normalizedSearchValue),
|
||||
matchingSymbolPercent,
|
||||
}
|
||||
}
|
||||
|
||||
export const startSearch = (
|
||||
items: SerumMarketWithMarketData[],
|
||||
searchValue: string,
|
||||
) => {
|
||||
return items
|
||||
.map((item) => generateSearchTerm(item, searchValue))
|
||||
.filter((item) => item.matchingIdx >= 0)
|
||||
.sort((i1, i2) => i1.matchingIdx - i2.matchingIdx)
|
||||
.sort((i1, i2) => i2.matchingSymbolPercent - i1.matchingSymbolPercent)
|
||||
.map((item) => item.token)
|
||||
}
|
||||
|
|
|
@ -568,10 +568,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.6.tgz#3073c066b85535c9d28783da0a4d9288b5354d0c"
|
||||
integrity sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==
|
||||
|
||||
"@heroicons/react@2.0.10":
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.10.tgz#191a305aa2dc2271903f027c9f4700ca3dfa9e7b"
|
||||
integrity sha512-Ufr+pgAElNiRCSklnHGOR10bXb02BLlosvbDK7sCRUMOcQ3R/HCXTfXs4BUkYZ4dKpx6l5dUD06VSW1dTpTEDw==
|
||||
"@heroicons/react@2.0.18":
|
||||
version "2.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.18.tgz#f80301907c243df03c7e9fd76c0286e95361f7c1"
|
||||
integrity sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.5"
|
||||
|
|
Loading…
Reference in New Issue