2023-09-18 22:32:21 +00:00
|
|
|
'use client';
|
|
|
|
|
|
2024-05-02 18:27:27 -05:00
|
|
|
import { ArrowDownIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
2023-10-01 16:37:51 -05:00
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
Button,
|
2024-04-28 18:43:12 -05:00
|
|
|
Card,
|
2023-10-01 16:37:51 -05:00
|
|
|
Flex,
|
2024-05-01 17:20:08 -05:00
|
|
|
Heading,
|
2024-04-28 18:43:12 -05:00
|
|
|
Link,
|
2024-05-01 17:20:08 -05:00
|
|
|
Table,
|
2023-10-01 16:37:51 -05:00
|
|
|
Text,
|
|
|
|
|
TextField,
|
|
|
|
|
} from '@radix-ui/themes';
|
2023-09-18 23:05:19 +00:00
|
|
|
import { debounce } from 'lodash';
|
2024-05-01 17:20:08 -05:00
|
|
|
import { use, useEffect, useMemo, useRef, useState } from 'react';
|
2023-09-18 22:32:21 +00:00
|
|
|
import PageWrapper from '../../../components/PageWrapper';
|
2024-04-28 18:43:12 -05:00
|
|
|
import { UNLOCALIZED_REGION_NAMES_SHORT } from '../../../constants/regions';
|
2024-05-01 17:55:13 -05:00
|
|
|
import {
|
|
|
|
|
generateStats,
|
|
|
|
|
prettifyStats,
|
|
|
|
|
STAT_NAMES,
|
2024-05-01 22:11:12 -05:00
|
|
|
sumStats,
|
2024-05-01 17:55:13 -05:00
|
|
|
} from '../../../core/blitz/generateStats';
|
2024-05-01 17:20:08 -05:00
|
|
|
import {
|
|
|
|
|
getAccountInfo,
|
|
|
|
|
IndividualAccountInfo,
|
|
|
|
|
} from '../../../core/blitz/getAccountInfo';
|
2024-04-28 19:16:08 -05:00
|
|
|
import getTankStats from '../../../core/blitz/getTankStats';
|
2024-04-28 18:43:12 -05:00
|
|
|
import { idToRegion } from '../../../core/blitz/idToRegion';
|
2023-10-14 11:15:42 -05:00
|
|
|
import searchPlayersAcrossRegions, {
|
|
|
|
|
AccountListWithServer,
|
|
|
|
|
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
2024-05-01 17:20:08 -05:00
|
|
|
import { tankDefinitions } from '../../../core/blitzkit/tankDefinitions';
|
|
|
|
|
import { tankIcon } from '../../../core/blitzkit/tankIcon';
|
2024-05-01 17:55:13 -05:00
|
|
|
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
2024-05-01 20:54:27 -05:00
|
|
|
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
2024-05-02 18:27:27 -05:00
|
|
|
import { useWideFormat } from '../../../hooks/useWideFormat';
|
2024-05-01 17:29:40 -05:00
|
|
|
import { theme } from '../../../stitches.config';
|
2024-04-28 18:43:12 -05:00
|
|
|
import { useApp } from '../../../stores/app';
|
|
|
|
|
import mutateSession, {
|
|
|
|
|
SessionTracking,
|
|
|
|
|
useSession,
|
|
|
|
|
} from '../../../stores/session';
|
2024-05-01 17:20:08 -05:00
|
|
|
import { IndividualTankStats } from '../../../types/tanksStats';
|
2023-09-18 22:32:21 +00:00
|
|
|
|
2023-09-18 21:55:40 +00:00
|
|
|
export default function Page() {
|
2024-05-02 18:27:27 -05:00
|
|
|
const wideFormat = useWideFormat(640);
|
2024-05-01 17:20:08 -05:00
|
|
|
const awaitedTankDefinitions = use(tankDefinitions);
|
2024-05-01 17:55:13 -05:00
|
|
|
const awaitedTankAvearges = use(tankAverages);
|
2024-04-28 18:43:12 -05:00
|
|
|
const [showSearch, setShowSearch] = useState(false);
|
|
|
|
|
const [searching, setSearching] = useState(false);
|
|
|
|
|
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
|
2024-05-02 18:27:27 -05:00
|
|
|
const [showCCInaccessibilityPrompt, setShowCCInaccessibilityPrompt] =
|
|
|
|
|
useState(false);
|
2023-09-18 23:05:19 +00:00
|
|
|
const input = useRef<HTMLInputElement>(null);
|
2023-09-23 21:51:00 +00:00
|
|
|
const session = useSession();
|
2024-04-28 18:43:12 -05:00
|
|
|
const login = useApp((state) => state.login);
|
2024-05-01 17:20:08 -05:00
|
|
|
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
const accountInfoPromise = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
new Promise<IndividualAccountInfo | null>(async (resolve) => {
|
|
|
|
|
resolve(
|
|
|
|
|
session.tracking
|
|
|
|
|
? await getAccountInfo(session.player.region, session.player.id)
|
|
|
|
|
: null,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
[session.tracking && session.player.id],
|
|
|
|
|
);
|
|
|
|
|
const accountInfo = use(accountInfoPromise);
|
2024-05-01 20:54:27 -05:00
|
|
|
const delta = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
session.tracking && tankStatsB
|
2024-05-01 22:11:12 -05:00
|
|
|
? deltaTankStats(session.player.stats, tankStatsB)
|
|
|
|
|
.sort((a, b) => b.last_battle_time - a.last_battle_time)
|
|
|
|
|
.map((entry) => {
|
|
|
|
|
const tank = awaitedTankDefinitions[entry.tank_id];
|
|
|
|
|
const average = awaitedTankAvearges[tank.id];
|
|
|
|
|
const stats = generateStats(entry.all, average?.all);
|
|
|
|
|
const statsPretty = prettifyStats(stats);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tank,
|
|
|
|
|
stats,
|
|
|
|
|
statsPretty,
|
|
|
|
|
};
|
|
|
|
|
})
|
2024-05-01 20:54:27 -05:00
|
|
|
: undefined,
|
|
|
|
|
[session.tracking && session.player, tankStatsB],
|
|
|
|
|
);
|
2024-05-01 22:11:12 -05:00
|
|
|
const total = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
delta
|
|
|
|
|
? prettifyStats(sumStats(delta.map(({ stats }) => stats)))
|
|
|
|
|
: undefined,
|
|
|
|
|
[delta],
|
|
|
|
|
);
|
2024-04-28 18:43:12 -05:00
|
|
|
|
|
|
|
|
const search = debounce(async () => {
|
|
|
|
|
if (!input.current) return;
|
|
|
|
|
|
|
|
|
|
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 9));
|
|
|
|
|
setSearching(false);
|
|
|
|
|
}, 500);
|
2023-09-23 21:51:00 +00:00
|
|
|
|
2023-10-14 12:04:11 -05:00
|
|
|
useEffect(() => {
|
2024-05-02 18:27:27 -05:00
|
|
|
async function update() {
|
|
|
|
|
if (!session.tracking) return;
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-02 18:27:27 -05:00
|
|
|
const tankStats = await getTankStats(
|
|
|
|
|
idToRegion(session.player.id),
|
2024-04-28 19:16:08 -05:00
|
|
|
session.player.id,
|
|
|
|
|
);
|
2024-05-02 18:27:27 -05:00
|
|
|
setTankStatsB(tankStats);
|
2024-05-01 17:20:08 -05:00
|
|
|
}
|
2024-05-02 18:27:27 -05:00
|
|
|
|
|
|
|
|
update();
|
|
|
|
|
const interval = setInterval(update, 5 * 1000);
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
2024-04-28 18:43:12 -05:00
|
|
|
}, [session.tracking && session.player.id]);
|
2023-10-01 16:37:51 -05:00
|
|
|
|
2023-09-18 22:32:21 +00:00
|
|
|
return (
|
2024-04-28 18:43:12 -05:00
|
|
|
<PageWrapper>
|
2024-05-02 18:27:27 -05:00
|
|
|
<AlertDialog.Root
|
|
|
|
|
open={showCCInaccessibilityPrompt}
|
|
|
|
|
onOpenChange={setShowCCInaccessibilityPrompt}
|
|
|
|
|
>
|
|
|
|
|
<AlertDialog.Content>
|
|
|
|
|
<AlertDialog.Title>
|
|
|
|
|
You're attempting to track a CC account
|
|
|
|
|
</AlertDialog.Title>
|
|
|
|
|
<AlertDialog.Description>
|
|
|
|
|
CC (community contributor) accounts do not have public tank
|
|
|
|
|
statistics. <Link href="/tools/embeds">Looking for embeds?</Link>
|
|
|
|
|
</AlertDialog.Description>
|
2024-04-01 18:36:14 -05:00
|
|
|
|
2024-05-02 18:27:27 -05:00
|
|
|
<Flex mt="4" justify="end">
|
|
|
|
|
<AlertDialog.Action>
|
|
|
|
|
<Button variant="solid">Alright</Button>
|
|
|
|
|
</AlertDialog.Action>
|
|
|
|
|
</Flex>
|
|
|
|
|
</AlertDialog.Content>
|
|
|
|
|
</AlertDialog.Root>
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 17:20:08 -05:00
|
|
|
{!session.tracking && (
|
|
|
|
|
<Flex
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
align="center"
|
|
|
|
|
justify="center"
|
|
|
|
|
direction="column"
|
|
|
|
|
gap="3"
|
|
|
|
|
>
|
|
|
|
|
<Flex gap="2" align="center">
|
|
|
|
|
<Text color="gray">Look up a player to get started</Text>
|
|
|
|
|
<ArrowDownIcon />
|
|
|
|
|
</Flex>
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 17:20:08 -05:00
|
|
|
<TextField.Root
|
|
|
|
|
size="3"
|
2024-04-28 18:43:12 -05:00
|
|
|
style={{
|
2024-05-01 17:20:08 -05:00
|
|
|
position: 'relative',
|
|
|
|
|
width: '75vw',
|
|
|
|
|
maxWidth: 480,
|
|
|
|
|
}}
|
|
|
|
|
ref={input}
|
|
|
|
|
placeholder="Search players..."
|
|
|
|
|
onChange={() => {
|
|
|
|
|
if (!input.current) return;
|
|
|
|
|
|
|
|
|
|
const sanitized = input.current.value.trim();
|
|
|
|
|
|
|
|
|
|
if (sanitized.length > 0) {
|
|
|
|
|
setShowSearch(true);
|
|
|
|
|
setSearching(true);
|
|
|
|
|
search();
|
|
|
|
|
} else {
|
|
|
|
|
setShowSearch(false);
|
|
|
|
|
}
|
2024-04-28 18:43:12 -05:00
|
|
|
}}
|
|
|
|
|
>
|
2024-05-01 17:20:08 -05:00
|
|
|
<TextField.Slot>
|
|
|
|
|
<MagnifyingGlassIcon />
|
|
|
|
|
</TextField.Slot>
|
|
|
|
|
|
|
|
|
|
{showSearch && (
|
|
|
|
|
<Card
|
|
|
|
|
mt="2"
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '100%',
|
|
|
|
|
left: 0,
|
|
|
|
|
width: '100%',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{searching && (
|
|
|
|
|
<Flex justify="center">
|
|
|
|
|
<Text color="gray">Searching...</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!searching && searchResults.length === 0 && (
|
|
|
|
|
<Flex justify="center">
|
|
|
|
|
<Text color="gray">No players found</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!searching && searchResults.length > 0 && (
|
|
|
|
|
<Flex direction="column" gap="2">
|
|
|
|
|
{searchResults.map((player) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={player.account_id}
|
|
|
|
|
variant="ghost"
|
2024-05-01 20:54:27 -05:00
|
|
|
onClick={async () => {
|
2024-05-02 18:27:27 -05:00
|
|
|
const stats = await getTankStats(
|
2024-05-01 20:54:27 -05:00
|
|
|
player.region,
|
|
|
|
|
player.account_id,
|
2024-05-02 18:27:27 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (stats === null) {
|
|
|
|
|
setShowCCInaccessibilityPrompt(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-05-01 20:54:27 -05:00
|
|
|
|
2024-05-01 17:20:08 -05:00
|
|
|
setShowSearch(false);
|
|
|
|
|
mutateSession((draft) => {
|
|
|
|
|
if (!input.current) return;
|
|
|
|
|
|
|
|
|
|
draft.tracking = true;
|
|
|
|
|
(draft as SessionTracking).player = {
|
|
|
|
|
id: player.account_id,
|
|
|
|
|
region: player.region,
|
2024-05-01 17:23:33 -05:00
|
|
|
since: Date.now(),
|
2024-05-01 20:54:27 -05:00
|
|
|
stats,
|
2024-05-01 17:20:08 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
input.current.value = player.nickname;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{player.nickname} (
|
|
|
|
|
{UNLOCALIZED_REGION_NAMES_SHORT[player.region]})
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
2024-04-28 18:43:12 -05:00
|
|
|
)}
|
2024-05-01 17:20:08 -05:00
|
|
|
</TextField.Root>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 17:20:08 -05:00
|
|
|
{session.tracking && (
|
2024-05-02 18:27:27 -05:00
|
|
|
<Flex
|
|
|
|
|
mt="2"
|
|
|
|
|
mb="2"
|
|
|
|
|
direction={wideFormat ? 'row' : 'column'}
|
|
|
|
|
align="center"
|
|
|
|
|
justify="between"
|
|
|
|
|
gap="2"
|
|
|
|
|
>
|
|
|
|
|
<Flex mt="2" mb="2" direction="column" gap="2">
|
|
|
|
|
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
|
|
|
|
|
<Text color="gray">
|
|
|
|
|
Since {new Date(session.player.since).toLocaleString()}
|
|
|
|
|
</Text>
|
|
|
|
|
</Flex>
|
2024-05-01 17:29:40 -05:00
|
|
|
|
2024-05-02 18:27:27 -05:00
|
|
|
<Flex gap="2" align="center" justify="center">
|
2024-05-01 17:23:33 -05:00
|
|
|
<Button
|
|
|
|
|
color="red"
|
2024-05-02 18:27:27 -05:00
|
|
|
onClick={async () => {
|
|
|
|
|
const stats = await getTankStats(
|
|
|
|
|
session.player.region,
|
|
|
|
|
session.player.id,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (stats === null) {
|
|
|
|
|
setShowCCInaccessibilityPrompt(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mutateSession((draft) => {
|
|
|
|
|
(draft as SessionTracking).player.stats = stats;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Reset
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
2024-05-01 17:23:33 -05:00
|
|
|
onClick={() => {
|
|
|
|
|
mutateSession((draft) => {
|
|
|
|
|
draft.tracking = false;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-05-02 18:27:27 -05:00
|
|
|
Change
|
2024-05-01 17:29:40 -05:00
|
|
|
</Button>
|
2024-05-01 17:23:33 -05:00
|
|
|
</Flex>
|
2024-05-01 17:20:08 -05:00
|
|
|
</Flex>
|
|
|
|
|
)}
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 20:54:27 -05:00
|
|
|
{session.tracking && delta && delta.length > 0 && (
|
2024-05-01 17:20:08 -05:00
|
|
|
<Table.Root variant="surface">
|
|
|
|
|
<Table.Header>
|
|
|
|
|
<Table.Row>
|
|
|
|
|
<Table.ColumnHeaderCell>Tank</Table.ColumnHeaderCell>
|
2024-05-01 17:55:13 -05:00
|
|
|
{session.columns.map((column) => (
|
|
|
|
|
<Table.ColumnHeaderCell width="0" key={column} align="right">
|
|
|
|
|
{STAT_NAMES[column]}
|
|
|
|
|
</Table.ColumnHeaderCell>
|
|
|
|
|
))}
|
2024-05-01 17:20:08 -05:00
|
|
|
</Table.Row>
|
|
|
|
|
</Table.Header>
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 17:20:08 -05:00
|
|
|
<Table.Body>
|
2024-05-01 22:11:12 -05:00
|
|
|
<Table.Row
|
|
|
|
|
style={{
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Table.RowHeaderCell
|
|
|
|
|
style={{
|
|
|
|
|
paddingLeft: 32,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflowY: 'hidden',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Total
|
|
|
|
|
</Table.RowHeaderCell>
|
|
|
|
|
{session.columns.map((column) => (
|
|
|
|
|
<Table.Cell key={column} align="right">
|
|
|
|
|
{total![column]}
|
|
|
|
|
</Table.Cell>
|
|
|
|
|
))}
|
|
|
|
|
</Table.Row>
|
2024-04-28 18:43:12 -05:00
|
|
|
|
2024-05-01 22:11:12 -05:00
|
|
|
{delta.map(({ statsPretty, tank }) => {
|
2024-05-01 20:54:27 -05:00
|
|
|
return (
|
|
|
|
|
<Table.Row
|
|
|
|
|
key={tank.id}
|
|
|
|
|
style={{
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Table.RowHeaderCell
|
2024-05-01 17:20:08 -05:00
|
|
|
style={{
|
2024-05-01 20:54:27 -05:00
|
|
|
paddingLeft: 32,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
overflowY: 'hidden',
|
2024-04-28 18:43:12 -05:00
|
|
|
}}
|
|
|
|
|
>
|
2024-05-01 20:54:27 -05:00
|
|
|
<Link href={`/tools/tankopedia/${tank.id}`}>
|
|
|
|
|
<img
|
|
|
|
|
draggable={false}
|
|
|
|
|
src={tankIcon(tank.id)}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
width: 128 + 32,
|
|
|
|
|
height: '200%',
|
|
|
|
|
top: '-50%',
|
|
|
|
|
left: 0,
|
|
|
|
|
objectFit: 'contain',
|
|
|
|
|
objectPosition: '50% 50%',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2024-05-01 17:20:08 -05:00
|
|
|
|
2024-05-01 20:54:27 -05:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: 'red',
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
height: '100%',
|
|
|
|
|
width: 128,
|
|
|
|
|
background:
|
|
|
|
|
'linear-gradient(90deg, #00000080, #00000000)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2024-05-01 17:20:08 -05:00
|
|
|
|
2024-05-01 20:54:27 -05:00
|
|
|
<Text
|
|
|
|
|
style={{
|
|
|
|
|
color: theme.colors.textHighContrast,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
textWrap: 'nowrap',
|
|
|
|
|
textShadow: 'black 0 0 4px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tank.name}
|
|
|
|
|
</Text>
|
|
|
|
|
</Link>
|
|
|
|
|
</Table.RowHeaderCell>
|
|
|
|
|
{session.columns.map((column) => (
|
|
|
|
|
<Table.Cell key={column} align="right">
|
2024-05-01 22:11:12 -05:00
|
|
|
{statsPretty[column]}
|
2024-05-01 20:54:27 -05:00
|
|
|
</Table.Cell>
|
|
|
|
|
))}
|
|
|
|
|
</Table.Row>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2024-05-01 17:20:08 -05:00
|
|
|
</Table.Body>
|
|
|
|
|
</Table.Root>
|
|
|
|
|
)}
|
|
|
|
|
|
2024-05-01 20:54:27 -05:00
|
|
|
{session.tracking && delta && delta.length === 0 && (
|
2024-05-01 17:20:08 -05:00
|
|
|
<Flex
|
|
|
|
|
align="center"
|
|
|
|
|
justify="center"
|
|
|
|
|
direction="column"
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Heading color="gray">No battles</Heading>
|
|
|
|
|
<Text color="gray">Go ahead and play a game</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
2023-09-18 22:32:21 +00:00
|
|
|
</PageWrapper>
|
|
|
|
|
);
|
2023-09-18 21:55:40 +00:00
|
|
|
}
|