Agregar carga de archivos y drag & drop para foto de perfil
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
- Agregar zona de drag & drop con feedback visual - Agregar botón para subir foto desde dispositivo - Implementar validación de tipo de archivo (solo imágenes) - Implementar validación de tamaño (máximo 5MB) - Agregar estilos responsive y soporte para modo oscuro
This commit is contained in:
@@ -33,7 +33,32 @@
|
||||
<UIcon name="i-heroicons-camera" class="w-5 h-5" />
|
||||
Foto de Perfil
|
||||
</h3>
|
||||
<div class="avatar-actions-simple">
|
||||
|
||||
<!-- Zona de drag & drop -->
|
||||
<div
|
||||
class="avatar-dropzone"
|
||||
:class="{ 'dropzone-active': isDragging }"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
>
|
||||
<UIcon name="i-heroicons-photo" class="w-12 h-12 dropzone-icon" />
|
||||
<p class="dropzone-text">
|
||||
Arrastra una imagen aquí
|
||||
</p>
|
||||
<span class="dropzone-hint">o usa los botones de abajo</span>
|
||||
</div>
|
||||
|
||||
<!-- Botones de acción -->
|
||||
<div class="avatar-actions">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleFileSelect"
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
@click="showCamera = true"
|
||||
color="primary"
|
||||
@@ -41,7 +66,18 @@
|
||||
:disabled="isUploading"
|
||||
>
|
||||
<UIcon name="i-heroicons-camera" class="w-4 h-4" />
|
||||
{{ currentAvatar && currentAvatar.includes('/avatars/') ? 'Cambiar foto' : 'Tomar foto' }}
|
||||
Tomar foto
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@click="triggerFileInput"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
:disabled="isUploading"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-up-tray" class="w-4 h-4" />
|
||||
Subir desde dispositivo
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@@ -392,6 +428,10 @@ const isSubmitting = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
const showExitConfirm = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
// Ref para el input de archivo
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Cookie para persistir cambios del formulario
|
||||
const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
|
||||
@@ -694,6 +734,74 @@ const removeAvatar = async () => {
|
||||
isUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Abrir selector de archivos
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
// Manejar selección de archivo desde input
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (file) {
|
||||
processImageFile(file)
|
||||
}
|
||||
// Limpiar input para permitir seleccionar el mismo archivo de nuevo
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
// Manejar drag over
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = true
|
||||
}
|
||||
|
||||
// Manejar drag leave
|
||||
const handleDragLeave = () => {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
// Manejar drop
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
isDragging.value = false
|
||||
|
||||
const file = event.dataTransfer?.files[0]
|
||||
if (file) {
|
||||
processImageFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
// Procesar archivo de imagen
|
||||
const processImageFile = async (file: File) => {
|
||||
// Validar que sea una imagen
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.add({
|
||||
title: 'Archivo inválido',
|
||||
description: 'Por favor selecciona un archivo de imagen',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-exclamation-triangle'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validar tamaño (máximo 5MB)
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
if (file.size > maxSize) {
|
||||
toast.add({
|
||||
title: 'Archivo muy grande',
|
||||
description: 'La imagen debe ser menor a 5MB',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-exclamation-triangle'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Convertir a Blob y subir
|
||||
const blob = new Blob([file], { type: file.type })
|
||||
await handleAvatarCapture(blob)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -825,7 +933,58 @@ const removeAvatar = async () => {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.avatar-actions-simple {
|
||||
/* Zona de drag & drop para avatar */
|
||||
.avatar-dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
padding: 2rem;
|
||||
border: 2px dashed rgba(var(--color-primary-500), 0.3);
|
||||
border-radius: 1rem;
|
||||
background: rgba(var(--color-primary-500), 0.05);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-dropzone:hover {
|
||||
border-color: rgba(var(--color-primary-500), 0.5);
|
||||
background: rgba(var(--color-primary-500), 0.1);
|
||||
}
|
||||
|
||||
.avatar-dropzone.dropzone-active {
|
||||
border-color: rgb(var(--color-primary-500));
|
||||
background: rgba(var(--color-primary-500), 0.15);
|
||||
border-style: solid;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.dropzone-icon {
|
||||
color: rgb(var(--color-primary-500));
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.avatar-dropzone:hover .dropzone-icon,
|
||||
.avatar-dropzone.dropzone-active .dropzone-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropzone-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-700);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropzone-hint {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-500);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.avatar-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
@@ -1054,6 +1213,33 @@ const removeAvatar = async () => {
|
||||
color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
.dark .avatar-dropzone {
|
||||
border-color: rgba(var(--color-primary-400), 0.3) !important;
|
||||
background: rgba(var(--color-primary-500), 0.08) !important;
|
||||
}
|
||||
|
||||
.dark .avatar-dropzone:hover {
|
||||
border-color: rgba(var(--color-primary-400), 0.5) !important;
|
||||
background: rgba(var(--color-primary-500), 0.12) !important;
|
||||
}
|
||||
|
||||
.dark .avatar-dropzone.dropzone-active {
|
||||
border-color: rgb(var(--color-primary-400)) !important;
|
||||
background: rgba(var(--color-primary-500), 0.18) !important;
|
||||
}
|
||||
|
||||
.dark .dropzone-icon {
|
||||
color: rgb(var(--color-primary-400)) !important;
|
||||
}
|
||||
|
||||
.dark .dropzone-text {
|
||||
color: var(--color-gray-200) !important;
|
||||
}
|
||||
|
||||
.dark .dropzone-hint {
|
||||
color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
.dark .form-actions {
|
||||
border-top-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user