/** * File Upload Component with Drag & Drop and Preview * For image/file upload interfaces */ const { useState, useRef, useCallback, useEffect } = window.PreactLib || {}; export function FileUpload({ accept = 'image/*', multiple = true, maxFiles = null, onFilesSelected, className = '' }) { const html = window.html; const [isDragOver, setIsDragOver] = useState(false); const inputRef = useRef(null); const handleFiles = useCallback((files) => { const fileArray = Array.from(files); if (onFilesSelected) { onFilesSelected(fileArray); } }, [onFilesSelected]); const onDragOver = useCallback((e) => { e.preventDefault(); setIsDragOver(true); }, []); const onDragLeave = useCallback((e) => { e.preventDefault(); setIsDragOver(false); }, []); const onDrop = useCallback((e) => { e.preventDefault(); setIsDragOver(false); handleFiles(e.dataTransfer.files); }, [handleFiles]); const onClick = () => { inputRef.current?.click(); }; const onInputChange = (e) => { handleFiles(e.target.files); // Reset input so same file can be selected again e.target.value = ''; }; return html`
Drag & Drop files here

or click to browse

${accept ? html`

Accepted: ${accept}

` : ''} ${multiple && maxFiles ? html`

Max ${maxFiles} files

` : ''}
`; } // === File Preview Card === export function FilePreviewCard({ file, index, onRemove, onDragStart, onDrop, onDragOver, isDragging = false }) { const html = window.html; const [previewUrl, setPreviewUrl] = useState(null); useEffect(() => { if (file && file.type?.startsWith('image/')) { const url = URL.createObjectURL(file); setPreviewUrl(url); return () => URL.revokeObjectURL(url); } }, [file]); const cardClass = isDragging ? 'preview-card dragging' : 'preview-card'; return html`
onDragStart?.(e, index)} onDragOver=${(e) => onDragOver?.(e, index)} onDrop=${(e) => onDrop?.(e, index)} > ${previewUrl ? html` ${file.name} ` : html`
`}
${file.name}
`; } // === File Preview Grid === export function FilePreviewGrid({ files = [], onFilesChange, sortable = true, className = '' }) { const html = window.html; const [draggedIndex, setDraggedIndex] = useState(null); if (files.length === 0) return null; const removeFile = (index) => { const newFiles = files.filter((_, i) => i !== index); onFilesChange?.(newFiles); }; const handleDragStart = (e, index) => { if (!sortable) return; setDraggedIndex(index); e.target.classList.add('dragging'); }; const handleDragOver = (e, index) => { if (!sortable || draggedIndex === null) return; e.preventDefault(); if (draggedIndex !== index) { e.currentTarget.classList.add('drag-over'); } }; const handleDragLeave = (e) => { e.currentTarget.classList.remove('drag-over'); }; const handleDrop = (e, dropIndex) => { if (!sortable || draggedIndex === null) return; e.preventDefault(); e.currentTarget.classList.remove('drag-over'); if (draggedIndex !== dropIndex) { const newFiles = [...files]; const [draggedFile] = newFiles.splice(draggedIndex, 1); newFiles.splice(dropIndex, 0, draggedFile); onFilesChange?.(newFiles); } setDraggedIndex(null); }; const handleDragEnd = (e) => { e.target.classList.remove('dragging'); setDraggedIndex(null); document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over'); }); }; return html`
${files.map((file, index) => html`
<${FilePreviewCard} file=${file} index=${index} onRemove=${removeFile} onDragStart=${handleDragStart} onDragOver=${handleDragOver} onDrop=${handleDrop} isDragging=${draggedIndex === index} />
`)}
`; } // === Complete Upload Interface === export function UploadInterface({ accept = 'image/*', multiple = true, maxFiles = null, onUpload, className = '' }) { const html = window.html; const [files, setFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [status, setStatus] = useState(null); const handleFilesSelected = (newFiles) => { if (maxFiles && files.length + newFiles.length > maxFiles) { setStatus({ type: 'danger', message: `Maximum ${maxFiles} files allowed` }); return; } setFiles([...files, ...newFiles]); setStatus(null); }; const handleUpload = async () => { if (files.length === 0) return; setIsUploading(true); setStatus({ type: 'info', message: `Uploading ${files.length} files...` }); try { if (onUpload) { await onUpload(files); } setStatus({ type: 'success', message: 'Upload successful!' }); setFiles([]); } catch (error) { setStatus({ type: 'danger', message: `Upload failed: ${error.message}` }); } finally { setIsUploading(false); } }; return html`
<${FileUpload} accept=${accept} multiple=${multiple} onFilesSelected=${handleFilesSelected} className="mb-4" /> ${files.length > 0 ? html` <>
Preview ${sortable ? html`(Drag to reorder)` : ''}
<${FilePreviewGrid} files=${files} onFilesChange=${setFiles} sortable=${sortable} className="mb-4" /> ` : ''} ${status ? html` ` : ''} ${files.length > 0 ? html` <${Button} variant="success" size="lg" className="w-100 py-3" onClick=${handleUpload} loading=${isUploading} > ${isUploading ? 'Uploading...' : `Upload ${files.length} File${files.length > 1 ? 's' : ''}`} ` : ''}
`; } export default FileUpload;