diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 27d7e1b20..88dd85bbe 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -855,6 +855,8 @@ "tools": { "tankopedia": { + "search": "Search a stat...", + "name": "Tankopedia", "button": "Find tanks", "description": "Blitz tank encyclopedia", diff --git a/packages/website/src/components/Mark/index.css b/packages/website/src/components/Mark/index.css new file mode 100644 index 000000000..50564f725 --- /dev/null +++ b/packages/website/src/components/Mark/index.css @@ -0,0 +1,13 @@ +.mark { + color: var(--gray-12); + background-color: var(--purple-a9); + outline: 4px solid var(--purple-a4); + box-shadow: 0 0 1rem var(--purple-9); +} + +.focused .mark { + color: var(--gray-1); + background-color: var(--amber-a9); + outline: 4px solid var(--amber-a4); + box-shadow: 0 0 1rem var(--amber-9); +} diff --git a/packages/website/src/components/Mark/index.tsx b/packages/website/src/components/Mark/index.tsx new file mode 100644 index 000000000..c097675ec --- /dev/null +++ b/packages/website/src/components/Mark/index.tsx @@ -0,0 +1,16 @@ +import type { ComponentProps } from 'react'; +import { Var } from '../../core/radix/var'; +import './index.css'; + +export function Mark({ style, ...props }: ComponentProps<'mark'>) { + return ( + + ); +} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Info.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Info.tsx index ce051f3a6..5335a0676 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Info.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Info.tsx @@ -1,6 +1,13 @@ import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons'; import { Box, Flex, Text } from '@radix-ui/themes'; -import type { ReactNode } from 'react'; +import fuzzysort from 'fuzzysort'; +import { Quicklime } from 'quicklime'; +import { useEffect, useRef, type ReactNode } from 'react'; +import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; +import { Mark } from '../../../../../Mark'; + +export const highlightedRows = new Set(); +export const highlightedRowsUpdate = new Quicklime(); export interface InfoProps { name: ReactNode; @@ -21,9 +28,46 @@ export function Info({ prefix, deltaType = 'higherIsBetter', }: InfoProps) { + const statSearch = TankopediaEphemeral.use((state) => state.statSearch); + const container = useRef(null); + + let label = name; + let highlighted = false; + + if (statSearch !== undefined && typeof name === 'string') { + const result = fuzzysort.single(statSearch, name); + + if (result) { + label = fuzzysort.highlight(result, (match, index) => ( + {match} + )); + highlighted = true; + } + } + + useEffect(() => { + if (!container.current) return; + + if (highlighted) { + highlightedRows.add(container.current); + highlightedRowsUpdate.dispatch(); + } + + return () => { + highlightedRows.delete(container.current!); + highlightedRowsUpdate.dispatch(); + }; + }); + return ( - - {name} + + {label} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatSearch.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatSearch.tsx new file mode 100644 index 000000000..e6a0c4ad5 --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatSearch.tsx @@ -0,0 +1,104 @@ +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { Flex, Spinner, TextField, type FlexProps } from '@radix-ui/themes'; +import { debounce } from 'lodash-es'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useLocale } from '../../../../../../hooks/useLocale'; +import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; +import { highlightedRows, highlightedRowsUpdate } from './Info'; + +export function StatSearch(props: FlexProps) { + const { strings } = useLocale(); + const [searching, setSearching] = useState(false); + const input = useRef(null); + const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); + const count = useRef(null); + const [index, setIndex] = useState(0); + const search = useCallback(() => { + if (!input.current || !count.current) return; + + if (input.current.value.length > 0) { + setSearching(true); + } else { + setSearching(false); + count.current.textContent = ''; + } + + requestUpdate(); + setIndex(0); + }, []); + const requestUpdate = useCallback( + debounce(() => { + if (!input.current || !count.current) return; + + if (input.current.value.length === 0) { + count.current.textContent = ''; + + setSearching(false); + mutateTankopediaEphemeral((draft) => { + draft.statSearch = undefined; + }); + return; + } + + setSearching(false); + mutateTankopediaEphemeral((draft) => { + draft.statSearch = input.current!.value; + }); + }, 500), + [], + ); + + useEffect(() => { + const handleHighlightedRowsUpdate = () => { + if (!count.current || !input.current) return; + + count.current.textContent = + input.current.value.length === 0 ? '' : `0 / ${highlightedRows.size}`; + }; + + highlightedRowsUpdate.on(handleHighlightedRowsUpdate); + + return () => { + highlightedRowsUpdate.off(handleHighlightedRowsUpdate); + }; + }, []); + + return ( + + { + if (!count.current) return; + + if (event.key === 'Enter') { + const newIndex = (index + 1) % highlightedRows.size; + count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`; + const array = [...highlightedRows]; + + array.forEach((element) => element.classList.remove('focused')); + + if (array[newIndex]) { + array[newIndex].classList.add('focused'); + array[newIndex].scrollIntoView({ + block: 'center', + inline: 'center', + }); + } + + setIndex((index) => index + 1); + } + }} + > + + {searching ? : } + + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/index.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/index.tsx index 41c72c0af..e2877e860 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/index.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/index.tsx @@ -1,6 +1,7 @@ import { Flex } from '@radix-ui/themes'; import { Characteristics } from './components/Characteristics'; import { CompareOptions } from './components/Characteristics/components/CompareOptions'; +import { StatSearch } from './components/Characteristics/components/StatSearch'; import { Consumables } from './components/Consumables'; import { Equipment } from './components/Equipment'; import { Miscellaneous } from './components/Miscellaneous'; @@ -11,7 +12,17 @@ import { Skills } from './components/Skills'; export function CharacteristicsSection() { return ( - + + + + - + + + + + diff --git a/packages/website/src/stores/tankopediaEphemeral.ts b/packages/website/src/stores/tankopediaEphemeral.ts index 5b6081960..58e613d9d 100644 --- a/packages/website/src/stores/tankopediaEphemeral.ts +++ b/packages/website/src/stores/tankopediaEphemeral.ts @@ -122,6 +122,7 @@ interface TankopediaEphemeral { xpMultiplier: (typeof XP_MULTIPLIERS)[number]; customShell?: ShellDefinition; display: TankopediaDisplay; + statSearch?: string; } const skillDefinitions = await awaitableSkillDefinitions;