React LogoFile Upload Manager

A File Upload Manager is a crucial component in web applications that allows users to select one or more files from their local system and upload them to a server. It goes beyond a simple HTML <input type="file"> by providing an enhanced user experience and robust handling of the upload process.

Key functionalities of a comprehensive File Upload Manager include:

1. File Selection: Users can select files either by clicking a button that opens a file browser dialog or by dragging and dropping files directly onto a designated area (drop zone).
2. File Previews: For certain file types (like images, videos, or PDFs), the manager can display a thumbnail or a basic preview of the file before it's uploaded. This helps users confirm they've selected the correct files.
3. Validation: Before upload, files can be validated against various criteria such as file type (MIME type), size limits, number of files, and dimensions (for images).
4. Upload Progress Tracking: Displays real-time progress for each file being uploaded, often with a progress bar and percentage complete. This provides feedback to the user and indicates that the application is active.
5. Cancellation/Removal: Users should be able to cancel an ongoing upload or remove a file from the selection queue before or during the upload process.
6. Error Handling: Gracefully handles various upload errors, such as network issues, server-side validation failures (e.g., file too large, incorrect format), or authentication problems. It should inform the user about the error.
7. Batch Uploads: Supports uploading multiple files simultaneously or in a queued manner.
8. Server Interaction: Communicates with a backend API (e.g., using AJAX, Fetch API) to send the file data and receive responses regarding the upload status. This often involves sending `FormData` objects.
9. User Interface (UI): Provides a clear and intuitive UI to display selected files, their status, progress, and actions (upload, remove, retry).

Why use a File Upload Manager?

* Improved User Experience: Provides visual feedback, allows previews, and makes the process feel more interactive and less like a black box.
* Robustness: Handles edge cases, errors, and provides retry mechanisms.
* Flexibility: Easily extendable to add more features like image manipulation, tagging, or advanced validation.
* Modern Web Standards: Leverages browser APIs like `FileReader`, `DragEvent`, and `XMLHttpRequest.upload.onprogress` to deliver a rich experience.

Building a File Upload Manager from scratch in React typically involves managing file state using `useState`, handling file input and drag-and-drop events, making asynchronous API calls for uploads, and updating the UI based on upload progress and status.

Example Code

```jsx
import React, { useState, useRef, useEffect } from 'react';
import './FileUploadManager.css'; // Assume you have some basic CSS for styling

const FileUploadManager = () => {
  const [selectedFiles, setSelectedFiles] = useState([]);
  const [uploadProgress, setUploadProgress] = useState({}); // { fileName: percentage }
  const [uploadStatus, setUploadStatus] = useState({}); // { fileName: 'pending' | 'uploading' | 'completed' | 'failed' }
  const fileInputRef = useRef(null);

  const handleFileChange = (event) => {
    const files = Array.from(event.target.files);
    addFiles(files);
  };

  const handleDrop = (event) => {
    event.preventDefault();
    const files = Array.from(event.dataTransfer.files);
    addFiles(files);
  };

  const addFiles = (files) => {
    const newFiles = files.filter(file => 
      !selectedFiles.some(existingFile => existingFile.name === file.name && existingFile.size === file.size)
    );
    setSelectedFiles(prevFiles => [...prevFiles, ...newFiles]);
    newFiles.forEach(file => {
      setUploadProgress(prev => ({ ...prev, [file.name]: 0 }));
      setUploadStatus(prev => ({ ...prev, [file.name]: 'pending' }));
    });
  };

  const handleRemoveFile = (fileName) => {
    setSelectedFiles(prevFiles => prevFiles.filter(file => file.name !== fileName));
    setUploadProgress(prev => {
      const newState = { ...prev };
      delete newState[fileName];
      return newState;
    });
    setUploadStatus(prev => {
      const newState = { ...prev };
      delete newState[fileName];
      return newState;
    });
  };

  const simulateUpload = async (file) => {
    setUploadStatus(prev => ({ ...prev, [file.name]: 'uploading' }));
    console.log(`Starting upload for ${file.name}`);

    const totalSize = file.size; // Or just simulate percentage out of 100
    let uploaded = 0;
    const chunkSize = Math.max(1, totalSize / 10); // Simulate 10 chunks

    try {
      while (uploaded < totalSize) {
        await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network latency
        uploaded += chunkSize;
        const currentProgress = Math.min(100, Math.round((uploaded / totalSize) * 100));
        setUploadProgress(prev => ({ ...prev, [file.name]: currentProgress }));

        // Simulate a failure for specific files or randomly
        if (file.name.includes('fail') && currentProgress > 50) {
          throw new Error('Simulated network error or server rejection');
        }

        if (currentProgress === 100) {
          uploaded = totalSize; // Ensure it reaches 100%
        }
      }
      setUploadStatus(prev => ({ ...prev, [file.name]: 'completed' }));
      console.log(`Upload completed for ${file.name}`);
    } catch (error) {
      setUploadStatus(prev => ({ ...prev, [file.name]: 'failed' }));
      console.error(`Upload failed for ${file.name}:`, error.message);
    }
  };

  const handleUploadAll = async () => {
    const filesToUpload = selectedFiles.filter(file => uploadStatus[file.name] === 'pending' || uploadStatus[file.name] === 'failed');
    if (filesToUpload.length === 0) return;

    // In a real app, you'd send each file to your backend API
    // Example: Using FormData and Fetch API
    // const formData = new FormData();
    // formData.append('file', file);
    // await fetch('/api/upload', { method: 'POST', body: formData });

    // Simulate sequential uploads (can be parallelized too)
    for (const file of filesToUpload) {
      await simulateUpload(file);
    }
  };

  const getFilePreview = (file) => {
    if (file.type.startsWith('image/')) {
      return URL.createObjectURL(file);
    } else if (file.type.startsWith('video/')) {
      return '<video width="100" controls><source src="' + URL.createObjectURL(file) + '" type="' + file.type + '"></video>';
    }
    return null;
  };

  useEffect(() => {
    // Clean up created object URLs when files are removed or component unmounts
    return () => {
      selectedFiles.forEach(file => {
        if (file.type.startsWith('image/') || file.type.startsWith('video/')) {
          URL.revokeObjectURL(file.preview);
        }
      });
    };
  }, [selectedFiles]);

  const isUploading = Object.values(uploadStatus).some(status => status === 'uploading');
  const hasPendingFiles = selectedFiles.some(file => uploadStatus[file.name] === 'pending' || uploadStatus[file.name] === 'failed');

  return (
    <div className="file-upload-manager-container">
      <h2>File Upload Manager</h2>

      <div
        className="drop-zone"
        onDragOver={(event) => event.preventDefault()}
        onDrop={handleDrop}
        onClick={() => fileInputRef.current.click()}
      >
        <p>Drag & Drop files here or click to browse</p>
        <input
          type="file"
          ref={fileInputRef}
          multiple
          hidden
          onChange={handleFileChange}
          accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.mp4,.mov"
        />
      </div>

      {selectedFiles.length > 0 && (
        <div className="file-list-section">
          <h3>Selected Files ({selectedFiles.length})</h3>
          <ul className="file-list">
            {selectedFiles.map((file) => (
              <li key={file.name} className="file-item">
                <div className="file-info">
                  {getFilePreview(file) && file.type.startsWith('image/') && (
                    <img src={getFilePreview(file)} alt="preview" className="file-preview-thumbnail" />
                  )}
                  {getFilePreview(file) && file.type.startsWith('video/') && (
                    <div className="file-preview-thumbnail" dangerouslySetInnerHTML={{ __html: getFilePreview(file) }} />
                  )}
                  <div className="file-details">
                    <span className="file-name">{file.name}</span>
                    <span className="file-size">({(file.size / 1024 / 1024).toFixed(2)} MB)</span>
                  </div>
                </div>
                <div className="file-status-section">
                  {uploadStatus[file.name] === 'uploading' && (
                    <div className="progress-bar-container">
                      <div
                        className="progress-bar"
                        style={{ width: `${uploadProgress[file.name] || 0}%` }}
                      ></div>
                      <span className="progress-text">{uploadProgress[file.name] || 0}%</span>
                    </div>
                  )}
                  <span className={`file-status status-${uploadStatus[file.name]}`}>
                    {uploadStatus[file.name] === 'pending' && 'Ready to upload'}
                    {uploadStatus[file.name] === 'uploading' && 'Uploading...'}
                    {uploadStatus[file.name] === 'completed' && 'Uploaded'}
                    {uploadStatus[file.name] === 'failed' && 'Failed'}
                  </span>
                  <button 
                    className="remove-button"
                    onClick={() => handleRemoveFile(file.name)}
                    disabled={uploadStatus[file.name] === 'uploading'}
                  >
                    ×
                  </button>
                </div>
              </li>
            ))}
          </ul>
          <button 
            className="upload-all-button"
            onClick={handleUploadAll}
            disabled={isUploading || !hasPendingFiles}
          >
            {isUploading ? 'Uploading...' : 'Upload All Files'}
          </button>
        </div>
      )}
    </div>
  );
};

export default FileUploadManager;
```

```css
/* FileUploadManager.css */

.file-upload-manager-container {
  font-family: Arial, sans-serif;
  max-width: 800px;
  margin: 40px auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  background-color: #fff;
}

h2 {
  text-align: center;
  color: #333;
  margin-bottom: 25px;
}

.drop-zone {
  border: 2px dashed #007bff;
  border-radius: 5px;
  padding: 40px 20px;
  text-align: center;
  cursor: pointer;
  color: #007bff;
  background-color: #eaf5ff;
  margin-bottom: 25px;
  transition: background-color 0.3s ease;
}

.drop-zone:hover {
  background-color: #d9ecff;
}

.drop-zone p {
  margin: 0;
  font-size: 1.1em;
  font-weight: 500;
}

.file-list-section h3 {
  color: #333;
  margin-top: 20px;
  margin-bottom: 15px;
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

.file-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.file-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid #eee;
}

.file-item:last-child {
  border-bottom: none;
}

.file-info {
  display: flex;
  align-items: center;
  flex-grow: 1;
}

.file-preview-thumbnail {
  width: 50px;
  height: 50px;
  object-fit: cover;
  border-radius: 4px;
  margin-right: 15px;
  border: 1px solid #eee;
  flex-shrink: 0;
}

.file-preview-thumbnail video {
  width: 100%;
  height: 100%;
  border-radius: 4px;
}

.file-details {
  display: flex;
  flex-direction: column;
}

.file-name {
  font-weight: 600;
  color: #333;
  font-size: 0.95em;
  word-break: break-all;
}

.file-size {
  font-size: 0.85em;
  color: #666;
}

.file-status-section {
  display: flex;
  align-items: center;
  margin-left: 20px;
  flex-shrink: 0;
}

.progress-bar-container {
  width: 100px;
  height: 8px;
  background-color: #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
  position: relative;
  margin-right: 10px;
}

.progress-bar {
  height: 100%;
  background-color: #28a745;
  border-radius: 4px;
  transition: width 0.3s ease-in-out;
}

.progress-text {
  position: absolute;
  top: -15px;
  right: 0;
  font-size: 0.7em;
  color: #555;
}

.file-status {
  font-size: 0.8em;
  padding: 3px 8px;
  border-radius: 4px;
  margin-right: 10px;
  white-space: nowrap;
}

.status-pending {
  background-color: #f0f0f0;
  color: #555;
}

.status-uploading {
  background-color: #e0f7fa;
  color: #007bff;
}

.status-completed {
  background-color: #e6ffe6;
  color: #28a745;
}

.status-failed {
  background-color: #ffe6e6;
  color: #dc3545;
}

.remove-button {
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 50%;
  width: 24px;
  height: 24px;
  font-size: 1em;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: background-color 0.2s ease;
  flex-shrink: 0;
}

.remove-button:hover {
  background-color: #c82333;
}

.remove-button:disabled {
  background-color: #e0e0e0;
  cursor: not-allowed;
}

.upload-all-button {
  display: block;
  width: 100%;
  padding: 12px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 1.1em;
  cursor: pointer;
  margin-top: 25px;
  transition: background-color 0.2s ease;
}

.upload-all-button:hover {
  background-color: #0056b3;
}

.upload-all-button:disabled {
  background-color: #a0c3e6;
  cursor: not-allowed;
}
```