mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: stat search
This commit is contained in:
1 parent
c1000e1608
commit
e05358952a
7 files changed
+207
-5
No files matched your search
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+47
-3
@@ -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" />
|
||||
|
||||
|
||||
+104
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user