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
JavaScript
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