Files
blitzkit/packages/website/src/pages/[...locale]/tools/session/_index.tsx
T

488 lines
15 KiB
TypeScript
Raw Normal View History

2024-09-07 21:45:00 -05:00
import {
2024-11-12 15:12:34 -06:00
compositeStats,
compositeStatsKeys,
2024-09-08 10:17:40 -05:00
deltaTankStats,
2024-11-12 15:12:34 -06:00
formatCompositeStat,
2024-09-08 10:15:42 -05:00
getAccountInfo,
2024-09-08 10:17:40 -05:00
getTankStats,
idToRegion,
searchPlayersAcrossRegions,
2024-11-12 15:12:34 -06:00
sumCompositeStats,
2024-10-07 17:39:20 -05:00
type AccountListWithServer,
2024-11-12 15:12:34 -06:00
type CompositeStatsKey,
2024-10-07 17:39:20 -05:00
type IndividualAccountInfo,
type IndividualTankStats,
2024-09-08 10:17:40 -05:00
} from '@blitzkit/core';
2025-01-15 20:14:05 -06:00
import { literals } from '@blitzkit/i18n/src/literals';
2024-05-03 09:02:06 -05:00
import {
ArrowDownIcon,
MagnifyingGlassIcon,
PlusIcon,
} from '@radix-ui/react-icons';
2023-10-01 16:37:51 -05:00
import {
AlertDialog,
Button,
2024-05-03 09:02:06 -05:00
DropdownMenu,
2023-10-01 16:37:51 -05:00
Flex,
2024-05-01 17:20:08 -05:00
Heading,
2024-05-03 09:02:06 -05:00
IconButton,
Select,
2024-05-01 17:20:08 -05:00
Table,
2023-10-01 16:37:51 -05:00
Text,
TextField,
} from '@radix-ui/themes';
2024-09-14 19:43:14 -05:00
import { debounce } from 'lodash-es';
2024-10-07 17:39:20 -05:00
import { useEffect, useMemo, useRef, useState } from 'react';
2024-12-27 12:38:38 -06:00
import { PageWrapper } from '../../../../components/PageWrapper';
import { StickyRowHeaderCell } from '../../../../components/StickyRowHeaderCell';
import { StickyTableRoot } from '../../../../components/StickyTableRoot';
import { TankRowHeaderCell } from '../../../../components/TankRowHeaderCell';
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
2025-01-14 20:32:17 -06:00
import {
LocaleProvider,
useLocale,
type LocaleAcceptorProps,
} from '../../../../hooks/useLocale';
2024-12-27 12:38:38 -06:00
import { Session, type SessionTracking } from '../../../../stores/session';
2024-10-05 23:20:04 -05:00
2024-11-29 13:20:27 -06:00
const [tankDefinitions, averageDefinitions] = await Promise.all([
awaitableTankDefinitions,
awaitableAverageDefinitions,
]);
2023-09-18 22:32:21 +00:00
2025-01-14 20:32:17 -06:00
export function Page({ locale }: LocaleAcceptorProps) {
2024-10-07 17:39:20 -05:00
return (
2025-01-14 20:32:17 -06:00
<LocaleProvider locale={locale}>
<Session.Provider>
<Content />
</Session.Provider>
</LocaleProvider>
2024-10-07 17:39:20 -05:00
);
}
function Content() {
2025-01-14 20:32:17 -06:00
const { locale, strings } = useLocale();
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);
2024-07-09 19:42:57 -05:00
const session = Session.use();
const mutateSession = Session.useMutation();
2024-05-01 17:20:08 -05:00
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
null,
);
2024-10-21 10:49:06 -05:00
const [accountInfo, setAccountInfo] = useState<
IndividualAccountInfo | null | undefined
>(undefined);
2024-10-07 17:39:20 -05:00
2024-10-21 10:49:06 -05:00
useEffect(() => {
(async () => {
const id = session.tracking ? session.player.id : undefined;
if (!id) return setAccountInfo(null);
setAccountInfo(
session.tracking ? await getAccountInfo(idToRegion(id), id) : null,
);
})();
}, [session.tracking && session.player.id]);
2024-05-01 20:54:27 -05:00
const delta = useMemo(
() =>
session.tracking && tankStatsB
2024-05-03 09:19:29 -05:00
? deltaTankStats(session.player.stats, tankStatsB)
2024-05-01 22:11:12 -05:00
.sort((a, b) => b.last_battle_time - a.last_battle_time)
.map((entry) => {
2024-10-07 17:39:20 -05:00
const tank = tankDefinitions.tanks[entry.tank_id];
2024-11-29 13:20:27 -06:00
const average = averageDefinitions.averages[tank.id];
2024-11-12 15:12:34 -06:00
const composite = compositeStats(
{ ...entry.all, battle_life_time: entry.battle_life_time },
average?.mu,
);
2024-05-01 22:11:12 -05:00
2024-11-12 15:12:34 -06:00
return { tank, composite };
2024-05-01 22:11:12 -05:00
})
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
2024-11-12 15:12:34 -06:00
? sumCompositeStats(delta.map(({ composite }) => composite))
2024-05-01 22:11:12 -05:00
: undefined,
[delta],
);
2024-04-28 18:43:12 -05:00
const search = debounce(async () => {
if (!input.current) return;
2024-07-22 20:59:38 -05:00
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 15));
2024-04-28 18:43:12 -05:00
setSearching(false);
}, 500);
2023-09-23 21:51:00 +00:00
2024-05-23 14:05:25 -05:00
async function track(id: number) {
const region = idToRegion(id);
const player = await getAccountInfo(region, id);
const stats = await getTankStats(region, id);
if (stats === null) {
setShowCCInaccessibilityPrompt(true);
return;
}
setShowSearch(false);
mutateSession((draft) => {
if (!input.current) return;
draft.tracking = true;
2024-10-07 17:39:20 -05:00
(draft as SessionTracking).player = {
2024-05-23 14:05:25 -05:00
id,
region,
since: Date.now(),
stats,
};
input.current.value = player.nickname;
});
}
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-06-03 17:45:50 -05:00
<PageWrapper color="blue">
2024-05-02 18:27:27 -05:00
<AlertDialog.Root
open={showCCInaccessibilityPrompt}
onOpenChange={setShowCCInaccessibilityPrompt}
>
<AlertDialog.Content>
<AlertDialog.Title>
2025-01-14 20:32:17 -06:00
{strings.website.tools.session.no_cc.title}
2024-05-02 18:27:27 -05:00
</AlertDialog.Title>
<AlertDialog.Description>
2025-01-14 20:32:17 -06:00
{strings.website.tools.session.no_cc.body}
2024-05-02 18:27:27 -05:00
</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>
2025-01-14 20:32:17 -06:00
<Button variant="solid">
{strings.website.tools.session.no_cc.alright}
</Button>
2024-05-02 18:27:27 -05:00
</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">
2025-01-14 20:32:17 -06:00
<Text color="gray">
{strings.website.tools.session.search.lookup}
</Text>
2024-05-01 17:20:08 -05:00
<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}
2025-01-14 20:32:17 -06:00
placeholder={strings.website.tools.session.search.hint}
2024-05-01 17:20:08 -05:00
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>
</TextField.Root>
2024-07-22 20:59:38 -05:00
{showSearch && (
<Flex mt="2">
{searching && (
<Flex justify="center">
2025-01-14 20:32:17 -06:00
<Text color="gray">
{strings.website.tools.session.search.searching}
</Text>
2024-07-22 20:59:38 -05:00
</Flex>
)}
{!searching && searchResults.length === 0 && (
<Flex justify="center">
2025-01-14 20:32:17 -06:00
<Text color="gray">
{strings.website.tools.session.search.no_results}
</Text>
2024-07-22 20:59:38 -05:00
</Flex>
)}
{!searching && searchResults.length > 0 && (
<Flex direction="column" gap="2">
{searchResults.map((player) => (
<Button
key={player.account_id}
variant="ghost"
onClick={() => track(player.account_id)}
>
{player.nickname} (
2024-10-09 10:29:42 -05:00
{strings.common.regions.short[player.region]})
2024-07-22 20:59:38 -05:00
</Button>
))}
</Flex>
)}
</Flex>
)}
2024-05-01 17:20:08 -05:00
</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"
2024-07-09 20:53:00 -05:00
direction={{ initial: 'column', sm: 'row' }}
2024-05-02 18:27:27 -05:00
align="center"
justify="between"
gap="2"
>
<Flex mt="2" mb="2" direction="column" gap="2">
2025-01-14 20:32:17 -06:00
<Heading size="5">
{literals(strings.website.tools.session.controls.title, [
`${accountInfo?.nickname}`,
])}
</Heading>
2024-05-02 18:27:27 -05:00
<Text color="gray">
2025-01-14 20:32:17 -06:00
{literals(strings.website.tools.session.controls.subtitle, [
new Date(session.player.since).toLocaleString(locale),
])}
2024-05-02 18:27:27 -05:00
</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) => {
2024-10-07 17:39:20 -05:00
(draft as SessionTracking).player.stats = stats;
2024-05-02 18:27:27 -05:00
});
}}
>
2025-01-14 20:32:17 -06:00
{strings.website.tools.session.controls.reset}
2024-05-02 18:27:27 -05:00
</Button>
<Button
2024-05-01 17:23:33 -05:00
onClick={() => {
mutateSession((draft) => {
draft.tracking = false;
});
}}
>
2025-01-14 20:32:17 -06:00
{strings.website.tools.session.controls.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-11-12 15:15:57 -06:00
<Flex justify="center">
<StickyTableRoot variant="surface">
<Table.Header>
<Table.Row>
2025-01-14 20:32:17 -06:00
<Table.ColumnHeaderCell>
{strings.website.tools.session.table.tank}
</Table.ColumnHeaderCell>
2024-11-12 15:12:34 -06:00
2024-11-12 15:15:57 -06:00
{session.columns.map((column, index) => (
<Table.ColumnHeaderCell
align="center"
width="0"
key={column}
style={{ position: 'relative' }}
2024-05-03 09:02:06 -05:00
>
2024-11-12 15:15:57 -06:00
{index === 0 && session.columns.length < 4 && (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton
size="1"
variant="surface"
style={{
position: 'absolute',
left: 0,
top: '50%',
transform: 'translate(-100%, -50%)',
zIndex: 1,
}}
>
<PlusIcon />
</IconButton>
</DropdownMenu.Trigger>
2024-05-03 09:02:06 -05:00
2024-11-12 15:15:57 -06:00
<DropdownMenu.Content>
{compositeStatsKeys
.filter((item) => !session.columns.includes(item))
.map((key) => (
<DropdownMenu.Item
key={key}
onClick={() => {
mutateSession((draft) => {
(draft as SessionTracking).columns.unshift(
key,
);
});
}}
>
{strings.common.composite_stats[key]}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)}
2024-05-03 09:02:06 -05:00
2024-11-12 15:15:57 -06:00
<Select.Root
value={column}
onValueChange={(value) => {
mutateSession((draft) => {
if (value === 'remove') {
(draft as SessionTracking).columns.splice(index, 1);
} else {
(draft as SessionTracking).columns[index] =
value as CompositeStatsKey;
}
});
}}
>
<Select.Trigger variant="ghost" />
2024-05-03 09:02:06 -05:00
2024-11-12 15:15:57 -06:00
<Select.Content>
2025-01-14 20:32:17 -06:00
<Select.Item value="remove">
{strings.website.tools.session.table.remove}
</Select.Item>
<Select.Separator />
2024-11-12 15:15:57 -06:00
{compositeStatsKeys
.filter(
(item) =>
!session.columns.includes(item) ||
item === column,
)
.map((key) => (
<Select.Item key={key} value={key}>
{strings.common.composite_stats[key]}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</Table.ColumnHeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row
2024-05-01 22:11:12 -05:00
style={{
2024-11-12 15:15:57 -06:00
overflow: 'hidden',
2024-05-01 22:11:12 -05:00
}}
>
2024-11-12 15:18:06 -06:00
<StickyRowHeaderCell
2024-05-01 20:54:27 -05:00
style={{
2024-11-12 15:15:57 -06:00
paddingLeft: 32,
position: 'relative',
overflowY: 'hidden',
2024-05-01 20:54:27 -05:00
}}
>
2025-01-14 20:32:17 -06:00
{strings.website.tools.session.table.total}
2024-11-12 15:18:06 -06:00
</StickyRowHeaderCell>
2024-11-12 15:15:57 -06:00
{session.columns.map((column) => (
<Table.Cell align="center" key={column}>
{formatCompositeStat(total![column], column, total!)}
</Table.Cell>
))}
</Table.Row>
{delta.map(({ tank, composite }) => {
return (
<Table.Row
key={tank.id}
style={{
overflow: 'hidden',
}}
>
<TankRowHeaderCell tank={tank} />
{session.columns.map((column) => (
<Table.Cell align="center" key={column}>
{formatCompositeStat(
composite[column],
column,
composite,
)}
</Table.Cell>
))}
</Table.Row>
);
})}
</Table.Body>
</StickyTableRoot>
</Flex>
2024-05-01 17:20:08 -05:00
)}
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,
}}
>
2025-01-14 20:32:17 -06:00
<Heading color="gray">
{strings.website.tools.session.no_battles.title}
</Heading>
<Text color="gray">
{strings.website.tools.session.no_battles.hint}
</Text>
2024-05-01 17:20:08 -05:00
</Flex>
)}
2023-09-18 22:32:21 +00:00
</PageWrapper>
);
2023-09-18 21:55:40 +00:00
}