All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 58s
261 lines
7.0 KiB
Vue
261 lines
7.0 KiB
Vue
<template>
|
|
<UApp>
|
|
<NuxtRouteAnnouncer />
|
|
<UNotifications />
|
|
|
|
<!-- Barra de título para Window Controls Overlay (PWA) -->
|
|
<WindowTitleBar />
|
|
|
|
<!-- Fondo animado -->
|
|
<AnimatedBackground />
|
|
|
|
<!-- Contenido principal -->
|
|
<div class="main-content">
|
|
<UContainer class="py-8">
|
|
<div v-if="isAuthenticated" class="space-y-6">
|
|
<!-- Header principal con info del usuario -->
|
|
<UserHeader @edit-profile="activeTab = 'perfil'" />
|
|
|
|
<!-- Sistema de Tabs -->
|
|
<div class="tabs-container">
|
|
<UTabs
|
|
v-model="activeTab"
|
|
:items="tabItems"
|
|
variant="pill"
|
|
color="primary"
|
|
:content="false"
|
|
:ui="{
|
|
list: 'tabs-list',
|
|
trigger: 'tabs-trigger',
|
|
indicator: 'tabs-indicator'
|
|
}"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Contenido según tab activo -->
|
|
<KeepAlive>
|
|
<ContactsList v-if="activeTab === 'contactos'" />
|
|
<AuthApplicationsList v-else-if="activeTab === 'aplicaciones'" />
|
|
<UserProfileForm
|
|
v-else-if="activeTab === 'perfil'"
|
|
:shared-image-url="sharedImageUrl"
|
|
@close="activeTab = 'aplicaciones'"
|
|
/>
|
|
</KeepAlive>
|
|
|
|
<!-- Acciones rápidas en footer transparente -->
|
|
<div class="quick-actions">
|
|
<AuthSessionStatusButton />
|
|
<AuthProfileButton />
|
|
<AuthLogoutButton />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mensaje si no está autenticado -->
|
|
<div v-else class="auth-message">
|
|
<UCard class="text-center">
|
|
<div class="py-12">
|
|
<UIcon name="i-heroicons-shield-exclamation" class="w-20 h-20 mx-auto mb-6 text-gray-400" />
|
|
<h2 class="text-3xl font-bold mb-3">No autenticado</h2>
|
|
<p class="text-gray-600 dark:text-gray-400 text-lg">
|
|
Authentik Proxy Outpost debería redirigirte automáticamente.
|
|
</p>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</UContainer>
|
|
</div>
|
|
</UApp>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { TabsItem } from '@nuxt/ui'
|
|
import ContactsList from '~/components/contacts/List.vue'
|
|
|
|
const { isAuthenticated } = useAuthentik()
|
|
const { isNight } = useTheme()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
// Tab activo (persistido en cookie)
|
|
const tabCookie = useCookie<string>('active-tab', {
|
|
maxAge: 60 * 60 * 24 * 7, // 1 semana
|
|
default: () => 'contactos'
|
|
})
|
|
const activeTab = ref(tabCookie.value)
|
|
|
|
// Sincronizar con cookie
|
|
watch(activeTab, (newTab) => {
|
|
tabCookie.value = newTab
|
|
})
|
|
|
|
// Definir tabs
|
|
const tabItems: TabsItem[] = [
|
|
{
|
|
label: 'Contactos',
|
|
value: 'contactos',
|
|
icon: 'i-heroicons-users'
|
|
},
|
|
{
|
|
label: 'Aplicaciones',
|
|
value: 'aplicaciones',
|
|
icon: 'i-heroicons-squares-2x2'
|
|
},
|
|
{
|
|
label: 'Perfil',
|
|
value: 'perfil',
|
|
icon: 'i-heroicons-user-circle'
|
|
}
|
|
]
|
|
|
|
// URL de imagen compartida (si existe)
|
|
const sharedImageUrl = ref<string | null>(null)
|
|
|
|
// Detectar si se compartió una imagen
|
|
onMounted(() => {
|
|
const sharedToken = route.query.shared as string
|
|
const sharedFile = route.query.file as string
|
|
|
|
if (sharedToken && sharedFile) {
|
|
// Construir URL del archivo temporal
|
|
sharedImageUrl.value = `/temp-shared/${sharedFile}`
|
|
|
|
// Abrir automáticamente el formulario de perfil
|
|
activeTab.value = 'perfil'
|
|
|
|
// Limpiar query params de la URL sin recargar
|
|
router.replace({ query: {} })
|
|
}
|
|
})
|
|
|
|
// Configurar meta tags para PWA
|
|
useHead({
|
|
title: 'NucleoV3',
|
|
htmlAttrs: {
|
|
lang: 'es'
|
|
},
|
|
link: [
|
|
{ rel: 'manifest', href: '/manifest.webmanifest' },
|
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico', sizes: '48x48' },
|
|
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' },
|
|
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }
|
|
],
|
|
meta: [
|
|
{ name: 'theme-color', content: '#00DC82' },
|
|
{ name: 'mobile-web-app-capable', content: 'yes' },
|
|
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
|
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
|
|
{ name: 'language', content: 'es' },
|
|
{ property: 'og:locale', content: 'es_ES' }
|
|
]
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.main-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
min-height: 100vh;
|
|
/* Ajustar padding cuando Window Controls Overlay está activo */
|
|
padding-top: max(2rem, env(titlebar-area-height, 0px));
|
|
padding-top: max(2rem, calc(env(titlebar-area-height, 0px) + 1rem));
|
|
}
|
|
|
|
/* Contenedor de tabs con glassmorphism */
|
|
.tabs-container {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
border-radius: 1rem;
|
|
padding: 0.375rem;
|
|
box-shadow:
|
|
0 4px 16px 0 rgba(31, 38, 135, 0.1),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
}
|
|
|
|
.quick-actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
padding: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
border-radius: 1.5rem;
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.auth-message {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 60vh;
|
|
}
|
|
|
|
.auth-message :deep(.card) {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.main-content {
|
|
padding-top: 1rem;
|
|
}
|
|
|
|
.quick-actions {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
/* Estilos de modo oscuro (sin scoped para que .dark funcione correctamente) */
|
|
.dark .tabs-container {
|
|
background: rgba(0, 0, 0, 0.2) !important;
|
|
box-shadow:
|
|
0 4px 16px 0 rgba(0, 0, 0, 0.4),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
|
}
|
|
|
|
/* Personalizar tabs para estilo glassmorphism */
|
|
.tabs-container .tabs-list {
|
|
background: transparent !important;
|
|
}
|
|
|
|
.tabs-container .tabs-indicator {
|
|
background: rgba(var(--color-primary-500), 0.2) !important;
|
|
backdrop-filter: blur(10px) !important;
|
|
}
|
|
|
|
.dark .tabs-container .tabs-indicator {
|
|
background: rgba(var(--color-primary-500), 0.3) !important;
|
|
}
|
|
|
|
.dark .quick-actions {
|
|
background: rgba(0, 0, 0, 0.15) !important;
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(0, 0, 0, 0.5),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
|
}
|
|
|
|
.dark .auth-message :deep(.card) {
|
|
background: rgba(0, 0, 0, 0.15) !important;
|
|
border: 1px solid rgba(255, 255, 255, 0.08) !important;
|
|
box-shadow:
|
|
0 8px 32px 0 rgba(0, 0, 0, 0.5),
|
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
|
|
}
|
|
</style>
|