import clsx from "clsx"; import { ChevronsUpDown, Search, X } from "lucide-react"; import * as React from "react"; import { Autocomplete, Button, Input, type Key, ListBox, ListBoxItem, Popover, SearchField, Select, type SelectProps, SelectValue, } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { useFetcher } from "react-router"; import { useDebounce } from "react-use"; import { SendouBottomTexts } from "~/components/elements/BottomTexts"; import { SendouLabel } from "~/components/elements/Label"; import type { SearchLoaderData } from "~/features/search/routes/search"; import { Avatar } from "../Avatar"; import selectStyles from "./Select.module.css"; import userSearchStyles from "./UserSearch.module.css"; type UserResult = Extract< NonNullable["results"][number], { type: "user" } >; interface UserSearchProps extends Omit, "children" | "onChange"> { name?: string; label?: string; bottomText?: string; errorText?: string; initialUserId?: number; onChange?: (user: UserResult | null) => void; } export const UserSearch = React.forwardRef(function UserSearch< T extends object, >( { name, label, bottomText, errorText, initialUserId, onChange, ...rest }: UserSearchProps, ref?: React.Ref, ) { const [selectedKey, setSelectedKey] = React.useState(initialUserId ?? null); const { initialUser, items, ...list } = useUserSearch( setSelectedKey, initialUserId, ); const onSelectionChange = (userId: number) => { setSelectedKey(userId); onChange?.(items.find((user) => user.id === userId) as UserResult); }; // clear if selected user is not in the new filtered items React.useEffect(() => { if ( selectedKey && selectedKey !== initialUserId && !items.some((user) => user.id === selectedKey) ) { setSelectedKey(null); onChange?.(null); } }, [items, selectedKey, onChange, initialUserId]); return ( user !== undefined)} className={selectStyles.listBox} > {(item) => } ); }); function UserItem({ item, }: { item: | UserResult | { id: "NO_RESULTS"; } | { id: "PLACEHOLDER"; }; }) { const { t } = useTranslation(["common"]); // for some reason the `renderEmptyState` on ListBox is not working // so doing this as a workaround if (typeof item.id === "string") { return ( {item.id === "PLACEHOLDER" ? t("common:forms.userSearch.placeholder") : t("common:forms.userSearch.noResults")} ); } const additionalText = () => { const plusServer = item.plusTier ? `+${item.plusTier}` : ""; const profileUrl = item.customUrl ? `/u/${item.customUrl}` : ""; if (plusServer && profileUrl) { return `${plusServer} • ${profileUrl}`; } if (plusServer) { return plusServer; } if (profileUrl) { return profileUrl; } return ""; }; return ( clsx(userSearchStyles.item, { [selectStyles.itemFocused]: isFocused, [selectStyles.itemSelected]: isSelected, }) } data-testid="user-search-item" >
{item.name} {additionalText() ? (
{additionalText()}
) : null}
); } function useUserSearch( setSelectedKey: (userId: number | null) => void, initialUserId?: number, ) { const [filterText, setFilterText] = React.useState(""); const queryFetcher = useFetcher(); const initialUserFetcher = useFetcher(); React.useEffect(() => { if ( !initialUserId || initialUserFetcher.state !== "idle" || initialUserFetcher.data ) { return; } initialUserFetcher.load(`/search?q=${initialUserId}&type=users&limit=1`); }, [initialUserId, initialUserFetcher]); React.useEffect(() => { if (initialUserId !== undefined) { setSelectedKey(initialUserId); } }, [initialUserId, setSelectedKey]); useDebounce( () => { if (!filterText) return; queryFetcher.load(`/search?q=${filterText}&type=users&limit=6`); setSelectedKey(null); }, 500, [filterText], ); const items = () => { // data fetched for the query user has currently typed if (queryFetcher.data && queryFetcher.data.query === filterText) { const userResults = queryFetcher.data.results.filter( (r): r is UserResult => r.type === "user", ); if (userResults.length === 0) { return [{ id: "NO_RESULTS" as const }]; } return userResults; } return [{ id: "PLACEHOLDER" as const }]; }; const initialUserResult = initialUserFetcher.data?.results.find( (r): r is UserResult => r.type === "user", ); return { filterText, setFilterText, items: items(), initialUser: initialUserResult, }; }