@gambito-corp/mbs-library
Version:
Librería de componentes React reutilizables - Sistema de diseño modular y escalable
313 lines (270 loc) • 11.2 kB
JSX
import React, { useState, useEffect } from 'react';
import Label from '../atoms/Label';
import Input from '../atoms/Input';
import FileInput from '../atoms/FileInput';
import ImagePreview from '../atoms/ImagePreview';
const ImageUploadField = ({
id,
label,
urlValue = '',
fileValue = null,
onUrlChange,
onFileChange,
onClear,
error,
disabled = false,
className = ''
}) => {
const [localUrlValue, setLocalUrlValue] = useState(urlValue);
const [urlError, setUrlError] = useState('');
const [isValidatingUrl, setIsValidatingUrl] = useState(false);
const [previewSrc, setPreviewSrc] = useState('');
const [previewType, setPreviewType] = useState('');
// Sincronizar con prop externa
useEffect(() => {
setLocalUrlValue(urlValue);
}, [urlValue]);
// Solo manejar preview para archivos
useEffect(() => {
if (fileValue) {
const fileUrl = URL.createObjectURL(fileValue);
setPreviewSrc(fileUrl);
setPreviewType('file');
return () => URL.revokeObjectURL(fileUrl);
} else if (!localUrlValue) {
// Si no hay archivo y no hay URL, limpiar preview
setPreviewSrc('');
setPreviewType('');
}
}, [fileValue, localUrlValue]);
// Función para validar si una cadena es una URL válida
const isValidUrl = (string) => {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
};
// Función para verificar si la URL existe (HEAD request)
const checkUrlExists = async (url) => {
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'no-cors' // Para evitar problemas de CORS
});
return true; // Si no hay error, asumimos que existe
} catch (error) {
// Intentar con GET si HEAD falla
try {
const response = await fetch(url, {
method: 'GET',
mode: 'no-cors'
});
return true;
} catch (getError) {
return false;
}
}
};
// Manejar cambio local (solo actualizar estado local)
const handleUrlChange = (e) => {
if (disabled) return;
const newValue = e.target.value;
setLocalUrlValue(newValue);
// Limpiar errores mientras escribe
if (urlError) {
setUrlError('');
}
// Si se vacía, notificar inmediatamente y limpiar preview
if (!newValue.trim()) {
onUrlChange('');
setPreviewSrc('');
setPreviewType('');
}
};
// Manejar cuando el input pierde el foco
// Manejar cuando el input pierde el foco
const handleUrlBlur = async () => {
if (disabled) return;
const trimmedValue = localUrlValue.trim();
if (trimmedValue === '') {
// Si está vacío, no mostrar error ni previsualización
setUrlError('');
setPreviewSrc('');
setPreviewType('');
onUrlChange('');
return;
}
if (!isValidUrl(trimmedValue)) {
// Si no es URL válida, mostrar error
setUrlError('La URL no es válida');
setPreviewSrc('');
setPreviewType('');
onUrlChange('');
return;
}
// Verificar si parece ser una URL de imagen por extensión
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i;
const looksLikeImage = imageExtensions.test(trimmedValue);
if (looksLikeImage) {
// Si parece una imagen, intentar cargarla pero ser más permisivo
setIsValidatingUrl(true);
setUrlError('');
try {
const img = new Image();
img.crossOrigin = 'anonymous';
const imageExists = await new Promise((resolve) => {
img.onload = () => resolve(true);
img.onerror = () => {
// Si falla por CORS, asumir que la imagen existe
// porque la URL tiene extensión de imagen válida
resolve(true);
};
img.src = trimmedValue;
// Timeout más corto - 3 segundos
setTimeout(() => resolve(true), 3000); // ← Asumir que existe si tarda mucho
});
// Si llegamos aquí, mostrar la imagen
setUrlError('');
setPreviewSrc(trimmedValue);
setPreviewType('url');
onUrlChange(trimmedValue);
} catch (error) {
// Incluso si hay error, si parece imagen, permitirla
setUrlError('');
setPreviewSrc(trimmedValue);
setPreviewType('url');
onUrlChange(trimmedValue);
} finally {
setIsValidatingUrl(false);
}
} else {
// Si no parece una imagen, intentar validación más estricta
setIsValidatingUrl(true);
setUrlError('');
try {
const img = new Image();
img.crossOrigin = 'anonymous';
const imageExists = await new Promise((resolve) => {
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = trimmedValue;
// Timeout de 5 segundos para URLs sin extensión
setTimeout(() => resolve(false), 5000);
});
if (imageExists) {
setUrlError('');
setPreviewSrc(trimmedValue);
setPreviewType('url');
onUrlChange(trimmedValue);
} else {
setUrlError('La URL no contiene una imagen válida o no es accesible');
setPreviewSrc('');
setPreviewType('');
onUrlChange('');
}
} catch (error) {
setUrlError('Error al validar la URL');
setPreviewSrc('');
setPreviewType('');
onUrlChange('');
} finally {
setIsValidatingUrl(false);
}
}
};
const handleFileChange = (e) => {
if (disabled) return;
const file = e.target.files[0];
onFileChange(file);
// Si se selecciona archivo, limpiar URL
if (file) {
setLocalUrlValue('');
setUrlError('');
}
};
const handleRemoveImage = () => {
if (disabled) return;
setPreviewSrc('');
setPreviewType('');
setLocalUrlValue('');
setUrlError('');
onClear();
};
const hasImage = previewSrc && (fileValue || (previewType === 'url'));
return (
<div className={`mb-4 ${className}`}>
<Label htmlFor={id}>{label}</Label>
<div className="mt-2 space-y-3">
{/* Preview de imagen */}
{hasImage && (
<div className="flex justify-center">
<ImagePreview
src={previewSrc}
onRemove={handleRemoveImage}
alt={`Preview ${label}`}
/>
</div>
)}
{/* Inputs - solo mostrar cuando no hay imagen válida */}
{!hasImage && (
<div className="space-y-3">
{/* Input URL */}
<div>
<Input
id={`${id}_url`}
type="url"
value={localUrlValue}
onChange={handleUrlChange}
onBlur={handleUrlBlur} // ← Validar solo en blur
placeholder="URL de la imagen (opcional)"
className="w-full"
disabled={disabled}
error={urlError}
/>
{/* Indicadores de estado */}
{isValidatingUrl && (
<div className="flex items-center text-xs text-blue-600 mt-1">
<div className="animate-spin h-3 w-3 border border-blue-600 border-t-transparent rounded-full mr-2"></div>
Validando URL...
</div>
)}
{urlError && (
<div className="text-xs text-red-500 mt-1">
{urlError}
</div>
)}
</div>
{/* Separador */}
<div className="flex items-center">
<div className="flex-1 border-t border-gray-300"></div>
<span className="px-3 text-sm text-gray-500 bg-white">o</span>
<div className="flex-1 border-t border-gray-300"></div>
</div>
{/* Input File */}
<div>
<FileInput
id={`${id}_file`}
onChange={handleFileChange}
accept="image/*"
className="w-full"
disabled={disabled}
/>
</div>
</div>
)}
{/* Mostrar tipo de imagen actual */}
{hasImage && (
<div className="text-xs text-gray-500 text-center">
{previewType === 'url' ? '📎 Imagen desde URL' : '📁 Archivo subido'}
</div>
)}
</div>
{error && (
<span className="text-red-500 text-sm mt-1 block">{error}</span>
)}
</div>
);
};
export default ImageUploadField;