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'
|
'use client'
|
||||||
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
type ComponentPropsWithoutRef,
|
type ComponentPropsWithoutRef,
|
||||||
Fragment,
|
Fragment,
|
||||||
@@ -11,6 +15,8 @@ import {
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { CardContent } from '@/components/ui/card'
|
import { CardContent } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Slider } from '@/components/ui/slider'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -37,13 +43,18 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
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() {
|
export function LeaderboardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
// Get the leaderboard type from URL or default to 'ranked'
|
// Get the leaderboard type from URL or default to 'ranked'
|
||||||
const leaderboardType = searchParams.get('type') || 'ranked'
|
const leaderboardType = searchParams.get('type') || 'ranked'
|
||||||
|
const [gamesAmount, setGamesAmount] = useState([0, 100])
|
||||||
|
|
||||||
// State for search and sorting
|
// State for search and sorting
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
@@ -60,63 +71,101 @@ export function LeaderboardPage() {
|
|||||||
channel_id: VANILLA_CHANNEL,
|
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
|
// Handle tab change
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
|
setGamesAmount([0, maxGamesAmount])
|
||||||
params.set('type', value)
|
params.set('type', value)
|
||||||
router.push(`?${params.toString()}`)
|
router.push(`?${params.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current leaderboard based on selected tab
|
const [sliderValue, setSliderValue] = useState([0, maxGamesAmount])
|
||||||
const currentLeaderboard =
|
const handleGamesAmountSliderChange = (value: number[]) => {
|
||||||
leaderboardType === 'ranked' ? rankedLeaderboard : vanillaLeaderboard
|
setSliderValue(value)
|
||||||
|
}
|
||||||
// Filter leaderboard by search query
|
const handleGamesAmountSliderCommit = (value: number[]) => {
|
||||||
const filteredLeaderboard = currentLeaderboard.filter((entry) =>
|
setGamesAmount(value)
|
||||||
entry.name.toLowerCase().includes(searchQuery.toLowerCase())
|
}
|
||||||
)
|
|
||||||
// Sort leaderboard
|
// Sort leaderboard
|
||||||
const sortedLeaderboard = [...filteredLeaderboard].sort((a, b) => {
|
const sortedLeaderboard = useMemo(
|
||||||
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
|
() =>
|
||||||
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
|
[...filteredLeaderboard].sort((a, b) => {
|
||||||
let valueA, valueB
|
// biome-ignore lint/style/useSingleVarDeclarator: <explanation>
|
||||||
|
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
|
||||||
|
let valueA, valueB
|
||||||
|
|
||||||
// Handle special case for rank which is already sorted
|
// Handle special case for rank which is already sorted
|
||||||
if (sortColumn === 'rank') {
|
if (sortColumn === 'rank') {
|
||||||
valueA = a.rank
|
valueA = a.rank
|
||||||
valueB = b.rank
|
valueB = b.rank
|
||||||
} else if (sortColumn === 'name') {
|
} else if (sortColumn === 'name') {
|
||||||
valueA = a.name.toLowerCase()
|
valueA = a.name.toLowerCase()
|
||||||
valueB = b.name.toLowerCase()
|
valueB = b.name.toLowerCase()
|
||||||
return sortDirection === 'asc'
|
return sortDirection === 'asc'
|
||||||
? valueA.localeCompare(valueB)
|
? valueA.localeCompare(valueB)
|
||||||
: valueB.localeCompare(valueA)
|
: valueB.localeCompare(valueA)
|
||||||
} else {
|
} else {
|
||||||
valueA = a[sortColumn as keyof typeof a] as number
|
valueA = a[sortColumn as keyof typeof a] as number
|
||||||
valueB = b[sortColumn as keyof typeof b] 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
|
// Handle column sort
|
||||||
const handleSort = (column: string) => {
|
const handleSort = useCallback(
|
||||||
if (sortColumn === column) {
|
(column: string) => {
|
||||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
if (sortColumn === column) {
|
||||||
} else {
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||||
setSortColumn(column)
|
} else {
|
||||||
setSortDirection('asc')
|
setSortColumn(column)
|
||||||
}
|
setSortDirection('asc')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[sortColumn, sortDirection]
|
||||||
|
)
|
||||||
|
|
||||||
// Get medal for top 3 players
|
// 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 (
|
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'>
|
<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}
|
onValueChange={handleTabChange}
|
||||||
className='flex flex-1 flex-col p-4 md:p-6'
|
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'>
|
<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='ranked'>Ranked Leaderboard</TabsTrigger>
|
||||||
<TabsTrigger value='vanilla'>Vanilla Leaderboard</TabsTrigger>
|
<TabsTrigger value='vanilla'>Vanilla Leaderboard</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
<div
|
||||||
<div className='relative w-full sm:w-auto'>
|
className={
|
||||||
<Search className='absolute top-2.5 left-2.5 h-4 w-4 text-gray-400 dark:text-zinc-400' />
|
'flex w-full flex-col items-center justify-end gap-2 lg:w-fit lg:flex-row lg:gap-4'
|
||||||
<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'
|
<div className={'flex w-full flex-col gap-1 md:w-[300px]'}>
|
||||||
value={searchQuery}
|
<Label>Games</Label>
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value='ranked' className='m-0 flex flex-1 flex-col'>
|
<div className='m-0 flex flex-1 flex-col'>
|
||||||
<LeaderboardTable
|
<LeaderboardTable
|
||||||
leaderboard={sortedLeaderboard}
|
leaderboard={leaderboardFilteredByGameAmounts}
|
||||||
sortColumn={sortColumn}
|
sortColumn={sortColumn}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
getMedal={getMedal}
|
getMedal={getMedal}
|
||||||
type='ranked'
|
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
|
||||||
<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>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,16 +267,14 @@ interface LeaderboardTableProps {
|
|||||||
sortDirection: 'asc' | 'desc'
|
sortDirection: 'asc' | 'desc'
|
||||||
onSort: (column: string) => void
|
onSort: (column: string) => void
|
||||||
getMedal: (rank: number) => React.ReactNode
|
getMedal: (rank: number) => React.ReactNode
|
||||||
type: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function LeaderboardTable({
|
function RawLeaderboardTable({
|
||||||
leaderboard,
|
leaderboard,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
onSort,
|
onSort,
|
||||||
getMedal,
|
getMedal,
|
||||||
type,
|
|
||||||
}: LeaderboardTableProps) {
|
}: LeaderboardTableProps) {
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null)
|
const tableContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -499,3 +557,6 @@ function SortableHeader({
|
|||||||
</button>
|
</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({
|
function Slider({
|
||||||
className,
|
className,
|
||||||
@@ -25,35 +25,35 @@ function Slider({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SliderPrimitive.Root
|
<SliderPrimitive.Root
|
||||||
data-slot="slider"
|
data-slot='slider'
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track
|
<SliderPrimitive.Track
|
||||||
data-slot="slider-track"
|
data-slot='slider-track'
|
||||||
className={cn(
|
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
|
<SliderPrimitive.Range
|
||||||
data-slot="slider-range"
|
data-slot='slider-range'
|
||||||
className={cn(
|
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>
|
</SliderPrimitive.Track>
|
||||||
{Array.from({ length: _values.length }, (_, index) => (
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
<SliderPrimitive.Thumb
|
<SliderPrimitive.Thumb
|
||||||
data-slot="slider-thumb"
|
data-slot='slider-thumb'
|
||||||
key={index}
|
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>
|
</SliderPrimitive.Root>
|
||||||
|
|||||||
Reference in New Issue
Block a user