/**
* 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`

` : 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`
${status.message}
` : ''}
${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' : ''}`}
${Button}>
` : ''}
`;
}
export default FileUpload;