freshyo/apps/user-ui/hooks/useUploadToObjectStore.ts
2026-03-26 17:16:56 +05:30

119 lines
3.1 KiB
TypeScript

import { useState } from 'react'
import { trpc } from '@/src/trpc-client'
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile'
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<Error | null>(null)
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null)
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation()
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true)
setError(null)
setProgress({ completed: 0, total: input.images.length })
try {
const { images, contextString } = input
if (images.length === 0) {
return { keys: [], presignedUrls: [] }
}
const mimeTypes = images.map((img) => img.mimeType)
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: contextString as any,
mimeTypes,
})
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`)
}
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}`)
}
setProgress((prev) => (prev ? { ...prev, completed: prev.completed + 1 } : null))
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
}
})
const results = await Promise.all(uploadPromises)
return {
keys: results.map((result) => result.key),
presignedUrls: results.map((result) => result.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 parsedUrl = new URL(url)
let rawKey = parsedUrl.pathname.replace(/^\/+/, '')
rawKey = rawKey.split('/').slice(1).join('/')
return decodeURIComponent(rawKey)
}