([])
+
+ useEffect(() => {
+ let cancelled = false
+ const done = () => { if (!cancelled) setLoading(false) }
+ const clear = () => {
+ setLocalEntries([]); setGlobalEntries([])
+ setStreakEntries([]); setCompletionistEntries([])
+ }
+
+ queueMicrotask(() => { if (!cancelled) { setLoading(true); clear() } })
+
+ if (tab === 'daily') {
+ const fn = cleanOnly ? api.fetchCleanLeaderboard : api.fetchDailyLeaderboard
+ fn(date, preset)
+ .then(d => { if (!cancelled) setLocalEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ } else if (tab === 'alltime') {
+ api.fetchAlltimeLeaderboard(preset)
+ .then(d => { if (!cancelled) setLocalEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ } else if (tab === 'weekly') {
+ if (cleanOnly) {
+ api.fetchCleanLeaderboard(date, preset)
+ .then(d => { if (!cancelled) setLocalEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ } else {
+ api.fetchWeeklyLeaderboard(preset)
+ .then(d => { if (!cancelled) setLocalEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ }
+ } else if (tab === 'streaks') {
+ api.fetchStreakLeaderboard()
+ .then(d => { if (!cancelled) setStreakEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ } else if (tab === 'total') {
+ api.fetchCompletionistLeaderboard()
+ .then(d => { if (!cancelled) setCompletionistEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ } else if (tab === 'global') {
+ api.fetchFederatedLeaderboard(date, preset)
+ .then(d => { if (!cancelled) setGlobalEntries(d.entries || []) })
+ .catch(() => {})
+ .finally(done)
+ }
+
+ return () => { cancelled = true }
+ }, [tab, preset, date, cleanOnly])
+
+ const renderHeader = () => {
+ if (tab === 'streaks') {
+ return (
+
+ {t('leaderboard.columns.rank')}
+ {t('leaderboard.columns.player')}
+ {t('leaderboard.columns.current')}
+ {t('leaderboard.columns.best')}
+
+ )
+ }
+ if (tab === 'total') {
+ return (
+
+ {t('leaderboard.columns.rank')}
+ {t('leaderboard.columns.player')}
+ {t('leaderboard.columns.solves')}
+
+ )
+ }
+ if (tab === 'global') {
+ return (
+
+ {t('leaderboard.columns.rank')}
+ {t('leaderboard.columns.player')}
+ {t('leaderboard.columns.instance')}
+ {t('leaderboard.columns.time')}
+
+ )
+ }
+ // daily, alltime, weekly
+ return (
+
+ {t('leaderboard.columns.rank')}
+ {t('leaderboard.columns.player')}
+ {t('leaderboard.columns.time')}
+ {t('leaderboard.columns.hints')}
+ {tab === 'alltime' && {t('leaderboard.columns.date')}}
+
+ )
+ }
+
+ const renderRow = (i: number) => {
+ const rank = i + 1
+
+ if (tab === 'streaks') {
+ const entry = streakEntries[i]
+ return (
+
+
+ {entry ? : {rank}}
+
+
+ {entry ? entry.name || t('common.anonymous') : -}
+
+
+ {entry ? (
+ <> {entry.current}>
+ ) : -}
+
+
+ {entry ? entry.best : ''}
+
+
+ )
+ }
+
+ if (tab === 'total') {
+ const entry = completionistEntries[i]
+ return (
+
+
+ {entry ? : {rank}}
+
+
+ {entry ? entry.name || t('common.anonymous') : -}
+
+
+ {entry ? entry.count : -}
+
+
+ )
+ }
+
+ if (tab === 'global') {
+ const entry = globalEntries[i]
+ return (
+
+
+ {entry ? : {rank}}
+
+
+ {entry ? entry.name || t('common.anonymous') : -}
+
+
+ {entry?.instance || ''}
+
+
+ {entry ? (
+ <> {formatTime(entry.time)}>
+ ) : --:--}
+
+
+ )
+ }
+
+ // daily, alltime, weekly
+ const entry = localEntries[i]
+ return (
+
+
+ {entry ? : {rank}}
+
+
+ {entry ? entry.name || t('common.anonymous') : -}
+
+
+ {entry ? (
+ <> {formatTime(entry.time)}>
+ ) : --:--}
+
+
+ {entry && entry.hints > 0 ? (
+ <> {entry.hints}>
+ ) : ''}
+
+ {tab === 'alltime' && (
+
+ {entry?.date || ''}
+
+ )}
+
+ )
+ }
+
+ return (
+
+ e.stopPropagation()}
+ role="dialog" aria-modal="true" aria-label={t('leaderboard.title')}
+ initial={{ opacity: 0, scale: 0.95, y: 12 }}
+ animate={{ opacity: 1, scale: 1, y: 0 }}
+ exit={{ opacity: 0, scale: 0.95, y: 12 }}
+ transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
+ >
+
+
+
{t('leaderboard.title')}
+
+
+
+
+
+
+ {TAB_IDS.map(tb => (
+
+ ))}
+
+
+ {showPresets(tab) && (
+
+ {PRESETS.map(p => (
+
+ ))}
+ {tab === 'daily' && (
+
+ )}
+
+ )}
+
+ {showCleanToggle(tab) && (
+
+ )}
+
+
+
+
+
+ {renderHeader()}
+ {Array.from({ length: SLOTS }, (_, i) => renderRow(i))}
+
+
+
+
+ {loading && (
+
+
+
+ )}
+
+
+
+
+ )
+}