293 lines
12 KiB
HTML
Executable File
293 lines
12 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="es">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>U-site</title>
|
|
<!-- Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<!-- Select2 CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet"/>
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="/css/app.css">
|
|
<!-- Axios -->
|
|
<script src="https://cdn.jsdelivr.net/npm/axios@1.6.7/dist/axios.min.js"></script>
|
|
<!-- Alpine.js -->
|
|
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.5/dist/cdn.min.js" defer></script>
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<!-- jQuery -->
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<!-- Select2 JS -->
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
|
[x-cloak] { display: none !important; }
|
|
|
|
:root {
|
|
--color-primary: #8eb02f;
|
|
--color-primary-dark: #6d8c24;
|
|
--color-secondary: #1e293b;
|
|
--color-bg: #f1f5f9;
|
|
--color-border: #e2e8f0;
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
/* Sidebar scroll */
|
|
.sidebar-scroll { scrollbar-width: none; -ms-overflow-style: none; }
|
|
.sidebar-scroll::-webkit-scrollbar { display: none; }
|
|
|
|
/* Avatar circle */
|
|
.ui-avatar {
|
|
width: 2rem; height: 2rem; border-radius: 9999px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
font-weight: 700; font-size: .7rem; color: #fff;
|
|
background-color: var(--color-primary);
|
|
text-transform: uppercase; flex-shrink: 0; user-select: none;
|
|
}
|
|
.ui-avatar-lg { width: 2.25rem; height: 2.25rem; font-size: .8rem; }
|
|
|
|
/* Pulse animation for activity dot */
|
|
@keyframes pulse-ring {
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: .75; transform: scale(1.15); }
|
|
}
|
|
.pulse-dot { animation: pulse-ring 2.5s ease-in-out infinite; }
|
|
|
|
/* Smooth focus inputs */
|
|
.ui-input {
|
|
width: 100%; padding: .5rem .75rem;
|
|
border: 1px solid var(--color-border); border-radius: var(--radius);
|
|
font-size: .875rem; outline: none; font-family: inherit;
|
|
transition: border-color .15s ease, box-shadow .15s ease;
|
|
}
|
|
.ui-input:focus {
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 3px rgba(142, 176, 47, .15);
|
|
}
|
|
.ui-input::placeholder { color: #94a3b8; }
|
|
|
|
/* Active nav item in sidebar */
|
|
.nav-active { background-color: var(--color-primary) !important; color: #fff !important; }
|
|
|
|
/* === Global content page improvements === */
|
|
|
|
/* Card wrapper (used in all content pages) */
|
|
.bg-white.rounded-lg.shadow,
|
|
.bg-white.rounded-lg {
|
|
border-radius: 0.75rem !important;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04) !important;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
/* Page title */
|
|
h1.text-2xl.font-bold {
|
|
font-size: 1.125rem !important;
|
|
font-weight: 700 !important;
|
|
color: #1e293b !important;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* Search input improvements */
|
|
input[placeholder="Buscar..."] {
|
|
border-radius: 0.5rem !important;
|
|
border: 1px solid #e2e8f0 !important;
|
|
font-size: 0.875rem !important;
|
|
transition: border-color .15s ease, box-shadow .15s ease !important;
|
|
outline: none !important;
|
|
padding: 0.5rem 0.75rem 0.5rem 2rem !important;
|
|
}
|
|
input[placeholder="Buscar..."]:focus {
|
|
border-color: var(--color-primary) !important;
|
|
box-shadow: 0 0 0 3px rgba(142,176,47,.12) !important;
|
|
}
|
|
|
|
/* Table - global improvements */
|
|
.table-auto, table.table-auto {
|
|
border-collapse: collapse;
|
|
font-size: 0.875rem;
|
|
}
|
|
.table-auto thead tr, table.table-auto thead tr {
|
|
background-color: #f8fafc !important;
|
|
}
|
|
.table-auto thead th, table.table-auto thead th {
|
|
font-size: 0.7rem !important;
|
|
font-weight: 600 !important;
|
|
text-transform: uppercase !important;
|
|
letter-spacing: 0.05em !important;
|
|
color: #475569 !important;
|
|
padding: 0.75rem 1rem !important;
|
|
white-space: nowrap;
|
|
}
|
|
.table-auto tbody td, table.table-auto tbody td {
|
|
padding: 0.875rem 1rem !important;
|
|
vertical-align: middle;
|
|
font-size: 0.8125rem !important;
|
|
}
|
|
.table-auto tbody tr, table.table-auto tbody tr {
|
|
transition: background-color 0.1s ease;
|
|
}
|
|
.table-auto tbody tr:hover, table.table-auto tbody tr:hover {
|
|
background-color: #f8fafc !important;
|
|
}
|
|
|
|
/* Action icons in rows */
|
|
.table-auto button svg, table.table-auto button svg {
|
|
transition: transform 0.1s ease;
|
|
}
|
|
.table-auto button:hover svg, table.table-auto button:hover svg {
|
|
transform: scale(1.15);
|
|
}
|
|
|
|
/* Loading overlay */
|
|
.fixed.inset-0.bg-gray-800 {
|
|
background-color: rgba(15, 23, 42, 0.55) !important;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
/* Page loading bar at top */
|
|
.page-content { animation: fadeIn .2s ease; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-slate-100 w-full">
|
|
<main class="flex w-full">
|
|
<!-- Sidebar -->
|
|
{{template "partials/lateral" .}}
|
|
<!-- Main area -->
|
|
<div class="flex-1 flex flex-col min-w-0">
|
|
{{template "partials/header" .}}
|
|
<div class="flex-1 p-4 overflow-auto page-content" style="height:calc(100vh - 64px)">
|
|
{{embed}}
|
|
</div>
|
|
{{template "partials/footer" .}}
|
|
</div>
|
|
</main>
|
|
<!-- Session Timeout -->
|
|
<div x-data="sessionTimeout" x-init="init()" x-cloak>
|
|
<!-- Activity pill -->
|
|
<div x-show="!showModal"
|
|
x-transition
|
|
class="fixed top-4 right-4 z-50 hidden sm:flex items-center gap-2 bg-white border border-slate-200 shadow-sm rounded-full px-3 py-1.5 text-xs text-slate-500 select-none">
|
|
<div class="w-2 h-2 rounded-full bg-emerald-400 pulse-dot flex-shrink-0"></div>
|
|
<span>Sesión: <span class="font-semibold text-slate-700" x-text="formatTime(inactivityCountdown)"></span></span>
|
|
</div>
|
|
<!-- Modal -->
|
|
<div x-show="showModal"
|
|
x-transition
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden">
|
|
<div class="px-6 py-5" style="background: linear-gradient(135deg, #1e293b 0%, #334155 100%)">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0"
|
|
style="background:rgba(251,191,36,.15)">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" style="color:#fbbf24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-white font-semibold">Sesión por vencer</h2>
|
|
<p class="text-slate-400 text-xs mt-0.5">Tu sesión expirará por inactividad</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="px-6 py-6">
|
|
<div class="flex items-center justify-center gap-2 bg-red-50 rounded-xl py-3 mb-5 border border-red-100">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-red-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
</svg>
|
|
<span class="text-sm text-red-600">Cierre automático en <strong x-text="countdown"></strong> segundos</span>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button @click="continueSession"
|
|
class="flex-1 py-2.5 rounded-xl text-sm font-semibold text-white transition-all"
|
|
style="background-color:#8eb02f"
|
|
onmouseover="this.style.backgroundColor='#6d8c24'"
|
|
onmouseout="this.style.backgroundColor='#8eb02f'">
|
|
Continuar sesión
|
|
</button>
|
|
<form action="/do/logout" method="POST" class="flex-1">
|
|
<button type="submit"
|
|
class="w-full py-2.5 rounded-xl text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition-colors">
|
|
Cerrar sesión
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("alpine:init", () => {
|
|
Alpine.data("sessionTimeout", () => ({
|
|
showModal: false,
|
|
countdown: 60,
|
|
inactivityCountdown: 300,
|
|
inactivityTimer: null,
|
|
timer: null,
|
|
|
|
init() {
|
|
this.resetInactivityTimer();
|
|
window.addEventListener("mousemove", () => this.resetInactivityTimer());
|
|
window.addEventListener("keydown", () => this.resetInactivityTimer());
|
|
},
|
|
|
|
resetInactivityTimer() {
|
|
clearTimeout(this.inactivityTimer);
|
|
this.inactivityCountdown = 300;
|
|
this.startInactivityTimer();
|
|
},
|
|
|
|
startInactivityTimer() {
|
|
this.inactivityTimer = setInterval(() => {
|
|
this.inactivityCountdown--;
|
|
if (this.inactivityCountdown <= 0) {
|
|
clearInterval(this.inactivityTimer);
|
|
this.promptLogout();
|
|
}
|
|
}, 1000);
|
|
},
|
|
|
|
promptLogout() {
|
|
this.showModal = true;
|
|
this.startCountdown();
|
|
},
|
|
|
|
startCountdown() {
|
|
this.countdown = 60;
|
|
clearInterval(this.timer);
|
|
this.timer = setInterval(() => {
|
|
this.countdown--;
|
|
if (this.countdown <= 0) this.logout();
|
|
}, 1000);
|
|
},
|
|
|
|
continueSession() {
|
|
clearInterval(this.timer);
|
|
this.showModal = false;
|
|
this.resetInactivityTimer();
|
|
},
|
|
|
|
logout() {
|
|
clearInterval(this.timer);
|
|
document.querySelector('form[action="/do/logout"]').submit();
|
|
},
|
|
|
|
formatTime(seconds) {
|
|
const m = Math.floor(seconds / 60);
|
|
const s = seconds % 60;
|
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
},
|
|
}));
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html> |