mango-v4-ui/components/explore/Spot.tsx

200 lines
7.0 KiB
TypeScript
Raw Normal View History

2023-10-04 04:52:39 -07:00
import useListedMarketsWithMarketData, {
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
2023-10-03 22:05:16 -07:00
import useMangoGroup from 'hooks/useMangoGroup'
2023-10-04 04:52:39 -07:00
import { ChangeEvent, useMemo, useState } from 'react'
2023-10-03 22:05:16 -07:00
import { useTranslation } from 'react-i18next'
2023-10-04 04:52:39 -07:00
import SpotTable from './SpotTable'
2023-10-03 22:05:16 -07:00
import {
MagnifyingGlassIcon,
Squares2X2Icon,
TableCellsIcon,
} from '@heroicons/react/20/solid'
2023-10-04 04:52:39 -07:00
import { AllowedKeys } from 'utils/markets'
2023-10-03 22:05:16 -07:00
import ButtonGroup from '@components/forms/ButtonGroup'
2023-10-04 04:52:39 -07:00
import SpotCards from './SpotCards'
2023-10-03 22:05:16 -07:00
import Input from '@components/forms/Input'
import EmptyState from '@components/nftMarket/EmptyState'
2023-10-04 04:52:39 -07:00
import { Bank } from '@blockworks-foundation/mango-v4'
2023-10-04 16:04:51 -07:00
import useBanks from 'hooks/useBanks'
2023-12-18 01:08:58 -08:00
import SheenLoader from '@components/shared/SheenLoader'
2023-10-03 22:05:16 -07:00
2023-10-04 04:52:39 -07:00
export type BankWithMarketData = {
bank: Bank
market: SerumMarketWithMarketData | undefined
}
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) => {
2023-10-06 03:33:15 -07:00
let aValue: number | undefined
let bValue: number | undefined
if (sortByKey === 'change_24h') {
const aPrice = a?.bank?.uiPrice || 0
const bPrice = b?.bank?.uiPrice || 0
const aPastPrice = a.market?.marketData?.price_24h
const bPastPrice = b.market?.marketData?.price_24h
const aVolume = a.market?.marketData?.quote_volume_24h || 0
const bVolume = b.market?.marketData?.quote_volume_24h || 0
aValue =
aVolume > 0 && aPastPrice
? ((aPrice - aPastPrice) / aPastPrice) * 100
: undefined
bValue =
bVolume > 0 && bPastPrice
? ((bPrice - bPastPrice) / bPastPrice) * 100
: undefined
} else {
aValue = a?.market?.marketData?.[sortByKey]
bValue = b?.market?.marketData?.[sortByKey]
}
2023-10-04 04:52:39 -07:00
// 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
})
}
2023-10-03 22:05:16 -07:00
const Spot = () => {
const { t } = useTranslation(['common', 'explore', 'trade'])
const { group } = useMangoGroup()
2023-10-04 16:04:51 -07:00
const { banks } = useBanks()
2023-12-18 01:08:58 -08:00
const { serumMarketsWithData, isLoading: loadingMarketsData } =
useListedMarketsWithMarketData()
2023-10-03 22:05:16 -07:00
const [sortByKey, setSortByKey] = useState<AllowedKeys>('quote_volume_24h')
const [search, setSearch] = useState('')
2023-10-04 16:04:51 -07:00
const [showTableView, setShowTableView] = useState(true)
2023-10-03 22:05:16 -07:00
2023-10-04 04:52:39 -07:00
const banksWithMarketData = useMemo(() => {
2023-10-04 16:04:51 -07:00
if (!banks.length || !group || !serumMarketsWithData.length) return []
2023-10-04 04:52:39 -07:00
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
2023-10-04 16:04:51 -07:00
}, [banks, group, serumMarketsWithData])
2023-10-04 04:52:39 -07:00
const sortedTokensToShow = useMemo(() => {
if (!banksWithMarketData.length) return []
2023-10-03 22:05:16 -07:00
return search
2023-10-04 04:52:39 -07:00
? startSearch(banksWithMarketData, search)
: sortTokens(banksWithMarketData, sortByKey)
}, [search, banksWithMarketData, sortByKey, showTableView])
2023-10-03 22:05:16 -07:00
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
return (
2023-11-10 03:36:35 -08:00
<div className="lg:-mt-10">
2023-11-14 15:53:34 -08:00
<div className="flex flex-col px-4 md:px-6 lg:flex-row lg:items-center lg:justify-end 2xl:px-12">
2023-11-10 03:36:35 -08:00
<div className="flex w-full flex-col lg:w-auto lg:flex-row lg:space-x-3">
2023-11-14 15:53:34 -08:00
<div className="relative mb-3 w-full lg:mb-0 lg:w-40">
2023-10-17 04:28:43 -07:00
<Input
heightClass="h-10 pl-8"
type="text"
value={search}
onChange={handleUpdateSearch}
/>
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4" />
2023-10-03 22:05:16 -07:00
</div>
2023-10-17 04:28:43 -07:00
<div className="flex space-x-3">
2023-11-10 03:36:35 -08:00
<div className="w-full lg:w-48">
2023-10-17 04:28:43 -07:00
<ButtonGroup
activeValue={sortByKey}
onChange={(v) => setSortByKey(v)}
names={[t('trade:24h-volume'), t('rolling-change')]}
values={['quote_volume_24h', 'change_24h']}
/>
2023-10-05 18:28:39 -07:00
</div>
2023-10-17 04:28:43 -07:00
<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)}
>
<TableCellsIcon 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)}
>
<Squares2X2Icon className="h-5 w-5" />
</button>
2023-10-05 18:28:39 -07:00
</div>
2023-10-03 22:05:16 -07:00
</div>
</div>
</div>
2023-12-18 01:08:58 -08:00
{loadingMarketsData ? (
<div className="mx-4 my-6 space-y-1 md:mx-6">
{[...Array(4)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : sortedTokensToShow.length ? (
2023-10-17 04:28:43 -07:00
showTableView ? (
<div className="mt-6 border-t border-th-bkg-3">
<SpotTable tokens={sortedTokensToShow} />
2023-10-03 22:05:16 -07:00
</div>
) : (
2023-10-17 04:28:43 -07:00
<SpotCards tokens={sortedTokensToShow} />
)
) : (
<div className="px-4 pt-2 md:px-6 2xl:px-12">
<EmptyState text="No results found..." />
</div>
)}
</div>
2023-10-03 22:05:16 -07:00
)
}
export default Spot