add games filter

This commit is contained in:
2025-04-04 18:12:21 +02:00
parent 598582672e
commit ccc68a38ec
2 changed files with 145 additions and 84 deletions

View File

@@ -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,24 +71,50 @@ 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) => {
const sortedLeaderboard = useMemo(
() =>
[...filteredLeaderboard].sort((a, b) => {
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
let valueA, valueB
@@ -98,25 +135,37 @@ export function LeaderboardPage() {
}
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) => {
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={
'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 sm:w-[250px] dark:border-zinc-700 dark:bg-zinc-900'
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'

View File

@@ -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>