Skip to content

Commit 7e518fd

Browse files
feat: improve leaderboard page
1 parent 66e6c2a commit 7e518fd

File tree

4 files changed

+109
-80
lines changed

4 files changed

+109
-80
lines changed

src/components/Leaderboard/LeaderboardColumn.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,43 @@ import { LeaderboardEntry } from 'src/components'
44
interface Props {
55
id: 'regular' | 'puzzles' | 'turing' | 'hand' | 'brain'
66
name: 'Regular' | 'Puzzles' | 'Bot/Not' | 'Hand' | 'Brain'
7-
icon: React.JSX.Element
87
ranking: {
98
display_name: string
109
elo: number
1110
}[]
1211
}
1312

13+
const getIconForType = (id: Props['id']): string => {
14+
switch (id) {
15+
case 'regular':
16+
return 'chess_knight'
17+
case 'puzzles':
18+
return 'toys_and_games'
19+
case 'turing':
20+
return 'mystery'
21+
case 'hand':
22+
return 'back_hand'
23+
case 'brain':
24+
return 'network_intelligence'
25+
default:
26+
return 'chess_knight'
27+
}
28+
}
29+
1430
export const LeaderboardColumn: React.FC<Props> = ({
1531
id,
16-
icon,
1732
name,
1833
ranking,
1934
}: Props) => {
2035
return (
21-
<div className="flex flex-col rounded border border-white/10 bg-background-1/60">
22-
<div className="flex flex-row items-center justify-start gap-2 rounded-t bg-background-2 px-3 py-1.5">
23-
<i className="*:h-4 *:w-4">{icon}</i>
24-
<h2 className="text-lg font-bold uppercase">{name}</h2>
36+
<div className="from-white/8 to-white/4 group relative flex flex-col rounded-md border border-white/10 bg-gradient-to-br backdrop-blur-md">
37+
<div className="flex h-12 flex-row items-center justify-start gap-2 border-b border-white/10 bg-white/5 px-3">
38+
<span className="material-symbols-outlined material-symbols-filled text-2xl text-white/70">
39+
{getIconForType(id)}
40+
</span>
41+
<h2 className="text-base font-bold uppercase text-white/95">{name}</h2>
2542
</div>
26-
<div className="flex w-full flex-col rounded-b">
43+
<div className="flex w-full flex-col">
2744
{ranking.map((player, index) => (
2845
<LeaderboardEntry
2946
key={index}

src/components/Leaderboard/LeaderboardEntry.tsx

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import Link from 'next/link'
2-
import { useCallback, useEffect, useRef, useState } from 'react'
2+
import { useCallback, useEffect, useRef, useState, useContext } from 'react'
33

44
import { PlayerStats } from 'src/types'
55
import { fetchPlayerStats } from 'src/api'
66
import { useLeaderboardContext } from './LeaderboardContext'
7+
import { AuthContext } from 'src/contexts'
78

89
interface Props {
910
index: number
@@ -24,9 +25,11 @@ export const LeaderboardEntry = ({
2425
const [stats, setStats] = useState<PlayerStats | null>(null)
2526
const shouldShowPopupRef = useRef(false)
2627
const { activePopup, setActivePopup } = useLeaderboardContext()
28+
const { user } = useContext(AuthContext)
2729

2830
const entryKey = `${typeId}-${display_name}-${index}`
2931
const isPopupVisible = activePopup === entryKey
32+
const isCurrentUser = user?.lichessId?.toLowerCase() === display_name.toLowerCase()
3033

3134
let ratingKey:
3235
| 'regularRating'
@@ -130,51 +133,57 @@ export const LeaderboardEntry = ({
130133

131134
return (
132135
<div
133-
className={`relative flex w-full items-center justify-between px-3 py-1 text-sm ${index % 2 === 0 ? 'bg-opacity-0' : 'bg-white bg-opacity-[0.015]'}`}
136+
className={`group relative flex w-full items-center justify-between px-3 py-1 text-sm transition-colors duration-200 ${
137+
isCurrentUser
138+
? 'bg-yellow-500/10 hover:bg-yellow-500/15'
139+
: index % 2 === 0
140+
? 'bg-transparent hover:bg-white/3'
141+
: 'bg-white/3 hover:bg-white/5'
142+
}`}
134143
onMouseEnter={() => setHover(true)}
135144
onMouseLeave={() => setHover(false)}
136145
>
137146
<div className="flex items-center gap-1.5">
138-
<p className="w-4 text-xs">{index + 1}</p>
147+
<p className="w-4 text-xs text-white/60">{index + 1}</p>
139148
<Link
140149
href={`/profile/${display_name}`}
141-
className="flex items-center gap-1.5 hover:underline"
150+
className="flex items-center gap-1.5 text-white/90 transition-colors duration-200 hover:text-white hover:underline"
142151
>
143152
<p>
144153
{display_name} {index == 0 && '👑'}
145154
</p>
146155
</Link>
147156
</div>
148-
<p className="text-sm font-medium">{elo}</p>
157+
<p className="text-sm font-medium text-white/95">{elo}</p>
149158
{isPopupVisible && stats && (
150-
<div className="absolute left-0 top-[100%] z-20 flex w-full max-w-[24rem] flex-col overflow-hidden rounded-lg border-2 border-white/20 bg-background-1 outline outline-1 outline-white/5 backdrop-blur-sm">
151-
<div className="flex w-full justify-between border-b border-white/10 bg-gradient-to-r from-background-2/80 to-background-2/60 px-3 py-1.5">
152-
<p className="text-sm">
159+
<div className="absolute left-0 top-[100%] z-50 flex w-full max-w-[24rem] flex-col overflow-hidden rounded border border-white/20 bg-black backdrop-blur-sm">
160+
<div className="flex w-full justify-between border-b border-white/10 bg-black/40 px-3 py-2">
161+
<p className="text-sm text-white/95">
153162
<span className="font-bold">{display_name}</span>&apos;s {type}{' '}
154163
Statistics
155164
</p>
156165
<Link href={`/profile/${display_name}`}>
157-
<i className="material-symbols-outlined select-none text-base text-primary transition-colors hover:text-human-1">
166+
<i className="material-symbols-outlined select-none text-base text-white/70 transition-colors hover:text-white">
158167
open_in_new
159168
</i>
160169
</Link>
161170
</div>
162-
<div className="flex items-center justify-between bg-gradient-to-b from-background-1 to-background-1/95 px-3 py-2">
163-
<div className="flex flex-col items-center justify-center gap-0.5 text-human-1">
164-
<p className="text-xs">Rating</p>
165-
<b className="text-xl">{stats[ratingKey]}</b>
171+
<div className="flex items-center justify-between px-3 py-3">
172+
<div className="flex flex-col items-center justify-center gap-0.5">
173+
<p className="text-xs text-white/70">Rating</p>
174+
<b className="text-xl text-white/95">{stats[ratingKey]}</b>
166175
</div>
167176
<div className="flex flex-col items-center justify-center gap-0.5">
168-
<p className="text-xs">Highest</p>
169-
<b className="text-xl">{stats[highestRatingKey]}</b>
177+
<p className="text-xs text-white/70">Highest</p>
178+
<b className="text-xl text-white/95">{stats[highestRatingKey]}</b>
170179
</div>
171180
<div className="flex flex-col items-center justify-center gap-0.5">
172-
<p className="text-xs">Games</p>
173-
<b className="text-xl">{stats[gamesKey]}</b>
181+
<p className="text-xs text-white/70">Games</p>
182+
<b className="text-xl text-white/95">{stats[gamesKey]}</b>
174183
</div>
175184
<div className="flex flex-col items-center justify-center gap-0.5">
176-
<p className="text-xs">Win %</p>
177-
<b className="text-xl">
185+
<p className="text-xs text-white/70">Win %</p>
186+
<b className="text-xl text-white/95">
178187
{((stats[gamesWonKey] / stats[gamesKey]) * 100).toFixed(0)}%
179188
</b>
180189
</div>

src/components/Profile/UserProfile.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const UserProfile = ({ stats }: Props) => {
2323
}}
2424
/>
2525
<ProfileColumn
26-
icon="network_intel_node"
26+
icon="back_hand"
2727
name="Hand"
2828
data={{
2929
rating: stats.handRating,
@@ -48,7 +48,7 @@ export const UserProfile = ({ stats }: Props) => {
4848
/>
4949
<ProfileColumn
5050
icon="toys_and_games"
51-
name="Train"
51+
name="Puzzles"
5252
data={{
5353
rating: stats.trainRating,
5454
highest: stats.trainMax,

src/pages/leaderboard.tsx

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import Head from 'next/head'
2-
import React, { useCallback, useEffect, useState } from 'react'
2+
import React, { useEffect, useState } from 'react'
33
import { motion, AnimatePresence } from 'framer-motion'
44

5-
import {
6-
BrainIcon,
7-
HandIcon,
8-
RegularPlayIcon,
9-
TrainIcon,
10-
BotOrNotIcon,
11-
} from 'src/components/Common/Icons'
125
import { fetchLeaderboard } from 'src/api'
136
import { LeaderboardColumn, DelayedLoading } from 'src/components'
147
import { LeaderboardProvider } from 'src/components/Leaderboard/LeaderboardContext'
@@ -18,7 +11,6 @@ const Leaderboard: React.FC = () => {
1811
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
1912
const [leaderboard, setLeaderboard] = useState<
2013
{
21-
icon: React.JSX.Element
2214
ranking: { display_name: string; elo: number }[]
2315
name: 'Regular' | 'Puzzles' | 'Bot/Not' | 'Hand' | 'Brain'
2416
id: 'regular' | 'puzzles' | 'turing' | 'hand' | 'brain'
@@ -51,31 +43,29 @@ const Leaderboard: React.FC = () => {
5143
setLeaderboard([
5244
{
5345
id: 'regular',
54-
icon: <RegularPlayIcon />,
5546
name: 'Regular',
5647
ranking: lb.play_leaders,
5748
},
5849
{
5950
id: 'puzzles',
60-
icon: <TrainIcon />,
6151
name: 'Puzzles',
6252
ranking: lb.puzzles_leaders,
6353
},
6454
{
6555
id: 'turing',
66-
icon: <BotOrNotIcon />,
56+
6757
name: 'Bot/Not',
6858
ranking: lb.turing_leaders,
6959
},
7060
{
7161
id: 'hand',
72-
icon: <HandIcon />,
62+
7363
name: 'Hand',
7464
ranking: lb.hand_leaders,
7565
},
7666
{
7767
id: 'brain',
78-
icon: <BrainIcon />,
68+
7969
name: 'Brain',
8070
ranking: lb.brain_leaders,
8171
},
@@ -125,47 +115,60 @@ const Leaderboard: React.FC = () => {
125115
<LeaderboardProvider>
126116
<DelayedLoading isLoading={loading}>
127117
<AnimatePresence mode="wait">
128-
<motion.div
129-
key="leaderboard-content"
130-
variants={containerVariants}
131-
initial="hidden"
132-
animate="visible"
133-
exit="exit"
134-
style={{ willChange: 'transform, opacity' }}
135-
className="mx-auto flex h-full w-[90%] flex-col items-start justify-center gap-3 py-[1%]"
136-
>
137-
<Head>
138-
<title>Leaderboard – Maia Chess</title>
139-
<meta
140-
name="description"
141-
content="View top-ranked players across all Maia Chess game modes. Competitive leaderboards for regular play, puzzles, bot detection, hand & brain chess with live ratings and statistics."
142-
/>
143-
</Head>
144-
<motion.div
145-
variants={itemVariants}
146-
className="flex w-full flex-col items-start justify-between md:flex-row md:items-end"
147-
>
148-
<h1 className="text-2xl font-bold">Rating Leaderboards</h1>
149-
<p className="text-sm text-secondary">
150-
Last updated: {lastUpdated ? getTimeAgo(lastUpdated) : '...'}
151-
</p>
152-
</motion.div>
118+
<>
119+
<div
120+
className="pointer-events-none absolute inset-0"
121+
style={{
122+
background:
123+
'radial-gradient(ellipse 75% 60% at center top, rgba(239, 68, 68, 0.08) 0%, transparent 60%)',
124+
}}
125+
/>
153126
<motion.div
154-
variants={itemVariants}
155-
className="grid h-full w-full grid-cols-1 justify-start gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
127+
key="leaderboard-content"
128+
variants={containerVariants}
129+
initial="hidden"
130+
animate="visible"
131+
exit="exit"
132+
style={{ willChange: 'transform, opacity' }}
133+
className="container relative mx-auto px-6 py-8"
156134
>
157-
{leaderboard?.map((column, index) => (
158-
<LeaderboardColumn key={index} {...column} />
159-
))}
160-
</motion.div>
161-
<motion.div variants={itemVariants} className="my-2 w-full">
162-
<p className="text-xs text-secondary">
163-
<span className="font-medium">Note:</span> Each leaderboard
164-
column only features players who have played atleast one game of
165-
the corresponding type within the last 7 days.
166-
</p>
135+
<Head>
136+
<title>Leaderboard – Maia Chess</title>
137+
<meta
138+
name="description"
139+
content="View top-ranked players across all Maia Chess game modes. Competitive leaderboards for regular play, puzzles, bot detection, hand & brain chess with live ratings and statistics."
140+
/>
141+
</Head>
142+
<motion.div
143+
variants={itemVariants}
144+
className="mb-2 flex w-full flex-col items-start justify-between md:flex-row md:items-center"
145+
>
146+
<h1 className="mb-2 text-3xl font-bold text-white">
147+
Rating Leaderboards
148+
</h1>
149+
150+
<p className="mt-4 text-sm text-white/60 md:mt-0">
151+
Last updated: {lastUpdated ? getTimeAgo(lastUpdated) : '...'}
152+
</p>
153+
</motion.div>
154+
<motion.div
155+
variants={itemVariants}
156+
className="grid w-full grid-cols-1 justify-start gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
157+
>
158+
{leaderboard?.map((column, index) => (
159+
<LeaderboardColumn key={index} {...column} />
160+
))}
161+
</motion.div>
162+
<motion.div variants={itemVariants} className="mt-3 w-full">
163+
<p className="text-xs text-white/60">
164+
<span className="font-medium text-white/80">Note:</span> Each
165+
leaderboard column only features players who have played at
166+
least one game of the corresponding type within the last 7
167+
days.
168+
</p>
169+
</motion.div>
167170
</motion.div>
168-
</motion.div>
171+
</>
169172
</AnimatePresence>
170173
</DelayedLoading>
171174
</LeaderboardProvider>

0 commit comments

Comments
 (0)