Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions frontend/src/components/bounty/BountySearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { Search, X } from 'lucide-react';

interface BountySearchBarProps {
onSearch: (query: string) => void;
/** Debounce delay in ms (default 300) */
debounceMs?: number;
placeholder?: string;
}

export function BountySearchBar({
onSearch,
debounceMs = 300,
placeholder = 'Search bounties by title, description, or tags…',
}: BountySearchBarProps) {
const [value, setValue] = useState('');
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const debouncedSearch = useCallback(
(query: string) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => onSearch(query), debounceMs);
},
[onSearch, debounceMs],
);

useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setValue(query);
debouncedSearch(query);
};

const handleClear = () => {
setValue('');
onSearch('');
};

return (
<div className="relative w-full max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted pointer-events-none" />
<input
type="search"
value={value}
onChange={handleChange}
placeholder={placeholder}
aria-label="Search bounties"
className="w-full pl-9 pr-9 py-2 rounded-lg bg-forge-800 border border-border text-sm text-text-primary placeholder:text-text-muted focus:border-emerald focus:ring-1 focus:ring-emerald/30 outline-none transition-colors duration-150"
/>
{value && (
<button
onClick={handleClear}
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary transition-colors"
aria-label="Clear search"
>
<X className="w-4 h-4" />
</button>
)}
</div>
);
}