Report-Generator / static /js /components /FileUpload.js
root
Working CHanges to revesion ; add preact support
e8a57cb
/**
* 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`
<div
class="border-2 border-dashed rounded-3 p-5 text-center ${isDragOver ? 'border-primary bg-primary bg-opacity-10' : 'border-secondary'} ${className}"
onDragOver=${onDragOver}
onDragLeave=${onDragLeave}
onDrop=${onDrop}
onClick=${onClick}
style="cursor: pointer; transition: all 0.2s ease;"
>
<input
ref=${inputRef}
type="file"
accept=${accept}
multiple=${multiple}
onChange=${onInputChange}
style="display: none;"
/>
<i class="bi bi-cloud-upload display-4 text-primary mb-3"></i>
<h5 class="mb-2">Drag & Drop files here</h5>
<p class="text-muted mb-0">or click to browse</p>
${accept ? html`<p class="text-muted small mt-2">Accepted: ${accept}</p>` : ''}
${multiple && maxFiles ? html`<p class="text-muted small">Max ${maxFiles} files</p>` : ''}
</div>
`;
}
// === 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`
<div
class="${cardClass}"
draggable="true"
data-index=${index}
onDragStart=${(e) => onDragStart?.(e, index)}
onDragOver=${(e) => onDragOver?.(e, index)}
onDrop=${(e) => onDrop?.(e, index)}
>
${previewUrl ? html`
<img src=${previewUrl} class="preview-img" alt=${file.name} />
` : html`
<div class="preview-img d-flex align-items-center justify-content-center bg-secondary">
<i class="bi bi-file-earmark display-4"></i>
</div>
`}
<div class="preview-overlay">
${file.name}
</div>
<button
class="remove-btn"
onClick=${(e) => {
e.stopPropagation();
onRemove?.(index);
}}
>
&times;
</button>
</div>
`;
}
// === 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`
<div class="row g-3 ${className}">
${files.map((file, index) => html`
<div class="col-6 col-md-4 col-lg-3">
<${FilePreviewCard}
file=${file}
index=${index}
onRemove=${removeFile}
onDragStart=${handleDragStart}
onDragOver=${handleDragOver}
onDrop=${handleDrop}
isDragging=${draggedIndex === index}
/>
</div>
`)}
</div>
`;
}
// === 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`
<div class="${className}">
<${FileUpload}
accept=${accept}
multiple=${multiple}
onFilesSelected=${handleFilesSelected}
className="mb-4"
/>
${files.length > 0 ? html`
<>
<h6 class="text-white-50 border-bottom border-secondary pb-2 mb-3">
Preview ${sortable ? html`<span class="small">(Drag to reorder)</span>` : ''}
</h6>
<${FilePreviewGrid}
files=${files}
onFilesChange=${setFiles}
sortable=${sortable}
className="mb-4"
/>
</>
` : ''}
${status ? html`
<div class="alert alert-${status.type} alert-dismissible fade show" role="alert">
${status.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
` : ''}
${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}>
` : ''}
</div>
`;
}
export default FileUpload;