feat: enhance layout and component structure for dashboard, including responsive sidebar and header actions, and update footer styling
This commit is contained in:
@@ -4,8 +4,8 @@
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="bg-base-200 text-base-content border-base-300 border-t">
|
<footer class="bg-base-200 text-base-content border-base-300 mt-16 border-t">
|
||||||
<div class="container mx-auto px-4 py-10">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 gap-8 text-center md:grid-cols-3 md:text-left">
|
<div class="grid grid-cols-1 gap-8 text-center md:grid-cols-3 md:text-left">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-primary mb-4 text-lg font-bold">SGSE</h3>
|
<h3 class="text-primary mb-4 text-lg font-bold">SGSE</h3>
|
||||||
|
|||||||
@@ -1,20 +1,43 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import logo from '$lib/assets/logo_governo_PE.png';
|
import logo from '$lib/assets/logo_governo_PE.png';
|
||||||
import { page } from '$app/state';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
left?: Snippet;
|
||||||
|
right?: Snippet;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { left, right }: HeaderProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="sticky top-0 z-50 w-full border-b backdrop-blur-md bg-base-100/90 border-base-200 shadow-sm transition-all duration-300">
|
<header
|
||||||
<div class="container mx-auto px-4 h-16 flex items-center justify-between">
|
class="bg-base-200 border-base-100 sticky top-0 z-50 w-full border-b shadow-sm backdrop-blur-md transition-all duration-300"
|
||||||
<a href="/" class="flex items-center gap-3 group transition-transform hover:scale-[1.02]">
|
>
|
||||||
|
<div class=" flex h-16 w-full items-center justify-between px-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{#if left}
|
||||||
|
{@render left()}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={resolve('/')}
|
||||||
|
class="group flex items-center gap-3 transition-transform hover:scale-[1.02]"
|
||||||
|
>
|
||||||
<img src={logo} alt="Logo Governo PE" class="h-10 w-auto object-contain drop-shadow-sm" />
|
<img src={logo} alt="Logo Governo PE" class="h-10 w-auto object-contain drop-shadow-sm" />
|
||||||
<div class="flex flex-col hidden sm:flex">
|
<div class="hidden flex-col sm:flex">
|
||||||
<span class="text-xs font-bold text-primary tracking-wider uppercase">Governo de</span>
|
<span class="text-primary text-xs font-bold tracking-wider uppercase">Governo de</span>
|
||||||
<span class="text-lg font-extrabold -mt-1 tracking-tight text-base-content leading-none">Pernambuco</span>
|
<span class="text-base-content -mt-1 text-lg leading-none font-extrabold tracking-tight"
|
||||||
|
>Pernambuco</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Links can be added here based on auth state or specific requirements -->
|
{#if right}
|
||||||
</nav>
|
{@render right()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -3,39 +3,19 @@
|
|||||||
import { useQuery } from 'convex-svelte';
|
import { useQuery } from 'convex-svelte';
|
||||||
import type { FunctionReference } from 'convex/server';
|
import type { FunctionReference } from 'convex/server';
|
||||||
import {
|
import {
|
||||||
Home,
|
ChevronDown,
|
||||||
User,
|
|
||||||
UserPlus,
|
|
||||||
XCircle,
|
|
||||||
Users,
|
|
||||||
DollarSign,
|
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
FileText,
|
FileText,
|
||||||
ShoppingCart,
|
|
||||||
Scale,
|
|
||||||
Megaphone,
|
|
||||||
Trophy,
|
|
||||||
Briefcase,
|
|
||||||
UserCog,
|
|
||||||
Monitor,
|
|
||||||
ChevronDown,
|
|
||||||
GitMerge,
|
GitMerge,
|
||||||
|
Home,
|
||||||
Settings,
|
Settings,
|
||||||
Check,
|
Tag,
|
||||||
LogIn,
|
Users,
|
||||||
Menu,
|
Briefcase,
|
||||||
Plus,
|
UserPlus
|
||||||
Tag
|
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import logo from '$lib/assets/logo_governo_PE.png';
|
|
||||||
import { authClient } from '$lib/auth';
|
|
||||||
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
|
||||||
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
|
||||||
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
|
||||||
|
|
||||||
interface MenuItemPermission {
|
interface MenuItemPermission {
|
||||||
recurso: string;
|
recurso: string;
|
||||||
@@ -198,12 +178,13 @@
|
|||||||
|
|
||||||
type IconType = typeof Home;
|
type IconType = typeof Home;
|
||||||
|
|
||||||
const { children }: { children: Snippet } = $props();
|
type SidebarProps = {
|
||||||
|
onNavigate?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { onNavigate }: SidebarProps = $props();
|
||||||
|
|
||||||
let currentPath = $derived(page.url.pathname);
|
let currentPath = $derived(page.url.pathname);
|
||||||
let showAboutModal = $state(false);
|
|
||||||
|
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>, {});
|
|
||||||
const permissionsQuery = useQuery(api.menu.getUserPermissions as FunctionReference<'query'>, {});
|
const permissionsQuery = useQuery(api.menu.getUserPermissions as FunctionReference<'query'>, {});
|
||||||
|
|
||||||
// Filtrar menu baseado nas permissões do usuário
|
// Filtrar menu baseado nas permissões do usuário
|
||||||
@@ -267,27 +248,14 @@
|
|||||||
|
|
||||||
const iconMap: Record<string, IconType> = {
|
const iconMap: Record<string, IconType> = {
|
||||||
Home,
|
Home,
|
||||||
User,
|
|
||||||
UserPlus,
|
UserPlus,
|
||||||
XCircle,
|
|
||||||
Users,
|
Users,
|
||||||
DollarSign,
|
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
FileText,
|
FileText,
|
||||||
ShoppingCart,
|
|
||||||
Scale,
|
|
||||||
Megaphone,
|
|
||||||
Trophy,
|
|
||||||
Briefcase,
|
Briefcase,
|
||||||
UserCog,
|
|
||||||
Monitor,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
GitMerge,
|
GitMerge,
|
||||||
Settings,
|
Settings,
|
||||||
Check,
|
|
||||||
LogIn,
|
|
||||||
Menu,
|
|
||||||
Plus,
|
|
||||||
Tag
|
Tag
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -330,202 +298,11 @@
|
|||||||
function getSolicitarClasses(active: boolean) {
|
function getSolicitarClasses(active: boolean) {
|
||||||
return getMenuClasses(active);
|
return getMenuClasses(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Função para obter a URL do avatar/foto do usuário
|
|
||||||
let avatarUrlDoUsuario = $derived.by(() => {
|
|
||||||
if (!currentUser.data) return null;
|
|
||||||
|
|
||||||
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
|
||||||
if (currentUser.data.fotoPerfilUrl) {
|
|
||||||
return currentUser.data.fotoPerfilUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUser.data.avatar) {
|
|
||||||
return currentUser.data.avatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: retornar null para usar o ícone User do Lucide
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function goToLogin(redirectTo?: string) {
|
|
||||||
const target = redirectTo || currentPath || '/';
|
|
||||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(target)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAboutModal() {
|
|
||||||
showAboutModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeAboutModal() {
|
|
||||||
showAboutModal = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogout() {
|
|
||||||
const result = await authClient.signOut();
|
|
||||||
if (result.error) {
|
|
||||||
console.error('Sign out error:', result.error);
|
|
||||||
}
|
|
||||||
// Resetar tema para padrão ao fazer logout
|
|
||||||
const { aplicarTemaPadrao } = await import('$lib/utils/temas');
|
|
||||||
aplicarTemaPadrao();
|
|
||||||
goto(resolve('/'));
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Header Fixo acima de tudo -->
|
<nav
|
||||||
<!-- Header Fixo Minimalista & Premium -->
|
class="menu text-base-content bg-base-200 border-base-100 h-[calc(100vh-64px)] w-full flex-col gap-2 overflow-y-auto p-4"
|
||||||
<div
|
|
||||||
class="navbar border-primary/10 from-primary/10 via-primary/5 to-primary/10 fixed top-0 right-0 left-0 z-50 h-16 border-b bg-linear-to-r px-4 shadow-sm"
|
|
||||||
>
|
>
|
||||||
<div class="flex-none lg:hidden">
|
|
||||||
<label for="my-drawer-3" class="btn btn-square btn-ghost btn-sm" aria-label="Abrir menu">
|
|
||||||
<Menu class="h-5 w-5" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 items-center gap-4 lg:gap-6">
|
|
||||||
<!-- Logo Visível e Ajustado -->
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="relative h-10 overflow-hidden rounded-lg shadow-sm lg:h-14">
|
|
||||||
<img src={logo} alt="Logo do Governo de PE" class="h-full w-full object-contain p-1" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col justify-center">
|
|
||||||
<h1 class="text-base-content text-xl leading-none font-bold tracking-tight lg:text-2xl">
|
|
||||||
SGSE
|
|
||||||
</h1>
|
|
||||||
<span class="text-base-content/60 font-medium tracking-wider uppercase"
|
|
||||||
>Secretaria de Esportes</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-auto flex flex-none items-center gap-3 lg:gap-5">
|
|
||||||
{#if currentUser.data}
|
|
||||||
<!-- Nome e Perfil -->
|
|
||||||
<div class="hidden flex-col items-end lg:flex">
|
|
||||||
<span class="text-base-content text-sm leading-tight font-semibold"
|
|
||||||
>{currentUser.data.nome}</span
|
|
||||||
>
|
|
||||||
<span class="text-base-content/60 text-xs leading-tight">{currentUser.data.role?.nome}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<!-- Botão de Perfil com Avatar -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
tabindex="0"
|
|
||||||
class="btn avatar ring-base-200 hover:ring-primary/50 h-12 w-12 p-0 ring-2 ring-offset-2 transition-all"
|
|
||||||
aria-label="Menu do usuário"
|
|
||||||
>
|
|
||||||
<div class="h-full w-full overflow-hidden rounded-full">
|
|
||||||
{#if avatarUrlDoUsuario}
|
|
||||||
<img
|
|
||||||
src={avatarUrlDoUsuario}
|
|
||||||
alt={currentUser.data?.nome || 'Usuário'}
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="bg-primary/10 text-primary flex h-full w-full items-center justify-center"
|
|
||||||
>
|
|
||||||
<User class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box ring-base-content/5 z-1 mt-3 w-56 p-2 shadow-xl ring-1"
|
|
||||||
>
|
|
||||||
<li class="menu-title border-base-200 mb-2 border-b px-4 py-2">
|
|
||||||
<span class="text-base-content font-bold">{currentUser.data?.nome}</span>
|
|
||||||
<span class="text-base-content/60 text-xs font-normal">{currentUser.data.email}</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/perfil')} class="active:bg-primary/10 active:text-primary"
|
|
||||||
><UserCog class="mr-2 h-4 w-4" /> Meu Perfil</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href={resolve('/alterar-senha')} class="active:bg-primary/10 active:text-primary"
|
|
||||||
><Settings class="mr-2 h-4 w-4" /> Alterar Senha</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<div class="divider my-1"></div>
|
|
||||||
<li>
|
|
||||||
<button type="button" onclick={handleLogout} class="text-error hover:bg-error/10"
|
|
||||||
><LogIn class="mr-2 h-4 w-4 rotate-180" /> Sair</button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sino de notificações -->
|
|
||||||
<div class="relative">
|
|
||||||
<NotificationBell />
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm rounded-full px-6"
|
|
||||||
onclick={() => goToLogin()}
|
|
||||||
>
|
|
||||||
Entrar
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer lg:drawer-open" style="margin-top: 64px;">
|
|
||||||
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
|
|
||||||
<div
|
|
||||||
class="drawer-content flex flex-col transition-all duration-300 lg:ml-72"
|
|
||||||
style="min-height: calc(100vh - 64px);"
|
|
||||||
>
|
|
||||||
<!-- Page content -->
|
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer Minimalista -->
|
|
||||||
<footer
|
|
||||||
class="footer footer-center text-base-content/60 border-base-200 bg-base-100/50 border-t p-4 text-xs backdrop-blur-sm"
|
|
||||||
>
|
|
||||||
<div class="flex flex-wrap justify-center gap-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="hover:text-primary font-medium transition-colors"
|
|
||||||
onclick={() => openAboutModal()}>Sobre</button
|
|
||||||
>
|
|
||||||
<a href={resolve('/')} class="hover:text-primary font-medium transition-colors">Contato</a>
|
|
||||||
<a href={resolve('/abrir-chamado')} class="hover:text-primary font-medium transition-colors"
|
|
||||||
>Suporte</a
|
|
||||||
>
|
|
||||||
<span class="text-base-content/30">•</span>
|
|
||||||
<a
|
|
||||||
href={resolve('/abrir-chamado')}
|
|
||||||
class="link link-hover hover:text-primary transition-colors">Suporte</a
|
|
||||||
>
|
|
||||||
<span class="text-base-content/30">•</span>
|
|
||||||
<a
|
|
||||||
href={resolve('/privacidade')}
|
|
||||||
class="link link-hover hover:text-primary transition-colors">Privacidade</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 opacity-70">
|
|
||||||
<p>© {new Date().getFullYear()} Governo de Pernambuco - Secretaria de Esportes</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<div class="drawer-side fixed z-40" style="margin-top: 64px;">
|
|
||||||
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"></label>
|
|
||||||
<div
|
|
||||||
class="menu text-base-content border-base-300 from-primary/10 via-primary/5 to-primary/10 h-[calc(100vh-64px)] w-72 flex-col gap-2 overflow-y-auto border-r-2 bg-linear-to-b p-4 backdrop-blur-sm"
|
|
||||||
>
|
|
||||||
<!-- Sidebar menu items -->
|
|
||||||
<!-- Sidebar menu items -->
|
|
||||||
{#snippet menuItem(item: MenuItem)}
|
{#snippet menuItem(item: MenuItem)}
|
||||||
{@const Icon = getIconComponent(item.icon)}
|
{@const Icon = getIconComponent(item.icon)}
|
||||||
{@const isActive = isRouteActive(item.link, {
|
{@const isActive = isRouteActive(item.link, {
|
||||||
@@ -560,7 +337,11 @@
|
|||||||
exact: sub.exact
|
exact: sub.exact
|
||||||
})}
|
})}
|
||||||
<li>
|
<li>
|
||||||
<a href={resolve(sub.link as any)} class={getMenuClasses(isSubActive, true)}>
|
<a
|
||||||
|
href={resolve(sub.link as any)}
|
||||||
|
class={getMenuClasses(isSubActive, true)}
|
||||||
|
onclick={() => onNavigate?.()}
|
||||||
|
>
|
||||||
<span>{sub.label}</span>
|
<span>{sub.label}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -573,6 +354,7 @@
|
|||||||
href={resolve(item.link as any)}
|
href={resolve(item.link as any)}
|
||||||
aria-current={isActive ? 'page' : undefined}
|
aria-current={isActive ? 'page' : undefined}
|
||||||
class={getMenuClasses(isActive)}
|
class={getMenuClasses(isActive)}
|
||||||
|
onclick={() => onNavigate?.()}
|
||||||
>
|
>
|
||||||
<Icon class="h-5 w-5" strokeWidth={2} />
|
<Icon class="h-5 w-5" strokeWidth={2} />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
@@ -602,147 +384,16 @@
|
|||||||
<a
|
<a
|
||||||
href={resolve('/abrir-chamado')}
|
href={resolve('/abrir-chamado')}
|
||||||
class={getSolicitarClasses(currentPath === '/abrir-chamado')}
|
class={getSolicitarClasses(currentPath === '/abrir-chamado')}
|
||||||
|
onclick={() => onNavigate?.()}
|
||||||
>
|
>
|
||||||
<UserPlus class="h-5 w-5" strokeWidth={2} />
|
<UserPlus class="h-5 w-5" strokeWidth={2} />
|
||||||
<span>Abrir Chamado</span>
|
<span>Abrir Chamado</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Sobre -->
|
|
||||||
{#if showAboutModal}
|
|
||||||
<dialog class="modal modal-open">
|
|
||||||
<div
|
|
||||||
class="modal-box from-base-100 to-base-200 relative max-w-md overflow-hidden bg-linear-to-br shadow-xl"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 absolute top-2 right-2 z-10"
|
|
||||||
onclick={closeAboutModal}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="space-y-5 px-6 py-6 text-center">
|
|
||||||
<!-- Logo e Título -->
|
|
||||||
<div class="flex flex-col items-center gap-3">
|
|
||||||
<div class="avatar">
|
|
||||||
<div class="ring-primary/20 w-20 rounded-xl bg-white p-3 shadow-lg ring-2">
|
|
||||||
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-primary text-2xl font-bold tracking-tight">SGSE</h3>
|
|
||||||
<p class="text-base-content/70 text-sm font-medium">
|
|
||||||
Sistema de Gerenciamento de Secretaria
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="divider my-1"></div>
|
|
||||||
|
|
||||||
<!-- Informações de Versão -->
|
|
||||||
<div
|
|
||||||
class="from-primary/10 to-primary/5 border-primary/10 space-y-2 rounded-xl border bg-linear-to-br p-4 shadow-sm"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center gap-2">
|
|
||||||
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
|
|
||||||
<p class="text-base-content/60 text-xs font-medium tracking-wide uppercase">Versão</p>
|
|
||||||
</div>
|
|
||||||
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
|
|
||||||
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
|
|
||||||
<Plus class="h-3.5 w-3.5" strokeWidth={2} />
|
|
||||||
Em Desenvolvimento
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desenvolvido por -->
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<p class="text-base-content/50 text-xs font-medium tracking-wide uppercase">
|
|
||||||
Desenvolvido por
|
|
||||||
</p>
|
|
||||||
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="divider my-1"></div>
|
|
||||||
|
|
||||||
<!-- Informações Adicionais -->
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<div
|
|
||||||
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
|
||||||
>
|
|
||||||
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Governo</p>
|
|
||||||
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
|
|
||||||
>
|
|
||||||
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Ano</p>
|
|
||||||
<p class="text-base-content/60 text-xs font-medium">2025</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botão OK -->
|
|
||||||
<div class="pt-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm mx-auto w-full max-w-xs shadow-md transition-all duration-200 hover:shadow-lg"
|
|
||||||
onclick={closeAboutModal}
|
|
||||||
>
|
|
||||||
<Check class="h-4 w-4" strokeWidth={2} />
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="modal-backdrop"
|
|
||||||
onclick={closeAboutModal}
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onkeydown={(e) => e.key === 'Escape' && closeAboutModal()}
|
|
||||||
></div>
|
|
||||||
</dialog>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Componentes de Chat (apenas se autenticado) -->
|
|
||||||
{#if currentUser.data}
|
|
||||||
<PresenceManager />
|
|
||||||
<ChatWidget />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Animação de pulso sutil para o anel do botão de perfil */
|
|
||||||
@keyframes pulse-ring-subtle {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.3;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animação de pulso para o badge de status online */
|
|
||||||
@keyframes pulse-dot {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove default details marker */
|
/* Remove default details marker */
|
||||||
details > summary {
|
details > summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import { useQuery } from 'convex-svelte';
|
||||||
|
import type { FunctionReference } from 'convex/server';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { LogIn, Settings, User, UserCog } from 'lucide-svelte';
|
||||||
|
import { authClient } from '$lib/auth';
|
||||||
|
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
||||||
|
|
||||||
|
let currentPath = $derived(page.url.pathname);
|
||||||
|
|
||||||
|
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>, {});
|
||||||
|
|
||||||
|
// Função para obter a URL do avatar/foto do usuário
|
||||||
|
let avatarUrlDoUsuario = $derived.by(() => {
|
||||||
|
if (!currentUser.data) return null;
|
||||||
|
|
||||||
|
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
|
||||||
|
if (currentUser.data.fotoPerfilUrl) {
|
||||||
|
return currentUser.data.fotoPerfilUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser.data.avatar) {
|
||||||
|
return currentUser.data.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: retornar null para usar o ícone User do Lucide
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function goToLogin(redirectTo?: string) {
|
||||||
|
const target = redirectTo || currentPath || '/';
|
||||||
|
goto(`${resolve('/login')}?redirect=${encodeURIComponent(target)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
const result = await authClient.signOut();
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Sign out error:', result.error);
|
||||||
|
}
|
||||||
|
// Resetar tema para padrão ao fazer logout
|
||||||
|
const { aplicarTemaPadrao } = await import('$lib/utils/temas');
|
||||||
|
aplicarTemaPadrao();
|
||||||
|
goto(resolve('/home'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{#if currentUser.data}
|
||||||
|
<!-- Nome e Perfil -->
|
||||||
|
<div class="hidden flex-col items-end lg:flex">
|
||||||
|
<span class="text-base-content text-sm leading-tight font-semibold"
|
||||||
|
>{currentUser.data.nome}</span
|
||||||
|
>
|
||||||
|
<span class="text-base-content/60 text-xs leading-tight">{currentUser.data.role?.nome}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<!-- Botão de Perfil com Avatar -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="btn avatar ring-base-200 hover:ring-primary/50 h-10 w-10 p-0 ring-2 ring-offset-2 transition-all"
|
||||||
|
aria-label="Menu do usuário"
|
||||||
|
>
|
||||||
|
<div class="h-full w-full overflow-hidden rounded-full">
|
||||||
|
{#if avatarUrlDoUsuario}
|
||||||
|
<img
|
||||||
|
src={avatarUrlDoUsuario}
|
||||||
|
alt={currentUser.data?.nome || 'Usuário'}
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="bg-primary/10 text-primary flex h-full w-full items-center justify-center">
|
||||||
|
<User class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content menu bg-base-100 rounded-box ring-base-content/5 z-1 mt-3 w-56 p-2 shadow-xl ring-1"
|
||||||
|
>
|
||||||
|
<li class="menu-title border-base-200 mb-2 border-b px-4 py-2">
|
||||||
|
<span class="text-base-content font-bold">{currentUser.data?.nome}</span>
|
||||||
|
<span class="text-base-content/60 text-xs font-normal">{currentUser.data.email}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/perfil')} class="active:bg-primary/10 active:text-primary"
|
||||||
|
><UserCog class="mr-2 h-4 w-4" /> Meu Perfil</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={resolve('/alterar-senha')} class="active:bg-primary/10 active:text-primary"
|
||||||
|
><Settings class="mr-2 h-4 w-4" /> Alterar Senha</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-1"></div>
|
||||||
|
<li>
|
||||||
|
<button type="button" onclick={handleLogout} class="text-error hover:bg-error/10"
|
||||||
|
><LogIn class="mr-2 h-4 w-4 rotate-180" /> Sair</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sino de notificações -->
|
||||||
|
<div class="relative">
|
||||||
|
<NotificationBell />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm rounded-full px-6"
|
||||||
|
onclick={() => goToLogin()}
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,16 +1,92 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { Toaster } from 'svelte-sonner';
|
import { Toaster } from 'svelte-sonner';
|
||||||
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
|
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
|
||||||
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
import Header from '$lib/components/Header.svelte';
|
||||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||||
|
import DashboardHeaderActions from '$lib/components/dashboard/DashboardHeaderActions.svelte';
|
||||||
|
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
||||||
|
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
||||||
|
import { Menu, X } from 'lucide-svelte';
|
||||||
|
|
||||||
const { children } = $props();
|
const { children } = $props();
|
||||||
|
|
||||||
|
let sidebarOpen = $state(false);
|
||||||
|
const toggleSidebar = () => (sidebarOpen = !sidebarOpen);
|
||||||
|
const closeSidebar = () => (sidebarOpen = false);
|
||||||
|
|
||||||
|
// No desktop, abrir por padrão; no mobile, começar fechado
|
||||||
|
onMount(() => {
|
||||||
|
sidebarOpen = window.matchMedia('(min-width: 1024px)').matches;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div
|
||||||
<Sidebar>
|
class="bg-base-100 text-base-content selection:bg-primary selection:text-primary-content flex min-h-screen flex-col font-sans"
|
||||||
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
|
>
|
||||||
|
<Header>
|
||||||
|
{#snippet left()}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
aria-label={sidebarOpen ? 'Fechar menu' : 'Abrir menu'}
|
||||||
|
onclick={toggleSidebar}
|
||||||
|
>
|
||||||
|
{#if sidebarOpen}
|
||||||
|
<X class="h-5 w-5" />
|
||||||
|
{:else}
|
||||||
|
<Menu class="h-5 w-5" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet right()}
|
||||||
|
<DashboardHeaderActions />
|
||||||
|
{/snippet}
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<div class="relative flex min-h-[calc(100vh-4rem)] flex-1">
|
||||||
|
<!-- Overlay (mobile) -->
|
||||||
|
{#if sidebarOpen}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-40 bg-black/30 backdrop-blur-[1px] lg:hidden"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Fechar menu"
|
||||||
|
onclick={closeSidebar}
|
||||||
|
onkeydown={(e) => e.key === 'Escape' && closeSidebar()}
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside
|
||||||
|
class="bg-base-100 border-base-200 fixed top-16 bottom-0 left-0 z-50 w-72 border-r shadow-sm transition-transform duration-200"
|
||||||
|
class:translate-x-0={sidebarOpen}
|
||||||
|
class:-translate-x-full={!sidebarOpen}
|
||||||
|
>
|
||||||
|
<div class="h-full overflow-y-auto">
|
||||||
|
<Sidebar onNavigate={closeSidebar} />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Conteúdo -->
|
||||||
|
<div
|
||||||
|
class="min-w-0 flex-1 transition-[padding] duration-200 {sidebarOpen
|
||||||
|
? 'lg:pl-72'
|
||||||
|
: 'lg:pl-0'}"
|
||||||
|
>
|
||||||
|
<div id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</div>
|
||||||
</Sidebar>
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Componentes de Chat (gerenciam auth internamente) -->
|
||||||
|
<PresenceManager />
|
||||||
|
<ChatWidget />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Notifications (Sonner) -->
|
<!-- Toast Notifications (Sonner) -->
|
||||||
@@ -18,3 +94,14 @@
|
|||||||
|
|
||||||
<!-- Push Notification Manager (registra subscription automaticamente) -->
|
<!-- Push Notification Manager (registra subscription automaticamente) -->
|
||||||
<PushNotificationManager />
|
<PushNotificationManager />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Evita “corredor” quando páginas usam `container mx-auto` */
|
||||||
|
#container-central :global(.container) {
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
#container-central :global(.container.mx-auto) {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="w-full px-4 py-4">
|
||||||
<!-- Alerta de Acesso Negado / Autenticação -->
|
<!-- Alerta de Acesso Negado / Autenticação -->
|
||||||
{#if showAlert}
|
{#if showAlert}
|
||||||
{@const alertData = getAlertMessage()}
|
{@const alertData = getAlertMessage()}
|
||||||
|
|||||||
Reference in New Issue
Block a user