import Change from '@components/shared/Change' import FormatNumericValue from '@components/shared/FormatNumericValue' import TokenLogo from '@components/shared/TokenLogo' import useListedMarketsWithMarketData, { SerumMarketWithMarketData, } from 'hooks/useListedMarketsWithMarketData' import useMangoGroup from 'hooks/useMangoGroup' import { useRouter } from 'next/router' import { ChangeEvent, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import SpotTable from './SpotTable' import { goToTokenPage } from '@components/stats/tokens/TokenOverviewTable' import { BoltIcon, ChevronRightIcon, FaceFrownIcon, MagnifyingGlassIcon, RocketLaunchIcon, Squares2X2Icon, TableCellsIcon, } from '@heroicons/react/20/solid' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { AllowedKeys } from 'utils/markets' import ButtonGroup from '@components/forms/ButtonGroup' import SpotCards from './SpotCards' import Input from '@components/forms/Input' import EmptyState from '@components/nftMarket/EmptyState' import { Bank } from '@blockworks-foundation/mango-v4' import Link from 'next/link' import useBanks from 'hooks/useBanks' import SheenLoader from '@components/shared/SheenLoader' import mangoStore from '@store/mangoStore' dayjs.extend(relativeTime) export type BankWithMarketData = { bank: Bank market: SerumMarketWithMarketData | undefined } const CALLOUT_TILES_WRAPPER_CLASSES = 'col-span-12 flex flex-col rounded-lg border border-th-bkg-3 p-6 lg:col-span-4' const generateSearchTerm = (item: BankWithMarketData, searchValue: string) => { const normalizedSearchValue = searchValue.toLowerCase() const value = item.bank.name.toLowerCase() const isMatchingWithName = item.bank.name.toLowerCase().indexOf(normalizedSearchValue) >= 0 const matchingSymbolPercent = isMatchingWithName ? normalizedSearchValue.length / item.bank.name.length : 0 return { token: item, matchingIdx: value.indexOf(normalizedSearchValue), matchingSymbolPercent, } } const startSearch = (items: BankWithMarketData[], 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 sortTokens = (tokens: BankWithMarketData[], sortByKey: AllowedKeys) => { return tokens.sort((a: BankWithMarketData, b: BankWithMarketData) => { const aValue: number | undefined = a?.market?.marketData?.[sortByKey] const bValue: number | undefined = b?.market?.marketData?.[sortByKey] // Handle marketData[sortByKey] is undefined if (typeof aValue === 'undefined' && typeof bValue === 'undefined') { return 0 // Consider them equal } if (typeof aValue === 'undefined') { return 1 // b should come before a } if (typeof bValue === 'undefined') { return -1 // a should come before b } return bValue - aValue }) } const Spot = () => { const { t } = useTranslation(['common', 'explore', 'trade']) const router = useRouter() const { group } = useMangoGroup() const { banks } = useBanks() const { serumMarketsWithData, isLoading: loadingSerumMarkets } = useListedMarketsWithMarketData() const groupLoaded = mangoStore((s) => s.groupLoaded) const [sortByKey, setSortByKey] = useState('quote_volume_24h') const [search, setSearch] = useState('') const [showTableView, setShowTableView] = useState(true) const banksWithMarketData = useMemo(() => { if (!banks.length || !group || !serumMarketsWithData.length) return [] const banksWithMarketData = [] const usdcQuoteMarkets = serumMarketsWithData.filter( (market) => market.quoteTokenIndex === 0, ) for (const bank of banks) { const market = usdcQuoteMarkets.find( (market) => market.baseTokenIndex === bank.tokenIndex, ) if (market) { banksWithMarketData.push({ bank, market }) } else { banksWithMarketData.push({ bank, market: undefined }) } } return banksWithMarketData }, [banks, group, serumMarketsWithData]) const newlyListedMintInfo = useMemo(() => { if (!group) return [] const mintInfos = Array.from(group.mintInfosMapByTokenIndex).map( ([, mintInfo]) => mintInfo, ) const sortByRegistrationTime = mintInfos .sort((a, b) => { return b.registrationTime.toNumber() - a.registrationTime.toNumber() }) .slice(0, 3) return sortByRegistrationTime }, [group]) const newlyListed = useMemo(() => { if (!newlyListedMintInfo.length || !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 sortedTokensToShow = useMemo(() => { if (!banksWithMarketData.length) return [] return search ? startSearch(banksWithMarketData, search) : sortTokens(banksWithMarketData, sortByKey) }, [search, banksWithMarketData, sortByKey, showTableView]) const handleUpdateSearch = (e: ChangeEvent) => { setSearch(e.target.value) } return ( <>

{t('explore:recently-listed')}

{t('governance:list-token')}
{groupLoaded ? (
{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 (
goToTokenPage(bank.name.split(' ')[0], router) } >

{bank.name}

{timeSinceListing}
) })}
) : ( )}

{t('explore:gainers')}

{!loadingSerumMarkets && groupLoaded ? (
{gainers.length ? ( gainers.map((gainer) => { const bank = group?.getFirstBankByTokenIndex( gainer.baseTokenIndex, ) if (!bank) return null return (
goToTokenPage(bank.name.split(' ')[0], router) } >

{bank.name}

) }) ) : (

{t('explore:no-gainers')}

)}
) : ( )}

{t('explore:losers')}

{!loadingSerumMarkets && groupLoaded ? (
{losers.length ? ( losers.map((loser) => { const bank = group?.getFirstBankByTokenIndex( loser.baseTokenIndex, ) if (!bank) return null return (
goToTokenPage(bank.name.split(' ')[0], router) } >

{bank.name}

) }) ) : (

{t('explore:no-losers')}

)}
) : ( )}

{t('tokens')}

setSortByKey(v)} names={[t('trade:24h-volume'), t('rolling-change')]} values={['quote_volume_24h', 'change_24h']} />
{sortedTokensToShow.length ? ( showTableView ? (
) : ( ) ) : (
)}
) } export default Spot const CalloutTilesLoader = () => { return (
{[...Array(3)].map((x, i) => (
))}
) }