mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
add games filter
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
@@ -11,6 +15,8 @@ import {
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -37,13 +43,18 @@ import {
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
const getMedal = (rank: number) => {
|
||||
if (rank === 1) return <Medal className='h-5 w-5 text-yellow-500' />
|
||||
if (rank === 2) return <Medal className='h-5 w-5 text-slate-400' />
|
||||
if (rank === 3) return <Medal className='h-5 w-5 text-amber-700' />
|
||||
return null
|
||||
}
|
||||
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'
|
||||
const [gamesAmount, setGamesAmount] = useState([0, 100])
|
||||
|
||||
// State for search and sorting
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -60,63 +71,101 @@ export function LeaderboardPage() {
|
||||
channel_id: VANILLA_CHANNEL,
|
||||
}
|
||||
)
|
||||
// Get the current leaderboard based on selected tab
|
||||
const currentLeaderboard = useMemo(
|
||||
() =>
|
||||
leaderboardType === 'ranked' ? rankedLeaderboard : vanillaLeaderboard,
|
||||
[leaderboardType, rankedLeaderboard, vanillaLeaderboard]
|
||||
)
|
||||
|
||||
const filteredLeaderboard = useMemo(
|
||||
() =>
|
||||
currentLeaderboard.filter((entry) =>
|
||||
entry.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
[currentLeaderboard, searchQuery]
|
||||
)
|
||||
|
||||
const maxGamesAmount = useMemo(
|
||||
() => Math.max(...filteredLeaderboard.map((entry) => entry.totalgames)),
|
||||
[filteredLeaderboard]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (maxGamesAmount === gamesAmount[1]) return
|
||||
setGamesAmount([0, maxGamesAmount])
|
||||
}, [maxGamesAmount])
|
||||
|
||||
// Handle tab change
|
||||
const handleTabChange = (value: string) => {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
setGamesAmount([0, maxGamesAmount])
|
||||
params.set('type', value)
|
||||
router.push(`?${params.toString()}`)
|
||||
}
|
||||
|
||||
// Get the current leaderboard based on selected tab
|
||||
const currentLeaderboard =
|
||||
leaderboardType === 'ranked' ? rankedLeaderboard : vanillaLeaderboard
|
||||
|
||||
// Filter leaderboard by search query
|
||||
const filteredLeaderboard = currentLeaderboard.filter((entry) =>
|
||||
entry.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
const [sliderValue, setSliderValue] = useState([0, maxGamesAmount])
|
||||
const handleGamesAmountSliderChange = (value: number[]) => {
|
||||
setSliderValue(value)
|
||||
}
|
||||
const handleGamesAmountSliderCommit = (value: number[]) => {
|
||||
setGamesAmount(value)
|
||||
}
|
||||
// Sort leaderboard
|
||||
const sortedLeaderboard = [...filteredLeaderboard].sort((a, b) => {
|
||||
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
|
||||
let valueA, valueB
|
||||
const sortedLeaderboard = useMemo(
|
||||
() =>
|
||||
[...filteredLeaderboard].sort((a, b) => {
|
||||
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
|
||||
let valueA, valueB
|
||||
|
||||
// Handle special case for rank which is already sorted
|
||||
if (sortColumn === 'rank') {
|
||||
valueA = a.rank
|
||||
valueB = b.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[sortColumn as keyof typeof a] as number
|
||||
valueB = b[sortColumn as keyof typeof b] as number
|
||||
}
|
||||
// Handle special case for rank which is already sorted
|
||||
if (sortColumn === 'rank') {
|
||||
valueA = a.rank
|
||||
valueB = b.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[sortColumn as keyof typeof a] as number
|
||||
valueB = b[sortColumn as keyof typeof b] as number
|
||||
}
|
||||
|
||||
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA
|
||||
})
|
||||
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA
|
||||
}),
|
||||
[filteredLeaderboard, sortColumn, sortDirection]
|
||||
)
|
||||
|
||||
const leaderboardFilteredByGameAmounts = useMemo(
|
||||
() =>
|
||||
sortedLeaderboard.filter((entry) => {
|
||||
if (!gamesAmount) return true
|
||||
|
||||
return (
|
||||
entry.totalgames >= (gamesAmount[0] ?? 0) &&
|
||||
entry.totalgames <= (gamesAmount[1] ?? Number.MAX_SAFE_INTEGER)
|
||||
)
|
||||
}),
|
||||
[sortedLeaderboard, gamesAmount]
|
||||
)
|
||||
|
||||
// Handle column sort
|
||||
const handleSort = (column: string) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortColumn(column)
|
||||
setSortDirection('asc')
|
||||
}
|
||||
}
|
||||
const handleSort = useCallback(
|
||||
(column: string) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
setSortColumn(column)
|
||||
setSortDirection('asc')
|
||||
}
|
||||
},
|
||||
[sortColumn, sortDirection]
|
||||
)
|
||||
|
||||
// Get medal for top 3 players
|
||||
const getMedal = (rank: number) => {
|
||||
if (rank === 1) return <Medal className='h-5 w-5 text-yellow-500' />
|
||||
if (rank === 2) return <Medal className='h-5 w-5 text-slate-400' />
|
||||
if (rank === 3) return <Medal className='h-5 w-5 text-amber-700' />
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-screen flex-col overflow-hidden bg-gradient-to-b from-gray-50 to-gray-100 dark:from-zinc-900 dark:to-zinc-950'>
|
||||
@@ -155,44 +204,55 @@ export function LeaderboardPage() {
|
||||
onValueChange={handleTabChange}
|
||||
className='flex flex-1 flex-col p-4 md:p-6'
|
||||
>
|
||||
<div className='mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
|
||||
<div className='mb-6 flex w-full flex-col items-start justify-between gap-4 md:items-center lg:flex-row'>
|
||||
<TabsList className='border border-gray-200 border-b bg-gray-50 dark:border-zinc-800 dark:bg-zinc-800/50'>
|
||||
<TabsTrigger value='ranked'>Ranked Leaderboard</TabsTrigger>
|
||||
<TabsTrigger value='vanilla'>Vanilla Leaderboard</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className='relative w-full sm:w-auto'>
|
||||
<Search className='absolute top-2.5 left-2.5 h-4 w-4 text-gray-400 dark:text-zinc-400' />
|
||||
<Input
|
||||
placeholder='Search players...'
|
||||
className='w-full border-gray-200 bg-white pl-9 sm:w-[250px] dark:border-zinc-700 dark:bg-zinc-900'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-col items-center justify-end gap-2 lg:w-fit lg:flex-row lg:gap-4'
|
||||
}
|
||||
>
|
||||
<div className={'flex w-full flex-col gap-1 md:w-[300px]'}>
|
||||
<Label>Games</Label>
|
||||
<div className='flex w-full items-center gap-2'>
|
||||
<span>{gamesAmount[0]}</span>
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
onValueCommit={handleGamesAmountSliderCommit}
|
||||
max={maxGamesAmount}
|
||||
onValueChange={handleGamesAmountSliderChange}
|
||||
step={1}
|
||||
className={cn('w-full')}
|
||||
/>
|
||||
<span>{gamesAmount[1]}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex w-full flex-col gap-1 md:w-[250px]'}>
|
||||
<Label>Search players</Label>
|
||||
<div className='relative w-full sm:w-auto'>
|
||||
<Search className='absolute top-2.5 left-2.5 h-4 w-4 text-gray-400 dark:text-zinc-400' />
|
||||
<Input
|
||||
placeholder='Search players...'
|
||||
className='w-full border-gray-200 bg-white pl-9 dark:border-zinc-700 dark:bg-zinc-900'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value='ranked' className='m-0 flex flex-1 flex-col'>
|
||||
<div className='m-0 flex flex-1 flex-col'>
|
||||
<LeaderboardTable
|
||||
leaderboard={sortedLeaderboard}
|
||||
leaderboard={leaderboardFilteredByGameAmounts}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
getMedal={getMedal}
|
||||
type='ranked'
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='vanilla' className='m-0 flex flex-1 flex-col'>
|
||||
<LeaderboardTable
|
||||
leaderboard={sortedLeaderboard}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
getMedal={getMedal}
|
||||
type='vanilla'
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</div>
|
||||
@@ -207,16 +267,14 @@ interface LeaderboardTableProps {
|
||||
sortDirection: 'asc' | 'desc'
|
||||
onSort: (column: string) => void
|
||||
getMedal: (rank: number) => React.ReactNode
|
||||
type: string
|
||||
}
|
||||
|
||||
function LeaderboardTable({
|
||||
function RawLeaderboardTable({
|
||||
leaderboard,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
getMedal,
|
||||
type,
|
||||
}: LeaderboardTableProps) {
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -499,3 +557,6 @@ function SortableHeader({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const LeaderboardTable = memo(RawLeaderboardTable)
|
||||
LeaderboardTable.displayName = 'LeaderboardTable'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
@@ -25,35 +25,35 @@ function Slider({
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
data-slot='slider'
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
'relative flex w-full touch-none select-none items-center data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
data-slot='slider-track'
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
|
||||
'relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1.5'
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
data-slot='slider-range'
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
|
||||
'absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full'
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
data-slot='slider-thumb'
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
className='block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50'
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
|
||||
Reference in New Issue
Block a user