zentrixui
Version:
ZentrixUI - A modern, highly customizable and accessible React file upload component library with multiple variants, JSON-based configuration, and excellent developer experience.
350 lines (349 loc) • 11.2 kB
JavaScript
import { jsx } from "react/jsx-runtime";
import { useReducer, useState, useCallback, createContext, useContext } from "react";
import { processError, logError, processErrors } from "../../utils/error-handling.js";
const initialState = {
files: [],
isUploading: false,
progress: 0,
overallProgress: 0,
error: null,
isDragOver: false,
isDropValid: false,
selectedFiles: [],
rejectedFiles: [],
uploadQueue: [],
completedUploads: [],
failedUploads: []
};
function fileUploadReducer(state, action) {
switch (action.type) {
case "SET_FILES":
return { ...state, files: action.payload };
case "ADD_FILES":
return { ...state, files: [...state.files, ...action.payload] };
case "REMOVE_FILE":
return {
...state,
files: state.files.filter((file) => file.id !== action.payload),
completedUploads: state.completedUploads.filter((id) => id !== action.payload),
failedUploads: state.failedUploads.filter((id) => id !== action.payload),
uploadQueue: state.uploadQueue.filter((id) => id !== action.payload)
};
case "UPDATE_FILE":
return {
...state,
files: state.files.map(
(file) => file.id === action.payload.id ? { ...file, ...action.payload.updates } : file
)
};
case "SET_UPLOADING":
return { ...state, isUploading: action.payload };
case "SET_PROGRESS":
return { ...state, progress: action.payload };
case "SET_OVERALL_PROGRESS":
return { ...state, overallProgress: action.payload };
case "SET_ERROR":
return { ...state, error: action.payload };
case "SET_DRAG_OVER":
return { ...state, isDragOver: action.payload };
case "SET_DROP_VALID":
return { ...state, isDropValid: action.payload };
case "SET_SELECTED_FILES":
return { ...state, selectedFiles: action.payload };
case "CLEAR_ALL":
return {
...state,
files: [],
selectedFiles: [],
rejectedFiles: [],
uploadQueue: [],
completedUploads: [],
failedUploads: [],
error: null,
progress: 0,
overallProgress: 0,
isUploading: false
};
case "RESET":
return initialState;
default:
return state;
}
}
const FileUploadContext = createContext(null);
const FileUploadProvider = ({
children,
config,
handlers = {}
}) => {
const [state, dispatch] = useReducer(fileUploadReducer, initialState);
const [processedErrors, setProcessedErrors] = useState([]);
const selectFiles = useCallback((files) => {
dispatch({ type: "SET_SELECTED_FILES", payload: files });
const uploadFiles2 = files.map((file) => ({
id: `file_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
file,
status: "pending",
progress: 0,
size: file.size,
type: file.type,
name: file.name,
lastModified: file.lastModified,
retryCount: 0,
maxRetries: 3,
startedAt: /* @__PURE__ */ new Date()
}));
dispatch({ type: "ADD_FILES", payload: uploadFiles2 });
if (handlers.onFileSelect) {
handlers.onFileSelect({
type: "select",
files: uploadFiles2,
timestamp: /* @__PURE__ */ new Date()
});
}
}, [handlers]);
const removeFile = useCallback((fileId) => {
const fileToRemove = state.files.find((f) => f.id === fileId);
if (fileToRemove) {
dispatch({ type: "REMOVE_FILE", payload: fileId });
if (handlers.onFileRemove) {
handlers.onFileRemove({
type: "remove",
files: [fileToRemove],
timestamp: /* @__PURE__ */ new Date()
});
}
}
}, [state.files, handlers]);
const retryUpload = useCallback((fileId) => {
const file = state.files.find((f) => f.id === fileId);
if (file) {
const updatedFile = {
...file,
status: "pending",
progress: 0,
error: void 0,
retryCount: file.retryCount + 1
};
dispatch({
type: "UPDATE_FILE",
payload: { id: fileId, updates: updatedFile }
});
if (handlers.onUploadRetry) {
handlers.onUploadRetry({
type: "retry",
files: [updatedFile],
timestamp: /* @__PURE__ */ new Date()
});
}
}
}, [state.files, handlers]);
const clearAll = useCallback(() => {
dispatch({ type: "CLEAR_ALL" });
}, []);
const uploadFiles = useCallback(async () => {
const pendingFiles = state.files.filter((f) => f.status === "pending");
if (pendingFiles.length === 0) return;
dispatch({ type: "SET_UPLOADING", payload: true });
dispatch({ type: "SET_ERROR", payload: null });
if (handlers.onUploadStart) {
handlers.onUploadStart({
type: "upload",
files: pendingFiles,
timestamp: /* @__PURE__ */ new Date()
});
}
for (const file of pendingFiles) {
dispatch({
type: "UPDATE_FILE",
payload: {
id: file.id,
updates: { status: "uploading", startedAt: /* @__PURE__ */ new Date() }
}
});
for (let progress = 0; progress <= 100; progress += 10) {
await new Promise((resolve) => setTimeout(resolve, 100));
dispatch({
type: "UPDATE_FILE",
payload: { id: file.id, updates: { progress } }
});
if (handlers.onUploadProgress) {
handlers.onUploadProgress({
type: "progress",
files: [{ ...file, progress }],
timestamp: /* @__PURE__ */ new Date()
});
}
}
dispatch({
type: "UPDATE_FILE",
payload: {
id: file.id,
updates: {
status: "success",
progress: 100,
completedAt: /* @__PURE__ */ new Date()
}
}
});
if (handlers.onUploadSuccess) {
handlers.onUploadSuccess({
type: "success",
files: [{ ...file, status: "success", progress: 100 }],
timestamp: /* @__PURE__ */ new Date()
});
}
}
dispatch({ type: "SET_UPLOADING", payload: false });
}, [state.files, handlers]);
const updateProgress = useCallback((fileId, progress) => {
dispatch({
type: "UPDATE_FILE",
payload: { id: fileId, updates: { progress } }
});
}, []);
const setError = useCallback((fileId, error) => {
dispatch({
type: "UPDATE_FILE",
payload: {
id: fileId,
updates: {
status: "error",
error,
completedAt: /* @__PURE__ */ new Date()
}
}
});
const file = state.files.find((f) => f.id === fileId);
if (file && handlers.onUploadError) {
handlers.onUploadError({
type: "error",
files: [{ ...file, status: "error", error }],
timestamp: /* @__PURE__ */ new Date()
});
}
}, [state.files, handlers]);
const setSuccess = useCallback((fileId) => {
dispatch({
type: "UPDATE_FILE",
payload: {
id: fileId,
updates: {
status: "success",
progress: 100,
completedAt: /* @__PURE__ */ new Date()
}
}
});
}, []);
const handleError = useCallback((error, context = {}) => {
try {
const processedError = processError(error, {
fileName: context.fileName,
operation: context.operation,
timestamp: /* @__PURE__ */ new Date()
}, config);
logError(processedError, { fileId: context.fileId });
setProcessedErrors((prev) => [...prev, processedError]);
if (context.fileId) {
setError(context.fileId, processedError.userMessage);
} else {
dispatch({ type: "SET_ERROR", payload: processedError.userMessage });
}
if (handlers.onUploadError) {
const file = context.fileId ? state.files.find((f) => f.id === context.fileId) : void 0;
handlers.onUploadError({
type: "error",
files: file ? [{ ...file, status: "error", error: processedError.userMessage }] : [],
timestamp: /* @__PURE__ */ new Date()
});
}
return processedError;
} catch (processingError) {
console.error("Error processing error:", processingError);
const fallbackMessage = typeof error === "string" ? error : error.message;
dispatch({ type: "SET_ERROR", payload: fallbackMessage });
return null;
}
}, [config, handlers, state.files, setError]);
const handleValidationErrors = useCallback((errors, context = {}) => {
try {
const { errors: processedErrors2 } = processErrors(errors, {
operation: context.operation,
timestamp: /* @__PURE__ */ new Date()
}, config);
processedErrors2.forEach((error) => logError(error));
setProcessedErrors((prev) => [...prev, ...processedErrors2]);
const errorSummary = processedErrors2.length === 1 ? processedErrors2[0].userMessage : `${processedErrors2.length} validation errors occurred`;
dispatch({ type: "SET_ERROR", payload: errorSummary });
return processedErrors2;
} catch (processingError) {
console.error("Error processing validation errors:", processingError);
dispatch({ type: "SET_ERROR", payload: "Multiple validation errors occurred" });
return [];
}
}, [config]);
const dismissError = useCallback((errorId) => {
setProcessedErrors((prev) => prev.filter((error) => error.id !== errorId));
}, []);
const dismissAllErrors = useCallback(() => {
setProcessedErrors([]);
dispatch({ type: "SET_ERROR", payload: null });
}, []);
const retryFailedUploads = useCallback(() => {
const failedFiles = state.files.filter((f) => f.status === "error");
failedFiles.forEach((file) => {
if (file.retryCount < file.maxRetries) {
retryUpload(file.id);
}
});
}, [state.files, retryUpload]);
const clearFailedUploads = useCallback(() => {
const failedFileIds = state.files.filter((f) => f.status === "error").map((f) => f.id);
failedFileIds.forEach((fileId) => {
dispatch({ type: "REMOVE_FILE", payload: fileId });
});
setProcessedErrors(
(prev) => prev.filter(
(error) => !failedFileIds.some(
(fileId) => error.context.fileName === state.files.find((f) => f.id === fileId)?.name
)
)
);
}, [state.files]);
const contextValue = {
state,
config,
processedErrors,
actions: {
selectFiles,
removeFile,
retryUpload,
clearAll,
uploadFiles,
updateProgress,
setError,
setSuccess,
handleError,
handleValidationErrors,
dismissError,
dismissAllErrors,
retryFailedUploads,
clearFailedUploads
},
handlers
};
return /* @__PURE__ */ jsx(FileUploadContext.Provider, { value: contextValue, children });
};
const useFileUpload = () => {
const context = useContext(FileUploadContext);
if (!context) {
throw new Error("useFileUpload must be used within a FileUploadProvider");
}
return context;
};
export {
FileUploadProvider,
useFileUpload
};
//# sourceMappingURL=file-upload-context.js.map