mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: username display
This commit is contained in:
1 parent
04ce20857e
commit
d7dfbaa799
4 files changed
+204
-113
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const noArrows = style({
|
||||
'::-webkit-progress-inner-value': {
|
||||
appearance: 'none',
|
||||
display: 'none',
|
||||
margin: 0,
|
||||
},
|
||||
});
|
||||
+194
-88
@@ -9,53 +9,104 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { produce } from 'immer';
|
||||
import { range } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { create } from 'zustand';
|
||||
import * as Leaderboard from '../../../components/Leaderboard';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { REGIONS, REGION_NAMES, Region } from '../../../constants/regions';
|
||||
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
|
||||
import getRatingsInfo from '../../../core/blitz/getRatingsInfo';
|
||||
import getArchivedRatingsInfoAPI from '../../../core/blitzkrieg/getArchivedRatingsInfoAPI';
|
||||
import { getArchivedRatingsLeaderboardAPI } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboardAPI';
|
||||
import { numberFetcher } from '../../../core/blitzkrieg/numberFetcher';
|
||||
import { useRatings } from '../../../stores/ratings';
|
||||
import { noArrows } from './page.css';
|
||||
|
||||
export const FIRST_ARCHIVED_RATINGS_SEASON = 49;
|
||||
const ROWS_PER_PAGE = 64;
|
||||
|
||||
const useUsernameCache = create<Record<Region, Record<number, string>>>(() => ({
|
||||
asia: {},
|
||||
com: {},
|
||||
eu: {},
|
||||
}));
|
||||
const clanCache: Record<Region, Record<number, string>> = {
|
||||
com: {},
|
||||
eu: {},
|
||||
asia: {},
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const ratings = useRatings();
|
||||
const usernameCache = useUsernameCache();
|
||||
const [region, setRegion] = useState<Region>('com');
|
||||
// null being the latest season
|
||||
const [season, setSeason] = useState<null | number>(null);
|
||||
const { data: ratingsInfo } = useSWR(
|
||||
`ratings-info-${ratings.region}-${season}`,
|
||||
`ratings-info-${region}-${season}`,
|
||||
() =>
|
||||
season === null
|
||||
? getRatingsInfo(ratings.region)
|
||||
: getArchivedRatingsInfoAPI(ratings.region, season),
|
||||
? getRatingsInfo(region)
|
||||
: getArchivedRatingsInfoAPI(region, season),
|
||||
);
|
||||
const [jumpToLeague, setJumpToLeague] = useState(0);
|
||||
const { data: latestArchivedSeasonNumber } = useSWR<number>(
|
||||
'/api/ratings/latest-archived-season-number',
|
||||
numberFetcher,
|
||||
);
|
||||
const players = useSWR(`ratings-players-${ratings.region}-${season}`, () => {
|
||||
const players = useSWR(`ratings-players-${region}-${season}`, () => {
|
||||
if (season === null) {
|
||||
// i'll deal with this later
|
||||
return null;
|
||||
} else {
|
||||
return getArchivedRatingsLeaderboardAPI(ratings.region, season);
|
||||
return getArchivedRatingsLeaderboardAPI(region, season);
|
||||
}
|
||||
});
|
||||
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;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [season, region]);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<Flex gap="2">
|
||||
<Select.Root
|
||||
defaultValue={ratings.region}
|
||||
onValueChange={(value) =>
|
||||
useRatings.setState({ region: value as Region })
|
||||
}
|
||||
defaultValue={region}
|
||||
onValueChange={(value) => setRegion(region)}
|
||||
>
|
||||
<Select.Trigger style={{ flex: 1 }} />
|
||||
|
||||
@@ -98,84 +149,139 @@ export default function Page() {
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<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>
|
||||
|
||||
{players.data && (
|
||||
<Leaderboard.Root>
|
||||
{players.data.map((player, index) => (
|
||||
<Leaderboard.Item
|
||||
nickname={`${player.id}`}
|
||||
position={index + 1}
|
||||
score={player.score}
|
||||
key={player.id}
|
||||
<Flex justify="between">
|
||||
<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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Leaderboard.Root>
|
||||
)}
|
||||
<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}
|
||||
key={player.id}
|
||||
/>
|
||||
))}
|
||||
</Leaderboard.Root>
|
||||
|
||||
{players.isLoading && <Text>Loading...</Text>}
|
||||
</PageWrapper>
|
||||
|
||||
@@ -32,7 +32,7 @@ async function manageQueue() {
|
||||
if (data.status === 'ok') {
|
||||
resolve(data.data);
|
||||
} else {
|
||||
throw new Error(`Wargaming response error status:"${data.status}"`, {
|
||||
throw new Error(`Wargaming response error status: "${data.status}"`, {
|
||||
cause: `Message: "${data.error.message}"\nURL: "${url}"`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { Region } from '../constants/regions';
|
||||
|
||||
interface Ratings {
|
||||
/**
|
||||
* When null, the current season is used unless there ain't one in which case
|
||||
* we do whatever lol
|
||||
*/
|
||||
region: Region;
|
||||
}
|
||||
|
||||
export const useRatings = create<Ratings>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
region: 'com',
|
||||
}),
|
||||
{
|
||||
name: 'ratings',
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
Reference in new issue
Block a user