From ba290fd520ba6c1561bf8b8f4f6dfbfcf79a73c7 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Wed, 11 Dec 2024 19:12:17 -0800 Subject: [PATCH] feat: clean up leaderboard+profile page page & add dropdowns to leaderboard page --- src/api/profile/profile.ts | 3 +- .../Leaderboard/LeaderboardColumn.tsx | 29 ++- .../Leaderboard/LeaderboardEntry.tsx | 169 ++++++++++++++++++ src/components/Leaderboard/index.ts | 1 + src/components/Profile/UserProfile.tsx | 5 +- src/pages/leaderboard.tsx | 10 +- src/pages/profile/[name].tsx | 7 +- src/types/auth/index.ts | 40 +++++ 8 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 src/components/Leaderboard/LeaderboardEntry.tsx diff --git a/src/api/profile/profile.ts b/src/api/profile/profile.ts index 39f1f1d..9425d01 100644 --- a/src/api/profile/profile.ts +++ b/src/api/profile/profile.ts @@ -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 => { const res = await fetch( buildUrl(`auth/get_player_stats${name ? `/${name}` : ''}`), ) diff --git a/src/components/Leaderboard/LeaderboardColumn.tsx b/src/components/Leaderboard/LeaderboardColumn.tsx index 185d473..50e4117 100644 --- a/src/components/Leaderboard/LeaderboardColumn.tsx +++ b/src/components/Leaderboard/LeaderboardColumn.tsx @@ -1,8 +1,9 @@ -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 @@ -10,6 +11,7 @@ interface Props { } export const LeaderboardColumn: React.FC = ({ + id, icon, name, ranking, @@ -22,24 +24,13 @@ export const LeaderboardColumn: React.FC = ({
{ranking.map((player, index) => ( -
-
-

{index + 1}

- -

- {player.display_name} {index == 0 && '👑'} -

- -
- -

{player.elo}

-
+ typeId={id} + type={name} + index={index} + {...player} + /> ))}
diff --git a/src/components/Leaderboard/LeaderboardEntry.tsx b/src/components/Leaderboard/LeaderboardEntry.tsx new file mode 100644 index 0000000..e5eee92 --- /dev/null +++ b/src/components/Leaderboard/LeaderboardEntry.tsx @@ -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(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 ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > +
+

{index + 1}

+ +

+ {display_name} {index == 0 && '👑'} +

+ +
+

{elo}

+ {popup && stats && ( +
+
+

+ {display_name}'s {type}{' '} + Statistics +

+ + + open_in_new + + +
+
+
+

Rating

+ {stats[ratingKey]} +
+
+

Highest

+ {stats[highestRatingKey]} +
+
+

Games

+ {stats[gamesKey]} +
+
+

Win %

+ + {((stats[gamesWonKey] / stats[gamesKey]) * 100).toFixed(0)}% + +
+
+
+ )} +
+ ) +} diff --git a/src/components/Leaderboard/index.ts b/src/components/Leaderboard/index.ts index 0d02c41..af5523e 100644 --- a/src/components/Leaderboard/index.ts +++ b/src/components/Leaderboard/index.ts @@ -1 +1,2 @@ +export * from './LeaderboardEntry' export * from './LeaderboardColumn' diff --git a/src/components/Profile/UserProfile.tsx b/src/components/Profile/UserProfile.tsx index fac60c6..b3f7890 100644 --- a/src/components/Profile/UserProfile.tsx +++ b/src/components/Profile/UserProfile.tsx @@ -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) => { diff --git a/src/pages/leaderboard.tsx b/src/pages/leaderboard.tsx index c6995a7..b0bbc12 100644 --- a/src/pages/leaderboard.tsx +++ b/src/pages/leaderboard.tsx @@ -16,8 +16,9 @@ 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 () => { @@ -25,26 +26,31 @@ const Leaderboard: React.FC = () => { setLastUpdated(new Date(lb.last_updated + 'Z')) setLeaderboard([ { + id: 'regular', icon: , name: 'Regular', ranking: lb.play_leaders, }, { + id: 'train', icon: , name: 'Train', ranking: lb.puzzles_leaders, }, { + id: 'turing', icon: , name: 'Bot/Not', ranking: lb.turing_leaders, }, { + id: 'hand', icon: , name: 'Hand', ranking: lb.hand_leaders, }, { + id: 'brain', icon: , name: 'Brain', ranking: lb.brain_leaders, @@ -81,7 +87,7 @@ const Leaderboard: React.FC = () => { : '...'}

-
+
{leaderboard?.map((column, index) => ( ))} diff --git a/src/pages/profile/[name].tsx b/src/pages/profile/[name].tsx index 9ba1f24..d2d1524 100644 --- a/src/pages/profile/[name].tsx +++ b/src/pages/profile/[name].tsx @@ -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' @@ -12,7 +13,7 @@ const ProfilePage: NextPage = () => { const router = useRouter() const [name, setName] = useState('') - const [stats, setStats] = useState({ + const [stats, setStats] = useState({ regularRating: 0, regularWins: 0, regularDraws: 0, @@ -77,9 +78,7 @@ const ProfilePage: NextPage = () => { interface Props { name: string - stats: { - [key: string]: number - } + stats: PlayerStats } const Profile: React.FC = (props: Props) => { const { isMobile } = useContext(WindowSizeContext) diff --git a/src/types/auth/index.ts b/src/types/auth/index.ts index 5398d42..8e4ced7 100644 --- a/src/types/auth/index.ts +++ b/src/types/auth/index.ts @@ -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 +}