feat: stat search

This commit is contained in:
tresabhi committed 2025-04-24 22:04:33 -05:00
1 parent c1000e1608
commit e05358952a
7 files changed
+207 -5

No files matched your search

+2
View File
@@ -855,6 +855,8 @@
"tools": {
"tankopedia": {
"search": "Search a stat...",
"name": "Tankopedia",
"button": "Find tanks",
"description": "Blitz tank encyclopedia",
@@ -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);
}
@@ -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 (
<mark
className="mark"
style={{
borderRadius: Var('radius-1'),
...style,
}}
{...props}
/>
);
}
@@ -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<HTMLDivElement>();
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<HTMLDivElement>(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) => (
<Mark key={index}>{match}</Mark>
));
highlighted = true;
}
}
useEffect(() => {
if (!container.current) return;
if (highlighted) {
highlightedRows.add(container.current);
highlightedRowsUpdate.dispatch();
}
return () => {
highlightedRows.delete(container.current!);
highlightedRowsUpdate.dispatch();
};
});
return (
<Flex align="center" pl={indent ? '2' : '0'} width="100%" gap="4">
<Text color="gray">{name}</Text>
<Flex
align="center"
pl={indent ? '2' : '0'}
width="100%"
gap="4"
ref={container}
>
<Text color="gray">{label}</Text>
<Box flexGrow="1" />
@@ -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<HTMLInputElement>(null);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const count = useRef<HTMLDivElement>(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 (
<Flex flexGrow="1" maxWidth="20rem" width="100%" {...props}>
<TextField.Root
ref={input}
placeholder={strings.website.tools.tankopedia.search}
variant="classic"
style={{ width: '100%' }}
onChange={search}
onKeyDown={(event) => {
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);
}
}}
>
<TextField.Slot>
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
</TextField.Slot>
<TextField.Slot ref={count} />
</TextField.Root>
</Flex>
);
}
@@ -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 (
<Flex direction="column" gap="3">
<CompareOptions display={{ initial: 'none', sm: 'flex' }} />
<Flex
display={{ initial: 'none', sm: 'flex' }}
direction={{ initial: 'column', md: 'row' }}
align="center"
justify="center"
mb="8"
gap={{ initial: '4', md: '8' }}
>
<CompareOptions />
<StatSearch />
</Flex>
<Flex mt="4" px="4" style={{ boxSizing: 'border-box' }} gap="6">
<Flex
@@ -41,7 +52,18 @@ export function CharacteristicsSection() {
align={{ initial: 'stretch', sm: 'center' }}
width={{ initial: '100%', sm: 'auto' }}
>
<CompareOptions display={{ initial: 'flex', sm: 'none' }} />
<Flex
display={{ initial: 'flex', sm: 'none' }}
direction="column"
align="center"
justify="center"
mb="8"
gap="8"
>
<StatSearch />
<CompareOptions />
</Flex>
<Characteristics />
</Flex>
</Flex>
@@ -122,6 +122,7 @@ interface TankopediaEphemeral {
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
customShell?: ShellDefinition;
display: TankopediaDisplay;
statSearch?: string;
}
const skillDefinitions = await awaitableSkillDefinitions;