Skip to content

Commit

Permalink
feat: clean up leaderboard+profile page page & add dropdowns to leade…
Browse files Browse the repository at this point in the history
…rboard page
  • Loading branch information
kevinjosethomas committed Dec 12, 2024
1 parent 56eb733 commit ba290fd
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 29 deletions.
3 changes: 2 additions & 1 deletion src/api/profile/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { buildUrl } from '../utils'
import { PlayerStats } from 'src/types'

export const getPlayerStats = async (name?: string) => {
export const getPlayerStats = async (name?: string): Promise<PlayerStats> => {
const res = await fetch(
buildUrl(`auth/get_player_stats${name ? `/${name}` : ''}`),
)
Expand Down
29 changes: 10 additions & 19 deletions src/components/Leaderboard/LeaderboardColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import Link from 'next/link'
import { LeaderboardEntry } from 'src/components'

interface Props {
id: 'regular' | 'train' | 'turing' | 'hand' | 'brain'
name: 'Regular' | 'Train' | 'Bot/Not' | 'Hand' | 'Brain'
icon: JSX.Element
name: string
ranking: {
display_name: string
elo: number
}[]
}

export const LeaderboardColumn: React.FC<Props> = ({
id,
icon,
name,
ranking,
Expand All @@ -22,24 +24,13 @@ export const LeaderboardColumn: React.FC<Props> = ({
</div>
<div className="flex w-full flex-col">
{ranking.map((player, index) => (
<div
<LeaderboardEntry
key={index}
className={`flex w-full items-center justify-between px-6 py-2 ${index % 2 === 0 ? 'bg-background-1/90' : 'bg-background-1/50'}`}
>
<div className="flex items-center gap-2">
<p className="w-5">{index + 1}</p>
<Link
href={`/profile/${player.display_name}`}
className="flex items-center gap-2 hover:underline"
>
<p>
{player.display_name} {index == 0 && '👑'}
</p>
</Link>
</div>

<p>{player.elo}</p>
</div>
typeId={id}
type={name}
index={index}
{...player}
/>
))}
</div>
</div>
Expand Down
169 changes: 169 additions & 0 deletions src/components/Leaderboard/LeaderboardEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'

import { PlayerStats } from 'src/types'
import { getPlayerStats } from 'src/api'

interface Props {
index: number
typeId: 'regular' | 'train' | 'turing' | 'hand' | 'brain'
type: 'Regular' | 'Train' | 'Bot/Not' | 'Hand' | 'Brain'
display_name: string
elo: number
}

export const LeaderboardEntry = ({
typeId,
type,
index,
display_name,
elo,
}: Props) => {
const [hover, setHover] = useState(false)
const [popup, setPopup] = useState(false)
const [stats, setStats] = useState<PlayerStats | null>(null)

let ratingKey:
| 'regularRating'
| 'trainRating'
| 'botNotRating'
| 'handRating'
| 'brainRating'
let highestRatingKey:
| 'regularMax'
| 'trainMax'
| 'botNotMax'
| 'handMax'
| 'brainMax'
let gamesKey:
| 'regularGames'
| 'trainGames'
| 'botNotCorrect'
| 'handGames'
| 'brainGames'

let gamesWonKey:
| 'regularWins'
| 'trainCorrect'
| 'botNotCorrect'
| 'handWins'
| 'brainWins'

switch (typeId) {
case 'regular':
ratingKey = 'regularRating'
highestRatingKey = 'regularMax'
gamesKey = 'regularGames'
gamesWonKey = 'regularWins'
break
case 'train':
ratingKey = 'trainRating'
highestRatingKey = 'trainMax'
gamesKey = 'trainGames'
gamesWonKey = 'trainCorrect'
break
case 'turing':
ratingKey = 'botNotRating'
highestRatingKey = 'botNotMax'
gamesKey = 'botNotCorrect'
gamesWonKey = 'botNotCorrect'
break
case 'hand':
ratingKey = 'handRating'
highestRatingKey = 'handMax'
gamesKey = 'handGames'
gamesWonKey = 'handWins'
break
case 'brain':
ratingKey = 'brainRating'
highestRatingKey = 'brainMax'
gamesKey = 'brainGames'
gamesWonKey = 'brainWins'
break
default:
ratingKey = 'regularRating'
highestRatingKey = 'regularMax'
gamesKey = 'regularGames'
gamesWonKey = 'regularWins'
break
}

useEffect(() => {
let timer: NodeJS.Timeout
if (hover) {
timer = setTimeout(() => {
fetchStats()
}, 300)
} else {
setPopup(false)
}

return () => clearTimeout(timer)
}, [hover])

const fetchStats = async () => {
try {
const playerStats = await getPlayerStats(display_name)
setStats(playerStats)
setPopup(true)
} catch (error) {
console.error(error)
}
}

return (
<div
className={`relative flex w-full items-center justify-between px-6 py-2 ${index % 2 === 0 ? 'bg-background-1/90' : 'bg-background-1/50'}`}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<div className="flex items-center gap-2">
<p className="w-5">{index + 1}</p>
<Link
href={`/profile/${display_name}`}
className="flex items-center gap-2 hover:underline"
>
<p>
{display_name} {index == 0 && '👑'}
</p>
</Link>
</div>
<p>{elo}</p>
{popup && stats && (
<div className="absolute left-0 top-[100%] z-20 flex w-full max-w-[26rem] flex-col overflow-hidden rounded border border-white/10 bg-background-1">
<div className="flex w-full justify-between bg-backdrop/50 px-4 py-2">
<p>
<span className="font-bold">{display_name}</span>&apos;s {type}{' '}
Statistics
</p>
<Link href={`/profile/${display_name}`}>
<i className="material-symbols-outlined select-none text-lg text-primary hover:text-human-1">
open_in_new
</i>
</Link>
</div>
<div className="flex items-center justify-between px-4 py-2">
<div className="flex flex-col items-center justify-center gap-0.5 text-human-1">
<p className="text-sm xl:text-base">Rating</p>
<b className="text-3xl xl:text-3xl">{stats[ratingKey]}</b>
</div>
<div className="flex flex-col items-center justify-center gap-0.5">
<p className="text-sm xl:text-base">Highest</p>
<b className="text-3xl xl:text-3xl">{stats[highestRatingKey]}</b>
</div>
<div className="flex flex-col items-center justify-center gap-0.5">
<p className="text-sm xl:text-base">Games</p>
<b className="text-3xl xl:text-3xl">{stats[gamesKey]}</b>
</div>
<div className="flex flex-col items-center justify-center gap-0.5">
<p className="text-sm xl:text-base">Win %</p>
<b className="text-3xl xl:text-3xl">
{((stats[gamesWonKey] / stats[gamesKey]) * 100).toFixed(0)}%
</b>
</div>
</div>
</div>
)}
</div>
)
}
1 change: 1 addition & 0 deletions src/components/Leaderboard/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './LeaderboardEntry'
export * from './LeaderboardColumn'
5 changes: 2 additions & 3 deletions src/components/Profile/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import {
RegularPlayIcon,
} from '../Icons/icons'
import { ProfileColumn } from 'src/components'
import { PlayerStats } from 'src/types'

interface Props {
wide?: boolean
stats: {
[key: string]: number
}
stats: PlayerStats
}

export const UserProfile = ({ wide, stats }: Props) => {
Expand Down
10 changes: 8 additions & 2 deletions src/pages/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,41 @@ const Leaderboard: React.FC = () => {
const [leaderboard, setLeaderboard] = useState<
{
icon: JSX.Element
name: string
ranking: { display_name: string; elo: number }[]
name: 'Regular' | 'Train' | 'Bot/Not' | 'Hand' | 'Brain'
id: 'regular' | 'train' | 'turing' | 'hand' | 'brain'
}[]
>()
const fetchLeaderboard = useCallback(async () => {
const lb = await getLeaderboard()
setLastUpdated(new Date(lb.last_updated + 'Z'))
setLeaderboard([
{
id: 'regular',
icon: <RegularPlayIcon />,
name: 'Regular',
ranking: lb.play_leaders,
},
{
id: 'train',
icon: <TrainIcon />,
name: 'Train',
ranking: lb.puzzles_leaders,
},
{
id: 'turing',
icon: <TuringIcon />,
name: 'Bot/Not',
ranking: lb.turing_leaders,
},
{
id: 'hand',
icon: <HandIcon />,
name: 'Hand',
ranking: lb.hand_leaders,
},
{
id: 'brain',
icon: <BrainIcon />,
name: 'Brain',
ranking: lb.brain_leaders,
Expand Down Expand Up @@ -81,7 +87,7 @@ const Leaderboard: React.FC = () => {
: '...'}
</p>
</div>
<div className="grid h-full w-full grid-cols-1 justify-start gap-4 md:grid-cols-3">
<div className="grid h-full w-full grid-cols-1 justify-start gap-4 md:grid-cols-2 lg:grid-cols-3">
{leaderboard?.map((column, index) => (
<LeaderboardColumn key={index} {...column} />
))}
Expand Down
7 changes: 3 additions & 4 deletions src/pages/profile/[name].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextPage } from 'next'
import { useRouter } from 'next/router'
import { useContext, useState, useEffect } from 'react'

import { PlayerStats } from 'src/types'
import { getPlayerStats } from 'src/api'
import { WindowSizeContext } from 'src/contexts'
import { UserIcon } from 'src/components/Icons/icons'
Expand All @@ -12,7 +13,7 @@ const ProfilePage: NextPage = () => {
const router = useRouter()

const [name, setName] = useState('')
const [stats, setStats] = useState({
const [stats, setStats] = useState<PlayerStats>({
regularRating: 0,
regularWins: 0,
regularDraws: 0,
Expand Down Expand Up @@ -77,9 +78,7 @@ const ProfilePage: NextPage = () => {

interface Props {
name: string
stats: {
[key: string]: number
}
stats: PlayerStats
}
const Profile: React.FC<Props> = (props: Props) => {
const { isMobile } = useContext(WindowSizeContext)
Expand Down
40 changes: 40 additions & 0 deletions src/types/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,43 @@ export interface User {
displayName: string
lichessId?: string
}

export interface PlayerStats {
regularRating: number
regularWins: number
regularDraws: number
regularGames: number
regularMax: number
regularMin: number
regularHours: number

handRating: number
handWins: number
handDraws: number
handGames: number
handMax: number
handMin: number
handHours: number

brainRating: number
brainWins: number
brainDraws: number
brainGames: number
brainMax: number
brainMin: number
brainHours: number

trainRating: number
trainCorrect: number
trainGames: number
trainMax: number
trainMin: number
trainHours: number

botNotRating: number
botNotCorrect: number
botNotWrong: number
botNotMax: number
botNotMin: number
botNotHours: number
}

0 comments on commit ba290fd

Please sign in to comment.