import { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import type { ReactElement } from 'react'; import { ChevronDown, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; interface PaginatedSelectOption { value: string; label: string; } interface PaginatedSelectProps { label: string; required?: boolean; error?: string; helperText?: string; placeholder?: string; value: string; onValueChange: (value: string) => void; onLoadOptions: (page: number, limit: number) => Promise<{ options: PaginatedSelectOption[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasMore: boolean; }; }>; className?: string; id?: string; } export const PaginatedSelect = ({ label, required = false, error, helperText, placeholder = 'Select Item', value, onValueChange, onLoadOptions, className, id, }: PaginatedSelectProps): ReactElement => { const [isOpen, setIsOpen] = useState(false); const [options, setOptions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; totalPages: number; hasMore: boolean; }>({ page: 1, limit: 20, total: 0, totalPages: 1, hasMore: false, }); const dropdownRef = useRef(null); const buttonRef = useRef(null); const dropdownMenuRef = useRef(null); const scrollContainerRef = useRef(null); const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; left: string; width: string; }>({ left: '0', width: '0' }); // Load initial options const loadOptions = useCallback( async (page: number = 1, append: boolean = false) => { try { if (page === 1) { setIsLoading(true); } else { setIsLoadingMore(true); } const result = await onLoadOptions(page, pagination.limit); if (append) { setOptions((prev) => [...prev, ...result.options]); } else { setOptions(result.options); } setPagination(result.pagination); } catch (err) { console.error('Error loading options:', err); } finally { setIsLoading(false); setIsLoadingMore(false); } }, [onLoadOptions, pagination.limit] ); // Load options when dropdown opens useEffect(() => { if (isOpen) { if (options.length === 0) { loadOptions(1, false); } } else { // Reset pagination when dropdown closes (but keep options for faster reopening) // Only reset if we want fresh data each time // setOptions([]); // setPagination({ page: 1, limit: 20, total: 0, totalPages: 1, hasMore: false }); } }, [isOpen, options.length, loadOptions]); // Handle scroll for infinite loading useEffect(() => { const scrollContainer = scrollContainerRef.current; if (!scrollContainer || !isOpen) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50; if (isNearBottom && pagination.hasMore && !isLoadingMore && !isLoading) { loadOptions(pagination.page + 1, true); } }; scrollContainer.addEventListener('scroll', handleScroll); return () => { scrollContainer.removeEventListener('scroll', handleScroll); }; }, [isOpen, pagination, isLoadingMore, isLoading, loadOptions]); // Handle click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node; if ( dropdownRef.current && !dropdownRef.current.contains(target) && dropdownMenuRef.current && !dropdownMenuRef.current.contains(target) ) { setIsOpen(false); } }; if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const dropdownHeight = Math.min(240, 240); const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; if (shouldOpenUp) { setDropdownStyle({ bottom: `${window.innerHeight - rect.top + 5}px`, left: `${rect.left}px`, width: `${rect.width}px`, }); } else { setDropdownStyle({ top: `${rect.bottom + 5}px`, left: `${rect.left}px`, width: `${rect.width}px`, }); } } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); const fieldId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`; const hasError = Boolean(error); const selectedOption = options.find((opt) => opt.value === value); const handleSelect = (optionValue: string) => { onValueChange(optionValue); setIsOpen(false); }; return (
{isOpen && buttonRef.current && createPortal(
{isLoading && options.length === 0 ? (
) : ( <>
    {options.map((option) => (
  • ))} {isLoadingMore && (
  • )}
)}
, document.body )}
{error && ( )} {helperText && !error && (

{helperText}

)}
); };