From df0dc4f118ccdf8889d4fde0c746268acadc3ee6 Mon Sep 17 00:00:00 2001
From: tresabhi <43380238+tresabhi@users.noreply.github.com>
Date: Thu, 3 Apr 2025 16:33:48 -0500
Subject: [PATCH] feat: shell filters
---
packages/i18n/strings/en.json | 1 +
.../src/components/MissingShellIcon.tsx | 22 +
.../TankSearch/components/FilterControl.tsx | 632 +++++++++++-------
.../website/src/core/blitzkit/filterTank.ts | 23 +-
packages/website/src/stores/tankFilters.ts | 9 +-
5 files changed, 437 insertions(+), 250 deletions(-)
create mode 100644 packages/website/src/components/MissingShellIcon.tsx
diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json
index 4af90c3f4..980ebccb0 100644
--- a/packages/i18n/strings/en.json
+++ b/packages/i18n/strings/en.json
@@ -756,6 +756,7 @@
},
"tank_search": {
+ "clear_filters": "Clear",
"search_bar_hint": "Search tanks...",
"recent": "Recently viewed",
"select_all": "Select all",
diff --git a/packages/website/src/components/MissingShellIcon.tsx b/packages/website/src/components/MissingShellIcon.tsx
new file mode 100644
index 000000000..686c4b492
--- /dev/null
+++ b/packages/website/src/components/MissingShellIcon.tsx
@@ -0,0 +1,22 @@
+import type { ComponentProps } from 'react';
+
+export function MissingShellIcon(props: ComponentProps<'svg'>) {
+ return (
+
+ );
+}
diff --git a/packages/website/src/components/TankSearch/components/FilterControl.tsx b/packages/website/src/components/TankSearch/components/FilterControl.tsx
index aca24fec1..cbcd4f84f 100644
--- a/packages/website/src/components/TankSearch/components/FilterControl.tsx
+++ b/packages/website/src/components/TankSearch/components/FilterControl.tsx
@@ -1,15 +1,21 @@
import {
asset,
+ ShellType,
TANK_CLASSES,
TankType,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { useStore } from '@nanostores/react';
import { ReloadIcon } from '@radix-ui/react-icons';
-import { Box, Flex, IconButton, Text } from '@radix-ui/themes';
+import { Box, Button, Flex, IconButton, Popover, Text } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
-import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
+import { useLocale } from '../../../hooks/useLocale';
+import {
+ $tankFilters,
+ initialTankFilters,
+ type TankFilters,
+} from '../../../stores/tankFilters';
import {
$tankopediaSort,
initialTankopediaSort,
@@ -19,6 +25,7 @@ import { ExperimentIcon } from '../../ExperimentIcon';
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';
@@ -29,14 +36,97 @@ interface FilterControlProps {
const gameDefinitions = await awaitableGameDefinitions;
-export function FilterControl({ compact }: FilterControlProps) {
+const shellTypeIcons: Record = {
+ [ShellType.AP]: 'ap',
+ [ShellType.APCR]: 'ap_cr',
+ [ShellType.HE]: 'he',
+ [ShellType.HEAT]: 'hc',
+};
+
+function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
const tankFilters = useStore($tankFilters);
return (
-
+
+
+
+ {tankFilters.shells[index] === null && (
+
+
+
+ )}
+ {tankFilters.shells[index] !== null && (
+
+ )}
+
+
+
+
+
+ {Object.values(ShellType).map((shellType) => {
+ if (typeof shellType === 'string') return null;
+
+ const selected = tankFilters.shells[index] === shellType;
+
+ return (
+ {
+ const mutated = [
+ tankFilters.shells[0],
+ tankFilters.shells[1],
+ tankFilters.shells[2],
+ ] as TankFilters['shells'];
+
+ mutated[index] = selected ? null : shellType;
+
+ $tankFilters.setKey('shells', mutated);
+ }}
+ highContrast={selected}
+ color={selected ? undefined : 'gray'}
+ >
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export function FilterControl({ compact }: FilterControlProps) {
+ const tankFilters = useStore($tankFilters);
+ const { strings } = useLocale();
+
+ return (
+
@@ -101,6 +191,7 @@ export function FilterControl({ compact }: FilterControlProps) {
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
overflow="hidden"
style={{ borderRadius: 'var(--radius-5)' }}
+ gap={{ initial: '0', md: '1' }}
>
{gameDefinitions.nations
@@ -176,270 +267,315 @@ export function FilterControl({ compact }: FilterControlProps) {
- {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 (
+ return (
+ {
+ if (tankFilters.classes.includes(tankClass)) {
+ $tankFilters.setKey(
+ 'classes',
+ tankFilters.classes.filter((c) => c !== tankClass),
+ );
+ } else {
+ $tankFilters.setKey('classes', [
+ ...tankFilters.classes,
+ tankClass,
+ ]);
+ }
+ }}
+ >
+
+
+ );
+ })}
+
+
+
{
- if (tankFilters.classes.includes(tankClass)) {
+ if (tankFilters.gunType.includes('regular')) {
$tankFilters.setKey(
- 'classes',
- tankFilters.classes.filter((c) => c !== tankClass),
+ 'gunType',
+ tankFilters.gunType.filter((t) => t !== 'regular'),
);
} else {
- $tankFilters.setKey('classes', [
- ...tankFilters.classes,
- tankClass,
+ $tankFilters.setKey('gunType', [
+ ...tankFilters.gunType,
+ 'regular',
]);
}
}}
>
-
+
- );
- })}
-
+ {
+ if (tankFilters.gunType.includes('auto_loader')) {
+ $tankFilters.setKey(
+ 'gunType',
+ tankFilters.gunType.filter((t) => t !== 'auto_loader'),
+ );
+ } else {
+ $tankFilters.setKey('gunType', [
+ ...tankFilters.gunType,
+ 'auto_loader',
+ ]);
+ }
+ }}
+ >
+
+
+ {
+ if (tankFilters.gunType.includes('auto_reloader')) {
+ $tankFilters.setKey(
+ 'gunType',
+ tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
+ );
+ } else {
+ $tankFilters.setKey('gunType', [
+ ...tankFilters.gunType,
+ 'auto_reloader',
+ ]);
+ }
+ }}
+ >
+
+
+
-
- {
- if (tankFilters.gunType.includes('regular')) {
- $tankFilters.setKey(
- 'gunType',
- tankFilters.gunType.filter((t) => t !== 'regular'),
- );
- } else {
- $tankFilters.setKey('gunType', [
- ...tankFilters.gunType,
- 'regular',
- ]);
- }
- }}
- >
-
-
- {
- if (tankFilters.gunType.includes('auto_loader')) {
- $tankFilters.setKey(
- 'gunType',
- tankFilters.gunType.filter((t) => t !== 'auto_loader'),
- );
- } else {
- $tankFilters.setKey('gunType', [
- ...tankFilters.gunType,
- 'auto_loader',
- ]);
- }
- }}
- >
-
-
- {
- if (tankFilters.gunType.includes('auto_reloader')) {
- $tankFilters.setKey(
- 'gunType',
- tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
- );
- } else {
- $tankFilters.setKey('gunType', [
- ...tankFilters.gunType,
- 'auto_reloader',
- ]);
- }
- }}
- >
-
-
-
+
+
+
+
-
- {
- if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
- $tankFilters.setKey(
- 'types',
- tankFilters.types.filter((t) => t !== TankType.RESEARCHABLE),
- );
- } else {
- $tankFilters.setKey('types', [
- ...tankFilters.types,
- TankType.RESEARCHABLE,
- ]);
- }
- }}
- >
-
-
- {
- if (tankFilters.types.includes(TankType.PREMIUM)) {
- $tankFilters.setKey(
- 'types',
- tankFilters.types.filter((t) => t !== TankType.PREMIUM),
- );
- } else {
- $tankFilters.setKey('types', [
- ...tankFilters.types,
- TankType.PREMIUM,
- ]);
- }
- }}
- >
-
+
-
-
-
- {
- if (tankFilters.types.includes(TankType.COLLECTOR)) {
- $tankFilters.setKey(
- 'types',
- tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
- );
- } else {
- $tankFilters.setKey('types', [
- ...tankFilters.types,
- TankType.COLLECTOR,
- ]);
- }
- }}
- >
- {
+ if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
+ $tankFilters.setKey(
+ 'types',
+ tankFilters.types.filter(
+ (t) => t !== TankType.RESEARCHABLE,
+ ),
+ );
+ } else {
+ $tankFilters.setKey('types', [
+ ...tankFilters.types,
+ TankType.RESEARCHABLE,
+ ]);
+ }
+ }}
+ >
+
+
+ {
+ if (tankFilters.types.includes(TankType.PREMIUM)) {
+ $tankFilters.setKey(
+ 'types',
+ tankFilters.types.filter((t) => t !== TankType.PREMIUM),
+ );
+ } else {
+ $tankFilters.setKey('types', [
+ ...tankFilters.types,
+ TankType.PREMIUM,
+ ]);
+ }
+ }}
+ >
+
+
+
+
+ {
+ if (tankFilters.types.includes(TankType.COLLECTOR)) {
+ $tankFilters.setKey(
+ 'types',
+ tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
+ );
+ } else {
+ $tankFilters.setKey('types', [
+ ...tankFilters.types,
+ TankType.COLLECTOR,
+ ]);
+ }
+ }}
+ >
+
+
+
+
+
+
+
-
-
-
+ {
+ $tankFilters.setKey('testing', 'include');
+ }}
+ >
+
+
+ {
+ $tankFilters.setKey('testing', 'exclude');
+ }}
+ >
+
+
+ {
+ $tankFilters.setKey('testing', 'only');
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
-
- {
- $tankFilters.setKey('testing', 'include');
- }}
- >
-
-
- {
- $tankFilters.setKey('testing', 'exclude');
- }}
- >
-
-
- {
- $tankFilters.setKey('testing', 'only');
- }}
- >
-
-
-
-
-
+
{
diff --git a/packages/website/src/core/blitzkit/filterTank.ts b/packages/website/src/core/blitzkit/filterTank.ts
index abba5fa6f..a8c66c67d 100644
--- a/packages/website/src/core/blitzkit/filterTank.ts
+++ b/packages/website/src/core/blitzkit/filterTank.ts
@@ -22,6 +22,27 @@ export function filterTank(filters: TankFilters, tank: TankDefinition) {
(filters.gunType.includes('auto_reloader') &&
tank.turrets.some((turret) =>
turret.guns.some((gun) => gun.gun_type!.$case === 'auto_reloader'),
- )))
+ ))) &&
+ (filters.shells[0] === null ||
+ tank.turrets.some((turret) =>
+ turret.guns.some(
+ (gun) =>
+ gun.gun_type!.value.base.shells[0]?.type === filters.shells[0],
+ ),
+ )) &&
+ (filters.shells[1] === null ||
+ tank.turrets.some((turret) =>
+ turret.guns.some(
+ (gun) =>
+ gun.gun_type!.value.base.shells[1]?.type === filters.shells[1],
+ ),
+ )) &&
+ (filters.shells[2] === null ||
+ tank.turrets.some((turret) =>
+ turret.guns.some(
+ (gun) =>
+ gun.gun_type!.value.base.shells[2]?.type === filters.shells[2],
+ ),
+ ))
);
}
diff --git a/packages/website/src/stores/tankFilters.ts b/packages/website/src/stores/tankFilters.ts
index bdb4b7dc2..a2ba14df9 100644
--- a/packages/website/src/stores/tankFilters.ts
+++ b/packages/website/src/stores/tankFilters.ts
@@ -1,4 +1,9 @@
-import type { GunDefinition, TankClass, TankType } from '@blitzkit/core';
+import type {
+ GunDefinition,
+ ShellType,
+ TankClass,
+ TankType,
+} from '@blitzkit/core';
import { map } from 'nanostores';
export type CaseType = T extends {
@@ -16,6 +21,7 @@ export interface TankFilters {
search?: string;
searching: boolean;
gunType: CaseType[];
+ shells: [ShellType | null, ShellType | null, ShellType | null];
}
export const initialTankFilters: TankFilters = {
@@ -27,6 +33,7 @@ export const initialTankFilters: TankFilters = {
search: undefined,
searching: false,
gunType: [],
+ shells: [null, null, null],
};
export const $tankFilters = map(initialTankFilters);