components/ImageUpload.tsx (127 lines of code) (raw):
"use client";
import { useCallback, useState, useEffect } from "react";
import { useDropzone } from "react-dropzone";
import { Button } from "./ui/button";
import { Upload as UploadIcon, Image as ImageIcon, X } from "lucide-react";
interface ImageUploadProps {
onImageSelect: (imageData: string) => void;
currentImage: string | null;
onError?: (error: string) => void;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
);
}
export function ImageUpload({ onImageSelect, currentImage, onError }: ImageUploadProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Update the selected file when the current image changes
useEffect(() => {
if (!currentImage) {
setSelectedFile(null);
}
}, [currentImage]);
const onDrop = useCallback(
(acceptedFiles: File[], fileRejections) => {
if (fileRejections?.length > 0) {
const error = fileRejections[0].errors[0];
onError?.(error.message);
return;
}
const file = acceptedFiles[0];
if (!file) return;
setSelectedFile(file);
setIsLoading(true);
// Convert the file to base64
const reader = new FileReader();
reader.onload = (event) => {
if (event.target && event.target.result) {
const result = event.target.result as string;
onImageSelect(result);
}
setIsLoading(false);
};
reader.onerror = (error) => {
onError?.("Error reading file. Please try again.");
setIsLoading(false);
};
reader.readAsDataURL(file);
},
[onImageSelect]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"]
},
maxSize: 10 * 1024 * 1024, // 10MB
multiple: false
});
const handleRemove = () => {
setSelectedFile(null);
onImageSelect("");
};
return (
<div className="w-full">
{!currentImage ? (
<div
{...getRootProps()}
className={`min-h-[150px] p-4 rounded-lg
${isDragActive ? "bg-secondary/50" : "bg-secondary"}
${isLoading ? "opacity-50 cursor-wait" : ""}
transition-colors duration-200 ease-in-out hover:bg-secondary/50
border-2 border-dashed border-secondary
cursor-pointer flex items-center justify-center gap-4
`}
>
<input {...getInputProps()} />
<div className="flex flex-row items-center" role="presentation">
<UploadIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" aria-hidden="true" />
<div className="">
<p className="text-sm font-medium text-foreground">
Drop your image here or click to browse
</p>
<p className="text-xs text-muted-foreground">
Maximum file size: 10MB
</p>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center p-4 rounded-lg bg-secondary">
<div className="flex w-full items-center mb-4">
<ImageIcon className="w-8 h-8 text-primary mr-3 flex-shrink-0" aria-hidden="true" />
<div className="flex-grow min-w-0">
<p className="text-sm font-medium truncate text-foreground">
{selectedFile?.name || "Current Image"}
</p>
{selectedFile && (
<p className="text-xs text-muted-foreground">
{formatFileSize(selectedFile?.size ?? 0)}
</p>
)}
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRemove}
className="flex-shrink-0 ml-2"
>
<X className="w-4 h-4" />
<span className="sr-only">Remove image</span>
</Button>
</div>
<div className="w-full overflow-hidden rounded-md">
<img
src={currentImage}
alt="Selected"
className="w-full h-auto object-contain"
/>
</div>
</div>
)}
</div>
);
}