import { useState, useEffect, useRef } from 'react'; import { Website } from '../types'; import { getWebsiteIcon } from './utils/iconService'; interface WebsiteEditModalProps { website?: Website; edit: boolean; onClose: () => void; onSave: (website: Partial) => void; onDelete: () => void; } interface IconMetadata { name: string; base: string; aliases: string[]; categories: string[]; update: { timestamp: string; author: { id: number; name: string; }; }; colors: any; } let iconMetadataCache: IconMetadata[] | null = null; const WebsiteEditModal: React.FC = ({ website, edit, onClose, onSave, onDelete }) => { const [name, setName] = useState(website ? website.name : ''); const [url, setUrl] = useState(website ? website.url : ''); const [icon, setIcon] = useState(website ? website.icon : ''); const [iconQuery, setIconQuery] = useState(''); const [filteredIcons, setFilteredIcons] = useState([]); const [iconMetadata, setIconMetadata] = useState([]); const [iconsFetched, setIconsFetched] = useState(false); const debounceRef = useRef(null); const ensureIconMetadata = () => { if (iconMetadataCache || iconsFetched) return; setIconsFetched(true); fetch('/icon-metadata.json', { cache: 'force-cache' }) .then(response => response.json()) .then(data => { const iconsArray: IconMetadata[] = Object.entries(data).map(([name, details]) => ({ name, ...(details as object), })) as IconMetadata[]; iconMetadataCache = iconsArray; setIconMetadata(iconsArray); }) .catch(err => console.error('Failed to load icon metadata', err)); }; useEffect(() => { if (iconQuery && Array.isArray(iconMetadata) && iconMetadata.length > 0) { if (debounceRef.current) window.clearTimeout(debounceRef.current); debounceRef.current = window.setTimeout(() => { const lowerCaseQuery = iconQuery.toLowerCase(); const filtered: IconMetadata[] = []; for (const ic of iconMetadata) { if (ic.name.toLowerCase().includes(lowerCaseQuery)) { filtered.push(ic); if (filtered.length >= 50) break; } if (ic.colors) { const colors = Object.values(ic.colors).filter(key => key !== ic.name); for (const color of colors) { if (typeof color === 'string' && color.toLowerCase().includes(lowerCaseQuery)) { filtered.push({ ...ic, name: color }); if (filtered.length >= 50) break; } } if (filtered.length >= 50) break; } } setFilteredIcons(filtered); }, 150); return () => { if (debounceRef.current) window.clearTimeout(debounceRef.current); }; } else { setFilteredIcons([]); } }, [iconQuery, iconMetadata]); const fetchIcon = async () => { if (url) { const fetchedIcon = await getWebsiteIcon(url); setIcon(fetchedIcon); } }; const handleSave = () => { onSave({ id: website?.id, name, url, icon }); }; const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return (

{edit ? 'Edit Website' : 'Add Website'}

{icon ? ( Website Icon ) : (
)}
setName(e.target.value)} className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" /> setUrl(e.target.value)} className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400" />
{ setIcon(e.target.value); setIconQuery(e.target.value); }} onFocus={ensureIconMetadata} className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full" /> {filteredIcons.length > 0 && (
{filteredIcons.map(iconData => (
{ const iconUrl = `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/${iconData.base}/${iconData.name}.${iconData.base}`; setIcon(iconUrl); setFilteredIcons([]); }} className="cursor-pointer flex items-center p-2 hover:bg-gray-700" > {iconData.name} {iconData.name}
))}
)}
{edit && ( )}
); }; export default WebsiteEditModal;