import { useState, useEffect, useRef } from 'react'; import { Website } from '../types'; import { getWebsiteIcon } from './utils/iconService'; import ModalShell from './ModalShell'; 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 getIconPickUrl = (iconData: IconMetadata): string => `https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/${iconData.base}/${iconData.name}.${iconData.base}`; 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(() => iconMetadataCache ?? []); const [iconsFetched, setIconsFetched] = useState(() => iconMetadataCache !== null); const debounceRef = useRef(null); useEffect(() => { return () => { iconMetadataCache = null; }; }, []); const ensureIconMetadata = () => { if (iconMetadataCache) { setIconMetadata(iconMetadataCache); return; } if (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 && typeof ic.colors === 'object') { const colors = Object.values(ic.colors).filter(key => typeof key === 'string' && key !== ic.name); for (const color of colors as string[]) { if (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); } }; return ( onSave({ id: website?.id, name, url, icon })} onDelete={edit ? onDelete : undefined} >
{icon ? ( Website Icon ) : (
)}
setName(e.target.value)} className="liquid-input p-3" /> setUrl(e.target.value)} className="liquid-input p-3" />
{ setIcon(e.target.value); setIconQuery(e.target.value); }} onFocus={ensureIconMetadata} className="liquid-input p-3" /> {filteredIcons.length > 0 && (
{filteredIcons.map((iconData, index) => (
{ setIcon(getIconPickUrl(iconData)); setFilteredIcons([]); }} className="cursor-pointer flex items-center p-2 transition-colors duration-150 ease-ios hover:bg-white/20" > {iconData.name} {iconData.name}
))}
)}
); }; export default WebsiteEditModal;