Files
blitzkit/src/app/tools/ratings/page.tsx
T

910 lines
32 KiB
TypeScript
Raw Normal View History

2023-10-29 13:40:32 -05:00
'use client';
2023-11-08 17:50:00 -06:00
import { blackA, orangeDark } from '@radix-ui/colors';
2023-11-08 19:28:22 -06:00
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
2023-11-07 11:55:27 -06:00
import {
Button,
Dialog,
Flex,
Select,
Text,
TextField,
} from '@radix-ui/themes';
2023-10-31 15:20:37 -05:00
import { produce } from 'immer';
2023-11-07 18:58:19 -06:00
import { debounce, range } from 'lodash';
2023-11-07 10:32:01 -06:00
import { ChangeEvent, useEffect, useRef, useState } from 'react';
2023-10-31 13:15:28 -05:00
import useSWR from 'swr';
2023-10-31 15:20:37 -05:00
import { create } from 'zustand';
2023-11-07 18:58:19 -06:00
import { BlitzkriegRatingsLeaderboardEntry } from '../../../../scripts/buildRatingsLeaderboard';
2023-11-08 17:50:00 -06:00
import { RatingsPlayer } from '../../../commands/ratings';
2023-10-31 13:30:47 -05:00
import * as Leaderboard from '../../../components/Leaderboard';
2023-10-29 13:40:32 -05:00
import PageWrapper from '../../../components/PageWrapper';
2023-11-07 11:02:11 -06:00
import { LEAGUES } from '../../../constants/leagues';
2023-11-01 11:27:41 -05:00
import { FIRST_ARCHIVED_RATINGS_SEASON } from '../../../constants/ratings';
2023-10-30 21:57:22 -05:00
import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions';
2023-11-07 10:32:01 -06:00
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
import fetchBlitz from '../../../core/blitz/fetchBlitz';
2023-10-31 15:20:37 -05:00
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
2023-10-31 15:31:03 -05:00
import { getClanAccountInfo } from '../../../core/blitz/getClanAccountInfo';
import getRatingsInfo from '../../../core/blitz/getRatingsInfo';
2023-11-07 18:58:19 -06:00
import { getRatingsLeague } from '../../../core/blitz/getRatingsLeague';
2023-11-07 20:47:20 -06:00
import { getRatingsNeighbors } from '../../../core/blitz/getRatingsNeighbors';
2023-11-08 12:17:08 -06:00
import regionToRegionSubdomain from '../../../core/blitz/regionToRegionSubdomain';
import {
AccountList,
AccountListItem,
} from '../../../core/blitz/searchPlayersAcrossRegions';
2023-11-07 17:24:51 -06:00
import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchivedLatestSeasonNumber';
2023-11-07 11:36:08 -06:00
import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo';
import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard';
2023-11-08 12:17:08 -06:00
import { withCORSProxy } from '../../../core/blitzkrieg/withCORSProxy';
2023-11-08 19:28:22 -06:00
import { PageTurner } from './components/PageTurner';
2023-10-29 13:40:32 -05:00
2023-11-07 20:47:20 -06:00
const ROWS_OPTIONS = [5, 10, 15, 25, 30];
2023-11-08 17:50:00 -06:00
const SEEDING_SIZE = 2 ** 7;
2023-10-31 15:20:37 -05:00
2023-11-07 11:13:30 -06:00
type UsernameCache = Record<Region, Record<number, string | null>>;
2023-11-07 17:24:51 -06:00
type ClanCache = Record<Region, Record<number, string | undefined>>;
2023-11-08 19:28:22 -06:00
type PlayersCache = Record<
Region,
Record<number, Record<number, BlitzkriegRatingsLeaderboardEntry>>
>;
2023-11-07 11:13:30 -06:00
const useUsernameCache = create<UsernameCache>(() => ({
2023-10-31 15:20:37 -05:00
asia: {},
com: {},
eu: {},
}));
2023-11-07 17:24:51 -06:00
const useClanCache = create<ClanCache>(() => ({
asia: {},
com: {},
eu: {},
}));
2023-11-08 19:28:22 -06:00
const usePlayersCache = create<PlayersCache>(() => ({
asia: { 0: {} },
com: { 0: {} },
eu: { 0: {} },
}));
2023-10-31 13:15:28 -05:00
2023-10-29 13:40:32 -05:00
export default function Page() {
2023-10-31 15:20:37 -05:00
const [region, setRegion] = useState<Region>('com');
2023-11-08 19:28:22 -06:00
const [season, setSeason] = useState<number>(0);
2023-10-31 15:20:37 -05:00
const [page, setPage] = useState(0);
2023-11-08 19:28:22 -06:00
const [rowsPerPage, setRowsPerPage] = useState(25);
const [jumpToLeague, setJumpToLeague] = useState(0);
const [searchLoading, setSearchLoading] = useState(false);
const [jumpToScoreOpen, setJumpToScoreOpen] = useState(false);
const [jumpToPositionOpen, setJumpToPositionOpen] = useState(false);
2023-11-08 12:17:08 -06:00
const [searchResults, setSearchResults] = useState<
(AccountListItem & { number: number })[] | undefined
>(undefined);
2023-11-08 17:50:00 -06:00
const [loadingProgress, setLoadingProgress] = useState<
[number, number] | null
>(null);
2023-11-08 19:28:22 -06:00
const [highlightedPlayerId, setHighlightedPlayerId] = useState<number | null>(
null,
);
2023-10-31 15:20:37 -05:00
2023-11-08 19:28:22 -06:00
const { data: ratingsInfo } = useSWR(
`ratings-info-${region}-${season}`,
() =>
season === 0
? getRatingsInfo(region)
: getArchivedRatingsInfo(region, season),
);
const { data: latestArchivedSeasonNumber } = useSWR<number>(
getArchivedLatestSeasonNumber.name,
getArchivedLatestSeasonNumber,
);
const usernames = useUsernameCache();
const clans = useClanCache();
const players = usePlayersCache();
const positionInput = useRef<HTMLInputElement>(null);
const scoreInput = useRef<HTMLInputElement>(null);
const leaguePositionCache = useRef<Record<number, number>>({});
const pages = Math.ceil(
(ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) /
rowsPerPage,
);
// page reset on season or region change
2023-11-07 18:58:19 -06:00
useEffect(() => {
2023-11-08 19:28:22 -06:00
setPage(0);
}, [season, region]);
// initial leagues caching for current season
useEffect(() => {
if (season === 0) {
2023-11-08 11:16:51 -06:00
(async () => {
const newUsernameCache: Record<number, string> = {};
const newClanCache: Record<number, string> = {};
const results = await Promise.all(
range(0, 5).map(async (league: number) => {
const leaguePlayers = await getRatingsLeague(region, league);
leaguePositionCache.current[league] =
leaguePlayers.result[0].number - 1;
leaguePlayers.result.forEach((player) => {
newUsernameCache[player.spa_id] = player.nickname;
newClanCache[player.spa_id] = player.clan_tag;
});
return leaguePlayers.result.reduce<
Record<number, BlitzkriegRatingsLeaderboardEntry>
>(
(accumulator, player) => ({
...accumulator,
[player.number - 1]: {
id: player.spa_id,
score: player.score,
} satisfies BlitzkriegRatingsLeaderboardEntry,
}),
{},
);
}),
);
2023-11-07 20:47:20 -06:00
const newPlayers = results.reduce<
Record<number, BlitzkriegRatingsLeaderboardEntry>
>(
(accumulator, result) => ({
...accumulator,
...result,
}),
{},
);
2023-11-08 19:28:22 -06:00
usePlayersCache.setState(
produce((draft: PlayersCache) => {
draft[region][season] = {
...draft[region][season],
...newPlayers,
};
}),
);
useUsernameCache.setState(
produce((draft: UsernameCache) => {
draft[region] = { ...draft[region], ...newUsernameCache };
}),
);
useClanCache.setState(
produce((draft: ClanCache) => {
draft[region] = { ...draft[region], ...newClanCache };
}),
);
2023-11-08 11:16:51 -06:00
})();
2023-11-07 20:47:20 -06:00
} else {
2023-11-08 19:28:22 -06:00
getArchivedRatingsLeaderboard(region, season).then((result) => {
usePlayersCache.setState(
produce((draft: PlayersCache) => {
draft[region][season] = {
...draft[region][season],
...result,
};
}),
);
});
2023-11-07 20:47:20 -06:00
}
}, []);
2023-11-08 19:28:22 -06:00
// 2nd page caching for current season
2023-11-08 12:17:08 -06:00
useEffect(() => {
2023-11-08 19:28:22 -06:00
if (players !== null && 0 in players[region][season]) {
cacheNeighbors(players[region][season][0].id, rowsPerPage * 3);
2023-11-08 12:17:08 -06:00
}
2023-11-08 19:28:22 -06:00
}, [0 in players[region][season]]);
// neighboring page caching for all seasons
2023-11-07 20:47:20 -06:00
useEffect(() => {
2023-11-08 19:28:22 -06:00
if (!ratingsInfo || ratingsInfo?.detail) return;
2023-11-07 20:47:20 -06:00
2023-11-08 19:28:22 -06:00
if (season === 0) {
2023-11-07 20:47:20 -06:00
const middleIndex = Math.round(page * rowsPerPage + rowsPerPage / 2);
2023-11-08 19:28:22 -06:00
if (!(middleIndex in players[region][season])) return;
const middleId = players[region][season][middleIndex].id;
2023-11-08 11:16:51 -06:00
cacheNeighbors(middleId, rowsPerPage * 3);
2023-11-07 20:47:20 -06:00
} else {
2023-11-07 18:58:19 -06:00
const ids = range(
2023-11-07 20:47:20 -06:00
Math.max(0, (page - 1) * rowsPerPage),
Math.min(ratingsInfo.count - 1, (page + 1) * rowsPerPage + rowsPerPage),
2023-11-07 18:58:19 -06:00
)
2023-11-08 19:28:22 -06:00
.map((index) => players[region][season][index].id)
.filter(Boolean)
.filter((id) => !(id in usernames[region]));
2023-10-31 15:31:03 -05:00
2023-11-07 18:58:19 -06:00
if (ids && ids.length > 0) {
getAccountInfo(region, ids).then((data) => {
data.map((player, index) => {
useUsernameCache.setState(
produce((draft: UsernameCache) => {
if (player) {
draft[region][ids[index]] = player.nickname;
} else {
draft[region][ids[index]] = null;
}
}),
);
});
2023-10-31 15:31:03 -05:00
});
2023-11-07 18:58:19 -06:00
getClanAccountInfo(region, ids, ['clan']).then((data) => {
data.map((player, index) => {
useClanCache.setState(
produce((draft: ClanCache) => {
if (player) {
draft[region][ids[index]] = player.clan?.tag;
}
}),
);
});
});
}
2023-10-31 15:20:37 -05:00
}
2023-11-08 11:16:51 -06:00
}, [page]);
2023-11-07 20:47:20 -06:00
2023-11-08 19:28:22 -06:00
async function cacheNeighbors(id: number, size: number) {
const { neighbors } = await getRatingsNeighbors(region, id, size);
usePlayersCache.setState(
produce((draft: PlayersCache) => {
neighbors.forEach((neighbor) => {
draft[region][season][neighbor.number - 1] = {
id: neighbor.spa_id,
score: neighbor.score,
} satisfies BlitzkriegRatingsLeaderboardEntry;
});
}),
);
useUsernameCache.setState(
produce((draft: UsernameCache) => {
neighbors.forEach((neighbor) => {
if (neighbor.nickname) {
draft[region][neighbor.spa_id] = neighbor.nickname;
}
});
}),
);
useClanCache.setState(
produce((draft: ClanCache) => {
neighbors.forEach((neighbor) => {
if (neighbor.clan_tag) {
draft[region][neighbor.spa_id] = neighbor.clan_tag;
}
});
}),
);
return neighbors;
}
async function handleJumpToPosition() {
if (!ratingsInfo || ratingsInfo?.detail) return;
2023-11-07 11:50:51 -06:00
2023-11-07 10:47:28 -06:00
const rawPosition = positionInput.current!.valueAsNumber - 1;
2023-11-08 17:50:00 -06:00
const targetPosition = Math.max(
0,
Math.min(rawPosition, ratingsInfo.count - 1),
);
2023-11-07 10:47:28 -06:00
2023-11-08 19:28:22 -06:00
if (season === 0) {
const playerEntries = Object.entries(players[region][season]);
2023-11-08 17:50:00 -06:00
let closest: BlitzkriegRatingsLeaderboardEntry | null = null;
let closestPosition: number | null = null;
let closestPositionDistance = Infinity;
playerEntries.forEach(([positionAsString, player]) => {
const position = parseInt(positionAsString);
const distance = Math.abs(position - targetPosition);
if (distance < closestPositionDistance) {
closestPositionDistance = distance;
closestPosition = position;
closest = player;
}
});
if (closest === null || closestPosition === null) return;
if (closestPositionDistance === 0) {
setHighlightedPlayerId(
(closest as BlitzkriegRatingsLeaderboardEntry).id,
);
setPage(Math.floor(targetPosition / rowsPerPage));
return;
}
// 1 going down and -1 going up
const loadingDirection = Math.sign(targetPosition - closestPosition);
let seedingPlayer = (closest as BlitzkriegRatingsLeaderboardEntry).id;
let targetPositionAcquired = false;
let neighbors: RatingsPlayer[];
setLoadingProgress([0, closestPositionDistance]);
while (!targetPositionAcquired) {
neighbors = await cacheNeighbors(seedingPlayer, SEEDING_SIZE);
seedingPlayer = neighbors.at(loadingDirection === -1 ? 0 : -1)!.spa_id;
if (loadingDirection === 1) {
targetPositionAcquired =
targetPosition <= neighbors.at(-1)!.number - 1;
setLoadingProgress([
Math.abs(neighbors.at(-1)!.number - 1 - closestPosition),
closestPositionDistance,
]);
} else {
targetPositionAcquired = targetPosition >= neighbors[0].number - 1;
setLoadingProgress([
Math.abs(neighbors[0].number - 1 - closestPosition),
closestPositionDistance,
]);
}
}
setLoadingProgress(null);
setPage(Math.floor(targetPosition / rowsPerPage));
setHighlightedPlayerId(
neighbors!.find((neighbor) => neighbor.number - 1 === targetPosition)!
.spa_id,
);
} else {
setPage(Math.floor(targetPosition / rowsPerPage));
if (players) {
2023-11-08 19:28:22 -06:00
setHighlightedPlayerId(players[region][season][targetPosition].id);
2023-11-08 17:50:00 -06:00
}
2023-11-07 10:47:28 -06:00
}
2023-11-08 19:28:22 -06:00
}
function handleJumpToScore() {
if (!ratingsInfo || ratingsInfo?.detail) return;
2023-11-07 11:50:51 -06:00
2023-11-07 18:58:19 -06:00
let playerIndex = -1;
for (let index = ratingsInfo.count - 1; index >= 0; index--) {
2023-11-08 19:28:22 -06:00
if (
players[region][season][index].score >=
scoreInput.current!.valueAsNumber
) {
2023-11-07 18:58:19 -06:00
playerIndex = index;
break;
}
}
2023-11-07 11:50:51 -06:00
if (playerIndex === -1) return;
setPage(Math.floor(playerIndex / rowsPerPage));
2023-11-07 20:47:20 -06:00
if (players) {
2023-11-08 19:28:22 -06:00
setHighlightedPlayerId(players[region][season][playerIndex].id);
2023-11-07 11:50:51 -06:00
}
2023-11-07 10:06:15 -06:00
}
2023-10-29 13:40:32 -05:00
return (
2023-11-07 10:32:01 -06:00
<PageWrapper color="orange">
2023-11-07 17:33:41 -06:00
<Flex gap="4" wrap="wrap" justify="center">
2023-11-07 18:58:19 -06:00
<Flex gap="2" wrap="wrap" justify="center">
2023-11-07 17:33:41 -06:00
<Select.Root
defaultValue={region}
onValueChange={(value) => setRegion(value as Region)}
>
<Select.Trigger style={{ flex: 1 }} />
2023-10-29 13:40:32 -05:00
2023-11-07 17:33:41 -06:00
<Select.Content>
{REGIONS.map((region) => (
<Select.Item key={region} value={region}>
{REGION_NAMES[region]}
</Select.Item>
))}
</Select.Content>
</Select.Root>
2023-10-30 21:57:22 -05:00
2023-11-07 17:33:41 -06:00
<Select.Root
value={`${season}`}
2023-11-08 19:28:22 -06:00
onValueChange={(value) => setSeason(parseInt(value))}
2023-11-07 17:33:41 -06:00
>
<Select.Trigger style={{ flex: 1 }} />
2023-10-29 13:40:32 -05:00
2023-11-07 17:33:41 -06:00
<Select.Content>
{ratingsInfo?.detail === undefined && (
2023-11-08 19:28:22 -06:00
<Select.Item value="0">Current season</Select.Item>
2023-11-07 17:33:41 -06:00
)}
2023-10-30 21:57:22 -05:00
2023-11-07 17:33:41 -06:00
<Select.Group>
<Select.Label>Archives</Select.Label>
2023-10-30 21:57:22 -05:00
2023-11-07 17:33:41 -06:00
{latestArchivedSeasonNumber &&
range(
FIRST_ARCHIVED_RATINGS_SEASON,
latestArchivedSeasonNumber + 1,
)
.map((season) => (
<Select.Item key={season} value={`${season}`}>
Season {season}
2023-11-07 10:47:28 -06:00
</Select.Item>
2023-11-07 17:33:41 -06:00
))
.reverse()}
</Select.Group>
</Select.Content>
</Select.Root>
2023-11-07 10:47:28 -06:00
2023-11-07 17:33:41 -06:00
<Select.Root
onValueChange={(value) => setRowsPerPage(parseInt(value))}
2023-11-07 18:58:19 -06:00
value={`${rowsPerPage}`}
2023-11-07 17:33:41 -06:00
>
<Select.Trigger />
2023-11-07 11:02:11 -06:00
2023-11-07 17:33:41 -06:00
<Select.Content>
2023-11-07 20:47:20 -06:00
{ROWS_OPTIONS.map((size) => (
2023-11-07 17:33:41 -06:00
<Select.Item value={`${size}`} key={size}>
{size} per page
</Select.Item>
))}
</Select.Content>
</Select.Root>
</Flex>
2023-11-07 11:02:11 -06:00
2023-11-07 17:33:41 -06:00
<Flex gap="3" justify="center" wrap="wrap" align="center">
<Text>Jump to:</Text>
<Dialog.Root open={jumpToScoreOpen} onOpenChange={setJumpToScoreOpen}>
<Dialog.Trigger>
<Button variant="ghost">Score</Button>
</Dialog.Trigger>
2023-11-07 10:47:28 -06:00
2023-11-07 17:33:41 -06:00
<Dialog.Content>
<Flex gap="4" justify="center">
2023-11-07 10:47:28 -06:00
<TextField.Input
2023-11-07 17:33:41 -06:00
ref={scoreInput}
type="number"
placeholder="Type a score..."
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleJumpToScore();
setJumpToScoreOpen(false);
}
}}
2023-11-07 10:47:28 -06:00
/>
2023-11-07 17:33:41 -06:00
<Flex gap="2">
<Dialog.Close>
<Button color="red">Cancel</Button>
</Dialog.Close>
<Dialog.Close>
<Button onClick={handleJumpToScore}>Jump</Button>
</Dialog.Close>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root
open={jumpToPositionOpen}
onOpenChange={setJumpToPositionOpen}
>
<Dialog.Trigger>
<Button variant="ghost">Position</Button>
</Dialog.Trigger>
<Dialog.Content>
<Flex gap="4" justify="center">
<TextField.Input
ref={positionInput}
type="number"
placeholder="Type a position..."
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleJumpToPosition();
setJumpToPositionOpen(false);
}
}}
/>
<Flex gap="2">
<Dialog.Close>
<Button color="red">Cancel</Button>
</Dialog.Close>
<Dialog.Close>
<Button onClick={handleJumpToPosition}>Jump</Button>
</Dialog.Close>
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">League</Button>
</Dialog.Trigger>
<Dialog.Content>
<Flex gap="2" justify="center">
<Select.Root
value={`${jumpToLeague}`}
onValueChange={(value) => setJumpToLeague(parseInt(value))}
>
<Select.Trigger />
<Select.Content>
{ratingsInfo?.detail === undefined &&
ratingsInfo?.leagues.map(({ index, title }) => (
<Select.Item key={index} value={`${index}`}>
{title}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<Flex gap="2">
<Dialog.Close>
<Button color="red">Cancel</Button>
</Dialog.Close>
2023-11-07 10:47:28 -06:00
<Dialog.Close>
<Button
2023-11-07 18:58:19 -06:00
onClick={async () => {
2023-11-08 19:28:22 -06:00
if (season === 0) {
2023-11-08 11:16:51 -06:00
const position =
leaguePositionCache.current[jumpToLeague];
const newPage = Math.floor(position / rowsPerPage);
2023-11-07 10:47:28 -06:00
2023-11-08 11:16:51 -06:00
setPage(newPage);
if (players) {
2023-11-08 19:28:22 -06:00
setHighlightedPlayerId(
players[region][season][position].id,
);
2023-11-08 11:16:51 -06:00
cacheNeighbors(
2023-11-08 19:28:22 -06:00
players[region][season][position].id,
2023-11-08 11:16:51 -06:00
rowsPerPage * 3,
);
}
2023-11-07 18:58:19 -06:00
} else {
2023-11-07 20:47:20 -06:00
if (!ratingsInfo || ratingsInfo.detail || !players)
2023-11-07 18:58:19 -06:00
return;
const minScore =
LEAGUES[jumpToLeague - 1]?.minScore ?? Infinity;
let firstPlayerIndex = -1;
for (
let index = 0;
index < ratingsInfo.count;
index++
) {
2023-11-08 19:28:22 -06:00
if (
players[region][season][index].score < minScore
) {
2023-11-07 18:58:19 -06:00
firstPlayerIndex = index;
break;
}
}
if (firstPlayerIndex === -1) return;
setPage(Math.floor(firstPlayerIndex / rowsPerPage));
2023-11-08 19:28:22 -06:00
setHighlightedPlayerId(
players[region][season][firstPlayerIndex].id,
);
2023-11-07 18:58:19 -06:00
}
2023-11-07 10:47:28 -06:00
}}
>
2023-11-07 17:33:41 -06:00
Jump
2023-11-07 10:47:28 -06:00
</Button>
</Dialog.Close>
2023-11-07 17:33:41 -06:00
</Flex>
2023-11-07 10:47:28 -06:00
</Flex>
2023-11-07 17:33:41 -06:00
</Dialog.Content>
</Dialog.Root>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">Player</Button>
</Dialog.Trigger>
2023-11-07 10:47:28 -06:00
2023-11-07 17:33:41 -06:00
<Dialog.Content>
<Flex gap="4" direction="column">
<TextField.Root>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
<TextField.Input
2023-11-08 12:17:08 -06:00
onChange={debounce(
async (event: ChangeEvent<HTMLInputElement>) => {
const trimmedSearch = event.target.value.trim();
const encodedSearch = encodeURIComponent(trimmedSearch);
if (trimmedSearch) {
2023-11-08 19:28:22 -06:00
if (season === 0) {
2023-11-08 12:17:08 -06:00
setSearchLoading(true);
const response = await fetch(
withCORSProxy(
`https://${regionToRegionSubdomain(
region,
)}.wotblitz.com/en/api/rating-leaderboards/search/?prefix=${encodedSearch}`,
),
);
const accountList =
(await response.json()) as Record<
number,
{
spa_id: number;
nickname: string;
clan_tag: string;
} & (
| {
skip: true;
}
| {
skip: false;
mmr: number;
season_number: number;
calibrationBattlesLeft: number;
number: number;
updated_at: string;
neighbors: [];
}
)
>;
const values = Object.values(accountList).filter(
(searchedPlayer) => !searchedPlayer.skip,
);
const valuesWithTypes =
values as ((typeof values)[number] & {
skip: false;
})[];
2023-11-08 19:28:22 -06:00
usePlayersCache.setState(
produce((draft: PlayersCache) => {
2023-11-08 12:17:08 -06:00
if (draft) {
values.map((player) => {
const playerWithTypes =
player as typeof player & { skip: false };
2023-11-08 19:28:22 -06:00
draft[region][season][
playerWithTypes.number - 1
] = {
2023-11-08 12:17:08 -06:00
id: playerWithTypes.spa_id,
score: 0,
};
});
}
}),
);
useUsernameCache.setState(
2023-11-08 19:28:22 -06:00
produce((draft: typeof usernames) => {
2023-11-08 12:17:08 -06:00
valuesWithTypes.forEach((searchedPlayer) => {
if (searchedPlayer.nickname) {
draft[region][searchedPlayer.spa_id] =
searchedPlayer.nickname;
}
});
}),
);
useClanCache.setState(
2023-11-08 19:28:22 -06:00
produce((draft: typeof clans) => {
2023-11-08 12:17:08 -06:00
valuesWithTypes.forEach((searchedPlayer) => {
if (searchedPlayer.clan_tag) {
draft[region][searchedPlayer.spa_id] =
searchedPlayer.clan_tag;
}
});
}),
);
setSearchResults(
valuesWithTypes.map((searchedPlayer) => ({
account_id: searchedPlayer.spa_id,
nickname: searchedPlayer.nickname,
number: searchedPlayer.number,
})),
);
setSearchLoading(false);
} else {
setSearchLoading(true);
const accountList = await fetchBlitz<AccountList>(
`https://api.wotblitz.${region}/wotb/account/list/?application_id=${WARGAMING_APPLICATION_ID}&search=${encodedSearch}&limit=100`,
);
const playerKeys = Object.keys(players!);
setSearchResults(
accountList
.filter(
(searchedPlayer) =>
players &&
searchedPlayer.account_id in players,
)
.map((searchedPlayer) => ({
...searchedPlayer,
number: parseInt(
playerKeys.find(
(index) =>
2023-11-08 19:28:22 -06:00
players[region][season][parseInt(index)]
.id === searchedPlayer.account_id,
2023-11-08 12:17:08 -06:00
)!,
),
})),
);
setSearchLoading(false);
}
} else {
setSearchResults(undefined);
}
},
500,
)}
2023-11-07 17:33:41 -06:00
placeholder="Search for player..."
/>
</TextField.Root>
<Flex direction="column" gap="2">
2023-11-08 12:17:08 -06:00
{!searchLoading &&
searchResults?.map((player) => (
2023-11-08 19:28:22 -06:00
<Dialog.Close key={player.account_id}>
2023-11-08 12:17:08 -06:00
<Button
key={player.account_id}
variant="ghost"
onClick={() => {
2023-11-08 19:28:22 -06:00
if (season === 0) {
2023-11-08 12:17:08 -06:00
setPage(
Math.floor((player.number - 1) / rowsPerPage),
);
cacheNeighbors(
player.account_id,
rowsPerPage * 3,
);
setHighlightedPlayerId(player.account_id);
} else {
if (!ratingsInfo || ratingsInfo.detail) return;
let playerIndex = -1;
2023-11-07 18:58:19 -06:00
2023-11-08 12:17:08 -06:00
for (
let index = 0;
index < ratingsInfo.count;
index++
) {
2023-11-08 19:28:22 -06:00
if (
players[region][season][index].id ===
player.account_id
) {
2023-11-08 12:17:08 -06:00
playerIndex = index;
break;
}
}
const playerPage = Math.floor(
playerIndex! / rowsPerPage,
);
setPage(playerPage);
setHighlightedPlayerId(player.account_id);
2023-11-07 18:58:19 -06:00
}
2023-11-08 12:17:08 -06:00
}}
>
{player.nickname}
</Button>
</Dialog.Close>
))}
2023-11-07 18:58:19 -06:00
2023-11-08 12:17:08 -06:00
{!searchLoading && searchResults?.length === 0 && (
2023-11-07 17:33:41 -06:00
<Button disabled variant="ghost">
2023-11-08 12:17:08 -06:00
No players found in leaderboard (try typing in the whole
name because Wargaming's search engine is finicky)
</Button>
)}
{!searchLoading && searchResults === undefined && (
<Button disabled variant="ghost">
Type a username in the text field above
</Button>
)}
{searchLoading && (
<Button disabled variant="ghost">
Searching...
2023-11-07 17:33:41 -06:00
</Button>
)}
</Flex>
<Flex gap="2">
<Dialog.Close>
<Button color="red">Cancel</Button>
</Dialog.Close>
</Flex>
2023-11-07 10:47:28 -06:00
</Flex>
2023-11-07 17:33:41 -06:00
</Dialog.Content>
</Dialog.Root>
</Flex>
2023-10-31 15:20:37 -05:00
</Flex>
2023-11-08 19:28:22 -06:00
<PageTurner page={page} pages={pages} setPage={setPage} />
2023-11-07 10:50:41 -06:00
2023-10-31 15:20:37 -05:00
<Leaderboard.Root>
2023-11-07 20:47:20 -06:00
{players === null ? (
2023-11-07 10:06:15 -06:00
<Leaderboard.Gap message="Loading players..." />
) : (
2023-11-07 19:15:41 -06:00
range(
page * rowsPerPage,
Math.min(
page * rowsPerPage + rowsPerPage,
ratingsInfo?.detail ? 0 : (ratingsInfo?.count ?? 1) - 1,
),
2023-11-08 19:28:22 -06:00
).map((index) => {
const id = players[region][season][index]?.id;
return (
<Leaderboard.Item
nickname={
id
? usernames[region][id] ?? `Deleted player ${id}`
: `Loading player...`
}
position={index + 1}
score={players[region][season][index]?.score}
clan={
players[region][season][index]
? clans[region][players[region][season][index].id]
: undefined
}
key={index}
highlight={
highlightedPlayerId === players[region][season][index]?.id
}
/>
);
})
2023-11-07 10:06:15 -06:00
)}
2023-10-31 15:20:37 -05:00
</Leaderboard.Root>
2023-10-31 13:30:47 -05:00
2023-11-08 19:28:22 -06:00
<PageTurner page={page} pages={pages} setPage={setPage} />
2023-11-08 17:50:00 -06:00
{loadingProgress !== null && (
<Flex
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: blackA.blackA10,
zIndex: 2,
2023-11-08 19:38:59 -06:00
boxSizing: 'border-box',
padding: 32,
textAlign: 'center',
2023-11-08 17:50:00 -06:00
}}
gap="4"
justify="center"
align="center"
direction="column"
>
<Text>
Loading {loadingProgress[0]} of {loadingProgress[1]} players...
</Text>
<div
style={{
height: 16,
width: '100%',
maxWidth: 256,
borderRadius: 8,
backgroundColor: orangeDark.orange2,
}}
>
<div
style={{
height: '100%',
transitionDuration: '200ms',
width: `${Math.round(
(loadingProgress[0] / loadingProgress[1]) * 100,
)}%`,
backgroundColor: orangeDark.orange9,
borderRadius: 8,
}}
/>
</div>
2023-11-08 19:38:59 -06:00
<Flex direction="column">
<Text color="gray">Please be patient. Wargaming is slow.</Text>
<Text color="gray">
Players are loaded in batches of {SEEDING_SIZE}.
</Text>
</Flex>
2023-11-08 17:50:00 -06:00
</Flex>
)}
2023-10-29 13:40:32 -05:00
</PageWrapper>
);
}