From d186368c9802805215434bca2fe970d698827f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 25 Jun 2024 19:17:53 -0500 Subject: [PATCH] feat: compact search --- .../tankopedia/components/FilterControl.tsx | 24 +- .../tools/tankopedia/components/Results.tsx | 404 --------- .../tankopedia/components/TankCard.css.ts | 1 + .../tools/tankopedia/components/TankCard.tsx | 8 +- .../components/CompactSearchResultRow.tsx | 51 -- .../components/Options/index.css.ts | 17 - .../TankSearch/components/Options/index.tsx | 354 -------- .../TankSearch/components/Results.tsx | 175 ---- .../components/TankSearch/index.tsx | 830 ++++++++---------- src/app/tools/tankopedia/page.tsx | 4 +- 10 files changed, 368 insertions(+), 1500 deletions(-) delete mode 100644 src/app/tools/tankopedia/components/Results.tsx delete mode 100644 src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx delete mode 100644 src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts delete mode 100644 src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx delete mode 100644 src/app/tools/tankopedia/components/TankSearch/components/Results.tsx diff --git a/src/app/tools/tankopedia/components/FilterControl.tsx b/src/app/tools/tankopedia/components/FilterControl.tsx index a7311792c..fbe87c22d 100644 --- a/src/app/tools/tankopedia/components/FilterControl.tsx +++ b/src/app/tools/tankopedia/components/FilterControl.tsx @@ -17,18 +17,22 @@ import { useTankopediaFilters, } from '../../../../stores/tankopediaFilters'; -export function FilterControl() { +interface FilterControlProps { + compact?: boolean; +} + +export function FilterControl({ compact }: FilterControlProps) { const awaitedGameDefinitions = use(gameDefinitions); const filters = useTankopediaFilters(); return ( - + {times(5, (index) => { const tier = (index + 1) as Tier; const selected = filters.tier === tier; @@ -51,7 +55,7 @@ export function FilterControl() { ); })} - + {times(5, (index) => { const tier = (index + 6) as Tier; const selected = filters.tier === tier; @@ -77,11 +81,11 @@ export function FilterControl() { - + {awaitedGameDefinitions.nations .slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2)) .map((nation) => { @@ -109,7 +113,7 @@ export function FilterControl() { ); })} - + {awaitedGameDefinitions.nations .slice(Math.ceil(awaitedGameDefinitions.nations.length / 2)) .map((nation) => { @@ -143,7 +147,7 @@ export function FilterControl() { {TANK_CLASSES.map((tankClass) => { const Icon = classIcons[tankClass]; @@ -172,7 +176,7 @@ export function FilterControl() { { - const awaitedGameDefinitions = use(gameDefinitions); - const awaitedModelDefinitions = use(modelDefinitions); - const awaitedTankDefinitions = use(tankDefinitions); - const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); - const awaitedTankNames = use(tankNames); - const filters = useTankopediaFilters(); - const tanks = useMemo(() => { - if (filters.search === undefined) { - const filtered = awaitedTanksDefinitionsArray.filter((tank) => - tankopediaFilterTank(filters, tank), - ); - let sorted: TankDefinition[]; - - switch (filters.sort.by) { - case 'meta.none': - sorted = filtered - .sort( - (a, b) => - treeTypeOrder.indexOf(b.treeType) - - treeTypeOrder.indexOf(a.treeType), - ) - .sort( - (a, b) => - TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class), - ) - .sort( - (a, b) => - awaitedGameDefinitions.nations.indexOf(b.nation) - - awaitedGameDefinitions.nations.indexOf(a.nation), - ); - break; - - case 'survivability.health': { - sorted = filtered.sort((a, b) => { - const aHealth = a.health + a.turrets.at(-1)!.health; - const bHealth = b.health + b.turrets.at(-1)!.health; - - return aHealth - bHealth; - }); - break; - } - - case 'survivability.viewRange': - sorted = filtered.sort( - (a, b) => a.turrets.at(-1)!.viewRange - b.turrets.at(-1)!.viewRange, - ); - break; - - case 'survivability.camouflageStill': - sorted = filtered.sort( - (a, b) => a.camouflage.still - b.camouflage.still, - ); - break; - - case 'survivability.camouflageMoving': - sorted = filtered.sort( - (a, b) => a.camouflage.moving - b.camouflage.moving, - ); - break; - - case 'survivability.camouflageShooting': - sorted = filtered.sort( - (a, b) => - a.camouflage.still * - a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss - - b.camouflage.still * - b.turrets.at(-1)!.guns.at(-1)!.camouflageLoss, - ); - break; - - case 'survivability.volume': - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = awaitedModelDefinitions[a.id]; - const bTankModelDefinition = awaitedModelDefinitions[b.id]; - const aTurretModelDefinition = - aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; - const bTurretModelDefinition = - bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; - const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.boundingBox, - aTurretModelDefinition.boundingBox, - ), - ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), - ); - const aVolume = aSize[0] * aSize[1] * aSize[2]; - const bVolume = bSize[0] * bSize[1] * bSize[2]; - - return aVolume - bVolume; - }); - break; - - case 'survivability.length': - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = awaitedModelDefinitions[a.id]; - const bTankModelDefinition = awaitedModelDefinitions[b.id]; - const aTurretModelDefinition = - aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; - const bTurretModelDefinition = - bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; - const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.boundingBox, - aTurretModelDefinition.boundingBox, - ), - ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), - ); - const aLength = Math.max(aSize[0], aSize[1], aSize[2]); - const bLength = Math.max(bSize[0], bSize[1], bSize[2]); - - return aLength - bLength; - }); - break; - - case 'fire.dpm': - sorted = filtered.sort( - (a, b) => - resolveDpm( - a.turrets.at(-1)!.guns.at(-1)!, - a.turrets.at(-1)!.guns.at(-1)!.shells[0], - ) - - resolveDpm( - b.turrets.at(-1)!.guns.at(-1)!, - b.turrets.at(-1)!.guns.at(-1)!.shells[0], - ), - ); - break; - - case 'fire.reload': - sorted = filtered.sort( - (a, b) => - resolveReload(a.turrets.at(-1)!.guns.at(-1)!) - - resolveReload(b.turrets.at(-1)!.guns.at(-1)!), - ); - break; - - case 'fire.caliber': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber, - ); - break; - - case 'fire.standardPenetration': - sorted = filtered.sort( - (a, b) => - resolveNearPenetration( - a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, - ) - - resolveNearPenetration( - b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, - ), - ); - break; - - case 'fire.premiumPenetration': - sorted = filtered.sort( - (a, b) => - resolveNearPenetration( - a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, - ) - - resolveNearPenetration( - b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, - ), - ); - break; - - case 'fire.damage': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor, - ); - break; - - case 'fire.shellVelocity': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].speed - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].speed, - ); - break; - - case 'fire.aimTime': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.aimTime - - b.turrets.at(-1)!.guns.at(-1)!.aimTime, - ); - break; - - case 'fire.dispersionStill': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.dispersion.base - - b.turrets.at(-1)!.guns.at(-1)!.dispersion.base, - ); - break; - - case 'fire.dispersionMoving': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.dispersion.move - - b.tracks.at(-1)!.dispersion.move, - ); - break; - - case 'fire.gunDepression': - sorted = filtered.sort( - (a, b) => - awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ - a.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.max + - (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0) - - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.max - - (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0), - ); - break; - - case 'fire.gunElevation': - sorted = filtered.sort( - (a, b) => - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min + - (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0) - - awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ - a.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min - - (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0), - ); - break; - - case 'maneuverability.forwardsSpeed': - sorted = filtered.sort((a, b) => a.speed.forwards - b.speed.forwards); - break; - - case 'maneuverability.backwardsSpeed': - sorted = filtered.sort( - (a, b) => a.speed.backwards - b.speed.backwards, - ); - break; - - case 'maneuverability.power': - sorted = filtered.sort( - (a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power, - ); - break; - - case 'maneuverability.powerToWeight': - sorted = filtered.sort( - (a, b) => - a.engines.at(-1)!.power / - (a.weight + - a.engines.at(-1)!.weight + - a.tracks.at(-1)!.weight + - a.turrets.at(-1)!.weight + - a.turrets.at(-1)!.guns.at(-1)!.weight) - - b.engines.at(-1)!.power / - (b.weight + - b.engines.at(-1)!.weight + - b.tracks.at(-1)!.weight + - b.turrets.at(-1)!.weight + - b.turrets.at(-1)!.guns.at(-1)!.weight), - ); - break; - - case 'maneuverability.weight': - sorted = filtered.sort( - (a, b) => - a.weight + - a.engines.at(-1)!.weight + - a.tracks.at(-1)!.weight + - a.turrets.at(-1)!.weight + - a.turrets.at(-1)!.guns.at(-1)!.weight - - (b.weight + - b.engines.at(-1)!.weight + - b.tracks.at(-1)!.weight + - b.turrets.at(-1)!.weight + - b.turrets.at(-1)!.guns.at(-1)!.weight), - ); - break; - - case 'maneuverability.traverseSpeed': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.traverseSpeed - b.tracks.at(-1)!.traverseSpeed, - ); - break; - } - - return filters.sort.direction === 'ascending' ? sorted : sorted.reverse(); - } else { - const searchedRaw = go(filters.search, awaitedTankNames, { - key: 'combined', - limit: 25, - }); - const searchedTanks = searchedRaw.map( - (result) => awaitedTankDefinitions[result.obj.id], - ); - return searchedTanks; - } - }, [filters]); - - return ( - - - - {!filters.search && !filters.searching && } - - {filters.sort.by !== 'meta.none' && ( - - - Sorting by {SORT_NAMES[filters.sort.by]} - {SORT_UNITS[filters.sort.by] === undefined - ? '' - : ` (${SORT_UNITS[filters.sort.by]})`} - , {filters.sort.direction} - - - )} - - {filters.testing === 'only' && ( - - - - - - - Tanks in testing are subject to change and many not represent the - final product. - - - - )} - - {!filters.searching && ( - <> - {tanks.length > 0 && ( - - {tanks.map((tank) => ( - - ))} - - )} - - {tanks.length === 0 && } - - )} - - {filters.searching && ( - - {times(Math.round(10 + 10 * Math.random()), (index) => ( - - ))} - - )} - - ); -}); diff --git a/src/app/tools/tankopedia/components/TankCard.css.ts b/src/app/tools/tankopedia/components/TankCard.css.ts index 4eee27a4f..ade257683 100644 --- a/src/app/tools/tankopedia/components/TankCard.css.ts +++ b/src/app/tools/tankopedia/components/TankCard.css.ts @@ -3,6 +3,7 @@ import { createVar, style } from '@vanilla-extract/css'; export const cardBackgroundVar = createVar(); export const card = style({ + cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', diff --git a/src/app/tools/tankopedia/components/TankCard.tsx b/src/app/tools/tankopedia/components/TankCard.tsx index 891ace3e1..b467b4516 100644 --- a/src/app/tools/tankopedia/components/TankCard.tsx +++ b/src/app/tools/tankopedia/components/TankCard.tsx @@ -17,9 +17,10 @@ import * as styles from './TankCard.css'; interface TankCardProps { tank: TankDefinition; + onSelect?: (tank: TankDefinition) => void; } -export function TankCard({ tank }: TankCardProps) { +export function TankCard({ tank, onSelect }: TankCardProps) { const awaitedModelDefinitions = use(modelDefinitions); const Icon = classIcons[tank.class]; // const link = useRef(null); @@ -131,7 +132,7 @@ export function TankCard({ tank }: TankCardProps) { return ( onSelect(tank) : undefined} className={styles.card} style={assignInlineVars({ [styles.cardBackgroundVar]: `top left / contain no-repeat url(${asset(`flags/scratched/${tank.nation}.webp`)})`, diff --git a/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx b/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx deleted file mode 100644 index 9dc885764..000000000 --- a/src/app/tools/tankopedia/components/TankSearch/components/CompactSearchResultRow.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button, Flex, Text } from '@radix-ui/themes'; -import { SmallTankIcon } from '../../../../../../components/SmallTankIcon'; -import { TankDefinition } from '../../../../../../core/blitzkit/tankDefinitions'; -import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants'; - -interface CompactSearchResultRowProps { - tanks: TankDefinition[]; - onSelect?: (tank: TankDefinition) => void; -} - -export function CompactSearchResultRow({ - tanks, - onSelect = () => {}, -}: CompactSearchResultRowProps) { - return ( - - - {tanks.map((tank) => ( - - ))} - - - ); -} diff --git a/src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts b/src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts deleted file mode 100644 index ffc473783..000000000 --- a/src/app/tools/tankopedia/components/TankSearch/components/Options/index.css.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const splitFilters = style({ - '@media': { - 'screen and (min-width: 513px)': { - display: 'none', - }, - }, -}); - -export const oneLineFilters = style({ - '@media': { - 'screen and (max-width: 512px)': { - display: 'none', - }, - }, -}); diff --git a/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx b/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx deleted file mode 100644 index 5f1ca2000..000000000 --- a/src/app/tools/tankopedia/components/TankSearch/components/Options/index.tsx +++ /dev/null @@ -1,354 +0,0 @@ -'use client'; - -import { CaretDownIcon } from '@radix-ui/react-icons'; -import { Button, DropdownMenu, Flex } from '@radix-ui/themes'; -import { use } from 'react'; -import { Flag } from '../../../../../../../components/Flag'; -import {} from '../../../../../../../components/Tanks'; -import { - TANK_CLASSES, - TREE_TYPES, - TREE_TYPE_ICONS, - TREE_TYPE_IMAGES, -} from '../../../../../../../components/Tanks/components/Item/constants'; -import { gameDefinitions } from '../../../../../../../core/blitzkit/gameDefinitions'; -import { - TIERS, - TIER_ROMAN_NUMERALS, -} from '../../../../../../../core/blitzkit/tankDefinitions/constants'; -import mutateTankopediaPersistent, { - TankopediaTestTankDisplay, - useTankopediaPersistent, -} from '../../../../../../../stores/tankopedia'; -import * as styles from './index.css'; - -export function Options() { - const awaitedGameDefinitions = use(gameDefinitions); - const filters = useTankopediaPersistent((state) => state.filters); - - return ( - - - {TREE_TYPES.map((type, index) => ( - - ))} - - - {awaitedGameDefinitions.nations.map((nation, index) => ( - - ))} - - - {[...TANK_CLASSES].map((type, index) => ( - - ))} - - - {[...TIERS].map((tier, index) => ( - - ))} - {' '} - - - {[...TIERS].slice(0, 5).map((tier, index) => ( - - ))} - - - {[...TIERS].slice(5).map((tier, index) => ( - - ))} - - - - - {awaitedGameDefinitions.nations.slice(0, 5).map((nation, index) => ( - - ))} - - - - {awaitedGameDefinitions.nations.slice(5).map((nation, index) => ( - - ))} - - - - - - - - - mutateTankopediaPersistent((draft) => { - draft.filters.test = value as TankopediaTestTankDisplay; - }) - } - > - - Include - - - Exclude - - Only - - - - - ); -} diff --git a/src/app/tools/tankopedia/components/TankSearch/components/Results.tsx b/src/app/tools/tankopedia/components/TankSearch/components/Results.tsx deleted file mode 100644 index 80e924293..000000000 --- a/src/app/tools/tankopedia/components/TankSearch/components/Results.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { slateDark } from '@radix-ui/colors'; -import { Card, Flex, Inset, Link, Text } from '@radix-ui/themes'; -import { asset } from '../../../../../../core/blitzkit/asset'; -import { TankDefinition } from '../../../../../../core/blitzkit/tankDefinitions'; -import { - TANK_ICONS, - TANK_ICONS_COLLECTOR, - TANK_ICONS_PREMIUM, - TIER_ROMAN_NUMERALS, -} from '../../../../../../core/blitzkit/tankDefinitions/constants'; -import { tankIcon } from '../../../../../../core/blitzkit/tankIcon'; -import { theme } from '../../../../../../stitches.config'; -import * as styles from '../../../page.css'; -import { CompactSearchResultRow } from './CompactSearchResultRow'; - -interface ResultsProps { - compact?: boolean; - results: TankDefinition[]; - onSelect?: (tank: TankDefinition) => void; -} - -export function Results({ compact, results, onSelect }: ResultsProps) { - const firstChunk = compact - ? results.slice(0, Math.ceil(results.length / 2)) - : []; - const secondChunk = compact - ? results.slice(Math.ceil(results.length / 2)) - : []; - - return ( - 0 && secondChunk.length > 0 - ? 'space-around' - : 'center', - }} - > - {!compact && - results.map((tank) => ( - - - - {tank.name} - -
- -
- {tank.name} -
- - - - {tank.name} - - {tank.name} - - - - - {TIER_ROMAN_NUMERALS[tank.tier]} - - - - - - ))} - - {compact && firstChunk.length > 0 && ( - - )} - - {compact && secondChunk.length > 0 && ( - - )} - - ); -} diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index c0a0cbb4a..be04fb43e 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -1,19 +1,8 @@ -import { - CaretRightIcon, - MagnifyingGlassIcon, - TrashIcon, -} from '@radix-ui/react-icons'; -import { - AlertDialog, - Button, - Flex, - Spinner, - Text, - TextField, -} from '@radix-ui/themes'; +import { Callout, Flex, Text } from '@radix-ui/themes'; import { go } from 'fuzzysort'; -import { use, useEffect, useMemo, useRef, useState } from 'react'; -import { isMobileOnly } from 'react-device-detect'; +import { times } from 'lodash'; +import { memo, use, useMemo } from 'react'; +import { ExperimentIcon } from '../../../../../components/ExperimentIcon'; import { TANK_CLASSES } from '../../../../../components/Tanks/components/Item/constants'; import { resolveNearPenetration } from '../../../../../core/blitz/resolveNearPenetration'; import { gameDefinitions } from '../../../../../core/blitzkit/gameDefinitions'; @@ -23,527 +12,400 @@ import { resolveDpm } from '../../../../../core/blitzkit/resolveDpm'; import { resolveReload } from '../../../../../core/blitzkit/resolveReload'; import { TankDefinition, + tankDefinitions, + tankNames, tanksDefinitionsArray, } from '../../../../../core/blitzkit/tankDefinitions'; +import { tankopediaFilterTank } from '../../../../../core/blitzkit/tankopediaFilterTank'; import { unionBoundingBox } from '../../../../../core/blitzkit/unionBoundingBox'; -import mutateTankopediaPersistent, { - SORT_NAMES, - useTankopediaPersistent, -} from '../../../../../stores/tankopedia'; -import { PageTurner } from '../PageTurner'; -import { Sort } from '../Sort'; -import { Options } from './components/Options'; -import { Results } from './components/Results'; +import { SORT_NAMES, SORT_UNITS } from '../../../../../stores/tankopedia'; +import { useTankopediaFilters } from '../../../../../stores/tankopediaFilters'; +import { FilterControl } from '../FilterControl'; +import { NoResults } from '../NoResults'; +import { SearchBar } from '../SearchBar'; +import { SkeletonTankCard } from '../SkeletonTankCard'; +import { TankCard } from '../TankCard'; +import { TankCardWrapper } from '../TankCardWrapper'; import { treeTypeOrder } from './constants'; interface TankSearchProps { compact?: boolean; onSelect?: (tank: TankDefinition) => void; - onSelectAll?: (tanks: TankDefinition[]) => void; } -export function TankSearch({ - compact, - onSelect = () => {}, - onSelectAll, -}: TankSearchProps) { - const tanksPerPage = (compact ? 16 : 96) * (isMobileOnly ? 0.5 : 1); +export const TankSearch = memo(({ compact, onSelect }) => { const awaitedGameDefinitions = use(gameDefinitions); - const filters = useTankopediaPersistent((state) => state.filters); - const sort = useTankopediaPersistent((state) => state.sort); - const awaitedTanks = use(tanksDefinitionsArray); const awaitedModelDefinitions = use(modelDefinitions); - const [loaded, setLoaded] = useState(false); - const defaultSortedTanks = useMemo( - () => - awaitedTanks - .sort( - (a, b) => - (treeTypeOrder.indexOf(a.treeType) - - treeTypeOrder.indexOf(b.treeType)) * - (sort.direction === 'descending' ? -1 : 1), - ) - .sort( - (a, b) => - (TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class)) * - (sort.direction === 'descending' ? -1 : 1), - ) - .sort( - (a, b) => - (awaitedGameDefinitions.nations.indexOf(a.nation) - - awaitedGameDefinitions.nations.indexOf(b.nation)) * - (sort.direction === 'descending' ? -1 : 1), - ), - [], - ); - const input = useRef(null); - const searchableTanks = useMemo(() => { - const filtered = defaultSortedTanks.filter( - (tank) => - (filters.tiers.length === 0 - ? true - : filters.tiers.includes(tank.tier)) && - (filters.types.length === 0 - ? true - : filters.types.includes(tank.class)) && - (filters.treeTypes.length === 0 - ? true - : filters.treeTypes.includes(tank.treeType)) && - (filters.nations.length === 0 - ? true - : filters.nations.includes(tank.nation)) && - (filters.test === 'include' - ? true - : filters.test === 'exclude' - ? !tank.testing - : tank.testing), - ); - let sorted: TankDefinition[] = []; + const awaitedTankDefinitions = use(tankDefinitions); + const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); + const awaitedTankNames = use(tankNames); + const filters = useTankopediaFilters(); + const tanks = useMemo(() => { + if (filters.search === undefined) { + const filtered = awaitedTanksDefinitionsArray.filter((tank) => + tankopediaFilterTank(filters, tank), + ); + let sorted: TankDefinition[]; - switch (sort.by) { - case 'meta.none': - sorted = filtered.sort((a, b) => a.tier - b.tier); - break; + switch (filters.sort.by) { + case 'meta.none': + sorted = filtered + .sort( + (a, b) => + treeTypeOrder.indexOf(b.treeType) - + treeTypeOrder.indexOf(a.treeType), + ) + .sort( + (a, b) => + TANK_CLASSES.indexOf(b.class) - TANK_CLASSES.indexOf(a.class), + ) + .sort( + (a, b) => + awaitedGameDefinitions.nations.indexOf(b.nation) - + awaitedGameDefinitions.nations.indexOf(a.nation), + ); + break; - case 'survivability.health': { - sorted = filtered.sort((a, b) => { - const aHealth = a.health + a.turrets.at(-1)!.health; - const bHealth = b.health + b.turrets.at(-1)!.health; + case 'survivability.health': { + sorted = filtered.sort((a, b) => { + const aHealth = a.health + a.turrets.at(-1)!.health; + const bHealth = b.health + b.turrets.at(-1)!.health; - return aHealth - bHealth; - }); - break; - } + return aHealth - bHealth; + }); + break; + } - case 'survivability.viewRange': - sorted = filtered.sort( - (a, b) => a.turrets.at(-1)!.viewRange - b.turrets.at(-1)!.viewRange, - ); - break; - - case 'survivability.camouflageStill': - sorted = filtered.sort( - (a, b) => a.camouflage.still - b.camouflage.still, - ); - break; - - case 'survivability.camouflageMoving': - sorted = filtered.sort( - (a, b) => a.camouflage.moving - b.camouflage.moving, - ); - break; - - case 'survivability.camouflageShooting': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss - - b.turrets.at(-1)!.guns.at(-1)!.camouflageLoss, - ); - break; - - case 'survivability.volume': - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = awaitedModelDefinitions[a.id]; - const bTankModelDefinition = awaitedModelDefinitions[b.id]; - const aTurretModelDefinition = - aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; - const bTurretModelDefinition = - bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; - const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.boundingBox, - aTurretModelDefinition.boundingBox, - ), + case 'survivability.viewRange': + sorted = filtered.sort( + (a, b) => a.turrets.at(-1)!.viewRange - b.turrets.at(-1)!.viewRange, ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), + break; + + case 'survivability.camouflageStill': + sorted = filtered.sort( + (a, b) => a.camouflage.still - b.camouflage.still, ); - const aVolume = aSize[0] * aSize[1] * aSize[2]; - const bVolume = bSize[0] * bSize[1] * bSize[2]; + break; - return aVolume - bVolume; - }); - break; - - case 'survivability.length': - sorted = filtered.sort((a, b) => { - const aTankModelDefinition = awaitedModelDefinitions[a.id]; - const bTankModelDefinition = awaitedModelDefinitions[b.id]; - const aTurretModelDefinition = - aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; - const bTurretModelDefinition = - bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; - const aSize = normalizeBoundingBox( - unionBoundingBox( - aTankModelDefinition.boundingBox, - aTurretModelDefinition.boundingBox, - ), + case 'survivability.camouflageMoving': + sorted = filtered.sort( + (a, b) => a.camouflage.moving - b.camouflage.moving, ); - const bSize = normalizeBoundingBox( - unionBoundingBox( - bTankModelDefinition.boundingBox, - bTurretModelDefinition.boundingBox, - ), + break; + + case 'survivability.camouflageShooting': + sorted = filtered.sort( + (a, b) => + a.camouflage.still * + a.turrets.at(-1)!.guns.at(-1)!.camouflageLoss - + b.camouflage.still * + b.turrets.at(-1)!.guns.at(-1)!.camouflageLoss, ); - const aLength = Math.max(aSize[0], aSize[1], aSize[2]); - const bLength = Math.max(bSize[0], bSize[1], bSize[2]); + break; - return aLength - bLength; - }); - break; + case 'survivability.volume': + sorted = filtered.sort((a, b) => { + const aTankModelDefinition = awaitedModelDefinitions[a.id]; + const bTankModelDefinition = awaitedModelDefinitions[b.id]; + const aTurretModelDefinition = + aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; + const bTurretModelDefinition = + bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; + const aSize = normalizeBoundingBox( + unionBoundingBox( + aTankModelDefinition.boundingBox, + aTurretModelDefinition.boundingBox, + ), + ); + const bSize = normalizeBoundingBox( + unionBoundingBox( + bTankModelDefinition.boundingBox, + bTurretModelDefinition.boundingBox, + ), + ); + const aVolume = aSize[0] * aSize[1] * aSize[2]; + const bVolume = bSize[0] * bSize[1] * bSize[2]; - case 'fire.dpm': - sorted = filtered.sort( - (a, b) => - resolveDpm( - a.turrets.at(-1)!.guns.at(-1)!, - a.turrets.at(-1)!.guns.at(-1)!.shells[0], - ) - - resolveDpm( - b.turrets.at(-1)!.guns.at(-1)!, - b.turrets.at(-1)!.guns.at(-1)!.shells[0], - ), - ); - break; + return aVolume - bVolume; + }); + break; - case 'fire.reload': - sorted = filtered.sort( - (a, b) => - resolveReload(a.turrets.at(-1)!.guns.at(-1)!) - - resolveReload(b.turrets.at(-1)!.guns.at(-1)!), - ); - break; + case 'survivability.length': + sorted = filtered.sort((a, b) => { + const aTankModelDefinition = awaitedModelDefinitions[a.id]; + const bTankModelDefinition = awaitedModelDefinitions[b.id]; + const aTurretModelDefinition = + aTankModelDefinition.turrets[a.turrets.at(-1)!.id]; + const bTurretModelDefinition = + bTankModelDefinition.turrets[b.turrets.at(-1)!.id]; + const aSize = normalizeBoundingBox( + unionBoundingBox( + aTankModelDefinition.boundingBox, + aTurretModelDefinition.boundingBox, + ), + ); + const bSize = normalizeBoundingBox( + unionBoundingBox( + bTankModelDefinition.boundingBox, + bTurretModelDefinition.boundingBox, + ), + ); + const aLength = Math.max(aSize[0], aSize[1], aSize[2]); + const bLength = Math.max(bSize[0], bSize[1], bSize[2]); - case 'fire.caliber': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber, - ); - break; + return aLength - bLength; + }); + break; - case 'fire.standardPenetration': - sorted = filtered.sort( - (a, b) => - resolveNearPenetration( - a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, - ) - - resolveNearPenetration( - b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, - ), - ); - break; + case 'fire.dpm': + sorted = filtered.sort( + (a, b) => + resolveDpm( + a.turrets.at(-1)!.guns.at(-1)!, + a.turrets.at(-1)!.guns.at(-1)!.shells[0], + ) - + resolveDpm( + b.turrets.at(-1)!.guns.at(-1)!, + b.turrets.at(-1)!.guns.at(-1)!.shells[0], + ), + ); + break; - case 'fire.premiumPenetration': - sorted = filtered.sort( - (a, b) => - resolveNearPenetration( - ( - a.turrets.at(-1)!.guns.at(-1)!.shells[1] ?? - a.turrets.at(-1)!.guns.at(-1)!.shells[0] - ).penetration, - ) - - resolveNearPenetration( - ( - b.turrets.at(-1)!.guns.at(-1)!.shells[1] ?? - b.turrets.at(-1)!.guns.at(-1)!.shells[0] - ).penetration, - ), - ); - break; + case 'fire.reload': + sorted = filtered.sort( + (a, b) => + resolveReload(a.turrets.at(-1)!.guns.at(-1)!) - + resolveReload(b.turrets.at(-1)!.guns.at(-1)!), + ); + break; - case 'fire.damage': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor, - ); - break; + case 'fire.caliber': + sorted = filtered.sort( + (a, b) => + a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber - + b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber, + ); + break; - case 'fire.shellVelocity': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.shells[0].speed - - b.turrets.at(-1)!.guns.at(-1)!.shells[0].speed, - ); - break; + case 'fire.standardPenetration': + sorted = filtered.sort( + (a, b) => + resolveNearPenetration( + a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, + ) - + resolveNearPenetration( + b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration, + ), + ); + break; - case 'fire.aimTime': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.aimTime - - b.turrets.at(-1)!.guns.at(-1)!.aimTime, - ); - break; + case 'fire.premiumPenetration': + sorted = filtered.sort( + (a, b) => + resolveNearPenetration( + a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, + ) - + resolveNearPenetration( + b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration ?? 0, + ), + ); + break; - case 'fire.dispersionStill': - sorted = filtered.sort( - (a, b) => - a.turrets.at(-1)!.guns.at(-1)!.dispersion.base - - b.turrets.at(-1)!.guns.at(-1)!.dispersion.base, - ); - break; + case 'fire.damage': + sorted = filtered.sort( + (a, b) => + a.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor - + b.turrets.at(-1)!.guns.at(-1)!.shells[0].damage.armor, + ); + break; - case 'fire.dispersionMoving': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.dispersion.move - b.tracks.at(-1)!.dispersion.move, - ); - break; + case 'fire.shellVelocity': + sorted = filtered.sort( + (a, b) => + a.turrets.at(-1)!.guns.at(-1)!.shells[0].speed - + b.turrets.at(-1)!.guns.at(-1)!.shells[0].speed, + ); + break; - case 'fire.gunDepression': - sorted = filtered.sort( - (a, b) => - awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ - a.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.max - - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.max, - ); - break; + case 'fire.aimTime': + sorted = filtered.sort( + (a, b) => + a.turrets.at(-1)!.guns.at(-1)!.aimTime - + b.turrets.at(-1)!.guns.at(-1)!.aimTime, + ); + break; - case 'fire.gunElevation': - sorted = filtered.sort( - (a, b) => - // reversed because of negative values - awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ - b.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min - - awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ - a.turrets.at(-1)!.guns.at(-1)!.id - ].pitch.min, - ); - break; + case 'fire.dispersionStill': + sorted = filtered.sort( + (a, b) => + a.turrets.at(-1)!.guns.at(-1)!.dispersion.base - + b.turrets.at(-1)!.guns.at(-1)!.dispersion.base, + ); + break; - case 'maneuverability.forwardsSpeed': - sorted = filtered.sort((a, b) => a.speed.forwards - b.speed.forwards); - break; + case 'fire.dispersionMoving': + sorted = filtered.sort( + (a, b) => + a.tracks.at(-1)!.dispersion.move - + b.tracks.at(-1)!.dispersion.move, + ); + break; - case 'maneuverability.backwardsSpeed': - sorted = filtered.sort((a, b) => a.speed.backwards - b.speed.backwards); - break; + case 'fire.gunDepression': + sorted = filtered.sort( + (a, b) => + awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ + a.turrets.at(-1)!.guns.at(-1)!.id + ].pitch.max + + (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0) - + awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ + b.turrets.at(-1)!.guns.at(-1)!.id + ].pitch.max - + (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0), + ); + break; - case 'maneuverability.power': - sorted = filtered.sort( - (a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power, - ); - break; + case 'fire.gunElevation': + sorted = filtered.sort( + (a, b) => + awaitedModelDefinitions[b.id].turrets[b.turrets.at(-1)!.id].guns[ + b.turrets.at(-1)!.guns.at(-1)!.id + ].pitch.min + + (awaitedModelDefinitions[b.id].turretRotation?.pitch ?? 0) - + awaitedModelDefinitions[a.id].turrets[a.turrets.at(-1)!.id].guns[ + a.turrets.at(-1)!.guns.at(-1)!.id + ].pitch.min - + (awaitedModelDefinitions[a.id].turretRotation?.pitch ?? 0), + ); + break; - case 'maneuverability.powerToWeight': - sorted = filtered.sort( - (a, b) => - a.engines.at(-1)!.power / - (a.weight + - a.engines.at(-1)!.weight + - a.tracks.at(-1)!.weight + - a.turrets.at(-1)!.weight + - a.turrets.at(-1)!.guns.at(-1)!.weight) - - b.engines.at(-1)!.power / + case 'maneuverability.forwardsSpeed': + sorted = filtered.sort((a, b) => a.speed.forwards - b.speed.forwards); + break; + + case 'maneuverability.backwardsSpeed': + sorted = filtered.sort( + (a, b) => a.speed.backwards - b.speed.backwards, + ); + break; + + case 'maneuverability.power': + sorted = filtered.sort( + (a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power, + ); + break; + + case 'maneuverability.powerToWeight': + sorted = filtered.sort( + (a, b) => + a.engines.at(-1)!.power / + (a.weight + + a.engines.at(-1)!.weight + + a.tracks.at(-1)!.weight + + a.turrets.at(-1)!.weight + + a.turrets.at(-1)!.guns.at(-1)!.weight) - + b.engines.at(-1)!.power / + (b.weight + + b.engines.at(-1)!.weight + + b.tracks.at(-1)!.weight + + b.turrets.at(-1)!.weight + + b.turrets.at(-1)!.guns.at(-1)!.weight), + ); + break; + + case 'maneuverability.weight': + sorted = filtered.sort( + (a, b) => + a.weight + + a.engines.at(-1)!.weight + + a.tracks.at(-1)!.weight + + a.turrets.at(-1)!.weight + + a.turrets.at(-1)!.guns.at(-1)!.weight - (b.weight + b.engines.at(-1)!.weight + b.tracks.at(-1)!.weight + b.turrets.at(-1)!.weight + b.turrets.at(-1)!.guns.at(-1)!.weight), - ); - break; + ); + break; - case 'maneuverability.weight': - sorted = filtered.sort( - (a, b) => - a.weight + - a.engines.at(-1)!.weight + - a.tracks.at(-1)!.weight + - a.turrets.at(-1)!.weight + - a.turrets.at(-1)!.guns.at(-1)!.weight - - (b.weight + - b.engines.at(-1)!.weight + - b.tracks.at(-1)!.weight + - b.turrets.at(-1)!.weight + - b.turrets.at(-1)!.guns.at(-1)!.weight), - ); - break; + case 'maneuverability.traverseSpeed': + sorted = filtered.sort( + (a, b) => + a.tracks.at(-1)!.traverseSpeed - b.tracks.at(-1)!.traverseSpeed, + ); + break; + } - case 'maneuverability.traverseSpeed': - sorted = filtered.sort( - (a, b) => - a.tracks.at(-1)!.traverseSpeed - b.tracks.at(-1)!.traverseSpeed, - ); - break; - } - - return sort.direction === 'ascending' ? sorted : sorted.reverse(); - }, [ - sort, - ...Object.entries(filters) - .filter(([name]) => name !== 'page') - .map(([, value]) => value), - ]); - const [searchQuery, setSearchQuery] = useState(''); - const searchResults = useMemo( - () => - go(searchQuery, searchableTanks, { - keys: ['name', 'nameFull', 'id'] satisfies (keyof TankDefinition)[], - all: true, - }).map(({ obj }) => obj), - [searchQuery, searchableTanks], - ); - const page = useTankopediaPersistent((state) => state.filters.page); - const searchResultsPageSlice = searchResults.slice( - page * tanksPerPage, - (page + 1) * tanksPerPage, - ); - - useEffect(() => { - mutateTankopediaPersistent((draft) => { - draft.filters.page = Math.min( - Math.max(0, page), - Math.ceil(searchResults.length / tanksPerPage) - 1, + return filters.sort.direction === 'ascending' ? sorted : sorted.reverse(); + } else { + const searchedRaw = go(filters.search, awaitedTankNames, { + key: 'combined', + limit: 25, + }); + const searchedTanks = searchedRaw.map( + (result) => awaitedTankDefinitions[result.obj.id], ); - }); - }, [searchResults]); - - useEffect(() => { - setLoaded(true); - }, []); - - if (!loaded) { - return ( - - - - ); - } + return searchedTanks; + } + }, [filters]); return ( - <> - - - { - setSearchQuery(event.target.value); - }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - if ( - searchResults.length > 0 && - input.current && - input.current.value.length > 0 - ) { - onSelect(searchResults[0]); - } - } - }} - > - - - + + - {searchResults.length > 0 && - input.current && - input.current.value.length > 0 && ( - - - - )} - + {!filters.search && !filters.searching && ( + + )} - - - + {filters.sort.by !== 'meta.none' && ( + + + Sorting by {SORT_NAMES[filters.sort.by]} + {SORT_UNITS[filters.sort.by] === undefined + ? '' + : ` (${SORT_UNITS[filters.sort.by]})`} + , {filters.sort.direction} + - - - - - - - Sorting: {SORT_NAMES[sort.by]}, {sort.direction} - - - {onSelectAll && searchResults.length <= 8 && ( - - )} - {onSelectAll && searchResults.length > 8 && ( - - - - - - - - Woah! That's a lot of tanks. - - - Would you like to select{' '} - all {searchResults.length} tanks? - - - - - - - - onSelectAll(searchResults)}> - - - - - - )} - - - {!compact && ( - )} - - - {searchResultsPageSlice.length > tanksPerPage / 4 && ( - + {filters.testing === 'only' && ( + + + + + + + Tanks in testing are subject to change and many not represent the + final product. + + + )} - + + {!filters.searching && ( + <> + {tanks.length > 0 && ( + + {tanks.map((tank) => ( + + ))} + + )} + + {tanks.length === 0 && } + + )} + + {filters.searching && ( + + {times(Math.round(10 + 10 * Math.random()), (index) => ( + + ))} + + )} + ); -} +}); diff --git a/src/app/tools/tankopedia/page.tsx b/src/app/tools/tankopedia/page.tsx index f27557f56..cd052867f 100644 --- a/src/app/tools/tankopedia/page.tsx +++ b/src/app/tools/tankopedia/page.tsx @@ -1,12 +1,12 @@ 'use client'; import PageWrapper from '../../../components/PageWrapper'; -import { Results } from './components/Results'; +import { TankSearch } from './components/TankSearch'; export default function Page() { return ( - + ); }