113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect } from 'react'
|
|
import { io } from 'socket.io-client'
|
|
import Link from 'next/link'
|
|
import useSWR from 'swr'
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
import { UserBadges } from '@/components/ui/user-badges'
|
|
import { VerifiedBadge } from './ui/verified-badge'
|
|
|
|
interface Holder {
|
|
_id: string
|
|
name: string
|
|
image: string
|
|
amount: number
|
|
percentage?: number
|
|
isSystem?: boolean
|
|
verified?: boolean
|
|
isAdmin?: boolean
|
|
isBetaTester?: boolean
|
|
isBugHunter?: boolean
|
|
}
|
|
|
|
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
|
|
|
export function TopHolders({ coinId, totalSupply }: { coinId: string, totalSupply: number }) {
|
|
const { data: holders, error, mutate } = useSWR<Holder[]>(
|
|
`/api/coins/${coinId}/holders`,
|
|
fetcher
|
|
)
|
|
|
|
useEffect(() => {
|
|
const socket = io("https://pummmp.fun/", {
|
|
path: "/socket.io",
|
|
transports: ["websocket"],
|
|
});
|
|
|
|
|
|
socket.on(`trade:${coinId}`, () => {
|
|
mutate()
|
|
})
|
|
|
|
return () => {
|
|
socket.disconnect()
|
|
}
|
|
}, [coinId, mutate])
|
|
|
|
if (!Array.isArray(holders)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-xl border border-border bg-card/30 p-4">
|
|
<h3 className="mb-4 font-semibold">Top Holders</h3>
|
|
<div className="space-y-3">
|
|
{holders.map((holder, i) => (
|
|
<div key={holder._id} className="flex items-center justify-between text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-4 text-xs text-muted-foreground">{i + 1}.</span>
|
|
<Link href={holder.isSystem ? '#' : `/u/${encodeURIComponent(holder.name)}`} className={`flex items-center gap-2 group ${holder.isSystem ? 'pointer-events-none' : ''}`}>
|
|
<Avatar className="h-6 w-6 border border-border">
|
|
{holder.name === 'Raydium Pool' ? (
|
|
<div className="flex h-full w-full items-center justify-center bg-violet-500/20 text-[12px] font-bold text-violet-500">
|
|
R<span className="text-white">P</span>
|
|
</div>
|
|
) : holder.name === 'Bonding Curve' ? (
|
|
<div className="flex h-full w-full items-center justify-center bg-primary/20 text-[12px] font-bold text-primary">
|
|
B<span className="text-white">C</span>
|
|
</div>
|
|
) : (<>
|
|
<AvatarImage src={holder.image} />
|
|
<AvatarFallback>{holder.name.substring(0, 2)}</AvatarFallback></>
|
|
)}
|
|
</Avatar>
|
|
<span className={`font-medium truncate max-w-[100px] ${!holder.isSystem && 'group-hover:underline'}`}>
|
|
{holder.name}
|
|
</span>
|
|
{!holder.isSystem && (
|
|
<UserBadges
|
|
isAdmin={holder.isAdmin}
|
|
isVerified={holder.verified}
|
|
isBugHunter={holder.isBugHunter}
|
|
isBetaTester={holder.isBetaTester}
|
|
onlyShowHighest
|
|
/>
|
|
)}
|
|
{holder.isSystem && (
|
|
// <span className="ml-[-4px] flex h-3 w-3 items-center justify-center rounded-full bg-violet-500 text-[6px] text-white">✓</span>
|
|
<VerifiedBadge className="h-4 w-4 text-white fill-purple-500 [&>path:first-child]:stroke-purple-500" />
|
|
)}
|
|
{holder.name === 'Bonding Curve' && (
|
|
// <span className="ml-[-4px] flex h-3 w-3 items-center justify-center rounded-full bg-primary text-[6px] text-white">✓</span>
|
|
<VerifiedBadge className="h-4 w-4" />
|
|
)}
|
|
</Link>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-muted-foreground">
|
|
{((holder.amount / totalSupply) * 100).toFixed(2)}%
|
|
</span>
|
|
<span className="font-mono">
|
|
{(holder.amount / 1_000_000).toFixed(1)}M
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!holders?.length && (
|
|
<p className="text-sm text-muted-foreground mt-[-12px]">No holders yet {`;-;`}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|