feat: playlist table

This commit is contained in:
TrèsAbhi committed 2025-10-12 19:37:44 -05:00
1 parent ed0dde37d2
commit bfceacdf3c
10 files changed
+166 -7

No files matched your search

+6 -1
View File
@@ -118,8 +118,9 @@
"packages/varuna": {
"name": "varuna",
"dependencies": {
"@types/react": "^19.2.2",
"immer": "^10.1.3",
"react": "^19.1.1",
"react": "^19.2.0",
},
},
"packages/website": {
@@ -2880,6 +2881,10 @@
"unbzip2-stream/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"varuna/@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"varuna/react": ["[email protected]", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"vscode-json-languageservice/jsonc-parser": ["[email protected]", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"which-builtin-type/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+5
View File
@@ -860,6 +860,11 @@
"tools": {
"playlist": {
"table": {
"tank": "Tank"
},
"no_login_warning": "You are not logged in. BlitzKit will not keep track of your played tanks.",
"generate": "Generate playlist",
"name": "Playlist",
"description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
+2 -1
View File
@@ -3,7 +3,8 @@
"type": "module",
"main": "src/index.ts",
"dependencies": {
"@types/react": "^19.2.2",
"immer": "^10.1.3",
"react": "^19.1.1"
"react": "^19.2.0"
}
}
@@ -7,12 +7,13 @@ import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { filterTanks } from "../core/blitzkit/filterTanks";
import { useLocale } from "../hooks/useLocale";
import { TankFilters } from "../stores/tankFilters";
import { LinkI18n } from "./LinkI18n";
import { FilterControl } from "./TankSearch/components/FilterControl";
const tankDefinitions = await awaitableTankDefinitions;
export function PlaylistPool() {
const { strings } = useLocale();
const { strings, locale } = useLocale();
const filters = TankFilters.use();
const randomTankList = useMemo(() => {
const list = Object.values(tankDefinitions.tanks);
@@ -52,7 +53,10 @@ export function PlaylistPool() {
width="100%"
>
{tanksTrimmed.map((tank) => (
<Box
<LinkI18n
target="_blank"
locale={locale}
href={`/tanks/${tank.slug}`}
key={tank.id}
style={{
aspectRatio: "4 / 3",
@@ -72,8 +76,8 @@ export function PlaylistPool() {
width="100%"
height="100%"
style={{
background:
"linear-gradient(transparent, var(--gray-1) calc(100% - 2rem))",
background: "linear-gradient(transparent, var(--gray-1))",
pointerEvents: "none",
}}
/>
</Box>
@@ -0,0 +1,34 @@
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { Button, Flex, Text } from "@radix-ui/themes";
import { generatePlaylist } from "../core/blitzkit/generatePlaylist";
import { useLocale } from "../hooks/useLocale";
import { App } from "../stores/app";
export function PlaylistGenerator() {
const { strings } = useLocale();
const wargaming = App.use((state) => state.logins.wargaming);
return (
<Flex
flexGrow="1"
flexBasis="1"
direction="column"
align="center"
justify="center"
gap="4"
>
<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>
);
}
@@ -0,0 +1,38 @@
import { Checkbox, Flex, Table } from "@radix-ui/themes";
import { useLocale } from "../hooks/useLocale";
import { Playlist } from "../stores/playlist";
import { TankRowHeaderCell } from "./TankRowHeaderCell";
export function PlaylistOrder() {
const { unwrap, strings } = useLocale();
const list = Playlist.use((state) => state.list);
if (!list) return null;
return (
<Flex justify="center" flexGrow="1" flexBasis="0">
<Table.Root variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell />
<Table.ColumnHeaderCell>
{strings.website.tools.playlist.table.tank}
</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{list.map(({ tank }) => (
<Table.Row>
<Table.Cell>
<Checkbox variant="classic" checked={Math.random() > 0.5} />
</Table.Cell>
<TankRowHeaderCell tank={tank} />
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Flex>
);
}
@@ -0,0 +1,40 @@
import {
getTankStats,
idToRegion,
type IndividualTankStats,
} from "@blitzkit/core";
import { App } from "../../stores/app";
import { Playlist } from "../../stores/playlist";
import { TankFilters } from "../../stores/tankFilters";
import { awaitableTankDefinitions } from "../awaitables/tankDefinitions";
import { filterTanks } from "./filterTanks";
const tankDefinitions = await awaitableTankDefinitions;
const tanks = Object.values(tankDefinitions.tanks);
export async function generatePlaylist() {
const id = App.state.logins.wargaming?.id;
const filtered = await filterTanks(TankFilters.state, tanks);
const statsList = id ? await getTankStats(idToRegion(id), id) : undefined;
const stats: Record<number, IndividualTankStats> = {};
if (statsList) {
for (const tankStat of statsList) {
stats[tankStat.tank_id] = tankStat;
}
}
for (let i = filtered.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[filtered[i], filtered[j]] = [filtered[j], filtered[i]];
}
Playlist.mutate((draft) => {
draft.list = filtered.map((tank, index) => ({
index,
tank,
battles: stats[tank.id]?.all.battles,
wins: stats[tank.id]?.all.wins,
}));
});
}
Whitespace-only changes.
@@ -1,6 +1,12 @@
import { useEffect } from "react";
import { PageWrapper } from "../../../components/PageWrapper";
import { PlaylistPool } from "../../../components/PlaylistFilters";
import { PlaylistGenerator } from "../../../components/PlaylistGenerator";
import { PlaylistOrder } from "../../../components/PLaylistOrder";
import { LocaleProvider } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { Playlist } from "../../../stores/playlist";
import { TankFilters } from "../../../stores/tankFilters";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
interface Props {
@@ -11,10 +17,23 @@ export function Page({
locale,
skeleton,
}: Props & MaybeSkeletonComponentProps) {
const list = Playlist.use((state) => state.list);
const wargaming = App.use((state) => state.logins.wargaming);
useEffect(() => {
if (wargaming) {
TankFilters.mutate((draft) => {
draft.ownership = "owned";
});
}
}, []);
return (
<LocaleProvider locale={locale}>
<PageWrapper color="tomato" direction="row">
<PageWrapper color="tomato" direction="row" maxWidth="100rem">
<PlaylistPool />
{!list && <PlaylistGenerator />}
{list && <PlaylistOrder />}
</PageWrapper>
</LocaleProvider>
);
+13
View File
@@ -0,0 +1,13 @@
import type { TankDefinition } from "@blitzkit/core";
import { Varuna } from "varuna";
export interface Playlist {
list?: {
index: number;
tank: TankDefinition;
battles?: number;
wins?: number;
}[];
}
export const Playlist = new Varuna<Playlist>({}, "playlist");