feat: tracking

This commit is contained in:
TrèsAbhi committed 2025-10-14 21:36:37 -05:00
1 parent 263f292a59
commit ea048da7a2
11 files changed
+272 -106

No files matched your search

@@ -1,11 +1,13 @@
import { fisherYates } from "@blitzkit/core";
import { CaretDownIcon } from "@radix-ui/react-icons";
import { Button, Flex } from "@radix-ui/themes";
import { Button, DropdownMenu, Flex } from "@radix-ui/themes";
import { useLocale } from "../hooks/useLocale";
import { App } from "../stores/app";
import { Playlist } from "../stores/playlist";
export function PlaylistActions() {
const { strings } = useLocale();
const wargaming = App.use((state) => state.logins.wargaming);
return (
<Flex gap="2">
@@ -22,7 +24,7 @@ export function PlaylistActions() {
</Button>
<Button
variant="outline"
variant={wargaming ? "outline" : undefined}
onClick={() => {
Playlist.mutate((draft) => {
if (draft.list) fisherYates(draft.list);
@@ -32,10 +34,59 @@ export function PlaylistActions() {
{strings.website.tools.playlist.shuffle}
</Button>
<Button>
{strings.website.tools.playlist.sort}
<CaretDownIcon />
</Button>
{wargaming && (
<>
<Button
variant="outline"
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.filter(
(entry) =>
!entry.now ||
!entry.then ||
entry.now.battles === entry.then.battles
);
});
}}
>
{strings.website.tools.playlist.remove}
</Button>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button>
{strings.website.tools.playlist.sort.button}
<CaretDownIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.sort(
(a, b) => (a.then?.last ?? 0) - (b.then?.last ?? 0)
);
});
}}
>
{strings.website.tools.playlist.sort.last_played}
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={() => {
Playlist.mutate((draft) => {
draft.list = draft.list?.sort(
(a, b) => (a.then?.battles ?? 0) - (b.then?.battles ?? 0)
);
});
}}
>
{strings.website.tools.playlist.sort.least_played}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</>
)}
</Flex>
);
}
@@ -33,73 +33,81 @@ export function PlaylistGenerator() {
const tanksTrimmed = useMemo(() => tanks.slice(0, 65), [filters]);
return (
<Flex justify="center" width="100%" gap="9" align="center">
<Flex direction="column" gap="4" align="center">
<FilterControl />
<Button onClick={generatePlaylist} size="3">
{strings.website.tools.playlist.generate}
</Button>
{!wargaming && (
<Text color="gray">
<Flex align="center" gap="1">
<ExclamationTriangleIcon />
{strings.website.tools.playlist.no_login_warning}
</Flex>
</Text>
)}
</Flex>
<Flex justify="center" width="100%" align="center">
<Flex
direction="column"
flexGrow="1"
flexBasis="0"
gap="9"
align="center"
gap="4"
justify="center"
maxWidth="80rem"
>
<Grid
columns="repeat(auto-fill, minmax(4rem, 1fr))"
flow="dense"
overflow="hidden"
position="relative"
maxHeight={POOL_HEIGHT}
width="100%"
<Flex direction="column" gap="4" align="center">
<FilterControl />
<Button onClick={generatePlaylist} size="3">
{strings.website.tools.playlist.generate}
</Button>
{!wargaming && (
<Text color="gray">
<Flex align="center" gap="1">
<ExclamationTriangleIcon />
{strings.website.tools.playlist.no_login_warning}
</Flex>
</Text>
)}
</Flex>
<Flex
direction="column"
flexGrow="1"
flexBasis="0"
align="center"
gap="4"
>
{tanksTrimmed.map((tank) => (
<LinkI18n
target="_blank"
locale={locale}
href={`/tanks/${tank.slug}`}
key={tank.id}
<Grid
columns="repeat(auto-fill, minmax(4rem, 1fr))"
flow="dense"
overflow="hidden"
position="relative"
maxHeight={POOL_HEIGHT}
width="100%"
>
{tanksTrimmed.map((tank) => (
<LinkI18n
target="_blank"
locale={locale}
href={`/tanks/${tank.slug}`}
key={tank.id}
style={{
aspectRatio: "4 / 3",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundImage: `url(${tankIcon(tank.id)})`,
backgroundRepeat: "no-repeat",
}}
/>
))}
<Box
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
style={{
aspectRatio: "4 / 3",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundImage: `url(${tankIcon(tank.id)})`,
backgroundRepeat: "no-repeat",
pointerEvents: "none",
background: `linear-gradient(transparent, var(--gray-1) ${POOL_HEIGHT})`,
}}
/>
))}
</Grid>
<Box
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
style={{
pointerEvents: "none",
background: `linear-gradient(transparent, var(--gray-1) ${POOL_HEIGHT})`,
}}
/>
</Grid>
<Text color="gray">
{literals(strings.website.tools.playlist.pool_size, {
count: tanks.length,
})}
</Text>
<Text color="gray">
{literals(strings.website.tools.playlist.pool_size, {
count: tanks.length,
})}
</Text>
</Flex>
</Flex>
</Flex>
);
@@ -2,6 +2,7 @@ import { Box, Flex } from "@radix-ui/themes";
import { CurrentlyPlaying } from "./CurrentlyPlaying";
import { PlaylistActions } from "./PlaylistActions";
import { PlaylistTable } from "./PlaylistTable";
import { PlaylistTracking } from "./PlaylistTracking";
export function PlaylistOrder() {
return (
@@ -15,6 +16,7 @@ export function PlaylistOrder() {
>
<CurrentlyPlaying />
<PlaylistActions />
<PlaylistTracking />
<Box flexGrow="1" width="100%" position="relative">
<PlaylistTable />
@@ -31,27 +31,31 @@ export function PlaylistTable() {
</Text>
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="0" />
<StickyColumnHeaderCell width="0" />
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.tank}
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.last_played}
{strings.website.tools.playlist.table.wins}
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.battles}
</StickyColumnHeaderCell>
<StickyColumnHeaderCell width="0" />
<StickyColumnHeaderCell width="0" />
<StickyColumnHeaderCell width="1">
{strings.website.tools.playlist.table.last_played}
</StickyColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{list.map((props, index) => (
<PlaylistTankEntry key={props.tank.id} index={index} {...props} />
<PlaylistTankEntry key={props.id} index={index} {...props} />
))}
</Table.Body>
</StickyTableRoot>
@@ -1,6 +1,11 @@
import { MinusCircledIcon } from "@radix-ui/react-icons";
import { Checkbox, IconButton, Table } from "@radix-ui/themes";
import {
MinusCircledIcon,
TriangleDownIcon,
TriangleUpIcon,
} from "@radix-ui/react-icons";
import { Checkbox, Flex, IconButton, Table, Text } from "@radix-ui/themes";
import { memo } from "react";
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { useLocale } from "../hooks/useLocale";
import { Playlist, type PlaylistEntry } from "../stores/playlist";
import { TankRowHeaderCell } from "./TankRowHeaderCell";
@@ -9,27 +14,19 @@ interface Props extends PlaylistEntry {
index: number;
}
const tankDefinitions = await awaitableTankDefinitions;
export const PlaylistTankEntry = memo<Props>(
({ tank, checked, last, battles, index }) => {
({ id, checked, then, now, index }) => {
const { locale, strings } = useLocale();
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
const nowDays = Date.now() / (1000 * 60 * 60 * 24);
const lastDays = last ? last / (60 * 60 * 24) : undefined;
const dateNow = Date.now();
const tank = tankDefinitions.tanks[id];
return (
<Table.Row>
<Table.Cell>{index + 1}</Table.Cell>
<TankRowHeaderCell tank={tank} />
<Table.Cell>
{lastDays
? rtf.format(Math.floor(lastDays - nowDays), "day")
: strings.website.tools.playlist.table.never}
</Table.Cell>
<Table.Cell>{battles}</Table.Cell>
<Table.Cell
onClick={() => {
Playlist.mutate((draft) => {
@@ -37,7 +34,16 @@ export const PlaylistTankEntry = memo<Props>(
});
}}
>
<Checkbox variant="classic" checked={checked} />
<Checkbox
variant="classic"
checked={
(checked === undefined &&
now &&
then &&
now.battles > then.battles) ||
checked
}
/>
</Table.Cell>
<Table.Cell>
@@ -53,13 +59,48 @@ export const PlaylistTankEntry = memo<Props>(
<MinusCircledIcon />
</IconButton>
</Table.Cell>
<TankRowHeaderCell tank={tank} />
<Table.Cell>
{now && then && now.battles !== then.battles ? (
<Flex gap="2">
<Text color="green">
<Flex align="center">
<TriangleUpIcon />
{now.wins - then.wins}
</Flex>
</Text>
<Text color="tomato">
<Flex align="center">
<TriangleDownIcon />
{now.battles - then.battles - now.wins + then.wins}
</Flex>
</Text>
</Flex>
) : (
strings.website.tools.playlist.table.unrecorded
)}
</Table.Cell>
<Table.Cell>
{now?.battles ?? strings.website.tools.playlist.table.unrecorded}
</Table.Cell>
<Table.Cell>
{now && now.last !== 0
? rtf.format(
Math.ceil((now.last - dateNow / 1000) / (60 * 60 * 24)),
"day"
)
: strings.website.tools.playlist.table.unrecorded}
</Table.Cell>
</Table.Row>
);
},
(a, b) =>
a.checked === b.checked &&
a.battles === b.battles &&
a.last === b.last &&
a.wins === b.wins &&
a.then?.battles === b.then?.battles &&
a.index === b.index
);
@@ -0,0 +1,14 @@
import { useEffect } from "react";
import { updatePlaylist } from "../core/blitzkit/updatePlaylist";
export function PlaylistTracking() {
useEffect(() => {
const interval = setInterval(updatePlaylist, 5000);
return () => {
clearInterval(interval);
};
}, []);
return null;
}