@health-ecosystem/file-upload
Version:
File upload library for Health Ecosystem applications with improved Next.js 15+ compatibility
1,244 lines (1,196 loc) • 217 kB
JavaScript
'use strict';
var React = require('react');
var axios = require('axios');
var jsxRuntime = require('react/jsx-runtime');
var lucideReact = require('lucide-react');
var ReactDOM = require('react-dom');
var clsx = require('clsx');
var tailwindMerge = require('tailwind-merge');
var classVarianceAuthority = require('class-variance-authority');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM);
/**
* React Compatibility Layer
* Handles differences between React versions (18, 19+)
*/
// Check React version
const REACT_VERSION = React.version;
const IS_REACT_19_PLUS = parseInt(React.version.split('.')[0]) >= 19;
const IS_REACT_18 = React.version.startsWith('18.');
// Export compatibility info
const REACT_COMPAT_INFO = {
version: REACT_VERSION,
isReact19Plus: IS_REACT_19_PLUS,
isReact18: IS_REACT_18,
supportsNewJSXTransform: true, // Both 18 and 19+ support this
supportsConcurrentFeatures: true
};
/**
* File Upload Client
* Production-ready client for Health Ecosystem file uploads
*/
class FileUploadClient {
constructor(config) {
this.cancelTokens = new Map();
const defaultAllowedTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain', 'text/csv'
];
this.config = {
maxFileSize: config.maxFileSize || 50 * 1024 * 1024, // 50MB
allowedTypes: config.allowedTypes || defaultAllowedTypes,
timeout: config.timeout || 30000, // 30 seconds
enableRetry: config.enableRetry ?? true,
maxRetries: config.maxRetries || 3,
baseUrl: config.baseUrl,
authToken: config.authToken
};
this.client = axios.create({
baseURL: this.config.baseUrl,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.authToken}`
}
});
// Add response interceptor for error handling
this.client.interceptors.response.use((response) => response, (error) => this.handleError(error));
}
/**
* Upload a single file
*/
async uploadFile(file, options = { category: 'other' }, callbacks) {
const uploadId = this.generateUploadId();
try {
// Validate file
this.validateFile(file);
// Create cancel token
const cancelToken = axios.CancelToken.source();
this.cancelTokens.set(uploadId, cancelToken);
// Prepare form data
const formData = new FormData();
formData.append('file', file);
formData.append('file_category', options.category || 'other');
if (options.entityType) {
formData.append('associated_entity_type', options.entityType);
}
if (options.entityId) {
formData.append('associated_entity_id', options.entityId);
}
formData.append('is_public', String(options.isPublic || false));
if (options.metadata) {
formData.append('metadata', options.metadata);
}
// Call onStart callback
options.onStart?.();
// Upload with progress tracking
const response = await this.client.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
cancelToken: cancelToken.token,
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
options.onProgress?.(progress);
callbacks?.onProgress?.(progress);
}
}
});
// Clean up cancel token
this.cancelTokens.delete(uploadId);
// Parse response
const result = this.parseUploadResponse(response.data);
// Call success callback
options.onSuccess?.(result);
return result;
}
catch (error) {
// Clean up cancel token
this.cancelTokens.delete(uploadId);
const uploadError = this.createUploadError(error);
// Call error callback
options.onError?.(uploadError);
throw uploadError;
}
}
/**
* Upload multiple files
*/
async uploadFiles(files, options) {
const results = [];
const errors = [];
for (let i = 0; i < files.length; i++) {
try {
const fileOptions = {
...options,
onProgress: (progress) => {
const overallProgress = ((i * 100) + progress) / files.length;
options.onProgress?.(Math.round(overallProgress));
}
};
const result = await this.uploadFile(files[i], fileOptions);
results.push(result);
}
catch (error) {
errors.push(error);
}
}
if (errors.length > 0 && results.length === 0) {
throw new Error(`All uploads failed: ${errors.map(e => e.message).join(', ')}`);
}
return results;
}
/**
* Upload file from URL
* Downloads the file from the provided URL and uploads it to the server
*/
async uploadFromUrl(url, options) {
const uploadId = this.generateUploadId();
try {
// Validate URL
if (!this.isValidUrl(url)) {
throw new Error('Invalid URL provided');
}
// Call start callback
options.onStart?.();
// Create cancel token
const cancelToken = axios.CancelToken.source();
this.cancelTokens.set(uploadId, cancelToken);
// Step 1: Download file from URL with progress tracking
options.onProgress?.(10); // Initial progress
const fileResponse = await axios.get(url, {
responseType: 'blob',
cancelToken: cancelToken.token,
onDownloadProgress: (progressEvent) => {
if (progressEvent.total) {
// Use 10-60% for download progress
const downloadProgress = Math.round((progressEvent.loaded * 50) / progressEvent.total) + 10;
options.onProgress?.(downloadProgress);
}
}
});
// Step 2: Extract filename and content type
const filename = options.filename || this.extractFilenameFromUrl(url) || 'downloaded-file';
const contentType = fileResponse.headers['content-type'] || 'application/octet-stream';
// Create File object from blob
const file = new File([fileResponse.data], filename, { type: contentType });
// Validate file size
if (file.size > this.config.maxFileSize) {
throw new Error(`File size (${file.size} bytes) exceeds maximum allowed size (${this.config.maxFileSize} bytes)`);
}
// Validate file type
if (!this.isAllowedFileType(file.type)) {
throw new Error(`File type ${file.type} is not allowed`);
}
options.onProgress?.(70); // Download complete, starting upload
// Step 3: Upload the file
const formData = new FormData();
formData.append('file', file);
// Add metadata
if (options.category)
formData.append('file_category', options.category);
if (options.entityType)
formData.append('entity_type', options.entityType);
if (options.entityId)
formData.append('entity_id', options.entityId);
if (options.isPublic !== undefined)
formData.append('is_public', options.isPublic.toString());
if (options.metadata)
formData.append('metadata', options.metadata);
// Add source URL as metadata
const sourceMetadata = { source_url: url, upload_method: 'url' };
const existingMetadata = options.metadata ? JSON.parse(options.metadata) : {};
formData.append('metadata', JSON.stringify({ ...existingMetadata, ...sourceMetadata }));
const response = await this.client.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
cancelToken: cancelToken.token,
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
// Use 70-100% for upload progress
const uploadProgress = Math.round((progressEvent.loaded * 30) / progressEvent.total) + 70;
options.onProgress?.(uploadProgress);
}
}
});
// Clean up cancel token
this.cancelTokens.delete(uploadId);
// Parse response
const result = this.parseUploadResponse(response.data);
// Call success callback
options.onSuccess?.(result);
return result;
}
catch (error) {
// Clean up cancel token
this.cancelTokens.delete(uploadId);
if (axios.isCancel(error)) {
const cancelError = {
code: 'UPLOAD_CANCELLED',
message: 'URL upload was cancelled',
status: 0
};
options.onError?.(cancelError);
throw cancelError;
}
const uploadError = this.createUploadError(error);
// Call error callback
options.onError?.(uploadError);
throw uploadError;
}
}
/**
* Get file metadata
*/
async getFileMetadata(fileId) {
try {
const response = await this.client.get(`/files/${fileId}`);
return this.parseFileMetadata(response.data.data);
}
catch (error) {
throw this.createUploadError(error);
}
}
/**
* Get download URL for a file
*/
getDownloadUrl(fileId) {
return `${this.config.baseUrl}/files/${fileId}/download`;
}
/**
* Get thumbnail URL for an image
*/
getThumbnailUrl(fileId) {
return `${this.config.baseUrl}/files/${fileId}/thumbnail`;
}
/**
* Get preview URL for a file with optional transformations
* @param previewUrl The preview URL from the file response
* @param options Optional transformation parameters
* @returns Full URL to the preview
*/
getPreviewUrl(previewUrl, options) {
if (!previewUrl)
return '';
const baseUrl = `${this.config.baseUrl}${previewUrl}`;
const params = new URLSearchParams();
if (options?.thumbnail)
params.append('thumbnail', 'true');
if (options?.width)
params.append('width', options.width.toString());
if (options?.height)
params.append('height', options.height.toString());
if (options?.quality)
params.append('quality', options.quality.toString());
if (options?.format)
params.append('format', options.format);
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
/**
* Delete a file
*/
async deleteFile(fileId) {
try {
await this.client.delete(`/files/${fileId}`);
}
catch (error) {
throw this.createUploadError(error);
}
}
/**
* List files with optional filters - Updated to match backend API structure
*/
async listFiles(options) {
try {
const params = new URLSearchParams();
// Set default pagination
params.append('skip', (options?.skip || 0).toString());
params.append('limit', (options?.limit || 100).toString());
if (options?.category)
params.append('file_category', options.category);
if (options?.entityType)
params.append('associated_entity_type', options.entityType);
if (options?.entityId)
params.append('associated_entity_id', options.entityId);
if (options?.isPublic !== undefined)
params.append('is_public', options.isPublic.toString());
// Use the correct endpoint: /files (since baseUrl already includes /api/files)
const response = await this.client.get(`/files?${params.toString()}`);
// Backend returns array directly, not wrapped in data object
const files = Array.isArray(response.data) ? response.data : (response.data.data || []);
return files.map((file) => this.parseFileMetadata(file));
}
catch (error) {
throw this.createUploadError(error);
}
}
/**
* Cancel an ongoing upload
*/
cancelUpload(uploadId) {
if (uploadId && this.cancelTokens.has(uploadId)) {
this.cancelTokens.get(uploadId)?.cancel('Upload cancelled by user');
this.cancelTokens.delete(uploadId);
}
else {
// Cancel all uploads
this.cancelTokens.forEach((cancelToken) => {
cancelToken.cancel('Upload cancelled by user');
});
this.cancelTokens.clear();
}
}
/**
* Update authentication token
*/
updateAuthToken(token) {
this.config.authToken = token;
this.client.defaults.headers['Authorization'] = `Bearer ${token}`;
}
/**
* Validate file before upload
*/
validateFile(file) {
// Check file size
if (file.size > this.config.maxFileSize) {
throw new Error(`File size (${this.formatFileSize(file.size)}) exceeds maximum allowed size (${this.formatFileSize(this.config.maxFileSize)})`);
}
// Check file type - ensure allowedTypes is an array and has items
if (this.config.allowedTypes && Array.isArray(this.config.allowedTypes) && this.config.allowedTypes.length > 0) {
const isAllowed = this.config.allowedTypes.some(allowedType => {
// Handle wildcard patterns like "image/*"
if (allowedType.includes('*')) {
const baseType = allowedType.split('/')[0];
return file.type.startsWith(baseType + '/');
}
// Handle exact matches
return allowedType === file.type;
});
if (!isAllowed) {
throw new Error(`File type '${file.type}' is not allowed. Allowed types: ${this.config.allowedTypes.join(', ')}`);
}
}
}
/**
* Parse upload response
*/
parseUploadResponse(data) {
const responseData = data.data || data;
return {
id: responseData.file_id || responseData.id || `file-${Date.now()}`,
fileId: responseData.file_id || responseData.id || `file-${Date.now()}`,
originalFilename: responseData.original_filename || responseData.originalFilename,
storedFilename: responseData.stored_filename || responseData.storedFilename,
filename: responseData.stored_filename || responseData.storedFilename || responseData.filename,
fileSize: responseData.file_size || responseData.fileSize || 0,
mimeType: responseData.mime_type || responseData.mimeType || 'application/octet-stream',
category: responseData.file_category || responseData.category || 'other',
downloadUrl: responseData.download_url || responseData.downloadUrl || this.getDownloadUrl(responseData.file_id || responseData.id),
thumbnailUrl: responseData.thumbnail_url || responseData.thumbnailUrl || ((responseData.mime_type || responseData.mimeType)?.startsWith('image/') ?
this.getThumbnailUrl(responseData.file_id || responseData.id) :
undefined),
previewUrl: responseData.preview_url || responseData.previewUrl,
uploadedAt: responseData.uploaded_at || responseData.uploadedAt || new Date().toISOString(),
isPublic: responseData.is_public || responseData.isPublic || false
};
}
/**
* Parse file metadata response - Updated to match backend structure
*/
parseFileMetadata(data) {
return {
id: data.id,
originalFilename: data.original_filename,
fileSize: data.file_size,
mimeType: data.mime_type,
category: data.file_category,
uploadedBy: data.uploaded_by,
entityType: data.associated_entity_type,
entityId: data.associated_entity_id,
isPublic: data.is_public,
createdAt: data.created_at,
downloadUrl: data.download_url,
thumbnailUrl: data.thumbnail_url,
previewUrl: data.preview_url
};
}
/**
* Create standardized upload error
*/
createUploadError(error) {
if (axios.isCancel(error)) {
return {
code: 'UPLOAD_CANCELLED',
message: 'Upload was cancelled',
status: 0
};
}
if (error.response) {
return {
code: error.response.data?.code || 'HTTP_ERROR',
message: error.response.data?.detail || error.response.data?.message || error.message,
status: error.response.status,
details: error.response.data
};
}
if (error.request) {
return {
code: 'NETWORK_ERROR',
message: 'Network error occurred during upload',
details: error.request
};
}
return {
code: 'UNKNOWN_ERROR',
message: error.message || 'An unknown error occurred',
details: error
};
}
/**
* Handle axios errors with retry logic
*/
async handleError(error) {
if (this.config.enableRetry && this.shouldRetry(error)) ;
return Promise.reject(error);
}
/**
* Determine if error should be retried
*/
shouldRetry(error) {
if (!error.response)
return true; // Network errors
const status = error.response.status;
return status >= 500 || status === 429; // Server errors or rate limiting
}
/**
* Generate unique upload ID
*/
generateUploadId() {
return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Format file size for display
*/
formatFileSize(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Validate URL format
*/
isValidUrl(url) {
try {
const urlObj = new URL(url);
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
}
catch {
return false;
}
}
/**
* Extract filename from URL
*/
extractFilenameFromUrl(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const filename = pathname.split('/').pop() || '';
// If no filename or just extension, generate a default name
if (!filename || filename.startsWith('.')) {
const extension = this.getFileExtensionFromUrl(url);
return `downloaded-file${extension ? '.' + extension : ''}`;
}
return filename;
}
catch {
return 'downloaded-file';
}
}
/**
* Get file extension from URL or content type
*/
getFileExtensionFromUrl(url) {
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const lastDot = pathname.lastIndexOf('.');
if (lastDot > 0) {
return pathname.substring(lastDot + 1).toLowerCase();
}
}
catch {
// Ignore URL parsing errors
}
return '';
}
/**
* Check if file type is allowed
*/
isAllowedFileType(mimeType) {
if (!this.config.allowedTypes || this.config.allowedTypes.length === 0) {
return true; // No restrictions
}
return this.config.allowedTypes.some(allowedType => {
if (allowedType === '*/*')
return true;
// Handle wildcard patterns like "image/*"
if (allowedType.includes('*')) {
const baseType = allowedType.split('/')[0];
return mimeType.startsWith(baseType + '/');
}
// Handle exact matches
return allowedType === mimeType;
});
}
}
const FileUploadLibrary = ({ config, options = {}, onFileSelect, multiple = false, accept = "*/*", className = "", style = {} }) => {
const [client] = React.useState(() => new FileUploadClient(config));
const [files, setFiles] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [uploadState, setUploadState] = React.useState({
selectedFile: null,
isUploading: false,
uploadProgress: 0,
error: null
});
// Load files from the library
const loadFiles = React.useCallback(async () => {
try {
setLoading(true);
const libraryFiles = await client.listFiles({
skip: 0,
limit: 100,
...options
});
setFiles(libraryFiles);
}
catch (error) {
console.error('Failed to load files:', error);
setUploadState(prev => ({ ...prev, error: 'Failed to load files' }));
}
finally {
setLoading(false);
}
}, [client, options]);
// Load files on component mount
React.useEffect(() => {
loadFiles();
}, [loadFiles]);
// Handle file selection for upload
const handleFileSelect = (event) => {
const file = event.target.files?.[0];
if (file) {
setUploadState(prev => ({
...prev,
selectedFile: file,
error: null
}));
}
};
// Handle file upload
const handleUpload = async () => {
if (!uploadState.selectedFile)
return;
try {
setUploadState(prev => ({
...prev,
isUploading: true,
uploadProgress: 0,
error: null
}));
const uploadOptions = {
...options,
onProgress: (progress) => {
setUploadState(prev => ({ ...prev, uploadProgress: progress }));
}
};
await client.uploadFile(uploadState.selectedFile, uploadOptions);
// Reset upload state and reload files
setUploadState({
selectedFile: null,
isUploading: false,
uploadProgress: 0,
error: null
});
// Reload the library to show the new file
await loadFiles();
}
catch (error) {
setUploadState(prev => ({
...prev,
isUploading: false,
error: error.message || 'Upload failed'
}));
}
};
// Handle file deletion
const handleDelete = async (fileId) => {
if (!confirm('Are you sure you want to delete this file?'))
return;
try {
await client.deleteFile(fileId);
await loadFiles(); // Reload files after deletion
}
catch (error) {
alert('Failed to delete file: ' + (error.message || 'Unknown error'));
}
};
// Handle file selection from library
const handleLibraryFileSelect = (file) => {
onFileSelect?.(file);
};
// Handle preview
const handlePreview = (file) => {
if (file.previewUrl) {
const fullPreviewUrl = client.getPreviewUrl(file.previewUrl);
window.open(fullPreviewUrl, '_blank');
}
else if (file.downloadUrl) {
window.open(`${config.baseUrl}${file.downloadUrl}`, '_blank');
}
};
// Cancel file selection
const cancelSelection = () => {
setUploadState(prev => ({
...prev,
selectedFile: null,
error: null
}));
};
if (loading) {
return (jsxRuntime.jsx("div", { className: `file-upload-library ${className}`, style: style, children: jsxRuntime.jsxs("div", { className: "loading-state", children: [jsxRuntime.jsx("div", { className: "spinner" }), jsxRuntime.jsx("p", { children: "Loading files..." })] }) }));
}
return (jsxRuntime.jsxs("div", { className: `file-upload-library ${className}`, style: style, children: [jsxRuntime.jsxs("div", { className: "upload-section", children: [!uploadState.selectedFile ? (
// File selection area
jsxRuntime.jsxs("div", { className: "file-select-area", children: [jsxRuntime.jsx("input", { type: "file", id: "file-input", accept: accept, multiple: multiple, onChange: handleFileSelect, style: { display: 'none' } }), jsxRuntime.jsxs("label", { htmlFor: "file-input", className: "file-select-button", children: [jsxRuntime.jsx("div", { className: "upload-icon", children: "\uD83D\uDCC1" }), jsxRuntime.jsx("p", { children: "Click to select a file to upload" }), jsxRuntime.jsx("small", { children: "or drag and drop files here" })] })] })) : (
// Upload confirmation area
jsxRuntime.jsxs("div", { className: "upload-confirmation", children: [jsxRuntime.jsxs("div", { className: "selected-file-info", children: [jsxRuntime.jsx("div", { className: "file-icon", children: "\uD83D\uDCC4" }), jsxRuntime.jsxs("div", { className: "file-details", children: [jsxRuntime.jsx("h4", { children: uploadState.selectedFile.name }), jsxRuntime.jsxs("p", { children: [(uploadState.selectedFile.size / 1024 / 1024).toFixed(2), " MB"] })] })] }), uploadState.isUploading ? (jsxRuntime.jsxs("div", { className: "upload-progress", children: [jsxRuntime.jsx("div", { className: "progress-bar", children: jsxRuntime.jsx("div", { className: "progress-fill", style: { width: `${uploadState.uploadProgress}%` } }) }), jsxRuntime.jsxs("p", { children: [uploadState.uploadProgress, "% uploaded"] })] })) : (jsxRuntime.jsxs("div", { className: "upload-actions", children: [jsxRuntime.jsx("button", { className: "upload-button primary", onClick: handleUpload, children: "Upload File" }), jsxRuntime.jsx("button", { className: "cancel-button secondary", onClick: cancelSelection, children: "Cancel" })] }))] })), uploadState.error && (jsxRuntime.jsx("div", { className: "error-message", children: uploadState.error }))] }), jsxRuntime.jsxs("div", { className: "file-library", children: [jsxRuntime.jsx("h3", { children: "File Library" }), files.length === 0 ? (jsxRuntime.jsx("div", { className: "empty-state", children: jsxRuntime.jsx("p", { children: "No files uploaded yet" }) })) : (jsxRuntime.jsx("div", { className: "file-grid", children: files.map((file) => (jsxRuntime.jsxs("div", { className: "file-item", children: [jsxRuntime.jsxs("div", { className: "file-thumbnail", children: [file.thumbnailUrl ? (jsxRuntime.jsx("img", { src: `${config.baseUrl}${file.thumbnailUrl}`, alt: file.originalFilename, onError: (e) => {
// Fallback to file type icon if thumbnail fails
const target = e.target;
target.style.display = 'none';
target.nextElementSibling?.classList.remove('hidden');
} })) : null, jsxRuntime.jsx("div", { className: `file-type-icon ${file.thumbnailUrl ? 'hidden' : ''}`, children: getFileTypeIcon(file.mimeType) })] }), jsxRuntime.jsxs("div", { className: "file-info", children: [jsxRuntime.jsx("h4", { className: "file-name", title: file.originalFilename, children: file.originalFilename }), jsxRuntime.jsxs("p", { className: "file-size", children: [(file.fileSize / 1024 / 1024).toFixed(2), " MB"] }), jsxRuntime.jsx("p", { className: "file-date", children: new Date(file.createdAt).toLocaleDateString() })] }), jsxRuntime.jsxs("div", { className: "file-actions", children: [jsxRuntime.jsx("button", { className: "action-button select", onClick: () => handleLibraryFileSelect(file), title: "Select this file", children: "\u2713" }), jsxRuntime.jsx("button", { className: "action-button preview", onClick: () => handlePreview(file), title: "Preview file", children: "\uD83D\uDC41" }), jsxRuntime.jsx("button", { className: "action-button delete", onClick: () => handleDelete(file.id), title: "Delete file", children: "\uD83D\uDDD1" })] })] }, file.id))) }))] }), jsxRuntime.jsx("style", { dangerouslySetInnerHTML: {
__html: `
.file-upload-library {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.loading-state {
text-align: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.upload-section {
margin-bottom: 30px;
padding: 20px;
border: 2px dashed #ddd;
border-radius: 8px;
background: #fafafa;
}
.file-select-area {
text-align: center;
}
.file-select-button {
display: block;
padding: 40px;
cursor: pointer;
transition: background-color 0.2s;
}
.file-select-button:hover {
background-color: #f0f0f0;
}
.upload-icon {
font-size: 48px;
margin-bottom: 16px;
}
.upload-confirmation {
display: flex;
align-items: center;
gap: 20px;
}
.selected-file-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.file-icon {
font-size: 32px;
}
.file-details h4 {
margin: 0 0 4px 0;
font-size: 16px;
}
.file-details p {
margin: 0;
color: #666;
font-size: 14px;
}
.upload-progress {
flex: 1;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background-color: #007bff;
transition: width 0.3s ease;
}
.upload-actions {
display: flex;
gap: 12px;
}
.upload-button, .cancel-button {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
.upload-button.primary {
background-color: #007bff;
color: white;
}
.upload-button.primary:hover {
background-color: #0056b3;
}
.cancel-button.secondary {
background-color: #6c757d;
color: white;
}
.cancel-button.secondary:hover {
background-color: #545b62;
}
.error-message {
margin-top: 12px;
padding: 12px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.file-library h3 {
margin-bottom: 20px;
color: #333;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.file-item {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
background: white;
transition: box-shadow 0.2s;
}
.file-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.file-thumbnail {
width: 100%;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 4px;
margin-bottom: 12px;
overflow: hidden;
}
.file-thumbnail img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.file-type-icon {
font-size: 48px;
opacity: 0.5;
}
.hidden {
display: none;
}
.file-info {
margin-bottom: 12px;
}
.file-name {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size, .file-date {
margin: 0;
font-size: 12px;
color: #666;
}
.file-actions {
display: flex;
gap: 8px;
justify-content: center;
}
.action-button {
width: 32px;
height: 32px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s;
}
.action-button:hover {
background-color: #f8f9fa;
}
.action-button.select:hover {
background-color: #d4edda;
border-color: #28a745;
}
.action-button.preview:hover {
background-color: #d1ecf1;
border-color: #17a2b8;
}
.action-button.delete:hover {
background-color: #f8d7da;
border-color: #dc3545;
}
`
} })] }));
};
// Helper function to get file type icon
function getFileTypeIcon(mimeType) {
if (mimeType.startsWith('image/'))
return '🖼️';
if (mimeType.startsWith('video/'))
return '🎥';
if (mimeType.startsWith('audio/'))
return '🎵';
if (mimeType.includes('pdf'))
return '📄';
if (mimeType.includes('word') || mimeType.includes('document'))
return '📝';
if (mimeType.includes('excel') || mimeType.includes('spreadsheet'))
return '📊';
if (mimeType.includes('powerpoint') || mimeType.includes('presentation'))
return '📽️';
if (mimeType.includes('zip') || mimeType.includes('archive'))
return '📦';
return '📄';
}
const UPLOAD_SOURCES$1 = [
{
id: "device",
name: "My Device",
icon: lucideReact.Folder,
description: "Upload from computer",
},
{
id: "camera",
name: "Camera",
icon: lucideReact.Camera,
description: "Take photo or video",
},
{
id: "googledrive",
name: "Google Drive",
icon: lucideReact.Cloud,
description: "Import from Google Drive",
},
{
id: "dropbox",
name: "Dropbox",
icon: lucideReact.Cloud,
description: "Import from Dropbox",
},
{
id: "onedrive",
name: "OneDrive",
icon: lucideReact.Cloud,
description: "Import from OneDrive",
},
{
id: "url",
name: "From URL",
icon: lucideReact.Link,
description: "Import from web URL",
},
{
id: "instagram",
name: "Instagram",
icon: lucideReact.Image,
description: "Import from Instagram",
},
{
id: "facebook",
name: "Facebook",
icon: lucideReact.Globe,
description: "Import from Facebook",
},
];
const FileUploadWidget = ({ config, options = {}, isOpen, onClose, onFileSelect, onSuccess, onError, multiple = true, accept = "*/*", maxFileSize, title = "Upload Files", showLibrary = true, className = "", }) => {
const [activeSource, setActiveSource] = React.useState("device");
const [uploadingFiles, setUploadingFiles] = React.useState([]);
const [libraryFiles, setLibraryFiles] = React.useState([]);
const [selectedFiles, setSelectedFiles] = React.useState([]);
const [isDragActive, setIsDragActive] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [urlInput, setUrlInput] = React.useState("");
const fileInputRef = React.useRef(null);
const cameraInputRef = React.useRef(null);
const clientRef = React.useRef();
const dragCounter = React.useRef(0);
// Initialize client
React.useEffect(() => {
if (!clientRef.current) {
clientRef.current = new FileUploadClient(config);
}
}, [config]);
// Load library files when widget opens
React.useEffect(() => {
if (isOpen && showLibrary && clientRef.current) {
loadLibraryFiles();
}
}, [isOpen, showLibrary]);
const loadLibraryFiles = async () => {
if (!clientRef.current)
return;
setIsLoading(true);
try {
const files = await clientRef.current.listFiles({
limit: 50,
isPublic: true,
});
// Convert FileMetadata to UploadResult format
const uploadResults = files.map((file) => ({
id: file.id,
fileId: file.id,
originalFilename: file.originalFilename,
storedFilename: file.id, // Use ID as stored filename
filename: file.id,
fileSize: file.fileSize,
mimeType: file.mimeType,
category: file.category,
downloadUrl: file.downloadUrl,
thumbnailUrl: file.thumbnailUrl,
uploadedAt: file.createdAt,
}));
setLibraryFiles(uploadResults);
}
catch (error) {
console.error("Failed to load library files:", error);
}
finally {
setIsLoading(false);
}
};
const handleFileSelect = React.useCallback((event) => {
const files = event.target.files;
if (files && files.length > 0) {
const fileArray = Array.from(files);
uploadFiles(fileArray);
}
// Reset input
if (event.target) {
event.target.value = "";
}
}, []);
const uploadFiles = async (files) => {
if (!clientRef.current)
return;
const uploadingFilesList = files.map((file) => ({
id: `${Date.now()}-${Math.random()}`,
file,
progress: 0,
status: "uploading",
preview: file.type.startsWith("image/")
? URL.createObjectURL(file)
: undefined,
}));
setUploadingFiles((prev) => [...prev, ...uploadingFilesList]);
const uploadPromises = uploadingFilesList.map(async (uploadingFile) => {
try {
const uploadOptions = {
category: "document",
isPublic: true,
...options,
onProgress: (progress) => {
setUploadingFiles((prev) => prev.map((f) => f.id === uploadingFile.id ? { ...f, progress } : f));
},
};
const result = await clientRef.current.uploadFile(uploadingFile.file, uploadOptions);
setUploadingFiles((prev) => prev.map((f) => f.id === uploadingFile.id
? { ...f, status: "completed", result, progress: 100 }
: f));
return result;
}
catch (error) {
setUploadingFiles((prev) => prev.map((f) => f.id === uploadingFile.id
? {
...f,
status: "error",
error: error instanceof Error ? error.message : "Upload failed",
}
: f));
throw error;
}
});
try {
const results = await Promise.all(uploadPromises);
onSuccess?.(results);
// Add to library
setLibraryFiles((prev) => [...results, ...prev]);
// Clear uploading files after delay
setTimeout(() => {
setUploadingFiles((prev) => prev.filter((f) => f.status === "uploading"));
}, 2000);
}
catch (error) {
onError?.(error instanceof Error ? error : new Error("Upload failed"));
}
};
const handleUrlUpload = async () => {
if (!urlInput.trim() || !clientRef.current)
return;
try {
const response = await fetch(urlInput);
const blob = await response.blob();
const filename = urlInput.split("/").pop() || "url-file";
const file = new File([blob], filename, { type: blob.type });
await uploadFiles([file]);
setUrlInput("");
}
catch (error) {
onError?.(error instanceof Error ? error : new Error("URL upload failed"));
}
};
const handleDragEnter = React.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current++;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragActive(true);
}
}, []);
const handleDragLeave = React.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragActive(false);
}
}, []);
const handleDragOver = React.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = React.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
dragCounter.current = 0;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
uploadFiles(files);
}
}, []);
const handleLibraryFileSelect = (file) => {
if (multiple) {
setSelectedFiles((prev) => {
const exists = prev.find((f) => f.fileId === file.fileId);
if (exists) {
return prev.filter((f) => f.fileId !== file.fileId);
}
return [...prev, file];
});
}
else {
setSelectedFiles([file]);
}
};
const handleConfirmSelection = () => {
if (selectedFiles.length > 0) {
onFileSelect?.(selectedFiles);
onClose();
}
};
const formatFileSize = (bytes) => {
if (bytes === 0)
return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
if (!isOpen)
return null;
return (jsxRuntime.jsx("div", { className: `fixed inset-0 z-50 flex items-center justify-center bg-black/20 p-4 ${className}`, children: jsxRuntime.jsxs("div", { className: "bg-white rounded-xl shadow-2xl w-full max-w-6xl h-full max-h-[90vh] flex flex-col overflow-hidden", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between p-6 border-b border-gray-200", children: [jsxRuntime.jsx("h2", { className: "text-xl font-semibold text-gray-900", children: title }), jsxRuntime.jsx("button", { onClick: onClose, className: "p-2 hover:bg-gray-100 rounded-lg transition-colors", children: jsxRuntime.jsx(lucideReact.X, { className: "w-5 h-5 text-gray-500" }) })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden", children: [jsxRuntime.jsxs("div", { className: "w-64 bg-gray-50 border-r border-gray-200 flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-4", children: [jsxRuntime.jsx("h3", { className: "text-sm font-medium text-gray-900 mb-3", children: "Upload from" }), jsxRuntime.jsx("nav", { className: "space-y-1", children: UPLOAD_SOURCES$1.map((source) => {
const Icon = source.icon;
const isActive = activeSource === source.id;
const isDisabled = !["device", "camera", "url"].includes(source.id);
return (jsxRuntime.jsxs("button", { onClick: () => !isDisabled &&
setActiveSource(source.id), disabled: isDisabled, className: `w-full flex items-center px-3 py-2 text-sm rounded-lg transition-colors ${isActive
? "bg-blue-100 text-blue-700 border border-blue-200"
: isDisabled
? "text-gray-400 cursor-not-allowed"
: "text-gray-700 hover:bg-gray-100"}`, children: [jsxRuntime.jsx(Icon, { className: "w-4 h-4 mr-3" }), jsxRuntime.jsxs("div", { className: "text-left", children: [jsxRuntime.jsx("div", { className: "font-medium", children: source.name }), isDisabled && (jsxRuntime.jsx("div", { className: "text-xs text-gray-400", children: "Coming soon" }))] })] }, source.id));
}) })] }), showLibrary && (jsxRuntime.jsxs("div", { className: "flex-1 border-t border-gray-200 p-4", children: [jsxRuntime.jsxs("h3", { className: "text-sm font-medium text-gray-900 mb-3", children: ["File Library (", selectedFiles.length, " selected)"] }), selectedFiles.length > 0 && (jsxRuntime.jsx("button", { onClick: handleConfirmSelection, className: "w-full mb-3 px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors", children: "Use Selected Files" }))] }))] }), jsxRuntime.jsx("div", { className: "flex-1 flex flex-col overflow-hidden", children: jsxRuntime.jsxs("div", { className: "flex-1 p-6 overflow-auto", children: [activeSource === "device" && (jsxRuntime.jsx(DeviceUploadContent, { onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, isDragActive: isDragActive, fileInputRef: fileInputRef, onFileSelect: handleFileSelect, accept: accept, multiple: multiple, uploadingFiles: uploadingFiles, formatFileSize: formatFileSize })), activeSource === "camera" && (jsxRuntime.jsx(CameraUploadContent, { cameraInputRef: cameraInputRef, onFileSelect: handleFileSelect })), activeSource === "url" && (jsxRuntime.jsx(UrlUploadContent, { urlInput: urlInput, setUrlInput: setUrlInput, onUpload: handleUrlUpload })), showLibrary && (jsxRuntime.jsx(FileLibraryContent, { files: libraryFiles, selectedFiles: selectedFiles, onFileSelect: handleLibraryFileSelect, isLoading: isLoading, formatFileSize: formatFileSize }))] }) })] })] }) }));
};
// Device Upload Content Component
const DeviceUploadContent = ({ onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive, fileInputRef, onFileSelect, accept, multiple, uploadingFiles, formatFileSize, }) => (jsxRuntime.jsxs("div", { className: "space-y-6", children: [jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: onFileSelect, className: "hidden" }), jsxRuntime.jsx("div", { onDragEnter: onDragEnter, onDragLeave: onDragLeave, onDragOver: onDragOver, onDrop