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()(() => ({