mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-09 12:46:10 -05:00
Allow using mode shorthands to filter map pool search results
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { mapSearchFilter } from "./BracketMapListDialog";
|
||||
|
||||
const contains = (string: string, substring: string) =>
|
||||
string.toLowerCase().includes(substring.toLowerCase());
|
||||
|
||||
const filter = (textValue: string, inputValue: string) =>
|
||||
mapSearchFilter(textValue, inputValue, contains);
|
||||
|
||||
describe("mapSearchFilter", () => {
|
||||
test("matches everything for empty input", () => {
|
||||
expect(filter("SZ Hagglefish Market", "")).toBe(true);
|
||||
expect(filter("TC Crableg Capital", " ")).toBe(true);
|
||||
});
|
||||
|
||||
test("matches against stage name when no mode prefix is given", () => {
|
||||
expect(filter("SZ Crableg Capital", "crab")).toBe(true);
|
||||
expect(filter("TC Crableg Capital", "crab")).toBe(true);
|
||||
expect(filter("SZ Hagglefish Market", "crab")).toBe(false);
|
||||
});
|
||||
|
||||
test("mode prefix alone matches all maps of that mode", () => {
|
||||
expect(filter("SZ Hagglefish Market", "sz")).toBe(true);
|
||||
expect(filter("SZ Crableg Capital", "sz")).toBe(true);
|
||||
expect(filter("TC Crableg Capital", "sz")).toBe(false);
|
||||
});
|
||||
|
||||
test("mode prefix is case-insensitive", () => {
|
||||
expect(filter("SZ Hagglefish Market", "Sz")).toBe(true);
|
||||
expect(filter("SZ Hagglefish Market", "SZ")).toBe(true);
|
||||
});
|
||||
|
||||
test("trailing whitespace after a mode prefix still matches all of that mode", () => {
|
||||
expect(filter("SZ Hagglefish Market", "sz ")).toBe(true);
|
||||
expect(filter("TC Hagglefish Market", "sz ")).toBe(false);
|
||||
});
|
||||
|
||||
test("mode prefix combined with a query filters within that mode", () => {
|
||||
expect(filter("SZ Crableg Capital", "sz crab")).toBe(true);
|
||||
expect(filter("SZ Hagglefish Market", "sz crab")).toBe(false);
|
||||
expect(filter("TC Crableg Capital", "sz crab")).toBe(false);
|
||||
});
|
||||
|
||||
test("query after mode prefix is matched against the stage name", () => {
|
||||
expect(filter("RM Museum d'Alfonsino", "rm museum")).toBe(true);
|
||||
expect(filter("RM Museum d'Alfonsino", "rm market")).toBe(false);
|
||||
});
|
||||
|
||||
test("non-mode first word falls back to stage name search", () => {
|
||||
expect(filter("SZ Hagglefish Market", "hagg")).toBe(true);
|
||||
expect(filter("SZ Hagglefish Market", "market")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Unlink,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useFilter } from "react-aria-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type FetcherWithComponents, Link, useFetcher } from "react-router";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
@@ -1072,6 +1073,7 @@ function MapListRow({
|
||||
onMapChange: (map: NonNullable<TournamentRoundMaps["list"]>[number]) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["common", "game-misc"]);
|
||||
const { contains } = useFilter({ sensitivity: "base" });
|
||||
const tournament = useTournament();
|
||||
|
||||
const items = modesShort.flatMap((mode) => {
|
||||
@@ -1118,13 +1120,20 @@ function MapListRow({
|
||||
search={{
|
||||
placeholder: t("common:forms.stageSearch.search.placeholder"),
|
||||
}}
|
||||
filter={(textValue, inputValue) =>
|
||||
mapSearchFilter(textValue, inputValue, contains)
|
||||
}
|
||||
className={styles.mapRowSelect}
|
||||
popoverClassName={styles.mapRowSelectPopover}
|
||||
>
|
||||
{(group) => (
|
||||
<SendouSelectItemSection key={group.key} heading={group.modeLabel}>
|
||||
{group.maps.map((m) => (
|
||||
<SendouSelectItem key={m.id} id={m.id} textValue={m.name}>
|
||||
<SendouSelectItem
|
||||
key={m.id}
|
||||
id={m.id}
|
||||
textValue={`${m.mode} ${m.name}`}
|
||||
>
|
||||
<div className={styles.mapSelectItem}>
|
||||
<ModeImage mode={m.mode} size={20} />
|
||||
<StageImage
|
||||
@@ -1143,6 +1152,51 @@ function MapListRow({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for the map search input that supports an optional game mode prefix.
|
||||
*
|
||||
* - `"sz"` matches every Splat Zones map
|
||||
* - `"sz crab"` matches Splat Zones maps whose stage name contains "crab"
|
||||
* - input without a recognized mode prefix matches against the stage name as before
|
||||
*
|
||||
* Expects `textValue` to be formatted as `"${modeShort} ${stageName}"`.
|
||||
*/
|
||||
export function mapSearchFilter(
|
||||
textValue: string,
|
||||
inputValue: string,
|
||||
contains: (string: string, substring: string) => boolean,
|
||||
) {
|
||||
const separatorIndex = textValue.indexOf(" ");
|
||||
const mode = textValue.slice(0, separatorIndex);
|
||||
const stageName = textValue.slice(separatorIndex + 1);
|
||||
|
||||
const trimmedInput = inputValue.trim();
|
||||
const firstSpaceIndex = trimmedInput.indexOf(" ");
|
||||
const firstWord =
|
||||
firstSpaceIndex === -1
|
||||
? trimmedInput
|
||||
: trimmedInput.slice(0, firstSpaceIndex);
|
||||
|
||||
const matchedMode = modesShort.find(
|
||||
(m) => m.toLowerCase() === firstWord.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!matchedMode) {
|
||||
return contains(stageName, trimmedInput);
|
||||
}
|
||||
|
||||
if (mode !== matchedMode) return false;
|
||||
|
||||
const restQuery =
|
||||
firstSpaceIndex === -1
|
||||
? ""
|
||||
: trimmedInput.slice(firstSpaceIndex + 1).trim();
|
||||
|
||||
if (restQuery === "") return true;
|
||||
|
||||
return contains(stageName, restQuery);
|
||||
}
|
||||
|
||||
function MysteryRow({
|
||||
number,
|
||||
isCounterpicks,
|
||||
|
||||
Reference in New Issue
Block a user