mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-19 21:50:52 -05:00
This broke after upgrading deps and couldn't figure it out with a quick look. It just makes it a bit more convenient when adding new pages & debugging but not really that necessary so decided to delete it for now.
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import { Combobox } from "@headlessui/react";
|
|
import { useFetcher } from "@remix-run/react";
|
|
import clsx from "clsx";
|
|
import * as React from "react";
|
|
import { useDebounce } from "react-use";
|
|
import type { UserSearchLoaderData } from "~/features/user-search/routes/u";
|
|
import { Avatar } from "./Avatar";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
type UserSearchUserItem = NonNullable<UserSearchLoaderData>["users"][number];
|
|
|
|
export function UserSearch({
|
|
inputName,
|
|
onChange,
|
|
initialUserId,
|
|
id,
|
|
className,
|
|
userIdsToOmit,
|
|
required,
|
|
}: {
|
|
inputName: string;
|
|
onChange?: (user: UserSearchUserItem) => void;
|
|
initialUserId?: number;
|
|
id?: string;
|
|
className?: string;
|
|
userIdsToOmit?: Set<number>;
|
|
required?: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [selectedUser, setSelectedUser] =
|
|
React.useState<UserSearchUserItem | null>(null);
|
|
const queryFetcher = useFetcher<UserSearchLoaderData>();
|
|
const initialUserFetcher = useFetcher<UserSearchLoaderData>();
|
|
const [query, setQuery] = React.useState("");
|
|
useDebounce(
|
|
() => {
|
|
if (!query) return;
|
|
|
|
queryFetcher.load(`/u?q=${query}&limit=6`);
|
|
},
|
|
1000,
|
|
[query],
|
|
);
|
|
|
|
// load initial user
|
|
React.useEffect(() => {
|
|
if (
|
|
!initialUserId ||
|
|
initialUserFetcher.state !== "idle" ||
|
|
initialUserFetcher.data
|
|
) {
|
|
return;
|
|
}
|
|
|
|
initialUserFetcher.load(`/u?q=${initialUserId}`);
|
|
}, [initialUserId, initialUserFetcher]);
|
|
React.useEffect(() => {
|
|
if (!initialUserFetcher.data) return;
|
|
|
|
setSelectedUser(initialUserFetcher.data.users[0]);
|
|
}, [initialUserFetcher.data]);
|
|
|
|
const allUsers = queryFetcher.data?.users ?? [];
|
|
|
|
const users = allUsers.filter((u) => !userIdsToOmit?.has(u.id));
|
|
const noMatches = queryFetcher.data && users.length === 0;
|
|
|
|
const initialSelectionIsLoading = Boolean(
|
|
initialUserId && !initialUserFetcher.data,
|
|
);
|
|
|
|
return (
|
|
<div className="combobox-wrapper">
|
|
{selectedUser && inputName ? (
|
|
<input type="hidden" name={inputName} value={selectedUser.id} />
|
|
) : null}
|
|
<Combobox
|
|
value={selectedUser}
|
|
onChange={(newUser) => {
|
|
setSelectedUser(newUser);
|
|
onChange?.(newUser!);
|
|
}}
|
|
disabled={initialSelectionIsLoading}
|
|
>
|
|
<Combobox.Input
|
|
placeholder={
|
|
initialSelectionIsLoading
|
|
? t("actions.loading")
|
|
: "Search via name or ID..."
|
|
}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
displayValue={(user: UserSearchUserItem) => user?.discordName ?? ""}
|
|
className={clsx("combobox-input", className)}
|
|
data-1p-ignore
|
|
data-testid={`${inputName}-combobox-input`}
|
|
id={id}
|
|
required={required}
|
|
/>
|
|
<Combobox.Options
|
|
className={clsx("combobox-options", {
|
|
empty: noMatches,
|
|
hidden: !queryFetcher.data,
|
|
})}
|
|
>
|
|
{noMatches ? (
|
|
<div className="combobox-no-matches">
|
|
{t("forms.errors.noSearchMatches")}{" "}
|
|
<span className="combobox-emoji">🤔</span>
|
|
</div>
|
|
) : null}
|
|
{users.map((user, i) => (
|
|
<Combobox.Option key={user.id} value={user} as={React.Fragment}>
|
|
{({ active }) => (
|
|
<li
|
|
className={clsx("combobox-item", { active })}
|
|
data-testid={`combobox-option-${i}`}
|
|
>
|
|
<Avatar user={user} size="xs" />
|
|
<div>
|
|
<div className="stack xs horizontal items-center">
|
|
<span className="combobox-username">
|
|
{user.discordName}
|
|
</span>{" "}
|
|
{user.plusTier ? (
|
|
<span className="text-xxs">+{user.plusTier}</span>
|
|
) : null}
|
|
</div>
|
|
{user.discordUniqueName ? (
|
|
<div className="text-xs">{user.discordUniqueName}</div>
|
|
) : null}
|
|
</div>
|
|
</li>
|
|
)}
|
|
</Combobox.Option>
|
|
))}
|
|
</Combobox.Options>
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
}
|