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.

350 lines (349 loc) 11.2 kB
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