Files
blitzkit/packages/website/src/components/PlaylistGenerator.tsx
T

174 lines
5.4 KiB
TypeScript
Raw Normal View History

2025-10-14 20:19:52 -05:00
import { fisherYates, tankIcon } from "@blitzkit/core";
2025-10-12 22:48:14 -05:00
import { literals } from "@blitzkit/i18n";
2025-10-14 22:21:22 -05:00
import { ChevronRightIcon } from "@radix-ui/react-icons";
import {
AlertDialog,
Box,
Button,
Flex,
Grid,
Skeleton,
Text,
} from "@radix-ui/themes";
import { useMemo, useState } from "react";
2025-10-12 22:48:14 -05:00
import usePromise from "react-promise-suspense";
2026-08-04 20:31:49 -05:00
import { api } from "../core/blitzkit/api";
2025-10-12 22:48:14 -05:00
import { filterTanks } from "../core/blitzkit/filterTanks";
2025-10-12 19:37:44 -05:00
import { generatePlaylist } from "../core/blitzkit/generatePlaylist";
import { useLocale } from "../hooks/useLocale";
import { App } from "../stores/app";
2025-10-12 22:48:14 -05:00
import { TankFilters } from "../stores/tankFilters";
2025-10-14 22:02:47 -05:00
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
2025-10-12 22:48:14 -05:00
import { LinkI18n } from "./LinkI18n";
2026-08-26 14:02:44 -05:00
import { TankSearchFilters } from "./TankSearchFilters";
2025-10-12 22:48:14 -05:00
2026-08-04 20:31:49 -05:00
const tankDefinitions = await api.tankDefinitions();
2025-10-12 22:48:14 -05:00
const POOL_HEIGHT = "16rem";
2025-10-12 19:37:44 -05:00
2025-10-14 22:02:47 -05:00
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
2025-10-14 22:21:22 -05:00
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
2025-10-12 22:48:14 -05:00
const { strings, locale } = useLocale();
2025-10-12 19:37:44 -05:00
const wargaming = App.use((state) => state.logins.wargaming);
2025-10-12 22:48:14 -05:00
const filters = TankFilters.use();
2025-10-14 20:19:52 -05:00
const randomTankList = useMemo(
() => fisherYates(Object.values(tankDefinitions.tanks)),
2026-08-04 20:31:49 -05:00
[filters],
2025-10-14 20:19:52 -05:00
);
2025-10-12 22:48:14 -05:00
const tanks = usePromise(
() => filterTanks(filters, randomTankList, wargaming?.id),
// react-promise-suspense has awful type annotations
2026-08-04 20:31:49 -05:00
[filters] as any,
2025-10-12 22:48:14 -05:00
);
const tanksTrimmed = useMemo(() => tanks.slice(0, 65), [filters]);
2025-10-12 19:37:44 -05:00
return (
2025-10-14 22:21:22 -05:00
<>
<AlertDialog.Root
open={largeWarningOpen}
onOpenChange={setLargeWarningOpen}
2025-10-12 22:48:14 -05:00
>
2025-10-14 22:21:22 -05:00
<AlertDialog.Content>
<AlertDialog.Title>
{strings.website.tools.playlist.large_warning.title}
</AlertDialog.Title>
<AlertDialog.Description>
{literals(
strings.website.tools.playlist.large_warning.description,
{
count: tanks.length,
time: Math.round(tanks.length * ((2 * 4) / 60)),
2026-08-04 20:31:49 -05:00
},
2025-10-14 22:02:47 -05:00
)}
2025-10-14 22:21:22 -05:00
</AlertDialog.Description>
2025-10-14 21:36:37 -05:00
2025-10-14 22:21:22 -05:00
<Flex justify="end" gap="2" mt="4">
<AlertDialog.Cancel>
<Button variant="outline">
{strings.website.tools.playlist.large_warning.cancel}
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action onClick={generatePlaylist}>
<Button color="red">
{strings.website.tools.playlist.large_warning.proceed}
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
2025-10-14 22:26:13 -05:00
<Flex justify="center" width="100%" align="center" px="4">
2025-10-14 22:21:22 -05:00
<Flex
flexGrow="1"
gap="9"
align="center"
justify="center"
maxWidth="80rem"
2025-10-14 22:26:13 -05:00
direction={{ initial: "column-reverse", md: "row" }}
2025-10-14 22:21:22 -05:00
>
<Flex direction="column" gap="4" align="center">
2026-08-26 14:02:44 -05:00
<TankSearchFilters />
2025-10-14 22:21:22 -05:00
<Button
onClick={() => {
if (tanks.length >= 32) {
setLargeWarningOpen(true);
return;
}
generatePlaylist();
2025-10-12 22:48:14 -05:00
}}
2025-10-14 22:21:22 -05:00
size="3"
>
{strings.website.tools.playlist.generate} <ChevronRightIcon />
</Button>
</Flex>
2025-10-12 22:48:14 -05:00
2025-10-14 22:21:22 -05:00
<Flex
direction="column"
flexGrow="1"
flexBasis="0"
align="center"
gap="4"
2025-10-14 22:26:13 -05:00
width="100%"
2025-10-14 22:21:22 -05:00
>
<Grid
columns="repeat(auto-fill, minmax(4rem, 1fr))"
flow="dense"
overflow="hidden"
position="relative"
maxHeight={POOL_HEIGHT}
width="100%"
>
{tanksTrimmed.map((tank) =>
skeleton ? (
<Box
2025-10-14 22:28:00 -05:00
key={tank.id}
2025-10-14 22:21:22 -05:00
width="100%"
height="100%"
style={{ aspectRatio: "4 / 3" }}
p="1"
>
<Skeleton width="100%" height="100%" />
</Box>
) : (
<LinkI18n
2025-10-14 22:28:00 -05:00
key={tank.id}
2025-10-14 22:21:22 -05:00
target="_blank"
locale={locale}
href={`/tanks/${tank.slug}`}
style={{
aspectRatio: "4 / 3",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundImage: `url(${tankIcon(tank.id)})`,
backgroundRepeat: "no-repeat",
}}
/>
2026-08-04 20:31:49 -05:00
),
2025-10-14 22:21:22 -05:00
)}
<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>
</Flex>
2025-10-14 21:36:37 -05:00
</Flex>
2025-10-12 22:48:14 -05:00
</Flex>
2025-10-14 22:21:22 -05:00
</>
2025-10-12 19:37:44 -05:00
);
}