import clsx from "clsx"; 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 { ChevronUpDownIcon } from "~/components/icons/ChevronUpDown"; import { CrossIcon } from "~/components/icons/Cross"; import type { UserSearchLoaderData } from "~/features/user-search/loaders/u.server"; import { Avatar } from "../Avatar"; import { SearchIcon } from "../icons/Search"; import selectStyles from "./Select.module.css"; import userSearchStyles from "./UserSearch.module.css"; type UserSearchUserItem = NonNullable["users"][number]; interface UserSearchProps extends Omit, "children" | "onChange"> { name?: string; label?: string; bottomText?: string; errorText?: string; initialUserId?: number; onChange?: (user: UserSearchUserItem | 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 UserSearchUserItem); }; // 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: | UserSearchUserItem | { 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.username} {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(`/u?q=${initialUserId}`); }, [initialUserId, initialUserFetcher]); React.useEffect(() => { if (initialUserId !== undefined) { setSelectedKey(initialUserId); } }, [initialUserId, setSelectedKey]); useDebounce( () => { if (!filterText) return; queryFetcher.load(`/u?q=${filterText}&limit=6`); setSelectedKey(null); }, 500, [filterText], ); const items = () => { // data fetched for the query user has currently typed if (queryFetcher.data && queryFetcher.data.query === filterText) { if (queryFetcher.data.users.length === 0) { return [{ id: "NO_RESULTS" }]; } return queryFetcher.data.users; } return [{ id: "PLACEHOLDER" }]; }; const initialUser = initialUserFetcher.data?.users[0]; return { filterText, setFilterText, items: items(), initialUser, }; }