import React, { useState, useRef, useEffect, useMemo } from "react" import { Search, RotateCw, ChevronDown } from "lucide-react" import { LoadingIndicator } from "./LoadingIndicator" import { Empty } from "antd" export interface SelectOption { label: string | JSX.Element value: string } interface SelectProps { options: SelectOption[] value?: string onChange: (option: SelectOption) => void placeholder?: string onRefresh?: () => void className?: string dropdownClassName?: string optionClassName?: string searchClassName?: string disabled?: boolean isLoading?: boolean loadingText?: string filterOption?: (input: string, option: SelectOption) => boolean } export const PageAssistSelect: React.FC = ({ options = [], value, onChange, placeholder = "Select an option", onRefresh, className = "", dropdownClassName = "", optionClassName = "", searchClassName = "", disabled = false, isLoading = false, loadingText = "Loading...", filterOption }) => { const [isOpen, setIsOpen] = useState(false) const [searchTerm, setSearchTerm] = useState("") const [filteredOptions, setFilteredOptions] = useState([]) const containerRef = useRef(null) const optionsContainerRef = useRef(null) const [activeIndex, setActiveIndex] = useState(-1) useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsOpen(false) } } document.addEventListener("mousedown", handleClickOutside) return () => document.removeEventListener("mousedown", handleClickOutside) }, []) useEffect(() => { try { if (isOpen && optionsContainerRef.current && value) { const selectedOptionElement = optionsContainerRef.current.querySelector( `[data-value="${value}"]` ) if (selectedOptionElement) { selectedOptionElement.scrollIntoView({ block: "nearest" }) } } } catch (error) { console.error("Error scrolling to selected option:", error) } }, [isOpen, value]) useEffect(() => { if (!options) return const filtered = options.filter((option) => { if (!searchTerm) return true if (filterOption) { return filterOption(searchTerm, option) } if (typeof option.label === "string") { return option.label.toLowerCase().includes(searchTerm.toLowerCase()) } if (React.isValidElement(option.label)) { const textContent = extractTextFromJSX(option.label) return textContent.toLowerCase().includes(searchTerm.toLowerCase()) } return false }) setFilteredOptions(filtered) setActiveIndex(-1) }, [searchTerm, options, filterOption]) const extractTextFromJSX = (element: React.ReactElement): string => { if (typeof element.props.children === "string") { return element.props.children } if (Array.isArray(element.props.children)) { return element.props.children .map((child) => { if (typeof child === "string") return child if (React.isValidElement(child)) return extractTextFromJSX(child) return "" }) .join(" ") } if (React.isValidElement(element.props.children)) { return extractTextFromJSX(element.props.children) } return "" } const handleRefresh = (e: React.MouseEvent) => { e.stopPropagation() onRefresh?.() } const handleKeyDown = (e: React.KeyboardEvent) => { if (disabled || isLoading) return switch (e.key) { case "Enter": if (isOpen && activeIndex >= 0) { e.preventDefault() const selectedOption = filteredOptions[activeIndex] if (selectedOption) { onChange(selectedOption) setIsOpen(false) setSearchTerm("") } } else { setIsOpen(!isOpen) } break case " ": if (!isOpen) { e.preventDefault() setIsOpen(true) } break case "Escape": setIsOpen(false) break case "ArrowDown": e.preventDefault() if (!isOpen) { setIsOpen(true) } else { setActiveIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev ) } break case "ArrowUp": e.preventDefault() setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev)) break } } const defaultSelectClass = ` flex items-center justify-between p-2.5 rounded-lg border ${disabled || isLoading ? "cursor-not-allowed opacity-50" : "cursor-pointer"} ${isOpen ? "ring-2 ring-blue-500" : ""} bg-transparent border-gray-300 text-gray-900 transition-all duration-200 dark:text-white dark:border-[#353534] bg-white dark:bg-[#171717] ` const defaultDropdownClass = ` absolute z-50 w-full mt-1 bg-white dark:bg-[#1e1e1f] dark:text-white rounded-lg shadow-lg border border-gray-200 dark:border-[#353534] ` const defaultSearchClass = ` w-full pl-8 pr-8 py-1.5 rounded-md bg-gray-50 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-100 text-gray-900 dark:bg-[#1e1e1f] dark:text-white dark:border-[#353534] dark:focus:ring-gray-700 dark:focus:border-gray-700 dark:placeholder-gray-400 dark:bg-opacity-90 dark:hover:bg-opacity-100 dark:focus:bg-opacity-100 dark:hover:border-gray-700 dark:hover:bg-[#2a2a2b] dark:focus:bg-[#2a2a2b] ` const defaultOptionClass = ` p-2 cursor-pointer transition-colors duration-150 ` const selectedOption = useMemo(() => { if (!value || !options) return null return options?.find((opt) => opt.value === value) }, [value, options]) if (!options) { return (
{loadingText}
) } return (
!disabled && !isLoading && setIsOpen(!isOpen)} onKeyDown={handleKeyDown} className={`${defaultSelectClass} ${className}`}> {isLoading && } {isLoading ? ( loadingText ) : selectedOption ? ( selectedOption.label ) : ( {placeholder} )}
{isOpen && (
setSearchTerm(e.target.value)} placeholder="Search..." className={`${defaultSearchClass} ${searchClassName}`} disabled={isLoading} aria-label="Search options" />
{isLoading ? (
{loadingText}
) : filteredOptions.length === 0 ? (
) : ( filteredOptions.map((option, index) => (
{ onChange(option) setIsOpen(false) setSearchTerm("") }} className={` ${defaultOptionClass} ${value === option.value ? "bg-blue-50 dark:bg-[#262627]" : "hover:bg-gray-100 dark:hover:bg-[#272728]"} ${activeIndex === index ? "bg-gray-100 dark:bg-[#272728]" : ""} ${optionClassName}`}> {option.label}
)) )}
)}
) }