import clsx from "clsx"; import { Search } from "lucide-react"; import * as React from "react"; import { Dialog, DialogTrigger, ListBox, ListBoxItem, Modal, ModalOverlay, Radio, RadioGroup, } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { useFetcher, useNavigate } from "react-router"; import { useDebounce } from "react-use"; import { Avatar } from "~/components/Avatar"; import { Image } from "~/components/Image"; import { Input } from "~/components/Input"; import type { SearchLoaderData } from "~/features/search/routes/search"; import { navIconUrl, teamPage, tournamentOrganizationPage, userPage, } from "~/utils/urls"; import styles from "./CommandPalette.module.css"; const SEARCH_TYPES = [ "users", "teams", "organizations", "tournaments", ] as const; type SearchType = (typeof SEARCH_TYPES)[number]; const STORAGE_KEY = "command-palette-search-type"; const SEARCH_TYPE_ICONS: Record = { users: "u", teams: "t", organizations: "associations", tournaments: "calendar", }; function getInitialSearchType(): SearchType { if (typeof window === "undefined") return "users"; try { const stored = localStorage.getItem(STORAGE_KEY); if (stored && SEARCH_TYPES.includes(stored as SearchType)) { return stored as SearchType; } } catch { // localStorage may be unavailable } return "users"; } export function CommandPalette() { const { t } = useTranslation(["common"]); const [isOpen, setIsOpen] = React.useState(false); const [isMac, setIsMac] = React.useState(false); React.useEffect(() => { setIsMac(/Mac|iPhone|iPad|iPod/.test(navigator.userAgent)); }, []); React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const modifierKey = isMac ? e.metaKey : e.ctrlKey; if (modifierKey && e.key === "k") { e.preventDefault(); setIsOpen(true); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isMac]); return ( setIsOpen(false)} /> ); } function CommandPaletteContent({ onClose }: { onClose: () => void }) { const { t } = useTranslation(["common"]); const navigate = useNavigate(); const [query, setQuery] = React.useState(""); const [searchType, setSearchType] = React.useState(getInitialSearchType); const inputRef = React.useRef(null); const fetcher = useFetcher(); React.useEffect(() => { inputRef.current?.focus(); }, []); React.useEffect(() => { try { localStorage.setItem(STORAGE_KEY, searchType); } catch { // localStorage may be unavailable } }, [searchType]); useDebounce( () => { if (!query) return; fetcher.load( `/search?q=${encodeURIComponent(query)}&type=${searchType}&limit=10`, ); }, 300, [query, searchType], ); const results = fetcher.data?.results ?? []; const hasQuery = query.length > 0; const handleSelect = (key: React.Key) => { const result = results.find((r) => getResultKey(r) === key); if (result) { navigate(getResultHref(result)); onClose(); } }; const handleSearchTypeChange = (value: string) => { setSearchType(value as SearchType); }; const handleRadioGroupKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); inputRef.current?.focus(); } }; return ( <> setQuery(e.target.value)} icon={} /> {/* biome-ignore lint/a11y/noStaticElementInteractions: keydown handler redirects Enter to input */}
{SEARCH_TYPES.map((type) => ( {({ isSelected, isHovered, isFocusVisible }) => ( {t(`common:search.type.${type}`)} )} ))}
hasQuery ? (
{t("common:search.noResults")}
) : (
{t("common:search.hint")}
) } > {results.map((result) => ( ))}
); } type SearchResult = NonNullable["results"][number]; function getResultKey(result: SearchResult): string { switch (result.type) { case "user": return `user-${result.id}`; case "team": return `team-${result.customUrl}`; case "organization": return `org-${result.id}`; case "tournament": return `tournament-${result.id}`; } } function getResultHref(result: SearchResult): string { switch (result.type) { case "user": return userPage({ discordId: result.discordId, customUrl: result.customUrl, }); case "team": return teamPage(result.customUrl); case "organization": return tournamentOrganizationPage({ organizationSlug: result.slug }); case "tournament": return `/to/${result.id}`; } } function ResultItem({ result }: { result: SearchResult }) { switch (result.type) { case "user": return (
{result.name} {result.secondaryName ? ( {result.secondaryName} ) : null}
); case "team": return (
{result.name}
); case "organization": return (
{result.name}
); case "tournament": return (
{result.logoUrl ? ( ) : null} {result.name}
); } }