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",
"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",
+3 -2
View File
@@ -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;
}
-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;
}
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",
});