diff --git a/src/app/tools/ratings/components/PageTurner.tsx b/src/app/tools/ratings/components/PageTurner.tsx new file mode 100644 index 000000000..f8bdf0fd5 --- /dev/null +++ b/src/app/tools/ratings/components/PageTurner.tsx @@ -0,0 +1,62 @@ +import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons'; +import { Button, Flex, TextField } from '@radix-ui/themes'; +import { useEffect, useRef } from 'react'; + +interface PageTurnerProps { + page: number; + pages: number; + setPage: (page: number | ((page: number) => number)) => void; +} + +export function PageTurner({ page, setPage, pages }: PageTurnerProps) { + const pageInput = useRef(null); + + useEffect(() => { + if (pageInput.current) pageInput.current.value = `${page + 1}`; + }, [page]); + + return ( + + + + + Page + { + setPage( + Math.max( + 0, + Math.min(pages - 1, event.target.valueAsNumber - 1), + ), + ); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + (event.target as HTMLInputElement).blur(); + } + }} + /> + out of {pages} + + + + + ); +} diff --git a/src/app/tools/ratings/page.css.ts b/src/app/tools/ratings/page.css.ts deleted file mode 100644 index 057061637..000000000 --- a/src/app/tools/ratings/page.css.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const noArrows = style({ - '::-webkit-progress-inner-value': { - appearance: 'none', - display: 'none', - margin: 0, - }, -}); diff --git a/src/app/tools/ratings/page.tsx b/src/app/tools/ratings/page.tsx index 9abab8dc5..79c13574b 100644 --- a/src/app/tools/ratings/page.tsx +++ b/src/app/tools/ratings/page.tsx @@ -1,11 +1,7 @@ 'use client'; import { blackA, orangeDark } from '@radix-ui/colors'; -import { - CaretLeftIcon, - CaretRightIcon, - MagnifyingGlassIcon, -} from '@radix-ui/react-icons'; +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { Button, Dialog, @@ -42,13 +38,17 @@ import { getArchivedLatestSeasonNumber } from '../../../core/blitzkrieg/getArchi import getArchivedRatingsInfo from '../../../core/blitzkrieg/getArchivedRatingsInfo'; import { getArchivedRatingsLeaderboard } from '../../../core/blitzkrieg/getArchivedRatingsLeaderboard'; import { withCORSProxy } from '../../../core/blitzkrieg/withCORSProxy'; -import { noArrows } from './page.css'; +import { PageTurner } from './components/PageTurner'; const ROWS_OPTIONS = [5, 10, 15, 25, 30]; const SEEDING_SIZE = 2 ** 7; type UsernameCache = Record>; type ClanCache = Record>; +type PlayersCache = Record< + Region, + Record> +>; const useUsernameCache = create(() => ({ asia: {}, @@ -60,57 +60,71 @@ const useClanCache = create(() => ({ com: {}, eu: {}, })); +const usePlayersCache = create(() => ({ + asia: { 0: {} }, + com: { 0: {} }, + eu: { 0: {} }, +})); export default function Page() { - const [rowsPerPage, setRowsPerPage] = useState(25); - const usernameCache = useUsernameCache(); - const clanCache = useClanCache(); const [region, setRegion] = useState('com'); - const leaguePositionCache = useRef>({}); - // null being the latest season - const [season, setSeason] = useState(null); - const { data: ratingsInfo } = useSWR( - `ratings-info-${region}-${season}`, - () => - season === null - ? getRatingsInfo(region) - : getArchivedRatingsInfo(region, season), - ); - const [jumpToLeague, setJumpToLeague] = useState(0); - const { data: latestArchivedSeasonNumber } = useSWR( - getArchivedLatestSeasonNumber.name, - getArchivedLatestSeasonNumber, - ); - const [players, setPlayers] = useState>(null); + const [season, setSeason] = useState(0); const [page, setPage] = useState(0); - const [highlightedPlayerId, setHighlightedPlayerId] = useState( - null, - ); - const positionInput = useRef(null); - const scoreInput = useRef(null); + 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); const [searchResults, setSearchResults] = useState< (AccountListItem & { number: number })[] | undefined >(undefined); const [loadingProgress, setLoadingProgress] = useState< [number, number] | null >(null); + const [highlightedPlayerId, setHighlightedPlayerId] = useState( + null, + ); + const { data: ratingsInfo } = useSWR( + `ratings-info-${region}-${season}`, + () => + season === 0 + ? getRatingsInfo(region) + : getArchivedRatingsInfo(region, season), + ); + const { data: latestArchivedSeasonNumber } = useSWR( + getArchivedLatestSeasonNumber.name, + getArchivedLatestSeasonNumber, + ); + + const usernames = useUsernameCache(); + const clans = useClanCache(); + const players = usePlayersCache(); + + const positionInput = useRef(null); + const scoreInput = useRef(null); + const leaguePositionCache = useRef>({}); + + const pages = Math.ceil( + (ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) / + rowsPerPage, + ); + + // page reset on season or region change useEffect(() => { - // league caching - if (season === null) { + setPage(0); + }, [season, region]); + // initial leagues caching for current season + useEffect(() => { + if (season === 0) { (async () => { const newUsernameCache: Record = {}; const newClanCache: Record = {}; 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; @@ -141,87 +155,61 @@ export default function Page() { {}, ); - setPlayers(newPlayers); - useUsernameCache.setState((old) => ({ - ...old, - [region]: { ...old[region], ...newUsernameCache }, - })); - useClanCache.setState((old) => ({ - ...old, - [region]: { ...old[region], ...newClanCache }, - })); + 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 }; + }), + ); })(); } else { - getArchivedRatingsLeaderboard(region, season).then((result) => - setPlayers({ ...result }), - ); + getArchivedRatingsLeaderboard(region, season).then((result) => { + usePlayersCache.setState( + produce((draft: PlayersCache) => { + draft[region][season] = { + ...draft[region][season], + ...result, + }; + }), + ); + }); } }, []); - + // 2nd page caching for current season useEffect(() => { - if (players !== null && 0 in players) { - cacheNeighbors(players[0].id, rowsPerPage * 3); + if (players !== null && 0 in players[region][season]) { + cacheNeighbors(players[region][season][0].id, rowsPerPage * 3); } - }, [players !== null && 0 in players]); - - async function cacheNeighbors(id: number, size: number) { - const { neighbors } = await getRatingsNeighbors(region, id, size); - - setPlayers( - produce((draft) => { - draft = draft ?? {}; - - neighbors.forEach((neighbor) => { - draft![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; - } - + }, [0 in players[region][season]]); + // neighboring page caching for all seasons useEffect(() => { - // TODO: caches twice on initial load - if (!players || !ratingsInfo || ratingsInfo?.detail) return; + if (!ratingsInfo || ratingsInfo?.detail) return; - if (season === null) { + if (season === 0) { const middleIndex = Math.round(page * rowsPerPage + rowsPerPage / 2); - - if (!(middleIndex in players)) return; - - const middleId = players[middleIndex].id; - + if (!(middleIndex in players[region][season])) return; + const middleId = players[region][season][middleIndex].id; cacheNeighbors(middleId, rowsPerPage * 3); } else { const ids = range( Math.max(0, (page - 1) * rowsPerPage), Math.min(ratingsInfo.count - 1, (page + 1) * rowsPerPage + rowsPerPage), ) - .map((index) => players![index].id) - .filter((id) => !(id in usernameCache[region])); + .map((index) => players[region][season][index].id) + .filter(Boolean) + .filter((id) => !(id in usernames[region])); if (ids && ids.length > 0) { getAccountInfo(region, ids).then((data) => { @@ -253,8 +241,44 @@ export default function Page() { } }, [page]); - const handleJumpToPosition = async () => { - if (!players || !ratingsInfo || ratingsInfo?.detail) return; + 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; const rawPosition = positionInput.current!.valueAsNumber - 1; const targetPosition = Math.max( @@ -262,8 +286,8 @@ export default function Page() { Math.min(rawPosition, ratingsInfo.count - 1), ); - if (season === null) { - const playerEntries = Object.entries(players); + if (season === 0) { + const playerEntries = Object.entries(players[region][season]); let closest: BlitzkriegRatingsLeaderboardEntry | null = null; let closestPosition: number | null = null; let closestPositionDistance = Infinity; @@ -299,9 +323,7 @@ export default function Page() { setLoadingProgress([0, closestPositionDistance]); while (!targetPositionAcquired) { - console.log('starting'); neighbors = await cacheNeighbors(seedingPlayer, SEEDING_SIZE); - console.log('done'); seedingPlayer = neighbors.at(loadingDirection === -1 ? 0 : -1)!.spa_id; if (loadingDirection === 1) { @@ -330,18 +352,20 @@ export default function Page() { setPage(Math.floor(targetPosition / rowsPerPage)); if (players) { - setHighlightedPlayerId(players[targetPosition].id); + setHighlightedPlayerId(players[region][season][targetPosition].id); } } - }; - const [jumpToPositionOpen, setJumpToPositionOpen] = useState(false); - const handleJumpToScore = () => { - if (!players || !ratingsInfo || ratingsInfo?.detail) return; + } + function handleJumpToScore() { + if (!ratingsInfo || ratingsInfo?.detail) return; let playerIndex = -1; for (let index = ratingsInfo.count - 1; index >= 0; index--) { - if (players[index].score >= scoreInput.current!.valueAsNumber) { + if ( + players[region][season][index].score >= + scoreInput.current!.valueAsNumber + ) { playerIndex = index; break; } @@ -352,75 +376,8 @@ export default function Page() { setPage(Math.floor(playerIndex / rowsPerPage)); if (players) { - setHighlightedPlayerId(players[playerIndex].id); + setHighlightedPlayerId(players[region][season][playerIndex].id); } - }; - const [jumpToScoreOpen, setJumpToScoreOpen] = useState(false); - const pages = Math.ceil( - (ratingsInfo && ratingsInfo.detail === undefined ? ratingsInfo?.count : 1) / - rowsPerPage, - ); - const [searchLoading, setSearchLoading] = useState(false); - - useEffect(() => { - setPage(0); - }, [season, region]); - useEffect(() => { - if (season === null && rowsPerPage > 30) setRowsPerPage(25); - }, [season]); - - function PageTurner() { - const pageInput = useRef(null); - - useEffect(() => { - if (pageInput.current) pageInput.current.value = `${page + 1}`; - }, [page]); - - return ( - - - - - Page - { - setPage( - Math.max( - 0, - Math.min(pages - 1, event.target.valueAsNumber - 1), - ), - ); - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - (event.target as HTMLInputElement).blur(); - } - }} - /> - out of {pages} - - - - - ); } return ( @@ -444,15 +401,13 @@ export default function Page() { - setSeason(value === 'null' ? null : parseInt(value)) - } + onValueChange={(value) => setSeason(parseInt(value))} > {ratingsInfo?.detail === undefined && ( - Current season + Current season )} @@ -584,16 +539,18 @@ export default function Page() {