From ccc68a38ecf733f5424dcb4083a4fd7f4e5a5e23 Mon Sep 17 00:00:00 2001 From: Andres Date: Fri, 4 Apr 2025 18:12:21 +0200 Subject: [PATCH] add games filter --- src/app/_components/leaderboard.tsx | 205 ++++++++++++++++++---------- src/components/ui/slider.tsx | 24 ++-- 2 files changed, 145 insertions(+), 84 deletions(-) diff --git a/src/app/_components/leaderboard.tsx b/src/app/_components/leaderboard.tsx index 1c52571..81141e8 100644 --- a/src/app/_components/leaderboard.tsx +++ b/src/app/_components/leaderboard.tsx @@ -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 + if (rank === 2) return + if (rank === 3) return + 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: - // biome-ignore lint/suspicious/noImplicitAnyLet: - let valueA, valueB + const sortedLeaderboard = useMemo( + () => + [...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.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 - if (rank === 2) return - if (rank === 3) return - return null - } return (
@@ -155,44 +204,55 @@ export function LeaderboardPage() { onValueChange={handleTabChange} className='flex flex-1 flex-col p-4 md:p-6' > -
+
Ranked Leaderboard Vanilla Leaderboard - -
- - setSearchQuery(e.target.value)} - /> +
+
+ +
+ {gamesAmount[0]} + + {gamesAmount[1]} +
+
+
+ +
+ + setSearchQuery(e.target.value)} + /> +
+
- +
- - - - - +
@@ -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(null) @@ -499,3 +557,6 @@ function SortableHeader({ ) } + +export const LeaderboardTable = memo(RawLeaderboardTable) +LeaderboardTable.displayName = 'LeaderboardTable' diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx index 09391e8..d111e23 100644 --- a/src/components/ui/slider.tsx +++ b/src/components/ui/slider.tsx @@ -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 ( {Array.from({ length: _values.length }, (_, index) => ( ))}