2023-10-29 13:40:32 -05:00
|
|
|
'use client';
|
|
|
|
|
|
2023-10-30 21:57:22 -05:00
|
|
|
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
2023-10-31 13:30:47 -05:00
|
|
|
import {
|
|
|
|
|
Button,
|
|
|
|
|
Flex,
|
|
|
|
|
Popover,
|
|
|
|
|
Select,
|
|
|
|
|
Text,
|
|
|
|
|
TextField,
|
|
|
|
|
} from '@radix-ui/themes';
|
2023-10-31 15:20:37 -05:00
|
|
|
import { produce } from 'immer';
|
2023-10-30 21:57:22 -05:00
|
|
|
import { range } from 'lodash';
|
2023-10-31 15:20:37 -05:00
|
|
|
import { 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-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-10-30 21:57:22 -05:00
|
|
|
import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions';
|
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';
|
2023-10-31 12:35:32 -05:00
|
|
|
import getRatingsInfo from '../../../core/blitz/getRatingsInfo';
|
2023-10-31 12:58:24 -05:00
|
|
|
import getArchivedRatingsInfoAPI from '../../../core/blitzkrieg/getArchivedRatingsInfoAPI';
|
2023-10-31 13:30:47 -05:00
|
|
|
import { getArchivedRatingsLeaderboardAPI } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboardAPI';
|
2023-10-31 13:15:28 -05:00
|
|
|
import { numberFetcher } from '../../../core/blitzkrieg/numberFetcher';
|
2023-10-31 15:20:37 -05:00
|
|
|
import { noArrows } from './page.css';
|
2023-10-29 13:40:32 -05:00
|
|
|
|
2023-10-31 13:15:28 -05:00
|
|
|
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
|
2023-10-31 15:20:37 -05:00
|
|
|
const ROWS_PER_PAGE = 64;
|
|
|
|
|
|
|
|
|
|
const useUsernameCache = create<Record<Region, Record<number, string>>>(() => ({
|
|
|
|
|
asia: {},
|
|
|
|
|
com: {},
|
|
|
|
|
eu: {},
|
|
|
|
|
}));
|
2023-10-31 15:31:03 -05:00
|
|
|
const useClanCache = create<Record<Region, Record<number, string | undefined>>>(
|
|
|
|
|
() => ({
|
|
|
|
|
asia: {},
|
|
|
|
|
com: {},
|
|
|
|
|
eu: {},
|
|
|
|
|
}),
|
|
|
|
|
);
|
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 usernameCache = useUsernameCache();
|
2023-10-31 15:31:03 -05:00
|
|
|
const clanCache = useClanCache();
|
2023-10-31 15:20:37 -05:00
|
|
|
const [region, setRegion] = useState<Region>('com');
|
2023-10-30 21:57:22 -05:00
|
|
|
// null being the latest season
|
|
|
|
|
const [season, setSeason] = useState<null | number>(null);
|
2023-10-31 13:30:47 -05:00
|
|
|
const { data: ratingsInfo } = useSWR(
|
2023-10-31 15:20:37 -05:00
|
|
|
`ratings-info-${region}-${season}`,
|
2023-10-31 13:30:47 -05:00
|
|
|
() =>
|
|
|
|
|
season === null
|
2023-10-31 15:20:37 -05:00
|
|
|
? getRatingsInfo(region)
|
|
|
|
|
: getArchivedRatingsInfoAPI(region, season),
|
2023-10-31 13:15:28 -05:00
|
|
|
);
|
|
|
|
|
const [jumpToLeague, setJumpToLeague] = useState(0);
|
|
|
|
|
const { data: latestArchivedSeasonNumber } = useSWR<number>(
|
|
|
|
|
'/api/ratings/latest-archived-season-number',
|
|
|
|
|
numberFetcher,
|
|
|
|
|
);
|
2023-10-31 15:20:37 -05:00
|
|
|
const players = useSWR(`ratings-players-${region}-${season}`, () => {
|
2023-10-31 13:30:47 -05:00
|
|
|
if (season === null) {
|
|
|
|
|
// i'll deal with this later
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
2023-10-31 15:20:37 -05:00
|
|
|
return getArchivedRatingsLeaderboardAPI(region, season);
|
2023-10-31 13:30:47 -05:00
|
|
|
}
|
|
|
|
|
});
|
2023-10-31 15:20:37 -05:00
|
|
|
const [page, setPage] = useState(0);
|
|
|
|
|
const pageInput = useRef<HTMLInputElement>(null);
|
|
|
|
|
const playerSlice = players.data?.slice(
|
|
|
|
|
page * ROWS_PER_PAGE,
|
|
|
|
|
page * ROWS_PER_PAGE + ROWS_PER_PAGE,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
cachePage(page - 1);
|
|
|
|
|
cachePage(page);
|
|
|
|
|
cachePage(page + 1);
|
|
|
|
|
|
|
|
|
|
function cachePage(page: number) {
|
|
|
|
|
const ids = players.data
|
|
|
|
|
?.slice(page * ROWS_PER_PAGE, page * ROWS_PER_PAGE + ROWS_PER_PAGE)
|
|
|
|
|
.map(({ id }) => id)
|
|
|
|
|
.filter((id) => !(id in usernameCache[region]));
|
|
|
|
|
|
|
|
|
|
if (ids && ids.length > 0) {
|
|
|
|
|
getAccountInfo(region, ids).then((data) => {
|
|
|
|
|
data.map((player) => {
|
|
|
|
|
useUsernameCache.setState(
|
|
|
|
|
produce((draft) => {
|
|
|
|
|
draft[region][player.account_id] = player.nickname;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-10-31 15:31:03 -05:00
|
|
|
|
|
|
|
|
getClanAccountInfo(region, ids, ['clan']).then((data) => {
|
|
|
|
|
data.map((player) => {
|
|
|
|
|
useClanCache.setState(
|
|
|
|
|
produce((draft) => {
|
|
|
|
|
if (player) {
|
|
|
|
|
draft[region][player.account_id] =
|
|
|
|
|
player.clan?.tag ?? undefined;
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-10-31 15:20:37 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
|
|
|
|
}, [page]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setPage(0);
|
|
|
|
|
}, [season, region]);
|
2023-10-29 13:40:32 -05:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<PageWrapper>
|
2023-10-31 15:20:37 -05:00
|
|
|
<Flex gap="2">
|
2023-10-29 13:40:32 -05:00
|
|
|
<Select.Root
|
2023-10-31 15:20:37 -05:00
|
|
|
defaultValue={region}
|
2023-10-31 15:24:00 -05:00
|
|
|
onValueChange={(value) => setRegion(value as Region)}
|
2023-10-29 13:40:32 -05:00
|
|
|
>
|
|
|
|
|
<Select.Trigger style={{ flex: 1 }} />
|
|
|
|
|
|
|
|
|
|
<Select.Content>
|
2023-10-30 21:57:22 -05:00
|
|
|
{REGIONS.map((region) => (
|
|
|
|
|
<Select.Item key={region} value={region}>
|
|
|
|
|
{REGION_NAMES[region]}
|
|
|
|
|
</Select.Item>
|
|
|
|
|
))}
|
2023-10-29 13:40:32 -05:00
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
2023-10-30 21:57:22 -05:00
|
|
|
|
2023-10-29 13:40:32 -05:00
|
|
|
<Select.Root
|
2023-10-30 21:57:22 -05:00
|
|
|
value={`${season}`}
|
2023-10-29 13:40:32 -05:00
|
|
|
onValueChange={(value) =>
|
2023-10-30 21:57:22 -05:00
|
|
|
setSeason(value === 'null' ? null : parseInt(value))
|
2023-10-29 13:40:32 -05:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger style={{ flex: 1 }} />
|
|
|
|
|
|
|
|
|
|
<Select.Content>
|
2023-10-30 21:57:22 -05:00
|
|
|
{ratingsInfo?.detail === undefined && (
|
|
|
|
|
<Select.Item value="null">Current season</Select.Item>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Select.Group>
|
|
|
|
|
<Select.Label>Archives</Select.Label>
|
|
|
|
|
|
|
|
|
|
{latestArchivedSeasonNumber &&
|
|
|
|
|
range(
|
|
|
|
|
FIRST_ARCHIVED_RATINGS_SEASON,
|
|
|
|
|
latestArchivedSeasonNumber + 1,
|
|
|
|
|
)
|
|
|
|
|
.map((season) => (
|
|
|
|
|
<Select.Item key={season} value={`${season}`}>
|
|
|
|
|
Season {season}
|
|
|
|
|
</Select.Item>
|
|
|
|
|
))
|
|
|
|
|
.reverse()}
|
|
|
|
|
</Select.Group>
|
2023-10-29 13:40:32 -05:00
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
</Flex>
|
2023-10-31 13:30:47 -05:00
|
|
|
|
2023-10-31 15:31:03 -05:00
|
|
|
<Flex justify="between" wrap="wrap" gap="2">
|
2023-10-31 15:20:37 -05:00
|
|
|
<Flex gap="2">
|
|
|
|
|
<Popover.Root>
|
|
|
|
|
<Popover.Trigger>
|
|
|
|
|
<Button variant="soft">Jump to league</Button>
|
|
|
|
|
</Popover.Trigger>
|
|
|
|
|
|
|
|
|
|
<Popover.Content>
|
|
|
|
|
<Flex direction="column" gap="2">
|
|
|
|
|
<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">
|
|
|
|
|
<Popover.Close>
|
|
|
|
|
<Button color="red">Cancel</Button>
|
|
|
|
|
</Popover.Close>
|
|
|
|
|
|
|
|
|
|
<Popover.Close>
|
|
|
|
|
<Button>Jump</Button>
|
|
|
|
|
</Popover.Close>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Popover.Content>
|
|
|
|
|
</Popover.Root>
|
|
|
|
|
|
|
|
|
|
<Popover.Root>
|
|
|
|
|
<Popover.Trigger>
|
|
|
|
|
<Button variant="soft">Jump to player</Button>
|
|
|
|
|
</Popover.Trigger>
|
|
|
|
|
|
|
|
|
|
<Popover.Content>
|
|
|
|
|
<Flex gap="2" direction="column">
|
|
|
|
|
<TextField.Root>
|
|
|
|
|
<TextField.Slot>
|
|
|
|
|
<MagnifyingGlassIcon height="16" width="16" />
|
|
|
|
|
</TextField.Slot>
|
|
|
|
|
|
|
|
|
|
<TextField.Input placeholder="Search for player..." />
|
|
|
|
|
</TextField.Root>
|
|
|
|
|
|
|
|
|
|
<Flex gap="2">
|
|
|
|
|
<Popover.Close>
|
|
|
|
|
<Button color="red">Cancel</Button>
|
|
|
|
|
</Popover.Close>
|
|
|
|
|
|
|
|
|
|
<Popover.Close>
|
|
|
|
|
<Button>Jump</Button>
|
|
|
|
|
</Popover.Close>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Popover.Content>
|
|
|
|
|
</Popover.Root>
|
|
|
|
|
</Flex>
|
|
|
|
|
<Flex gap="2" align="center">
|
|
|
|
|
<Button
|
|
|
|
|
variant="soft"
|
|
|
|
|
onClick={() => setPage((page) => Math.max(page - 1, 0))}
|
|
|
|
|
>
|
|
|
|
|
{'<'}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<TextField.Root className={noArrows}>
|
|
|
|
|
<TextField.Slot>Page</TextField.Slot>
|
|
|
|
|
<TextField.Input
|
|
|
|
|
className={noArrows}
|
|
|
|
|
type="number"
|
|
|
|
|
ref={pageInput}
|
|
|
|
|
style={{ width: 64, textAlign: 'center' }}
|
|
|
|
|
onBlur={(event) => {
|
|
|
|
|
setPage(
|
|
|
|
|
Math.max(
|
|
|
|
|
0,
|
|
|
|
|
Math.min(
|
|
|
|
|
Math.floor((players.data?.length ?? 0) / ROWS_PER_PAGE),
|
|
|
|
|
event.target.valueAsNumber - 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
|
(event.target as HTMLInputElement).blur();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2023-10-31 13:30:47 -05:00
|
|
|
/>
|
2023-10-31 15:20:37 -05:00
|
|
|
<TextField.Slot>
|
|
|
|
|
out of {Math.ceil((players.data?.length ?? 0) / ROWS_PER_PAGE)}
|
|
|
|
|
</TextField.Slot>
|
|
|
|
|
</TextField.Root>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="soft"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setPage((page) =>
|
|
|
|
|
Math.min(
|
|
|
|
|
page + 1,
|
|
|
|
|
Math.floor((players.data?.length ?? 0) / ROWS_PER_PAGE),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{'>'}
|
|
|
|
|
</Button>
|
|
|
|
|
</Flex>
|
|
|
|
|
</Flex>
|
|
|
|
|
|
|
|
|
|
<Leaderboard.Root>
|
|
|
|
|
{playerSlice?.map((player, index) => (
|
|
|
|
|
<Leaderboard.Item
|
|
|
|
|
nickname={
|
|
|
|
|
usernameCache[region][player.id] ?? `Unknown player ${player.id}`
|
|
|
|
|
}
|
|
|
|
|
position={page * ROWS_PER_PAGE + index + 1}
|
|
|
|
|
score={player.score}
|
2023-10-31 15:31:03 -05:00
|
|
|
clan={clanCache[region][player.id]}
|
2023-10-31 15:20:37 -05:00
|
|
|
key={player.id}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Leaderboard.Root>
|
2023-10-31 13:30:47 -05:00
|
|
|
|
|
|
|
|
{players.isLoading && <Text>Loading...</Text>}
|
2023-10-29 13:40:32 -05:00
|
|
|
</PageWrapper>
|
|
|
|
|
);
|
|
|
|
|
}
|