mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: filter owned tanks
This commit is contained in:
1 parent
66cf590f1c
commit
9412be2d7c
10 files changed
+435
-354
No files matched your search
@@ -750,6 +750,7 @@
|
||||
"select_dialog_title": "Search for tanks",
|
||||
"swap_dialog_title": "Swap tank",
|
||||
"clear_filters": "Clear filters",
|
||||
"login": "Login to use this",
|
||||
"search_bar_hint": "Search tanks...",
|
||||
"recent": "Recently viewed",
|
||||
"select_all": "Select all",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getStrings } from "../core/i18n/getStrings";
|
||||
import { Var } from "../core/radix/var";
|
||||
import { LinkI18n } from "./LinkI18n";
|
||||
import { tools } from "../constants/tools";
|
||||
import { NavbarAccount } from "./NavbarAccount";
|
||||
|
||||
export interface Props {
|
||||
integrated?: boolean;
|
||||
@@ -182,7 +183,7 @@ const strings = getStrings(locale);
|
||||
<GearIcon width="1em" height="1em" />
|
||||
</LinkI18n>
|
||||
|
||||
<!-- <NavbarAccount client:idle /> -->
|
||||
<NavbarAccount {locale} client:idle />
|
||||
|
||||
<Flex align="center" display={{ initial: "flex", sm: "none" }}>
|
||||
<IconButton variant="ghost" color="gray" className="hamburger">
|
||||
@@ -200,7 +201,7 @@ const strings = getStrings(locale);
|
||||
display={{ initial: "flex", sm: "none" }}
|
||||
>
|
||||
{
|
||||
toolsFiltered.map(( tool) => {
|
||||
toolsFiltered.map((tool) => {
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.strings ?? tool.id) as keyof typeof strings.website.tools
|
||||
|
||||
@@ -8,13 +8,25 @@ import {
|
||||
Text,
|
||||
} from "@radix-ui/themes";
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../hooks/useLocale";
|
||||
import { App } from "../../stores/app";
|
||||
import { BlitzKitTheme } from "../BlitzKitTheme";
|
||||
import { WargamingAccountName } from "../WargamingAccountName";
|
||||
import { WargamingIcon } from "../WargamingIcon";
|
||||
import { WargamingLoginButton } from "../WargamingLoginButton";
|
||||
|
||||
export function NavbarAccount() {
|
||||
export function NavbarAccount({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const logins = App.use((state) => state.logins);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
TankType,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
ComponentBooleanIcon,
|
||||
LockClosedIcon,
|
||||
LockOpen2Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -13,15 +18,17 @@ import {
|
||||
Link,
|
||||
Popover,
|
||||
Text,
|
||||
Tooltip,
|
||||
type FlexProps,
|
||||
} from "@radix-ui/themes";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
|
||||
import { Var } from "../../../core/radix/var";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { App } from "../../../stores/app";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { classIcons } from "../../ClassIcon";
|
||||
import { ExperimentIcon } from "../../ExperimentIcon";
|
||||
import { GunAutoloaderIcon } from "../../GunAutoloaderIcon";
|
||||
import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon";
|
||||
import { GunRegularIcon } from "../../GunRegularIcon";
|
||||
@@ -108,28 +115,80 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterControl({ compact }: FilterControlProps) {
|
||||
function OwnershipFilter(props: FlexProps) {
|
||||
const tankFilters = TankFilters.use();
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
{...props}
|
||||
>
|
||||
<IconButton
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "all" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.ownership === "all" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.ownership = "all";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ComponentBooleanIcon />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "owned" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.ownership === "owned" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.ownership = "owned";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LockOpen2Icon />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={!wargaming}
|
||||
variant={tankFilters.ownership === "unowned" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.ownership === "unowned" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.ownership = "unowned";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LockClosedIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterControl() {
|
||||
const tankFilters = TankFilters.use();
|
||||
const { strings } = useLocale();
|
||||
const clearable = !isEqual(tankFilters, TankFilters.initial);
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center">
|
||||
<Flex
|
||||
height="fit-content"
|
||||
gap="2"
|
||||
align={{ initial: "start", md: "center" }}
|
||||
justify="center"
|
||||
>
|
||||
<Flex height="fit-content" gap="1" align="start" justify="center">
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
style={{ borderRadius: "var(--radius-4)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<Flex direction="column">
|
||||
{times(5, (index) => {
|
||||
const tier = index + 1;
|
||||
const selected = tankFilters.tiers?.includes(tier);
|
||||
@@ -167,9 +226,7 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<Flex direction="column">
|
||||
{times(5, (index) => {
|
||||
const tier = index + 6;
|
||||
const selected = tankFilters.tiers?.includes(tier);
|
||||
@@ -202,13 +259,10 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
style={{ borderRadius: "var(--radius-4)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<Flex direction="column">
|
||||
{gameDefinitions.nations
|
||||
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
||||
.map((nation) => {
|
||||
@@ -250,9 +304,7 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<Flex direction="column">
|
||||
{gameDefinitions.nations
|
||||
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
||||
.map((nation) => {
|
||||
@@ -289,298 +341,274 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
align="start"
|
||||
gap={{ initial: "2", md: "1" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
>
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{TANK_CLASSES.map((tankClass) => {
|
||||
const Icon = classIcons[tankClass];
|
||||
const selected = tankFilters.classes?.includes(tankClass);
|
||||
{TANK_CLASSES.map((tankClass) => {
|
||||
const Icon = classIcons[tankClass];
|
||||
const selected = tankFilters.classes?.includes(tankClass);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
key={tankClass}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
draft.classes = draft.classes.filter(
|
||||
(c) => c !== tankClass
|
||||
);
|
||||
} else {
|
||||
draft.classes = [...draft.classes, tankClass];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
return (
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes("regular") ? "solid" : "soft"
|
||||
}
|
||||
key={tankClass}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes("regular") ? undefined : "gray"
|
||||
}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("regular")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "regular"
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
draft.classes = draft.classes.filter(
|
||||
(c) => c !== tankClass
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "regular"];
|
||||
draft.classes = [...draft.classes, tankClass];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||
<Icon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes("auto_loader")
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_loader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_loader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_reloader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<ShellFilter index={0} />
|
||||
<ShellFilter index={1} premium />
|
||||
<ShellFilter index={2} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? "amber"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.PREMIUM
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.PREMIUM];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? undefined
|
||||
: "amber"
|
||||
}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? "blue"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.COLLECTOR
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? undefined
|
||||
: "blue"
|
||||
}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "include";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ExperimentIcon
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "exclude";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceOffIcon
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "only";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceIcon
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
variant={tankFilters.gunType.includes("regular") ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.gunType.includes("regular") ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("regular")) {
|
||||
draft.gunType = draft.gunType.filter((t) => t !== "regular");
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "regular"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes("auto_loader") ? undefined : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_loader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_loader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes("auto_reloader") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes("auto_reloader") ? undefined : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_reloader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
>
|
||||
<ShellFilter index={0} />
|
||||
<ShellFilter index={1} premium />
|
||||
<ShellFilter index={2} />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.PREMIUM) ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM) ? "amber" : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.PREMIUM
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.PREMIUM];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? undefined
|
||||
: "amber"
|
||||
}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR) ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR) ? "blue" : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.COLLECTOR
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? undefined
|
||||
: "blue"
|
||||
}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction="column"
|
||||
>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "include";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ComponentBooleanIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "exclude";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceOffIcon
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "only";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceIcon
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
{wargaming ? (
|
||||
<OwnershipFilter />
|
||||
) : (
|
||||
<Tooltip content={strings.website.common.tank_search.login}>
|
||||
<OwnershipFilter />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{clearable && (
|
||||
|
||||
@@ -11,13 +11,15 @@ import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { times, uniq } from "lodash-es";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import usePromise from "react-promise-suspense";
|
||||
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { filterTanks } from "../../core/blitzkit/filterTanks";
|
||||
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { App } from "../../stores/app";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||
@@ -54,15 +56,34 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
|
||||
export const TankSearch = memo<TankSearchProps>(
|
||||
({ compact, onSelect, onSelectAll, skeleton, ...props }) => {
|
||||
const { strings, locale } = useLocale();
|
||||
|
||||
const wargaming = App.use((state) => state.logins.wargaming);
|
||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||
const tankopediaSort = TankSort.use();
|
||||
const tankFilters = TankFilters.use();
|
||||
const tanksFiltered = useMemo(() => {
|
||||
|
||||
const filtered = usePromise(
|
||||
() => {
|
||||
if (tankFilters.search === null) {
|
||||
return filterTanks(
|
||||
tankFilters,
|
||||
awaitedTanksDefinitionsArray,
|
||||
wargaming?.id
|
||||
);
|
||||
} else {
|
||||
return Promise.resolve(
|
||||
fuzzysort
|
||||
.go(tankFilters.search, tankNames, {
|
||||
keys: SEARCH_KEYS,
|
||||
})
|
||||
.map((result) => tankDefinitions.tanks[result.obj.id])
|
||||
);
|
||||
}
|
||||
},
|
||||
// react-promise-suspense has awful type annotations
|
||||
[tankFilters] as any
|
||||
);
|
||||
const sorted = useMemo(() => {
|
||||
if (tankFilters.search === null) {
|
||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||
filterTank(tankFilters, tank)
|
||||
);
|
||||
let sorted: TankDefinition[];
|
||||
|
||||
switch (tankopediaSort.by) {
|
||||
@@ -396,17 +417,12 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
? sorted
|
||||
: sorted.reverse();
|
||||
} else {
|
||||
const searchedRaw = fuzzysort.go(tankFilters.search, tankNames, {
|
||||
keys: SEARCH_KEYS,
|
||||
});
|
||||
const searchedTanks = searchedRaw.map(
|
||||
(result) => tankDefinitions.tanks[result.obj.id]
|
||||
);
|
||||
return searchedTanks;
|
||||
return filtered;
|
||||
}
|
||||
}, [tankFilters, tankopediaSort]);
|
||||
|
||||
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED_CARDS);
|
||||
const tanks = tanksFiltered.slice(0, loadedCards);
|
||||
const tanks = sorted.slice(0, loadedCards);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadedCards(DEFAULT_LOADED_CARDS);
|
||||
@@ -420,19 +436,17 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
|
||||
{!tankFilters.search && !tankFilters.searching && (
|
||||
<FilterControl compact={compact} />
|
||||
)}
|
||||
{!tankFilters.search && !tankFilters.searching && <FilterControl />}
|
||||
|
||||
{!skeleton && !compact && <RecentlyViewed />}
|
||||
|
||||
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
|
||||
<Flex gap="2">
|
||||
<Text color="gray">
|
||||
{tanksFiltered.length === 1
|
||||
{sorted.length === 1
|
||||
? strings.website.common.tank_search.count_singular
|
||||
: literals(strings.website.common.tank_search.count_plural, {
|
||||
count: tanksFiltered.length.toLocaleString(locale),
|
||||
count: sorted.length.toLocaleString(locale),
|
||||
})}
|
||||
</Text>
|
||||
|
||||
@@ -442,10 +456,10 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onSelectAll(tanksFiltered);
|
||||
onSelectAll(sorted);
|
||||
TankopediaPersistent.mutate((draft) => {
|
||||
draft.recentlyViewed = uniq([
|
||||
...tanksFiltered.map(({ id }) => id),
|
||||
...sorted.map(({ id }) => id),
|
||||
...draft.recentlyViewed,
|
||||
])
|
||||
.filter((id) => id in tankDefinitions.tanks)
|
||||
@@ -499,14 +513,14 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
))}
|
||||
|
||||
{times(
|
||||
Math.min(PREVIEW_COUNT, tanksFiltered.length - loadedCards),
|
||||
Math.min(PREVIEW_COUNT, sorted.length - loadedCards),
|
||||
(index) => {
|
||||
return (
|
||||
<SkeletonTankCard
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedCards((state) =>
|
||||
Math.min(state + 2, tanksFiltered.length)
|
||||
Math.min(state + 2, sorted.length)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
import type { TankDefinition } from '@blitzkit/core';
|
||||
import { times } from 'lodash-es';
|
||||
import type { TankFilters } from '../../stores/tankFilters';
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { times } from "lodash-es";
|
||||
import type { TankFilters } from "../../stores/tankFilters";
|
||||
|
||||
const SHELLS = times(3, (index) => index);
|
||||
|
||||
export function filterTank(filters: TankFilters, tank: TankDefinition) {
|
||||
export async function filterTank(
|
||||
filters: TankFilters,
|
||||
tank: TankDefinition,
|
||||
owned: number[] = []
|
||||
) {
|
||||
return (
|
||||
(filters.ownership === "all" ||
|
||||
(filters.ownership === "owned" && owned.includes(tank.id)) ||
|
||||
(filters.ownership === "unowned" && !owned.includes(tank.id))) &&
|
||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
||||
(filters.testing === 'include' ||
|
||||
(filters.testing === 'only' && tank.testing) ||
|
||||
(filters.testing === 'exclude' && !tank.testing)) &&
|
||||
(filters.testing === "include" ||
|
||||
(filters.testing === "only" && tank.testing) ||
|
||||
(filters.testing === "exclude" && !tank.testing)) &&
|
||||
(filters.gunType.length === 0 ||
|
||||
(filters.gunType.includes('regular') &&
|
||||
(filters.gunType.includes("regular") &&
|
||||
tank.turrets.some((turret) =>
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === 'regular'),
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === "regular")
|
||||
)) ||
|
||||
(filters.gunType.includes('auto_loader') &&
|
||||
(filters.gunType.includes("auto_loader") &&
|
||||
tank.turrets.some((turret) =>
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === 'auto_loader'),
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === "auto_loader")
|
||||
)) ||
|
||||
(filters.gunType.includes('auto_reloader') &&
|
||||
(filters.gunType.includes("auto_reloader") &&
|
||||
tank.turrets.some((turret) =>
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === 'auto_reloader'),
|
||||
turret.guns.some((gun) => gun.gun_type!.$case === "auto_reloader")
|
||||
))) &&
|
||||
tank.turrets.some((turret) =>
|
||||
turret.guns.some((gun) =>
|
||||
@@ -32,9 +39,9 @@ export function filterTank(filters: TankFilters, tank: TankDefinition) {
|
||||
(index) =>
|
||||
filters.shells[index] === null ||
|
||||
gun.shells[index] === undefined ||
|
||||
gun.shells[index].type === filters.shells[index],
|
||||
),
|
||||
),
|
||||
gun.shells[index].type === filters.shells[index]
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getTankStats, idToRegion, TankDefinition } from "@blitzkit/core";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { filterTank } from "./filterTank";
|
||||
|
||||
export async function filterTanks(
|
||||
filters: TankFilters,
|
||||
tanks: TankDefinition[],
|
||||
player?: number
|
||||
) {
|
||||
const filtered: TankDefinition[] = [];
|
||||
let owned: number[] | undefined;
|
||||
|
||||
if (player) {
|
||||
const tankStats = await getTankStats(idToRegion(player), player, {
|
||||
fields: "tank_id",
|
||||
});
|
||||
|
||||
owned = tankStats?.map(({ tank_id }) => tank_id);
|
||||
}
|
||||
|
||||
for (const tank of tanks) {
|
||||
if (await filterTank(filters, tank, owned)) filtered.push(tank);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import usePromise from 'react-promise-suspense';
|
||||
|
||||
/**
|
||||
* A more stable alternative to `use` using `usePromise`.
|
||||
*/
|
||||
export function useAwait<Type>(promise: Promise<Type>) {
|
||||
const callback = useRef(async () => await promise);
|
||||
return usePromise(callback.current, []);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export interface WargamingLogin {
|
||||
expires: number;
|
||||
}
|
||||
|
||||
interface App {
|
||||
export interface App {
|
||||
developerMode: boolean;
|
||||
policiesAgreementIndex: number;
|
||||
logins: {
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface TankFilters {
|
||||
searching: boolean;
|
||||
gunType: CaseType<GunDefinition>[];
|
||||
shells: [ShellType | null, ShellType | null, ShellType | null];
|
||||
ownership: "all" | "owned" | "unowned";
|
||||
}
|
||||
|
||||
export const TankFilters = new Varuna<TankFilters>({
|
||||
@@ -34,4 +35,5 @@ export const TankFilters = new Varuna<TankFilters>({
|
||||
searching: false,
|
||||
gunType: [],
|
||||
shells: [null, null, null],
|
||||
ownership: "all",
|
||||
});
|
||||
Reference in new issue
Block a user