aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
378 lines (375 loc) • 13 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useRef, useCallback } from 'react';
import { GlassCore } from '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useA11yId } from '../../utils/a11y.js';
const GlassFileUpload = ({
accept,
maxSize = 10 * 1024 * 1024,
// 10MB default
maxFiles = 5,
multiple = true,
onUpload,
onChange,
disabled = false,
uploadText = "Drag and drop files here, or click to browse",
browseText = "Browse Files",
showPreview = true,
validator,
uploadUrl,
className,
...props
}) => {
const [files, setFiles] = useState([]);
const [isDragOver, setIsDragOver] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef(null);
const inputId = useA11yId("file-upload-input");
// File validation
const validateFile = useCallback(file => {
// Custom validator first
if (validator) {
const customError = validator(file);
if (customError) return customError;
}
// Size validation
if (file.size > maxSize) {
return `File size exceeds ${formatFileSize(maxSize)} limit`;
}
// Type validation
if (accept) {
const acceptedTypes = accept.split(",").map(type => type.trim());
const isValidType = acceptedTypes.some(type => {
if (type.startsWith(".")) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
} else if (type.includes("/*")) {
return file.type.startsWith(type.split("/")[0]);
} else {
return file.type === type;
}
});
if (!isValidType) {
return `File type not supported. Accepted types: ${accept}`;
}
}
return null;
}, [accept, maxSize, validator]);
// Handle file selection
const handleFiles = useCallback(newFiles => {
const fileArray = Array.from(newFiles);
// Check max files limit
if (files.length + fileArray.length > maxFiles) {
alert(`Maximum ${maxFiles} files allowed`);
return;
}
const validFiles = [];
const invalidFiles = [];
fileArray.forEach(file => {
const error = validateFile(file);
if (error) {
invalidFiles.push(`${file.name}: ${error}`);
} else {
validFiles.push({
file,
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
progress: 0,
status: "uploading"
});
}
});
if (invalidFiles.length > 0) {
alert(`Invalid files:\n${invalidFiles.join("\n")}`);
}
if (validFiles.length > 0) {
const updatedFiles = [...files, ...validFiles];
setFiles(updatedFiles);
onChange?.(updatedFiles);
// Start upload if handler provided
if (onUpload || uploadUrl) {
uploadFiles(validFiles);
} else {
// Mark as completed if no upload handler
const completedFiles = validFiles.map(f => ({
...f,
status: "completed",
progress: 100
}));
setFiles(prev => prev.map(f => completedFiles.find(cf => cf.id === f.id) || f));
}
}
}, [files, maxFiles, validateFile, onChange, onUpload, uploadUrl]);
// Upload files
const uploadFiles = async filesToUpload => {
setIsUploading(true);
for (const uploadedFile of filesToUpload) {
try {
if (onUpload) {
// Custom upload handler
await onUpload([uploadedFile.file]);
updateFileStatus(uploadedFile.id, "completed", 100);
} else if (uploadUrl) {
// Upload to URL endpoint
await uploadFileToUrl(uploadedFile);
}
} catch (error) {
updateFileStatus(uploadedFile.id, "error", 0, error instanceof Error ? error.message : "Upload failed");
}
}
setIsUploading(false);
};
// Upload file to URL
const uploadFileToUrl = async uploadedFile => {
const formData = new FormData();
formData.append("file", uploadedFile.file);
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const progress = Math.round(e.loaded / e.total * 100);
updateFileProgress(uploadedFile.id, progress);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
updateFileStatus(uploadedFile.id, "completed", 100);
resolve();
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.open("POST", uploadUrl);
xhr.send(formData);
});
};
// Update file progress
const updateFileProgress = (id, progress) => {
setFiles(prev => prev.map(f => f.id === id ? {
...f,
progress
} : f));
};
// Update file status
const updateFileStatus = (id, status, progress, error) => {
setFiles(prev => prev.map(f => f.id === id ? {
...f,
status,
progress: progress ?? f.progress,
error
} : f));
};
// Remove file
const removeFile = id => {
const updatedFiles = files.filter(f => f.id !== id);
setFiles(updatedFiles);
onChange?.(updatedFiles);
};
// Drag and drop handlers
const handleDragOver = useCallback(e => {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
setIsDragOver(true);
}
}, [disabled]);
const handleDragLeave = useCallback(e => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDrop = useCallback(e => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (disabled) return;
const droppedFiles = e.dataTransfer.files;
if (droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
}, [disabled, handleFiles]);
// File input click handler
const handleClick = () => {
if (!disabled) {
fileInputRef.current?.click();
}
};
// File input change handler
const handleFileInputChange = e => {
const selectedFiles = e.target.files;
if (selectedFiles) {
handleFiles(selectedFiles);
}
// Reset input value
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
// Format file size
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];
};
// Get file icon
const getFileIcon = file => {
const type = file.type.toLowerCase();
if (type.startsWith("image/")) return "🖼️";
if (type.startsWith("video/")) return "🎥";
if (type.startsWith("audio/")) return "🎵";
if (type.includes("pdf")) return "📄";
if (type.includes("word") || type.includes("document")) return "📝";
if (type.includes("excel") || type.includes("spreadsheet")) return "📊";
if (type.includes("powerpoint") || type.includes("presentation")) return "📽️";
if (type.includes("zip") || type.includes("archive")) return "📦";
return "📄";
};
// Render file preview
const renderFilePreview = uploadedFile => {
const {
file,
progress,
status,
error
} = uploadedFile;
return jsxs("div", {
"data-glass-component": true,
role: "listitem",
className: "glass-flex glass-items-center glass-p-3 glass-surface-subtle glass-radius-lg glass-border",
children: [jsx("div", {
className: 'glass-text-2xl mr-3',
"aria-hidden": "true",
children: getFileIcon(file)
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsx("p", {
className: 'glass-text-sm font-medium text-primary truncate',
children: file.name
}), jsx("button", {
onClick: () => removeFile(uploadedFile.id),
className: 'ml-2 text-primary hover:text-primary glass-text-sm',
disabled: status === "uploading",
"aria-label": `Remove ${file.name}`,
children: "\u2715"
})]
}), jsxs("p", {
className: "glass-text-xs glass-text-secondary",
children: [formatFileSize(file.size), status === "completed" && jsx("span", {
className: 'text-primary ml-2',
role: "status",
children: "\u2713 Completed"
}), status === "error" && jsxs("span", {
className: 'text-primary ml-2',
role: "alert",
children: ["\u2717 ", error]
})]
}), status === "uploading" && jsxs("div", {
className: 'mt-2',
role: "progressbar",
"aria-valuenow": progress,
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-label": `Uploading ${file.name}`,
children: [jsx("div", {
className: 'glass-w-full glass-surface-subtle glass-radius-full h-1',
children: jsx("div", {
className: 'glass-surface-blue h-1 glass-radius-full transition-all duration-300',
style: {
width: `${progress}%`
}
})
}), jsxs("p", {
className: 'glass-text-xs glass-text-secondary mt-1',
children: [progress, "%"]
})]
})]
})]
}, uploadedFile.id);
};
return jsxs("div", {
className: cn("w-full", className),
...props,
children: [jsx("input", {
ref: fileInputRef,
type: "file",
accept: accept,
multiple: multiple,
onChange: handleFileInputChange,
className: 'hidden glass-touch-target glass-contrast-guard',
disabled: disabled,
"aria-label": "File upload input",
id: inputId
}), jsx("label", {
htmlFor: inputId,
role: "button",
"aria-label": uploadText,
"aria-describedby": "file-upload-instructions",
"aria-disabled": disabled,
tabIndex: disabled ? -1 : 0,
className: "block cursor-pointer",
onDragOver: handleDragOver,
onDragLeave: handleDragLeave,
onDrop: handleDrop,
onClick: e => {
if (disabled) {
e.preventDefault();
}
},
onKeyDown: e => {
if ((e.key === "Enter" || e.key === " ") && !disabled) {
e.preventDefault();
handleClick();
}
},
children: jsx(GlassCore, {
role: "presentation",
className: cn("border-2 border-dashed glass-p-8 text-center transition-all duration-200", isDragOver && !disabled && "border-primary bg-primary/5", disabled ? "opacity-50 cursor-not-allowed" : "hover:border-primary hover:bg-primary/5", files.length > 0 ? "glass-radius-t-xl" : "glass-radius-xl"),
children: jsxs("div", {
className: "glass-flex glass-flex-col glass-items-center glass-gap-4 pointer-events-none",
children: [jsx("div", {
className: 'glass-text-4xl opacity-60',
children: isUploading ? "⏳" : "📁"
}), jsxs("div", {
children: [jsx("p", {
className: 'glass-text-lg font-medium text-primary mb-2',
children: isUploading ? "Uploading..." : uploadText
}), jsxs("p", {
id: "file-upload-instructions",
className: "glass-text-secondary",
children: [accept && `Accepted formats: ${accept}`, maxSize && ` • Max size: ${formatFileSize(maxSize)}`, multiple && ` • Max files: ${maxFiles}`]
})]
}), !isUploading && jsx("span", {
className: 'glass-px-4 glass-py-2 glass-surface-primary text-primary-foreground glass-radius-lg transition-colors font-medium inline-flex',
"aria-hidden": "true",
children: browseText
})]
})
})
}), showPreview && files.length > 0 && jsxs(GlassCore, {
className: 'glass-border-t-0 glass-radius-b-xl glass-p-4 space-y-3',
role: "region",
"aria-label": "Uploaded files list",
children: [jsxs("h4", {
id: "uploaded-files-heading",
className: 'font-medium text-primary mb-3',
children: ["Uploaded Files (", files.length, "/", maxFiles, ")"]
}), jsx("div", {
role: "list",
"aria-labelledby": "uploaded-files-heading",
children: files.map(renderFilePreview)
})]
})]
});
};
GlassFileUpload.displayName = "GlassFileUpload";
export { GlassFileUpload };
//# sourceMappingURL=GlassFileUpload.js.map