import { useState } from 'react'; import { trpc } from '../src/trpc-client'; type ContextString = 'review' | 'product_info' | 'notification' | 'store'; interface UploadInput { blob: Blob; mimeType: string; } interface UploadBatchInput { images: UploadInput[]; contextString: ContextString; } interface UploadResult { keys: string[]; presignedUrls: string[]; } export function useUploadToObjectStorage() { const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null); const generateUploadUrls = trpc.common.generateUploadUrls.useMutation(); const upload = async (input: UploadBatchInput): Promise => { setIsUploading(true); setError(null); setProgress({ completed: 0, total: input.images.length }); try { const { images, contextString } = input; if (images.length === 0) { return { keys: [], presignedUrls: [] }; } // 1. Get presigned URLs from backend (one call for all images) const mimeTypes = images.map(img => img.mimeType); const { uploadUrls } = await generateUploadUrls.mutateAsync({ contextString, mimeTypes, }); if (uploadUrls.length !== images.length) { throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`); } // 2. Upload all images in parallel const uploadPromises = images.map(async (image, index) => { const presignedUrl = uploadUrls[index]; const { blob, mimeType } = image; const response = await fetch(presignedUrl, { method: 'PUT', body: blob, headers: { 'Content-Type': mimeType }, }); if (!response.ok) { throw new Error(`Upload ${index + 1} failed with status ${response.status}`); } // Update progress setProgress(prev => prev ? { ...prev, completed: prev.completed + 1 } : null); return { key: extractKeyFromPresignedUrl(presignedUrl), presignedUrl, }; }); // Use Promise.all - if any fails, entire batch fails const results = await Promise.all(uploadPromises); return { keys: results.map(r => r.key), presignedUrls: results.map(r => r.presignedUrl), }; } catch (err) { const uploadError = err instanceof Error ? err : new Error('Upload failed'); setError(uploadError); throw uploadError; } finally { setIsUploading(false); setProgress(null); } }; const uploadSingle = async (blob: Blob, mimeType: string, contextString: ContextString): Promise<{ key: string; presignedUrl: string }> => { const result = await upload({ images: [{ blob, mimeType }], contextString, }); return { key: result.keys[0], presignedUrl: result.presignedUrls[0], }; }; return { upload, uploadSingle, isUploading, error, progress, isPending: generateUploadUrls.isPending }; } function extractKeyFromPresignedUrl(url: string): string { const u = new URL(url); let rawKey = u.pathname.replace(/^\/+/, ''); rawKey = rawKey.split('/').slice(1).join('/'); // make meatfarmer/product-images/asdf as product-images/asdf return decodeURIComponent(rawKey); }