component designer search and user search

This commit is contained in:
hamid zarghami
2026-02-25 15:26:32 +03:30
parent 1d4978ddb7
commit e0e9ada60f
9 changed files with 264 additions and 9 deletions
+100
View File
@@ -0,0 +1,100 @@
import { type FC, useEffect, useRef, useState } from "react";
import { clx } from "@/helpers/utils";
import { useDesignerSearch } from "./hooks/useDesignerSearch";
import type { AdminItemType } from "../admin/types/Types";
const DEBOUNCE_MS = 400;
type DesignerSearchProps = {
label?: string;
placeholder?: string;
className?: string;
onChange?: (designId: string) => void;
value?: string;
};
const DesignerSearch: FC<DesignerSearchProps> = ({
label,
placeholder = "جستجوی طراح (نام، موبایل، ...)",
className,
onChange,
value = "",
}) => {
const [search, setSearch] = useState(value);
const [debouncedSearch, setDebouncedSearch] = useState("");
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const t = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_MS);
return () => clearTimeout(t);
}, [search]);
const { designers, isFetching } = useDesignerSearch(debouncedSearch);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleSelect = (designer: AdminItemType) => {
setSearch(`${designer.firstName} ${designer.lastName}`);
onChange?.(designer.id);
setIsOpen(false);
};
const displayLabel = (designer: AdminItemType) =>
`${designer.firstName} ${designer.lastName}`;
return (
<div ref={wrapperRef} className={clx("w-full relative", className)}>
{label && (
<label className="text-sm text-primary-content block mb-1">{label}</label>
)}
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setIsOpen(true);
}}
onFocus={() => setIsOpen(true)}
placeholder={placeholder}
className="w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border"
/>
{isOpen && debouncedSearch.length > 0 && (
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-white border border-border rounded-xl shadow-lg max-h-60 overflow-auto">
{isFetching ? (
<div className="px-4 py-3 text-xs text-description">در حال جستجو...</div>
) : designers.length === 0 ? (
<div className="px-4 py-3 text-xs text-description">نتیجهای یافت نشد</div>
) : (
<ul className="py-1">
{designers.map((designer) => (
<li key={designer.id}>
<button
type="button"
onClick={() => handleSelect(designer)}
className="w-full text-right px-4 py-2 text-xs hover:bg-muted transition-colors flex flex-col"
>
<span className="font-medium">{displayLabel(designer)}</span>
{designer.phone && (
<span className="text-description text-[10px]">{designer.phone}</span>
)}
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
);
};
export default DesignerSearch;
@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { searchAdmins } from "../../admin/service/AdminService";
import type { AdminItemType } from "../../admin/types/Types";
export const useDesignerSearch = (debouncedSearch: string) => {
const { data, isFetching } = useQuery({
queryKey: ["designers", "search", debouncedSearch],
queryFn: () => searchAdmins(debouncedSearch),
enabled: debouncedSearch.length > 0,
});
const designers = (data?.data ?? []) as AdminItemType[];
return { designers, isFetching };
};