All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 54s
- Implementar sistema de tema día/noche con persistencia en localStorage - Crear componente AnimatedBackground con paisajes SVG animados - Generar todos los assets SVG desde cero (sol, luna, estrellas, nubes, montañas) - Añadir animaciones suaves para nubes, estrellas y elementos del paisaje - Rediseñar UserHeader como componente principal clickeable - Integrar modal de edición de perfil en el header - Reorganizar layout principal mostrando solo aplicaciones - Mejorar diseño de ApplicationsList con glassmorphism - Implementar efectos hover y transiciones elegantes - Diseño responsive mobile-first - Diferencias visuales notorias entre modo día y noche
376 lines
8.4 KiB
Vue
376 lines
8.4 KiB
Vue
<template>
|
|
<div class="user-header">
|
|
<!-- Header principal clickable -->
|
|
<div class="header-content" @click="openEditProfile">
|
|
<!-- Avatar -->
|
|
<div class="avatar-section">
|
|
<UAvatar
|
|
v-if="user"
|
|
:src="user.avatar"
|
|
:alt="user.name || user.username"
|
|
size="xl"
|
|
class="avatar-glow"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Info del usuario -->
|
|
<div class="user-info">
|
|
<h1 class="user-name">{{ user?.name || user?.username }}</h1>
|
|
<p class="user-email">{{ user?.email }}</p>
|
|
<div class="user-badges">
|
|
<UBadge
|
|
v-for="group in user?.groups.slice(0, 3)"
|
|
:key="group"
|
|
size="sm"
|
|
color="primary"
|
|
variant="soft"
|
|
>
|
|
{{ group }}
|
|
</UBadge>
|
|
<UBadge
|
|
v-if="user && user.groups.length > 3"
|
|
size="sm"
|
|
color="neutral"
|
|
variant="soft"
|
|
>
|
|
+{{ user.groups.length - 3 }}
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ícono de edición -->
|
|
<div class="edit-hint">
|
|
<UIcon name="i-heroicons-pencil-square" class="w-5 h-5" />
|
|
<span class="edit-text">Editar perfil</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Botón de tema -->
|
|
<button class="theme-toggle" @click.stop="toggleTheme" :title="isNight ? 'Cambiar a modo día' : 'Cambiar a modo noche'">
|
|
<transition name="theme-icon" mode="out-in">
|
|
<UIcon v-if="isNight" key="moon" name="i-heroicons-moon" class="w-6 h-6" />
|
|
<UIcon v-else key="sun" name="i-heroicons-sun" class="w-6 h-6" />
|
|
</transition>
|
|
</button>
|
|
|
|
<!-- Modal de edición de perfil -->
|
|
<UModal v-model="isEditModalOpen">
|
|
<div class="modal-content">
|
|
<h3 class="text-xl font-semibold mb-4">Editar Perfil</h3>
|
|
|
|
<form @submit.prevent="handleSubmit" class="space-y-4">
|
|
<UFormGroup label="Nombre de usuario" help="No se puede modificar">
|
|
<UInput
|
|
:model-value="user?.username"
|
|
disabled
|
|
icon="i-heroicons-user"
|
|
/>
|
|
</UFormGroup>
|
|
|
|
<UFormGroup label="Nombre completo" required>
|
|
<UInput
|
|
v-model="formData.name"
|
|
placeholder="Tu nombre completo"
|
|
icon="i-heroicons-user-circle"
|
|
:disabled="isSubmitting"
|
|
/>
|
|
</UFormGroup>
|
|
|
|
<UFormGroup label="Email" required>
|
|
<UInput
|
|
v-model="formData.email"
|
|
type="email"
|
|
placeholder="tu@email.com"
|
|
icon="i-heroicons-envelope"
|
|
:disabled="isSubmitting"
|
|
/>
|
|
</UFormGroup>
|
|
|
|
<div class="flex justify-end gap-3 mt-6">
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
@click="isEditModalOpen = false"
|
|
:disabled="isSubmitting"
|
|
>
|
|
Cancelar
|
|
</UButton>
|
|
<UButton
|
|
type="submit"
|
|
color="primary"
|
|
:loading="isSubmitting"
|
|
>
|
|
Guardar cambios
|
|
</UButton>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</UModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { user } = useAuthentik()
|
|
const { isNight, toggleTheme } = useTheme()
|
|
const toast = useToast()
|
|
|
|
// Estado del modal
|
|
const isEditModalOpen = ref(false)
|
|
const isSubmitting = ref(false)
|
|
|
|
// Datos del formulario
|
|
const formData = ref({
|
|
name: user.value?.name || '',
|
|
email: user.value?.email || ''
|
|
})
|
|
|
|
// Abrir modal de edición
|
|
const openEditProfile = () => {
|
|
if (user.value) {
|
|
formData.value = {
|
|
name: user.value.name || '',
|
|
email: user.value.email || ''
|
|
}
|
|
isEditModalOpen.value = true
|
|
}
|
|
}
|
|
|
|
// Enviar formulario
|
|
const handleSubmit = async () => {
|
|
if (!formData.value.name || !formData.value.email) {
|
|
toast.add({
|
|
title: 'Error',
|
|
description: 'Por favor completa todos los campos',
|
|
color: 'error',
|
|
icon: 'i-heroicons-exclamation-triangle'
|
|
})
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
|
|
try {
|
|
await $fetch('/api/authentik/user', {
|
|
method: 'PATCH',
|
|
body: {
|
|
name: formData.value.name,
|
|
email: formData.value.email
|
|
}
|
|
})
|
|
|
|
toast.add({
|
|
title: 'Perfil actualizado',
|
|
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
|
|
color: 'success',
|
|
icon: 'i-heroicons-check-circle',
|
|
actions: [{
|
|
label: 'Recargar',
|
|
onClick: () => window.location.reload()
|
|
}]
|
|
})
|
|
|
|
isEditModalOpen.value = false
|
|
} catch (error) {
|
|
console.error('Error updating profile:', error)
|
|
toast.add({
|
|
title: 'Error',
|
|
description: 'No se pudo actualizar el perfil',
|
|
color: 'error',
|
|
icon: 'i-heroicons-x-circle'
|
|
})
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-header {
|
|
position: relative;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
padding: 1.5rem 2rem;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 1rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border: 2px solid transparent;
|
|
}
|
|
|
|
.header-content:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
border-color: rgba(var(--color-primary-500), 0.3);
|
|
}
|
|
|
|
:global(.dark) .header-content {
|
|
background: rgba(30, 30, 40, 0.9);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
:global(.dark) .header-content:hover {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.avatar-section {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.avatar-glow {
|
|
box-shadow: 0 0 20px rgba(var(--color-primary-500), 0.4);
|
|
transition: box-shadow 0.3s ease;
|
|
}
|
|
|
|
.header-content:hover .avatar-glow {
|
|
box-shadow: 0 0 30px rgba(var(--color-primary-500), 0.6);
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin: 0;
|
|
color: var(--color-gray-900);
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
:global(.dark) .user-name {
|
|
color: var(--color-gray-100);
|
|
}
|
|
|
|
.user-email {
|
|
font-size: 0.875rem;
|
|
color: var(--color-gray-600);
|
|
margin: 0.25rem 0 0.75rem 0;
|
|
}
|
|
|
|
:global(.dark) .user-email {
|
|
color: var(--color-gray-400);
|
|
}
|
|
|
|
.user-badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.modal-content {
|
|
padding: 1.5rem;
|
|
max-width: 28rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.edit-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.5rem;
|
|
background: rgba(var(--color-primary-500), 0.1);
|
|
color: rgb(var(--color-primary-500));
|
|
opacity: 0;
|
|
transform: translateX(-10px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.header-content:hover .edit-hint {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.edit-text {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.theme-toggle {
|
|
position: absolute;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
backdrop-filter: blur(10px);
|
|
border: 2px solid rgba(var(--color-primary-500), 0.2);
|
|
color: rgb(var(--color-primary-500));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
z-index: 10;
|
|
}
|
|
|
|
:global(.dark) .theme-toggle {
|
|
background: rgba(30, 30, 40, 0.9);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.theme-toggle:hover {
|
|
transform: scale(1.1) rotate(20deg);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.theme-icon-enter-active,
|
|
.theme-icon-leave-active {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.theme-icon-enter-from {
|
|
opacity: 0;
|
|
transform: rotate(-180deg) scale(0);
|
|
}
|
|
|
|
.theme-icon-leave-to {
|
|
opacity: 0;
|
|
transform: rotate(180deg) scale(0);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.header-content {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
padding: 1.5rem 1rem;
|
|
}
|
|
|
|
.edit-hint {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.edit-text {
|
|
display: none;
|
|
}
|
|
|
|
.theme-toggle {
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.user-badges {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|