render links to token pages on server for indexing

This commit is contained in:
saml33 2024-03-12 10:22:18 +11:00
parent df079a5994
commit 27013aa7f8
8 changed files with 253 additions and 287 deletions

View File

@ -1,10 +1,6 @@
'use client'
import { useMemo, useState } from 'react'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { MangoTokenData } from '../../types/mango'
import CategorySwitcher from './CategorySwitcher'
import TableViewToggle from './TableViewToggle'
import TokenTable from './TokenTable'
import { TokenCategoryPage } from '../../../contentful/tokenCategoryPage'
import { sortTokens } from './ExploreTokens'
@ -22,12 +18,12 @@ const Category = ({
tokensForCategory: TokenPageWithData[]
mangoTokensData: MangoTokenData[]
}) => {
const [showTableView, setShowTableView] = useState(true)
// const [showTableView, setShowTableView] = useState(true)
const { category, slug } = categoryPageData
const sortedTokens = useMemo(() => {
return sortTokens(tokensForCategory)
}, [tokensForCategory])
// const sortedTokens = useMemo(() => {
// return sortTokens(tokensForCategory)
// }, [tokensForCategory])
const backgroundImageUrl = `/images/categories/${slug}.webp`
@ -42,21 +38,20 @@ const Category = ({
className={`px-6 lg:px-20 ${MAX_CONTENT_WIDTH} mx-auto py-10 md:py-16`}
>
<div className="mb-4 flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between">
<p>{`${sortedTokens?.length} ${category} ${
sortedTokens?.length === 1 ? 'token' : 'tokens'
<p>{`${tokensForCategory?.length} ${category} ${
tokensForCategory?.length === 1 ? 'token' : 'tokens'
} listed on Mango`}</p>
<div className="flex space-x-2 mb-6 sm:mb-0">
<CategorySwitcher categories={categoryPages} />
<TableViewToggle
{/* <TableViewToggle
showTableView={showTableView}
setShowTableView={setShowTableView}
/>
/> */}
</div>
</div>
<TokenTable
tokens={sortedTokens}
tokens={sortTokens(tokensForCategory)}
mangoTokensData={mangoTokensData}
showTableView={showTableView}
/>
</div>
</>

View File

@ -1,3 +1,4 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import Select from '../forms/Select'
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'

View File

@ -1,51 +1,9 @@
'use client'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { MangoTokenData } from '../../types/mango'
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import { ChangeEvent, useMemo, useState } from 'react'
import Input from '../forms/Input'
import TokenTable from './TokenTable'
import TableViewToggle from './TableViewToggle'
import { MAX_CONTENT_WIDTH } from '../../utils/constants'
import PageHeader from './PageHeader'
const generateSearchTerm = (item: TokenPageWithData, searchValue: string) => {
const normalizedSearchValue = searchValue.toLowerCase()
const nameValue = item.tokenName.toLowerCase()
const symbolValue = item.symbol.toLowerCase()
const isMatchingName = nameValue.includes(normalizedSearchValue)
const isMatchingSymbol = symbolValue.includes(normalizedSearchValue)
const matchingNamePercent = isMatchingName
? normalizedSearchValue.length / item.tokenName.length
: 0
const matchingSymbolPercent = isMatchingSymbol
? normalizedSearchValue.length / item.symbol.length
: 0
const matchingPercent = Math.max(matchingNamePercent, matchingSymbolPercent)
const matchingIdx =
matchingPercent === matchingNamePercent
? nameValue.indexOf(normalizedSearchValue)
: symbolValue.indexOf(normalizedSearchValue)
return {
token: item,
matchingIdx,
matchingPercent,
}
}
const startSearch = (items: TokenPageWithData[], 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.matchingPercent - i1.matchingPercent)
.map((item) => item.token)
}
import TokensFilter from './TokensFilter'
export const sortTokens = (tokens: TokenPageWithData[]) => {
return tokens.sort((a, b) => {
@ -71,17 +29,6 @@ const ExploreTokens = ({
tokens: TokenPageWithData[]
mangoTokensData: MangoTokenData[]
}) => {
const [showTableView, setShowTableView] = useState(true)
const [searchString, setSearchString] = useState('')
const filteredTokens = useMemo(() => {
return searchString ? startSearch(tokens, searchString) : sortTokens(tokens)
}, [searchString, tokens])
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value)
}
return (
<>
<PageHeader title="Explore listed tokens" />
@ -91,25 +38,12 @@ const ExploreTokens = ({
<div className="mb-4 flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between">
<p>{`${tokens?.length} tokens listed on Mango`}</p>
<div className="flex space-x-2 mb-6 sm:mb-0">
<div className="relative w-full lg:mb-0 sm:w-44">
<Input
heightClass="h-10 pl-8"
type="text"
value={searchString}
onChange={handleUpdateSearch}
/>
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4 text-th-fgd-3" />
</div>
<TableViewToggle
showTableView={showTableView}
setShowTableView={setShowTableView}
/>
<TokensFilter tokens={tokens} />
</div>
</div>
<TokenTable
tokens={filteredTokens}
tokens={sortTokens(tokens)}
mangoTokensData={mangoTokensData}
showTableView={showTableView}
/>
</div>
</>

View File

@ -1,33 +1,20 @@
'use client'
import Image from 'next/image'
import { CUSTOM_TOKEN_ICONS } from '../../utils/constants'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from '../shared/TableElements'
import TokenCard from './TokenCard'
import { Table, Td, Th, TrBody, TrHead } from '../shared/TableElements'
import {
ChevronRightIcon,
QuestionMarkCircleIcon,
} from '@heroicons/react/20/solid'
import { formatNumericValue, numberCompacter } from '../../utils/numbers'
import SimpleAreaChart from '../shared/SimpleAreaChart'
import { useSortableData } from '../../hooks/useSortableData'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { useCallback } from 'react'
import { MangoTokenData } from '../../types/mango'
import { useViewport } from '../../hooks/useViewport'
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useRouter } from 'next/navigation'
import { BirdeyePriceHistoryData } from '../../types/birdeye'
import SheenLoader from '../shared/SheenLoader'
import NoResults from './NoResults'
import Solana from '../icons/Solana'
import Link from 'next/link'
import TokenCard from './TokenCard'
export type FormattedTableData = {
change: number | undefined
@ -46,22 +33,13 @@ export type FormattedTableData = {
ethCircSupply: number | undefined
}
const goToTokenPage = (slug: string, router: AppRouterInstance) => {
router.push(`/explore/tokens/${slug}`)
}
const TokenTable = ({
tokens,
mangoTokensData,
showTableView,
}: {
tokens: TokenPageWithData[]
mangoTokensData: MangoTokenData[]
showTableView: boolean
}) => {
const router = useRouter()
const { isDesktop, width } = useViewport()
const formattedTableData = useCallback(() => {
const formatted: FormattedTableData[] = []
for (const token of tokens) {
@ -135,189 +113,141 @@ const TokenTable = ({
return formatted
}, [tokens, mangoTokensData])
const {
items: tableData,
requestSort,
sortConfig,
} = useSortableData(formattedTableData())
return formattedTableData().length ? (
<>
<div className="hidden lg:block">
<Table>
<thead>
<TrHead>
<Th className="text-left">Token</Th>
<Th className="text-right">Price</Th>
<Th className="text-right">24h chart</Th>
<Th className="text-right">24h Change</Th>
<Th className="text-right">24h Volume</Th>
<Th className="text-right">FDV</Th>
<Th />
</TrHead>
</thead>
<tbody>
{formattedTableData().map((token) => {
const {
tokenName,
change,
chartData,
price,
volume,
fdv,
mangoSymbol,
logoURI,
symbol,
slug,
ethCircSupply,
ethMint,
} = token
const hasCustomIcon = mangoSymbol
? CUSTOM_TOKEN_ICONS[mangoSymbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${mangoSymbol?.toLowerCase()}.svg`
: logoURI
return tableData.length ? (
!width ? (
<SheenLoader className="flex flex-1">
<div className={`h-96 w-full rounded-lg bg-th-bkg-2`} />
</SheenLoader>
) : isDesktop && showTableView ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">
<SortableColumnHeader
sortKey="tokenName"
sort={() => requestSort('tokenName')}
sortConfig={sortConfig}
title="Token"
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="price"
sort={() => requestSort('price')}
sortConfig={sortConfig}
title="Price"
/>
</div>
</Th>
<Th className="text-right">24h chart</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="change"
sort={() => requestSort('change')}
sortConfig={sortConfig}
title="24h Change"
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="volume"
sort={() => requestSort('volume')}
sortConfig={sortConfig}
title="24h Volume"
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="fdv"
sort={() => requestSort('fdv')}
sortConfig={sortConfig}
title="FDV"
/>
</div>
</Th>
<Th />
</TrHead>
</thead>
<tbody>
{tableData.map((token) => {
const {
tokenName,
change,
chartData,
price,
volume,
fdv,
mangoSymbol,
logoURI,
symbol,
slug,
ethCircSupply,
ethMint,
} = token
const hasCustomIcon = mangoSymbol
? CUSTOM_TOKEN_ICONS[mangoSymbol.toLowerCase()]
: false
const logoPath = hasCustomIcon
? `/icons/tokens/${mangoSymbol?.toLowerCase()}.svg`
: logoURI
return (
<TrBody
key={slug}
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
onClick={() => goToTokenPage(slug, router)}
>
<Td>
<div className="flex items-center space-x-3">
{logoPath ? (
<Image src={logoPath} alt="Logo" height={32} width={32} />
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p>{tokenName}</p>
<p className="text-sm text-th-fgd-4">
{symbol || mangoSymbol}
</p>
</div>
</div>
</Td>
<Td>
<p className="text-right">
{price ? `$${formatNumericValue(price)}` : ''}
</p>
</Td>
<Td>
<div className="flex justify-end">
{chartData && chartData.length ? (
<div className="h-9 w-20">
<SimpleAreaChart
color={
chartData[0].value <=
chartData[chartData.length - 1].value
? 'var(--up)'
: 'var(--down)'
}
data={chartData}
name={tokenName}
xKey="unixTime"
yKey="value"
return (
<TrBody className="relative hover:bg-th-bkg-2" key={slug}>
<Td>
<div className="flex items-center space-x-3">
{logoPath ? (
<Image
src={logoPath}
alt="Logo"
height={32}
width={32}
/>
) : (
<QuestionMarkCircleIcon className="h-8 w-8 text-th-fgd-4" />
)}
<div>
<p>{tokenName}</p>
<p className="text-sm text-th-fgd-4">
{symbol || mangoSymbol}
</p>
</div>
) : (
<p className="text-th-fgd-4">Unavailable</p>
)}
</div>
</Td>
<Td>
<p
className={`text-right ${
!change
? 'text-th-fgd-3'
: change > 0
? 'text-th-up'
: change < 0
? 'text-th-down'
: 'text-th-fgd-3'
}`}
>
{change ? `${change.toFixed(2)}%` : ''}
</p>
</Td>
<Td>
<p className="text-right">
{volume ? `$${numberCompacter.format(volume)}` : ''}
</p>
</Td>
<Td>
<div className="flex items-center justify-end">
{ethMint && !ethCircSupply ? (
<Solana className="h-3.5 w-3.5 mr-1.5" />
) : null}
<p>{fdv ? `$${numberCompacter.format(fdv)}` : ''}</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
<ChevronRightIcon className="h-5 w-5 text-th-fgd-4" />
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tableData.map((data) => (
</div>
</Td>
<Td>
<p className="text-right">
{price ? `$${formatNumericValue(price)}` : ''}
</p>
</Td>
<Td>
<div className="flex justify-end">
{chartData && chartData.length ? (
<div className="h-9 w-20">
<SimpleAreaChart
color={
chartData[0].value <=
chartData[chartData.length - 1].value
? 'var(--up)'
: 'var(--down)'
}
data={chartData}
name={tokenName}
xKey="unixTime"
yKey="value"
/>
</div>
) : (
<p className="text-th-fgd-4">Unavailable</p>
)}
</div>
</Td>
<Td>
<p
className={`text-right ${
!change
? 'text-th-fgd-3'
: change > 0
? 'text-th-up'
: change < 0
? 'text-th-down'
: 'text-th-fgd-3'
}`}
>
{change ? `${change.toFixed(2)}%` : ''}
</p>
</Td>
<Td>
<p className="text-right">
{volume ? `$${numberCompacter.format(volume)}` : ''}
</p>
</Td>
<Td>
<div className="flex items-center justify-end">
{ethMint && !ethCircSupply ? (
<Solana className="h-3.5 w-3.5 mr-1.5" />
) : null}
<p>{fdv ? `$${numberCompacter.format(fdv)}` : ''}</p>
</div>
</Td>
<Td className="absolute w-full z-20 h-[69px] left-0 pt-0 pb-0 last:pr-2">
<Link
className="h-[69px] flex items-center justify-end"
href={`/explore/tokens/${slug}`}
>
<ChevronRightIcon className="h-6 w-6 text-th-fgd-4" />
</Link>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 lg:hidden">
{formattedTableData().map((data) => (
<TokenCard token={data} key={data.slug} />
))}
</div>
)
</>
) : (
<NoResults message="No tokens found..." />
)

View File

@ -0,0 +1,105 @@
'use client'
import { ChangeEvent, useMemo, useState } from 'react'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import Input from '../forms/Input'
import {
ChevronRightIcon,
MagnifyingGlassIcon,
} from '@heroicons/react/20/solid'
import Link from 'next/link'
const generateSearchTerm = (item: TokenPageWithData, searchValue: string) => {
const normalizedSearchValue = searchValue.toLowerCase()
const nameValue = item.tokenName.toLowerCase()
const symbolValue = item.symbol.toLowerCase()
const isMatchingName = nameValue.includes(normalizedSearchValue)
const isMatchingSymbol = symbolValue.includes(normalizedSearchValue)
const matchingNamePercent = isMatchingName
? normalizedSearchValue.length / item.tokenName.length
: 0
const matchingSymbolPercent = isMatchingSymbol
? normalizedSearchValue.length / item.symbol.length
: 0
const matchingPercent = Math.max(matchingNamePercent, matchingSymbolPercent)
const matchingIdx =
matchingPercent === matchingNamePercent
? nameValue.indexOf(normalizedSearchValue)
: symbolValue.indexOf(normalizedSearchValue)
return {
token: item,
matchingIdx,
matchingPercent,
}
}
const startSearch = (items: TokenPageWithData[], 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.matchingPercent - i1.matchingPercent)
.map((item) => item.token)
}
export const sortTokens = (tokens: TokenPageWithData[]) => {
return tokens.sort((a, b) => {
const aValue = a?.birdeyeData?.v24hUSD
const bValue = b?.birdeyeData?.v24hUSD
if (aValue === undefined && bValue === undefined) {
return 0
} else if (aValue === undefined) {
return 1
} else if (bValue === undefined) {
return -1
} else {
return bValue - aValue
}
})
}
const TokensFilter = ({ tokens }: { tokens: TokenPageWithData[] }) => {
const [searchString, setSearchString] = useState('')
const filteredTokens = useMemo(() => {
return searchString ? startSearch(tokens, searchString) : []
}, [searchString, tokens])
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearchString(e.target.value)
}
return (
<div className="relative w-full lg:mb-0 sm:w-44">
<Input
heightClass="h-10 pl-8"
type="text"
value={searchString}
onChange={handleUpdateSearch}
/>
<MagnifyingGlassIcon className="absolute left-2 top-3 h-4 w-4 text-th-fgd-3" />
{filteredTokens.length && searchString.length > 1 ? (
<div className="absolute z-20 top-12 bg-th-bkg-2 rounded-lg p-4 space-y-3 w-full">
{filteredTokens.map((token) => (
<Link
className="flex items-center justify-between text-th-fgd-2 md:hover:text-th-active"
key={token.slug}
href={`/explore/tokens/${token.slug}`}
>
<div className="flex-col flex">
<span className="text-sm">{token.symbol}</span>
<span className="text-xs text-th-fgd-4">{token.tokenName}</span>
</div>
<ChevronRightIcon className="h-5 w-5 text-th-fgd-4" />
</Link>
))}
</div>
) : null}
</div>
)
}
export default TokensFilter

View File

@ -53,7 +53,7 @@ const Select = ({
</div>
</Listbox.Button>
<Listbox.Options
className={`thin-scroll absolute left-0 z-20 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-2 p-2 outline-none ${dropdownPanelClassName}`}
className={`thin-scroll absolute left-0 z-30 mt-1 max-h-60 w-full origin-top-left overflow-auto rounded-md bg-th-bkg-2 p-2 outline-none ${dropdownPanelClassName}`}
>
{children}
</Listbox.Options>

View File

@ -1,3 +1,4 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useMemo } from 'react'
import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'

File diff suppressed because one or more lines are too long