115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|