import React, { useState, useEffect, useCallback, useRef } from 'react'; import { searchLibraries, installLibrary, getInstalledLibraries } from '../../services/libraryService'; import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService'; import { trackInstallLibrary } from '../../utils/analytics'; import './LibraryManagerModal.css'; interface LibraryManagerModalProps { isOpen: boolean; onClose: () => void; } type Tab = 'search' | 'installed'; export const LibraryManagerModal: React.FC = ({ isOpen, onClose }) => { const [activeTab, setActiveTab] = useState('search'); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [installedLibraries, setInstalledLibraries] = useState([]); const [loadingSearch, setLoadingSearch] = useState(false); const [loadingInstalled, setLoadingInstalled] = useState(false); const [installingLib, setInstallingLib] = useState(null); const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const debounceRef = useRef | null>(null); const fetchInstalled = useCallback(async () => { setLoadingInstalled(true); try { const libs = await getInstalledLibraries(); setInstalledLibraries(libs); } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Failed to load installed libraries' }); } finally { setLoadingInstalled(false); } }, []); // Reset state when modal closes useEffect(() => { if (!isOpen) { setSearchQuery(''); setSearchResults([]); setStatusMsg(null); } }, [isOpen]); // Fetch installed list when modal opens or switching to installed tab useEffect(() => { if (isOpen && activeTab === 'installed') fetchInstalled(); }, [isOpen, activeTab, fetchInstalled]); useEffect(() => { if (isOpen) fetchInstalled(); }, [isOpen, fetchInstalled]); // Search: immediate on open (empty query), debounced when typing useEffect(() => { if (!isOpen) return; if (debounceRef.current) clearTimeout(debounceRef.current); const delay = searchQuery ? 400 : 0; debounceRef.current = setTimeout(async () => { setLoadingSearch(true); setStatusMsg(null); try { const results = await searchLibraries(searchQuery); setSearchResults(results); } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Search failed' }); setSearchResults([]); } finally { setLoadingSearch(false); } }, delay); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [searchQuery, isOpen]); const handleInstall = async (libName: string) => { setInstallingLib(libName); setStatusMsg(null); try { const result = await installLibrary(libName); if (result.success) { trackInstallLibrary(libName); setStatusMsg({ type: 'success', text: `"${libName}" installed successfully!` }); fetchInstalled(); // Refresh installed list so search tab reflects new state } else { setStatusMsg({ type: 'error', text: result.error || `Failed to install "${libName}"` }); } } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Installation failed' }); } finally { setInstallingLib(null); } }; const handleClose = () => { onClose(); }; if (!isOpen) return null; const isInstalled = (libName: string): boolean => installedLibraries.some( (il) => (il.library?.name || il.name || '').toLowerCase() === libName.toLowerCase() ); const getLibName = (lib: ArduinoLibrary): string => lib.name || 'Unknown'; const getLibVersion = (lib: ArduinoLibrary): string => lib.latest?.version || lib.version || ''; const getLibAuthor = (lib: ArduinoLibrary): string => lib.latest?.author || lib.author || ''; const getLibDesc = (lib: ArduinoLibrary): string => lib.latest?.sentence || lib.sentence || ''; const getInstalledName = (lib: InstalledLibrary): string => lib.library?.name || lib.name || 'Unknown'; const getInstalledVersion = (lib: InstalledLibrary): string => lib.library?.version || lib.version || ''; const getInstalledAuthor = (lib: InstalledLibrary): string => lib.library?.author || lib.author || ''; const getInstalledDesc = (lib: InstalledLibrary): string => lib.library?.sentence || lib.sentence || ''; return (
e.stopPropagation()}> {/* Header */}
LIBRARY MANAGER
{/* Tabs */}
{/* Status bar */} {statusMsg && (
{statusMsg.type === 'success' ? ( ) : ( )} {statusMsg.text}
)} {/* Search Tab */} {activeTab === 'search' && (
setSearchQuery(e.target.value)} autoFocus /> {loadingSearch && ( )}
{loadingSearch && (

{searchQuery ? `Searching "${searchQuery}"…` : 'Loading libraries…'}

)} {!loadingSearch && searchResults.length === 0 && (

{searchQuery ? `No libraries found for "${searchQuery}"` : 'No libraries available'}

)} {!loadingSearch && searchResults.map((lib, i) => (
{getLibName(lib)} {getLibAuthor(lib) && ( by {getLibAuthor(lib)} )}
{getLibDesc(lib) && (

{getLibDesc(lib)}

)}
{getLibVersion(lib) && ( {getLibVersion(lib)} )} {isInstalled(getLibName(lib)) ? ( INSTALLED ) : ( )}
))}
)} {/* Installed Tab */} {activeTab === 'installed' && (
{loadingInstalled && (

Loading installed libraries...

)} {!loadingInstalled && installedLibraries.length === 0 && (

No libraries installed yet.

Use the Search tab to install libraries.

)} {installedLibraries.map((lib, i) => (
{getInstalledName(lib)} {getInstalledAuthor(lib) && ( by {getInstalledAuthor(lib)} )}
{getInstalledDesc(lib) && (

{getInstalledDesc(lib)}

)}
{getInstalledVersion(lib) && ( {getInstalledVersion(lib)} )}
))}
)}
); };