"use client"; import React, { useState, useRef, useCallback } from "react"; import { cn } from "@/lib/utils"; import { ImageIcon, X, Upload } from "lucide-react"; export interface FeatureImage { id: string; data: string; // base64 encoded mimeType: string; filename: string; size: number; } interface FeatureImageUploadProps { images: FeatureImage[]; onImagesChange: (images: FeatureImage[]) => void; maxFiles?: number; maxFileSize?: number; // in bytes, default 10MB className?: string; disabled?: boolean; } const ACCEPTED_IMAGE_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", ]; const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB export function FeatureImageUpload({ images, onImagesChange, maxFiles = 5, maxFileSize = DEFAULT_MAX_FILE_SIZE, className, disabled = false, }: FeatureImageUploadProps) { const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const fileInputRef = useRef(null); const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Failed to read file as base64")); } }; reader.onerror = () => reject(new Error("Failed to read file")); reader.readAsDataURL(file); }); }; const processFiles = useCallback( async (files: FileList) => { if (disabled || isProcessing) return; setIsProcessing(true); const newImages: FeatureImage[] = []; const errors: string[] = []; for (const file of Array.from(files)) { // Validate file type if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { errors.push( `${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.` ); continue; } // Validate file size if (file.size > maxFileSize) { const maxSizeMB = maxFileSize / (1024 * 1024); errors.push( `${file.name}: File too large. Maximum size is ${maxSizeMB}MB.` ); continue; } // Check if we've reached max files if (newImages.length + images.length >= maxFiles) { errors.push(`Maximum ${maxFiles} images allowed.`); break; } try { const base64 = await fileToBase64(file); const imageAttachment: FeatureImage = { id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, data: base64, mimeType: file.type, filename: file.name, size: file.size, }; newImages.push(imageAttachment); } catch (error) { errors.push(`${file.name}: Failed to process image.`); } } if (errors.length > 0) { console.warn("Image upload errors:", errors); } if (newImages.length > 0) { onImagesChange([...images, ...newImages]); } setIsProcessing(false); }, [disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (disabled) return; const files = e.dataTransfer.files; if (files.length > 0) { processFiles(files); } }, [disabled, processFiles] ); const handleDragOver = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!disabled) { setIsDragOver(true); } }, [disabled] ); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { processFiles(files); } // Reset the input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = ""; } }, [processFiles] ); const handleBrowseClick = useCallback(() => { if (!disabled && fileInputRef.current) { fileInputRef.current.click(); } }, [disabled]); const removeImage = useCallback( (imageId: string) => { onImagesChange(images.filter((img) => img.id !== imageId)); }, [images, onImagesChange] ); const clearAllImages = useCallback(() => { onImagesChange([]); }, [onImagesChange]); const formatFileSize = (bytes: number): string => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; }; return (
{/* Hidden file input */} {/* Drop zone */}
{isProcessing ? ( ) : ( )}

{isDragOver && !disabled ? "Drop images here" : "Click or drag images here"}

Up to {maxFiles} images, max{" "} {Math.round(maxFileSize / (1024 * 1024))}MB each

{/* Image previews */} {images.length > 0 && (

{images.length} image{images.length > 1 ? "s" : ""} selected

{images.map((image) => (
{/* Image thumbnail */}
{image.filename}
{/* Remove button */} {!disabled && ( )} {/* Filename tooltip on hover */}

{image.filename}

))}
)}
); }