pummmp.fun/components/holders.tsx
2026-07-04 12:49:09 -07:00

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>
)
}