'use client'; import { ComponentType, ReactNode, useEffect, useMemo, useState, } from 'react'; import Select, { OptionProps, GroupBase, InputActionMeta, MultiValue, SingleValue, } from 'react-select'; import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; import { useDebounce } from 'use-debounce'; import { cn } from '@/lib/helper'; export interface OptionType { value: string | number; label: string; className?: string; labelClassName?: string; } export type OptionComponent = ComponentType< OptionProps> >; interface SelectInputBaseProps { label?: ReactNode; bottomLabel?: ReactNode; options: T[]; optionComponent?: OptionComponent; isDisabled?: boolean; isLoading?: boolean; isClearable?: boolean; isRtl?: boolean; isSearchable?: boolean; isMulti?: boolean; placeholder?: string; required?: boolean; className?: { wrapper?: string; label?: string; select?: string; }; isError?: boolean; errorMessage?: string; isAnimated?: boolean; openMenu?: boolean; delay?: number; onInputChange?: (search: string) => void; } interface SelectInputProps extends SelectInputBaseProps { createables?: boolean; value?: T | T[] | null; onChange?: (val: T | T[] | null) => void; } const animatedComponents = makeAnimated(); const SelectInput = (props: SelectInputProps) => { const { label, bottomLabel, value, onChange, options, optionComponent, isDisabled, isLoading, isClearable, isRtl, isSearchable = true, isMulti, placeholder, required, className, isError, errorMessage, isAnimated = true, openMenu, delay = 300, createables = false, onInputChange, } = props; const [internalInputValue, setInternalInputValue] = useState(''); const [debouncedInputValue] = useDebounce(internalInputValue, delay); const components = useMemo(() => { const base = isAnimated ? animatedComponents : {}; return { ...base, IndicatorSeparator: () => null }; }, [isAnimated]); const internalInputChangeHandler = ( val: string, meta: InputActionMeta ) => { if (meta.action === 'input-change') setInternalInputValue(val); if (meta.action === 'menu-close') setInternalInputValue(''); }; useEffect(() => { onInputChange?.(debouncedInputValue); }, [onInputChange, debouncedInputValue]); const SelectComponent = createables ? CreatableSelect : Select; /** 🎯 handleChange tanpa any */ const handleChange = ( val: MultiValue | SingleValue ): void => { if (!val) { onChange?.(null); return; } if (isMulti) { onChange?.(val as T[]); } else { onChange?.(val as T); } }; return (
{label && ( {label} {required && ( * )} )} > instanceId="select" value={value ?? (isMulti ? [] : null)} onChange={handleChange} options={options} menuIsOpen={openMenu} inputValue={internalInputValue} onInputChange={internalInputChangeHandler} isMulti={isMulti} isDisabled={isDisabled} isLoading={isLoading} isClearable={isClearable} isRtl={isRtl} isSearchable={isSearchable} placeholder={placeholder} className={cn('w-full', className?.select)} classNames={{ control: ({ isFocused, isDisabled }) => cn( 'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer!', { 'border-red-500! ring-2 ring-red-200': isError, 'border-indigo-500 ring-2 ring-indigo-200': isFocused, 'border-gray-300': !isError && !isFocused, 'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled, } ), valueContainer: () => cn('flex-1 px-4! py-2! gap-1'), placeholder: () => cn({ 'text-gray-400': !isError, 'text-red-300!': isError }), singleValue: () => cn({ 'text-gray-900': !isError, 'text-error!': isError }), input: () => cn('text-gray-900'), indicatorsContainer: () => cn('flex items-center gap-1 pr-2'), dropdownIndicator: ({ isFocused }) => cn('p-1 rounded-md hover:bg-gray-100', { 'text-gray-900': isFocused, 'text-gray-500': !isFocused, 'text-error!': isError, }), menu: () => cn('border border-gray-200 rounded-lg bg-white shadow-lg!'), menuList: () => cn('p-2! max-h-60 overflow-auto'), option: ({ isFocused, isSelected }) => cn('mt-1 px-3 py-2 rounded-md cursor-pointer!', { 'bg-indigo-600 text-white': isFocused, 'bg-blue-500!': isSelected, 'text-gray-700': !isFocused && !isSelected, }), multiValue: ({ getValue, index }) => { const selectedValues = getValue() as T[]; return cn( 'bg-indigo-50 rounded-md py-0.5 pl-2 pr-1 flex items-center gap-1!', selectedValues[index]?.className ); }, multiValueLabel: ({ getValue, index }) => { const selectedValues = getValue() as T[]; return cn('text-indigo-700', selectedValues[index]?.labelClassName); }, }} components={{ ...components, ...(optionComponent ? { Option: optionComponent } : {}), }} menuPortalTarget={ typeof document !== 'undefined' ? document.body : undefined } styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }), }} /> {isError &&

{errorMessage}

} {!isError && bottomLabel && (

{bottomLabel}

)}
); }; export default SelectInput;