import { useState, useRef, useEffect, useCallback } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from './tabs'; import { Button } from './button'; import { Input } from './input'; import { Label } from './label'; import { Alert, AlertDescription } from './alert'; import { Upload, Link as LinkIcon, X, Loader2, CheckCircle, AlertCircle, } from 'lucide-react'; import { validateImageInput, validateImageUrlWithBase64, getImageDisplayName, fileToDataURL, type ImageValidationResult } from '../../lib/image-validation'; export interface ImageInputProps { value?: File | string | null; onChange: (file: File | string | null) => void; onValidation?: (result: ImageValidationResult) => void; disabled?: boolean; className?: string; maxSize?: number; // in bytes, default 10MB accept?: string; // file accept attribute placeholder?: string; showPreview?: boolean; previewClassName?: string; } export interface ImageInputState { mode: 'file' | 'url'; validation: ImageValidationResult | null; isValidating: boolean; error: string | null; previewUrl: string | null; } export function ImageInput({ value, onChange, onValidation, disabled = false, className = '', maxSize = 10 * 1024 * 1024, // 10MB accept = 'image/*', placeholder = 'Enter image URL or select a file', showPreview = true, previewClassName = '' }: ImageInputProps) { const [state, setState] = useState({ mode: 'file', validation: null, isValidating: false, error: null, previewUrl: null }); const [urlInput, setUrlInput] = useState(''); const fileInputRef = useRef(null); const validationTimeoutRef = useRef(null); const handleFileValidation = useCallback(async (file: File) => { setState(prev => ({ ...prev, isValidating: true, error: null })); const result = await validateImageInput(file); let previewUrl: string | null = null; if (result.isValid) { try { previewUrl = await fileToDataURL(file); } catch (error) { console.error('Failed to create preview URL:', error); } } setState(prev => ({ ...prev, isValidating: false, validation: result, error: result.isValid ? null : (result.error || null), previewUrl })); onValidation?.(result); }, [onValidation]); const handleUrlValidation = useCallback(async (url: string | null) => { if (!url || !url.trim()) { setState(prev => ({ ...prev, validation: null, error: null, previewUrl: null })); onValidation?.({ isValid: false, error: 'Please enter a URL' }); return; } setState(prev => ({ ...prev, isValidating: true, error: null })); try { const result = await validateImageUrlWithBase64(url); // Use temporary URL for preview if available, otherwise fall back to base64 data or original URL const previewUrl = result.isValid ? (result.tempUrl || result.base64Data || url) : null; setState(prev => ({ ...prev, isValidating: false, validation: result, error: result.isValid ? null : (result.error || null), previewUrl })); onValidation?.(result); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to validate URL'; setState(prev => ({ ...prev, isValidating: false, validation: { isValid: false, error: errorMessage }, error: errorMessage, previewUrl: null })); onValidation?.({ isValid: false, error: errorMessage }); } }, [onValidation]); // Handle external value changes useEffect(() => { if (value === null) { setState(prev => ({ ...prev, validation: null, error: null, previewUrl: null })); setUrlInput(''); return; } if (value instanceof File) { // File mode setState(prev => ({ ...prev, mode: 'file', validation: null, error: null, previewUrl: null })); setUrlInput(''); // Validate file immediately handleFileValidation(value); } else { // URL mode - value is a string (not File, not null) // Don't set preview URL yet, wait for validation to complete setState(prev => ({ ...prev, mode: 'url', validation: null, error: null, previewUrl: null })); // value should be a string here, but cast it to be safe setUrlInput(value || ''); // Validate URL (with debounce) if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current); } validationTimeoutRef.current = setTimeout(() => { // Only validate if we have a non-empty string const urlValue = typeof value === 'string' ? value : null; if (urlValue && urlValue.trim()) { handleUrlValidation(urlValue); } else { handleUrlValidation(null); } }, 500); } }, [value, maxSize, handleFileValidation, handleUrlValidation]); // Cleanup timeout on unmount useEffect(() => { return () => { if (validationTimeoutRef.current) { clearTimeout(validationTimeoutRef.current); } }; }, []); const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { onChange(file); } }; const handleUrlInputChange = (event: React.ChangeEvent) => { const url = event.target.value; setUrlInput(url); onChange(url || null); }; const handleModeChange = (mode: 'file' | 'url') => { setState(prev => ({ ...prev, mode, validation: null, error: null, previewUrl: null })); onChange(null); setUrlInput(''); }; const handleClear = () => { onChange(null); setUrlInput(''); setState(prev => ({ ...prev, validation: null, error: null, previewUrl: null })); }; const isValid = state.validation?.isValid; const hasError = state.error && !isValid; const canPreview = isValid && state.previewUrl; const isCorsBlocked = state.validation?.isCorsBlocked; const hasBase64Data = !!state.validation?.base64Data; return (
handleModeChange(value as 'file' | 'url')}> Upload File From URL
{value instanceof File && ( )}
{typeof value === 'string' && value && (

Current: {getImageDisplayName(value)}

)}

Enter a URL that ends with an image extension (.jpg, .png, .gif, etc.)

{/* Validation Status */} {state.isValidating && (
Validating and downloading image...
)} {isValid && ( Image is valid and ready to use {state.validation?.filename && ` (${state.validation.filename})`} {hasBase64Data && ( ✓ Image downloaded and cached for preview )} {isCorsBlocked && ( Note: Downloaded using fallback method due to CORS restrictions )} )} {hasError && ( {state.error} {state.error?.includes('CORS') && ( Try using a different image URL or upload the file directly )} )} {/* Image Preview */} {showPreview && canPreview && (
Image preview
)} {/* Clear Button */} {value && (
)}
); } export default ImageInput;