From 00596bd6df221be4a22759ac29f33c9a14e15471 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sat, 18 Oct 2025 00:20:39 -0600 Subject: [PATCH] =?UTF-8?q?Agregar=20opci=C3=B3n=20para=20cargar=20avatar?= =?UTF-8?q?=20desde=20URL=20en=20edici=C3=B3n=20de=20perfil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se añade campo de entrada para pegar URLs de imágenes directamente, permitiendo cargar avatares desde enlaces externos. Incluye validación de URLs, descarga de imágenes con manejo de errores CORS, validación de tipo y tamaño (máx 5MB), con feedback visual de errores específicos. --- nuxt4/app/components/UserProfileForm.vue | 131 +++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/nuxt4/app/components/UserProfileForm.vue b/nuxt4/app/components/UserProfileForm.vue index a54ea3a..b2d296d 100644 --- a/nuxt4/app/components/UserProfileForm.vue +++ b/nuxt4/app/components/UserProfileForm.vue @@ -49,6 +49,33 @@ o usa los botones de abajo + +
+
+ + O pega una URL de imagen +
+
+ + + + Cargar + +
+ {{ urlError }} +
+
(null) +// URL del avatar +const avatarUrl = ref('') +const urlError = ref('') + // Cookie para persistir cambios del formulario const formCookie = useCookie>('profile-form-draft', { maxAge: 60 * 60 * 24 * 7, // 7 días @@ -815,6 +847,91 @@ const processImageFile = async (file: File) => { await handleAvatarCapture(blob) } +// Cargar avatar desde URL +const loadAvatarFromUrl = async () => { + urlError.value = '' + + // Validar que la URL no esté vacía + if (!avatarUrl.value.trim()) { + urlError.value = 'Por favor ingresa una URL' + return + } + + // Validar formato de URL + try { + new URL(avatarUrl.value) + } catch (error) { + urlError.value = 'URL inválida. Por favor ingresa una URL válida (ej: https://ejemplo.com/imagen.jpg)' + return + } + + // Validar que la URL sea https + if (!avatarUrl.value.startsWith('https://') && !avatarUrl.value.startsWith('http://')) { + urlError.value = 'La URL debe comenzar con http:// o https://' + return + } + + isLoadingUrl.value = true + isUploading.value = true + + try { + // Descargar la imagen + const response = await fetch(avatarUrl.value, { + mode: 'cors' + }) + + if (!response.ok) { + throw new Error(`Error al descargar la imagen: ${response.statusText}`) + } + + const blob = await response.blob() + + // Validar que sea una imagen + if (!blob.type.startsWith('image/')) { + throw new Error('El archivo en la URL no es una imagen válida') + } + + // Validar tamaño (máximo 5MB) + const maxSize = 5 * 1024 * 1024 // 5MB + if (blob.size > maxSize) { + throw new Error('La imagen debe ser menor a 5MB') + } + + // Procesar la imagen + await handleAvatarCapture(blob) + + // Limpiar el campo de URL y errores + avatarUrl.value = '' + urlError.value = '' + + toast.add({ + title: 'Imagen cargada', + description: 'La imagen se descargó correctamente desde la URL', + color: 'success', + icon: 'i-heroicons-check-circle' + }) + } catch (error: any) { + console.error('Error loading image from URL:', error) + + // Manejar errores de CORS + if (error.message.includes('CORS') || error.message.includes('NetworkError')) { + urlError.value = 'No se pudo acceder a la imagen. Puede que el servidor no permita descargas externas (CORS). Intenta descargar la imagen manualmente y subirla desde tu dispositivo.' + } else { + urlError.value = error.message || 'No se pudo cargar la imagen desde la URL' + } + + toast.add({ + title: 'Error', + description: error.message || 'No se pudo cargar la imagen desde la URL', + color: 'error', + icon: 'i-heroicons-exclamation-triangle' + }) + } finally { + isLoadingUrl.value = false + isUploading.value = false + } +} + // Procesar imagen compartida desde Web Share Target const processSharedImage = async (imageUrl: string) => { try { @@ -1045,10 +1162,19 @@ const processSharedImage = async (imageUrl: string) => { font-style: italic; } +.avatar-url-section { + margin-top: 1rem; + padding: 1rem; + background: rgba(var(--color-gray-100), 0.5); + border-radius: 0.75rem; + border: 1px solid rgba(var(--color-gray-300), 0.5); +} + .avatar-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; + margin-top: 1rem; } .camera-modal { @@ -1301,6 +1427,11 @@ const processSharedImage = async (imageUrl: string) => { color: var(--color-gray-400) !important; } +.dark .avatar-url-section { + background: rgba(255, 255, 255, 0.03) !important; + border-color: rgba(255, 255, 255, 0.1) !important; +} + .dark .form-actions { border-top-color: rgba(255, 255, 255, 0.1); }