102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
interface SearchBarProps {
|
|
onSearch: (query: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
initialValue?: string;
|
|
}
|
|
|
|
export const SearchBar = ({
|
|
onSearch,
|
|
placeholder = "Rechercher...",
|
|
className = "",
|
|
initialValue = ""
|
|
}: SearchBarProps) => {
|
|
const [searchTerm, setSearchTerm] = useState(initialValue);
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const previousSearchTermRef = useRef(initialValue);
|
|
|
|
// useCallback dans ce cas est utilisé pour éviter les boucles infinies : change l'état du composant parent qui change l'état de searchBar etc
|
|
const regulatedSearch = useCallback(
|
|
(term: string) => {
|
|
// la recherche se fait si le terme a changé depuis la denrière recherche
|
|
if (term !== previousSearchTermRef.current) {
|
|
previousSearchTermRef.current = term;
|
|
|
|
// déclenche uniquement à partir de deux caractères
|
|
if (term.length >= 2 || term === '') {
|
|
onSearch(term);
|
|
}
|
|
}
|
|
},
|
|
[onSearch]
|
|
);
|
|
|
|
// useEffect avec un délai pour limiter les appels d'API
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
regulatedSearch(searchTerm);
|
|
}, 300);
|
|
|
|
return () => {
|
|
clearTimeout(timer);
|
|
};
|
|
}, [searchTerm, regulatedSearch]);
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (searchTerm.length < 2 && searchTerm.length > 0) {
|
|
// indication de la condition
|
|
if (inputRef.current) {
|
|
inputRef.current.classList.add('animate-shake');
|
|
setTimeout(() => {
|
|
inputRef.current?.classList.remove('animate-shake');
|
|
}, 500);
|
|
}
|
|
return;
|
|
}
|
|
|
|
regulatedSearch(searchTerm);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSearch} className={`relative w-full ${className}`}>
|
|
<div className={`relative flex items-center overflow-hidden rounded-lg border transition-all duration-300 ${isFocused ? 'border-primary shadow-sm ring-2 ring-primary/20' : 'border-border'}`}>
|
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-muted-foreground">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
placeholder={placeholder}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
onFocus={() => setIsFocused(true)}
|
|
onBlur={() => setIsFocused(false)}
|
|
className="flex-1 pl-10 pr-4 py-2 bg-transparent outline-none text-foreground placeholder:text-muted-foreground w-full"
|
|
/>
|
|
{searchTerm && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchTerm('')}
|
|
className="absolute right-0 mr-2 p-1 text-muted-foreground hover:text-foreground"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
{searchTerm.length === 1 && (
|
|
<p className="text-sm text-red-500 mt-1 absolute">
|
|
Veuillez saisir au moins 2 caractères
|
|
</p>
|
|
)}
|
|
</form>
|
|
);
|
|
}; |