From e05358952a04045adb217bfc3bc2f78e90799e50 Mon Sep 17 00:00:00 2001
From: tresabhi <43380238+tresabhi@users.noreply.github.com>
Date: Thu, 24 Apr 2025 22:04:33 -0500
Subject: [PATCH] feat: stat search
---
packages/i18n/strings/en.json | 2 +
.../website/src/components/Mark/index.css | 13 +++
.../website/src/components/Mark/index.tsx | 16 +++
.../Characteristics/components/Info.tsx | 50 ++++++++-
.../Characteristics/components/StatSearch.tsx | 104 ++++++++++++++++++
.../CharacteristicsSection/index.tsx | 26 ++++-
.../website/src/stores/tankopediaEphemeral.ts | 1 +
7 files changed, 207 insertions(+), 5 deletions(-)
create mode 100644 packages/website/src/components/Mark/index.css
create mode 100644 packages/website/src/components/Mark/index.tsx
create mode 100644 packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/StatSearch.tsx
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;