'use client'; import React, { useState, useRef, useCallback } from 'react'; import type { UploadSession } from '../types/api'; import { API_PATHS } from '../types/api'; interface UploadFile { id: string; file: File; progress: number; status: 'pending' | 'uploading' | 'paused' | 'completed' | 'error'; uploadSession?: UploadSession; error?: string; } interface UploadManagerProps { onUploadComplete?: (fileId: string, file: File) => void; onUploadError?: (error: string, file: File) => void; maxFileSize?: number; // in bytes allowedTypes?: string[]; maxConcurrentUploads?: number; chunkSize?: number; // in bytes } export default function UploadManager({ onUploadComplete, onUploadError, maxFileSize = 100 * 1024 * 1024, // 100MB default allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/ogg'], maxConcurrentUploads = 3, chunkSize = 1024 * 1024, // 1MB chunks }: UploadManagerProps) { const [files, setFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isPaused, setIsPaused] = useState(false); const fileInputRef = useRef(null); const activeUploads = useRef>(new Map()); // Validate file const validateFile = (file: File): string | null => { if (file.size > maxFileSize) { return `File size exceeds ${Math.round(maxFileSize / (1024 * 1024))}MB limit`; } if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) { return `File type ${file.type} is not supported`; } return null; }; // Handle file selection const handleFileSelect = (selectedFiles: FileList | null) => { if (!selectedFiles) return; const newFiles: UploadFile[] = []; Array.from(selectedFiles).forEach(file => { const error = validateFile(file); const uploadFile: UploadFile = { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, file, progress: 0, status: error ? 'error' : 'pending', error: error || undefined, }; newFiles.push(uploadFile); }); setFiles(prev => [...prev, ...newFiles]); }; // Initialize upload session const initializeUpload = async (uploadFile: UploadFile): Promise => { try { const response = await fetch(API_PATHS.UPLOAD_INIT, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName: uploadFile.file.name, fileSize: uploadFile.file.size, mimeType: uploadFile.file.type, chunkSize, }), }); if (!response.ok) { throw new Error('Failed to initialize upload'); } const session: UploadSession = await response.json(); return session; } catch (error) { console.error('Upload initialization error:', error); return null; } }; // Upload a single chunk const uploadChunk = async ( uploadFile: UploadFile, chunk: Blob, chunkIndex: number, uploadSession: UploadSession ): Promise => { const controller = new AbortController(); activeUploads.current.set(uploadFile.id, controller); try { const formData = new FormData(); formData.append('chunk', chunk); formData.append('chunkIndex', chunkIndex.toString()); formData.append('uploadId', uploadSession.id); const chunkPath = API_PATHS.UPLOAD_CHUNK .replace(':uploadId', uploadSession.id) .replace(':chunkIndex', chunkIndex.toString()); const response = await fetch(chunkPath, { method: 'POST', body: formData, signal: controller.signal, }); if (!response.ok) { throw new Error(`Failed to upload chunk ${chunkIndex}`); } } finally { activeUploads.current.delete(uploadFile.id); } }; // Complete upload const completeUpload = async (uploadSession: UploadSession): Promise => { try { const response = await fetch( API_PATHS.UPLOAD_COMPLETE.replace(':uploadId', uploadSession.id), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ uploadId: uploadSession.id, }), } ); if (!response.ok) { throw new Error('Failed to complete upload'); } const result = await response.json(); return result.fileId; } catch (error) { console.error('Upload completion error:', error); return null; } }; // Upload file with chunks const processFileUpload = async (uploadFile: UploadFile) => { if (!uploadFile.uploadSession) return; const { file, uploadSession } = uploadFile; const totalChunks = Math.ceil(file.size / chunkSize); let uploadedChunks = 0; setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'uploading', progress: 0 } : f ) ); try { // Upload chunks for (let i = 0; i < totalChunks; i++) { // Check if paused if (isPaused) { await new Promise(resolve => { const checkInterval = setInterval(() => { if (!isPaused) { clearInterval(checkInterval); resolve(undefined); } }, 100); }); } const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); await uploadChunk(uploadFile, chunk, i, uploadSession); uploadedChunks++; // Update progress const progress = (uploadedChunks / totalChunks) * 100; setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f ) ); } // Complete upload const fileId = await completeUpload(uploadSession); if (fileId) { setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'completed', progress: 100 } : f ) ); onUploadComplete?.(fileId, file); } else { throw new Error('Failed to complete upload'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Upload failed'; setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'error', error: errorMessage } : f ) ); onUploadError?.(errorMessage, file); } }; // Start upload process const initializeAndStartUpload = async (uploadFile: UploadFile) => { const session = await initializeUpload(uploadFile); if (session) { const updatedFile = { ...uploadFile, uploadSession: session }; setFiles(prev => prev.map(f => f.id === uploadFile.id ? updatedFile : f) ); await processFileUpload(updatedFile); } else { setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'error', error: 'Failed to initialize upload' } : f ) ); } }; // Process upload queue const processQueue = useCallback(() => { const pendingFiles = files.filter(f => f.status === 'pending'); const activeUploadsCount = files.filter(f => f.status === 'uploading').length; if (isPaused || activeUploadsCount >= maxConcurrentUploads) return; const slotsAvailable = maxConcurrentUploads - activeUploadsCount; const filesToUpload = pendingFiles.slice(0, slotsAvailable); filesToUpload.forEach(file => { initializeAndStartUpload(file); }); }, [files, isPaused, maxConcurrentUploads]); // Auto-process queue when files change React.useEffect(() => { processQueue(); }, [files, processQueue]); // Drag and drop handlers const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); handleFileSelect(e.dataTransfer.files); }; // Pause/resume all uploads const togglePause = () => { setIsPaused(!isPaused); if (!isPaused) { // Cancel all active uploads activeUploads.current.forEach(controller => { controller.abort(); }); activeUploads.current.clear(); } }; // Remove file from list const removeFile = (id: string) => { // Cancel if uploading const controller = activeUploads.current.get(id); if (controller) { controller.abort(); activeUploads.current.delete(id); } setFiles(prev => prev.filter(f => f.id !== id)); }; // Retry failed upload const retryUpload = (uploadFile: UploadFile) => { setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'pending', progress: 0, error: undefined } : f ) ); }; // Clear completed uploads const clearCompleted = () => { setFiles(prev => prev.filter(f => f.status !== 'completed')); }; return (

Upload Manager

{/* Upload area */}

Drag and drop audio files here, or{' '}

Supported formats: MP3, WAV, FLAC, OGG (max {Math.round(maxFileSize / (1024 * 1024))}MB)

handleFileSelect(e.target.files)} />
{/* File list */} {files.length > 0 && (

Uploads ({files.length} file{files.length !== 1 ? 's' : ''})

{files.some(f => f.status === 'uploading') && ( )} {files.some(f => f.status === 'completed') && ( )}
{files.map(uploadFile => (

{uploadFile.file.name}

{(uploadFile.file.size / (1024 * 1024)).toFixed(2)} MB

{uploadFile.status === 'error' && ( )} {uploadFile.status !== 'uploading' && ( )}
{/* Status and progress */}
{uploadFile.status === 'pending' && 'Pending'} {uploadFile.status === 'uploading' && `${Math.round(uploadFile.progress)}%`} {uploadFile.status === 'paused' && 'Paused'} {uploadFile.status === 'completed' && 'Completed'} {uploadFile.status === 'error' && 'Error'}
{/* Error message */} {uploadFile.error && (

{uploadFile.error}

)}
))}
)}
); }