'use client'; import { useState, useEffect, useRef } from 'react'; import { seriesApi, storyApi } 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 [filteredSeries, setFilteredSeries] = useState([]); const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(value || ''); const [authorSeriesMap, setAuthorSeriesMap] = useState>({}); const inputRef = useRef(null); const dropdownRef = useRef(null); // Load series and author-series mappings when component mounts or when dropdown opens useEffect(() => { const loadSeriesData = async () => { try { setLoading(true); // Load all series const seriesResult = await seriesApi.getSeries({ page: 0, size: 100 }); // Get first 100 series setSeries(seriesResult.content); // Load some recent stories to build author-series mapping // This gives us a sample of which authors have written in which series try { const storiesResult = await storyApi.getStories({ page: 0, size: 200 }); // Get recent stories const newAuthorSeriesMap: Record = {}; storiesResult.content.forEach(story => { if (story.authorId && story.seriesName) { if (!newAuthorSeriesMap[story.authorId]) { newAuthorSeriesMap[story.authorId] = []; } if (!newAuthorSeriesMap[story.authorId].includes(story.seriesName)) { newAuthorSeriesMap[story.authorId].push(story.seriesName); } } }); setAuthorSeriesMap(newAuthorSeriesMap); } catch (error) { console.error('Failed to load author-series mapping:', error); // Continue without author prioritization if this fails } } catch (error) { console.error('Failed to load series:', error); } finally { setLoading(false); } }; if (isOpen && series.length === 0) { loadSeriesData(); } }, [isOpen, series.length]); // Update internal value when prop changes useEffect(() => { setInputValue(value || ''); }, [value]); // Filter and sort series based on input and author priority useEffect(() => { let filtered: Series[]; if (!inputValue.trim()) { filtered = [...series]; } else { filtered = series.filter(s => s.name.toLowerCase().includes(inputValue.toLowerCase()) ); } // Sort series: prioritize those from the current author if authorId is provided if (authorId && authorSeriesMap[authorId]) { const authorSeriesNames = authorSeriesMap[authorId]; filtered.sort((a, b) => { const aIsAuthorSeries = authorSeriesNames.includes(a.name); const bIsAuthorSeries = authorSeriesNames.includes(b.name); if (aIsAuthorSeries && !bIsAuthorSeries) return -1; // a first if (!aIsAuthorSeries && bIsAuthorSeries) return 1; // b first // If both or neither are author series, sort alphabetically return a.name.localeCompare(b.name); }); } else { // No author prioritization, just sort alphabetically filtered.sort((a, b) => a.name.localeCompare(b.name)); } setFilteredSeries(filtered); }, [inputValue, series, authorId, authorSeriesMap]); // 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 ? (
Loading series...
) : ( <> {filteredSeries.length > 0 ? ( filteredSeries.map((s) => { const isAuthorSeries = authorId && authorSeriesMap[authorId]?.includes(s.name); return ( ); }) ) : ( <> {inputValue.trim() && ( )} {!inputValue.trim() && (
No series found
)} )} {inputValue.trim() && !filteredSeries.some(s => s.name.toLowerCase() === inputValue.toLowerCase()) && ( )} )}
)} {error && (

{error}

)}
); }