mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +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": {
|
"tools": {
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
|
"search": "Search a stat...",
|
||||||
|
|
||||||
"name": "Tankopedia",
|
"name": "Tankopedia",
|
||||||
"button": "Find tanks",
|
"button": "Find tanks",
|
||||||
"description": "Blitz tank encyclopedia",
|
"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 { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
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 {
|
export interface InfoProps {
|
||||||
name: ReactNode;
|
name: ReactNode;
|
||||||
@@ -21,9 +28,46 @@ export function Info({
|
|||||||
prefix,
|
prefix,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = 'higherIsBetter',
|
||||||
}: InfoProps) {
|
}: 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 (
|
return (
|
||||||
<Flex align="center" pl={indent ? '2' : '0'} width="100%" gap="4">
|
<Flex
|
||||||
<Text color="gray">{name}</Text>
|
align="center"
|
||||||
|
pl={indent ? '2' : '0'}
|
||||||
|
width="100%"
|
||||||
|
gap="4"
|
||||||
|
ref={container}
|
||||||
|
>
|
||||||
|
<Text color="gray">{label}</Text>
|
||||||
|
|
||||||
<Box flexGrow="1" />
|
<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 { Flex } from '@radix-ui/themes';
|
||||||
import { Characteristics } from './components/Characteristics';
|
import { Characteristics } from './components/Characteristics';
|
||||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||||
|
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||||
import { Consumables } from './components/Consumables';
|
import { Consumables } from './components/Consumables';
|
||||||
import { Equipment } from './components/Equipment';
|
import { Equipment } from './components/Equipment';
|
||||||
import { Miscellaneous } from './components/Miscellaneous';
|
import { Miscellaneous } from './components/Miscellaneous';
|
||||||
@@ -11,7 +12,17 @@ import { Skills } from './components/Skills';
|
|||||||
export function CharacteristicsSection() {
|
export function CharacteristicsSection() {
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3">
|
<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 mt="4" px="4" style={{ boxSizing: 'border-box' }} gap="6">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -41,7 +52,18 @@ export function CharacteristicsSection() {
|
|||||||
align={{ initial: 'stretch', sm: 'center' }}
|
align={{ initial: 'stretch', sm: 'center' }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
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 />
|
<Characteristics />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ interface TankopediaEphemeral {
|
|||||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||||
customShell?: ShellDefinition;
|
customShell?: ShellDefinition;
|
||||||
display: TankopediaDisplay;
|
display: TankopediaDisplay;
|
||||||
|
statSearch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const skillDefinitions = await awaitableSkillDefinitions;
|
const skillDefinitions = await awaitableSkillDefinitions;
|
||||||
|
|||||||
Reference in new issue
Block a user