mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-06 05:07:36 -05:00
Refine WeaponSearch
This commit is contained in:
parent
8d494c856e
commit
19a82082e9
|
|
@ -19,6 +19,7 @@ import { Image } from "~/components/Image";
|
|||
import { Input } from "~/components/Input";
|
||||
import type { SearchLoaderData } from "~/features/search/routes/search";
|
||||
import {
|
||||
mySlugify,
|
||||
navIconUrl,
|
||||
teamPage,
|
||||
tournamentOrganizationPage,
|
||||
|
|
@ -28,8 +29,11 @@ import {
|
|||
import styles from "./CommandPalette.module.css";
|
||||
import {
|
||||
filterWeaponResults,
|
||||
getRecentWeapons,
|
||||
getWeaponDestinationUrl,
|
||||
type SelectedWeapon,
|
||||
saveRecentWeapon,
|
||||
type WeaponDestination,
|
||||
WeaponDestinationMenu,
|
||||
WeaponResultsList,
|
||||
} from "./WeaponSearch";
|
||||
|
|
@ -165,8 +169,10 @@ function CommandPaletteContent({
|
|||
const fetcher = useFetcher<SearchLoaderData>();
|
||||
|
||||
React.useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
if (!selectedWeapon) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [selectedWeapon]);
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
|
|
@ -194,9 +200,19 @@ function CommandPaletteContent({
|
|||
const weaponResults =
|
||||
searchType === "weapons" ? filterWeaponResults(query, t) : [];
|
||||
|
||||
const recentWeapons: SelectedWeapon[] =
|
||||
searchType === "weapons"
|
||||
? getRecentWeapons().map((id) => {
|
||||
const name = t(`weapons:MAIN_${id}`);
|
||||
return { id, name, slug: mySlugify(name) };
|
||||
})
|
||||
: [];
|
||||
|
||||
const handleSelect = (key: React.Key) => {
|
||||
if (searchType === "weapons") {
|
||||
const weapon = weaponResults.find((w) => `weapon-${w.id}` === key);
|
||||
const weapon =
|
||||
weaponResults.find((w) => `weapon-${w.id}` === key) ??
|
||||
recentWeapons.find((w) => `weapon-${w.id}` === key);
|
||||
if (weapon) {
|
||||
setSelectedWeapon(weapon);
|
||||
setQuery("");
|
||||
|
|
@ -227,11 +243,13 @@ function CommandPaletteContent({
|
|||
const handleDestinationSelect = (key: React.Key) => {
|
||||
if (!selectedWeapon) return;
|
||||
|
||||
const url = getWeaponDestinationUrl(key as string, selectedWeapon);
|
||||
if (url) {
|
||||
navigate(url);
|
||||
onClose();
|
||||
}
|
||||
const url = getWeaponDestinationUrl(
|
||||
key as WeaponDestination,
|
||||
selectedWeapon,
|
||||
);
|
||||
saveRecentWeapon(selectedWeapon.id);
|
||||
navigate(url);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleBackToWeaponSearch = () => {
|
||||
|
|
@ -293,6 +311,7 @@ function CommandPaletteContent({
|
|||
{searchType === "weapons" ? (
|
||||
<WeaponResultsList
|
||||
weaponResults={weaponResults}
|
||||
recentWeapons={recentWeapons}
|
||||
onSelect={handleSelect}
|
||||
hasQuery={hasQuery}
|
||||
listBoxRef={listBoxRef}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,17 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import styles from "./CommandPalette.module.css";
|
||||
|
||||
const WEAPON_DESTINATIONS = [
|
||||
"builds",
|
||||
"popular",
|
||||
"stats",
|
||||
"analyzer",
|
||||
"vods",
|
||||
"art",
|
||||
"lfg",
|
||||
] as const;
|
||||
export type WeaponDestination = (typeof WEAPON_DESTINATIONS)[number];
|
||||
|
||||
export interface SelectedWeapon {
|
||||
id: MainWeaponId;
|
||||
name: string;
|
||||
|
|
@ -70,10 +81,10 @@ export function filterWeaponResults(
|
|||
}
|
||||
|
||||
export function getWeaponDestinationUrl(
|
||||
key: string,
|
||||
key: WeaponDestination,
|
||||
weapon: SelectedWeapon,
|
||||
): string | null {
|
||||
const destinations: Record<string, string> = {
|
||||
): string {
|
||||
const destinations: Record<WeaponDestination, string> = {
|
||||
builds: weaponBuildPage(weapon.slug),
|
||||
popular: weaponBuildPopularPage(weapon.slug),
|
||||
stats: weaponBuildStatsPage(weapon.slug),
|
||||
|
|
@ -83,7 +94,7 @@ export function getWeaponDestinationUrl(
|
|||
lfg: `${LFG_PAGE}?weapon=${weapon.id}`,
|
||||
};
|
||||
|
||||
return destinations[key] ?? null;
|
||||
return destinations[key];
|
||||
}
|
||||
|
||||
export function WeaponDestinationMenu({
|
||||
|
|
@ -95,12 +106,20 @@ export function WeaponDestinationMenu({
|
|||
selectedWeapon: SelectedWeapon;
|
||||
onBack: () => void;
|
||||
onSelect: (key: React.Key) => void;
|
||||
listBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||
listBoxRef: React.RefObject<HTMLDivElement>;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: keyboard navigation for Escape to go back
|
||||
<div onKeyDown={handleKeyDown}>
|
||||
<div className={styles.weaponDestinationHeader}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -118,7 +137,7 @@ export function WeaponDestinationMenu({
|
|||
className={styles.listBox}
|
||||
aria-label={selectedWeapon.name}
|
||||
onAction={onSelect}
|
||||
autoFocus
|
||||
autoFocus="first"
|
||||
>
|
||||
<ListBoxItem id="builds" className={styles.listBoxItem}>
|
||||
<div className={styles.resultItem}>
|
||||
|
|
@ -171,23 +190,29 @@ export function WeaponDestinationMenu({
|
|||
</div>
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WeaponResultsList({
|
||||
weaponResults,
|
||||
recentWeapons,
|
||||
onSelect,
|
||||
hasQuery,
|
||||
listBoxRef,
|
||||
}: {
|
||||
weaponResults: SelectedWeapon[];
|
||||
recentWeapons: SelectedWeapon[];
|
||||
onSelect: (key: React.Key) => void;
|
||||
hasQuery: boolean;
|
||||
listBoxRef: React.RefObject<HTMLDivElement | null>;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
const displayedWeapons = hasQuery ? weaponResults : recentWeapons;
|
||||
const showNoResults = hasQuery && weaponResults.length === 0;
|
||||
const showHint = !hasQuery && recentWeapons.length === 0;
|
||||
|
||||
return (
|
||||
<ListBox
|
||||
ref={listBoxRef}
|
||||
|
|
@ -196,16 +221,16 @@ export function WeaponResultsList({
|
|||
selectionMode="single"
|
||||
onAction={onSelect}
|
||||
renderEmptyState={() =>
|
||||
hasQuery ? (
|
||||
showNoResults ? (
|
||||
<div className={styles.emptyState}>
|
||||
{t("common:search.noResults")}
|
||||
</div>
|
||||
) : (
|
||||
) : showHint ? (
|
||||
<div className={styles.emptyState}>{t("common:search.hint")}</div>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{weaponResults.map((weapon) => (
|
||||
{displayedWeapons.map((weapon) => (
|
||||
<ListBoxItem
|
||||
key={`weapon-${weapon.id}`}
|
||||
id={`weapon-${weapon.id}`}
|
||||
|
|
@ -220,3 +245,33 @@ export function WeaponResultsList({
|
|||
</ListBox>
|
||||
);
|
||||
}
|
||||
|
||||
const RECENT_WEAPONS_KEY = "command-palette-recent-weapons";
|
||||
const MAX_RECENT_WEAPONS = 5;
|
||||
|
||||
export function getRecentWeapons(): MainWeaponId[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const stored = localStorage.getItem(RECENT_WEAPONS_KEY);
|
||||
if (!stored) return [];
|
||||
const parsed = JSON.parse(stored);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter(
|
||||
(id): id is MainWeaponId =>
|
||||
typeof id === "number" && mainWeaponIds.includes(id as MainWeaponId),
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveRecentWeapon(weaponId: MainWeaponId): void {
|
||||
try {
|
||||
const recent = getRecentWeapons();
|
||||
const filtered = recent.filter((id) => id !== weaponId);
|
||||
const updated = [weaponId, ...filtered].slice(0, MAX_RECENT_WEAPONS);
|
||||
localStorage.setItem(RECENT_WEAPONS_KEY, JSON.stringify(updated));
|
||||
} catch {
|
||||
// localStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user