119 lines
3.1 KiB
TypeScript
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)
|
|
}
|