From 8e438757fd092797a80cd08238644be02cf957d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com>
Date: Fri, 21 Jun 2024 19:04:34 -0500
Subject: [PATCH] feat: search bar
---
.../tankopedia/components/FilterControl.tsx | 318 +++++++++++++++
.../tools/tankopedia/components/Results.tsx | 60 +--
.../tools/tankopedia/components/SearchBar.tsx | 69 ++++
.../tankopedia/components/TankCardWrapper.tsx | 13 +
.../tools/tankopedia/components/TierCard.tsx | 25 +-
src/app/tools/tankopedia/page.tsx | 385 +-----------------
src/stores/tankopediaFilters.ts | 1 +
7 files changed, 450 insertions(+), 421 deletions(-)
create mode 100644 src/app/tools/tankopedia/components/FilterControl.tsx
create mode 100644 src/app/tools/tankopedia/components/SearchBar.tsx
create mode 100644 src/app/tools/tankopedia/components/TankCardWrapper.tsx
diff --git a/src/app/tools/tankopedia/components/FilterControl.tsx b/src/app/tools/tankopedia/components/FilterControl.tsx
new file mode 100644
index 000000000..d6a336b33
--- /dev/null
+++ b/src/app/tools/tankopedia/components/FilterControl.tsx
@@ -0,0 +1,318 @@
+import { TrashIcon } from '@radix-ui/react-icons';
+import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes';
+import { times } from 'lodash';
+import { use } from 'react';
+import { classIcons } from '../../../../components/ClassIcon';
+import { ExperimentIcon } from '../../../../components/ExperimentIcon';
+import { NAVBAR_HEIGHT } from '../../../../components/Navbar';
+import { ResearchedIcon } from '../../../../components/ResearchedIcon';
+import { ScienceIcon } from '../../../../components/ScienceIcon';
+import { ScienceOffIcon } from '../../../../components/ScienceOffIcon';
+import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
+import { asset } from '../../../../core/blitzkit/asset';
+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';
+
+export function FilterControl() {
+ const awaitedGameDefinitions = use(gameDefinitions);
+ const filters = useTankopediaFilters();
+
+ return (
+
+
+
+ {times(5, (index) => {
+ const tier = (index + 1) as Tier;
+ const selected = filters.tier === tier;
+
+ return (
+
+ mutateTankopediaFilters((draft) => {
+ draft.tier = draft.tier === tier ? undefined : tier;
+ })
+ }
+ >
+ {TIER_ROMAN_NUMERALS[tier]}
+
+ );
+ })}
+
+
+ {times(5, (index) => {
+ const tier = (index + 6) as Tier;
+ const selected = filters.tier === tier;
+
+ return (
+
+ mutateTankopediaFilters((draft) => {
+ draft.tier = draft.tier === tier ? undefined : tier;
+ })
+ }
+ >
+ {TIER_ROMAN_NUMERALS[tier]}
+
+ );
+ })}
+
+
+
+
+
+ {awaitedGameDefinitions.nations
+ .slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2))
+ .map((nation) => {
+ const selected = filters.nation === nation;
+
+ return (
+
+ mutateTankopediaFilters((draft) => {
+ draft.nation =
+ draft.nation === nation ? undefined : nation;
+ })
+ }
+ >
+
+
+ );
+ })}
+
+
+ {awaitedGameDefinitions.nations
+ .slice(Math.ceil(awaitedGameDefinitions.nations.length / 2))
+ .map((nation) => {
+ const selected = filters.nation === nation;
+
+ return (
+
+ mutateTankopediaFilters((draft) => {
+ draft.nation =
+ draft.nation === nation ? undefined : nation;
+ })
+ }
+ >
+
+
+ );
+ })}
+
+
+
+
+ {TANK_CLASSES.map((tankClass) => {
+ const Icon = classIcons[tankClass];
+ const selected = filters.class === tankClass;
+
+ return (
+
+ mutateTankopediaFilters((draft) => {
+ draft.class =
+ draft.class === tankClass ? undefined : tankClass;
+ })
+ }
+ >
+
+
+ );
+ })}
+
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.type =
+ draft.type === 'researchable' ? undefined : 'researchable';
+ })
+ }
+ >
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.type = draft.type === 'premium' ? undefined : 'premium';
+ })
+ }
+ >
+
+
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.type = draft.type === 'collector' ? undefined : 'collector';
+ })
+ }
+ >
+
+
+
+
+
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.testing = 'include';
+ })
+ }
+ >
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.testing = 'exclude';
+ })
+ }
+ >
+
+
+
+ mutateTankopediaFilters((draft) => {
+ draft.testing = 'only';
+ })
+ }
+ >
+
+
+
+
+
+
+ useTankopediaFilters.setState(
+ useTankopediaFilters.getInitialState(),
+ )
+ }
+ >
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/tankopedia/components/Results.tsx b/src/app/tools/tankopedia/components/Results.tsx
index 5947a9d55..59bf5bb5a 100644
--- a/src/app/tools/tankopedia/components/Results.tsx
+++ b/src/app/tools/tankopedia/components/Results.tsx
@@ -1,26 +1,41 @@
import { Callout, Flex } from '@radix-ui/themes';
+import { go } from 'fuzzysort';
import { range } from 'lodash';
-import { memo, use } from 'react';
+import { memo, use, useMemo } from 'react';
import { ExperimentIcon } from '../../../../components/ExperimentIcon';
-import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
-import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions';
import {
Tier,
- tanksDefinitionsArray,
+ tankDefinitions,
+ tankNames,
} from '../../../../core/blitzkit/tankDefinitions';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
-import { treeTypeOrder } from './TankSearch/constants';
+import { SearchBar } from './SearchBar';
+import { TankCard } from './TankCard';
+import { TankCardWrapper } from './TankCardWrapper';
import { TierCard } from './TierCard';
export const Results = memo(() => {
- const tanks = use(tanksDefinitionsArray);
- const awaitedGameDefinitions = use(gameDefinitions);
+ const awaitedTankDefinitions = use(tankDefinitions);
+ const awaitedTankNames = use(tankNames);
const testing = useTankopediaFilters((state) => state.testing);
+ const search = useTankopediaFilters((state) => state.search);
+ const searchedTanks = useMemo(() => {
+ if (search === undefined) return undefined;
+
+ const searchedRaw = go(search, awaitedTankNames, { key: 'combined' });
+ const searchedTanks = searchedRaw.map(
+ (result) => awaitedTankDefinitions[result.obj.id],
+ );
+
+ return searchedTanks;
+ }, [search]);
return (
-
+
+
+
{testing === 'only' && (
-
+
@@ -31,27 +46,14 @@ export const Results = memo(() => {
)}
- {range(10, 0).map((tierUntyped) => {
- const tier = tierUntyped as Tier;
- const tierTanks = tanks
- .filter((tank) => tank.tier === tier)
- .sort(
- (a, b) =>
- treeTypeOrder.indexOf(a.treeType) -
- treeTypeOrder.indexOf(b.treeType),
- )
- .sort(
- (a, b) =>
- TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
- )
- .sort(
- (a, b) =>
- awaitedGameDefinitions.nations.indexOf(a.nation) -
- awaitedGameDefinitions.nations.indexOf(b.nation),
- );
+ {!search &&
+ range(10, 0).map((tier) => )}
- return ;
- })}
+ {search && (
+
+ {searchedTanks?.map((tank) => )}
+
+ )}
);
});
diff --git a/src/app/tools/tankopedia/components/SearchBar.tsx b/src/app/tools/tankopedia/components/SearchBar.tsx
new file mode 100644
index 000000000..6d48a4ef4
--- /dev/null
+++ b/src/app/tools/tankopedia/components/SearchBar.tsx
@@ -0,0 +1,69 @@
+import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
+import { Button, Spinner, TextField } from '@radix-ui/themes';
+import { debounce } from 'lodash';
+import { useRouter } from 'next/navigation';
+import { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
+import { Link } from '../../../../components/Link';
+import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
+import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
+
+interface SearchBarProps {
+ topResult?: TankDefinition;
+}
+
+export function SearchBar({ topResult }: SearchBarProps) {
+ const router = useRouter();
+ const [searching, setSearching] = useState(false);
+ const input = useRef(null);
+ const search = useCallback(
+ debounce(() => {
+ setSearching(false);
+
+ if (!input.current) return;
+
+ const sanitized = input.current.value.trim();
+
+ useTankopediaFilters.setState({
+ search: sanitized.length === 0 ? undefined : sanitized,
+ });
+ }, 500),
+ [],
+ );
+ const handleChange = useCallback(() => {
+ setSearching(true);
+ search();
+ }, []);
+ const handleKeyDown = useCallback(
+ (event) => {
+ if (event.key !== 'Enter' || !topResult) return;
+
+ event.preventDefault();
+ router.push(`/tools/tankopedia/${topResult.id}`);
+ },
+ [topResult],
+ );
+
+ return (
+
+
+ {searching ? : }
+
+
+ {topResult && (
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/tools/tankopedia/components/TankCardWrapper.tsx b/src/app/tools/tankopedia/components/TankCardWrapper.tsx
new file mode 100644
index 000000000..43b09015a
--- /dev/null
+++ b/src/app/tools/tankopedia/components/TankCardWrapper.tsx
@@ -0,0 +1,13 @@
+import { Grid, GridProps } from '@radix-ui/themes';
+
+export function TankCardWrapper(props: GridProps) {
+ return (
+
+ );
+}
diff --git a/src/app/tools/tankopedia/components/TierCard.tsx b/src/app/tools/tankopedia/components/TierCard.tsx
index abfa63649..a27a3d19c 100644
--- a/src/app/tools/tankopedia/components/TierCard.tsx
+++ b/src/app/tools/tankopedia/components/TierCard.tsx
@@ -1,22 +1,27 @@
-import { Flex, Grid, Heading, Separator } from '@radix-ui/themes';
-import { memo, useEffect, useRef } from 'react';
+import { Flex, Heading, Separator } from '@radix-ui/themes';
+import { memo, use, useEffect, useMemo, useRef } from 'react';
import {
- TankDefinition,
Tier,
+ tanksDefinitionsArray,
} from '../../../../core/blitzkit/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import { TankCard } from './TankCard';
+import { TankCardWrapper } from './TankCardWrapper';
interface TierCardProps {
tier: Tier;
- tanks: TankDefinition[];
}
export const TierCard = memo(
- ({ tier, tanks }: TierCardProps) => {
+ ({ tier }: TierCardProps) => {
+ const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const container = useRef(null);
+ const tanks = useMemo(
+ () => awaitedTanksDefinitionsArray.filter((tank) => tank.tier === tier),
+ [tier],
+ );
useEffect(() => {
const unsubscribe = useTankopediaFilters.subscribe((filters) => {
@@ -37,22 +42,18 @@ export const TierCard = memo(
direction="column"
gap="4"
display={tanks.length === 0 ? 'none' : 'flex'}
+ py="4"
>
Tier {TIER_ROMAN_NUMERALS[tier]}
-
+
{tanks.map((tank) => (
))}
-
+
);
},
diff --git a/src/app/tools/tankopedia/page.tsx b/src/app/tools/tankopedia/page.tsx
index 561557a6b..a5b96b0c8 100644
--- a/src/app/tools/tankopedia/page.tsx
+++ b/src/app/tools/tankopedia/page.tsx
@@ -1,395 +1,20 @@
'use client';
-import { TrashIcon } from '@radix-ui/react-icons';
-import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes';
-import { times } from 'lodash';
-import { use } from 'react';
-import { classIcons } from '../../../components/ClassIcon';
-import { ExperimentIcon } from '../../../components/ExperimentIcon';
-import { NAVBAR_HEIGHT } from '../../../components/Navbar';
+import { Flex } from '@radix-ui/themes';
import PageWrapper from '../../../components/PageWrapper';
-import { ResearchedIcon } from '../../../components/ResearchedIcon';
-import { ScienceIcon } from '../../../components/ScienceIcon';
-import { ScienceOffIcon } from '../../../components/ScienceOffIcon';
-import { TANK_CLASSES } from '../../../components/Tanks/components/Item/constants';
-import { asset } from '../../../core/blitzkit/asset';
-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';
+import { FilterControl } from './components/FilterControl';
import { Results } from './components/Results';
export default function Page() {
- const awaitedGameDefinitions = use(gameDefinitions);
- const filters = useTankopediaFilters();
-
return (
-
-
-
- {times(5, (index) => {
- const tier = (index + 1) as Tier;
- const selected = filters.tier === tier;
-
- return (
-
- mutateTankopediaFilters((draft) => {
- draft.tier = draft.tier === tier ? undefined : tier;
- })
- }
- >
- {TIER_ROMAN_NUMERALS[tier]}
-
- );
- })}
-
-
- {times(5, (index) => {
- const tier = (index + 6) as Tier;
- const selected = filters.tier === tier;
-
- return (
-
- mutateTankopediaFilters((draft) => {
- draft.tier = draft.tier === tier ? undefined : tier;
- })
- }
- >
- {TIER_ROMAN_NUMERALS[tier]}
-
- );
- })}
-
-
-
-
-
- {awaitedGameDefinitions.nations
- .slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2))
- .map((nation) => {
- const selected = filters.nation === nation;
-
- return (
-
- mutateTankopediaFilters((draft) => {
- draft.nation =
- draft.nation === nation ? undefined : nation;
- })
- }
- >
-
-
- );
- })}
-
-
- {awaitedGameDefinitions.nations
- .slice(Math.ceil(awaitedGameDefinitions.nations.length / 2))
- .map((nation) => {
- const selected = filters.nation === nation;
-
- return (
-
- mutateTankopediaFilters((draft) => {
- draft.nation =
- draft.nation === nation ? undefined : nation;
- })
- }
- >
-
-
- );
- })}
-
-
-
-
- {TANK_CLASSES.map((tankClass) => {
- const Icon = classIcons[tankClass];
- const selected = filters.class === tankClass;
-
- return (
-
- mutateTankopediaFilters((draft) => {
- draft.class =
- draft.class === tankClass ? undefined : tankClass;
- })
- }
- >
-
-
- );
- })}
-
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.type =
- draft.type === 'researchable' ? undefined : 'researchable';
- })
- }
- >
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.type = draft.type === 'premium' ? undefined : 'premium';
- })
- }
- >
-
-
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.type =
- draft.type === 'collector' ? undefined : 'collector';
- })
- }
- >
-
-
-
-
-
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.testing = 'include';
- })
- }
- >
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.testing = 'exclude';
- })
- }
- >
-
-
-
- mutateTankopediaFilters((draft) => {
- draft.testing = 'only';
- })
- }
- >
-
-
-
-
-
-
- useTankopediaFilters.setState(
- useTankopediaFilters.getInitialState(),
- )
- }
- >
-
-
-
-
-
-
-
-
+
diff --git a/src/stores/tankopediaFilters.ts b/src/stores/tankopediaFilters.ts
index 18b0e7fac..13e65ed9f 100644
--- a/src/stores/tankopediaFilters.ts
+++ b/src/stores/tankopediaFilters.ts
@@ -9,6 +9,7 @@ export interface TankopediaFilters {
class?: TankClass;
type?: TreeType;
testing: 'include' | 'exclude' | 'only';
+ search?: string;
}
export const useTankopediaFilters = create()(() => ({