fix caching

This commit is contained in:
2025-04-04 02:12:45 +02:00
parent 21b04afd10
commit 1e16b4c01d
7 changed files with 166 additions and 144 deletions

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import type React from 'react' import type React from 'react'
import { useRef } from 'react' import { type ComponentPropsWithoutRef, Fragment, useRef } from 'react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -68,15 +68,13 @@ export function LeaderboardPage() {
// Get the current leaderboard based on selected tab // Get the current leaderboard based on selected tab
const currentLeaderboard = const currentLeaderboard =
leaderboardType === 'ranked' leaderboardType === 'ranked' ? rankedLeaderboard : vanillaLeaderboard
? rankedLeaderboard.alltime
: vanillaLeaderboard.alltime
// Filter leaderboard by search query // Filter leaderboard by search query
const filteredLeaderboard = currentLeaderboard.filter((entry) => const filteredLeaderboard = currentLeaderboard.filter((entry) =>
entry.name.toLowerCase().includes(searchQuery.toLowerCase()) entry.name.toLowerCase().includes(searchQuery.toLowerCase())
) )
console.log(filteredLeaderboard)
// Sort leaderboard // Sort leaderboard
const sortedLeaderboard = [...filteredLeaderboard].sort((a, b) => { const sortedLeaderboard = [...filteredLeaderboard].sort((a, b) => {
// biome-ignore lint/style/useSingleVarDeclarator: <explanation> // biome-ignore lint/style/useSingleVarDeclarator: <explanation>
@@ -85,8 +83,8 @@ export function LeaderboardPage() {
// 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.data.rank valueA = a.rank
valueB = b.data.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()
@@ -94,8 +92,8 @@ export function LeaderboardPage() {
? valueA.localeCompare(valueB) ? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA) : valueB.localeCompare(valueA)
} else { } else {
valueA = a.data[sortColumn as keyof typeof a.data] as number valueA = a[sortColumn as keyof typeof a] as number
valueB = b.data[sortColumn as keyof typeof b.data] 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
@@ -118,12 +116,11 @@ export function LeaderboardPage() {
if (rank === 3) return <Medal className='h-5 w-5 text-amber-700' /> if (rank === 3) return <Medal className='h-5 w-5 text-amber-700' />
return null return null
} }
console.log(currentLeaderboard)
return ( return (
<div className='min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900'> <div className='flex min-h-screen flex-col bg-gradient-to-b from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900'>
<div className='container mx-auto px-4 py-8'> <div className='container mx-auto flex flex-1 flex-col px-4 py-4'>
<Card className='overflow-hidden border-none bg-white py-0 shadow-lg dark:bg-slate-900'> <Card className='flex flex-1 flex-col overflow-hidden border-none bg-white p-0 shadow-lg dark:bg-slate-900'>
<CardHeader className='bg-gradient-to-r from-violet-500 to-purple-600 p-6'> <CardHeader className='bg-gradient-to-r from-violet-500 to-purple-600 p-6'>
<div className='flex flex-col items-center justify-between gap-4 md:flex-row'> <div className='flex flex-col items-center justify-between gap-4 md:flex-row'>
<div> <div>
@@ -157,12 +154,12 @@ export function LeaderboardPage() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className='p-0'> <CardContent className='flex flex-1 flex-col p-0'>
<Tabs <Tabs
defaultValue={leaderboardType} defaultValue={leaderboardType}
value={leaderboardType} value={leaderboardType}
onValueChange={handleTabChange} onValueChange={handleTabChange}
className='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 flex-col items-start justify-between gap-4 sm:flex-row sm:items-center'>
<TabsList className='bg-slate-100 dark:bg-slate-800'> <TabsList className='bg-slate-100 dark:bg-slate-800'>
@@ -181,7 +178,7 @@ export function LeaderboardPage() {
</div> </div>
</div> </div>
<TabsContent value='ranked' className='m-0'> <TabsContent value='ranked' className='m-0 flex flex-1 flex-col'>
<LeaderboardTable <LeaderboardTable
leaderboard={sortedLeaderboard} leaderboard={sortedLeaderboard}
sortColumn={sortColumn} sortColumn={sortColumn}
@@ -192,7 +189,7 @@ export function LeaderboardPage() {
/> />
</TabsContent> </TabsContent>
<TabsContent value='vanilla' className='m-0'> <TabsContent value='vanilla' className='m-0 flex flex-1 flex-col'>
<LeaderboardTable <LeaderboardTable
leaderboard={sortedLeaderboard} leaderboard={sortedLeaderboard}
sortColumn={sortColumn} sortColumn={sortColumn}
@@ -230,18 +227,20 @@ function LeaderboardTable({
const tableContainerRef = useRef<HTMLDivElement>(null) const tableContainerRef = useRef<HTMLDivElement>(null)
// Set a fixed row height for virtualization // Set a fixed row height for virtualization
const ROW_HEIGHT = 56 // Adjust based on your actual row height const ROW_HEIGHT = 39 // Adjust based on your actual row height
console.log(leaderboard.length)
// Create virtualizer instance // Create virtualizer instance
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: leaderboard.length, count: leaderboard.length,
getScrollElement: () => tableContainerRef.current, getScrollElement: () => tableContainerRef.current,
estimateSize: () => ROW_HEIGHT, estimateSize: () => ROW_HEIGHT,
overscan: 10, // Number of items to render before/after the visible area overscan: 12, // Number of items to render before/after the visible area
}) })
// Get the virtualized rows // Get the virtualized rows
const virtualRows = rowVirtualizer.getVirtualItems() const virtualRows = rowVirtualizer.getVirtualItems()
console.log({ virtualRows })
console.log(rowVirtualizer.getTotalSize())
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start : 0 const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start : 0
const paddingBottom = const paddingBottom =
virtualRows.length > 0 virtualRows.length > 0
@@ -249,15 +248,15 @@ function LeaderboardTable({
(virtualRows?.[virtualRows.length - 1]?.end ?? 0) (virtualRows?.[virtualRows.length - 1]?.end ?? 0)
: 0 : 0
return ( return (
<div className='overflow-hidden rounded-lg border'> <div className='flex flex-1 flex-col overflow-hidden rounded-lg border'>
<div <div
ref={tableContainerRef} ref={tableContainerRef}
className='overflow-auto' className='flex-1 overflow-auto overflow-x-auto'
style={{ height: '500px', maxHeight: '70vh' }} style={{ maxHeight: 'calc(100vh - 300px)' }}
> >
<Table> <Table>
<TableHeader className='sticky top-0 z-10 bg-white dark:bg-slate-900'> <TableHeader className='sticky top-0 z-10 bg-white dark:bg-slate-900'>
<TableRow className='bg-slate-50 dark:bg-slate-800/50'> <TableRow className=' bg-slate-50 dark:bg-slate-800/50'>
<TableHead className='w-[80px]'> <TableHead className='w-[80px]'>
<SortableHeader <SortableHeader
column='rank' column='rank'
@@ -278,6 +277,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='mmr' column='mmr'
label='MMR' label='MMR'
currentSort={sortColumn} currentSort={sortColumn}
@@ -285,8 +285,9 @@ function LeaderboardTable({
onSort={onSort} onSort={onSort}
/> />
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right' align={'right'}>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='peak_mmr' column='peak_mmr'
label='Peak MMR' label='Peak MMR'
currentSort={sortColumn} currentSort={sortColumn}
@@ -296,6 +297,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='winrate' column='winrate'
label='Win Rate' label='Win Rate'
currentSort={sortColumn} currentSort={sortColumn}
@@ -305,6 +307,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='wins' column='wins'
label='Wins' label='Wins'
currentSort={sortColumn} currentSort={sortColumn}
@@ -314,6 +317,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='losses' column='losses'
label='Losses' label='Losses'
currentSort={sortColumn} currentSort={sortColumn}
@@ -323,6 +327,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='totalgames' column='totalgames'
label='Games' label='Games'
currentSort={sortColumn} currentSort={sortColumn}
@@ -332,6 +337,7 @@ function LeaderboardTable({
</TableHead> </TableHead>
<TableHead className='text-right'> <TableHead className='text-right'>
<SortableHeader <SortableHeader
className='w-full justify-end'
column='streak' column='streak'
label='Streak' label='Streak'
currentSort={sortColumn} currentSort={sortColumn}
@@ -342,96 +348,105 @@ function LeaderboardTable({
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{paddingTop > 0 && (
<tr>
<td style={{ height: `${paddingTop}px` }} colSpan={9} />
</tr>
)}
{leaderboard.length > 0 ? ( {leaderboard.length > 0 ? (
leaderboard.map((entry) => { virtualRows.map((virtualRow) => {
const winrate = entry.data.winrate * 100 const entry = leaderboard[virtualRow.index]
const winrate = entry.winrate * 100
return ( return (
<TableRow <Fragment key={entry.id}>
key={entry.id} {/* Add padding to the top to push content into view */}
className={cn(
'transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/70', <TableRow
entry.data.rank <= 3 && className={cn(
'bg-amber-50/50 dark:bg-amber-950/20' 'transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/70'
)}
>
<TableCell className='font-medium'>
<div className='flex items-center gap-1.5'>
{getMedal(entry.data.rank)}
<span>{entry.data.rank}</span>
</div>
</TableCell>
<TableCell>
<Link
href={`/players/${entry.id}`}
className='flex items-center gap-2 hover:underline'
>
{/*<Avatar className='h-8 w-8'>*/}
{/* <AvatarImage*/}
{/* src={`https://cdn.discordapp.com/avatars/${entry.id}/avatar.png`}*/}
{/* alt={entry.name}*/}
{/* />*/}
{/* <AvatarFallback className='bg-violet-100 text-violet-700 text-xs dark:bg-violet-900 dark:text-violet-300'>*/}
{/* {entry.name.slice(0, 2).toUpperCase()}*/}
{/* </AvatarFallback>*/}
{/*</Avatar>*/}
<span className='font-medium'>{entry.name}</span>
{entry.data.streak >= 3 && (
<Badge className='bg-orange-500 text-white'>
<Flame className='mr-1 h-3 w-3' />
Hot Streak
</Badge>
)}
</Link>
</TableCell>
<TableCell className='text-right font-medium'>
{Math.round(entry.data.mmr)}
</TableCell>
<TableCell className='text-right'>
<div className='flex items-center justify-end gap-1'>
<TrendingUp className='h-3.5 w-3.5 text-violet-500' />
{Math.round(entry.data.peak_mmr)}
</div>
</TableCell>
<TableCell className='text-right'>
<Badge
variant='outline'
className={cn(
'font-normal',
winrate > 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)}%
</Badge>
</TableCell>
<TableCell className='text-right text-emerald-600 dark:text-emerald-400'>
{entry.data.wins}
</TableCell>
<TableCell className='text-right text-rose-600 dark:text-rose-400'>
{entry.data.losses}
</TableCell>
<TableCell className='text-right text-slate-600 dark:text-slate-400'>
{entry.data.totalgames}
</TableCell>
<TableCell className='text-right'>
{entry.data.streak > 0 ? (
<span className='flex items-center justify-end text-emerald-600 dark:text-emerald-400'>
<ArrowUp className='mr-1 h-3.5 w-3.5' />
{entry.data.streak}
</span>
) : entry.data.streak < 0 ? (
<span className='flex items-center justify-end text-rose-600 dark:text-rose-400'>
<ArrowDown className='mr-1 h-3.5 w-3.5' />
{Math.abs(entry.data.streak)}
</span>
) : (
<span>0</span>
)} )}
</TableCell> >
</TableRow> <TableCell className='font-medium'>
<div className='flex items-center gap-1.5'>
{getMedal(entry.rank)}
<span>{entry.rank}</span>
</div>
</TableCell>
<TableCell>
<Link
href={`/players/${entry.id}`}
className='flex items-center gap-2 hover:underline'
>
{/*<Avatar className='h-8 w-8'>*/}
{/* <AvatarImage*/}
{/* src={`https://cdn.discordapp.com/avatars/${entry.id}/avatar.png`}*/}
{/* alt={entry.name}*/}
{/* />*/}
{/* <AvatarFallback className='bg-violet-100 text-violet-700 text-xs dark:bg-violet-900 dark:text-violet-300'>*/}
{/* {entry.name.slice(0, 2).toUpperCase()}*/}
{/* </AvatarFallback>*/}
{/*</Avatar>*/}
<span className='font-medium'>{entry.name}</span>
{entry.streak >= 3 && (
<Badge className='bg-orange-500 text-white'>
<Flame className='mr-1 h-3 w-3' />
Hot Streak
</Badge>
)}
</Link>
</TableCell>
<TableCell className='text-right font-medium font-mono'>
{Math.round(entry.mmr)}
</TableCell>
<TableCell className='text-right font-mono'>
<div className='flex items-center justify-end gap-1'>
{Math.round(entry.peak_mmr)}
<TrendingUp className='h-3.5 w-3.5 text-violet-500' />
</div>
</TableCell>
<TableCell className='text-right'>
<Badge
variant='outline'
className={cn(
'font-normal ',
winrate > 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)}%
</Badge>
</TableCell>
<TableCell className='text-right text-emerald-600 dark:text-emerald-400'>
{entry.wins}
</TableCell>
<TableCell className='text-right text-rose-600 dark:text-rose-400'>
{entry.losses}
</TableCell>
<TableCell className='text-right font-mono text-slate-600 dark:text-slate-400'>
{entry.totalgames}
</TableCell>
<TableCell className='text-right'>
{entry.streak > 0 ? (
<span className='flex items-center justify-end text-emerald-600 dark:text-emerald-400'>
<ArrowUp className='mr-1 h-3.5 w-3.5' />
{entry.streak}
</span>
) : entry.streak < 0 ? (
<span className='flex items-center justify-end font-mono text-rose-600 dark:text-rose-400'>
<ArrowDown className='mr-1 h-3.5 w-3.5' />
<span className={'w-[2ch]'}>
{Math.abs(entry.streak)}
</span>
</span>
) : (
<span>0</span>
)}
</TableCell>
</TableRow>
</Fragment>
) )
}) })
) : ( ) : (
@@ -441,6 +456,11 @@ function LeaderboardTable({
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{paddingBottom > 0 && (
<tr>
<td style={{ height: `${paddingBottom}px` }} colSpan={9} />
</tr>
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@@ -448,7 +468,7 @@ function LeaderboardTable({
) )
} }
interface SortableHeaderProps { interface SortableHeaderProps extends ComponentPropsWithoutRef<'button'> {
column: string column: string
label: string label: string
currentSort: string currentSort: string
@@ -462,12 +482,19 @@ function SortableHeader({
currentSort, currentSort,
direction, direction,
onSort, onSort,
className,
...rest
}: SortableHeaderProps) { }: SortableHeaderProps) {
const isActive = currentSort === column const isActive = currentSort === column
return ( return (
<button <button
className='flex items-center gap-1 transition-colors hover:text-violet-600 dark:hover:text-violet-400' type={'button'}
className={cn(
'flex items-center gap-1 transition-colors hover:text-violet-600 dark:hover:text-violet-400',
className
)}
{...rest}
onClick={() => onSort(column)} onClick={() => onSort(column)}
> >
{label} {label}

View File

@@ -1,5 +1,4 @@
import { LeaderboardPage } from '@/app/_components/leaderboard' import { LeaderboardPage } from '@/app/_components/leaderboard'
import { UserStats } from '@/app/_components/user-stats'
import { auth } from '@/server/auth' import { auth } from '@/server/auth'
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
import { HydrateClient, api } from '@/trpc/server' import { HydrateClient, api } from '@/trpc/server'
@@ -19,11 +18,11 @@ export default async function Home() {
} }
return ( return (
<HydrateClient> <Suspense>
<Suspense> <HydrateClient>
{/*<UserStats/>*/} {/*<UserStats/>*/}
<LeaderboardPage /> <LeaderboardPage />
</Suspense> </HydrateClient>
</HydrateClient> </Suspense>
) )
} }

View File

@@ -25,7 +25,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants'
import { api } from '@/trpc/react' import { api } from '@/trpc/react'
import { isNotNull } from 'drizzle-orm'
import { import {
ArrowDownCircle, ArrowDownCircle,
ArrowUpCircle, ArrowUpCircle,
@@ -84,8 +83,6 @@ export function UserInfo() {
user_id: id, user_id: id,
}) })
console.log(rankedLeaderboard, vanillaLeaderboard)
// Filter games by leaderboard if needed // Filter games by leaderboard if needed
const filteredGamesByLeaderboard = const filteredGamesByLeaderboard =
leaderboardFilter === 'all' leaderboardFilter === 'all'

View File

@@ -32,7 +32,6 @@ export async function syncHistory() {
.where(eq(metadata.key, 'history_cursor')) .where(eq(metadata.key, 'history_cursor'))
.limit(1) .limit(1)
.then((res) => res[0]) .then((res) => res[0])
console.log('cursor', cursor)
const data = await ky const data = await ky
.get('https://api.neatqueue.com/api/history/1226193436521267223', { .get('https://api.neatqueue.com/api/history/1226193436521267223', {
searchParams: { searchParams: {
@@ -64,9 +63,6 @@ export async function syncHistory() {
value: firstGame, value: firstGame,
}, },
}) })
console.log('matches', matches)
console.log('firstGame', firstGame)
console.log('data', data)
const chunkedData = chunk(data.data, 100) const chunkedData = chunk(data.data, 100)
for (const chunk of chunkedData) { for (const chunk of chunkedData) {

View File

@@ -1,9 +1,6 @@
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc' import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'
import { LeaderboardService } from '@/server/services/leaderboard' import { LeaderboardService } from '@/server/services/leaderboard'
import { import type { LeaderboardResponse } from '@/server/services/neatqueue.service'
type LeaderboardResponse,
neatqueue_service,
} from '@/server/services/neatqueue.service'
import { z } from 'zod' import { z } from 'zod'
const service = new LeaderboardService() const service = new LeaderboardService()
@@ -17,7 +14,7 @@ export const leaderboard_router = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return (await service.getLeaderboard( return (await service.getLeaderboard(
input.channel_id input.channel_id
)) as LeaderboardResponse )) as LeaderboardResponse['alltime']
}), }),
get_user_rank: publicProcedure get_user_rank: publicProcedure
.input( .input(

View File

@@ -21,18 +21,12 @@ export class LeaderboardService {
const pipeline = redis.pipeline() const pipeline = redis.pipeline()
pipeline.del(zsetKey) // clear existing pipeline.del(zsetKey) // clear existing
for (const entry of fresh.alltime) { for (const entry of fresh) {
// store by mmr for ranking // store by mmr for ranking
pipeline.zadd(zsetKey, entry.data.mmr, entry.id) pipeline.zadd(zsetKey, entry.rank, entry.id)
// store user data separately for quick lookups // store user data separately for quick lookups
pipeline.hset(`user:${entry.id}`, { pipeline.hset(`user:${entry.id}`, entry)
name: entry.name,
mmr: entry.data.mmr,
wins: entry.data.wins,
losses: entry.data.losses,
// add other fields you need for quick lookup
})
} }
pipeline.expire(zsetKey, 180) pipeline.expire(zsetKey, 180)

View File

@@ -11,11 +11,23 @@ const BMM_SERVER_ID = '1226193436521267223'
export const neatqueue_service = { export const neatqueue_service = {
get_leaderboard: async (channel_id: string) => { get_leaderboard: async (channel_id: string) => {
const response = await instance.get( const res = await instance
`leaderboard/${BMM_SERVER_ID}/${channel_id}` .get(`leaderboard/${BMM_SERVER_ID}/${channel_id}`)
) .json<LeaderboardResponse>()
return response.json<LeaderboardResponse>() //desc
res.alltime.sort((a, b) => b.data.mmr - a.data.mmr)
const fixed: Array<Data & { id: string; name: string }> = res.alltime.map(
(entry, idx) => {
return {
...entry.data,
rank: idx + 1,
id: entry.id,
name: entry.name,
}
}
)
return fixed
}, },
get_history: async ( get_history: async (
player_ids: string[], player_ids: string[],