import api from './api'; export interface UploadedFile { id: string; name: string; size: number; type: string; url: string; uploadedAt: string; } export interface UploadProgress { loaded: number; total: number; percentage: number; } interface PresignedUrlResponse { uploadUrl: string; publicUrl: string; key: string; } type UploadFolder = 'avatars' | 'documents' | 'properties' | 'verification'; class UploadService { private basePath = '/upload'; /** * Get presigned URL from backend */ private async getPresignedUrl( filename: string, contentType: string, folder: UploadFolder = 'documents' ): Promise { const response = await api.post<{ data: PresignedUrlResponse }>( `${this.basePath}/presigned-url`, { filename, contentType, folder, } ); return response.data.data; } /** * Upload file directly to S3 using presigned URL */ private async uploadToS3( uploadUrl: string, file: File, onProgress?: (progress: UploadProgress) => void ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable && onProgress) { const percentage = Math.round((event.loaded * 100) / event.total); onProgress({ loaded: event.loaded, total: event.total, percentage, }); } }); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }); xhr.addEventListener('error', () => { reject(new Error('Upload failed')); }); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.send(file); }); } /** * Upload a single file to S3 */ async uploadFile( file: File, folder: UploadFolder = 'documents', onProgress?: (progress: UploadProgress) => void ): Promise { // Step 1: Get presigned URL from backend const { uploadUrl, publicUrl, key } = await this.getPresignedUrl( file.name, file.type, folder ); // Step 2: Upload directly to S3 await this.uploadToS3(uploadUrl, file, onProgress); // Step 3: Return uploaded file info return { id: key, // Use the S3 key as the ID name: file.name, size: file.size, type: file.type, url: publicUrl, uploadedAt: new Date().toISOString(), }; } /** * Upload multiple files to S3 */ async uploadFiles( files: File[], folder: UploadFolder = 'documents', onProgress?: (fileIndex: number, progress: UploadProgress) => void ): Promise { const uploadedFiles: UploadedFile[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; const uploadedFile = await this.uploadFile( file, folder, onProgress ? (progress) => onProgress(i, progress) : undefined ); uploadedFiles.push(uploadedFile); } return uploadedFiles; } /** * Delete a file from S3 */ async deleteFile(fileKey: string): Promise { // The key needs to be URL encoded for path parameter const encodedKey = encodeURIComponent(fileKey); await api.delete(`${this.basePath}/${encodedKey}`); } } export const uploadService = new UploadService();