mirror of
https://github.com/ershisan99/www.git
synced 2025-12-24 05:19:23 +00:00
adjust styling and store users profiles according to the leaderboard
This commit is contained in:
@@ -124,7 +124,7 @@ export function LeaderboardPage() {
|
|||||||
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'>
|
||||||
<div className='container mx-auto flex flex-1 flex-col'>
|
<div className='container mx-auto flex flex-1 flex-col'>
|
||||||
<div className='flex flex-1 flex-col overflow-hidden border-none bg-white p-0 shadow-lg dark:bg-zinc-900'>
|
<div className='flex flex-1 flex-col overflow-hidden border-none dark:bg-zinc-900'>
|
||||||
<div className='border-gray-200 border-b bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900'>
|
<div className='border-gray-200 border-b bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900'>
|
||||||
<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>
|
||||||
@@ -147,16 +147,6 @@ export function LeaderboardPage() {
|
|||||||
{currentLeaderboard.length} Players
|
{currentLeaderboard.length} Players
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
size='sm'
|
|
||||||
className='border-gray-200 dark:border-zinc-700'
|
|
||||||
>
|
|
||||||
<Info className='mr-1 h-4 w-4 text-gray-500 dark:text-zinc-400' />
|
|
||||||
<span className='text-gray-700 dark:text-zinc-300'>
|
|
||||||
How Rankings Work
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Filter,
|
Filter,
|
||||||
|
IceCreamCone,
|
||||||
MinusCircle,
|
MinusCircle,
|
||||||
|
ShieldHalf,
|
||||||
Star,
|
Star,
|
||||||
Trophy,
|
Trophy,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useFormatter } from 'next-intl'
|
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { isNonNullish } from 'remeda'
|
import { isNonNullish } from 'remeda'
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export function UserInfo() {
|
|||||||
channel_id: RANKED_CHANNEL,
|
channel_id: RANKED_CHANNEL,
|
||||||
user_id: id,
|
user_id: id,
|
||||||
})
|
})
|
||||||
|
console.log({ vanillaUserRank, rankedUserRank })
|
||||||
|
|
||||||
// Filter games by leaderboard if needed
|
// Filter games by leaderboard if needed
|
||||||
const filteredGamesByLeaderboard =
|
const filteredGamesByLeaderboard =
|
||||||
@@ -116,10 +118,10 @@ export function UserInfo() {
|
|||||||
const firstGame = games.at(-1)
|
const firstGame = games.at(-1)
|
||||||
|
|
||||||
// Get last games for each leaderboard
|
// Get last games for each leaderboard
|
||||||
const lastGameLeaderboard1 = games
|
const lastRankedGame = games
|
||||||
.filter((game) => game.gameType === 'ranked')
|
.filter((game) => game.gameType === 'ranked')
|
||||||
.at(0)
|
.at(0)
|
||||||
const lastGameLeaderboard2 = games
|
const lastVanillaGame = games
|
||||||
.filter((game) => game.gameType.toLowerCase() === 'vanilla')
|
.filter((game) => game.gameType.toLowerCase() === 'vanilla')
|
||||||
.at(0)
|
.at(0)
|
||||||
|
|
||||||
@@ -183,110 +185,112 @@ export function UserInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div className='flex flex-1 justify-end'>
|
className={cn(
|
||||||
<div className='flex gap-3'>
|
'grid w-full flex-grow grid-cols-2 divide-gray-100 md:w-auto md:grid-cols-3 md:divide-y-0 dark:divide-zinc-800',
|
||||||
{lastGameLeaderboard1 && (
|
isNonNullish(rankedUserRank?.mmr) && 'lg:grid-cols-5',
|
||||||
<div className='hidden rounded-lg border border-gray-200 bg-gray-50 p-3 md:block dark:border-zinc-700 dark:bg-zinc-800'>
|
isNonNullish(vanillaUserRank?.mmr) && 'lg:grid-cols-5',
|
||||||
<div className='font-medium text-gray-500 text-sm dark:text-zinc-400'>
|
isNonNullish(rankedUserRank?.mmr) &&
|
||||||
Ranked Queue MMR
|
isNonNullish(vanillaUserRank?.mmr) &&
|
||||||
</div>
|
'lg:grid-cols-6'
|
||||||
<div className='font-bold text-2xl text-gray-900 dark:text-white'>
|
)}
|
||||||
{Math.trunc(
|
>
|
||||||
lastGameLeaderboard1.playerMmr +
|
<StatsCard
|
||||||
lastGameLeaderboard1.mmrChange
|
title='Games'
|
||||||
)}
|
value={profileData.games}
|
||||||
</div>
|
icon={<BarChart3 className='h-5 w-5 text-violet-500' />}
|
||||||
<div className='text-gray-500 text-xs dark:text-zinc-400'>
|
description='Total matches'
|
||||||
{lastGameLeaderboard1.mmrChange > 0 ? (
|
/>
|
||||||
<span className='flex items-center text-emerald-500'>
|
<StatsCard
|
||||||
|
title='Wins'
|
||||||
|
value={profileData.wins}
|
||||||
|
icon={<ArrowUpCircle className='h-5 w-5 text-emerald-500' />}
|
||||||
|
description={`${profileData.winRate}% win rate`}
|
||||||
|
accentColor='text-emerald-500'
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title='Losses'
|
||||||
|
value={profileData.losses}
|
||||||
|
icon={<ArrowDownCircle className='h-5 w-5 text-rose-500' />}
|
||||||
|
description={`${profileData.games > 0 ? Math.round((profileData.losses / profileData.games) * 100) : 0}% loss rate`}
|
||||||
|
accentColor='text-rose-500'
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title='Ties'
|
||||||
|
value={profileData.ties}
|
||||||
|
icon={<MinusCircle className='h-5 w-5 text-amber-500' />}
|
||||||
|
description={`${profileData.games > 0 ? Math.round((profileData.ties / profileData.games) * 100) : 0}% tie rate`}
|
||||||
|
accentColor='text-amber-500'
|
||||||
|
/>
|
||||||
|
{isNonNullish(rankedUserRank?.mmr) && (
|
||||||
|
<StatsCard
|
||||||
|
title='Ranked MMR'
|
||||||
|
value={Math.round(rankedUserRank.mmr)}
|
||||||
|
description={
|
||||||
|
lastRankedGame ? (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center',
|
||||||
|
lastRankedGame.mmrChange > 0
|
||||||
|
? 'text-emerald-500'
|
||||||
|
: 'text-rose-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{lastRankedGame.mmrChange > 0 ? (
|
||||||
<ChevronUp className='h-3 w-3' />
|
<ChevronUp className='h-3 w-3' />
|
||||||
{numberFormatter.format(
|
) : (
|
||||||
Math.trunc(lastGameLeaderboard1.mmrChange)
|
|
||||||
)}{' '}
|
|
||||||
last match
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className='flex items-center text-rose-500'>
|
|
||||||
<ChevronDown className='h-3 w-3' />
|
<ChevronDown className='h-3 w-3' />
|
||||||
{numberFormatter.format(
|
)}
|
||||||
Math.trunc(lastGameLeaderboard1.mmrChange)
|
{numberFormatter.format(
|
||||||
)}{' '}
|
Math.trunc(lastRankedGame.mmrChange)
|
||||||
last match
|
)}{' '}
|
||||||
</span>
|
last match
|
||||||
)}
|
</span>
|
||||||
</div>
|
) : null
|
||||||
</div>
|
}
|
||||||
)}
|
icon={
|
||||||
|
<ShieldHalf className='h-5 w-5 text-zink-800 dark:text-zink-200' />
|
||||||
{lastGameLeaderboard2 && (
|
}
|
||||||
<div className='hidden rounded-lg border border-gray-200 bg-gray-50 p-3 md:block dark:border-zinc-700 dark:bg-zinc-800'>
|
accentColor='text-zink-800 dark:text-zink-200'
|
||||||
<div className='font-medium text-gray-500 text-sm dark:text-zinc-400'>
|
/>
|
||||||
Vanilla Queue MMR
|
)}
|
||||||
</div>
|
{isNonNullish(vanillaUserRank?.mmr) && (
|
||||||
<div className='font-bold text-2xl text-gray-900 dark:text-white'>
|
<StatsCard
|
||||||
{Math.trunc(
|
title='Vanilla MMR'
|
||||||
lastGameLeaderboard2.playerMmr +
|
value={Math.round(vanillaUserRank.mmr)}
|
||||||
lastGameLeaderboard2.mmrChange
|
icon={
|
||||||
)}
|
<IceCreamCone className='h-5 w-5 text-zink-800 dark:text-zink-200' />
|
||||||
</div>
|
}
|
||||||
<div className='text-gray-500 text-xs dark:text-zinc-400'>
|
accentColor='text-zink-800 dark:text-zink-200'
|
||||||
{lastGameLeaderboard2.mmrChange > 0 ? (
|
description={
|
||||||
<span className='flex items-center text-emerald-500'>
|
lastVanillaGame ? (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center',
|
||||||
|
lastVanillaGame.mmrChange > 0
|
||||||
|
? 'text-emerald-500'
|
||||||
|
: 'text-rose-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{lastVanillaGame.mmrChange > 0 ? (
|
||||||
<ChevronUp className='h-3 w-3' />
|
<ChevronUp className='h-3 w-3' />
|
||||||
{numberFormatter.format(
|
) : (
|
||||||
Math.trunc(lastGameLeaderboard2.mmrChange)
|
|
||||||
)}{' '}
|
|
||||||
last match
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className='flex items-center text-rose-500'>
|
|
||||||
<ChevronDown className='h-3 w-3' />
|
<ChevronDown className='h-3 w-3' />
|
||||||
{numberFormatter.format(
|
)}
|
||||||
Math.trunc(lastGameLeaderboard2.mmrChange)
|
{numberFormatter.format(
|
||||||
)}{' '}
|
Math.trunc(lastVanillaGame.mmrChange)
|
||||||
last match
|
)}{' '}
|
||||||
</span>
|
last match
|
||||||
)}
|
</span>
|
||||||
</div>
|
) : null
|
||||||
</div>
|
}
|
||||||
)}
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className='p-0'>
|
<CardContent className='p-0'>
|
||||||
<div className='grid grid-cols-2 divide-x divide-y divide-gray-100 md:grid-cols-4 md:divide-y-0 dark:divide-zinc-800'>
|
|
||||||
<StatsCard
|
|
||||||
title='Games'
|
|
||||||
value={profileData.games}
|
|
||||||
icon={<BarChart3 className='h-5 w-5 text-violet-500' />}
|
|
||||||
description='Total matches'
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title='Wins'
|
|
||||||
value={profileData.wins}
|
|
||||||
icon={<ArrowUpCircle className='h-5 w-5 text-emerald-500' />}
|
|
||||||
description={`${profileData.winRate}% win rate`}
|
|
||||||
accentColor='text-emerald-500'
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title='Losses'
|
|
||||||
value={profileData.losses}
|
|
||||||
icon={<ArrowDownCircle className='h-5 w-5 text-rose-500' />}
|
|
||||||
description={`${profileData.games > 0 ? Math.round((profileData.losses / profileData.games) * 100) : 0}% loss rate`}
|
|
||||||
accentColor='text-rose-500'
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title='Ties'
|
|
||||||
value={profileData.ties}
|
|
||||||
icon={<MinusCircle className='h-5 w-5 text-amber-500' />}
|
|
||||||
description={`${profileData.games > 0 ? Math.round((profileData.ties / profileData.games) * 100) : 0}% tie rate`}
|
|
||||||
accentColor='text-amber-500'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue='matches' className='p-6'>
|
<Tabs defaultValue='matches' className='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-gray-100 dark:bg-zinc-800'>
|
<TabsList className='bg-gray-100 dark:bg-zinc-800'>
|
||||||
@@ -338,15 +342,15 @@ export function UserInfo() {
|
|||||||
|
|
||||||
<TabsContent value='stats' className='m-0'>
|
<TabsContent value='stats' className='m-0'>
|
||||||
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
|
||||||
{(rankedLeaderboard || lastGameLeaderboard1) && (
|
{(rankedLeaderboard || lastRankedGame) && (
|
||||||
<LeaderboardStatsCard
|
<LeaderboardStatsCard
|
||||||
title='Ranked Queue Stats'
|
title='Ranked Queue Stats'
|
||||||
rank={rankedUserRank?.rank}
|
rank={rankedUserRank?.rank}
|
||||||
mmr={
|
mmr={
|
||||||
lastGameLeaderboard1
|
lastRankedGame
|
||||||
? Math.trunc(
|
? Math.trunc(
|
||||||
lastGameLeaderboard1.playerMmr +
|
lastRankedGame.playerMmr +
|
||||||
lastGameLeaderboard1.mmrChange
|
lastRankedGame.mmrChange
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -355,15 +359,15 @@ export function UserInfo() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(vanillaLeaderboard || lastGameLeaderboard2) && (
|
{(vanillaLeaderboard || lastVanillaGame) && (
|
||||||
<LeaderboardStatsCard
|
<LeaderboardStatsCard
|
||||||
title='Vanilla Queue Stats'
|
title='Vanilla Queue Stats'
|
||||||
rank={vanillaUserRank?.rank}
|
rank={vanillaUserRank?.rank}
|
||||||
mmr={
|
mmr={
|
||||||
lastGameLeaderboard2
|
lastVanillaGame
|
||||||
? Math.trunc(
|
? Math.trunc(
|
||||||
lastGameLeaderboard2.playerMmr +
|
lastVanillaGame.playerMmr +
|
||||||
lastGameLeaderboard2.mmrChange
|
lastVanillaGame.mmrChange
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@@ -374,8 +378,8 @@ export function UserInfo() {
|
|||||||
|
|
||||||
{!rankedLeaderboard &&
|
{!rankedLeaderboard &&
|
||||||
!vanillaLeaderboard &&
|
!vanillaLeaderboard &&
|
||||||
!lastGameLeaderboard1 &&
|
!lastRankedGame &&
|
||||||
!lastGameLeaderboard2 && (
|
!lastVanillaGame && (
|
||||||
<div className='col-span-2 flex h-40 items-center justify-center rounded-lg border bg-gray-50 dark:bg-zinc-800/50'>
|
<div className='col-span-2 flex h-40 items-center justify-center rounded-lg border bg-gray-50 dark:bg-zinc-800/50'>
|
||||||
<p className='text-gray-500 dark:text-zinc-400'>
|
<p className='text-gray-500 dark:text-zinc-400'>
|
||||||
No leaderboard data available
|
No leaderboard data available
|
||||||
@@ -404,7 +408,7 @@ interface StatsCardProps {
|
|||||||
title: string
|
title: string
|
||||||
value: number
|
value: number
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
description: string
|
description: React.ReactNode
|
||||||
accentColor?: string
|
accentColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,12 +420,14 @@ function StatsCard({
|
|||||||
accentColor = 'text-violet-500',
|
accentColor = 'text-violet-500',
|
||||||
}: StatsCardProps) {
|
}: StatsCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col items-center p-6 text-center'>
|
<div className='flex w-fit flex-col items-start justify-self-center p-2 text-center md:justify-self-auto'>
|
||||||
<div className='mb-2 flex items-center justify-center'>{icon}</div>
|
<h3 className='mb-1 text-nowrap font-medium text-gray-500 text-sm dark:text-zinc-400'>
|
||||||
<h3 className='mb-1 font-medium text-gray-500 text-sm dark:text-zinc-400'>
|
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={cn('font-bold text-3xl', accentColor)}>{value}</p>
|
<div className={'flex items-center gap-2'}>
|
||||||
|
<div className='flex items-center justify-center'>{icon}</div>
|
||||||
|
<p className={cn('font-bold text-3xl', accentColor)}>{value}</p>
|
||||||
|
</div>
|
||||||
<p className='mt-1 text-gray-500 text-xs dark:text-zinc-400'>
|
<p className='mt-1 text-gray-500 text-xs dark:text-zinc-400'>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { redis } from '../redis'
|
import { redis } from '../redis'
|
||||||
import { neatqueue_service } from './neatqueue.service'
|
import { neatqueue_service } from './neatqueue.service'
|
||||||
|
|
||||||
export class LeaderboardService {
|
export class LeaderboardService {
|
||||||
private getZSetKey(channel_id: string) {
|
private getZSetKey(channel_id: string) {
|
||||||
return `zset:leaderboard:${channel_id}`
|
return `zset:leaderboard:${channel_id}`
|
||||||
@@ -9,79 +10,68 @@ export class LeaderboardService {
|
|||||||
return `raw:leaderboard:${channel_id}`
|
return `raw:leaderboard:${channel_id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUserKey(user_id: string, channel_id: string) {
|
||||||
|
return `user:${user_id}:${channel_id}`
|
||||||
|
}
|
||||||
|
|
||||||
async refreshLeaderboard(channel_id: string) {
|
async refreshLeaderboard(channel_id: string) {
|
||||||
const fresh = await neatqueue_service.get_leaderboard(channel_id)
|
try {
|
||||||
const zsetKey = this.getZSetKey(channel_id)
|
const fresh = await neatqueue_service.get_leaderboard(channel_id)
|
||||||
const rawKey = this.getRawKey(channel_id)
|
const zsetKey = this.getZSetKey(channel_id)
|
||||||
|
const rawKey = this.getRawKey(channel_id)
|
||||||
|
|
||||||
// store raw data for full queries
|
const pipeline = redis.pipeline()
|
||||||
await redis.setex(rawKey, 180, JSON.stringify(fresh))
|
pipeline.setex(rawKey, 180, JSON.stringify(fresh))
|
||||||
|
pipeline.del(zsetKey)
|
||||||
|
|
||||||
// store sorted set for rank queries
|
for (const entry of fresh) {
|
||||||
const pipeline = redis.pipeline()
|
pipeline.zadd(zsetKey, entry.mmr, entry.id)
|
||||||
pipeline.del(zsetKey) // clear existing
|
pipeline.hset(this.getUserKey(entry.id, channel_id), {
|
||||||
|
...entry,
|
||||||
|
channel_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const entry of fresh) {
|
pipeline.expire(zsetKey, 180)
|
||||||
// store by mmr for ranking
|
await pipeline.exec()
|
||||||
pipeline.zadd(zsetKey, entry.rank, entry.id)
|
|
||||||
|
|
||||||
// store user data separately for quick lookups
|
return fresh
|
||||||
pipeline.hset(`user:${entry.id}`, entry)
|
} catch (error) {
|
||||||
|
console.error('Error refreshing leaderboard:', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline.expire(zsetKey, 180)
|
|
||||||
await pipeline.exec()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLeaderboard(channel_id: string) {
|
async getLeaderboard(channel_id: string) {
|
||||||
const cached = await redis.get(this.getRawKey(channel_id))
|
try {
|
||||||
if (cached) return JSON.parse(cached)
|
const cached = await redis.get(this.getRawKey(channel_id))
|
||||||
|
if (cached) return JSON.parse(cached)
|
||||||
|
|
||||||
// if not cached, refresh and return
|
return await this.refreshLeaderboard(channel_id)
|
||||||
await this.refreshLeaderboard(channel_id)
|
} catch (error) {
|
||||||
// @ts-ignore
|
console.error('Error getting leaderboard:', error)
|
||||||
return redis.get(this.getRawKey(channel_id)).then(JSON.parse)
|
throw error
|
||||||
}
|
|
||||||
|
|
||||||
async getUserRank(channel_id: string, user_id: string) {
|
|
||||||
const zsetKey = this.getZSetKey(channel_id)
|
|
||||||
|
|
||||||
// zrevrank because higher mmr = better rank
|
|
||||||
const rank = await redis.zrevrank(zsetKey, user_id)
|
|
||||||
if (rank === null) return null
|
|
||||||
|
|
||||||
// get user data
|
|
||||||
const userData = await redis.hgetall(`user:${user_id}`)
|
|
||||||
if (!userData) return null
|
|
||||||
|
|
||||||
return {
|
|
||||||
rank: rank + 1, // zero-based -> one-based
|
|
||||||
...userData,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get users around a specific rank
|
async getUserRank(channel_id: string, user_id: string) {
|
||||||
async getRankRange(channel_id: string, rank: number, range = 5) {
|
try {
|
||||||
const zsetKey = this.getZSetKey(channel_id)
|
const zsetKey = this.getZSetKey(channel_id)
|
||||||
|
const rank = await redis.zrevrank(zsetKey, user_id)
|
||||||
|
|
||||||
// get ids
|
if (rank === null) return null
|
||||||
const ids = await redis.zrevrange(
|
|
||||||
zsetKey,
|
|
||||||
Math.max(0, rank - range),
|
|
||||||
rank + range
|
|
||||||
)
|
|
||||||
|
|
||||||
// get data for each id
|
const userData = await redis.hgetall(this.getUserKey(user_id, channel_id))
|
||||||
const pipeline = redis.pipeline()
|
if (!userData) return null
|
||||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
|
||||||
ids.forEach((id) => pipeline.hgetall(`user:${id}`))
|
|
||||||
|
|
||||||
const results = await pipeline.exec()
|
return {
|
||||||
return ids.map((id, i) => ({
|
rank: rank + 1,
|
||||||
id,
|
...userData,
|
||||||
rank: rank - range + i + 1,
|
mmr: Number(userData.mmr),
|
||||||
// @ts-ignore
|
}
|
||||||
...results[i][1],
|
} catch (error) {
|
||||||
}))
|
console.error('Error getting user rank:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user