"use client"
import type * as React from "react"
import { useCallback, useState } from "react"
import { FileIcon, UploadCloud, X, AlertCircle, CheckCircle2, Loader2 } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
export interface FileUploadProps extends React.HTMLAttributes<HTMLDivElement> {
/** Maximum file size in bytes (default: 5MB) */
maxSize?: number
/** Allowed file types (e.g. ['image/png', 'image/jpeg']) */
accept?: string[]
/** Maximum number of files (default: 5) */
maxFiles?: number
/** Whether to allow multiple file selection (default: true) */
multiple?: boolean
/** Whether to disable the upload component */
disabled?: boolean
/** Function called when files are added */
onFilesAdded?: (files: File[]) => void | Promise<void>
/** Function to handle file upload (simulate progress for demo) */
onUpload?: (files: File[]) => Promise<void>
}
type FileWithProgress = {
file: File
progress: number
error?: string
uploaded?: boolean
}
export function FileUpload({
maxSize = 5 * 1024 * 1024, // 5MB
accept = [],
maxFiles = 5,
multiple = true,
disabled = false,
onFilesAdded,
onUpload,
className,
...props
}: FileUploadProps) {
const [isDragging, setIsDragging] = useState(false)
const [files, setFiles] = useState<FileWithProgress[]>([])
const [isUploading, setIsUploading] = useState(false)
const handleDragEnter = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
if (!disabled) setIsDragging(true)
},
[disabled],
)
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}, [])
const handleDragOver = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
if (!disabled) setIsDragging(true)
},
[disabled],
)
const validateFile = (file: File): string | null => {
if (file.size > maxSize) {
return `File is too large. Maximum size is ${maxSize / (1024 * 1024)}MB.`
}
if (accept.length > 0 && !accept.includes(file.type)) {
return `File type not supported. Accepted types: ${accept.join(", ")}`
}
return null
}
const processFiles = useCallback(
(fileList: FileList | null) => {
if (!fileList || disabled) return
const newFiles: File[] = []
// Convert FileList to array and validate
Array.from(fileList).forEach((file) => {
const error = validateFile(file)
if (!error && files.length + newFiles.length < maxFiles) {
newFiles.push(file)
}
})
if (newFiles.length > 0) {
const filesWithProgress = newFiles.map((file) => ({ file, progress: 0 }))
setFiles((prev) => [...prev, ...filesWithProgress])
if (onFilesAdded) {
onFilesAdded(newFiles)
}
}
},
[disabled, files.length, maxFiles, onFilesAdded],
)
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
processFiles(e.dataTransfer.files)
},
[processFiles],
)
const handleFileInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
processFiles(e.target.files)
// Reset the input value so the same file can be selected again
e.target.value = ""
},
[processFiles],
)
const removeFile = useCallback((indexToRemove: number) => {
setFiles((prev) => prev.filter((_, index) => index !== indexToRemove))
}, [])
const handleUpload = async () => {
if (files.length === 0 || !onUpload || isUploading) return
setIsUploading(true)
try {
// Simulate upload progress for each file
const filesToUpload = files.filter((f) => !f.uploaded && !f.error)
if (filesToUpload.length === 0) {
setIsUploading(false)
return
}
// Update progress for each file
const updateProgress = () => {
setFiles((prev) =>
prev.map((fileData) => {
if (fileData.uploaded || fileData.error) return fileData
const newProgress = Math.min(fileData.progress + Math.random() * 20, 100)
return {
...fileData,
progress: newProgress,
uploaded: newProgress >= 100,
}
}),
)
}
// Simulate progress updates
const progressInterval = setInterval(updateProgress, 500)
// Call the actual upload function
await onUpload(filesToUpload.map((f) => f.file))
// Ensure all files show as completed
clearInterval(progressInterval)
setFiles((prev) =>
prev.map((fileData) => {
if (fileData.uploaded || fileData.error) return fileData
return { ...fileData, progress: 100, uploaded: true }
}),
)
} catch (error) {
console.error("Upload failed:", error)
} finally {
setIsUploading(false)
}
}
const isMaxFilesReached = files.length >= maxFiles
return (
<div className={cn("space-y-4", className)} {...props}>
<div
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
"relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors",
isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25",
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-primary/50",
"focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
)}
>
<input
id="file-upload"
type="file"
className="sr-only"
multiple={multiple}
accept={accept.join(",")}
disabled={disabled || isMaxFilesReached}
onChange={handleFileInputChange}
/>
<label
htmlFor="file-upload"
className={cn(
"flex flex-col items-center justify-center gap-2 text-center",
disabled || isMaxFilesReached ? "cursor-not-allowed" : "cursor-pointer",
)}
>
<div className="rounded-full bg-primary/10 p-3">
<UploadCloud className="h-8 w-8 text-primary" />
</div>
<div className="space-y-1">
<p className="text-sm font-medium">{isDragging ? "Drop files here" : "Drag and drop files here"}</p>
<p className="text-xs text-muted-foreground">
or <span className="text-primary font-medium">browse</span> to upload
</p>
</div>
<div className="text-xs text-muted-foreground mt-2">
{accept.length > 0 && <p>Accepted file types: {accept.join(", ")}</p>}
<p>Max file size: {maxSize / (1024 * 1024)}MB</p>
<p>
Files: {files.length}/{maxFiles}
</p>
</div>
</label>
</div>
{files.length > 0 && (
<div className="space-y-4">
<div className="space-y-2">
{files.map((fileData, index) => (
<div key={`${fileData.file.name}-${index}`} className="flex items-center gap-2 rounded-md border p-2">
<div className="shrink-0 p-2">
<FileIcon className="h-6 w-6 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium">{fileData.file.name}</p>
<div className="flex items-center gap-2">
{fileData.error ? (
<AlertCircle className="h-4 w-4 text-destructive" />
) : fileData.uploaded ? (
<CheckCircle2 className="h-4 w-4 text-primary" />
) : fileData.progress > 0 ? (
<span className="text-xs text-muted-foreground">{Math.round(fileData.progress)}%</span>
) : null}
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => removeFile(index)}
disabled={isUploading}
>
<X className="h-4 w-4" />
<span className="sr-only">Remove file</span>
</Button>
</div>
</div>
<div className="mt-1">
{fileData.error ? (
<p className="text-xs text-destructive">{fileData.error}</p>
) : (
<Progress value={fileData.progress} className="h-1" />
)}
</div>
<p className="mt-1 text-xs text-muted-foreground">{(fileData.file.size / 1024).toFixed(1)} KB</p>
</div>
</div>
))}
</div>
<div className="flex justify-end">
<Button onClick={handleUpload} disabled={isUploading || files.every((f) => f.uploaded || f.error)}>
{isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isUploading ? "Uploading..." : "Upload Files"}
</Button>
</div>
</div>
)}
</div>
)
}