UNPKG

zentrixui

Version:

ZentrixUI - A modern, highly customizable and accessible React file upload component library with multiple variants, JSON-based configuration, and excellent developer experience.

466 lines (465 loc) 17.1 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import { useRef, useState, useCallback } from "react"; import { useFileUpload } from "../file-upload-context.js"; import { validateImageDimensions } from "../../../utils/file-validation.js"; const ImageUpload = ({ className, style, ariaLabel, ariaDescribedBy, children, aspectRatio, cropEnabled = false, resizeEnabled = false, quality = 0.8 }) => { const { config, actions, state } = useFileUpload(); const inputRef = useRef(null); const [imageValidationErrors, setImageValidationErrors] = useState({}); const validateImageFile = useCallback(async (file) => { if (!file.type.startsWith("image/")) return []; const errors = []; try { const validation = await validateImageDimensions( file, config.validation.maxWidth, config.validation.maxHeight, config.validation.minWidth, config.validation.minHeight ); if (!validation.isValid) { errors.push(...validation.errors.map((e) => e.message)); } } catch (error) { errors.push("Failed to validate image dimensions"); } return errors; }, [config.validation]); const handleFileSelect = async (event) => { const files = Array.from(event.target.files || []); const imageFiles = files.filter((file) => file.type.startsWith("image/")); if (imageFiles.length > 0) { const validationResults = {}; for (const file of imageFiles) { const errors = await validateImageFile(file); if (errors.length > 0) { validationResults[file.name] = errors; } } setImageValidationErrors(validationResults); const validFiles = imageFiles.filter((file) => !validationResults[file.name]); if (validFiles.length > 0) { const processedFiles = await Promise.all( validFiles.map((file) => processImage(file)) ); actions.selectFiles(processedFiles); } } }; const handleButtonClick = () => { inputRef.current?.click(); }; const handleKeyDown = (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); handleButtonClick(); } }; const getImagePreview = (file) => { return URL.createObjectURL(file); }; const resizeImage = useCallback((file, maxWidth, maxHeight) => { return new Promise((resolve) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { let { width, height } = img; if (width > height) { if (width > maxWidth) { height = height * maxWidth / width; width = maxWidth; } } else { if (height > maxHeight) { width = width * maxHeight / height; height = maxHeight; } } canvas.width = width; canvas.height = height; ctx?.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (blob) { const resizedFile = new File([blob], file.name, { type: file.type, lastModified: Date.now() }); resolve(resizedFile); } else { resolve(file); } }, file.type, quality); }; img.src = URL.createObjectURL(file); }); }, [quality]); const cropImage = useCallback((file, aspectRatio2) => { return new Promise((resolve) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { const { width, height } = img; let cropWidth = width; let cropHeight = height; let offsetX = 0; let offsetY = 0; const imageAspectRatio = width / height; if (imageAspectRatio > aspectRatio2) { cropWidth = height * aspectRatio2; offsetX = (width - cropWidth) / 2; } else { cropHeight = width / aspectRatio2; offsetY = (height - cropHeight) / 2; } canvas.width = cropWidth; canvas.height = cropHeight; ctx?.drawImage( img, offsetX, offsetY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight ); canvas.toBlob((blob) => { if (blob) { const croppedFile = new File([blob], file.name, { type: file.type, lastModified: Date.now() }); resolve(croppedFile); } else { resolve(file); } }, file.type, quality); }; img.src = URL.createObjectURL(file); }); }, [quality]); const processImage = useCallback(async (file) => { let processedFile = file; if (cropEnabled && aspectRatio) { processedFile = await cropImage(processedFile, aspectRatio); } if (resizeEnabled && (config.validation.maxWidth || config.validation.maxHeight)) { const maxWidth = config.validation.maxWidth || 1920; const maxHeight = config.validation.maxHeight || 1080; processedFile = await resizeImage(processedFile, maxWidth, maxHeight); } return processedFile; }, [cropEnabled, resizeEnabled, aspectRatio, config.validation, cropImage, resizeImage]); return /* @__PURE__ */ jsxs("div", { className: `file-upload file-upload--image ${className || ""}`, style, children: [ /* @__PURE__ */ jsx( "input", { ref: inputRef, type: "file", multiple: config.defaults.multiple, accept: "image/*", disabled: config.defaults.disabled || state.isUploading, onChange: handleFileSelect, className: "file-upload-input", style: { display: "none" }, "aria-hidden": "true" } ), /* @__PURE__ */ jsx( "div", { onClick: handleButtonClick, onKeyDown: handleKeyDown, role: "button", tabIndex: config.defaults.disabled || state.isUploading ? -1 : 0, "aria-label": ariaLabel || "Upload images", "aria-describedby": ariaDescribedBy, style: { border: `2px dashed ${config.styling.colors.border}`, borderRadius: config.styling.spacing.borderRadius, padding: "2rem", textAlign: "center", cursor: config.defaults.disabled || state.isUploading ? "not-allowed" : "pointer", backgroundColor: config.styling.colors.background, color: config.styling.colors.foreground, fontSize: config.styling.typography.fontSize, opacity: config.defaults.disabled || state.isUploading ? 0.6 : 1, minHeight: "200px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: config.styling.spacing.gap }, children: children || /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { className: "file-upload-image-icon", style: { fontSize: "3rem", color: config.styling.colors.muted }, children: "🖼️" }), /* @__PURE__ */ jsxs("div", { className: "file-upload-image-text", children: [ /* @__PURE__ */ jsx("div", { className: "file-upload-image-primary-text", style: { fontWeight: "600", marginBottom: "0.5rem", color: config.styling.colors.foreground }, children: state.isUploading ? config.labels.progressText : "Upload Images" }), /* @__PURE__ */ jsx("div", { className: "file-upload-image-secondary-text", style: { fontSize: "0.875rem", color: config.styling.colors.muted }, children: "Click to select image files" }) ] }), state.files.length > 0 && /* @__PURE__ */ jsxs("div", { className: "file-upload-image-count", style: { fontSize: "0.875rem", color: config.styling.colors.primary, fontWeight: "500" }, children: [ state.files.length, " ", state.files.length === 1 ? "image" : "images", " selected" ] }) ] }) } ), Object.keys(imageValidationErrors).length > 0 && /* @__PURE__ */ jsxs("div", { className: "file-upload-validation-errors", style: { marginTop: "1rem", padding: "1rem", backgroundColor: config.styling.colors.error + "10", border: `1px solid ${config.styling.colors.error}`, borderRadius: config.styling.spacing.borderRadius }, children: [ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.875rem", fontWeight: "600", color: config.styling.colors.error, marginBottom: "0.5rem" }, children: "Image Validation Errors:" }), Object.entries(imageValidationErrors).map(([fileName, errors]) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: "0.5rem" }, children: [ /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.8rem", fontWeight: "500", color: config.styling.colors.foreground, marginBottom: "0.25rem" }, children: [ fileName, ":" ] }), /* @__PURE__ */ jsx("ul", { style: { margin: 0, paddingLeft: "1rem", fontSize: "0.75rem", color: config.styling.colors.error }, children: errors.map((error, index) => /* @__PURE__ */ jsx("li", { children: error }, index)) }) ] }, fileName)) ] }), state.files.length > 0 && /* @__PURE__ */ jsx("div", { className: "file-upload-image-previews", style: { display: "grid", gridTemplateColumns: config.defaults.multiple ? "repeat(auto-fill, minmax(150px, 1fr))" : "1fr", gap: config.styling.spacing.gap, marginTop: config.styling.spacing.margin }, children: state.files.map((file) => { const preview = getImagePreview(file.file); return /* @__PURE__ */ jsxs("div", { className: "file-upload-image-preview", style: { position: "relative", border: `1px solid ${config.styling.colors.border}`, borderRadius: config.styling.spacing.borderRadius, overflow: "hidden", backgroundColor: config.styling.colors.background }, children: [ /* @__PURE__ */ jsxs("div", { style: { width: "100%", height: aspectRatio ? `${(config.defaults.multiple ? 150 : 200) / aspectRatio}px` : config.defaults.multiple ? "150px" : "200px", position: "relative", overflow: "hidden", aspectRatio: aspectRatio ? aspectRatio.toString() : void 0 }, children: [ /* @__PURE__ */ jsx( "img", { src: preview, alt: file.name, style: { width: "100%", height: "100%", objectFit: cropEnabled ? "cover" : "contain", backgroundColor: config.styling.colors.muted + "20" }, onLoad: () => { setTimeout(() => URL.revokeObjectURL(preview), 1e3); } } ), file.status === "uploading" && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.7)", display: "flex", alignItems: "center", justifyContent: "center", color: "white" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [ /* @__PURE__ */ jsx("div", { style: { width: "60px", height: "4px", backgroundColor: "rgba(255, 255, 255, 0.3)", borderRadius: "2px", overflow: "hidden", marginBottom: "0.5rem" }, children: /* @__PURE__ */ jsx("div", { style: { width: `${file.progress}%`, height: "100%", backgroundColor: config.styling.colors.primary, transition: "width 0.3s ease" } }) }), /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.875rem" }, children: [ file.progress, "%" ] }) ] }) }), file.status === "success" && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: "0.5rem", left: "0.5rem", backgroundColor: config.styling.colors.success, color: config.styling.colors.background, borderRadius: "50%", width: "24px", height: "24px", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.875rem" }, children: "✓" }), file.status === "error" && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: "0.5rem", left: "0.5rem", backgroundColor: config.styling.colors.error, color: config.styling.colors.background, borderRadius: "50%", width: "24px", height: "24px", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.875rem" }, children: "!" }), (file.status === "pending" || file.status === "error") && /* @__PURE__ */ jsx( "button", { type: "button", onClick: (e) => { e.stopPropagation(); actions.removeFile(file.id); }, style: { position: "absolute", top: "0.5rem", right: "0.5rem", background: config.styling.colors.error, color: config.styling.colors.background, border: "none", borderRadius: "50%", width: "24px", height: "24px", cursor: "pointer", fontSize: "0.875rem", display: "flex", alignItems: "center", justifyContent: "center" }, "aria-label": `${config.labels.removeText} ${file.name}`, children: "×" } ) ] }), /* @__PURE__ */ jsxs("div", { style: { padding: "0.75rem", borderTop: `1px solid ${config.styling.colors.border}` }, children: [ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.875rem", fontWeight: "500", color: config.styling.colors.foreground, marginBottom: "0.25rem", wordBreak: "break-word" }, children: file.name }), /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.75rem", color: config.styling.colors.muted, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [ /* @__PURE__ */ jsxs("span", { children: [ (file.size / 1024).toFixed(1), " KB" ] }), file.status === "error" && /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => actions.retryUpload(file.id), style: { background: "none", border: `1px solid ${config.styling.colors.error}`, color: config.styling.colors.error, cursor: "pointer", fontSize: "0.75rem", padding: "0.25rem 0.5rem", borderRadius: "0.25rem" }, "aria-label": `${config.labels.retryText} ${file.name}`, children: config.labels.retryText } ) ] }) ] }) ] }, file.id); }) }), state.files.some((f) => f.status === "pending") && /* @__PURE__ */ jsx( "button", { type: "button", onClick: actions.uploadFiles, disabled: state.isUploading, style: { backgroundColor: config.styling.colors.primary, color: config.styling.colors.background, padding: "0.75rem 1.5rem", border: "none", borderRadius: config.styling.spacing.borderRadius, cursor: state.isUploading ? "not-allowed" : "pointer", opacity: state.isUploading ? 0.6 : 1, fontSize: config.styling.typography.fontSize, fontWeight: "500", width: "100%", marginTop: "1rem" }, children: state.isUploading ? config.labels.progressText : "Upload Images" } ) ] }); }; ImageUpload.displayName = "ImageUpload"; export { ImageUpload }; //# sourceMappingURL=image-upload.js.map