Files
pinkudex/components/grid/GridSearchInput.tsx
2026-05-26 22:46:00 +02:00

87 lines
2.6 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Search, X } from "lucide-react";
export function GridSearchInput() {
const router = useRouter();
const params = useSearchParams();
const initial = params.get("q") ?? "";
const [value, setValue] = useState(initial);
const debounce = useRef<number | null>(null);
const lastApplied = useRef(initial);
useEffect(() => {
return () => {
if (debounce.current) window.clearTimeout(debounce.current);
};
}, []);
// Sync state from URL (e.g. when navigating, "All" link clears it).
useEffect(() => {
const fromUrl = params.get("q") ?? "";
if (fromUrl !== lastApplied.current) {
lastApplied.current = fromUrl;
setValue(fromUrl);
}
}, [params]);
function apply(next: string) {
if (next === lastApplied.current) return;
lastApplied.current = next;
const sp = new URLSearchParams(params.toString());
if (next.trim()) {
sp.set("q", next.trim());
// Activating search clears the letter filter so the user sees all matches.
sp.delete("letter");
} else {
sp.delete("q");
}
router.push(`?${sp.toString()}`, { scroll: false });
}
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const next = e.target.value;
setValue(next);
if (debounce.current) window.clearTimeout(debounce.current);
debounce.current = window.setTimeout(() => apply(next), 300);
}
function clear() {
setValue("");
if (debounce.current) window.clearTimeout(debounce.current);
apply("");
}
return (
<div className="relative">
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-fg-muted)] pointer-events-none" />
<input
type="text"
value={value}
onChange={onChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (debounce.current) window.clearTimeout(debounce.current);
apply(value);
} else if (e.key === "Escape") {
clear();
}
}}
placeholder="Search Code, Title, Notes…"
className="glass rounded-lg pl-8 pr-7 py-1.5 text-sm outline-none focus:border-[var(--color-cyan)] w-56"
/>
{value && (
<button
type="button"
onClick={clear}
aria-label="Clear search"
className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
);
}