342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
|
import dayjs from 'dayjs'
|
|
import { MedalIcon } from './icons'
|
|
import { useTranslation } from 'next-i18next'
|
|
import { abbreviateAddress, usdFormatter } from '../utils'
|
|
import { ChevronRightIcon } from '@heroicons/react/solid'
|
|
import ProfileImage from './ProfileImage'
|
|
import { useRouter } from 'next/router'
|
|
import { PublicKey } from '@solana/web3.js'
|
|
import { notify } from 'utils/notifications'
|
|
|
|
const utc = require('dayjs/plugin/utc')
|
|
dayjs.extend(utc)
|
|
|
|
const LeaderboardTable = ({ range = '29' }) => {
|
|
const { t } = useTranslation('common')
|
|
const [pnlLeaderboardData, setPnlLeaderboardData] = useState<any[]>([])
|
|
const [perpPnlLeaderboardData, setPerpPnlLeaderboardData] = useState<any[]>(
|
|
[]
|
|
)
|
|
const [spotPnlLeaderboardData, setSpotPnlLeaderboardData] = useState<any[]>(
|
|
[]
|
|
)
|
|
const [leaderboardType, setLeaderboardType] = useState<string>('total-pnl')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const formatLeaderboardData = async (leaderboard) => {
|
|
const walletPks = leaderboard.map((u) => u.wallet_pk)
|
|
const profileDetailsResponse = await fetch(
|
|
`https://mango-transaction-log.herokuapp.com/v3/user-data/multiple-profile-details?wallet-pks=${walletPks.toString()}`
|
|
)
|
|
const parsedProfileDetailsResponse = await profileDetailsResponse.json()
|
|
const leaderboardData = [] as any[]
|
|
for (const item of leaderboard) {
|
|
const profileDetails = parsedProfileDetailsResponse[item.wallet_pk]
|
|
leaderboardData.push({
|
|
...item,
|
|
profile: profileDetails ? profileDetails : null,
|
|
})
|
|
}
|
|
return leaderboardData
|
|
}
|
|
|
|
const fetchPnlLeaderboard = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await fetch(
|
|
`https://mango-transaction-log.herokuapp.com/v3/stats/pnl-leaderboard?start-date=${dayjs()
|
|
.utc()
|
|
.hour(0)
|
|
.minute(0)
|
|
.subtract(parseInt(range), 'day')
|
|
.add(1, 'hour')
|
|
.format('YYYY-MM-DDThh:00:00')}`
|
|
)
|
|
const parsedResponse = await response.json()
|
|
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
|
setPnlLeaderboardData(leaderboardData)
|
|
setLoading(false)
|
|
} catch {
|
|
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchPerpPnlLeaderboard = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await fetch(
|
|
`https://mango-transaction-log.herokuapp.com/v3/stats/perp-pnl-leaderboard?start-date=${dayjs()
|
|
.utc()
|
|
.hour(0)
|
|
.minute(0)
|
|
.subtract(parseInt(range), 'day')
|
|
.add(1, 'hour')
|
|
.format('YYYY-MM-DDThh:00:00')}`
|
|
)
|
|
const parsedResponse = await response.json()
|
|
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
|
setPerpPnlLeaderboardData(leaderboardData)
|
|
setLoading(false)
|
|
} catch {
|
|
notify({ type: 'error', title: t('fetch-leaderboard-fail') })
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchSpotPnlLeaderboard = async () => {
|
|
setLoading(true)
|
|
const response = await fetch(
|
|
`https://mango-transaction-log.herokuapp.com/v3/stats/spot-pnl-leaderboard?start-date=${dayjs()
|
|
.hour(0)
|
|
.minute(0)
|
|
.utc()
|
|
.subtract(parseInt(range), 'day')
|
|
.format('YYYY-MM-DDThh:00:00')}`
|
|
)
|
|
const parsedResponse = await response.json()
|
|
const leaderboardData = await formatLeaderboardData(parsedResponse)
|
|
setSpotPnlLeaderboardData(leaderboardData)
|
|
|
|
setLoading(false)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (leaderboardType === 'total-pnl') {
|
|
fetchPnlLeaderboard()
|
|
} else if (leaderboardType === 'futures-only') {
|
|
fetchPerpPnlLeaderboard()
|
|
} else {
|
|
fetchSpotPnlLeaderboard()
|
|
}
|
|
}, [range, leaderboardType])
|
|
|
|
useEffect(() => {
|
|
fetchPerpPnlLeaderboard()
|
|
fetchSpotPnlLeaderboard()
|
|
}, [])
|
|
|
|
const leaderboardData = useMemo(
|
|
() =>
|
|
leaderboardType === 'total-pnl'
|
|
? pnlLeaderboardData
|
|
: leaderboardType === 'futures-only'
|
|
? perpPnlLeaderboardData
|
|
: spotPnlLeaderboardData,
|
|
[
|
|
leaderboardType,
|
|
pnlLeaderboardData,
|
|
perpPnlLeaderboardData,
|
|
spotPnlLeaderboardData,
|
|
]
|
|
)
|
|
|
|
return (
|
|
<div className="grid grid-cols-12 gap-6">
|
|
<div className="col-span-12 flex space-x-3 lg:col-span-4 lg:flex-col lg:space-y-2 lg:space-x-0">
|
|
<LeaderboardTypeButton
|
|
leaderboardType={leaderboardType}
|
|
setLeaderboardType={setLeaderboardType}
|
|
range={range}
|
|
label="total-pnl"
|
|
/>
|
|
<LeaderboardTypeButton
|
|
leaderboardType={leaderboardType}
|
|
setLeaderboardType={setLeaderboardType}
|
|
range={range}
|
|
label="futures-only"
|
|
/>
|
|
<LeaderboardTypeButton
|
|
leaderboardType={leaderboardType}
|
|
setLeaderboardType={setLeaderboardType}
|
|
range={range}
|
|
label="spot-only"
|
|
/>
|
|
</div>
|
|
<div className="col-span-12 lg:col-span-8">
|
|
{!loading ? (
|
|
<div className="space-y-2">
|
|
{leaderboardData.map((acc, i) => (
|
|
<AccountCard
|
|
rank={i + 1}
|
|
acc={acc.mango_account}
|
|
key={acc.mango_account}
|
|
rawPnl={
|
|
leaderboardType === 'total-pnl'
|
|
? acc.pnl
|
|
: leaderboardType === 'futures-only'
|
|
? acc.perp_pnl
|
|
: acc.spot_pnl
|
|
}
|
|
pnl={
|
|
leaderboardType === 'total-pnl'
|
|
? acc.pnl.toLocaleString('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: 0,
|
|
})
|
|
: leaderboardType === 'futures-only'
|
|
? usdFormatter(acc.perp_pnl)
|
|
: usdFormatter(acc.spot_pnl)
|
|
}
|
|
walletPk={acc.wallet_pk}
|
|
profile={acc.profile}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
<div className="h-[84px] w-full animate-pulse rounded-md bg-th-bkg-3" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default LeaderboardTable
|
|
|
|
const AccountCard = ({ rank, acc, rawPnl, profile, pnl, walletPk }) => {
|
|
const router = useRouter()
|
|
const medalColors =
|
|
rank === 1
|
|
? {
|
|
darkest: '#E4AF11',
|
|
dark: '#F2C94C',
|
|
light: '#FFCF40',
|
|
lightest: '#FDE877',
|
|
}
|
|
: rank === 2
|
|
? {
|
|
darkest: '#B8B8B8',
|
|
dark: '#C0C0C0',
|
|
light: '#C7C7C7',
|
|
lightest: '#D8D6D6',
|
|
}
|
|
: {
|
|
darkest: '#CD7F32',
|
|
dark: '#E5994E',
|
|
light: '#DBA36B',
|
|
lightest: '#EFBF8D',
|
|
}
|
|
return (
|
|
<div className="relative" key={acc}>
|
|
{profile ? (
|
|
<button
|
|
className="absolute left-[118px] bottom-4 flex items-center space-x-2 rounded-full border border-th-fgd-4 px-2 py-1 hover:border-th-fgd-2 hover:filter"
|
|
onClick={() =>
|
|
router.push(
|
|
`/profile?name=${profile?.profile_name.replace(/\s/g, '-')}`,
|
|
undefined,
|
|
{
|
|
shallow: true,
|
|
}
|
|
)
|
|
}
|
|
>
|
|
<p className="mb-0 text-xs capitalize text-th-fgd-3">
|
|
{profile?.profile_name}
|
|
</p>
|
|
</button>
|
|
) : null}
|
|
<a
|
|
className="default-transition block flex h-[112px] w-full rounded-md border border-th-bkg-4 p-4 hover:border-th-fgd-4 sm:h-[84px] sm:justify-between sm:pb-4"
|
|
href={`/account?pubkey=${acc}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<p className="my-auto mr-4 flex w-5 justify-center font-bold">{rank}</p>
|
|
<div className="relative my-auto">
|
|
{rank < 4 ? (
|
|
<MedalIcon
|
|
className="absolute -top-1 -left-1 z-10 h-5 w-auto drop-shadow-lg"
|
|
colors={medalColors}
|
|
/>
|
|
) : null}
|
|
<ProfileImage
|
|
imageSize="56"
|
|
placeholderSize="32"
|
|
publicKey={walletPk}
|
|
/>
|
|
</div>
|
|
<div className="ml-3 flex flex-col sm:flex-grow sm:flex-row sm:justify-between">
|
|
<p className="mb-0 font-bold text-th-fgd-2">
|
|
{abbreviateAddress(new PublicKey(acc))}
|
|
</p>
|
|
|
|
<span
|
|
className={`flex items-center text-lg font-bold ${
|
|
rawPnl > 0 ? 'text-th-green' : 'text-th-red'
|
|
}`}
|
|
>
|
|
{pnl}
|
|
</span>
|
|
</div>
|
|
<div className="my-auto ml-auto">
|
|
<ChevronRightIcon className="ml-2 mt-0.5 h-5 w-5 text-th-fgd-4" />
|
|
</div>
|
|
</a>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const LeaderboardTypeButton = ({
|
|
leaderboardType,
|
|
setLeaderboardType,
|
|
range,
|
|
icon,
|
|
label,
|
|
}: {
|
|
leaderboardType: string
|
|
setLeaderboardType: (x) => void
|
|
range: string
|
|
icon?: ReactNode
|
|
label: string
|
|
}) => {
|
|
const { t } = useTranslation('common')
|
|
return (
|
|
<button
|
|
className={`relative flex w-full items-center justify-center rounded-md p-4 text-center lg:h-20 lg:justify-start lg:px-6 lg:text-left ${
|
|
leaderboardType === label
|
|
? 'bg-th-bkg-3 text-th-fgd-1 after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:transform after:border-l-[12px] after:border-r-[12px] after:border-t-[12px] after:border-l-transparent after:border-t-th-bkg-3 after:border-r-transparent lg:after:left-[100%] lg:after:top-1/2 lg:after:-translate-x-0 lg:after:-translate-y-1/2 lg:after:border-r-0 lg:after:border-b-[12px] lg:after:border-t-transparent lg:after:border-b-transparent lg:after:border-l-th-bkg-3'
|
|
: 'bg-th-bkg-2 text-th-fgd-3 md:hover:bg-th-bkg-3'
|
|
}`}
|
|
onClick={() => setLeaderboardType(label)}
|
|
>
|
|
{icon}
|
|
<div>
|
|
<div className="font-bold sm:text-lg">{t(label)}</div>
|
|
<span className="text-sm text-th-fgd-4">
|
|
{range === '9999'
|
|
? 'All-time'
|
|
: range === '29'
|
|
? 'Last 30 days'
|
|
: range === '1'
|
|
? 'Last 24 hours'
|
|
: `Last ${range} days`}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
)
|
|
}
|