'use client'; // Drag & Drop File Upload Component for Vercel Blob // Complete component with progress tracking, file preview, and error handling import { useState, useCallback } from 'react'; import { upload } from '@vercel/blob/client'; interface UploadedFile { url: string; pathname: string; name: string; size: number; } export function DragDropUpload() { const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState>({}); const [uploadedFiles, setUploadedFiles] = useState([]); const [errors, setErrors] = useState>({}); const [isUploading, setIsUploading] = useState(false); // File validation const validateFile = (file: File): string | null => { const maxSize = 10 * 1024 * 1024; // 10MB const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'application/pdf']; if (file.size > maxSize) { return `File too large. Max size: 10MB`; } if (!allowedTypes.includes(file.type)) { return `Invalid file type. Allowed: JPEG, PNG, WebP, GIF, PDF`; } return null; }; // Handle drag events const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); handleFiles(droppedFiles); }, []); // Handle file selection const handleFileInput = useCallback((e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files); handleFiles(selectedFiles); } }, []); const handleFiles = (newFiles: File[]) => { const validFiles: File[] = []; const newErrors: Record = {}; newFiles.forEach((file) => { const error = validateFile(file); if (error) { newErrors[file.name] = error; } else { validFiles.push(file); } }); setFiles((prev) => [...prev, ...validFiles]); setErrors((prev) => ({ ...prev, ...newErrors })); }; // Remove file from list const removeFile = (fileName: string) => { setFiles((prev) => prev.filter((f) => f.name !== fileName)); setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[fileName]; return newErrors; }); }; // Upload files const handleUpload = async () => { if (files.length === 0) return; setIsUploading(true); const newUploadedFiles: UploadedFile[] = []; for (const file of files) { try { // Update progress setUploadProgress((prev) => ({ ...prev, [file.name]: 0 })); // Get upload token from server const tokenResponse = await fetch('/api/upload-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: file.name }) }); if (!tokenResponse.ok) { throw new Error('Failed to get upload token'); } const { url: handleUploadUrl } = await tokenResponse.json(); // Upload file const blob = await upload(file.name, file, { access: 'public', handleUploadUrl, onUploadProgress: (progressEvent) => { const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100); setUploadProgress((prev) => ({ ...prev, [file.name]: progress })); } }); newUploadedFiles.push({ url: blob.url, pathname: blob.pathname, name: file.name, size: file.size }); // Mark complete setUploadProgress((prev) => ({ ...prev, [file.name]: 100 })); } catch (error) { console.error(`Failed to upload ${file.name}:`, error); setErrors((prev) => ({ ...prev, [file.name]: error instanceof Error ? error.message : 'Upload failed' })); } } setUploadedFiles((prev) => [...prev, ...newUploadedFiles]); setFiles([]); setIsUploading(false); setUploadProgress({}); }; // Format file size const formatSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return (

Upload Files

{/* Drag & Drop Zone */}
{/* Selected Files List */} {files.length > 0 && (

Selected Files ({files.length})

{files.map((file) => (

{file.name}

{formatSize(file.size)}

{/* Progress bar */} {uploadProgress[file.name] !== undefined && (

{uploadProgress[file.name]}%

)} {/* Error message */} {errors[file.name] && (

{errors[file.name]}

)}
))}
)} {/* Uploaded Files */} {uploadedFiles.length > 0 && (

Uploaded Files ({uploadedFiles.length})

{uploadedFiles.map((file) => (

{file.name}

{formatSize(file.size)}

View file
))}
)}
); } // ============================================================================ // SERVER: API Route for Upload Token // ============================================================================ // app/api/upload-token/route.ts /* import { handleUpload, type HandleUploadBody } from '@vercel/blob/client'; import { NextResponse } from 'next/server'; export async function POST(request: Request): Promise { const body = (await request.json()) as HandleUploadBody; try { const jsonResponse = await handleUpload({ body, request, onBeforeGenerateToken: async (pathname) => { // Validate user is authenticated // const session = await getSession(); // if (!session) throw new Error('Unauthorized'); return { allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'application/pdf'], maximumSizeInBytes: 10 * 1024 * 1024, // 10MB }; }, onUploadCompleted: async ({ blob, tokenPayload }) => { // Optional: Save to database console.log('Upload completed:', blob.url); }, }); return NextResponse.json(jsonResponse); } catch (error) { return NextResponse.json( { error: (error as Error).message }, { status: 400 } ); } } */