You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

102 lines
3.4 KiB

import mediaUpload, { UPLOAD_ABORTED_ERROR_MSG } from '@/services/media-upload.service'
import { useRef } from 'react'
import { toast } from 'sonner'
import logger from '@/lib/logger'
export default function Uploader({
children,
onUploadSuccess,
onUploadStart,
onUploadEnd,
onProgress,
onUploadCompressPhase,
onUploadCompressProgress,
className,
accept = 'image/*',
maxFileSizeMb,
maxCompressedSizeMb
}: {
children: React.ReactNode
onUploadSuccess: (result: { url: string; tags: string[][]; file?: File }) => void
onUploadStart?: (file: File, cancel: () => void) => void
onUploadEnd?: (file: File) => void
onProgress?: (file: File, progress: number) => void
/** After local compression (before network upload). */
onUploadCompressPhase?: (file: File, phase: 'compressing' | 'uploading') => void
/** 0–100 during local compression only. */
onUploadCompressProgress?: (file: File, percent: number) => void
className?: string
accept?: string
/** Reject files whose original size exceeds this limit (before compression). */
maxFileSizeMb?: number
/** Reject when compressed size exceeds this limit (after local encode, before upload). */
maxCompressedSizeMb?: number
}) {
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.files) return
const abortControllerMap = new Map<File, AbortController>()
for (const file of event.target.files) {
const abortController = new AbortController()
abortControllerMap.set(file, abortController)
onUploadStart?.(file, () => abortController.abort())
}
for (const file of event.target.files) {
if (maxFileSizeMb !== undefined && file.size > maxFileSizeMb * 1024 * 1024) {
toast.error(
`"${file.name}" is too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Maximum is ${maxFileSizeMb} MB.`
)
onUploadEnd?.(file)
continue
}
try {
const abortController = abortControllerMap.get(file)
const result = await mediaUpload.upload(file, {
onProgress: (p) => onProgress?.(file, p),
signal: abortController?.signal,
onCompressStart: () => onUploadCompressPhase?.(file, 'compressing'),
onCompressEnd: () => onUploadCompressPhase?.(file, 'uploading'),
onCompressProgress: (p) => onUploadCompressProgress?.(file, p),
maxCompressedSizeMb
})
onUploadSuccess({ ...result, file })
onUploadEnd?.(file)
} catch (error) {
logger.error('Error uploading file', { error, file: file.name })
const message = (error as Error).message
if (message !== UPLOAD_ABORTED_ERROR_MSG) {
toast.error(`Failed to upload file: ${message}`)
}
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
onUploadEnd?.(file)
}
}
}
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.value = '' // clear the value so that the same file can be uploaded again
fileInputRef.current.click()
}
}
return (
<div className={className}>
<div onClick={handleUploadClick}>{children}</div>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileChange}
accept={accept}
multiple
/>
</div>
)
}