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

891 lines
31 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: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-11 21:37:59 -06:00
import { searchCurrentRatingsPlayers } from '../../../core/blitz/searchCurrentRatingsPlayers';
2023-11-08 12:17:08 -06:00
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 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
interface NameCacheEntry {
nickname: string;
clan?: string;
}
type NameCache = Record<Region, Record<number, NameCacheEntry | null>>;
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 useNameCache = create<NameCache>(() => ({
2023-11-07 17:24:51 -06:00
asia: {},
com: {},
eu: {},
}));
2023-11-08 19:28:22 -06:00
const usePlayersCache = create<PlayersCache>(() => ({
asia: {},
com: {},
eu: {},
2023-11-08 19:28:22 -06:00
}));
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-11 20:59:48 -06:00
const [highlightedPlayer, setHighlightedPlayer] = useState<
{ type: 'id'; id: number } | { type: 'position'; position: 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 names = useNameCache();
2023-11-08 19:28:22 -06:00
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 in players[region]) return;
2023-11-08 19:28:22 -06:00
if (season === 0) {
2023-11-08 11:16:51 -06:00
(async () => {
const newNameCache: Record<number, NameCacheEntry> = {};
2023-11-08 11:16:51 -06:00
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) => {
newNameCache[player.spa_id] = {
nickname: player.nickname,
clan: player.clan_tag,
};
2023-11-08 11:16:51 -06:00
});
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,
};
}),
);
useNameCache.setState(
produce((draft: NameCache) => {
draft[region] = { ...draft[region], ...newNameCache };
2023-11-08 19:28:22 -06:00
}),
);
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
}
}, [season in players[region]]);
2023-11-08 19:28:22 -06:00
// 2nd page caching for current season
2023-11-08 12:17:08 -06:00
useEffect(() => {
if (
season === 0 &&
players !== null &&
players[region][season] &&
0 in players[region][season]
) {
2023-11-08 19:28:22 -06:00
cacheNeighbors(players[region][season][0].id, rowsPerPage * 3);
2023-11-08 12:17:08 -06:00
}
}, [players[region]?.[season] && 0 in players[region][season]]);
2023-11-08 19:28:22 -06:00
// 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 names[region]));
2023-10-31 15:31:03 -05:00
2023-11-07 18:58:19 -06:00
if (ids && ids.length > 0) {
getClanAccountInfo(region, ids, ['clan']).then((data) => {
data.map((player, index) => {
useNameCache.setState(
produce((draft: NameCache) => {
draft[region][ids[index]] = player
? {
nickname: player.account_name,
clan: player.clan?.tag,
}
: null;
2023-11-07 18:58:19 -06:00
}),
);
});
});
}
2023-10-31 15:20:37 -05:00
}
}, [page, region, season, ratingsInfo?.detail]);
2023-11-07 20:47:20 -06:00
async function cacheNeighbors(
id: number,
size: number,
): Promise<RatingsPlayer[]> {
2023-11-08 19:28:22 -06:00
const { neighbors } = await getRatingsNeighbors(region, id, size);
const radius = Math.round(size / 2);
const targetPosition = Object.entries(players[region][season]).find(
([, player]) => id === player.id,
)?.[0];
if (targetPosition !== undefined) {
let isMissing = false;
const targetPositionAsNumber = parseInt(targetPosition);
for (
let position = Math.max(0, targetPositionAsNumber - radius);
position <= targetPositionAsNumber + radius;
position++
) {
isMissing = !(position in players[region][season]);
if (isMissing) break;
}
if (!isMissing) return [];
}
2023-11-08 19:28:22 -06:00
usePlayersCache.setState(
produce((draft: PlayersCache) => {
neighbors.forEach((neighbor) => {
draft[region][season][neighbor.number - 1] = {
id: neighbor.spa_id,
score: neighbor.score,
} satisfies BlitzkriegRatingsLeaderboardEntry;
});
}),
);
useNameCache.setState(
produce((draft: NameCache) => {
2023-11-08 19:28:22 -06:00
neighbors.forEach((neighbor) => {
if (neighbor.nickname) {
draft[region][neighbor.spa_id] = {
nickname: neighbor.nickname,
clan: neighbor.clan_tag,
};
2023-11-08 19:28:22 -06:00
}
});
}),
);
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) {
2023-11-11 20:59:48 -06:00
setHighlightedPlayer({ type: 'position', position: targetPosition });
2023-11-08 17:50:00 -06:00
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));
2023-11-11 20:59:48 -06:00
setHighlightedPlayer({ type: 'position', position: closestPosition });
2023-11-08 17:50:00 -06:00
} else {
setPage(Math.floor(targetPosition / rowsPerPage));
if (players) {
2023-11-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: 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-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: 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-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: 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-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: 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 accountList =
2023-11-11 21:37:59 -06:00
await searchCurrentRatingsPlayers(
region,
trimmedSearch,
);
2023-11-08 12:17:08 -06:00
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,
};
});
}
}),
);
useNameCache.setState(
produce((draft: typeof names) => {
2023-11-08 12:17:08 -06:00
valuesWithTypes.forEach((searchedPlayer) => {
if (searchedPlayer.nickname) {
draft[region][searchedPlayer.spa_id] = {
nickname: searchedPlayer.nickname,
clan: searchedPlayer.clan_tag,
};
2023-11-08 12:17:08 -06:00
}
});
}),
);
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,
);
2023-11-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: player.account_id,
});
2023-11-08 12:17:08 -06:00
} 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);
2023-11-11 20:59:48 -06:00
setHighlightedPlayer({
type: 'id',
id: 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;
2023-11-08 19:28:22 -06:00
return (
<Leaderboard.Item
nickname={
names[region][id] === null
? `Deleted player ${id}`
: names[region][id]?.nickname ?? `Loading player...`
2023-11-08 19:28:22 -06:00
}
position={index + 1}
score={players[region][season]?.[index]?.score}
clan={id ? names[region][id]?.clan : undefined}
2023-11-08 19:28:22 -06:00
key={index}
2023-11-11 20:59:48 -06:00
highlight={
highlightedPlayer
? highlightedPlayer.type === 'id'
? highlightedPlayer.id === id
: highlightedPlayer.position === index
: undefined
}
2023-11-08 19:28:22 -06:00
/>
);
})
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>
);
}