Files
blitzkit/src/app/tools/session/page.tsx
T

430 lines
13 KiB
TypeScript
Raw Normal View History

2023-09-18 22:32:21 +00:00
'use client';
2024-05-01 17:20:08 -05:00
import {
ArrowDownIcon,
InfoCircledIcon,
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 13:04:31 -05:00
import { authURL } from '../../../core/blitz/authURL';
2024-05-01 17:55:13 -05:00
import {
generateStats,
prettifyStats,
STAT_NAMES,
} 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-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-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>([]);
const [showFurtherVerification, setShowFurtherVerification] = 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
? deltaTankStats(session.player.stats, tankStatsB).sort(
(a, b) => b.last_battle_time - a.last_battle_time,
)
: undefined,
[session.tracking && session.player, tankStatsB],
);
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-04-28 18:43:12 -05:00
(async () => {
2024-04-28 19:16:08 -05:00
if (!session.tracking || !input.current) return;
2024-04-28 18:43:12 -05:00
2024-04-28 19:16:08 -05:00
let accountInfo = await getAccountInfo(
session.player.region,
session.player.id,
);
let tankStats = await getTankStats(
session.player.region,
2024-04-28 18:43:12 -05:00
session.player.id,
);
if (accountInfo === null) {
if (login?.id === session.player.id) {
2024-04-28 19:16:08 -05:00
accountInfo = await getAccountInfo(
idToRegion(session.player.id),
session.player.id,
[],
{ access_token: login.token },
);
tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id,
{ access_token: login.token },
);
console.log(accountInfo, tankStats);
input.current.value = accountInfo.nickname;
2024-04-28 18:43:12 -05:00
} else {
setShowFurtherVerification(true);
}
}
})();
2024-05-01 17:20:08 -05:00
if (session.tracking) {
const interval = setInterval(async () => {
const tankStats = await getTankStats(
idToRegion(session.player.id),
session.player.id,
);
setTankStatsB(tankStats);
// TODO: make refresh rate configurable
}, 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>
{/* omit totally to avoid accessing undefined player */}
{showFurtherVerification && (
<AlertDialog.Root open>
<AlertDialog.Content>
<AlertDialog.Title>We need further verification</AlertDialog.Title>
<AlertDialog.Description>
2024-04-28 19:16:08 -05:00
You're trying to track a CC account which have private statistics.
Please sign in to verify the ownership of this account. Individual
tank statistics won't be available.
2024-04-28 18:43:12 -05:00
</AlertDialog.Description>
2024-04-01 18:36:14 -05:00
2024-04-28 18:43:12 -05:00
{login && (
<AlertDialog.Description mt="2" color="red">
2024-04-28 19:16:08 -05:00
<InfoCircledIcon /> You are currently signed with a different
account.
2023-10-14 12:04:11 -05:00
</AlertDialog.Description>
2024-04-28 18:43:12 -05:00
)}
2023-10-14 12:04:11 -05:00
2024-04-28 18:43:12 -05:00
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel>
<Button
variant="soft"
color="gray"
onClick={() => {
mutateSession((draft) => {
draft.tracking = false;
});
setShowFurtherVerification(false);
}}
>
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Link
2024-05-01 13:04:31 -05:00
href={authURL(
(session as SessionTracking).player.region,
location.href,
)}
2024-04-28 18:43:12 -05:00
>
<Button variant="solid">Sign in</Button>
</Link>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
2024-04-06 13:18:17 -05:00
)}
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 () => {
const stats = (await getTankStats(
player.region,
player.account_id,
))!;
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-01 17:23:33 -05:00
<Flex mt="2" mb="2" direction="column" align="center" gap="2">
2024-05-01 17:29:40 -05:00
<Heading size="5">Tracking {accountInfo?.nickname}</Heading>
<Text color="gray">
Since {new Date(session.player.since).toLocaleString()}
</Text>
2024-05-01 17:23:33 -05:00
<Flex gap="4" align="center" justify="center">
<Button
color="red"
variant="ghost"
onClick={() => {
mutateSession((draft) => {
draft.tracking = false;
});
}}
>
Stop
</Button>
2024-05-01 17:29:40 -05:00
<Button
color="red"
variant="ghost"
onClick={() => {
// TODO: add functionality
}}
>
Reset
</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 20:54:27 -05:00
{delta.map((entry) => {
const tank = awaitedTankDefinitions[entry.tank_id];
const average = awaitedTankAvearges[tank.id];
const stats = prettifyStats(
generateStats(entry.all, average?.all),
);
2024-04-28 18:43:12 -05:00
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">
{stats[column]}
</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
}