Files
plantillas-proyectos/frontend/src/lib/components/sidebar/nav-user.svelte

250 lines
8.2 KiB
Svelte

<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
import BadgeCheckIcon from '@lucide/svelte/icons/badge-check';
import BellIcon from '@lucide/svelte/icons/bell';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import CreditCardIcon from '@lucide/svelte/icons/credit-card';
import LogOutIcon from '@lucide/svelte/icons/log-out';
import LanguagesIcon from '@lucide/svelte/icons/languages';
import MoonIcon from '@lucide/svelte/icons/moon';
import SunIcon from '@lucide/svelte/icons/sun';
import ArrowLeftRightIcon from '@lucide/svelte/icons/arrow-left-right';
import BuildingIcon from '@lucide/svelte/icons/building';
import CheckIcon from '@lucide/svelte/icons/check';
import { logout } from '$lib/auth';
import { cookieName } from '$lib/paraglide/runtime';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { getBackendAssetUrl } from '$lib/utils';
import AppVersion from '$lib/components/app-version.svelte';
let { user, tenants = [] }: {
user: { name: string; email: string; avatar: string };
tenants: { id: number; name: string; slug: string }[];
} = $props();
const sidebar = useSidebar();
// Tenant activo (viene en el JWT como atributo tenant_slug)
let currentTenantSlug = $derived((page.data.user as any)?.tenant_slug ?? '');
// Estado de cambio de tenant
let switchingTenant = $state(false);
// URL completa del avatar
let avatarUrl = $derived(getBackendAssetUrl(user.avatar) || '/avatars/default.jpg');
// Iniciales del usuario (2 primeras letras)
let initials = $derived(user.name.slice(0, 2).toUpperCase());
// Estado reactivo del idioma actual
let currentLocale = $derived(page.data.locale || 'en');
// Estado reactivo del tema actual
let isDarkMode = $state(false);
// Inicializar el estado del tema al montar el componente
$effect(() => {
if (browser) {
// Cargar la preferencia guardada o usar el valor actual del HTML
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
isDarkMode = savedTheme === 'dark';
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} else {
// Si no hay preferencia guardada, usar el valor actual
isDarkMode = document.documentElement.classList.contains('dark');
// Guardar el estado actual
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}
}
});
async function handleLogout() {
await logout();
}
async function switchTenant(slug: string) {
if (slug === currentTenantSlug || switchingTenant) return;
switchingTenant = true;
try {
const res = await fetch('/api-sveltekit/auth/switch-tenant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_slug: slug })
});
if (res.ok) {
// Recargar el dashboard con los nuevos tokens
window.location.href = '/dashboard';
} else {
console.error('Error al cambiar de organización');
}
} finally {
switchingTenant = false;
}
}
function navigateToAccount() {
goto('/dashboard/account');
}
function toggleLanguage() {
if (!browser) return;
// Leer la cookie actual para obtener el idioma real
const cookies = document.cookie.split(';').map((c) => c.trim());
const localeCookie = cookies.find((c) => c.startsWith(`${cookieName}=`));
const current = localeCookie ? localeCookie.split('=')[1] : 'en';
// Alternar el idioma
const newLocale = current === 'en' ? 'es' : 'en';
// Establecer la cookie del idioma
document.cookie = `${cookieName}=${newLocale}; path=/; max-age=34560000; SameSite=Lax`;
// Recargar la página para que el servidor procese el nuevo idioma
window.location.reload();
}
function toggleTheme() {
if (!browser) return;
const html = document.documentElement;
const newTheme = html.classList.contains('dark') ? 'light' : 'dark';
if (newTheme === 'dark') {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
// Guardar la preferencia en localStorage
localStorage.setItem('theme', newTheme);
isDarkMode = newTheme === 'dark';
}
</script>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
{...props}
>
<Avatar.Root class="size-8 rounded-lg">
<Avatar.Image src={avatarUrl} alt={user.name} />
<Avatar.Fallback class="rounded-lg">{initials}</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{user.name}</span>
<span class="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDownIcon class="ml-auto size-4" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
side={sidebar.isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
<DropdownMenu.Label class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar.Root class="size-8 rounded-lg">
<Avatar.Image src={avatarUrl} alt={user.name} />
<Avatar.Fallback class="rounded-lg">{initials}</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">{user.name}</span>
<span class="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item onclick={navigateToAccount}>
<BadgeCheckIcon />
Account
</DropdownMenu.Item>
<DropdownMenu.Item>
<CreditCardIcon />
Billing
</DropdownMenu.Item>
<DropdownMenu.Item>
<BellIcon />
Notifications
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={toggleLanguage}>
<LanguagesIcon />
Language: {currentLocale.toUpperCase()}
</DropdownMenu.Item>
<DropdownMenu.Item onclick={toggleTheme}>
{#if isDarkMode}
<SunIcon />
Light Mode
{:else}
<MoonIcon />
Dark Mode
{/if}
</DropdownMenu.Item>
<DropdownMenu.Separator />
{#if tenants.length > 1}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger class="gap-2">
{#if switchingTenant}
<svg class="animate-spin size-4 shrink-0" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
{:else}
<ArrowLeftRightIcon class="size-4" />
{/if}
Cambiar organización
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="min-w-44">
<DropdownMenu.Label class="text-xs text-muted-foreground pb-1">
Mis organizaciones
</DropdownMenu.Label>
{#each tenants as tenant (tenant.id)}
<DropdownMenu.Item
class="gap-2 cursor-pointer"
disabled={switchingTenant}
onclick={() => switchTenant(tenant.slug)}
>
<BuildingIcon class="size-4 shrink-0 text-muted-foreground" />
<span class="flex-1 truncate">{tenant.name}</span>
{#if tenant.slug === currentTenantSlug}
<CheckIcon class="size-3.5 shrink-0 text-blue-600" />
{/if}
</DropdownMenu.Item>
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleLogout}>
<LogOutIcon />
Log out
</DropdownMenu.Item>
<DropdownMenu.Separator />
<div class="px-2 py-2">
<AppVersion />
</div>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>