69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSearchParams } from "react-router";
|
|
import ButtonPrimary from "../buttons/ButtonPrimary";
|
|
import clsx from "clsx";
|
|
import { TextInput } from "~/ui/components/TextInput";
|
|
|
|
interface Props {
|
|
onNew?: () => void;
|
|
onNewRoute?: string;
|
|
placeholder?: string;
|
|
size?: "xs" | "sm" | "lg";
|
|
className?: string;
|
|
}
|
|
|
|
// Subframe migration (strangler shim): renders Subframe TextInput with type="search"
|
|
// (built-in search icon) behind the original API. The debounced URL-sync logic is
|
|
// unchanged. Subframe's own SearchInput is presentational only and cannot back a
|
|
// controlled field. See docs/subframe/COMPONENT-MAPPING.md.
|
|
export default function InputSearchWithURL({ onNew, onNewRoute, placeholder, size = "lg", className }: Props) {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const { t } = useTranslation();
|
|
|
|
const [search, setSearch] = useState(searchParams.get("q")?.toString() ?? "");
|
|
const [debouncedSearch, setDebouncedSearch] = useState(searchParams.get("q")?.toString() ?? "");
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setDebouncedSearch(search), 500);
|
|
return () => clearTimeout(timer);
|
|
}, [search]);
|
|
|
|
useEffect(() => {
|
|
const query = searchParams.get("q") ?? "";
|
|
if (query !== debouncedSearch) {
|
|
searchParams.set("q", debouncedSearch);
|
|
if (searchParams.get("page")) {
|
|
searchParams.set("page", "1");
|
|
}
|
|
if (searchParams.get("pageSize")) {
|
|
// searchParams.delete("pageSize");
|
|
}
|
|
setSearchParams(searchParams, {
|
|
preventScrollReset: true,
|
|
});
|
|
}
|
|
}, [debouncedSearch, searchParams, setSearchParams]);
|
|
|
|
const resolvedPlaceholder = placeholder ?? t("shared.searchDot");
|
|
|
|
return (
|
|
<div className={clsx("flex justify-between space-x-2", className)}>
|
|
<TextInput className="w-full max-w-none" type="search" size={size === "lg" ? "medium" : "small"}>
|
|
<TextInput.Input
|
|
type="search"
|
|
name="q"
|
|
aria-label={resolvedPlaceholder}
|
|
placeholder={resolvedPlaceholder}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
autoComplete="off"
|
|
/>
|
|
</TextInput>
|
|
|
|
{onNew && <ButtonPrimary onClick={onNew}>{t("shared.new")}</ButtonPrimary>}
|
|
{onNewRoute && <ButtonPrimary to={onNewRoute}>{t("shared.new")}</ButtonPrimary>}
|
|
</div>
|
|
);
|
|
}
|