feat: Implement dedicated login page and public/dashboard layouts, refactoring authentication flow and removing the todos page.
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { loginModalStore } from '$lib/stores/loginModal.svelte';
|
||||
import { TriangleAlert } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
interface Props {
|
||||
recurso: string;
|
||||
@@ -34,7 +35,10 @@
|
||||
verificando = false;
|
||||
permitido = false;
|
||||
const currentPath = window.location.pathname;
|
||||
loginModalStore.open(currentPath);
|
||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(currentPath)}`, {
|
||||
replaceState: true,
|
||||
noScroll: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
52
apps/web/src/lib/components/Footer.svelte
Normal file
52
apps/web/src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<footer class="bg-base-200 text-base-content border-base-300 border-t">
|
||||
<div class="container mx-auto px-4 py-10">
|
||||
<div class="grid grid-cols-1 gap-8 text-center md:grid-cols-3 md:text-left">
|
||||
<div>
|
||||
<h3 class="text-primary mb-4 text-lg font-bold">SGSE</h3>
|
||||
<p class="mx-auto max-w-xs text-sm opacity-75 md:mx-0">
|
||||
Sistema de Gestão de Secretaria<br />
|
||||
Simplificando processos e conectando pessoas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-bold">Links Úteis</h3>
|
||||
<ul class="space-y-2 text-sm opacity-75">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.pe.gov.br/"
|
||||
target="_blank"
|
||||
class="hover:text-primary transition-colors">Portal do Governo</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href={resolve('/abrir-chamado')} class="hover:text-primary transition-colors"
|
||||
>Suporte</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-bold">Contato</h3>
|
||||
<p class="text-sm opacity-75">
|
||||
Secretaria de Educação<br />
|
||||
Recife - PE
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider mt-8 mb-4"></div>
|
||||
|
||||
<div class="flex flex-col items-center justify-between text-sm opacity-60 md:flex-row">
|
||||
<p>© {currentYear} Governo de Pernambuco. Todos os direitos reservados.</p>
|
||||
<p class="mt-2 md:mt-0">Desenvolvido com tecnologia de ponta.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,7 +1,20 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo_governo_PE.png';
|
||||
import { page } from '$app/state';
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-200 w-76 p-4 shadow-sm">
|
||||
<img src={logo} alt="Logo" class="" />
|
||||
</div>
|
||||
<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">
|
||||
<div class="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-3 group transition-transform hover:scale-[1.02]">
|
||||
<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">
|
||||
<span class="text-xs font-bold text-primary tracking-wider uppercase">Governo de</span>
|
||||
<span class="text-lg font-extrabold -mt-1 tracking-tight text-base-content leading-none">Pernambuco</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<nav class="flex items-center gap-2">
|
||||
<!-- Links can be added here based on auth state or specific requirements -->
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { loginModalStore } from '$lib/stores/loginModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
interface MenuProtectionProps {
|
||||
menuPath: string;
|
||||
@@ -23,16 +23,8 @@
|
||||
let temPermissao = $state(false);
|
||||
let motivoNegacao = $state('');
|
||||
|
||||
// Query para verificar permissões (só executa se o usuário estiver autenticado)
|
||||
// Usuário atual (para autenticação básica)
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
let permissaoQuery = $derived(
|
||||
currentUser?.data
|
||||
? useQuery(api.menuPermissoes.verificarAcesso, {
|
||||
usuarioId: currentUser.data._id as Id<'usuarios'>,
|
||||
menuPath: menuPath
|
||||
})
|
||||
: null
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
verificarPermissoes();
|
||||
@@ -43,13 +35,6 @@
|
||||
verificarPermissoes();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Re-verificar quando a query carregar
|
||||
if (permissaoQuery?.data) {
|
||||
verificarPermissoes();
|
||||
}
|
||||
});
|
||||
|
||||
function verificarPermissoes() {
|
||||
// Dashboard e abertura de chamados são públicos
|
||||
if (menuPath === '/' || menuPath === '/abrir-chamado') {
|
||||
@@ -64,43 +49,18 @@
|
||||
temPermissao = false;
|
||||
motivoNegacao = 'auth_required';
|
||||
|
||||
// Abrir modal de login e salvar rota de redirecionamento
|
||||
// Redirecionar para a página de login e salvar rota de redirecionamento
|
||||
const currentPath = window.location.pathname;
|
||||
loginModalStore.open(currentPath);
|
||||
|
||||
// NÃO redirecionar, apenas mostrar o modal
|
||||
// O usuário verá a mensagem "Verificando permissões..." enquanto o modal está aberto
|
||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(currentPath)}`, {
|
||||
replaceState: true,
|
||||
noScroll: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Se está autenticado, verificar permissões
|
||||
if (permissaoQuery?.data) {
|
||||
const permissao = permissaoQuery.data;
|
||||
|
||||
// Se não pode acessar
|
||||
if (!permissao.podeAcessar) {
|
||||
verificando = false;
|
||||
temPermissao = false;
|
||||
motivoNegacao = 'access_denied';
|
||||
return;
|
||||
}
|
||||
|
||||
// Se requer gravação mas não tem permissão
|
||||
if (requireGravar && !permissao.podeGravar) {
|
||||
verificando = false;
|
||||
temPermissao = false;
|
||||
motivoNegacao = 'write_denied';
|
||||
return;
|
||||
}
|
||||
|
||||
// Tem permissão!
|
||||
verificando = false;
|
||||
temPermissao = true;
|
||||
} else if (permissaoQuery?.error) {
|
||||
verificando = false;
|
||||
temPermissao = false;
|
||||
motivoNegacao = 'error';
|
||||
}
|
||||
// Se está autenticado, permitir acesso (component está sem verificação de menu específica no momento)
|
||||
verificando = false;
|
||||
temPermissao = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import type { FunctionReference } from 'convex/server';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import {
|
||||
Home,
|
||||
User,
|
||||
@@ -37,18 +36,6 @@
|
||||
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
|
||||
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
|
||||
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
|
||||
import { loginModalStore } from '$lib/stores/loginModal.svelte';
|
||||
import { obterIPPublico } from '$lib/utils/deviceInfo';
|
||||
|
||||
interface GPSLocation {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
precisao?: number;
|
||||
endereco?: string;
|
||||
cidade?: string;
|
||||
estado?: string;
|
||||
pais?: string;
|
||||
}
|
||||
|
||||
interface MenuItemPermission {
|
||||
recurso: string;
|
||||
@@ -214,11 +201,6 @@
|
||||
const { children }: { children: Snippet } = $props();
|
||||
|
||||
let currentPath = $derived(page.url.pathname);
|
||||
|
||||
let matricula = $state('');
|
||||
let senha = $state('');
|
||||
let erroLogin = $state('');
|
||||
let carregandoLogin = $state(false);
|
||||
let showAboutModal = $state(false);
|
||||
|
||||
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>, {});
|
||||
@@ -226,11 +208,11 @@
|
||||
|
||||
// Filtrar menu baseado nas permissões do usuário
|
||||
function filterSubmenusByPermissions(
|
||||
items: SubMenuItem[],
|
||||
items: readonly SubMenuItem[],
|
||||
isMaster: boolean,
|
||||
permissionsSet: Set<string>
|
||||
): SubMenuItem[] {
|
||||
if (isMaster) return items;
|
||||
if (isMaster) return [...items];
|
||||
|
||||
return items.filter((item) => {
|
||||
if (!item.permission) return true;
|
||||
@@ -240,11 +222,11 @@
|
||||
}
|
||||
|
||||
function filterMenuByPermissions(
|
||||
items: MenuItem[],
|
||||
items: readonly MenuItem[],
|
||||
isMaster: boolean,
|
||||
permissionsSet: Set<string>
|
||||
): MenuItem[] {
|
||||
if (isMaster) return items;
|
||||
if (isMaster) return [...items];
|
||||
|
||||
const filtered: MenuItem[] = [];
|
||||
|
||||
@@ -283,8 +265,6 @@
|
||||
return filterMenuByPermissions(MENU_STRUCTURE, data.isMaster, permissionsSet);
|
||||
});
|
||||
|
||||
const convexClient = useConvexClient();
|
||||
|
||||
const iconMap: Record<string, IconType> = {
|
||||
Home,
|
||||
User,
|
||||
@@ -368,20 +348,9 @@
|
||||
return null;
|
||||
});
|
||||
|
||||
function openLoginModal() {
|
||||
loginModalStore.open();
|
||||
matricula = '';
|
||||
senha = '';
|
||||
erroLogin = '';
|
||||
carregandoLogin = false;
|
||||
}
|
||||
|
||||
function closeLoginModal() {
|
||||
loginModalStore.close();
|
||||
matricula = '';
|
||||
senha = '';
|
||||
erroLogin = '';
|
||||
carregandoLogin = false;
|
||||
function goToLogin(redirectTo?: string) {
|
||||
const target = redirectTo || currentPath || '/';
|
||||
goto(`${resolve('/login')}?redirect=${encodeURIComponent(target)}`);
|
||||
}
|
||||
|
||||
function openAboutModal() {
|
||||
@@ -392,152 +361,6 @@
|
||||
showAboutModal = false;
|
||||
}
|
||||
|
||||
async function handleLogin(e: Event) {
|
||||
e.preventDefault();
|
||||
erroLogin = '';
|
||||
carregandoLogin = true;
|
||||
|
||||
// Obter IP público e userAgent (rápido, não bloqueia)
|
||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : undefined;
|
||||
|
||||
// Obter IP público com timeout curto (não bloquear login)
|
||||
const ipPublicoPromise = obterIPPublico().catch(() => undefined);
|
||||
const ipPublicoTimeout = new Promise<undefined>(
|
||||
(resolve) => setTimeout(() => resolve(undefined), 2000) // Timeout de 2 segundos
|
||||
);
|
||||
const ipPublico = await Promise.race([ipPublicoPromise, ipPublicoTimeout]);
|
||||
|
||||
// Função para coletar GPS em background (não bloqueia login)
|
||||
async function coletarGPS(): Promise<GPSLocation> {
|
||||
try {
|
||||
const { obterLocalizacaoRapida } = await import('$lib/utils/deviceInfo');
|
||||
// Usar versão rápida com timeout curto (3 segundos máximo)
|
||||
const gpsPromise = obterLocalizacaoRapida();
|
||||
const gpsTimeout = new Promise<GPSLocation>((resolve) =>
|
||||
setTimeout(() => resolve({}), 3000)
|
||||
);
|
||||
return await Promise.race([gpsPromise, gpsTimeout]);
|
||||
} catch (err) {
|
||||
console.warn('Erro ao obter GPS (não bloqueia login):', err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar coleta de GPS em background (não esperar)
|
||||
const gpsPromise = coletarGPS();
|
||||
|
||||
const result = await authClient.signIn.email(
|
||||
{ email: matricula.trim(), password: senha },
|
||||
{
|
||||
onError: async (ctx) => {
|
||||
// Registrar tentativa de login falha
|
||||
try {
|
||||
// Tentar obter GPS se já estiver disponível (não esperar)
|
||||
let localizacaoGPS: GPSLocation = {};
|
||||
try {
|
||||
localizacaoGPS = await Promise.race([
|
||||
gpsPromise,
|
||||
new Promise<GPSLocation>((resolve) => setTimeout(() => resolve({}), 100))
|
||||
]);
|
||||
} catch {
|
||||
// Ignorar se GPS não estiver pronto
|
||||
}
|
||||
|
||||
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
||||
matriculaOuEmail: matricula.trim(),
|
||||
sucesso: false,
|
||||
motivoFalha: ctx.error?.message || 'Erro desconhecido',
|
||||
userAgent: userAgent,
|
||||
ipAddress: ipPublico,
|
||||
latitudeGPS: localizacaoGPS.latitude,
|
||||
longitudeGPS: localizacaoGPS.longitude,
|
||||
precisaoGPS: localizacaoGPS.precisao,
|
||||
enderecoGPS: localizacaoGPS.endereco,
|
||||
cidadeGPS: localizacaoGPS.cidade,
|
||||
estadoGPS: localizacaoGPS.estado,
|
||||
paisGPS: localizacaoGPS.pais
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erro ao registrar tentativa de login falha:', err);
|
||||
}
|
||||
alert(ctx.error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
// Registrar tentativa de login bem-sucedida
|
||||
// Fazer de forma assíncrona para não bloquear o login
|
||||
// Não tentamos buscar getCurrentUser aqui porque pode causar timeout
|
||||
// O useQuery no componente já busca o usuário automaticamente quando a sessão estiver pronta
|
||||
(async () => {
|
||||
try {
|
||||
// Aguardar um pouco para o usuário ser sincronizado no Convex
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Tentar obter GPS se já estiver disponível (não esperar)
|
||||
let localizacaoGPS: GPSLocation = {};
|
||||
try {
|
||||
localizacaoGPS = await Promise.race([
|
||||
gpsPromise,
|
||||
new Promise<GPSLocation>((resolve) => setTimeout(() => resolve({}), 100))
|
||||
]);
|
||||
} catch {
|
||||
// Ignorar se GPS não estiver pronto
|
||||
}
|
||||
|
||||
// Buscar o usuário no Convex usando getCurrentUser
|
||||
// (o typesafe FunctionReference pode falhar aqui; tipamos o retorno e mantemos a chamada)
|
||||
const usuario = (await convexClient.query(
|
||||
api.auth.getCurrentUser as unknown as FunctionReference<'query'>,
|
||||
{}
|
||||
)) as { _id?: Id<'usuarios'> } | null;
|
||||
|
||||
if (usuario && usuario._id) {
|
||||
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
||||
usuarioId: usuario._id,
|
||||
matriculaOuEmail: matricula.trim(),
|
||||
sucesso: true,
|
||||
userAgent: userAgent,
|
||||
ipAddress: ipPublico,
|
||||
latitudeGPS: localizacaoGPS.latitude,
|
||||
longitudeGPS: localizacaoGPS.longitude,
|
||||
precisaoGPS: localizacaoGPS.precisao,
|
||||
enderecoGPS: localizacaoGPS.endereco,
|
||||
cidadeGPS: localizacaoGPS.cidade,
|
||||
estadoGPS: localizacaoGPS.estado,
|
||||
paisGPS: localizacaoGPS.pais
|
||||
});
|
||||
} else {
|
||||
// Se não encontrou o usuário, registrar sem usuarioId (será atualizado depois)
|
||||
await convexClient.mutation(api.logsLogin.registrarTentativaLogin, {
|
||||
matriculaOuEmail: matricula.trim(),
|
||||
sucesso: true,
|
||||
userAgent: userAgent,
|
||||
ipAddress: ipPublico,
|
||||
latitudeGPS: localizacaoGPS.latitude,
|
||||
longitudeGPS: localizacaoGPS.longitude,
|
||||
precisaoGPS: localizacaoGPS.precisao,
|
||||
enderecoGPS: localizacaoGPS.endereco,
|
||||
cidadeGPS: localizacaoGPS.cidade,
|
||||
estadoGPS: localizacaoGPS.estado,
|
||||
paisGPS: localizacaoGPS.pais
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erro ao registrar tentativa de login:', err);
|
||||
// Não bloquear o login se houver erro ao registrar
|
||||
}
|
||||
})();
|
||||
|
||||
closeLoginModal();
|
||||
goto(resolve('/'));
|
||||
} else {
|
||||
erroLogin = 'Erro ao fazer login';
|
||||
}
|
||||
carregandoLogin = false;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
const result = await authClient.signOut();
|
||||
if (result.error) {
|
||||
@@ -647,7 +470,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm rounded-full px-6"
|
||||
onclick={() => openLoginModal()}
|
||||
onclick={() => goToLogin()}
|
||||
>
|
||||
Entrar
|
||||
</button>
|
||||
@@ -737,7 +560,7 @@
|
||||
exact: sub.exact
|
||||
})}
|
||||
<li>
|
||||
<a href={resolve(sub.link)} class={getMenuClasses(isSubActive, true)}>
|
||||
<a href={resolve(sub.link as any)} class={getMenuClasses(isSubActive, true)}>
|
||||
<span>{sub.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -747,7 +570,7 @@
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(item.link)}
|
||||
href={resolve(item.link as any)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
class={getMenuClasses(isActive)}
|
||||
>
|
||||
@@ -789,148 +612,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Login -->
|
||||
{#if loginModalStore.showModal}
|
||||
<dialog class="modal modal-open">
|
||||
<div
|
||||
class="modal-box from-base-100 via-base-100 to-primary/5 relative max-w-md overflow-hidden bg-linear-to-br shadow-2xl backdrop-blur-sm"
|
||||
>
|
||||
<!-- Botão de fechar moderno -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-circle btn-ghost hover:bg-error/20 hover:text-error absolute top-4 right-4 z-10 transition-all duration-200"
|
||||
onclick={closeLoginModal}
|
||||
aria-label="Fechar modal"
|
||||
>
|
||||
<XCircle class="h-5 w-5" strokeWidth={2.5} />
|
||||
</button>
|
||||
|
||||
<!-- Decoração de fundo -->
|
||||
<div class="bg-primary/10 absolute -top-20 -right-20 h-40 w-40 rounded-full blur-3xl"></div>
|
||||
<div class="bg-primary/5 absolute -bottom-20 -left-20 h-40 w-40 rounded-full blur-3xl"></div>
|
||||
|
||||
<div class="relative z-10 p-8">
|
||||
<!-- Header com logo e título -->
|
||||
<div class="mb-8 text-center">
|
||||
<div class="avatar mx-auto mb-5">
|
||||
<div
|
||||
class="group ring-primary/20 relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="from-primary/10 absolute inset-0 bg-linear-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
></div>
|
||||
<img src={logo} alt="Logo SGSE" class="relative z-10 h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-primary mb-2 text-4xl font-bold tracking-tight">Login</h3>
|
||||
<p class="text-base-content/70 text-sm font-medium">
|
||||
Acesse o sistema com suas credenciais
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mensagem de erro -->
|
||||
{#if erroLogin}
|
||||
<div
|
||||
class="alert alert-error border-error/30 bg-error/10 mb-6 shadow-lg backdrop-blur-sm"
|
||||
>
|
||||
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2.5} />
|
||||
<span class="font-medium">{erroLogin}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Formulário -->
|
||||
<form class="space-y-5" onsubmit={handleLogin}>
|
||||
<!-- Campo Matrícula/E-mail -->
|
||||
<div class="form-control">
|
||||
<label class="label pb-2" for="login-matricula">
|
||||
<span class="text-primary label-text text-sm font-semibold">Matrícula ou E-mail</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="login-matricula"
|
||||
type="text"
|
||||
placeholder="Digite sua matrícula ou e-mail"
|
||||
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
||||
bind:value={matricula}
|
||||
required
|
||||
disabled={carregandoLogin}
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo Senha -->
|
||||
<div class="form-control">
|
||||
<label class="label pb-2" for="login-password">
|
||||
<span class="text-primary label-text text-sm font-semibold">Senha</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
|
||||
bind:value={senha}
|
||||
required
|
||||
disabled={carregandoLogin}
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botão de submit -->
|
||||
<div class="form-control pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg group from-primary via-primary to-primary/90 relative w-full overflow-hidden border-0 bg-linear-to-r shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
|
||||
disabled={carregandoLogin}
|
||||
>
|
||||
<!-- Efeito de brilho animado -->
|
||||
<div
|
||||
class="absolute inset-0 -translate-x-full bg-linear-to-r from-transparent via-white/30 to-transparent transition-transform duration-1000 group-hover:translate-x-full"
|
||||
></div>
|
||||
|
||||
{#if carregandoLogin}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span class="font-semibold">Entrando...</span>
|
||||
{:else}
|
||||
<LogIn
|
||||
class="h-5 w-5 transition-transform duration-300 group-hover:scale-110"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span class="font-semibold">Entrar</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Links auxiliares -->
|
||||
<div class="space-y-3 pt-4 text-center">
|
||||
<a
|
||||
href={resolve('/abrir-chamado')}
|
||||
class="link link-primary block text-sm font-medium transition-all duration-200 hover:scale-105"
|
||||
onclick={closeLoginModal}
|
||||
>
|
||||
Abrir Chamado
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/esqueci-senha')}
|
||||
class="link link-secondary block text-sm font-medium transition-all duration-200 hover:scale-105"
|
||||
onclick={closeLoginModal}
|
||||
>
|
||||
Esqueceu sua senha?
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<form method="dialog" class="modal-backdrop" onclick={closeLoginModal}>
|
||||
<button type="button">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
<!-- Modal Sobre -->
|
||||
{#if showAboutModal}
|
||||
<dialog class="modal modal-open">
|
||||
|
||||
Reference in New Issue
Block a user