feat: filter owned tanks

This commit is contained in:
TrèsAbhi committed 2025-10-04 21:35:12 -05:00
1 parent 66cf590f1c
commit 9412be2d7c
10 files changed
+435 -354

No files matched your search

+1
View File
@@ -750,6 +750,7 @@
"select_dialog_title": "Search for tanks", "select_dialog_title": "Search for tanks",
"swap_dialog_title": "Swap tank", "swap_dialog_title": "Swap tank",
"clear_filters": "Clear filters", "clear_filters": "Clear filters",
"login": "Login to use this",
"search_bar_hint": "Search tanks...", "search_bar_hint": "Search tanks...",
"recent": "Recently viewed", "recent": "Recently viewed",
"select_all": "Select all", "select_all": "Select all",
+3 -2
View File
@@ -11,6 +11,7 @@ import { getStrings } from "../core/i18n/getStrings";
import { Var } from "../core/radix/var"; import { Var } from "../core/radix/var";
import { LinkI18n } from "./LinkI18n"; import { LinkI18n } from "./LinkI18n";
import { tools } from "../constants/tools"; import { tools } from "../constants/tools";
import { NavbarAccount } from "./NavbarAccount";
export interface Props { export interface Props {
integrated?: boolean; integrated?: boolean;
@@ -182,7 +183,7 @@ const strings = getStrings(locale);
<GearIcon width="1em" height="1em" /> <GearIcon width="1em" height="1em" />
</LinkI18n> </LinkI18n>
<!-- <NavbarAccount client:idle /> --> <NavbarAccount {locale} client:idle />
<Flex align="center" display={{ initial: "flex", sm: "none" }}> <Flex align="center" display={{ initial: "flex", sm: "none" }}>
<IconButton variant="ghost" color="gray" className="hamburger"> <IconButton variant="ghost" color="gray" className="hamburger">
@@ -200,7 +201,7 @@ const strings = getStrings(locale);
display={{ initial: "flex", sm: "none" }} display={{ initial: "flex", sm: "none" }}
> >
{ {
toolsFiltered.map(( tool) => { toolsFiltered.map((tool) => {
const toolStrings = const toolStrings =
strings.website.tools[ strings.website.tools[
(tool.strings ?? tool.id) as keyof typeof strings.website.tools (tool.strings ?? tool.id) as keyof typeof strings.website.tools
@@ -8,13 +8,25 @@ import {
Text, Text,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { Suspense } from "react"; import { Suspense } from "react";
import {
LocaleProvider,
type LocaleAcceptorProps,
} from "../../hooks/useLocale";
import { App } from "../../stores/app"; import { App } from "../../stores/app";
import { BlitzKitTheme } from "../BlitzKitTheme"; import { BlitzKitTheme } from "../BlitzKitTheme";
import { WargamingAccountName } from "../WargamingAccountName"; import { WargamingAccountName } from "../WargamingAccountName";
import { WargamingIcon } from "../WargamingIcon"; import { WargamingIcon } from "../WargamingIcon";
import { WargamingLoginButton } from "../WargamingLoginButton"; 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); const logins = App.use((state) => state.logins);
return ( return (
@@ -5,6 +5,11 @@ import {
TankType, TankType,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
} from "@blitzkit/core"; } from "@blitzkit/core";
import {
ComponentBooleanIcon,
LockClosedIcon,
LockOpen2Icon,
} from "@radix-ui/react-icons";
import { import {
Box, Box,
Flex, Flex,
@@ -13,15 +18,17 @@ import {
Link, Link,
Popover, Popover,
Text, Text,
Tooltip,
type FlexProps,
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { isEqual, times } from "lodash-es"; import { isEqual, times } from "lodash-es";
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
import { Var } from "../../../core/radix/var"; import { Var } from "../../../core/radix/var";
import { useLocale } from "../../../hooks/useLocale"; import { useLocale } from "../../../hooks/useLocale";
import { App } from "../../../stores/app";
import { TankFilters } from "../../../stores/tankFilters"; import { TankFilters } from "../../../stores/tankFilters";
import { TankSort } from "../../../stores/tankopediaSort"; import { TankSort } from "../../../stores/tankopediaSort";
import { classIcons } from "../../ClassIcon"; import { classIcons } from "../../ClassIcon";
import { ExperimentIcon } from "../../ExperimentIcon";
import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; import { GunAutoloaderIcon } from "../../GunAutoloaderIcon";
import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon";
import { GunRegularIcon } from "../../GunRegularIcon"; 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 tankFilters = TankFilters.use();
const { strings } = useLocale(); const { strings } = useLocale();
const clearable = !isEqual(tankFilters, TankFilters.initial); const clearable = !isEqual(tankFilters, TankFilters.initial);
const wargaming = App.use((state) => state.logins.wargaming);
return ( return (
<Flex direction="column" align="center"> <Flex direction="column" align="center">
<Flex <Flex height="fit-content" gap="1" align="start" justify="center">
height="fit-content"
gap="2"
align={{ initial: "start", md: "center" }}
justify="center"
>
<Flex <Flex
flexShrink="0" flexShrink="0"
direction={compact ? "row" : { initial: "row", md: "column" }}
overflow="hidden" overflow="hidden"
style={{ borderRadius: "var(--radius-5)" }} style={{ borderRadius: "var(--radius-4)" }}
> >
<Flex <Flex direction="column">
direction={compact ? "column" : { md: "row", initial: "column" }}
>
{times(5, (index) => { {times(5, (index) => {
const tier = index + 1; const tier = index + 1;
const selected = tankFilters.tiers?.includes(tier); const selected = tankFilters.tiers?.includes(tier);
@@ -167,9 +226,7 @@ export function FilterControl({ compact }: FilterControlProps) {
}} }}
/> />
<Flex <Flex direction="column">
direction={compact ? "column" : { md: "row", initial: "column" }}
>
{times(5, (index) => { {times(5, (index) => {
const tier = index + 6; const tier = index + 6;
const selected = tankFilters.tiers?.includes(tier); const selected = tankFilters.tiers?.includes(tier);
@@ -202,13 +259,10 @@ export function FilterControl({ compact }: FilterControlProps) {
<Flex <Flex
flexShrink="0" flexShrink="0"
direction={compact ? "row" : { initial: "row", md: "column" }}
overflow="hidden" overflow="hidden"
style={{ borderRadius: "var(--radius-5)" }} style={{ borderRadius: "var(--radius-4)" }}
> >
<Flex <Flex direction="column">
direction={compact ? "column" : { md: "row", initial: "column" }}
>
{gameDefinitions.nations {gameDefinitions.nations
.slice(0, Math.ceil(gameDefinitions.nations.length / 2)) .slice(0, Math.ceil(gameDefinitions.nations.length / 2))
.map((nation) => { .map((nation) => {
@@ -250,9 +304,7 @@ export function FilterControl({ compact }: FilterControlProps) {
}} }}
/> />
<Flex <Flex direction="column">
direction={compact ? "column" : { md: "row", initial: "column" }}
>
{gameDefinitions.nations {gameDefinitions.nations
.slice(Math.ceil(gameDefinitions.nations.length / 2)) .slice(Math.ceil(gameDefinitions.nations.length / 2))
.map((nation) => { .map((nation) => {
@@ -289,298 +341,274 @@ export function FilterControl({ compact }: FilterControlProps) {
</Flex> </Flex>
<Flex <Flex
direction={compact ? "row" : { initial: "row", md: "column" }} overflow="hidden"
align="start" style={{ borderRadius: "var(--radius-full)" }}
gap={{ initial: "2", md: "1" }} direction="column"
> >
<Flex gap="2" align="start"> {TANK_CLASSES.map((tankClass) => {
<Flex const Icon = classIcons[tankClass];
overflow="hidden" const selected = tankFilters.classes?.includes(tankClass);
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);
return ( 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" }}
>
<IconButton <IconButton
variant={ key={tankClass}
tankFilters.gunType.includes("regular") ? "solid" : "soft" variant={selected ? "solid" : "soft"}
}
radius="none" radius="none"
color={ color={selected ? undefined : "gray"}
tankFilters.gunType.includes("regular") ? undefined : "gray"
}
highContrast highContrast
onClick={() => { onClick={() => {
TankFilters.mutate((draft) => { TankFilters.mutate((draft) => {
if (tankFilters.gunType.includes("regular")) { if (tankFilters.classes.includes(tankClass)) {
draft.gunType = draft.gunType.filter( draft.classes = draft.classes.filter(
(t) => t !== "regular" (c) => c !== tankClass
); );
} else { } 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>
<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>
<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> </Flex>
{clearable && ( {clearable && (
@@ -11,13 +11,15 @@ import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
import fuzzysort from "fuzzysort"; import fuzzysort from "fuzzysort";
import { times, uniq } from "lodash-es"; import { times, uniq } from "lodash-es";
import { memo, useEffect, useMemo, useState } from "react"; import { memo, useEffect, useMemo, useState } from "react";
import usePromise from "react-promise-suspense";
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions"; import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions"; import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
import { awaitableTankNames } from "../../core/awaitables/tankNames"; 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 { resolveReload } from "../../core/blitzkit/resolveReload";
import { useLocale } from "../../hooks/useLocale"; import { useLocale } from "../../hooks/useLocale";
import { App } from "../../stores/app";
import { TankFilters } from "../../stores/tankFilters"; import { TankFilters } from "../../stores/tankFilters";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants"; import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
@@ -54,15 +56,34 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
export const TankSearch = memo<TankSearchProps>( export const TankSearch = memo<TankSearchProps>(
({ compact, onSelect, onSelectAll, skeleton, ...props }) => { ({ compact, onSelect, onSelectAll, skeleton, ...props }) => {
const { strings, locale } = useLocale(); const { strings, locale } = useLocale();
const wargaming = App.use((state) => state.logins.wargaming);
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks); const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
const tankopediaSort = TankSort.use(); const tankopediaSort = TankSort.use();
const tankFilters = TankFilters.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) { if (tankFilters.search === null) {
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
filterTank(tankFilters, tank)
);
let sorted: TankDefinition[]; let sorted: TankDefinition[];
switch (tankopediaSort.by) { switch (tankopediaSort.by) {
@@ -396,17 +417,12 @@ export const TankSearch = memo<TankSearchProps>(
? sorted ? sorted
: sorted.reverse(); : sorted.reverse();
} else { } else {
const searchedRaw = fuzzysort.go(tankFilters.search, tankNames, { return filtered;
keys: SEARCH_KEYS,
});
const searchedTanks = searchedRaw.map(
(result) => tankDefinitions.tanks[result.obj.id]
);
return searchedTanks;
} }
}, [tankFilters, tankopediaSort]); }, [tankFilters, tankopediaSort]);
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED_CARDS); const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED_CARDS);
const tanks = tanksFiltered.slice(0, loadedCards); const tanks = sorted.slice(0, loadedCards);
useEffect(() => { useEffect(() => {
setLoadedCards(DEFAULT_LOADED_CARDS); setLoadedCards(DEFAULT_LOADED_CARDS);
@@ -420,19 +436,17 @@ export const TankSearch = memo<TankSearchProps>(
onSelect={onSelect} onSelect={onSelect}
/> />
{!tankFilters.search && !tankFilters.searching && ( {!tankFilters.search && !tankFilters.searching && <FilterControl />}
<FilterControl compact={compact} />
)}
{!skeleton && !compact && <RecentlyViewed />} {!skeleton && !compact && <RecentlyViewed />}
<Flex mt="2" gap="1" align="center" justify="center" direction="column"> <Flex mt="2" gap="1" align="center" justify="center" direction="column">
<Flex gap="2"> <Flex gap="2">
<Text color="gray"> <Text color="gray">
{tanksFiltered.length === 1 {sorted.length === 1
? strings.website.common.tank_search.count_singular ? strings.website.common.tank_search.count_singular
: literals(strings.website.common.tank_search.count_plural, { : literals(strings.website.common.tank_search.count_plural, {
count: tanksFiltered.length.toLocaleString(locale), count: sorted.length.toLocaleString(locale),
})} })}
</Text> </Text>
@@ -442,10 +456,10 @@ export const TankSearch = memo<TankSearchProps>(
href="#" href="#"
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
onSelectAll(tanksFiltered); onSelectAll(sorted);
TankopediaPersistent.mutate((draft) => { TankopediaPersistent.mutate((draft) => {
draft.recentlyViewed = uniq([ draft.recentlyViewed = uniq([
...tanksFiltered.map(({ id }) => id), ...sorted.map(({ id }) => id),
...draft.recentlyViewed, ...draft.recentlyViewed,
]) ])
.filter((id) => id in tankDefinitions.tanks) .filter((id) => id in tankDefinitions.tanks)
@@ -499,14 +513,14 @@ export const TankSearch = memo<TankSearchProps>(
))} ))}
{times( {times(
Math.min(PREVIEW_COUNT, tanksFiltered.length - loadedCards), Math.min(PREVIEW_COUNT, sorted.length - loadedCards),
(index) => { (index) => {
return ( return (
<SkeletonTankCard <SkeletonTankCard
key={index} key={index}
onIntersection={() => { onIntersection={() => {
setLoadedCards((state) => 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 type { TankDefinition } from "@blitzkit/core";
import { times } from 'lodash-es'; import { times } from "lodash-es";
import type { TankFilters } from '../../stores/tankFilters'; import type { TankFilters } from "../../stores/tankFilters";
const SHELLS = times(3, (index) => index); const SHELLS = times(3, (index) => index);
export function filterTank(filters: TankFilters, tank: TankDefinition) { export async function filterTank(
filters: TankFilters,
tank: TankDefinition,
owned: number[] = []
) {
return ( 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.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) && (filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
(filters.classes.length === 0 || filters.classes.includes(tank.class)) && (filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
(filters.types.length === 0 || filters.types.includes(tank.type)) && (filters.types.length === 0 || filters.types.includes(tank.type)) &&
(filters.testing === 'include' || (filters.testing === "include" ||
(filters.testing === 'only' && tank.testing) || (filters.testing === "only" && tank.testing) ||
(filters.testing === 'exclude' && !tank.testing)) && (filters.testing === "exclude" && !tank.testing)) &&
(filters.gunType.length === 0 || (filters.gunType.length === 0 ||
(filters.gunType.includes('regular') && (filters.gunType.includes("regular") &&
tank.turrets.some((turret) => 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) => 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) => 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) => tank.turrets.some((turret) =>
turret.guns.some((gun) => turret.guns.some((gun) =>
@@ -32,9 +39,9 @@ export function filterTank(filters: TankFilters, tank: TankDefinition) {
(index) => (index) =>
filters.shells[index] === null || filters.shells[index] === null ||
gun.shells[index] === undefined || 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;
}
-10
View File
@@ -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, []);
}
+1 -1
View File
@@ -6,7 +6,7 @@ export interface WargamingLogin {
expires: number; expires: number;
} }
interface App { export interface App {
developerMode: boolean; developerMode: boolean;
policiesAgreementIndex: number; policiesAgreementIndex: number;
logins: { logins: {
@@ -22,6 +22,7 @@ export interface TankFilters {
searching: boolean; searching: boolean;
gunType: CaseType<GunDefinition>[]; gunType: CaseType<GunDefinition>[];
shells: [ShellType | null, ShellType | null, ShellType | null]; shells: [ShellType | null, ShellType | null, ShellType | null];
ownership: "all" | "owned" | "unowned";
} }
export const TankFilters = new Varuna<TankFilters>({ export const TankFilters = new Varuna<TankFilters>({
@@ -34,4 +35,5 @@ export const TankFilters = new Varuna<TankFilters>({
searching: false, searching: false,
gunType: [], gunType: [],
shells: [null, null, null], shells: [null, null, null],
ownership: "all",
}); });