Skip to content

Commit 638239d

Browse files
fix: leaderboard duplicate hover popup
1 parent eb7ebf5 commit 638239d

4 files changed

Lines changed: 99 additions & 44 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { createContext, useContext, useState, ReactNode } from 'react'
2+
3+
interface LeaderboardContextType {
4+
activePopup: string | null
5+
setActivePopup: (entryKey: string | null) => void
6+
}
7+
8+
const LeaderboardContext = createContext<LeaderboardContextType | undefined>(
9+
undefined,
10+
)
11+
12+
export const useLeaderboardContext = () => {
13+
const context = useContext(LeaderboardContext)
14+
if (!context) {
15+
throw new Error(
16+
'useLeaderboardContext must be used within a LeaderboardProvider',
17+
)
18+
}
19+
return context
20+
}
21+
22+
interface LeaderboardProviderProps {
23+
children: ReactNode
24+
}
25+
26+
export const LeaderboardProvider: React.FC<LeaderboardProviderProps> = ({
27+
children,
28+
}) => {
29+
const [activePopup, setActivePopup] = useState<string | null>(null)
30+
31+
return (
32+
<LeaderboardContext.Provider value={{ activePopup, setActivePopup }}>
33+
{children}
34+
</LeaderboardContext.Provider>
35+
)
36+
}

src/components/Leaderboard/LeaderboardEntry.tsx

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

44
import { PlayerStats } from 'src/types'
55
import { getPlayerStats } from 'src/api'
6+
import { useLeaderboardContext } from './LeaderboardContext'
67

78
interface Props {
89
index: number
@@ -20,8 +21,12 @@ export const LeaderboardEntry = ({
2021
elo,
2122
}: Props) => {
2223
const [hover, setHover] = useState(false)
23-
const [popup, setPopup] = useState(false)
2424
const [stats, setStats] = useState<PlayerStats | null>(null)
25+
const shouldShowPopupRef = useRef(false)
26+
const { activePopup, setActivePopup } = useLeaderboardContext()
27+
28+
const entryKey = `${typeId}-${display_name}-${index}`
29+
const isPopupVisible = activePopup === entryKey
2530

2631
let ratingKey:
2732
| 'regularRating'
@@ -88,28 +93,38 @@ export const LeaderboardEntry = ({
8893
break
8994
}
9095

96+
const fetchStats = useCallback(async () => {
97+
try {
98+
const playerStats = await getPlayerStats(display_name)
99+
setStats(playerStats)
100+
// Only show popup if we're still supposed to (user still hovering)
101+
if (shouldShowPopupRef.current && hover) {
102+
setActivePopup(entryKey)
103+
}
104+
} catch (error) {
105+
console.error(error)
106+
}
107+
}, [display_name, hover, entryKey, setActivePopup])
108+
91109
useEffect(() => {
92110
let timer: NodeJS.Timeout
93111
if (hover) {
112+
shouldShowPopupRef.current = true
94113
timer = setTimeout(() => {
95114
fetchStats()
96115
}, 500)
97116
} else {
98-
setPopup(false)
117+
shouldShowPopupRef.current = false
118+
setActivePopup(null)
99119
}
100120

101-
return () => clearTimeout(timer)
102-
}, [hover])
103-
104-
const fetchStats = async () => {
105-
try {
106-
const playerStats = await getPlayerStats(display_name)
107-
setStats(playerStats)
108-
setPopup(true)
109-
} catch (error) {
110-
console.error(error)
121+
return () => {
122+
clearTimeout(timer)
123+
if (!hover) {
124+
shouldShowPopupRef.current = false
125+
}
111126
}
112-
}
127+
}, [hover, setActivePopup, entryKey, fetchStats])
113128

114129
return (
115130
<div
@@ -129,7 +144,7 @@ export const LeaderboardEntry = ({
129144
</Link>
130145
</div>
131146
<p>{elo}</p>
132-
{popup && stats && (
147+
{isPopupVisible && stats && (
133148
<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">
134149
<div className="flex w-full justify-between bg-backdrop/50 px-4 py-2">
135150
<p>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './LeaderboardEntry'
22
export * from './LeaderboardColumn'
3+
export * from './LeaderboardContext'

src/pages/leaderboard.tsx

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from 'src/components/Icons/icons'
1111
import { getLeaderboard } from 'src/api'
1212
import { LeaderboardColumn } from 'src/components'
13+
import { LeaderboardProvider } from 'src/components/Leaderboard/LeaderboardContext'
1314

1415
const Leaderboard: React.FC = () => {
1516
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
@@ -63,36 +64,38 @@ const Leaderboard: React.FC = () => {
6364
}, [fetchLeaderboard])
6465

6566
return (
66-
<div className="mx-auto flex h-full w-[90%] flex-col items-start justify-center gap-8 py-[2%]">
67-
<Head>
68-
<title>Leaderboard – Maia Chess</title>
69-
<meta
70-
name="description"
71-
content="Top users across all Maia Chess leaderboards"
72-
/>
73-
</Head>
74-
<div className="flex flex-col">
75-
<h1 className="text-4xl font-bold">Rating Leaderboards</h1>
76-
<p>
77-
Last Updated:{' '}
78-
{lastUpdated
79-
? lastUpdated.toLocaleString(undefined, {
80-
year: 'numeric',
81-
month: 'short',
82-
day: 'numeric',
83-
hour: '2-digit',
84-
minute: '2-digit',
85-
hour12: true,
86-
})
87-
: '...'}
88-
</p>
67+
<LeaderboardProvider>
68+
<div className="mx-auto flex h-full w-[90%] flex-col items-start justify-center gap-8 py-[2%]">
69+
<Head>
70+
<title>Leaderboard – Maia Chess</title>
71+
<meta
72+
name="description"
73+
content="Top users across all Maia Chess leaderboards"
74+
/>
75+
</Head>
76+
<div className="flex flex-col">
77+
<h1 className="text-4xl font-bold">Rating Leaderboards</h1>
78+
<p>
79+
Last Updated:{' '}
80+
{lastUpdated
81+
? lastUpdated.toLocaleString(undefined, {
82+
year: 'numeric',
83+
month: 'short',
84+
day: 'numeric',
85+
hour: '2-digit',
86+
minute: '2-digit',
87+
hour12: true,
88+
})
89+
: '...'}
90+
</p>
91+
</div>
92+
<div className="grid h-full w-full grid-cols-1 justify-start gap-4 md:grid-cols-2 lg:grid-cols-3">
93+
{leaderboard?.map((column, index) => (
94+
<LeaderboardColumn key={index} {...column} />
95+
))}
96+
</div>
8997
</div>
90-
<div className="grid h-full w-full grid-cols-1 justify-start gap-4 md:grid-cols-2 lg:grid-cols-3">
91-
{leaderboard?.map((column, index) => (
92-
<LeaderboardColumn key={index} {...column} />
93-
))}
94-
</div>
95-
</div>
98+
</LeaderboardProvider>
9699
)
97100
}
98101

0 commit comments

Comments
 (0)