diff --git a/packages/website-legacy/src/app/tools/rating/page.tsx b/packages/website-legacy/src/app/tools/rating/page.tsx index d54a126e8..e69de29bb 100644 --- a/packages/website-legacy/src/app/tools/rating/page.tsx +++ b/packages/website-legacy/src/app/tools/rating/page.tsx @@ -1,1150 +0,0 @@ -import { - AccountList, - AccountListItem, - fetchBlitz, - FIRST_MINIMAL_ARCHIVED_RATING_SEASON, - getArchivedLatestSeasonNumber, - getArchivedRatingInfo, - getArchivedRatingLeaderboard, - getClanAccountInfo, - getRatingInfo, - getRatingLeague, - getRatingNeighbors, - LEAGUES, - RatingInfo, - RatingPlayer, - Region, - REGIONS, - searchCurrentRatingPlayers, - UNLOCALIZED_REGION_NAMES, -} from '@blitzkit/core'; -import { blackA, orangeDark } from '@radix-ui/colors'; -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { - Button, - Dialog, - Flex, - Select, - Skeleton, - Table, - Text, - TextField, -} from '@radix-ui/themes'; -import { produce } from 'immer'; -import { debounce, range } from 'lodash-es'; -import { ChangeEvent, useEffect, useRef, useState } from 'react'; -import { create } from 'zustand'; -import { PageWrapper } from '../../../components/PageWrapper'; -import { Var } from '../../../core/radix/var'; -import { PageTurner } from './components/PageTurner'; - -const ROWS_PER_PAGE = Math.floor(100 / 3); -const SEEDING_SIZE = 2 ** 7; - -interface NameCacheEntry { - nickname: string; - clan: string | null; -} -type NameCache = Record>; -type LeaderboardCache = Record>>; -type InfoCache = Record>; -type ScoreCache = Record>>; - -const useNameCache = create(() => ({ - asia: {}, - com: {}, - eu: {}, -})); -const useLeaderboardCache = create(() => ({ - asia: {}, - com: {}, - eu: {}, -})); -const useInfoCache = create(() => ({ - asia: {}, - com: {}, - eu: {}, -})); -const useScoreCache = create(() => ({ - asia: {}, - com: {}, - eu: {}, -})); - -function toLatestURL(url: string) { - return url.replace(/(\d+\.)+\d+/, 'latest'); -} - -export default function Page() { - const [region, setRegion] = useState('com'); - const [season, setSeason] = useState(0); - const [page, setPage] = useState(0); - 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 [highlightedPlayer, setHighlightedPlayer] = useState< - { type: 'id'; id: number } | { type: 'position'; position: number } | null - >(null); - - const ratingInfo = useInfoCache()[region][season]; - const [latestArchivedSeasonNumber, setLatestArchivedSeasonNumber] = useState< - number | null - >(null); - const scores = useScoreCache()[region][season]; - - const names = useNameCache(); - const leaderboard = useLeaderboardCache(); - - const positionInput = useRef(null); - const scoreInput = useRef(null); - const leaguePositionCache = useRef>>({ - asia: {}, - com: {}, - eu: {}, - }); - - const pages = Math.ceil( - (ratingInfo && ratingInfo.detail === undefined ? ratingInfo?.count : 1) / - ROWS_PER_PAGE, - ); - - // page reset on season or region change - useEffect(() => { - setPage(0); - - (async () => { - const newInfo = - season === 0 - ? await getRatingInfo(region) - : await getArchivedRatingInfo(region, season); - useInfoCache.setState( - produce((draft: InfoCache) => { - draft[region][season] = newInfo; - }), - ); - })(); - }, [season, region]); - // initial caching - useEffect(() => { - if (season in leaderboard[region]) return; - - if (season === 0) { - (async () => { - const results = ( - await Promise.all( - range(0, 5).map(async (league: number) => { - const data = await getRatingLeague(region, league); - leaguePositionCache.current[region][league] = data[0].number - 1; - - return data; - }), - ) - ).flat(); - - useLeaderboardCache.setState( - produce((draft: LeaderboardCache) => { - if (!(season in draft[region])) draft[region][season] = {}; - - results.forEach((result) => { - draft[region][season][result.number - 1] = result.spa_id; - }); - }), - ); - useNameCache.setState( - produce((draft: NameCache) => { - results.forEach((result) => { - draft[region][result.spa_id] = { - nickname: result.nickname, - clan: result.clan_tag, - }; - }); - }), - ); - useScoreCache.setState( - produce((draft: ScoreCache) => { - if (!(season in draft[region])) draft[region][season] = {}; - - results.forEach((result) => { - draft[region][season][result.spa_id] = result.score; - }); - }), - ); - })(); - } else { - getArchivedRatingLeaderboard(region, season).then((results) => { - useLeaderboardCache.setState( - produce((draft: LeaderboardCache) => { - if (!(season in draft[region])) draft[region][season] = {}; - - results.entries.forEach((result, index) => { - draft[region][season][index] = result.id; - }); - }), - ); - useScoreCache.setState( - produce((draft: ScoreCache) => { - if (!(season in draft[region])) draft[region][season] = {}; - - results.entries.forEach((result) => { - draft[region][season][result.id] = result.score; - }); - }), - ); - }); - } - }, [season in leaderboard[region]]); - // 2nd page caching for current season - useEffect(() => { - if ( - season === 0 && - leaderboard !== null && - leaderboard[region][season] && - 0 in leaderboard[region][season] - ) { - cacheNeighbors(leaderboard[region][season][0], ROWS_PER_PAGE * 3); - } - }, [leaderboard[region]?.[season] && 0 in leaderboard[region][season]]); - // neighboring page caching for all seasons - useEffect(() => { - if (!ratingInfo || ratingInfo?.detail) return; - - if (season === 0) { - const middleIndex = page * ROWS_PER_PAGE + ROWS_PER_PAGE / 2; - const best = range( - page * ROWS_PER_PAGE, - page * ROWS_PER_PAGE + ROWS_PER_PAGE + 1, - ) - .filter((index) => index in leaderboard[region][season]) - .map((index) => ({ index, distance: Math.abs(index - middleIndex) })) - .sort((a, b) => b.distance - a.distance) - .at(0); - - if (best === undefined) return; - - cacheNeighbors( - leaderboard[region][season][best.index], - 2 * (Math.floor(ROWS_PER_PAGE / 2) + best.distance + ROWS_PER_PAGE), - ); - } else { - const ids = range( - Math.max(0, (page - 1) * ROWS_PER_PAGE), - Math.min( - ratingInfo.count - 1, - (page + 1) * ROWS_PER_PAGE + ROWS_PER_PAGE, - ), - ) - .map((index) => leaderboard[region][season]?.[index]) - .filter(Boolean) - .filter((id) => !(id in names[region])); - - 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, - } - : null; - }), - ); - }); - }); - } - } - }, [ - page, - region, - season, - ratingInfo?.detail, - leaderboard[region][season] !== undefined && - 0 in leaderboard[region][season], - ]); - // latest archived season number caching - useEffect(() => {}, [ - getArchivedLatestSeasonNumber().then((result) => { - setLatestArchivedSeasonNumber(result); - }), - ]); - - async function cacheNeighbors( - id: number, - size: number, - ): Promise { - const radius = Math.round(size / 2); - const targetPosition = Object.entries(leaderboard[region][season]).find( - ([, player]) => id === player, - )?.[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 leaderboard[region][season]); - if (isMissing) break; - } - - if (!isMissing) { - return null; - } - } - - const { neighbors } = await getRatingNeighbors(region, id, size, true); - - useLeaderboardCache.setState( - produce((draft: LeaderboardCache) => { - neighbors.forEach((neighbor, neighborIndex) => { - const position = neighbor.number - 1; - - if (!(position in draft[region][season])) { - draft[region][season][position] = neighbor.spa_id; - } - }); - }), - ); - useScoreCache.setState( - produce((draft: ScoreCache) => { - neighbors.forEach((neighbor) => { - if (!(neighbor.spa_id in draft[region][season])) { - draft[region][season][neighbor.spa_id] = neighbor.score; - } - }); - }), - ); - useNameCache.setState( - produce((draft: NameCache) => { - neighbors.forEach((neighbor) => { - if (neighbor.nickname) { - draft[region][neighbor.spa_id] = { - nickname: neighbor.nickname, - clan: neighbor.clan_tag, - }; - } - }); - }), - ); - - return neighbors; - } - async function jumpToPosition(position: number) { - if (!ratingInfo || ratingInfo?.detail) return; - - const targetPosition = Math.min( - ratingInfo.count - 1, - Math.max(0, Math.min(position, ratingInfo.count - 1)), - ); - - if (season === 0) { - const playerEntries = Object.entries(leaderboard[region][season]); - let closestPlayerId: number | 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; - closestPlayerId = player; - } - }); - - if (closestPlayerId === null || closestPosition === null) return; - - if (closestPositionDistance === 0) { - setHighlightedPlayer({ type: 'position', position: targetPosition }); - setPage(Math.floor(targetPosition / ROWS_PER_PAGE)); - - return; - } - - // 1 going down and -1 going up - const loadingDirection = Math.sign(targetPosition - closestPosition); - let seedingPlayer = closestPlayerId as number; - let targetPositionAcquired = false; - let neighbors: RatingPlayer[] | null; - - setLoadingProgress([0, closestPositionDistance]); - - while (!targetPositionAcquired) { - neighbors = await cacheNeighbors(seedingPlayer, SEEDING_SIZE); - - if (neighbors === null) { - targetPositionAcquired = true; - break; - } - - 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 / ROWS_PER_PAGE)); - setHighlightedPlayer({ type: 'position', position: targetPosition }); - } else { - setPage(Math.floor(targetPosition / ROWS_PER_PAGE)); - - if (leaderboard) { - setHighlightedPlayer({ - type: 'position', - position: targetPosition, - }); - } - } - } - async function jumpToScore() { - if (!ratingInfo || ratingInfo?.detail) return; - - const targetScore = scoreInput.current!.valueAsNumber; - - if (season === 0) { - const playerEntries = Object.entries(leaderboard[region][season]); - let closestId: number | null = null; - let closestScore: number | null = null; - let closestScoreDistance = Infinity; - let closesIndex = -1; - - playerEntries.forEach(([positionAsString, id]) => { - const position = parseInt(positionAsString); - const distance = Math.abs(scores[id] - targetScore); - - if (distance < closestScoreDistance) { - closestScoreDistance = distance; - closestScore = scores[id]; - closestId = id; - closesIndex = position; - } - }); - - if (closestId === null || closestScore === null) return; - - if (closestScoreDistance === 0) { - setHighlightedPlayer({ type: 'id', id: closesIndex }); - setPage(Math.floor(closesIndex / ROWS_PER_PAGE)); - } - - const loadingDirection = Math.sign(closestScore - targetScore); - let targetPlayerId: number; - let targetPlayerPosition: number; - let scoreAcquired = false; - let seedingId = closestId as number; - let seedingScore = scores[seedingId]; - - setLoadingProgress([0, closestScoreDistance]); - while (!scoreAcquired) { - const neighbors = await cacheNeighbors(seedingId, SEEDING_SIZE); - - if (neighbors === null) { - const closesEntry = Object.entries(scores) - .sort(([, a], [, b]) => b - a) - .findLast(([, score]) => score >= targetScore); - - if (closesEntry === undefined) return; - - targetPlayerId = parseInt(closesEntry[0]); - targetPlayerPosition = parseInt( - Object.entries(leaderboard[region][season]).find( - ([, id]) => id === targetPlayerId, - )![0], - ); - scoreAcquired = true; - - break; - } - - const seedingNeighbor = neighbors.at(loadingDirection === 1 ? -1 : 0)!; - seedingId = seedingNeighbor.spa_id; - seedingScore = scores[seedingId]; - - setLoadingProgress([ - closestScoreDistance - Math.abs(seedingNeighbor.score - targetScore), - closestScoreDistance, - ]); - - if (loadingDirection === 1) { - scoreAcquired = seedingScore <= targetScore; - } else { - scoreAcquired = seedingScore >= targetScore; - } - - if (scoreAcquired) { - if (loadingDirection === 1) { - const targetPlayer = neighbors.find( - (player) => player.score <= targetScore, - )!; - - targetPlayerId = targetPlayer.spa_id; - targetPlayerPosition = targetPlayer.number - 1; - } else { - const targetPlayer = neighbors.findLast( - (player) => player.score >= targetScore, - )!; - - targetPlayerId = targetPlayer.spa_id; - targetPlayerPosition = targetPlayer.number - 1; - } - } - } - - setPage(Math.floor(targetPlayerPosition! / ROWS_PER_PAGE)); - setHighlightedPlayer({ type: 'id', id: targetPlayerId! }); - setLoadingProgress(null); - } else { - let playerIndex = -1; - - for (let index = ratingInfo.count; index >= 0; index--) { - if (!(index in leaderboard[region][season])) continue; - - if (scores[leaderboard[region][season][index]] >= targetScore) { - playerIndex = index; - break; - } - } - - if (playerIndex === -1) return; - - setPage(Math.floor(playerIndex / ROWS_PER_PAGE)); - - if (leaderboard) { - setHighlightedPlayer({ - type: 'position', - position: playerIndex, - }); - } - } - } - - const totalPlayers = - !ratingInfo?.detail && ratingInfo?.count ? ratingInfo.count : undefined; - - return ( - - - - setRegion(value as Region)} - > - - - - {REGIONS.map((region) => ( - - {UNLOCALIZED_REGION_NAMES[region]} - - ))} - - - - setSeason(parseInt(value))} - > - - - - {ratingInfo?.detail === undefined && ( - Current season - )} - - - Archives - - {latestArchivedSeasonNumber && - range( - FIRST_MINIMAL_ARCHIVED_RATING_SEASON, - latestArchivedSeasonNumber + 1, - ) - .map((season) => ( - - Season {season} - - )) - .reverse()} - - - - - - - Jump to: - - - - - - - Jump to score - - - { - if (event.key === 'Enter') { - jumpToScore(); - setJumpToScoreOpen(false); - } - }} - /> - - - - - - - - - - - - - - - - - - - Jump to position - - - { - if (event.key === 'Enter') { - jumpToPosition(positionInput.current!.valueAsNumber - 1); - setJumpToPositionOpen(false); - } - }} - /> - - - - - - - - - - - - - - - - - - - Jump to league - - - setJumpToLeague(parseInt(value))} - > - - - - {ratingInfo?.detail === undefined && - ratingInfo?.leagues.map(({ index, title }) => ( - - {title} - - ))} - - - - - - - - - - - - - - - - - - - - - Jump to player - - - ) => { - const trimmedSearch = event.target.value.trim(); - const encodedSearch = encodeURIComponent(trimmedSearch); - - if (trimmedSearch) { - if (season === 0) { - setSearchLoading(true); - - const accountList = await searchCurrentRatingPlayers( - region, - trimmedSearch, - ); - const values = Object.values(accountList).filter( - (searchedPlayer) => !searchedPlayer.skip, - ); - const valuesWithTypes = - values as ((typeof values)[number] & { - skip: false; - })[]; - - useLeaderboardCache.setState( - produce((draft: LeaderboardCache) => { - if (draft) { - values.forEach((player) => { - const playerWithTypes = - player as typeof player & { skip: false }; - - draft[region][season][ - playerWithTypes.number - 1 - ] = playerWithTypes.spa_id; - }); - } - }), - ); - useNameCache.setState( - produce((draft: typeof names) => { - valuesWithTypes.forEach((searchedPlayer) => { - if (searchedPlayer.nickname) { - draft[region][searchedPlayer.spa_id] = { - nickname: searchedPlayer.nickname, - clan: 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( - region, - 'account/list', - { search: encodedSearch }, - ); - const playerKeys = Object.keys( - leaderboard[region][season], - ); - - setSearchResults( - accountList - .map((searchedPlayer) => ({ - ...searchedPlayer, - number: parseInt( - playerKeys.find( - (indexAsString) => - leaderboard[region][season][ - parseInt(indexAsString) - ] === searchedPlayer.account_id, - )!, - ), - })) - .filter((player) => !Number.isNaN(player.number)), - ); - - setSearchLoading(false); - } - } else { - setSearchResults(undefined); - } - }, - 500, - )} - placeholder="Search for player..." - > - - - - - - - {!searchLoading && - searchResults?.map((player) => ( - - - - ))} - - {!searchLoading && searchResults?.length === 0 && ( - - )} - - {!searchLoading && searchResults === undefined && ( - - )} - - {searchLoading && ( - - )} - - - - - - - - - - - - - - - isButtonClick ? setPage(page) : jumpToPosition(page * ROWS_PER_PAGE) - } - /> - - - - - Position - Player - Reward - - Percentile - - Score - - - - {range( - page * ROWS_PER_PAGE, - Math.min( - page * ROWS_PER_PAGE + ROWS_PER_PAGE, - ratingInfo?.detail ? 0 : (ratingInfo?.count ?? 1) - 1, - ), - ).map((index) => { - const id = leaderboard[region][season]?.[index]; - const clan = id ? names[region][id]?.clan : undefined; - const position = index + 1; - const highlight = highlightedPlayer - ? highlightedPlayer.type === 'id' - ? highlightedPlayer.id === id - : highlightedPlayer.position === index - : false; - const reward = - !ratingInfo?.detail && ratingInfo?.rewards - ? ratingInfo.rewards.find( - (reward) => - reward.from_position <= position && - reward.to_position >= position, - ) - : undefined; - - return ( - { - setHighlightedPlayer({ type: 'position', position: index }); - }} - > - {position.toLocaleString()}. - - - {names[region][id] === null - ? `Deleted player ${id}` - : (names[region][id]?.nickname ?? ( - - ))} - {clan ? ( - {`[${clan}]`} - ) : ( - clan === undefined && - names[region][id] !== null && ( - - ) - )} - - - - {reward ? ( - - {toLatestURL( - - {reward.count === 1 ? '' : `x ${reward.count}`} - - - ) : null} - - - {totalPlayers ? ( - `${Math.ceil(((index + 1) / totalPlayers) * 100)}%` - ) : ( - - )} - - - {(leaderboard[region][season]?.[index] - ? scores[leaderboard[region][season][index]]?.toLocaleString() - : undefined) ?? ( - - )} - - - ); - })} - - - - isButtonClick ? setPage(page) : jumpToPosition(page * ROWS_PER_PAGE) - } - /> - - {loadingProgress !== null && ( - - - Loading {loadingProgress[0]} of {loadingProgress[1]} assets... - - -
-
-
- - - Please be patient. Wargaming is slow. - {loadingProgress[0] === 0 && ( - First assets will be fetched shortly. - )} - - - )} - - ); -} diff --git a/packages/website/src/constants/tools.ts b/packages/website/src/constants/tools.ts index 45edd3f9e..de058fc88 100644 --- a/packages/website/src/constants/tools.ts +++ b/packages/website/src/constants/tools.ts @@ -33,19 +33,19 @@ export const tankopediaTool: Tool = { branches: ['main', 'dev', 'opentest'], }; -export const ratingTool: Tool = { - id: 'rating', - title: 'Rating', - description: 'Full rating leaderboard', - pageDescription: - 'Live and archived full rating leaderboard for World of Tanks Blitz', - image: 'WEyMZH3', - button: { - text: 'View leaderboard', - color: 'orange', - }, - branches: ['main', 'dev'], -}; +// export const ratingTool: Tool = { +// id: 'rating', +// title: 'Rating', +// description: 'Full rating leaderboard', +// pageDescription: +// 'Live and archived full rating leaderboard for World of Tanks Blitz', +// image: 'WEyMZH3', +// button: { +// text: 'View leaderboard', +// color: 'orange', +// }, +// branches: ['main', 'dev'], +// }; export const compareTool: Tool = { id: 'compare', @@ -156,5 +156,5 @@ export const TOOLS: Tool[] = [ tankPerformanceTool, sessionTool, embedTool, - ratingTool, + // ratingTool, ]; diff --git a/packages/website/src/pages/index.astro b/packages/website/src/pages/index.astro index fb8f8f7b2..d1d5b9764 100644 --- a/packages/website/src/pages/index.astro +++ b/packages/website/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import { Flex, Heading } from '@radix-ui/themes'; +import { Box, Flex, Heading } from '@radix-ui/themes'; import HomeHero from '../components/HomeHero.astro'; import { Plugs } from '../components/Plugs.tsx'; import ToolCard from '../components/ToolCard.astro'; @@ -7,7 +7,6 @@ import { compareTool, discordTool, moreTool, - ratingTool, sessionTool, tankopediaTool, tankPerformanceTool, @@ -41,7 +40,8 @@ import RootLayout from '../layouts/RootLayout.astro'; {/* */} - + + - +