Merge pull request #416 from blockworks-foundation/finn/prospective_dashboard
Prospective Tokens
This commit is contained in:
commit
b9f1472a13
|
@ -0,0 +1,71 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const { offset = 0 } = req.query
|
||||||
|
|
||||||
|
// Check if the API key is defined or provide a default/fallback value
|
||||||
|
const apiKey =
|
||||||
|
process.env.NEXT_PUBLIC_BIRDEYE_API_KEY || 'your-default-api-key' // Consider a fallback if environment variable might not be set
|
||||||
|
const options = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': apiKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the initial list of tokens
|
||||||
|
const response = await fetch(
|
||||||
|
`https://public-api.birdeye.so/defi/tokenlist?sort_by=v24hUSD&sort_type=desc&offset=${offset}&limit=50`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
const tokenListResponse = await response.json()
|
||||||
|
|
||||||
|
const tokenList = tokenListResponse['data']['tokens']
|
||||||
|
const filteredTokens = []
|
||||||
|
|
||||||
|
// Loop through each token to check additional criteria
|
||||||
|
for (const token of tokenList) {
|
||||||
|
const { address, v24hUSD } = token
|
||||||
|
if (v24hUSD > 50000) {
|
||||||
|
const now = new Date()
|
||||||
|
const nowInSeconds = Math.floor(now.getTime() / 1000)
|
||||||
|
const pastDate = new Date()
|
||||||
|
pastDate.setDate(pastDate.getDate() - 4)
|
||||||
|
pastDate.setMonth(pastDate.getMonth() - 1)
|
||||||
|
const pastDateInSeconds = Math.floor(pastDate.getTime() / 1000)
|
||||||
|
|
||||||
|
// Fetch history for the token
|
||||||
|
const historyResponse = await fetch(
|
||||||
|
`https://public-api.birdeye.so/defi/history_price?address=${address}&address_type=token&type=1D&time_from=${pastDateInSeconds}&time_to=${nowInSeconds}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
const historyData = await historyResponse.json()
|
||||||
|
|
||||||
|
if (historyData['data']['items']?.length >= 35) {
|
||||||
|
const detailResponse = await fetch(
|
||||||
|
`https://public-api.birdeye.so/defi/token_overview?address=${address}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
const tokenDetails = await detailResponse.json()
|
||||||
|
if (
|
||||||
|
tokenDetails['data']['mc'] > 15000000 &&
|
||||||
|
tokenDetails['data']['numberMarkets'] > 10
|
||||||
|
) {
|
||||||
|
// market cap greater than 1 mil
|
||||||
|
filteredTokens.push(tokenDetails['data'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the filtered list of tokens
|
||||||
|
res.status(200).json(filteredTokens)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tokens:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to fetch tokens' })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1380,6 +1380,19 @@ export const DashboardNavbar = () => {
|
||||||
</h4>
|
</h4>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link href={'/dashboard/prospective'} shallow={true}>
|
||||||
|
<h4
|
||||||
|
className={`${
|
||||||
|
asPath.includes('/dashboard/prospective')
|
||||||
|
? 'bg-th-bkg-2 text-th-active'
|
||||||
|
: ''
|
||||||
|
} cursor-pointer border-r border-th-bkg-3 px-6 py-4`}
|
||||||
|
>
|
||||||
|
Prospective
|
||||||
|
</h4>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { DashboardNavbar } from '.'
|
||||||
|
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
|
||||||
|
import { useState, useMemo, useEffect } from 'react'
|
||||||
|
import { LinkButton } from '@components/shared/Button'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import Loading from '@components/shared/Loading'
|
||||||
|
import useBanks from 'hooks/useBanks'
|
||||||
|
|
||||||
|
interface Token {
|
||||||
|
symbol: string // Every token will definitely have a symbol.
|
||||||
|
[key: string]: string // Other properties are unknown and can vary.
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTokens = async (
|
||||||
|
offset = 0,
|
||||||
|
existingTokens: Token[] = [],
|
||||||
|
): Promise<Token[]> => {
|
||||||
|
const res = await fetch(`/api/tokens?offset=${offset * 50}&limit=50`)
|
||||||
|
const data = await res.json()
|
||||||
|
return [...existingTokens, ...data] // Ensure the returned data conforms to Token[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const Prospective: NextPage = () => {
|
||||||
|
const banks = useBanks()['banks']
|
||||||
|
const bankNames = banks.map((bank) => bank.name.toUpperCase())
|
||||||
|
|
||||||
|
// Generate headers from the first token entry if available
|
||||||
|
const [tokensList, setTokensList] = useState<Token[]>([])
|
||||||
|
const [offset, setOffset] = useState<number>(0)
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialFetch = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const initialTokens: Token[] = await fetchTokens(0)
|
||||||
|
setTokensList(initialTokens)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tokens:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialFetch()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleShowMore = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const newTokens = await fetchTokens(offset + 1, tokensList)
|
||||||
|
setTokensList(newTokens)
|
||||||
|
setOffset((prevOffset) => prevOffset + 1)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch more tokens:', error)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const heads = useMemo(() => {
|
||||||
|
if (tokensList.length > 0) {
|
||||||
|
const allKeys = Object.keys(tokensList[0])
|
||||||
|
const filteredKeys = allKeys.filter(
|
||||||
|
(key) =>
|
||||||
|
![
|
||||||
|
'symbol',
|
||||||
|
'extensions',
|
||||||
|
'lastTradeUnixTime',
|
||||||
|
'uniqueWallet30m',
|
||||||
|
].includes(key),
|
||||||
|
)
|
||||||
|
return ['symbol', ...filteredKeys]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}, [tokensList])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-span-12 w-full lg:col-span-8 lg:col-start-3">
|
||||||
|
<DashboardNavbar />
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="mx-20 mb-4">
|
||||||
|
<p className="flex items-center space-x-4 text-th-fgd-4">
|
||||||
|
<span>Hidden Gems to Prospect On</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full overflow-scroll" style={{ maxHeight: '70vh' }}>
|
||||||
|
<Table className="h-full">
|
||||||
|
<thead>
|
||||||
|
<TrHead
|
||||||
|
style={{ boxShadow: '1px -5px 1px rgba(0,0,0,1)', zIndex: 19 }}
|
||||||
|
className="sticky top-0 border-t bg-th-bkg-2"
|
||||||
|
>
|
||||||
|
{heads.map((head, index) => (
|
||||||
|
<Th
|
||||||
|
key={head}
|
||||||
|
className="text-left"
|
||||||
|
style={{
|
||||||
|
borderLeft: index !== 0 ? '1px solid #ccc' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{head}
|
||||||
|
</Th>
|
||||||
|
))}
|
||||||
|
</TrHead>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tokensList.map((token: Token, idx) => (
|
||||||
|
<TrBody
|
||||||
|
key={idx}
|
||||||
|
className="h-10 cursor-pointer text-xs md:hover:bg-th-bkg-2"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`https://birdeye.so/token/${token['address']}?chain=solana&tab=recentTrades`,
|
||||||
|
'_blank',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{heads.map((head, valIdx) => {
|
||||||
|
if (head == 'symbol') {
|
||||||
|
token[head] = token[head]?.toUpperCase().toString()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
xBorder={valIdx != 0}
|
||||||
|
key={valIdx}
|
||||||
|
className={`!py-3
|
||||||
|
${
|
||||||
|
valIdx === 0
|
||||||
|
? 'sticky left-0 z-10 ' +
|
||||||
|
(bankNames.includes(token[head])
|
||||||
|
? 'bg-lime-200'
|
||||||
|
: 'bg-th-bkg-2')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="mr-2 h-full">
|
||||||
|
{head == 'logoURI' ? (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={token[head]}
|
||||||
|
alt="Token Logo"
|
||||||
|
width={100} // Set the desired width
|
||||||
|
height={100} // Set the desired height
|
||||||
|
unoptimized={true} // Use this only if you face issues with the Next.js optimizer
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{token[head] ? token[head]?.toString() : 'N/A'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TrBody>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{loading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '5vh', // Match the height of the table container
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loading className="mr-2 h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center py-6">
|
||||||
|
<LinkButton onClick={handleShowMore}>Show More</LinkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Prospective
|
Loading…
Reference in New Issue