add explore page

This commit is contained in:
saml33 2023-10-04 16:05:16 +11:00
parent 3f8f9f0877
commit 372876f0de
29 changed files with 840 additions and 205 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

275
components/explore/Spot.tsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

29
pages/explore.tsx Normal file
View File

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

View File

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

View File

@ -0,0 +1,5 @@
{
"gainers": "Gainers",
"losers": "Losers",
"recently-listed": "Recently Listed"
}

View File

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

View File

@ -0,0 +1,5 @@
{
"gainers": "Gainers",
"losers": "Losers",
"recently-listed": "Recently Listed"
}

View File

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

View File

@ -0,0 +1,5 @@
{
"gainers": "Gainers",
"losers": "Losers",
"recently-listed": "Recently Listed"
}

View File

@ -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": "反馈调查",

View File

@ -0,0 +1,5 @@
{
"gainers": "Gainers",
"losers": "Losers",
"recently-listed": "Recently Listed"
}

View File

@ -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": "反饋調查",

View File

@ -0,0 +1,5 @@
{
"gainers": "Gainers",
"losers": "Losers",
"recently-listed": "Recently Listed"
}

View File

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

View File

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