mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
Merge branch 'main' into fix-modules
This commit is contained in:
commit
501a618d5f
36 files changed
+1051
-82
No files matched your search
@@ -0,0 +1,12 @@
|
||||
# BlitzKit 1.29.0
|
||||
|
||||
## Features
|
||||
|
||||
- Added the Playlist tool
|
||||
- Don't know what to play today? Let BlitzKit do the thinking!
|
||||
- Navigate to [the tool on BlitzKit](https://blitzkit.app/playlist)
|
||||
- Make sure you login with your Wargaming account to unlock all features like live tracking and filtering by owned tanks
|
||||
- Use filters to pick tiers, nations, etc. then hit Generate playlist
|
||||
- BlitzKit will generate a checklist for you to go through as you play
|
||||
- Don't like the order of tanks? Hit Shuffle, Sort by Last played and Least played, or straight up remove tanks from the list by using the (-) button
|
||||
- If you're logged in, BlitzKit will automatically check off tanks as you play them
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "blitzkit",
|
||||
"version": "1.28.4",
|
||||
"version": "1.29.0",
|
||||
"license": "BlitzKit License",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function fisherYates<Type extends unknown[]>(array: Type) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
export * from './axis';
|
||||
export * from './formatCompact';
|
||||
export * from './isNumber';
|
||||
export * from './NaNFallback';
|
||||
export * from './normalizeAngleRad';
|
||||
export * from './normalToEuler';
|
||||
export * from './numberToRomanNumeral';
|
||||
export * from "./axis";
|
||||
export * from "./fisherYates";
|
||||
export * from "./formatCompact";
|
||||
export * from "./isNumber";
|
||||
export * from "./NaNFallback";
|
||||
export * from "./normalizeAngleRad";
|
||||
export * from "./normalToEuler";
|
||||
export * from "./numberToRomanNumeral";
|
||||
@@ -859,6 +859,43 @@
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
|
||||
"sort": {
|
||||
"button": "Sort by",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Tank",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Wins"
|
||||
},
|
||||
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Restablecer",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Ordenar por",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Tanque",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Victorias"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancelar",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Sort by",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Tank",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Victoires"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "リセット",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "並べ替え項目",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "車輌名",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "勝利"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Sort by",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Tank",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Zwycięstwa"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Restaurar",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Ordenar por",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Tanque",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Vitórias"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancelar",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Сброс",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Сортировка",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Танк",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Победы"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Отмена",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Смешать",
|
||||
"button": "Смешать и собрать",
|
||||
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "Reset",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "Сортувати за",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "Танк",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "Перемоги"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "Cancel",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
"name": "阶级",
|
||||
"description": "阶级",
|
||||
"choices": {
|
||||
"tier": "Tier {tier_1} - {tier_2}"
|
||||
"tier": "坦克等级 {tier_1} - {tier_2}"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -296,7 +296,7 @@
|
||||
},
|
||||
"filters": {
|
||||
"none": "无筛选器",
|
||||
"tier": "Tier {tier}"
|
||||
"tier": "坦克等级 {tier}"
|
||||
},
|
||||
"no_data": {
|
||||
"battles_in_period": "在这段时间里没有参与任何战斗",
|
||||
@@ -347,7 +347,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.",
|
||||
"tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.",
|
||||
"tier_1": "# {tank} 是一级坦克\n\n一级坦克一直是对所有玩家免费解锁的。",
|
||||
"already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.",
|
||||
"start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.",
|
||||
"tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.",
|
||||
@@ -762,6 +762,38 @@
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"playlist": {
|
||||
"reset": "重置",
|
||||
"shuffle": "Shuffle",
|
||||
"remove": "Remove played",
|
||||
"tracking": "BlitzKit is tracking your battles",
|
||||
"sort": {
|
||||
"button": "排序依据",
|
||||
"last_played": "Last played",
|
||||
"least_played": "Least played"
|
||||
},
|
||||
"table": {
|
||||
"index": "#",
|
||||
"never": "Never",
|
||||
"tank": "战车",
|
||||
"last_played": "Last played",
|
||||
"battles": "Career battles",
|
||||
"unrecorded": "--",
|
||||
"wins": "胜利"
|
||||
},
|
||||
"large_warning": {
|
||||
"title": "That's a lot of tanks!",
|
||||
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
|
||||
"cancel": "取消",
|
||||
"proceed": "Proceed"
|
||||
},
|
||||
"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!",
|
||||
"button": "Generate",
|
||||
"pool_size": "{count} possible tanks"
|
||||
},
|
||||
"mixer": {
|
||||
"name": "Mixer",
|
||||
"button": "Mix and match",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
@@ -0,0 +1,7 @@
|
||||
import { Flex } from "@radix-ui/themes";
|
||||
|
||||
export function CurrentlyPlaying() {
|
||||
return <Flex>
|
||||
|
||||
</Flex>;
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
const isAPI = window.location.pathname.startsWith("/api");
|
||||
const isI18nPath = locales.supported.some(({ locale }) =>
|
||||
window.location.pathname.startsWith(`/${locale}`)
|
||||
window.location.pathname.startsWith(`/${locale}/`)
|
||||
);
|
||||
const bestLocale = getBestLocale();
|
||||
const currentPath = locales.supported.find(({ locale }) =>
|
||||
window.location.pathname.startsWith(`/${locale}`)
|
||||
window.location.pathname.startsWith(`/${locale}/`)
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { fisherYates } from "@blitzkit/core";
|
||||
import { CaretDownIcon } from "@radix-ui/react-icons";
|
||||
import { Button, DropdownMenu, Flex, Spinner } 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">
|
||||
<Button
|
||||
size={{ initial: "1", xs: "2" }}
|
||||
variant="outline"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.playlist.reset}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size={{ initial: "1", xs: "2" }}
|
||||
variant={wargaming ? "outline" : undefined}
|
||||
onClick={() => {
|
||||
Playlist.mutate((draft) => {
|
||||
if (draft.list) fisherYates(draft.list);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.playlist.shuffle}
|
||||
</Button>
|
||||
|
||||
{wargaming && (
|
||||
<>
|
||||
<Button
|
||||
size={{ initial: "1", xs: "2" }}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list = draft.list?.filter(
|
||||
(entry) =>
|
||||
(!entry.now ||
|
||||
!entry.then ||
|
||||
entry.now.battles === entry.then.battles) &&
|
||||
!entry.checked
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.playlist.remove}
|
||||
</Button>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button size={{ initial: "1", xs: "2" }}>
|
||||
{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>
|
||||
|
||||
{wargaming && (
|
||||
<Flex gap="2" align="center">
|
||||
<Spinner />
|
||||
{strings.website.tools.playlist.tracking}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { fisherYates, tankIcon } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import { ChevronRightIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
AlertDialog,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
Skeleton,
|
||||
Text,
|
||||
} from "@radix-ui/themes";
|
||||
import { useMemo, useState } from "react";
|
||||
import usePromise from "react-promise-suspense";
|
||||
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||
import { filterTanks } from "../core/blitzkit/filterTanks";
|
||||
import { generatePlaylist } from "../core/blitzkit/generatePlaylist";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { App } from "../stores/app";
|
||||
import { TankFilters } from "../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
||||
import { LinkI18n } from "./LinkI18n";
|
||||
import { FilterControl } from "./TankSearch/components/FilterControl";
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
const POOL_HEIGHT = "16rem";
|
||||
|
||||
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
|
||||
const { strings, locale } = useLocale();
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
const filters = TankFilters.use();
|
||||
const randomTankList = useMemo(
|
||||
() => fisherYates(Object.values(tankDefinitions.tanks)),
|
||||
[filters]
|
||||
);
|
||||
const tanks = usePromise(
|
||||
() => filterTanks(filters, randomTankList, wargaming?.id),
|
||||
// react-promise-suspense has awful type annotations
|
||||
[filters] as any
|
||||
);
|
||||
const tanksTrimmed = useMemo(() => tanks.slice(0, 65), [filters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertDialog.Root
|
||||
open={largeWarningOpen}
|
||||
onOpenChange={setLargeWarningOpen}
|
||||
>
|
||||
<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)),
|
||||
}
|
||||
)}
|
||||
</AlertDialog.Description>
|
||||
|
||||
<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>
|
||||
|
||||
<Flex justify="center" width="100%" align="center" px="4">
|
||||
<Flex
|
||||
flexGrow="1"
|
||||
gap="9"
|
||||
align="center"
|
||||
justify="center"
|
||||
maxWidth="80rem"
|
||||
direction={{ initial: "column-reverse", md: "row" }}
|
||||
>
|
||||
<Flex direction="column" gap="4" align="center">
|
||||
<FilterControl />
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (tanks.length >= 32) {
|
||||
setLargeWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
generatePlaylist();
|
||||
}}
|
||||
size="3"
|
||||
>
|
||||
{strings.website.tools.playlist.generate} <ChevronRightIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
flexGrow="1"
|
||||
flexBasis="0"
|
||||
align="center"
|
||||
gap="4"
|
||||
width="100%"
|
||||
>
|
||||
<Grid
|
||||
columns="repeat(auto-fill, minmax(4rem, 1fr))"
|
||||
flow="dense"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
maxHeight={POOL_HEIGHT}
|
||||
width="100%"
|
||||
>
|
||||
{tanksTrimmed.map((tank) =>
|
||||
skeleton ? (
|
||||
<Box
|
||||
key={tank.id}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ aspectRatio: "4 / 3" }}
|
||||
p="1"
|
||||
>
|
||||
<Skeleton width="100%" height="100%" />
|
||||
</Box>
|
||||
) : (
|
||||
<LinkI18n
|
||||
key={tank.id}
|
||||
target="_blank"
|
||||
locale={locale}
|
||||
href={`/tanks/${tank.slug}`}
|
||||
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={{
|
||||
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>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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 (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
align="center"
|
||||
flexGrow="1"
|
||||
flexBasis="0"
|
||||
maxHeight="100%"
|
||||
>
|
||||
<CurrentlyPlaying />
|
||||
<PlaylistActions />
|
||||
<PlaylistTracking />
|
||||
|
||||
<Box flexGrow="1" width="100%" position="relative">
|
||||
<PlaylistTable />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Table, Text } from "@radix-ui/themes";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { Playlist } from "../stores/playlist";
|
||||
import { PlaylistTankEntry } from "./PlaylistTankEntry";
|
||||
import { StickyColumnHeaderCell } from "./StickyColumnHeaderCell";
|
||||
import { StickyTableRoot } from "./StickyTableRoot";
|
||||
|
||||
export function PlaylistTable() {
|
||||
const { strings } = useLocale();
|
||||
const list = Playlist.use((state) => state.list);
|
||||
|
||||
if (!list) return null;
|
||||
|
||||
return (
|
||||
<StickyTableRoot
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
variant="surface"
|
||||
>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<StickyColumnHeaderCell width="0">
|
||||
<Text color="gray">
|
||||
{strings.website.tools.playlist.table.index}
|
||||
</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.wins}
|
||||
</StickyColumnHeaderCell>
|
||||
|
||||
<StickyColumnHeaderCell width="1">
|
||||
{strings.website.tools.playlist.table.battles}
|
||||
</StickyColumnHeaderCell>
|
||||
|
||||
<StickyColumnHeaderCell width="1">
|
||||
{strings.website.tools.playlist.table.last_played}
|
||||
</StickyColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{list.map((props, index) => (
|
||||
<PlaylistTankEntry key={props.id} index={index} {...props} />
|
||||
))}
|
||||
</Table.Body>
|
||||
</StickyTableRoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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";
|
||||
|
||||
interface Props extends PlaylistEntry {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export const PlaylistTankEntry = memo<Props>(
|
||||
({ id, checked, then, now, index }) => {
|
||||
const { locale, strings } = useLocale();
|
||||
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
||||
const dateNow = Date.now();
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
<Table.Cell>{index + 1}</Table.Cell>
|
||||
|
||||
<Table.Cell
|
||||
onClick={() => {
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list![index].checked = !checked;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
size="3"
|
||||
variant="classic"
|
||||
checked={
|
||||
(checked === undefined &&
|
||||
now &&
|
||||
then &&
|
||||
now.battles > then.battles) ||
|
||||
checked
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
size="1"
|
||||
onClick={() => {
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list!.splice(index, 1);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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.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;
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import type { RowHeaderCellProps } from '@radix-ui/themes/src/components/table.js';
|
||||
import { Table } from "@radix-ui/themes";
|
||||
import type { RowHeaderCellProps } from "@radix-ui/themes/src/components/table.js";
|
||||
|
||||
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
|
||||
return (
|
||||
<Table.RowHeaderCell
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'sticky',
|
||||
position: "sticky",
|
||||
left: 0,
|
||||
backgroundColor: 'var(--color-panel)',
|
||||
boxShadow: 'var(--shadow-6)',
|
||||
backgroundColor: "var(--color-panel)",
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type TankDefinition, tankIcon, TankType } from '@blitzkit/core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { classIcons } from './ClassIcon';
|
||||
import { ExperimentIcon } from './ExperimentIcon';
|
||||
import { LinkI18n } from './LinkI18n';
|
||||
import { StickyRowHeaderCell } from './StickyRowHeaderCell';
|
||||
import { type TankDefinition, tankIcon, TankType } from "@blitzkit/core";
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { classIcons } from "./ClassIcon";
|
||||
import { ExperimentIcon } from "./ExperimentIcon";
|
||||
import { LinkI18n } from "./LinkI18n";
|
||||
import { StickyRowHeaderCell } from "./StickyRowHeaderCell";
|
||||
|
||||
interface TankRowHeaderCellProps {
|
||||
tank: TankDefinition;
|
||||
@@ -14,59 +15,48 @@ export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
|
||||
const Icon = classIcons[tank.class];
|
||||
|
||||
return (
|
||||
<StickyRowHeaderCell
|
||||
width={{ initial: '144px', sm: '240px' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<LinkI18n locale={locale} href={`/tanks/${tank.slug}`} tabIndex={-1}>
|
||||
<img
|
||||
alt={unwrap(tank.name)}
|
||||
draggable={false}
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
margin: 'calc(-1 * var(--table-cell-padding)) 0',
|
||||
height: 'calc(100% + 2 * var(--table-cell-padding))',
|
||||
aspectRatio: '4 / 3',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
</LinkI18n>
|
||||
|
||||
<StickyRowHeaderCell style={{ overflow: "hidden" }}>
|
||||
<LinkI18n
|
||||
locale={locale}
|
||||
color={
|
||||
tank.type === TankType.COLLECTOR
|
||||
? 'blue'
|
||||
? "blue"
|
||||
: tank.type === TankType.PREMIUM
|
||||
? 'amber'
|
||||
: 'gray'
|
||||
? "amber"
|
||||
: "gray"
|
||||
}
|
||||
highContrast={tank.type === TankType.RESEARCHABLE}
|
||||
underline="hover"
|
||||
wrap="nowrap"
|
||||
href={`/tanks/${tank.slug}`}
|
||||
style={{
|
||||
paddingLeft: 'var(--space-2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-1)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
size={{ initial: '1', sm: '2' }}
|
||||
style={{ height: "100%" }}
|
||||
size={{ initial: "1", sm: "2" }}
|
||||
>
|
||||
<Icon
|
||||
width="1em"
|
||||
height="1em"
|
||||
style={{ minWidth: '1em', minHeight: '1em' }}
|
||||
/>
|
||||
<Flex height="100%" align="center" gap="1">
|
||||
<Box
|
||||
draggable={false}
|
||||
height="calc(100% + 2 * var(--table-cell-padding))"
|
||||
style={{
|
||||
aspectRatio: "4 / 3",
|
||||
backgroundImage: `url(${tankIcon(tank.id)})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
|
||||
{tank.testing && (
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
)}
|
||||
<Icon
|
||||
width="1em"
|
||||
height="1em"
|
||||
style={{ minWidth: "1em", minHeight: "1em" }}
|
||||
/>
|
||||
|
||||
{unwrap(tank.name)}
|
||||
{tank.testing && (
|
||||
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||
)}
|
||||
|
||||
{unwrap(tank.name)}
|
||||
</Flex>
|
||||
</LinkI18n>
|
||||
</StickyRowHeaderCell>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Popover,
|
||||
Text,
|
||||
Tooltip,
|
||||
type ButtonProps,
|
||||
type FlexProps,
|
||||
} from "@radix-ui/themes";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
@@ -37,10 +38,6 @@ import { ResearchedIcon } from "../../ResearchedIcon";
|
||||
import { ScienceIcon } from "../../ScienceIcon";
|
||||
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
||||
|
||||
interface FilterControlProps {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const gameDefinitions = await awaitableGameDefinitions;
|
||||
|
||||
const shellTypeIcons: Record<ShellType, string> = {
|
||||
@@ -50,13 +47,21 @@ const shellTypeIcons: Record<ShellType, string> = {
|
||||
[ShellType.HEAT]: "hc",
|
||||
};
|
||||
|
||||
const size: ButtonProps["size"] = { initial: "1", xs: "2" };
|
||||
|
||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
const shells = TankFilters.use((state) => state.shells);
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant="soft"
|
||||
radius="none"
|
||||
color="gray"
|
||||
highContrast
|
||||
>
|
||||
{shells[index] === null && (
|
||||
<Text color="gray" style={{ display: "contents" }}>
|
||||
<MissingShellIcon width="1em" height="1em" />
|
||||
@@ -83,6 +88,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={shellType}
|
||||
value={`${shellType}`}
|
||||
radius="none"
|
||||
@@ -127,6 +133,7 @@ function OwnershipFilter(props: FlexProps) {
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -142,6 +149,7 @@ function OwnershipFilter(props: FlexProps) {
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size={size}
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -157,6 +165,7 @@ function OwnershipFilter(props: FlexProps) {
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size={size}
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -195,6 +204,7 @@ export function FilterControl() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={tier}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -233,6 +243,7 @@ export function FilterControl() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={tier}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -270,6 +281,7 @@ export function FilterControl() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={nation}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
color={selected ? undefined : "gray"}
|
||||
@@ -312,6 +324,7 @@ export function FilterControl() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={nation}
|
||||
style={{ flex: 1 }}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
@@ -351,6 +364,7 @@ export function FilterControl() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={size}
|
||||
key={tankClass}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
@@ -380,6 +394,7 @@ export function FilterControl() {
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.gunType.includes("regular") ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.gunType.includes("regular") ? undefined : "gray"}
|
||||
@@ -397,6 +412,7 @@ export function FilterControl() {
|
||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||
}
|
||||
@@ -420,6 +436,7 @@ export function FilterControl() {
|
||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_reloader") ? "solid" : "soft"
|
||||
}
|
||||
@@ -460,6 +477,7 @@ export function FilterControl() {
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? "solid"
|
||||
@@ -487,6 +505,7 @@ export function FilterControl() {
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.PREMIUM) ? "solid" : "soft"
|
||||
}
|
||||
@@ -519,6 +538,7 @@ export function FilterControl() {
|
||||
</Text>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR) ? "solid" : "soft"
|
||||
}
|
||||
@@ -558,6 +578,7 @@ export function FilterControl() {
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
@@ -571,6 +592,7 @@ export function FilterControl() {
|
||||
<ComponentBooleanIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||
@@ -586,6 +608,7 @@ export function FilterControl() {
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
|
||||
@@ -35,14 +35,18 @@ export const tools: Record<string, Tool> = {
|
||||
disabled: true,
|
||||
button: "bronze",
|
||||
},
|
||||
guess: {
|
||||
id: "guess",
|
||||
button: "cyan",
|
||||
playlist: {
|
||||
id: "playlist",
|
||||
button: "tomato",
|
||||
},
|
||||
mixer: {
|
||||
id: "mixer",
|
||||
button: "gray",
|
||||
},
|
||||
guess: {
|
||||
id: "guess",
|
||||
button: "cyan",
|
||||
},
|
||||
gallery: {
|
||||
id: "gallery",
|
||||
button: "gold",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
fisherYates,
|
||||
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, id);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
fisherYates(filtered);
|
||||
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list = filtered.map((tank) => {
|
||||
const tankStats = stats[tank.id];
|
||||
const then = tankStats
|
||||
? {
|
||||
battles: tankStats.all.battles,
|
||||
wins: tankStats.all.wins,
|
||||
last: tankStats.last_battle_time,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return { id: tank.id, then, now: then };
|
||||
});
|
||||
});
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
getTankStats,
|
||||
idToRegion,
|
||||
type IndividualTankStats,
|
||||
} from "@blitzkit/core";
|
||||
import { App } from "../../stores/app";
|
||||
import { Playlist } from "../../stores/playlist";
|
||||
|
||||
export async function updatePlaylist() {
|
||||
const id = App.state.logins.wargaming?.id;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Playlist.mutate((draft) => {
|
||||
draft.list?.map((item) => {
|
||||
const tankStats = stats[item.id];
|
||||
item.now = tankStats
|
||||
? {
|
||||
battles: tankStats.all.battles,
|
||||
wins: tankStats.all.wins,
|
||||
last: tankStats.last_battle_time,
|
||||
}
|
||||
: undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -16,11 +16,11 @@ export interface Props {
|
||||
noFooter?: boolean;
|
||||
noNavbar?: boolean;
|
||||
integrated?: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const servicing = import.meta.env.SERVICING;
|
||||
const { noFooter, noNavbar, integrated } = Astro.props;
|
||||
const locale = Astro.params.locale;
|
||||
const { noFooter, noNavbar, integrated, locale } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang={locale}>
|
||||
|
||||
@@ -7,10 +7,12 @@ import BaseLayout from "./BaseLayout.astro";
|
||||
|
||||
interface Props {
|
||||
tool: Tool;
|
||||
locale: string;
|
||||
noNavbar?: boolean;
|
||||
noFooter?: boolean;
|
||||
}
|
||||
|
||||
const { tool } = Astro.props;
|
||||
const { locale } = Astro.params;
|
||||
const { tool, locale, noNavbar, noFooter } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
@@ -20,7 +22,7 @@ const toolStrings =
|
||||
|
||||
<ToolOpenGraph {tool} />
|
||||
|
||||
<BaseLayout>
|
||||
<BaseLayout {locale} {noNavbar} {noFooter}>
|
||||
<Box
|
||||
style={{
|
||||
backgroundImage: `url(/assets/images/tools/${tool.id}.webp)`,
|
||||
|
||||
@@ -12,7 +12,7 @@ const { locale } = Astro.params;
|
||||
|
||||
<ToolOpenGraph tool={tools.performance} />
|
||||
|
||||
<BaseLayout noFooter>
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page client:only="react" {locale}>
|
||||
<Page skeleton slot="fallback" {locale} />
|
||||
</Page>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
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 {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
skeleton,
|
||||
}: Props & MaybeSkeletonComponentProps) {
|
||||
const generated = Playlist.use((state) => state.list !== undefined);
|
||||
|
||||
if (App.state.logins.wargaming && TankFilters.state.ownership !== "owned") {
|
||||
TankFilters.state.ownership = "owned";
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<PageWrapper color="tomato" direction="row" maxWidth="unset" p="0">
|
||||
{!generated && <PlaylistGenerator skeleton={skeleton} />}
|
||||
{generated && <PlaylistOrder />}
|
||||
</PageWrapper>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import ToolOpenGraph from "../../../components/ToolOpenGraph.astro";
|
||||
import { tools } from "../../../constants/tools";
|
||||
import BaseLayout from "../../../layouts/BaseLayout.astro";
|
||||
import { Page } from "./_index.tsx";
|
||||
|
||||
export { getStaticPaths } from "../index.astro";
|
||||
|
||||
const { locale } = Astro.params;
|
||||
---
|
||||
|
||||
<ToolOpenGraph tool={tools.playlist} />
|
||||
|
||||
<BaseLayout {locale} noFooter>
|
||||
<Page {locale} client:only="react">
|
||||
<Page {locale} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export interface PlaylistEntry {
|
||||
id: number;
|
||||
then?: { battles: number; wins: number; last: number };
|
||||
now?: { battles: number; wins: number; last: number };
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
export interface Playlist {
|
||||
list?: PlaylistEntry[];
|
||||
}
|
||||
|
||||
export const Playlist = new Varuna<Playlist>({}, "playlist");
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": ["../../tsconfig.json", "astro/tsconfigs/base"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
},
|
||||
"include": [".astro/types.d.ts"]
|
||||
"jsxImportSource": "react",
|
||||
"types": ["vite/client"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user