'use client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { seriesApi } from '../../lib/api'; import { Series } from '../../types/api'; interface SeriesSelectorProps { value: string; onChange: (seriesName: string, seriesId?: string) => void; placeholder?: string; error?: string; disabled?: boolean; required?: boolean; label?: string; authorId?: string; // Optional author ID to prioritize that author's series } export default function SeriesSelector({ value, onChange, placeholder = 'Enter or select a series', error, disabled = false, required = false, label = 'Series', authorId }: SeriesSelectorProps) { const [isOpen, setIsOpen] = useState(false); const [series, setSeries] = useState([]); const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(value || ''); const inputRef = useRef(null); const dropdownRef = useRef(null); const debounceTimerRef = useRef(null); // Search series dynamically based on input const searchSeries = useCallback(async (query: string) => { try { setLoading(true); if (!query.trim()) { // If empty query, load recent/popular series const result = await seriesApi.getSeries({ page: 0, size: 20, sortBy: 'name', sortDir: 'asc' }); setSeries(result.content); } else { // Search by name const result = await seriesApi.searchSeriesByName(query, { page: 0, size: 20 }); setSeries(result.content); } } catch (error) { console.error('Failed to search series:', error); setSeries([]); } finally { setLoading(false); } }, []); // Debounced search effect useEffect(() => { // Clear existing timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Only search when dropdown is open if (isOpen) { // Set new timer for debounced search debounceTimerRef.current = setTimeout(() => { searchSeries(inputValue); }, 300); // 300ms debounce delay } // Cleanup timer on unmount return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, [inputValue, isOpen, searchSeries]); // Update internal value when prop changes useEffect(() => { setInputValue(value || ''); }, [value]); // Handle clicks outside to close dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && !inputRef.current?.contains(event.target as Node) ) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setInputValue(newValue); setIsOpen(true); // If user is typing and it doesn't match any existing series exactly, clear the seriesId onChange(newValue, undefined); }; const handleInputFocus = () => { setIsOpen(true); }; const handleSeriesSelect = (selectedSeries: Series) => { setInputValue(selectedSeries.name); setIsOpen(false); onChange(selectedSeries.name, selectedSeries.id); inputRef.current?.blur(); }; const handleInputBlur = () => { // Small delay to allow clicks on dropdown items setTimeout(() => { if (!dropdownRef.current?.contains(document.activeElement)) { setIsOpen(false); } }, 200); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setIsOpen(false); inputRef.current?.blur(); } }; return (
{label && ( )}
{/* Dropdown Arrow */}
{/* Dropdown */} {isOpen && (
{loading ? (
Searching series...
) : ( <> {series.length > 0 ? ( series.map((s) => ( )) ) : ( <> {inputValue.trim() ? ( ) : (
Type to search for series or create a new one
)} )} {inputValue.trim() && !series.some(s => s.name.toLowerCase() === inputValue.toLowerCase()) && ( )} )}
)} {error && (

{error}

)}
); }