canina/frontend/application/components/SearchableSelect.tsx

115 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useRef, useEffect } from "react";
import { ChevronDown, Search, X } from "lucide-react";
import { cn } from "../lib/utils";
interface SearchableSelectProps {
id?: string;
options: string[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
disabled?: boolean;
className?: string;
}
export default function SearchableSelect({
id,
options,
value,
onChange,
placeholder = "انتخاب کنید...",
searchPlaceholder = "جستجو...",
disabled = false,
className
}: SearchableSelectProps) {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const filteredOptions = options.filter(opt =>
opt.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div ref={containerRef} className={cn("relative w-full", className)}>
<button
id={id}
type="button"
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
className={cn(
"w-full bg-white border border-medical-gray-200 rounded-2xl py-3 px-4 text-xs sm:text-sm font-bold flex items-center justify-between transition-all outline-none focus:ring-2 focus:ring-canina-blue/20",
disabled && "bg-medical-gray-50 opacity-60 cursor-not-allowed",
value ? "text-medical-gray-900" : "text-medical-gray-400"
)}
>
<span className="truncate">{value || placeholder}</span>
<ChevronDown className={cn("w-4 h-4 text-medical-gray-400 transition-transform", isOpen && "rotate-180")} />
</button>
{isOpen && (
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-white border border-medical-gray-200 rounded-2xl shadow-xl overflow-hidden max-h-60 flex flex-col font-vazir animate-in fade-in slide-in-from-top-2 duration-150">
<div className="p-2 border-b border-medical-gray-100 relative flex items-center">
<Search className="w-3.5 h-3.5 text-medical-gray-400 absolute right-4" />
<input
type="text"
autoFocus
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={searchPlaceholder}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-1.5 pr-8 pl-3 text-xs font-bold text-medical-gray-900 outline-none focus:border-canina-blue"
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery("")}
className="absolute left-4 text-medical-gray-400 hover:text-medical-gray-600"
>
<X className="w-3 h-3" />
</button>
)}
</div>
<div className="overflow-y-auto flex-1 p-1">
{filteredOptions.length === 0 ? (
<div className="p-3 text-center text-xs text-medical-gray-400 font-bold">
موردی یافت نشد
</div>
) : (
filteredOptions.map((opt) => (
<button
key={opt}
type="button"
onClick={() => {
onChange(opt);
setIsOpen(false);
setSearchQuery("");
}}
className={cn(
"w-full text-right px-3 py-2 rounded-xl text-xs font-bold transition-colors flex items-center justify-between",
value === opt ? "bg-canina-blue/10 text-canina-blue" : "hover:bg-medical-gray-50 text-medical-gray-700"
)}
>
<span>{opt}</span>
{value === opt && <span className="w-1.5 h-1.5 rounded-full bg-canina-blue" />}
</button>
))
)}
</div>
</div>
)}
</div>
);
}