feat: search

This commit is contained in:
TrèsAbhi committed 2026-04-27 19:57:44 -05:00
1 parent 385cc3b6d2
commit d6e37119a8
12 files changed
+98 -17

No files matched your search

+1
View File
@@ -67,6 +67,7 @@
"DEBUFF", "DEBUFF",
"debugflags", "debugflags",
"deburr", "deburr",
"deburred",
"decalmask", "decalmask",
"decompressor", "decompressor",
"décor", "décor",
+6
View File
@@ -121,5 +121,11 @@
"no_docs": "This method has no documentation.", "no_docs": "This method has no documentation.",
"test": "Test", "test": "Test",
"view_full": "View full types" "view_full": "View full types"
},
"tanks": {
"search": {
"placeholder": "Search tanks..."
}
} }
} }
@@ -43,13 +43,14 @@ import "@fontsource-variable/roboto-flex";
--weight-bold: 700; --weight-bold: 700;
--weight-black: 900; --weight-black: 900;
--outline-interactive: var(--space-1) solid var(--gray-4);
--outline-focus: var(--space-1) solid var(--accent-6); --outline-focus: var(--space-1) solid var(--accent-6);
--shadow-cavity: /* --shadow-cavity:
inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg)) inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg))
var(--space-1) var(--black-a3), var(--space-1) var(--black-a3),
inset calc(var(--space-1) * sin(45deg) * -1) inset calc(var(--space-1) * sin(45deg) * -1)
calc(var(--space-1) * sin(45deg) * -1) var(--space-1) var(--white-a1); calc(var(--space-1) * sin(45deg) * -1) var(--space-1) var(--white-a1); */
--shadow-bump: --shadow-bump:
inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg)) inset calc(var(--space-1) * sin(45deg)) calc(var(--space-1) * sin(45deg))
@@ -60,7 +61,7 @@ import "@fontsource-variable/roboto-flex";
--shadow-outline-width: 1.5px; --shadow-outline-width: 1.5px;
--shadow-outline-width-negative: calc(var(--shadow-outline-width) * -1); --shadow-outline-width-negative: calc(var(--shadow-outline-width) * -1);
--shadow-interactive-background: 0 0 0 var(--shadow-outline-width) /* --shadow-interactive-background: 0 0 0 var(--shadow-outline-width)
var(--gray-6); var(--gray-6);
--shadow-interactive-shine: var(--shadow-outline-width-negative) --shadow-interactive-shine: var(--shadow-outline-width-negative)
var(--shadow-outline-width-negative) var(--shadow-outline-width) var(--shadow-outline-width-negative) var(--shadow-outline-width)
@@ -69,7 +70,7 @@ import "@fontsource-variable/roboto-flex";
var(--shadow-outline-width) var(--shadow-outline-width) var(--black-a4); var(--shadow-outline-width) var(--shadow-outline-width) var(--black-a4);
--shadow-interactive: --shadow-interactive:
var(--shadow-interactive-shine), var(--shadow-interactive-shadow), var(--shadow-interactive-shine), var(--shadow-interactive-shadow),
var(--shadow-interactive-background); var(--shadow-interactive-background); */
--shadow-aggressive: var(--space-1) var(--space-1) var(--space-4) --shadow-aggressive: var(--space-1) var(--space-1) var(--space-4)
var(--black-a11); var(--black-a11);
@@ -6,7 +6,6 @@
.content { .content {
flex: 1; flex: 1;
max-width: var(--page-width); max-width: var(--page-width);
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -0,0 +1,28 @@
import { useStrings } from "../hooks/useStrings";
import { TankFilters } from "../stores/tankFilters";
import { TextField } from "./TextField";
export function TankSearch() {
const strings = useStrings();
return (
<TextField
placeholder={strings.tanks.search.placeholder}
onChange={(event) => {
const trimmed = event.target.value.trim();
if (trimmed.length === 0) {
TankFilters.mutate((draft) => {
draft.search = undefined;
});
return;
}
TankFilters.mutate((draft) => {
draft.search = trimmed;
});
}}
/>
);
}
@@ -14,7 +14,7 @@
border-radius: var(--max-size); border-radius: var(--max-size);
background-color: var(--gray-1); background-color: var(--gray-1);
box-shadow: var(--shadow-interactive), var(--shadow-cavity); outline: var(--outline-interactive);
input { input {
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
@@ -8,7 +8,7 @@ import {
type ReactNode, type ReactNode,
} from "react"; } from "react";
import { classNames } from "../../core/ui/classNames"; import { classNames } from "../../core/ui/classNames";
import "./index.css"; import styles from "./index.module.css";
export interface TextFieldProps extends ComponentProps<"input"> { export interface TextFieldProps extends ComponentProps<"input"> {
children?: ReactNode; children?: ReactNode;
@@ -51,7 +51,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
return ( return (
<div <div
{...props} {...props}
className={classNames("text-field", className)} className={classNames(styles["text-field"], className)}
data-has-icon={children !== undefined} data-has-icon={children !== undefined}
data-disabled={disabled} data-disabled={disabled}
data-variant={variant} data-variant={variant}
@@ -0,0 +1,17 @@
import { deburr } from "lodash-es";
import { api } from "../core/api/dynamic";
import { useAwait } from "./useAwait";
import { useGameStrings } from "./useGameStrings";
export function useSearchableTanks() {
const gameStrings = useGameStrings("TankEntity");
const tanks = useAwait(() => api.tanks(), "tanks");
return Object.values(tanks.tanks).map((tank) => {
const { id } = tank;
const name = gameStrings[tank.tank!.name!.value];
const deburred = deburr(name);
return { id, name, deburred };
});
}
@@ -1,10 +1,16 @@
import fuzzysort from "fuzzysort";
import { useMemo } from "react"; import { useMemo } from "react";
import { TankCard } from "../../../components/TankCard"; import { TankCard } from "../../../components/TankCard";
import { TankCards } from "../../../components/TankCards"; import { TankCards } from "../../../components/TankCards";
import { TankSearch } from "../../../components/TankSearch";
import { api } from "../../../core/api/dynamic"; import { api } from "../../../core/api/dynamic";
import { useAwait } from "../../../hooks/useAwait"; import { useAwait } from "../../../hooks/useAwait";
import { LocaleProvider } from "../../../hooks/useLocale"; import { LocaleProvider } from "../../../hooks/useLocale";
import { useSearchableTanks } from "../../../hooks/useSearchableTanks";
import { useTierCompare } from "../../../hooks/useTierCompare"; import { useTierCompare } from "../../../hooks/useTierCompare";
import { TankFilters } from "../../../stores/tankFilters";
const { go } = fuzzysort;
interface PageProps { interface PageProps {
locale: string; locale: string;
@@ -20,7 +26,11 @@ export function Page({ locale, ...props }: PageProps) {
function Content() { function Content() {
const tanks = useAwait(() => api.tanks(), "tanks"); const tanks = useAwait(() => api.tanks(), "tanks");
const compareTiers = useTierCompare(); const compareTiers = useTierCompare();
const searchableTanks = useSearchableTanks();
const search = TankFilters.use((state) => state.search);
const tanksOrdered = useMemo(() => { const tanksOrdered = useMemo(() => {
let list = Object.values(tanks.tanks); let list = Object.values(tanks.tanks);
@@ -30,12 +40,24 @@ function Content() {
return list; return list;
}, []); }, []);
const tanksFiltered = useMemo(() => {
if (!search) return tanksOrdered;
const results = go(search, searchableTanks, {
keys: ["id", "name", "deburred"],
});
return results.map(({ obj }) => obj);
}, [search]);
return ( return (
<TankCards> <>
{tanksOrdered.slice(0, 32).map(({ id }) => ( <TankSearch />
<TankCard key={id} id={id} /> <TankCards>
))} {tanksFiltered.slice(0, 32).map(({ id }) => (
</TankCards> <TankCard key={id} id={id} />
))}
</TankCards>
</>
); );
} }
@@ -2,7 +2,6 @@
import "@radix-ui/colors/violet-dark.css"; import "@radix-ui/colors/violet-dark.css";
import { Section } from "../../../components/Section"; import { Section } from "../../../components/Section";
import ToolBanner from "../../../components/ToolBanner.astro"; import ToolBanner from "../../../components/ToolBanner.astro";
import { api } from "../../../core/api/dynamic";
import { tools } from "../../../core/tools/tools"; import { tools } from "../../../core/tools/tools";
import BaseLayout from "../../../layouts/BaseLayout.astro"; import BaseLayout from "../../../layouts/BaseLayout.astro";
import { Page } from "./_index"; import { Page } from "./_index";
@@ -10,7 +9,6 @@ import { Page } from "./_index";
export { getStaticPaths } from "../index.astro"; export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props; const { locale } = Astro.props;
const tankList = await api.tankList();
--- ---
<BaseLayout {locale} accent="purple"> <BaseLayout {locale} accent="purple">
@@ -0,0 +1,9 @@
import { Varuna } from "varuna";
interface TankFilters {
search?: string;
}
export const TankFilters = new Varuna<TankFilters>({
search: undefined,
});
+2 -2
View File
@@ -1,4 +1,4 @@
import type { TankCatalogComponent } from "@protos/blitz_static_tank_component"; import type { TankCatalogComponent } from "@protos/game/proto/legacy/blitz_static_tank_component";
import { Varuna } from "varuna"; import { Varuna } from "varuna";
import { createTankState } from "../core/tankopedia/createTankState"; import { createTankState } from "../core/tankopedia/createTankState";
import { type TankState } from "../core/tankopedia/tankState"; import { type TankState } from "../core/tankopedia/tankState";
@@ -18,5 +18,5 @@ export const Tankopedia = new Varuna<Tankopedia, TankCatalogComponent>(
(tank) => ({ (tank) => ({
compare: TankopediaCompare.TierAndClass, compare: TankopediaCompare.TierAndClass,
protagonist: createTankState(tank), protagonist: createTankState(tank),
}) }),
); );