49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
'use client';
|
|
|
|
/* EngagementSearchInput — phase 07 (ENG-04).
|
|
* Purpose: Search input with leading Search icon. Debounced 300ms before emitting onChange.
|
|
* Page applies the filter client-side on loaded users (D-21).
|
|
* Props: value (controlled string), onChange (debounced callback). */
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Search } from 'lucide-react';
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
export interface EngagementSearchInputProps {
|
|
value: string;
|
|
onChange: (next: string) => void;
|
|
}
|
|
|
|
export function EngagementSearchInput({ value, onChange }: EngagementSearchInputProps) {
|
|
// Local immediate state for the input; debounce flushes to onChange
|
|
const [local, setLocal] = useState<string>(value);
|
|
|
|
// Keep local state synced when the parent resets (e.g. "Clear search" CTA on no-matches state)
|
|
useEffect(() => {
|
|
setLocal(value);
|
|
}, [value]);
|
|
|
|
// 300ms debounce per D-21
|
|
useEffect(() => {
|
|
if (local === value) return;
|
|
const id = setTimeout(() => { onChange(local); }, 300);
|
|
return () => clearTimeout(id);
|
|
}, [local, value, onChange]);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<Search
|
|
className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none"
|
|
aria-hidden="true"
|
|
/>
|
|
<Input
|
|
type="search"
|
|
placeholder="Search by name or email"
|
|
aria-label="Search team members by name or email"
|
|
className="pl-9"
|
|
value={local}
|
|
onChange={(e) => setLocal(e.target.value)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|