From b6204b1c2c3c4a53583e3c762c2a32e495f05b7a Mon Sep 17 00:00:00 2001 From: Mark Greene Date: Tue, 30 Dec 2025 20:45:44 +1300 Subject: [PATCH 01/12] Added ability to filter tanks by available provisions --- .../TankSearch/components/FilterControl.tsx | 169 +++++++++++++++++- .../website/src/core/blitzkit/filterTank.ts | 17 +- packages/website/src/stores/tankFilters.ts | 2 + 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 68ae1e60f..678ef60d0 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -18,8 +18,10 @@ import { type ButtonProps, type FlexProps, } from "@radix-ui/themes"; -import { isEqual, times } from "lodash-es"; +import { isEqual, times, chunk } from "lodash-es"; +import React from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; +import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { Var } from "../../../core/radix/var"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; @@ -35,6 +37,7 @@ import { ScienceIcon } from "../../ScienceIcon"; import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; +const consumableDefinitions = await awaitableConsumableDefinitions; const shellTypeIcons: Record = { [ShellType.AP]: "ap", @@ -43,6 +46,25 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; +// Get consumables +const regularConsumables = Object.values( + consumableDefinitions.consumables +).filter( + (consumable) => + !consumable.game_mode_exclusive && consumable.name.locales.en !== "" +); + +// Split consumables into chunks of 5 for columns +const consumableColumns = chunk(regularConsumables, 5); + +// Get game mode exclusive abilities +const gameModeAbilities = Object.values( + consumableDefinitions.consumables +).filter((consumable) => consumable.game_mode_exclusive); + +// Split abilities into chunks of 5 for columns +const abilityColumns = chunk(gameModeAbilities, 5); + const size: ButtonProps["size"] = { initial: "1", xs: "2" }; function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { @@ -337,6 +359,151 @@ export function FilterControl() { + {/* Consumables Filter - RIGHT AFTER NATIONS */} + + + {consumableColumns[0]?.map((consumable) => { + const selected = tankFilters.consumables?.includes(consumable.id); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + {consumableColumns[1] && ( + <> + + + + {consumableColumns[1].map((consumable) => { + const selected = tankFilters.consumables?.includes( + consumable.id + ); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + )} + + {consumableColumns[2] && ( + <> + + + + {consumableColumns[2].map((consumable) => { + const selected = tankFilters.consumables?.includes( + consumable.id + ); + + return ( + { + TankFilters.mutate((draft) => { + if (tankFilters.consumables.includes(consumable.id)) { + draft.consumables = draft.consumables.filter( + (c) => c !== consumable.id + ); + } else { + draft.consumables = [ + ...draft.consumables, + consumable.id, + ]; + } + }); + }} + > + + + ); + })} + + + )} + + index); +const consumableDefinitions = await awaitableConsumableDefinitions; export async function filterTank( filters: TankFilters, @@ -40,6 +43,18 @@ export async function filterTank( gun.shells[index].type === filters.shells[index] ) ) - ) + ) && + (filters.consumables.length === 0 || + filters.consumables.every((consumableId) => + tank.turrets.some((turret) => + turret.guns.some((gun) => + checkConsumableProvisionInclusivity( + consumableDefinitions.consumables[consumableId], + tank, + gun + ) + ) + ) + )) ); } diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 68999b562..245455edf 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -21,6 +21,7 @@ export interface TankFilters { searching: boolean; gunType: CaseType[]; shells: [ShellType | null, ShellType | null, ShellType | null]; + consumables: number[]; showTesting: boolean; showNonTesting: boolean; @@ -37,6 +38,7 @@ export const TankFilters = new Varuna({ searching: false, gunType: [], shells: [null, null, null], + consumables: [], // Added this line showTesting: true, showNonTesting: true, From 9eb6acc6244fc92747f88281cd78cead724669d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:49:06 -0600 Subject: [PATCH 02/12] Fix formatting of consumables array in tankFilters --- packages/website/src/stores/tankFilters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts index 245455edf..d758d29a5 100644 --- a/packages/website/src/stores/tankFilters.ts +++ b/packages/website/src/stores/tankFilters.ts @@ -38,7 +38,7 @@ export const TankFilters = new Varuna({ searching: false, gunType: [], shells: [null, null, null], - consumables: [], // Added this line + consumables: [], showTesting: true, showNonTesting: true, From dfa8be63b41bfc488530d42772150e33694cf84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:51:10 -0600 Subject: [PATCH 03/12] Pet peeve --- packages/website/src/core/blitzkit/filterTank.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/website/src/core/blitzkit/filterTank.ts b/packages/website/src/core/blitzkit/filterTank.ts index 6f2c6df5e..46d744c60 100644 --- a/packages/website/src/core/blitzkit/filterTank.ts +++ b/packages/website/src/core/blitzkit/filterTank.ts @@ -5,6 +5,7 @@ import { awaitableConsumableDefinitions } from "../../core/awaitables/consumable import type { TankFilters } from "../../stores/tankFilters"; const SHELLS = times(3, (index) => index); + const consumableDefinitions = await awaitableConsumableDefinitions; export async function filterTank( From b6d6fa7212c0b647f592e34bf8ce8b4ac7ce9c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:52:11 -0600 Subject: [PATCH 04/12] Remove unused React import from FilterControl.tsx --- .../src/components/TankSearch/components/FilterControl.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 678ef60d0..5739a3ee6 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -19,7 +19,6 @@ import { type FlexProps, } from "@radix-ui/themes"; import { isEqual, times, chunk } from "lodash-es"; -import React from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; import { Var } from "../../../core/radix/var"; From 06ce263668babf633a536676840eaf768cbdc5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 19:53:06 -0600 Subject: [PATCH 05/12] feat: compact nations selector --- packages/i18n/strings/en.json | 2 + .../TankSearch/components/FilterControl.tsx | 719 +++--------------- 2 files changed, 98 insertions(+), 623 deletions(-) diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 8ccbe5487..07204b42a 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -759,11 +759,13 @@ "count_singular": "1 tank", "count_plural": "{count} tanks", "sorting_by": "Sorting by {name}", + "no_tanks_found": { "body": "No tanks found.", "search_again": "Try searching again.", "clear_filters": "Try clearing filters." }, + "sort": { "meta.none": "none", "survivability.health": "health", diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 5739a3ee6..3f972ccd3 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,42 +1,28 @@ +import { asset, ShellType } from "@blitzkit/core"; import { - asset, - ShellType, - TANK_CLASSES, - TankType, - TIER_ROMAN_NUMERALS, -} from "@blitzkit/core"; -import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons"; + LockClosedIcon, + LockOpen2Icon, + TrashIcon, +} from "@radix-ui/react-icons"; import { Box, + Button, + DropdownMenu, Flex, IconButton, Inset, - Link, Popover, Text, - Tooltip, - type ButtonProps, type FlexProps, } from "@radix-ui/themes"; -import { isEqual, times, chunk } from "lodash-es"; +import { isEqual } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; -import { awaitableConsumableDefinitions } from "../../../core/awaitables/consumableDefinitions"; -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 { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; -import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; -import { GunRegularIcon } from "../../GunRegularIcon"; import { MissingShellIcon } from "../../MissingShellIcon"; -import { ResearchedIcon } from "../../ResearchedIcon"; -import { ScienceIcon } from "../../ScienceIcon"; -import { ScienceOffIcon } from "../../ScienceOffIcon"; const gameDefinitions = await awaitableGameDefinitions; -const consumableDefinitions = await awaitableConsumableDefinitions; const shellTypeIcons: Record = { [ShellType.AP]: "ap", @@ -45,26 +31,7 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; -// Get consumables -const regularConsumables = Object.values( - consumableDefinitions.consumables -).filter( - (consumable) => - !consumable.game_mode_exclusive && consumable.name.locales.en !== "" -); - -// Split consumables into chunks of 5 for columns -const consumableColumns = chunk(regularConsumables, 5); - -// Get game mode exclusive abilities -const gameModeAbilities = Object.values( - consumableDefinitions.consumables -).filter((consumable) => consumable.game_mode_exclusive); - -// Split abilities into chunks of 5 for columns -const abilityColumns = chunk(gameModeAbilities, 5); - -const size: ButtonProps["size"] = { initial: "1", xs: "2" }; +const MAX_NATIONS = 3; function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { const shells = TankFilters.use((state) => state.shells); @@ -72,13 +39,7 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { return ( - + {shells[index] === null && ( @@ -107,7 +68,6 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { return ( state.logins.wargaming); + const nations = + tankFilters.nations.length === 0 + ? gameDefinitions.nations + : tankFilters.nations; + return ( - - - - - {times(5, (index) => { - const tier = index + 1; - const selected = tankFilters.tiers?.includes(tier); - - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } + + + + + - - {times(5, (index) => { - const tier = index + 6; - const selected = tankFilters.tiers?.includes(tier); - - return ( - { - if (tankFilters.tiers.includes(tier)) { - TankFilters.mutate((draft) => { - draft.tiers = draft.tiers.filter((t) => t !== tier); - }); - } else { - TankFilters.mutate((draft) => { - draft.tiers = [...draft.tiers, tier]; - }); - } - }} - > - {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - - - {gameDefinitions.nations - .slice(0, Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - - - {gameDefinitions.nations - .slice(Math.ceil(gameDefinitions.nations.length / 2)) - .map((nation) => { - const selected = tankFilters.nations?.includes(nation); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.nations.includes(nation)) { - draft.nations = draft.nations.filter( - (n) => n !== nation - ); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - > - - - ); - })} - - - - {/* Consumables Filter - RIGHT AFTER NATIONS */} - - - {consumableColumns[0]?.map((consumable) => { - const selected = tankFilters.consumables?.includes(consumable.id); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - {consumableColumns[1] && ( - <> - - - - {consumableColumns[1].map((consumable) => { - const selected = tankFilters.consumables?.includes( - consumable.id - ); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - )} - - {consumableColumns[2] && ( - <> - - - - {consumableColumns[2].map((consumable) => { - const selected = tankFilters.consumables?.includes( - consumable.id - ); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.consumables.includes(consumable.id)) { - draft.consumables = draft.consumables.filter( - (c) => c !== consumable.id - ); - } else { - draft.consumables = [ - ...draft.consumables, - consumable.id, - ]; - } - }); - }} - > - - - ); - })} - - - )} - - - - {TANK_CLASSES.map((tankClass) => { - const Icon = classIcons[tankClass]; - const selected = tankFilters.classes?.includes(tankClass); + + {gameDefinitions.nations.map((nation) => { + const selected = tankFilters.nations.includes(nation); return ( - { + { + event.preventDefault(); + TankFilters.mutate((draft) => { - if (tankFilters.classes.includes(tankClass)) { - draft.classes = draft.classes.filter( - (c) => c !== tankClass - ); + if (selected) { + draft.nations = draft.nations.filter((n) => n !== nation); } else { - draft.classes = [...draft.classes, tankClass]; + draft.nations = [...draft.nations, nation]; } }); }} + checked={selected} + key={nation} + style={{ + position: "relative", + }} > - - + + + + + + { + strings.common.nations[ + nation as keyof typeof strings.common.nations + ] + } + + ); })} - - - { - TankFilters.mutate((draft) => { - if (tankFilters.gunType.includes("regular")) { - draft.gunType = draft.gunType.filter((t) => t !== "regular"); - } else { - draft.gunType = [...draft.gunType, "regular"]; - } - }); - }} - > - - - { - 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"]; - } - }); - }} - > - - - { - 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"]; - } - }); - }} - > - - - + - - - - - + { + event.preventDefault(); - - { 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]; - } + draft.nations = []; }); }} > - - - { - 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]; - } - }); - }} - > - - - - - { - 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]; - } - }); - }} - > - - - - - - - - { - TankFilters.mutate((draft) => { - draft.showNonTesting = !draft.showNonTesting; - }); - }} - > - - - - { - TankFilters.mutate((draft) => { - draft.showTesting = !draft.showTesting; - }); - }} - > - - - - - {wargaming ? ( - - ) : ( - - - - )} - - - {clearable && ( - { - event.preventDefault(); - TankFilters.set(TankFilters.initial); - TankSort.set(TankSort.initial); - }} - > - {strings.website.common.tank_search.clear_filters} - - )} + + Clear + + + ); } From 4462ebe82c44b288449228bf65b89badc29e16c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:00:18 -0600 Subject: [PATCH 06/12] feat: tiers filter --- .../TankSearch/components/FilterControl.tsx | 211 +++++++----------- 1 file changed, 79 insertions(+), 132 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 3f972ccd3..883156eeb 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,26 +1,11 @@ -import { asset, ShellType } from "@blitzkit/core"; -import { - LockClosedIcon, - LockOpen2Icon, - TrashIcon, -} from "@radix-ui/react-icons"; -import { - Box, - Button, - DropdownMenu, - Flex, - IconButton, - Inset, - Popover, - Text, - type FlexProps, -} from "@radix-ui/themes"; -import { isEqual } from "lodash-es"; +import { asset, ShellType, TIER_ROMAN_NUMERALS } from "@blitzkit/core"; +import { TrashIcon } from "@radix-ui/react-icons"; +import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; +import { isEqual, times } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { App } from "../../../stores/app"; import { TankFilters } from "../../../stores/tankFilters"; -import { MissingShellIcon } from "../../MissingShellIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -32,119 +17,8 @@ const shellTypeIcons: Record = { }; const MAX_NATIONS = 3; - -function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { - const shells = TankFilters.use((state) => state.shells); - - return ( - - - - {shells[index] === null && ( - - - - )} - {shells[index] !== null && ( - - )} - - - - - - - {Object.values(ShellType).map((shellType) => { - if (typeof shellType === "string") return null; - - const selected = shells[index] === shellType; - - return ( - { - const mutated = [...shells] as TankFilters["shells"]; - - mutated[index] = selected ? null : shellType; - - TankFilters.mutate((draft) => { - draft.shells = mutated; - }); - }} - highContrast={selected} - color={selected ? undefined : "gray"} - > - - - ); - })} - - - - - ); -} - -function OwnershipFilter(props: FlexProps) { - const tankFilters = TankFilters.use(); - const wargaming = App.use((state) => state.logins.wargaming); - - return ( - - { - TankFilters.mutate((draft) => { - draft.showOwned = !draft.showOwned; - }); - }} - > - - - - { - TankFilters.mutate((draft) => { - draft.showUnowned = !draft.showUnowned; - }); - }} - > - - - - ); -} +const MAX_TIERS = 2; +const TIERS = times(10, (i) => i + 1); export function FilterControl() { const tankFilters = TankFilters.use(); @@ -152,6 +26,7 @@ export function FilterControl() { const clearable = !isEqual(tankFilters, TankFilters.initial); const wargaming = App.use((state) => state.logins.wargaming); + const tiers = tankFilters.tiers.length === 0 ? TIERS : tankFilters.tiers; const nations = tankFilters.nations.length === 0 ? gameDefinitions.nations @@ -159,6 +34,78 @@ export function FilterControl() { return ( + + + + + + + {TIERS.map((tier) => { + const selected = tankFilters.tiers.includes(tier); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.tiers = draft.tiers.filter((t) => t !== tier); + } else { + draft.tiers = [...draft.tiers, tier]; + } + }); + }} + checked={selected} + key={tier} + > + Tier {TIER_ROMAN_NUMERALS[tier]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.tiers = []; + }); + }} + > + + Clear + + + + - - - - {TIERS.map((tier) => { - const selected = tankFilters.tiers.includes(tier); - - return ( - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - if (selected) { - draft.tiers = draft.tiers.filter((t) => t !== tier); - } else { - draft.tiers = [...draft.tiers, tier]; - } - }); - }} - checked={selected} - key={tier} - > - Tier {TIER_ROMAN_NUMERALS[tier]} - - ); - })} - - - - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - draft.tiers = []; - }); - }} - > - - Clear - - - - - - - - - - - {gameDefinitions.nations.map((nation) => { - const selected = tankFilters.nations.includes(nation); - - return ( - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - if (selected) { - draft.nations = draft.nations.filter((n) => n !== nation); - } else { - draft.nations = [...draft.nations, nation]; - } - }); - }} - checked={selected} - key={nation} - style={{ - position: "relative", - }} - > - - - - - - { - strings.common.nations[ - nation as keyof typeof strings.common.nations - ] - } - - - ); - })} - - - - { - event.preventDefault(); - - TankFilters.mutate((draft) => { - draft.nations = []; - }); - }} - > - - Clear - - - + + ); } + +function TiersFilter() { + const tiersRaw = TankFilters.use((state) => state.tiers); + const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw; + + return ( + + + + + + + {TIERS.map((tier) => { + const selected = tiersRaw.includes(tier); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.tiers = draft.tiers.filter((t) => t !== tier); + } else { + draft.tiers = [...draft.tiers, tier].sort((a, b) => b - a); + } + }); + }} + checked={selected} + key={tier} + > + Tier {TIER_ROMAN_NUMERALS[tier]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.tiers = []; + }); + }} + > + + Clear + + + + ); +} + +function NationsFilter() { + const rawNations = TankFilters.use((state) => state.nations); + const nations = + rawNations.length === 0 ? gameDefinitions.nations : rawNations; + const { strings } = useLocale(); + + return ( + + + + + + + {gameDefinitions.nations.map((nation) => { + const selected = rawNations.includes(nation); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.nations = draft.nations.filter((n) => n !== nation); + } else { + draft.nations = [...draft.nations, nation]; + } + }); + }} + checked={selected} + key={nation} + style={{ + position: "relative", + }} + > + + + + + + { + strings.common.nations[ + nation as keyof typeof strings.common.nations + ] + } + + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.nations = []; + }); + }} + > + + Clear + + + + ); +} From 5b4c85138e1414ca7eab0e58453f8ec26c5c23fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:12:25 -0600 Subject: [PATCH 08/12] feat: classes --- .../TankSearch/components/FilterControl.tsx | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index a459d5dba..a05a0ef19 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,10 +1,16 @@ -import { asset, ShellType, TIER_ROMAN_NUMERALS } from "@blitzkit/core"; +import { + asset, + ShellType, + TANK_CLASSES, + TIER_ROMAN_NUMERALS, +} from "@blitzkit/core"; import { TrashIcon } from "@radix-ui/react-icons"; import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { times } from "lodash-es"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; import { TankFilters } from "../../../stores/tankFilters"; +import { classIcons } from "../../ClassIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -17,6 +23,7 @@ const shellTypeIcons: Record = { const MAX_NATIONS = 3; const MAX_TIERS = 2; + const TIERS = times(10, (i) => 10 - i); export function FilterControl() { @@ -24,6 +31,7 @@ export function FilterControl() { + ); } @@ -111,6 +119,7 @@ function NationsFilter() { {nations.slice(0, MAX_NATIONS).map((nation, index) => ( 0 ? "-0.5em" : undefined, width: "1.25em", height: "1.25em", @@ -205,3 +214,84 @@ function NationsFilter() { ); } + +function ClassFilter() { + const { strings } = useLocale(); + const rawClasses = TankFilters.use((state) => state.classes); + const classes = rawClasses.length === 0 ? TANK_CLASSES : rawClasses; + + return ( + + + + + + + {TANK_CLASSES.map((tankClass) => { + const selected = rawClasses.includes(tankClass); + const Icon = classIcons[tankClass]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.classes = draft.classes.filter( + (c) => c !== tankClass + ); + } else { + draft.classes = [...draft.classes, tankClass]; + } + }); + }} + checked={selected} + key={tankClass} + > + + + {strings.common.tank_class_medium[tankClass]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.classes = []; + }); + }} + > + + Clear + + + + ); +} From 41228560245262f61b716740e6f2513f22561d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:13:37 -0600 Subject: [PATCH 09/12] fix: reduce gap --- packages/website/src/components/TankSearch/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx index 581833c1c..4008e0366 100644 --- a/packages/website/src/components/TankSearch/index.tsx +++ b/packages/website/src/components/TankSearch/index.tsx @@ -429,7 +429,7 @@ export const TankSearch = memo( }, [tankFilters, tankopediaSort]); return ( - + Date: Tue, 30 Dec 2025 20:23:06 -0600 Subject: [PATCH 10/12] feat: gun types --- .../TankSearch/components/FilterControl.tsx | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index a05a0ef19..979cb2621 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,5 +1,6 @@ import { asset, + GunDefinition, ShellType, TANK_CLASSES, TIER_ROMAN_NUMERALS, @@ -7,10 +8,14 @@ import { import { TrashIcon } from "@radix-ui/react-icons"; import { Box, Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { times } from "lodash-es"; +import type { ComponentProps, ReactNode } from "react"; import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions"; import { useLocale } from "../../../hooks/useLocale"; -import { TankFilters } from "../../../stores/tankFilters"; +import { TankFilters, type CaseType } from "../../../stores/tankFilters"; import { classIcons } from "../../ClassIcon"; +import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; +import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; +import { GunRegularIcon } from "../../GunRegularIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -21,6 +26,19 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; +const GUN_TYPE_ICONS: Record< + CaseType, + (props: ComponentProps<"svg">) => ReactNode +> = { + regular: GunRegularIcon, + auto_loader: GunAutoloaderIcon, + auto_reloader: GunAutoreloaderIcon, +}; + +const GUN_TYPES = Object.keys( + GUN_TYPE_ICONS +) as (keyof typeof GUN_TYPE_ICONS)[]; + const MAX_NATIONS = 3; const MAX_TIERS = 2; @@ -32,6 +50,7 @@ export function FilterControl() { + ); } @@ -295,3 +314,81 @@ function ClassFilter() { ); } + +function GunTypeFilter() { + const rawGunTypes = TankFilters.use((state) => state.gunType); + const gunTypes = rawGunTypes.length === 0 ? GUN_TYPES : rawGunTypes; + const { strings } = useLocale(); + + return ( + + + + + + + {GUN_TYPES.map((gunType) => { + const selected = rawGunTypes.includes(gunType); + const Icon = GUN_TYPE_ICONS[gunType]; + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.gunType = draft.gunType.filter((c) => c !== gunType); + } else { + draft.gunType = [...draft.gunType, gunType]; + } + }); + }} + checked={selected} + key={gunType} + > + + + {strings.common.gun_types[gunType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.gunType = []; + }); + }} + > + + Clear + + + + ); +} From 71e40a0113e2b16df8b93dc64612c89e76646691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:30:18 -0600 Subject: [PATCH 11/12] feat: tank types --- .../TankSearch/components/FilterControl.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 979cb2621..38d64521e 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -3,6 +3,8 @@ import { GunDefinition, ShellType, TANK_CLASSES, + TANK_TYPES, + TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; import { TrashIcon } from "@radix-ui/react-icons"; @@ -16,6 +18,7 @@ import { classIcons } from "../../ClassIcon"; import { GunAutoloaderIcon } from "../../GunAutoloaderIcon"; import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon"; import { GunRegularIcon } from "../../GunRegularIcon"; +import { ResearchedIcon } from "../../ResearchedIcon"; const gameDefinitions = await awaitableGameDefinitions; @@ -35,6 +38,12 @@ const GUN_TYPE_ICONS: Record< auto_reloader: GunAutoreloaderIcon, }; +const TANK_TYPE_COLORS: Record = { + [TankType.RESEARCHABLE]: "gray", + [TankType.PREMIUM]: "amber", + [TankType.COLLECTOR]: "blue", +}; + const GUN_TYPES = Object.keys( GUN_TYPE_ICONS ) as (keyof typeof GUN_TYPE_ICONS)[]; @@ -49,6 +58,7 @@ export function FilterControl() { + @@ -392,3 +402,91 @@ function GunTypeFilter() { ); } + +function TypeFilter() { + const { strings } = useLocale(); + const rawTypes = TankFilters.use((state) => state.types); + const types = rawTypes.length === 0 ? TANK_TYPES : rawTypes; + + return ( + + + + + + + {TANK_TYPES.map((tankType) => { + const selected = rawTypes.includes(tankType); + + return ( + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + if (selected) { + draft.types = draft.types.filter((c) => c !== tankType); + } else { + draft.types = [...draft.types, tankType]; + } + }); + }} + checked={selected} + key={tankType} + > + + + {strings.common.tree_type[tankType]} + + ); + })} + + + + { + event.preventDefault(); + + TankFilters.mutate((draft) => { + draft.types = []; + }); + }} + > + + Clear + + + + ); +} From 6b2958442440fa837ba59de4e7467aeaf2982c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:38:32 -0600 Subject: [PATCH 12/12] feat: minor adjusments --- .../TankSearch/components/FilterControl.tsx | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx index 38d64521e..bd3c86e5e 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -48,14 +48,13 @@ const GUN_TYPES = Object.keys( GUN_TYPE_ICONS ) as (keyof typeof GUN_TYPE_ICONS)[]; -const MAX_NATIONS = 3; -const MAX_TIERS = 2; +const MAX_TIERS = 4; const TIERS = times(10, (i) => 10 - i); export function FilterControl() { return ( - + @@ -73,7 +72,6 @@ function TiersFilter() { @@ -253,7 +244,6 @@ function ClassFilter() {