From ac243a3cdd61fbe67876088a3f2a6b91b9c1b4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 2 Jul 2024 19:30:29 -0500 Subject: [PATCH] feat: multi-select --- .../tank-performance/components/Tanks.tsx | 8 +- .../tankopedia/components/FilterControl.tsx | 119 +++++++++++------- .../tools/tankopedia/components/NoResults.tsx | 7 +- .../tools/tankopedia/components/QuickLink.tsx | 6 +- .../tools/tankopedia/components/SearchBar.tsx | 12 +- .../components/TankSearch/index.tsx | 8 +- .../tankopedia/components/TierFilter.tsx | 14 +-- src/core/blitzkit/filterTank.ts | 14 +++ src/core/blitzkit/tankopediaFilterTank.ts | 17 --- src/stores/tankFilters.ts | 27 ++++ src/stores/tankopediaFilters.ts | 25 ---- 11 files changed, 142 insertions(+), 115 deletions(-) create mode 100644 src/core/blitzkit/filterTank.ts delete mode 100644 src/core/blitzkit/tankopediaFilterTank.ts create mode 100644 src/stores/tankFilters.ts delete mode 100644 src/stores/tankopediaFilters.ts diff --git a/src/app/tools/tank-performance/components/Tanks.tsx b/src/app/tools/tank-performance/components/Tanks.tsx index 0f1d02ade..547a15c41 100644 --- a/src/app/tools/tank-performance/components/Tanks.tsx +++ b/src/app/tools/tank-performance/components/Tanks.tsx @@ -3,9 +3,9 @@ import { Table } from '@radix-ui/themes'; import { Suspense, use, useMemo } from 'react'; import { averageDefinitionsArray } from '../../../../core/blitzkit/averageDefinitions'; +import { filterTank } from '../../../../core/blitzkit/filterTank'; import { tankDefinitions } from '../../../../core/blitzkit/tankDefinitions'; -import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank'; -import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; +import { useTankFilters } from '../../../../stores/tankFilters'; import { useTankPerformanceSort } from '../../../../stores/tankPerformanceSort'; import { RowLoader } from './RowLoader'; import { TankRow } from './TankRow'; @@ -13,7 +13,7 @@ import { TankRow } from './TankRow'; export function Tanks() { const awaitedTankDefinitions = use(tankDefinitions); const awaitedAverageDefinitionsArray = use(averageDefinitionsArray); - const filters = useTankopediaFilters(); + const filters = useTankFilters(); const sort = useTankPerformanceSort(); const tanksSorted = useMemo(() => { switch (sort.type) { @@ -106,7 +106,7 @@ export function Tanks() { }, [sort]); const tanks = useMemo(() => { return tanksSorted.filter((tank) => - tankopediaFilterTank(filters, awaitedTankDefinitions[tank.id]), + filterTank(filters, awaitedTankDefinitions[tank.id]), ); }, [filters, tanksSorted]); diff --git a/src/app/tools/tankopedia/components/FilterControl.tsx b/src/app/tools/tankopedia/components/FilterControl.tsx index 68e498252..ba79cbb53 100644 --- a/src/app/tools/tankopedia/components/FilterControl.tsx +++ b/src/app/tools/tankopedia/components/FilterControl.tsx @@ -15,9 +15,9 @@ import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions'; import { Tier } from '../../../../core/blitzkit/tankDefinitions'; import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants'; import { - mutateTankopediaFilters, - useTankopediaFilters, -} from '../../../../stores/tankopediaFilters'; + mutateTankFilters, + useTankFilters, +} from '../../../../stores/tankFilters'; interface FilterControlProps { compact?: boolean; @@ -25,7 +25,7 @@ interface FilterControlProps { export function FilterControl({ compact }: FilterControlProps) { const awaitedGameDefinitions = use(gameDefinitions); - const filters = useTankopediaFilters(); + const filters = useTankFilters(); return ( @@ -37,7 +37,7 @@ export function FilterControl({ compact }: FilterControlProps) { {times(5, (index) => { const tier = (index + 1) as Tier; - const selected = filters.tier === tier; + const selected = filters.tiers?.includes(tier); return ( - mutateTankopediaFilters((draft) => { - draft.tier = draft.tier === tier ? undefined : tier; + mutateTankFilters((draft) => { + if (draft.tiers?.includes(tier)) { + draft.tiers = draft.tiers?.filter((t) => t !== tier); + } else { + draft.tiers = [...(draft.tiers ?? []), tier]; + } }) } > @@ -60,7 +64,7 @@ export function FilterControl({ compact }: FilterControlProps) { {times(5, (index) => { const tier = (index + 6) as Tier; - const selected = filters.tier === tier; + const selected = filters.tiers?.includes(tier); return ( - mutateTankopediaFilters((draft) => { - draft.tier = draft.tier === tier ? undefined : tier; + mutateTankFilters((draft) => { + if (draft.tiers?.includes(tier)) { + draft.tiers = draft.tiers?.filter((t) => t !== tier); + } else { + draft.tiers = [...(draft.tiers ?? []), tier]; + } }) } > @@ -91,7 +99,7 @@ export function FilterControl({ compact }: FilterControlProps) { {awaitedGameDefinitions.nations .slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2)) .map((nation) => { - const selected = filters.nation === nation; + const selected = filters.nations?.includes(nation); return ( - mutateTankopediaFilters((draft) => { - draft.nation = - draft.nation === nation ? undefined : nation; + mutateTankFilters((draft) => { + if (draft.nations?.includes(nation)) { + draft.nations = draft.nations?.filter( + (n) => n !== nation, + ); + } else { + draft.nations = [...(draft.nations ?? []), nation]; + } }) } > @@ -119,7 +132,7 @@ export function FilterControl({ compact }: FilterControlProps) { {awaitedGameDefinitions.nations .slice(Math.ceil(awaitedGameDefinitions.nations.length / 2)) .map((nation) => { - const selected = filters.nation === nation; + const selected = filters.nations?.includes(nation); return ( - mutateTankopediaFilters((draft) => { - draft.nation = - draft.nation === nation ? undefined : nation; + mutateTankFilters((draft) => { + if (draft.nations?.includes(nation)) { + draft.nations = draft.nations?.filter( + (n) => n !== nation, + ); + } else { + draft.nations = [...(draft.nations ?? []), nation]; + } }) } > @@ -153,7 +171,7 @@ export function FilterControl({ compact }: FilterControlProps) { > {TANK_CLASSES.map((tankClass) => { const Icon = classIcons[tankClass]; - const selected = filters.class === tankClass; + const selected = filters.classes?.includes(tankClass); return ( - mutateTankopediaFilters((draft) => { - draft.class = - draft.class === tankClass ? undefined : tankClass; + mutateTankFilters((draft) => { + if (draft.classes?.includes(tankClass)) { + draft.classes = draft.classes?.filter( + (c) => c !== tankClass, + ); + } else { + draft.classes = [...(draft.classes ?? []), tankClass]; + } }) } > @@ -181,50 +204,61 @@ export function FilterControl({ compact }: FilterControlProps) { direction={compact ? 'column' : { sm: 'row', initial: 'column' }} > - mutateTankopediaFilters((draft) => { - draft.type = - draft.type === 'researchable' ? undefined : 'researchable'; + mutateTankFilters((draft) => { + if (draft.types?.includes('researchable')) { + draft.types = draft.types?.filter((t) => t !== 'researchable'); + } else { + draft.types = [...(draft.types ?? []), 'researchable']; + } }) } > - mutateTankopediaFilters((draft) => { - draft.type = draft.type === 'premium' ? undefined : 'premium'; + mutateTankFilters((draft) => { + if (draft.types?.includes('premium')) { + draft.types = draft.types?.filter((t) => t !== 'premium'); + } else { + draft.types = [...(draft.types ?? []), 'premium']; + } }) } > - mutateTankopediaFilters((draft) => { - draft.type = draft.type === 'collector' ? undefined : 'collector'; + mutateTankFilters((draft) => { + if (draft.types?.includes('collector')) { + draft.types = draft.types?.filter((t) => t !== 'collector'); + } else { + draft.types = [...(draft.types ?? []), 'collector']; + } }) } > @@ -243,7 +277,7 @@ export function FilterControl({ compact }: FilterControlProps) { color={filters.testing === 'include' ? undefined : 'gray'} highContrast onClick={() => - mutateTankopediaFilters((draft) => { + mutateTankFilters((draft) => { draft.testing = 'include'; }) } @@ -258,7 +292,7 @@ export function FilterControl({ compact }: FilterControlProps) { color={filters.testing === 'exclude' ? undefined : 'gray'} highContrast onClick={() => - mutateTankopediaFilters((draft) => { + mutateTankFilters((draft) => { draft.testing = 'exclude'; }) } @@ -273,7 +307,7 @@ export function FilterControl({ compact }: FilterControlProps) { color={filters.testing === 'only' ? undefined : 'gray'} highContrast onClick={() => - mutateTankopediaFilters((draft) => { + mutateTankFilters((draft) => { draft.testing = 'only'; }) } @@ -288,10 +322,7 @@ export function FilterControl({ compact }: FilterControlProps) { - useTankopediaFilters.setState( - useTankopediaFilters.getInitialState(), - true, - ) + useTankFilters.setState(useTankFilters.getInitialState(), true) } > diff --git a/src/app/tools/tankopedia/components/NoResults.tsx b/src/app/tools/tankopedia/components/NoResults.tsx index c4c32c300..c4385aaf5 100644 --- a/src/app/tools/tankopedia/components/NoResults.tsx +++ b/src/app/tools/tankopedia/components/NoResults.tsx @@ -1,5 +1,5 @@ import { Flex, Link, Text } from '@radix-ui/themes'; -import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; +import { useTankFilters } from '../../../../stores/tankFilters'; interface NoResultsProps { type?: 'filters' | 'search'; @@ -15,10 +15,7 @@ export function NoResults({ type = 'filters' }: NoResultsProps) { underline="always" color="red" onClick={() => - useTankopediaFilters.setState( - useTankopediaFilters.getInitialState(), - true, - ) + useTankFilters.setState(useTankFilters.getInitialState(), true) } > {type === 'filters' ? 'Try clearing filters' : 'Try searching again'} diff --git a/src/app/tools/tankopedia/components/QuickLink.tsx b/src/app/tools/tankopedia/components/QuickLink.tsx index 47aa9fac1..d5471ba4f 100644 --- a/src/app/tools/tankopedia/components/QuickLink.tsx +++ b/src/app/tools/tankopedia/components/QuickLink.tsx @@ -1,15 +1,15 @@ import { CaretRightIcon } from '@radix-ui/react-icons'; import { Button, Link, TextField } from '@radix-ui/themes'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; -import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; +import { useTankFilters } from '../../../../stores/tankFilters'; interface QuickLinkProps { topResult?: TankDefinition; } export function QuickLink({ topResult }: QuickLinkProps) { - const search = useTankopediaFilters((state) => state.search); - const searching = useTankopediaFilters((state) => state.searching); + const search = useTankFilters((state) => state.search); + const searching = useTankFilters((state) => state.searching); if (!search || !topResult || searching) return null; diff --git a/src/app/tools/tankopedia/components/SearchBar.tsx b/src/app/tools/tankopedia/components/SearchBar.tsx index 66a47398b..f86a5ae92 100644 --- a/src/app/tools/tankopedia/components/SearchBar.tsx +++ b/src/app/tools/tankopedia/components/SearchBar.tsx @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { useRouter } from 'next/navigation'; import { KeyboardEventHandler, useCallback, useRef } from 'react'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; -import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; +import { useTankFilters } from '../../../../stores/tankFilters'; import { QuickLink } from './QuickLink'; import { Sort } from './Sort'; @@ -16,24 +16,24 @@ interface SearchBarProps { export function SearchBar({ topResult, onSelect }: SearchBarProps) { const router = useRouter(); const input = useRef(null); - const searching = useTankopediaFilters((state) => state.searching); + const searching = useTankFilters((state) => state.searching); const performSearch = useCallback( debounce(() => { - useTankopediaFilters.setState({ searching: false }); + useTankFilters.setState({ searching: false }); if (!input.current) return; const sanitized = input.current.value.trim(); - useTankopediaFilters.setState({ + useTankFilters.setState({ search: sanitized.length === 0 ? undefined : sanitized, }); }, 500), [], ); const handleChange = useCallback(() => { - if (!useTankopediaFilters.getState().searching) { - useTankopediaFilters.setState({ searching: true }); + if (!useTankFilters.getState().searching) { + useTankFilters.setState({ searching: true }); } performSearch(); diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index 33f6665e4..9f631646a 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -6,6 +6,7 @@ import { AdMidSectionResponsive } from '../../../../../components/AdMidSectionRe import { ExperimentIcon } from '../../../../../components/ExperimentIcon'; import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants'; import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; +import { filterTank } from '../../../../../core/blitzkit/filterTank'; import { gameDefinitions } from '../../../../../core/blitzkit/gameDefinitions'; import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions'; import { normalizeBoundingBox } from '../../../../../core/blitzkit/normalizeBoundingBox'; @@ -17,11 +18,10 @@ import { tankNames, tanksDefinitionsArray, } from '../../../../../core/blitzkit/tankDefinitions'; -import { tankopediaFilterTank } from '../../../../../core/blitzkit/tankopediaFilterTank'; import { unionBoundingBox } from '../../../../../core/blitzkit/unionBoundingBox'; import { useAdExempt } from '../../../../../hooks/useAdExempt'; +import { useTankFilters } from '../../../../../stores/tankFilters'; import { SORT_NAMES, SORT_UNITS } from '../../../../../stores/tankopedia'; -import { useTankopediaFilters } from '../../../../../stores/tankopediaFilters'; import { useTankopediaSort } from '../../../../../stores/tankopediaSort'; import { FilterControl } from '../FilterControl'; import { NoResults } from '../NoResults'; @@ -45,12 +45,12 @@ export const TankSearch = memo( const awaitedTankDefinitions = use(tankDefinitions); const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); const awaitedTankNames = use(tankNames); - const filters = useTankopediaFilters(); + const filters = useTankFilters(); const sort = useTankopediaSort(); const tanks = useMemo(() => { if (filters.search === undefined) { const filtered = awaitedTanksDefinitionsArray.filter((tank) => - tankopediaFilterTank(filters, tank), + filterTank(filters, tank), ); let sorted: TankDefinition[]; diff --git a/src/app/tools/tankopedia/components/TierFilter.tsx b/src/app/tools/tankopedia/components/TierFilter.tsx index fcb7cefca..f13898604 100644 --- a/src/app/tools/tankopedia/components/TierFilter.tsx +++ b/src/app/tools/tankopedia/components/TierFilter.tsx @@ -4,15 +4,15 @@ import { memo } from 'react'; import { Tier } from '../../../../core/blitzkit/tankDefinitions'; import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants'; import { - mutateTankopediaFilters, - useTankopediaFilters, -} from '../../../../stores/tankopediaFilters'; + mutateTankFilters, + useTankFilters, +} from '../../../../stores/tankFilters'; import { useTankopediaSort } from '../../../../stores/tankopediaSort'; export const TierFilter = memo(() => { - const tierFilter = useTankopediaFilters((state) => state.tier); + const tierFilter = useTankFilters((state) => state.tiers); const sort = useTankopediaSort(); - const search = useTankopediaFilters((state) => state.search); + const search = useTankFilters((state) => state.search); if (sort.by !== 'meta.none' || search) return null; @@ -35,8 +35,8 @@ export const TierFilter = memo(() => { color={selected ? undefined : 'gray'} highContrast onClick={() => - mutateTankopediaFilters((draft) => { - draft.tier = tier; + mutateTankFilters((draft) => { + draft.tiers = tier; }) } > diff --git a/src/core/blitzkit/filterTank.ts b/src/core/blitzkit/filterTank.ts new file mode 100644 index 000000000..e25e36fd1 --- /dev/null +++ b/src/core/blitzkit/filterTank.ts @@ -0,0 +1,14 @@ +import { TankFilters } from '../../stores/tankFilters'; +import { TankDefinition } from './tankDefinitions'; + +export function filterTank(filters: TankFilters, tank: TankDefinition) { + return ( + (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.treeType)) && + (filters.testing === 'include' || + (filters.testing === 'only' && tank.testing) || + (filters.testing === 'exclude' && !tank.testing)) + ); +} diff --git a/src/core/blitzkit/tankopediaFilterTank.ts b/src/core/blitzkit/tankopediaFilterTank.ts deleted file mode 100644 index 9c41665c8..000000000 --- a/src/core/blitzkit/tankopediaFilterTank.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TankopediaFilters } from '../../stores/tankopediaFilters'; -import { TankDefinition } from './tankDefinitions'; - -export function tankopediaFilterTank( - filters: TankopediaFilters, - tank: TankDefinition, -) { - return ( - (filters.tier === undefined || filters.tier === tank.tier) && - (filters.nation === undefined || filters.nation === tank.nation) && - (filters.class === undefined || filters.class === tank.class) && - (filters.type === undefined || filters.type === tank.treeType) && - (filters.testing === 'include' || - (filters.testing === 'only' && tank.testing) || - (filters.testing === 'exclude' && !tank.testing)) - ); -} diff --git a/src/stores/tankFilters.ts b/src/stores/tankFilters.ts new file mode 100644 index 000000000..586c2f228 --- /dev/null +++ b/src/stores/tankFilters.ts @@ -0,0 +1,27 @@ +import { produce } from 'immer'; +import { create } from 'zustand'; +import { TankClass, TreeType } from '../components/Tanks'; +import { Tier } from '../core/blitzkit/tankDefinitions'; + +export interface TankFilters { + tiers: Tier[]; + nations: string[]; + classes: TankClass[]; + types: TreeType[]; + testing: 'include' | 'exclude' | 'only'; + search?: string; + searching: boolean; +} + +export const useTankFilters = create()(() => ({ + testing: 'include', + searching: false, + classes: [], + nations: [], + tiers: [], + types: [], +})); + +export function mutateTankFilters(recipe: (draft: TankFilters) => void) { + useTankFilters.setState(produce(recipe)); +} diff --git a/src/stores/tankopediaFilters.ts b/src/stores/tankopediaFilters.ts deleted file mode 100644 index fd1e6e70a..000000000 --- a/src/stores/tankopediaFilters.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { produce } from 'immer'; -import { create } from 'zustand'; -import { TankClass, TreeType } from '../components/Tanks'; -import { Tier } from '../core/blitzkit/tankDefinitions'; - -export interface TankopediaFilters { - tier?: Tier; - nation?: string; - class?: TankClass; - type?: TreeType; - testing: 'include' | 'exclude' | 'only'; - search?: string; - searching: boolean; -} - -export const useTankopediaFilters = create()(() => ({ - testing: 'include', - searching: false, -})); - -export function mutateTankopediaFilters( - recipe: (draft: TankopediaFilters) => void, -) { - useTankopediaFilters.setState(produce(recipe)); -}