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 68ae1e60f..bd3c86e5e 100644 --- a/packages/website/src/components/TankSearch/components/FilterControl.tsx +++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx @@ -1,38 +1,24 @@ import { asset, + GunDefinition, ShellType, TANK_CLASSES, + TANK_TYPES, TankType, TIER_ROMAN_NUMERALS, } from "@blitzkit/core"; -import { LockClosedIcon, LockOpen2Icon } from "@radix-ui/react-icons"; -import { - Box, - Flex, - IconButton, - Inset, - Link, - Popover, - Text, - Tooltip, - type ButtonProps, - type FlexProps, -} from "@radix-ui/themes"; -import { isEqual, times } from "lodash-es"; +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 { 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 { TankFilters, type CaseType } from "../../../stores/tankFilters"; 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; @@ -43,583 +29,452 @@ const shellTypeIcons: Record = { [ShellType.HEAT]: "hc", }; -const size: ButtonProps["size"] = { initial: "1", xs: "2" }; +const GUN_TYPE_ICONS: Record< + CaseType, + (props: ComponentProps<"svg">) => ReactNode +> = { + regular: GunRegularIcon, + auto_loader: GunAutoloaderIcon, + auto_reloader: GunAutoreloaderIcon, +}; -function ShellFilter({ index, premium }: { index: number; premium?: boolean }) { - const shells = TankFilters.use((state) => state.shells); +const TANK_TYPE_COLORS: Record = { + [TankType.RESEARCHABLE]: "gray", + [TankType.PREMIUM]: "amber", + [TankType.COLLECTOR]: "blue", +}; - return ( - - - - {shells[index] === null && ( - - - - )} - {shells[index] !== null && ( - - )} - - +const GUN_TYPES = Object.keys( + GUN_TYPE_ICONS +) as (keyof typeof GUN_TYPE_ICONS)[]; - - - - {Object.values(ShellType).map((shellType) => { - if (typeof shellType === "string") return null; +const MAX_TIERS = 4; - 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 TIERS = times(10, (i) => 10 - i); 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 ( - - - - - {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]; - }); - } - }} - > - {TIER_ROMAN_NUMERALS[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]; - } - }); - }} - > - - - ); - })} - - - - - {TANK_CLASSES.map((tankClass) => { - const Icon = classIcons[tankClass]; - const selected = tankFilters.classes?.includes(tankClass); - - return ( - { - TankFilters.mutate((draft) => { - if (tankFilters.classes.includes(tankClass)) { - draft.classes = draft.classes.filter( - (c) => c !== tankClass - ); - } else { - draft.classes = [...draft.classes, tankClass]; - } - }); - }} - > - - - ); - })} - - - - { - 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"]; - } - }); - }} - > - - - - - - - - - - - - { - 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]; - } - }); - }} - > - - - { - 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} - - )} + + + + + + ); } + +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 + + + + ); +} + +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 + + + + ); +} + +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 + + + + ); +} + +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 + + + + ); +} 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 ( - + index); +const consumableDefinitions = await awaitableConsumableDefinitions; + export async function filterTank( filters: TankFilters, tank: TankDefinition, @@ -40,6 +44,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..d758d29a5 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: [], showTesting: true, showNonTesting: true,