# File Upload Integration Examples Real-world examples of file upload integration patterns for nself applications. ## Table of Contents 1. [Avatar Upload](#avatar-upload) 2. [Multi-File Upload](#multi-file-upload) 3. [Drag & Drop Interface](#drag--drop-interface) 4. [Progress Tracking](#progress-tracking) 5. [Image Cropping](#image-cropping) 6. [File Organization](#file-organization) 7. [Direct Upload to S3](#direct-upload-to-s3) 8. [Background Processing](#background-processing) ## Avatar Upload Simple avatar upload with preview and cropping. ### Component ```typescript // src/components/AvatarUpload.tsx 'use client'; import { useFileUpload } from '@/hooks/useFiles'; import { useState } from 'react'; import Image from 'next/image'; interface AvatarUploadProps { userId: string; currentAvatar?: string; onUploadComplete?: (url: string) => void; } export default function AvatarUpload({ userId, currentAvatar, onUploadComplete, }: AvatarUploadProps) { const { upload, loading } = useFileUpload(); const [preview, setPreview] = useState(null); const [error, setError] = useState(null); const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; // Validate file type if (!file.type.startsWith('image/')) { setError('Please select an image file'); return; } // Validate file size (2MB max for avatars) if (file.size > 2 * 1024 * 1024) { setError('Image too large. Max size: 2MB'); return; } // Show preview const reader = new FileReader(); reader.onload = (e) => setPreview(e.target?.result as string); reader.readAsDataURL(file); try { setError(null); // Upload with thumbnail generation const result = await upload(file, { path: `avatars/${userId}/`, isPublic: true, }); const uploadedFile = result.data.uploadFile; // Use thumbnail for avatar (150x150) const avatarUrl = uploadedFile.thumbnailUrl || uploadedFile.url; // Update user profile await fetch('/api/user/update-avatar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ avatarUrl }), }); onUploadComplete?.(avatarUrl); } catch (err) { setError(err instanceof Error ? err.message : 'Upload failed'); setPreview(null); } }; return (
{/* Current Avatar */}
{preview || currentAvatar ? ( Avatar ) : (
No avatar
)}
{/* Upload Button */}
{/* Loading State */} {loading && (

Uploading...

)} {/* Error Message */} {error && (

{error}

)}
); } ``` ### Usage ```typescript // src/app/profile/page.tsx import AvatarUpload from '@/components/AvatarUpload'; import { auth } from '@/lib/auth'; export default async function ProfilePage() { const session = await auth(); return (

Profile

{ console.log('Avatar updated:', url); // Optionally refresh page or update state }} />
); } ``` ## Multi-File Upload Upload multiple files at once with progress tracking. ```typescript // src/components/MultiFileUpload.tsx 'use client'; import { useMultipleFileUpload } from '@/hooks/useFiles'; import { useState } from 'react'; interface UploadProgress { file: File; progress: number; status: 'pending' | 'uploading' | 'complete' | 'error'; error?: string; url?: string; } export default function MultiFileUpload({ userId }: { userId: string }) { const { upload, loading } = useMultipleFileUpload(); const [uploads, setUploads] = useState([]); const handleFilesChange = async (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []); if (files.length === 0) return; // Initialize progress tracking const initialProgress: UploadProgress[] = files.map(file => ({ file, progress: 0, status: 'pending', })); setUploads(initialProgress); try { // Upload all files for (let i = 0; i < files.length; i++) { const file = files[i]; // Update status to uploading setUploads(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'uploading' } : item )); try { const result = await upload([file], { path: `users/${userId}/documents/`, isPublic: false, }); // Update status to complete setUploads(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'complete', progress: 100, url: result.data.uploadFiles[0].url, } : item )); } catch (err) { // Update status to error setUploads(prev => prev.map((item, idx) => idx === i ? { ...item, status: 'error', error: err instanceof Error ? err.message : 'Upload failed', } : item )); } } } catch (err) { console.error('Multi-file upload failed:', err); } }; return (
{uploads.length > 0 && (
{uploads.map((upload, idx) => (
{upload.file.name} {upload.status}
{upload.status === 'uploading' && (
)} {upload.status === 'error' && (

{upload.error}

)} {upload.status === 'complete' && upload.url && ( View file )}
))}
)}
); } ``` ## Drag & Drop Interface User-friendly drag and drop with react-dropzone. ```typescript // src/components/DropzoneUpload.tsx 'use client'; import { useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { useMultipleFileUpload } from '@/hooks/useFiles'; export default function DropzoneUpload({ userId }: { userId: string }) { const { upload, loading } = useMultipleFileUpload(); const onDrop = useCallback(async (acceptedFiles: File[]) => { try { const result = await upload(acceptedFiles, { path: `users/${userId}/uploads/`, isPublic: false, }); console.log('Uploaded files:', result.data.uploadFiles); alert(`Successfully uploaded ${acceptedFiles.length} file(s)`); } catch (error) { console.error('Upload failed:', error); alert('Upload failed. Please try again.'); } }, [upload, userId]); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, maxSize: 10 * 1024 * 1024, // 10MB accept: { 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'], 'application/pdf': ['.pdf'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], }, }); return (
{loading ? (

Uploading...

) : isDragActive ? (

Drop files here...

) : ( <>

Drag & drop files here, or click to select

Images, PDFs, and documents up to 10MB

)}
); } ``` ## Progress Tracking Advanced progress tracking with XHR upload. ```typescript // src/hooks/useUploadWithProgress.ts import { useState } from 'react'; interface UploadProgress { loaded: number; total: number; percentage: number; } export function useUploadWithProgress() { const [progress, setProgress] = useState(null); const [uploading, setUploading] = useState(false); const uploadFile = async ( file: File, options: { path?: string; isPublic?: boolean } = {} ) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); formData.append('path', options.path || ''); formData.append('isPublic', String(options.isPublic ?? false)); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { setProgress({ loaded: event.loaded, total: event.total, percentage: Math.round((event.loaded / event.total) * 100), }); } }); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(`Upload failed: ${xhr.statusText}`)); } setUploading(false); setProgress(null); }); xhr.addEventListener('error', () => { reject(new Error('Network error')); setUploading(false); setProgress(null); }); xhr.addEventListener('abort', () => { reject(new Error('Upload cancelled')); setUploading(false); setProgress(null); }); setUploading(true); xhr.open('POST', '/api/upload'); xhr.send(formData); }); }; return { uploadFile, progress, uploading }; } ``` ### Component ```typescript // src/components/ProgressUpload.tsx 'use client'; import { useUploadWithProgress } from '@/hooks/useUploadWithProgress'; export default function ProgressUpload() { const { uploadFile, progress, uploading } = useUploadWithProgress(); const handleUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; try { const result = await uploadFile(file, { isPublic: false }); console.log('Upload complete:', result); } catch (error) { console.error('Upload failed:', error); } }; const formatBytes = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; }; return (
{progress && (
{formatBytes(progress.loaded)} / {formatBytes(progress.total)} {progress.percentage}%
{progress.percentage}%
)}
); } ``` ## Image Cropping Crop images before upload using react-image-crop. ```typescript // src/components/ImageCropUpload.tsx 'use client'; import { useState, useRef } from 'react'; import ReactCrop, { Crop, PixelCrop } from 'react-image-crop'; import 'react-image-crop/dist/ReactCrop.css'; import { useFileUpload } from '@/hooks/useFiles'; export default function ImageCropUpload({ userId }: { userId: string }) { const { upload, loading } = useFileUpload(); const [src, setSrc] = useState(null); const [crop, setCrop] = useState(); const [completedCrop, setCompletedCrop] = useState(); const imgRef = useRef(null); const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => setSrc(reader.result as string); reader.readAsDataURL(file); }; const getCroppedImage = async (): Promise => { if (!completedCrop || !imgRef.current) { throw new Error('No crop defined'); } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('No 2d context'); const scaleX = imgRef.current.naturalWidth / imgRef.current.width; const scaleY = imgRef.current.naturalHeight / imgRef.current.height; canvas.width = completedCrop.width; canvas.height = completedCrop.height; ctx.drawImage( imgRef.current, completedCrop.x * scaleX, completedCrop.y * scaleY, completedCrop.width * scaleX, completedCrop.height * scaleY, 0, 0, completedCrop.width, completedCrop.height ); return new Promise((resolve) => { canvas.toBlob((blob) => { if (!blob) throw new Error('Canvas is empty'); resolve(blob); }, 'image/jpeg', 0.95); }); }; const handleUpload = async () => { try { const croppedBlob = await getCroppedImage(); const croppedFile = new File([croppedBlob], 'cropped.jpg', { type: 'image/jpeg', }); const result = await upload(croppedFile, { path: `avatars/${userId}/`, isPublic: true, }); console.log('Uploaded:', result.data.uploadFile); setSrc(null); } catch (error) { console.error('Upload failed:', error); } }; return (
{src && ( <> setCrop(c)} onComplete={(c) => setCompletedCrop(c)} aspect={1} // Square crop > Crop preview )}
); } ``` ## File Organization Organize files in folders with breadcrumb navigation. ```typescript // src/components/FileManager.tsx 'use client'; import { useUserFiles } from '@/hooks/useFiles'; import { useState } from 'react'; export default function FileManager({ userId }: { userId: string }) { const [currentPath, setCurrentPath] = useState([]); const { files, loading } = useUserFiles(userId); // Filter files by current path const currentFiles = files.filter(file => { const filePath = file.path.split('/'); filePath.pop(); // Remove filename return filePath.join('/') === currentPath.join('/'); }); // Get folders in current path const folders = [...new Set( files .filter(file => { const filePath = file.path.split('/'); filePath.pop(); return filePath.length > currentPath.length && filePath.slice(0, currentPath.length).join('/') === currentPath.join('/'); }) .map(file => { const filePath = file.path.split('/'); filePath.pop(); return filePath[currentPath.length]; }) )]; return (
{/* Breadcrumb */} {/* Folders */} {folders.length > 0 && (
{folders.map(folder => ( ))}
)} {/* Files */} {loading ? (

Loading...

) : currentFiles.length === 0 ? (

No files in this folder

) : (
{currentFiles.map(file => (
{file.thumbnailUrl && ( {file.name} )}

{file.name}

Download
))}
)}
); } ``` ## See Also - [File Upload Pipeline Guide](file-upload-pipeline.md) - [File Upload Security](../security/file-upload-security.md) - [Quick Start Tutorial](../tutorials/file-uploads-quickstart.md) - [GraphQL API Documentation](../reference/api/README.md)