Agregar opción para cargar avatar desde URL en edición de perfil
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s
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.
This commit is contained in:
@@ -49,6 +49,33 @@
|
|||||||
<span class="dropzone-hint">o usa los botones de abajo</span>
|
<span class="dropzone-hint">o usa los botones de abajo</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Campo para URL de imagen -->
|
||||||
|
<div class="avatar-url-section">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<UIcon name="i-heroicons-link" class="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">O pega una URL de imagen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UInput
|
||||||
|
v-model="avatarUrl"
|
||||||
|
placeholder="https://ejemplo.com/imagen.jpg"
|
||||||
|
:disabled="isUploading"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
@click="loadAvatarFromUrl"
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
:disabled="isUploading || !avatarUrl"
|
||||||
|
:loading="isLoadingUrl"
|
||||||
|
>
|
||||||
|
<UIcon name="i-heroicons-arrow-down-tray" class="w-4 h-4" />
|
||||||
|
Cargar
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<span v-if="urlError" class="text-xs text-red-500 mt-1">{{ urlError }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Botones de acción -->
|
<!-- Botones de acción -->
|
||||||
<div class="avatar-actions">
|
<div class="avatar-actions">
|
||||||
<input
|
<input
|
||||||
@@ -434,10 +461,15 @@ const isUploading = ref(false)
|
|||||||
const showCamera = ref(false)
|
const showCamera = ref(false)
|
||||||
const showExitConfirm = ref(false)
|
const showExitConfirm = ref(false)
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
|
const isLoadingUrl = ref(false)
|
||||||
|
|
||||||
// Ref para el input de archivo
|
// Ref para el input de archivo
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
// URL del avatar
|
||||||
|
const avatarUrl = ref('')
|
||||||
|
const urlError = ref('')
|
||||||
|
|
||||||
// Cookie para persistir cambios del formulario
|
// Cookie para persistir cambios del formulario
|
||||||
const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
|
const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 días
|
maxAge: 60 * 60 * 24 * 7, // 7 días
|
||||||
@@ -815,6 +847,91 @@ const processImageFile = async (file: File) => {
|
|||||||
await handleAvatarCapture(blob)
|
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
|
// Procesar imagen compartida desde Web Share Target
|
||||||
const processSharedImage = async (imageUrl: string) => {
|
const processSharedImage = async (imageUrl: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -1045,10 +1162,19 @@ const processSharedImage = async (imageUrl: string) => {
|
|||||||
font-style: italic;
|
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 {
|
.avatar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-modal {
|
.camera-modal {
|
||||||
@@ -1301,6 +1427,11 @@ const processSharedImage = async (imageUrl: string) => {
|
|||||||
color: var(--color-gray-400) !important;
|
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 {
|
.dark .form-actions {
|
||||||
border-top-color: rgba(255, 255, 255, 0.1);
|
border-top-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user