'use client'; import React, { useState, useEffect, useRef } from 'react'; import { Upload, X, Loader2, Image as ImageIcon } from 'lucide-react'; import { toast } from 'sonner'; import { getAccessToken } from '@/services/api/client'; import { API_BASE_URL } from '@/services/api/config'; interface ImageUploadProps { entityType: string; entityId?: string; onUploadSuccess: (url: string) => void; onBatchUpload?: (urls: string[]) => void; value?: string; onClear?: () => void; className?: string; size?: 'sm' | 'md'; } export default function ImageUpload({ entityType, entityId, onUploadSuccess, onBatchUpload, value, onClear, className = '', size = 'md', }: ImageUploadProps) { const [uploading, setUploading] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [localPreview, setLocalPreview] = useState(''); const fileInputRef = useRef(null); useEffect(() => { setLocalPreview(value || ''); }, [value]); // Generate a random ID if not provided, for temporary upload directory mapping const resolveEntityId = () => { if (entityId && entityId.trim() !== '') return entityId; return 'temp_' + Math.random().toString(36).substring(2, 11); }; const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { if (e.target.files.length > 1) { uploadMultipleFiles(Array.from(e.target.files)); } else { uploadFile(e.target.files[0]); } } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }; const handleDragLeave = () => { setIsDragOver(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 1) { uploadMultipleFiles(Array.from(e.dataTransfer.files)); } else { uploadFile(e.dataTransfer.files[0]); } } }; const triggerFileSelect = () => { fileInputRef.current?.click(); }; const uploadFile = async (file: File) => { // Basic file validation const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { toast.error('Only JPG, JPEG, PNG, or WebP images are allowed'); return; } const maxSize = 20 * 1024 * 1024; // 20MB if (file.size > maxSize) { toast.error('Image is larger than 20MB'); return; } setUploading(true); const formData = new FormData(); formData.append('file', file); formData.append('entity_type', entityType); formData.append('entity_id', resolveEntityId()); try { const token = getAccessToken(); const headers: Record = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${API_BASE_URL}/api/v1/files/upload`, { method: 'POST', headers, body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'Upload failed' })); throw new Error(errorData.detail || 'Upload failed'); } const data = await response.json(); // Format the public URL paths properly (prefer webp_path, fallback to storage_path / raw_path) const rawPath = data?.webp_path || data?.storage_path || data?.raw_path || data?.url || ''; const imageUrl = rawPath ? (rawPath.startsWith('/') ? rawPath : `/${rawPath}`) : ''; setLocalPreview(imageUrl); onUploadSuccess(imageUrl); toast.success('Image uploaded'); } catch (err: any) { toast.error(err?.message || 'Could not upload the image. Check that the server is running.'); console.error('File upload error:', err); } finally { setUploading(false); } }; const uploadMultipleFiles = async (files: File[]) => { const allowedExtensions = ['jpg', 'jpeg', 'png', 'webp']; const validFiles = files.filter((f) => { const ext = f.name.split('.').pop()?.toLowerCase() || ''; return (f.type && f.type.startsWith('image/')) || allowedExtensions.includes(ext); }); if (validFiles.length === 0) { toast.error('Only JPG, JPEG, PNG, or WebP images are allowed'); return; } setUploading(true); const token = getAccessToken(); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; try { const uploads = await Promise.all( validFiles.map(async (file) => { const formData = new FormData(); formData.append('file', file); formData.append('entity_type', entityType); formData.append('entity_id', resolveEntityId()); const res = await fetch(`${API_BASE_URL}/api/v1/files/upload`, { method: 'POST', headers, body: formData, }); if (res.ok) { const data = await res.json(); const rawPath = data?.webp_path || data?.storage_path || data?.raw_path || data?.url || ''; if (rawPath) { return rawPath.startsWith('/') ? rawPath : `/${rawPath}`; } } return null; }) ); const urls = uploads.filter((u): u is string => Boolean(u)); if (urls.length > 0) { toast.success(`Uploaded ${urls.length} image(s)`); if (onBatchUpload) { onBatchUpload(urls); } else { setLocalPreview(urls[0]); onUploadSuccess(urls[0]); } } else { toast.error('Failed to upload image(s)'); } } catch (err: any) { toast.error('Upload failed'); console.error(err); } finally { setUploading(false); } }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); setLocalPreview(''); if (onClear) { onClear(); } else { onUploadSuccess(''); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const activeValue = localPreview || value || ''; const hasValue = Boolean(activeValue && typeof activeValue === 'string' && activeValue.trim() !== ''); const displaySrc = hasValue ? activeValue.startsWith('http://') || activeValue.startsWith('https://') ? activeValue : `${API_BASE_URL}${activeValue.startsWith('/') ? activeValue : `/${activeValue}`}` : ''; const compact = size === 'sm'; return (
{hasValue && displaySrc ? (
Upload Preview {compact ? ( Image selected ) : (
Change Image
)}
) : (
{uploading ? (
Uploading...
) : compact ? ( <> Upload image ยท PNG, JPG, WEBP ) : (
Click to upload or drag and drop
PNG, JPG, WEBP (Max 20MB)
)}
)}
); }