'use client' import type React from 'react' import { useRef } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { api } from '@/trpc/react' import { useVirtualizer } from '@tanstack/react-virtual' import { ArrowDown, ArrowUp, ArrowUpDown, Flame, Info, Medal, Search, TrendingUp, Trophy, Users, } from 'lucide-react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' export function LeaderboardPage() { const router = useRouter() const searchParams = useSearchParams() // Get the leaderboard type from URL or default to 'ranked' const leaderboardType = searchParams.get('type') || 'ranked' // State for search and sorting const [searchQuery, setSearchQuery] = useState('') const [sortColumn, setSortColumn] = useState('rank') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Fetch leaderboard data const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({ channel_id: RANKED_CHANNEL, }) const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery( { channel_id: VANILLA_CHANNEL, } ) // Handle tab change const handleTabChange = (value: string) => { const params = new URLSearchParams(searchParams) params.set('type', value) router.push(`?${params.toString()}`) } // Get the current leaderboard based on selected tab const currentLeaderboard = leaderboardType === 'ranked' ? rankedLeaderboard.alltime : vanillaLeaderboard.alltime // Filter leaderboard by search query const filteredLeaderboard = currentLeaderboard.filter((entry) => entry.name.toLowerCase().includes(searchQuery.toLowerCase()) ) // Sort leaderboard const sortedLeaderboard = [...filteredLeaderboard].sort((a, b) => { // biome-ignore lint/style/useSingleVarDeclarator: // biome-ignore lint/suspicious/noImplicitAnyLet: let valueA, valueB // Handle special case for rank which is already sorted if (sortColumn === 'rank') { valueA = a.data.rank valueB = b.data.rank } else if (sortColumn === 'name') { valueA = a.name.toLowerCase() valueB = b.name.toLowerCase() return sortDirection === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA) } else { valueA = a.data[sortColumn as keyof typeof a.data] as number valueB = b.data[sortColumn as keyof typeof b.data] as number } return sortDirection === 'asc' ? valueA - valueB : valueB - valueA }) // Handle column sort const handleSort = (column: string) => { if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { setSortColumn(column) setSortDirection('asc') } } // Get medal for top 3 players const getMedal = (rank: number) => { if (rank === 1) return if (rank === 2) return if (rank === 3) return return null } console.log(currentLeaderboard) return (

Leaderboards

View player rankings and statistics

{currentLeaderboard.length} Players
Ranked Leaderboard Vanilla Leaderboard
setSearchQuery(e.target.value)} />
) } interface LeaderboardTableProps { leaderboard: any[] sortColumn: string sortDirection: 'asc' | 'desc' onSort: (column: string) => void getMedal: (rank: number) => React.ReactNode type: string } function LeaderboardTable({ leaderboard, sortColumn, sortDirection, onSort, getMedal, type, }: LeaderboardTableProps) { const tableContainerRef = useRef(null) // Set a fixed row height for virtualization const ROW_HEIGHT = 56 // Adjust based on your actual row height // Create virtualizer instance const rowVirtualizer = useVirtualizer({ count: leaderboard.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => ROW_HEIGHT, overscan: 10, // Number of items to render before/after the visible area }) // Get the virtualized rows const virtualRows = rowVirtualizer.getVirtualItems() const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start : 0 const paddingBottom = virtualRows.length > 0 ? rowVirtualizer.getTotalSize() - (virtualRows?.[virtualRows.length - 1]?.end ?? 0) : 0 return (
{leaderboard.length > 0 ? ( leaderboard.map((entry) => { const winrate = entry.data.winrate * 100 return (
{getMedal(entry.data.rank)} {entry.data.rank}
{/**/} {/* */} {/* */} {/* {entry.name.slice(0, 2).toUpperCase()}*/} {/* */} {/**/} {entry.name} {entry.data.streak >= 3 && ( Hot Streak )} {Math.round(entry.data.mmr)}
{Math.round(entry.data.peak_mmr)}
60 ? 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300' : winrate < 40 ? 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-300' : 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300' )} > {Math.round(winrate)}% {entry.data.wins} {entry.data.losses} {entry.data.totalgames} {entry.data.streak > 0 ? ( {entry.data.streak} ) : entry.data.streak < 0 ? ( {Math.abs(entry.data.streak)} ) : ( 0 )}
) }) ) : ( No players found )}
) } interface SortableHeaderProps { column: string label: string currentSort: string direction: 'asc' | 'desc' onSort: (column: string) => void } function SortableHeader({ column, label, currentSort, direction, onSort, }: SortableHeaderProps) { const isActive = currentSort === column return ( ) }