Files
rustycms/admin-ui/src/components/SearchBar.tsx

64 lines
2.0 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { Icon } from "@iconify/react";
export function SearchBar({
placeholder = "Search…",
paramName = "_q",
}: {
placeholder?: string;
paramName?: string;
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [value, setValue] = useState(searchParams.get(paramName) ?? "");
// Sync when URL changes externally
useEffect(() => {
setValue(searchParams.get(paramName) ?? "");
}, [searchParams, paramName]);
const push = useCallback(
(q: string) => {
const sp = new URLSearchParams(searchParams.toString());
if (q.trim()) {
sp.set(paramName, q.trim());
} else {
sp.delete(paramName);
}
sp.set("_page", "1");
router.push(`${pathname}?${sp.toString()}`);
},
[router, pathname, searchParams, paramName]
);
// Debounce: push URL 300ms after typing stops
useEffect(() => {
const timer = setTimeout(() => {
const current = searchParams.get(paramName) ?? "";
if (value.trim() !== current) push(value);
}, 300);
return () => clearTimeout(timer);
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="relative w-full min-w-0 sm:w-56">
<Icon
icon="mdi:magnify"
className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
aria-hidden
/>
<input
type="search"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
className="h-10 w-full min-w-0 rounded-md border border-input bg-background pl-8 pr-3 text-base text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/50 sm:h-9 sm:text-sm [touch-action:manipulation]"
/>
</div>
);
}