diff --git a/app/components/layout/CommandPalette.tsx b/app/components/layout/CommandPalette.tsx index b392a22b5..6efda93bd 100644 --- a/app/components/layout/CommandPalette.tsx +++ b/app/components/layout/CommandPalette.tsx @@ -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(); 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" ? ( = { +): string { + const destinations: Record = { 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; + listBoxRef: React.RefObject; }) { 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 +
); } export function WeaponResultsList({ weaponResults, + recentWeapons, onSelect, hasQuery, listBoxRef, }: { weaponResults: SelectedWeapon[]; + recentWeapons: SelectedWeapon[]; onSelect: (key: React.Key) => void; hasQuery: boolean; listBoxRef: React.RefObject; }) { const { t } = useTranslation(["common"]); + const displayedWeapons = hasQuery ? weaponResults : recentWeapons; + const showNoResults = hasQuery && weaponResults.length === 0; + const showHint = !hasQuery && recentWeapons.length === 0; + return ( - hasQuery ? ( + showNoResults ? (
{t("common:search.noResults")}
- ) : ( + ) : showHint ? (
{t("common:search.hint")}
- ) + ) : null } > - {weaponResults.map((weapon) => ( + {displayedWeapons.map((weapon) => ( ); } + +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 + } +}