mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-26 09:20:24 -05:00
120 lines
3.2 KiB
TypeScript
120 lines
3.2 KiB
TypeScript
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
|
|
import { Main } from "~/components/Main";
|
|
import { parseSearchParams, type SendouRouteHandle } from "~/utils/remix";
|
|
import { navIconUrl, userPage, USER_SEARCH_PAGE } from "~/utils/urls";
|
|
import { Input } from "~/components/Input";
|
|
import { SearchIcon } from "~/components/icons/Search";
|
|
import { Link, useLoaderData, useSearchParams } from "@remix-run/react";
|
|
import { useDebounce } from "react-use";
|
|
import * as React from "react";
|
|
import { Avatar } from "~/components/Avatar";
|
|
import { useTranslation } from "react-i18next";
|
|
import { z } from "zod";
|
|
import { queryToUserIdentifier } from "~/utils/users";
|
|
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
|
|
|
import "~/styles/u.css";
|
|
|
|
export const handle: SendouRouteHandle = {
|
|
i18n: ["user"],
|
|
breadcrumb: () => ({
|
|
imgPath: navIconUrl("u"),
|
|
href: USER_SEARCH_PAGE,
|
|
type: "IMAGE",
|
|
}),
|
|
};
|
|
|
|
export type UserSearchLoaderData = SerializeFrom<typeof loader>;
|
|
|
|
const searchParamsSchema = z.object({
|
|
q: z.string().max(100).default(""),
|
|
limit: z.coerce.number().int().min(1).max(25).default(25),
|
|
});
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { q: query, limit } = parseSearchParams({
|
|
request,
|
|
schema: searchParamsSchema,
|
|
});
|
|
|
|
if (!query) return null;
|
|
|
|
const identifier = queryToUserIdentifier(query);
|
|
|
|
return {
|
|
users: identifier
|
|
? await UserRepository.searchExact(identifier)
|
|
: await UserRepository.search({ query, limit }),
|
|
query,
|
|
};
|
|
};
|
|
|
|
export default function UserSearchPage() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [inputValue, setInputValue] = React.useState(
|
|
searchParams.get("q") ?? "",
|
|
);
|
|
useDebounce(
|
|
() => {
|
|
if (!inputValue) return;
|
|
|
|
setSearchParams({ q: inputValue });
|
|
},
|
|
1500,
|
|
[inputValue],
|
|
);
|
|
|
|
return (
|
|
<Main className="u-search__container">
|
|
<Input
|
|
className="u-search__input"
|
|
icon={<SearchIcon className="u-search__icon" />}
|
|
value={inputValue}
|
|
onChange={(e) => setInputValue(e.target.value)}
|
|
/>
|
|
<UsersList />
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function UsersList() {
|
|
const { t } = useTranslation(["user"]);
|
|
const data = useLoaderData<typeof loader>();
|
|
|
|
if (!data) {
|
|
return <div className="u-search__info">{t("user:search.info")}</div>;
|
|
}
|
|
|
|
if (data.users.length === 0) {
|
|
return (
|
|
<div className="u-search__info">
|
|
{t("user:search.noResults", { query: data.query })}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ul className="u-search__users">
|
|
{data.users.map((user) => {
|
|
return (
|
|
<li key={user.id}>
|
|
<Link to={userPage(user)}>
|
|
<div className="u-search__user">
|
|
<Avatar size="sm" user={user} />
|
|
<div>
|
|
<div>{user.discordName}</div>
|
|
{user.inGameName ? (
|
|
<div className="u-search__ign">
|
|
{t("user:ign.short")}: {user.inGameName}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|