mango-v4-ui/components/trade/MarketSelectDropdown.tsx

316 lines
13 KiB
TypeScript
Raw Normal View History

2023-02-22 11:04:18 -08:00
// import ChartRangeButtons from '@components/shared/ChartRangeButtons'
2023-04-03 19:48:31 -07:00
import Change from '@components/shared/Change'
2022-11-30 07:46:20 -08:00
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
2023-04-03 19:48:31 -07:00
import SheenLoader from '@components/shared/SheenLoader'
2023-05-02 10:47:26 -07:00
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
2022-11-30 07:46:20 -08:00
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
2023-04-03 19:48:31 -07:00
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
import useMangoGroup from 'hooks/useMangoGroup'
2022-11-30 07:46:20 -08:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2023-02-22 11:04:18 -08:00
import { useTranslation } from 'next-i18next'
2022-11-30 07:46:20 -08:00
import Link from 'next/link'
2023-06-18 22:39:18 -07:00
import { useMemo, useState } from 'react'
import {
floorToDecimal,
formatCurrencyValue,
formatNumericValue,
getDecimalCount,
} from 'utils/numbers'
2022-11-30 07:46:20 -08:00
import MarketLogos from './MarketLogos'
2023-06-14 17:43:33 -07:00
import SoonBadge from '@components/shared/SoonBadge'
2023-06-18 22:39:18 -07:00
import TabButtons from '@components/shared/TabButtons'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
2023-06-29 21:00:28 -07:00
import Loading from '@components/shared/Loading'
2022-11-30 07:46:20 -08:00
2023-05-03 18:41:18 -07:00
const MARKET_LINK_WRAPPER_CLASSES =
'flex items-center justify-between px-4 md:pl-6 md:pr-4'
const MARKET_LINK_CLASSES =
2023-06-18 22:39:18 -07:00
'mr-1 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
2023-05-03 18:41:18 -07:00
const MARKET_LINK_DISABLED_CLASSES =
'mr-2 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 md:hover:cursor-not-allowed'
2022-11-30 07:46:20 -08:00
const MarketSelectDropdown = () => {
2023-02-22 11:04:18 -08:00
const { t } = useTranslation('common')
2022-11-30 07:46:20 -08:00
const { selectedMarket } = useSelectedMarket()
const [spotOrPerp, setSpotOrPerp] = useState(
selectedMarket instanceof PerpMarket ? 'perp' : 'spot'
)
2022-11-30 07:46:20 -08:00
const serumMarkets = mangoStore((s) => s.serumMarkets)
2023-01-19 20:17:43 -08:00
const allPerpMarkets = mangoStore((s) => s.perpMarkets)
2023-04-03 19:48:31 -07:00
const perpStats = mangoStore((s) => s.perpStats.data)
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const { group } = useMangoGroup()
const { data: birdeyePrices, isLoading: loadingPrices } =
useBirdeyeMarketPrices()
2023-06-18 22:39:18 -07:00
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
2022-11-30 07:46:20 -08:00
2023-01-19 20:17:43 -08:00
const perpMarkets = useMemo(() => {
2023-02-13 16:37:23 -08:00
return allPerpMarkets
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw'
)
.sort((a, b) =>
2023-06-17 02:55:34 -07:00
a.oracleLastUpdatedSlot == 0 ? -1 : a.name.localeCompare(b.name)
)
2023-01-19 20:17:43 -08:00
}, [allPerpMarkets])
2023-06-18 22:39:18 -07:00
const spotBaseTokens: string[] = useMemo(() => {
if (serumMarkets.length) {
const baseTokens: string[] = ['All']
serumMarkets.map((m) => {
const base = m.name.split('/')[1]
if (!baseTokens.includes(base)) {
baseTokens.push(base)
}
})
return baseTokens.sort((a, b) => a.localeCompare(b))
}
return ['All']
}, [serumMarkets])
const serumMarketsToShow = useMemo(() => {
if (!serumMarkets || !serumMarkets.length) return []
if (spotBaseFilter !== 'All') {
return serumMarkets.filter((m) => {
const base = m.name.split('/')[1]
return base === spotBaseFilter
})
} else {
return serumMarkets
}
}, [serumMarkets, spotBaseFilter])
2022-11-30 07:46:20 -08:00
return (
<Popover>
2023-02-22 11:04:18 -08:00
{({ open, close }) => (
2022-11-30 07:46:20 -08:00
<div
className="relative flex flex-col overflow-visible md:-ml-2"
2022-11-30 07:46:20 -08:00
id="trade-step-one"
>
2023-06-29 21:00:28 -07:00
<Popover.Button
className="-ml-4 flex h-12 items-center justify-between px-4 focus-visible:bg-th-bkg-3 disabled:cursor-not-allowed disabled:opacity-60 md:hover:bg-th-bkg-2 disabled:md:hover:bg-th-bkg-1"
disabled={!group}
>
2022-12-06 21:25:37 -08:00
<div className="flex items-center">
2023-06-29 21:00:28 -07:00
{selectedMarket ? (
<MarketLogos market={selectedMarket} />
) : (
<Loading className="mr-2 h-5 w-5 flex-shrink-0" />
)}
2022-12-06 21:25:37 -08:00
<div className="whitespace-nowrap text-xl font-bold text-th-fgd-1 md:text-base">
2023-06-29 21:00:28 -07:00
{selectedMarket?.name || (
<span className="text-th-fgd-3">{t('loading')}</span>
)}
2022-12-06 21:25:37 -08:00
</div>
2022-11-30 07:46:20 -08:00
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
2022-12-06 21:25:37 -08:00
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-2`}
2022-11-30 07:46:20 -08:00
/>
</Popover.Button>
2023-06-18 22:39:18 -07:00
<Popover.Panel className="absolute -left-4 top-12 z-40 mr-4 w-screen rounded-none border-y border-r border-th-bkg-3 bg-th-bkg-2 md:-left-6 md:w-[420px] md:rounded-br-md">
<div className="border-b border-th-bkg-3">
<TabButtons
activeValue={spotOrPerp}
onChange={(v) => setSpotOrPerp(v)}
values={[
['perp', 0],
['spot', 0],
]}
fillWidth
/>
</div>
<div className="py-3">
{spotOrPerp === 'perp' && perpMarkets?.length
? perpMarkets.map((m) => {
const changeData = getOneDayPerpStats(perpStats, m.name)
const isComingSoon = m.oracleLastUpdatedSlot == 0
2023-04-03 19:48:31 -07:00
2023-06-18 22:39:18 -07:00
const change = changeData.length
? ((m.uiPrice - changeData[0].price) /
changeData[0].price) *
100
: 0
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
>
{!isComingSoon ? (
<>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
2023-06-20 16:40:51 -07:00
onClick={() => {
close()
}}
2023-06-18 22:39:18 -07:00
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="text-th-fgd-2">{m.name}</span>
</div>
<div className="flex items-center">
<span className="mr-3 font-mono text-xs text-th-fgd-2">
{formatCurrencyValue(
m.uiPrice,
getDecimalCount(m.tickSize)
)}
</span>
{!loadingPerpStats ? (
<Change
change={change}
suffix="%"
size="small"
/>
) : (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
)}
</div>
</Link>
<FavoriteMarketButton market={m} />
</>
) : (
<span className={MARKET_LINK_DISABLED_CLASSES}>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="mr-2">{m.name}</span>
<SoonBadge />
</div>
</span>
)}
</div>
)
})
: null}
{spotOrPerp === 'spot' && serumMarkets?.length ? (
<>
<div className="mb-3 px-4 md:px-6">
{spotBaseTokens.map((tab) => (
<button
className={`rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
spotBaseFilter === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`}
onClick={() => setSpotBaseFilter(tab)}
key={tab}
>
{t(tab)}
</button>
))}
</div>
{serumMarketsToShow
.map((x) => x)
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => {
const birdeyeData = birdeyePrices?.length
? birdeyePrices.find(
(market) =>
market.mint === m.serumMarketExternal.toString()
)
: null
const baseBank = group?.getFirstBankByTokenIndex(
m.baseTokenIndex
)
const quoteBank = group?.getFirstBankByTokenIndex(
m.quoteTokenIndex
)
const market = group?.getSerum3ExternalMarket(
m.serumMarketExternal
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize)
).toNumber()
}
const change =
birdeyeData && price
? ((price - birdeyeData.data[0].value) /
birdeyeData.data[0].value) *
100
: 0
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
2023-06-20 16:40:51 -07:00
onClick={() => {
close()
}}
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
2023-06-18 22:39:18 -07:00
<span className="text-th-fgd-2">{m.name}</span>
</div>
<div className="flex items-center">
{price && market?.tickSize ? (
<span className="mr-3 font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{getDecimalCount(market.tickSize) <= 6
? formatNumericValue(
price,
getDecimalCount(market.tickSize)
)
: price.toExponential(3)}{' '}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{quoteBank?.name}
</span>
) : null}
</span>
) : null}
{!loadingPrices ? (
change ? (
<Change
change={change}
suffix="%"
size="small"
/>
) : (
<span className="text-th-fgd-3"></span>
)
) : (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
)}
</div>
</Link>
<FavoriteMarketButton market={m} />
2023-06-18 22:39:18 -07:00
</div>
)
})}
</>
) : null}
</div>
2022-11-30 07:46:20 -08:00
</Popover.Panel>
</div>
)}
</Popover>
)
}
export default MarketSelectDropdown