ATS/templates/base.html
2026-02-01 13:38:06 +03:00

930 lines
45 KiB
HTML

{% load static i18n %}
{% load logo_tags %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>{% block title %}{% trans 'University ATS' %}{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'temple-red': '#9d2235',
'temple-dark': '#1a1a1a',
'temple-cream': '#f8f7f2',
'dashboard-blue': '#4e73df',
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar - desktop only */
@media (min-width: 1024px) {
.sidebar-scroll::-webkit-scrollbar { width: 4px; }
.sidebar-scroll::-webkit-scrollbar-thumb {
background: #333;
border-radius: 10px;
}
}
/* Touch-friendly tap highlight */
* {
-webkit-tap-highlight-color: rgba(157, 34, 53, 0.1);
}
/* Improved touch targets - minimum 44x44px */
button, a, .touch-target {
min-height: 44px;
min-width: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Prevent text selection on buttons */
button {
-webkit-user-select: none;
user-select: none;
}
/* Smooth transitions */
.smooth-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Mobile-optimized sidebar */
#sidebar {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-overflow-scrolling: touch;
}
/* Backdrop blur support */
@supports ((-webkit-backdrop-filter: blur(10px)) or (backdrop-filter: blur(10px))) {
.backdrop-blur-custom {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.5);
}
}
/* Safe area padding for notched devices */
@supports (padding: env(dQrgH6E6C$$!@9safe-area-inset-bottom)) {
.safe-area-padding-bottom {
padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
}
.safe-area-padding-top {
padding-top: calc(0.75rem + env(safe-area-inset-top));
}
}
/* Sticky header optimization */
.sticky-header {
position: sticky;
top: 0;
z-index: 30;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
/* Alert animation */
.alert {
animation: slideInDown 0.3s ease-out;
}
@keyframes slideInDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Mobile navigation optimization */
@media (max-width: 1023px) {
.nav-link {
padding: 0.875rem 0.75rem;
font-size: 0.9375rem;
display: flex;
align-items: center;
justify-content: flex-start;
}
.nav-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
margin-right: 0.75rem;
}
.nav-text {
line-height: 1.25rem;
flex-shrink: 0;
}
/* Ensure flex items align properly */
.sidebar-nav a {
display: flex;
align-items: center;
gap: 0;
}
/* Larger touch targets on mobile */
.sidebar-scroll {
padding-bottom: 2rem;
}
}
/* Desktop optimizations */
@media (min-width: 1024px) {
/* Smooth sidebar transitions */
#main-content {
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#sidebar {
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
/* Fix for iOS momentum scrolling */
.momentum-scroll {
-webkit-overflow-scrolling: touch;
overflow-y: auto;
}
/* Prevent zoom on input focus (iOS) */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
select,
textarea {
font-size: 16px;
}
/* Dropdown menu improvements */
.dropdown-menu {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* Progress bar glow effect */
.progress-bar-glow {
box-shadow: 0 0 8px rgba(255,255,255,0.4);
}
/* Optimized badge positioning */
.nav-badge {
font-size: 0.625rem;
line-height: 1;
padding: 0.125rem 0.375rem;
}
/* Better visibility for active states */
.nav-link.active {
font-weight: 600;
}
/* Tooltip for collapsed sidebar */
.sidebar-tooltip {
position: fixed;
background-color: #1a1a1a;
color: white;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.875rem;
white-space: nowrap;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
z-index: 9999;
pointer-events: none;
max-width: 200px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.sidebar-tooltip.visible {
opacity: 1;
visibility: visible;
}
/* Mobile-friendly footer */
@media (max-width: 640px) {
footer {
font-size: 0.6875rem;
padding: 1rem;
}
}
</style>
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'image/favicon/apple-touch-icon.png'%}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'image/favicon/favicon-32x32.png'%}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'image/favicon/favicon-16x16.png'%}">
<link rel="manifest" href="{% static 'image/favicon/site.webmanifest'%}">
{% block customCSS %}{% endblock %}
</head>
<body class="bg-temple-cream text-gray-800 flex min-h-screen overflow-x-hidden">
<!-- Tooltip Element -->
<div id="sidebar-tooltip" class="sidebar-tooltip"></div>
<!-- Mobile Sidebar Backdrop -->
<div id="sidebar-backdrop"
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 hidden lg:hidden backdrop-blur-custom"
onclick="closeMobileSidebar()"
role="button"
aria-label="{% trans 'Close sidebar' %}">
</div>
<!-- Sidebar -->
<aside id="sidebar"
class="fixed inset-y-0 left-0 z-50 bg-temple-dark text-gray-400 transform -translate-x-full lg:translate-x-0 w-64 lg:w-64 border-r border-gray-800 smooth-transition"
role="navigation"
aria-label="{% trans 'Main navigation' %}">
<div class="flex flex-col h-full">
<!-- Sidebar Header -->
<div class="p-4 sm:p-6 flex items-center gap-3 text-white sidebar-header safe-area-padding-top">
{% comment %} <div class="bg-temple-red p-2 rounded-lg shrink-0">
<i data-lucide="shield-check" class="w-5 h-5 sm:w-6 sm:h-6 text-white"></i>
</div>
<span class="text-lg sm:text-xl font-bold tracking-tight sidebar-text">{% trans "ATS" %}</span> {% endcomment %}
{% logo_ats height="40" %}
{% logo_tenhal_ats height="40" %}
<button onclick="closeMobileSidebar()"
class="lg:hidden ml-auto text-gray-400 hover:text-white transition p-2 -mr-2"
aria-label="{% trans 'Close menu' %}">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<!-- Navigation Menu -->
<nav class="flex-1 overflow-y-auto sidebar-scroll momentum-scroll px-3 sm:px-4 pb-4 sidebar-nav">
<div class="text-[10px] uppercase tracking-widest text-gray-600 font-bold mb-3 sm:mb-4 px-2 section-header">
{% trans "Main" %}
</div>
<ul class="space-y-1">
<li>
<a href="{% url 'dashboard' %}"
data-tooltip="{% trans 'Dashboard' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'dashboard' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name == 'dashboard' %}page{% endif %}">
<i data-lucide="layout-grid" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name == 'dashboard' %}text-temple-red{% endif %}"></i>
<span class="font-medium nav-text">{% trans "Dashboard" %}</span>
</a>
</li>
<li>
<a href="{% url 'job_list' %}"
data-tooltip="{% trans 'Jobs' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'job_list' or request.resolver_match.url_name == 'job_create' or request.resolver_match.url_name == 'job_detail' or request.resolver_match.url_name == 'job_update' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name == 'job_list' or request.resolver_match.url_name == 'job_create' or request.resolver_match.url_name == 'job_detail' or request.resolver_match.url_name == 'job_update' %}page{% endif %}">
<i data-lucide="briefcase" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name == 'job_list' or request.resolver_match.url_name == 'job_create' or request.resolver_match.url_name == 'job_detail' or request.resolver_match.url_name == 'job_update' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Jobs" %}</span>
</a>
</li>
<li>
<a href="{% url 'job_bank' %}"
data-tooltip="{% trans 'Job Bank' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'job_bank' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name == 'job_bank' %}page{% endif %}">
<i data-lucide="building-2" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name == 'job_bank' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Job Bank" %}</span>
</a>
</li>
<li>
<a href="{% url 'application_list' %}"
data-tooltip="{% trans 'Applications' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'application_list' or request.resolver_match.url_name|slice:':11' == 'application_' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if 'application_' in request.resolver_match.url_name %}page{% endif %}">
<i data-lucide="users" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if 'application_' in request.resolver_match.url_name %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Applications" %}</span>
</a>
</li>
<li>
<a href="{% url 'person_list' %}"
data-tooltip="{% trans 'Applicants' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'person_list' or request.resolver_match.url_name == 'person_detail' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name in 'person_list,person_detail' %}page{% endif %}">
<i data-lucide="user" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name in 'person_list,person_detail' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Applicants" %}</span>
</a>
</li>
<li>
<a href="{% url 'agency_list' %}"
data-tooltip="{% trans 'Agencies' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'agency_list' or request.resolver_match.url_name == 'agency_detail' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name in 'agency_list,agency_detail' %}page{% endif %}">
<i data-lucide="building" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name in 'agency_list,agency_detail' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Agencies" %}</span>
</a>
</li>
<li>
<a href="{% url 'interview_list' %}"
data-tooltip="{% trans 'Interviews' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'interview_list' or request.resolver_match.url_name|slice:':9' == 'interview_' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if 'interview_' in request.resolver_match.url_name %}page{% endif %}">
<i data-lucide="calendar" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if 'interview_' in request.resolver_match.url_name %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Interviews" %}</span>
</a>
</li>
<li>
<a href="{% url 'message_list' %}"
data-tooltip="{% trans 'Messages' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link relative {% if request.resolver_match.url_name == 'message_list' or request.resolver_match.url_name|slice:':8' == 'message_' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if 'message_' in request.resolver_match.url_name %}page{% endif %}">
<i data-lucide="mail" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if 'message_' in request.resolver_match.url_name %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Messages" %}</span>
{% if request.user.get_unread_message_count > 0 %}
<span class="ml-auto bg-temple-red text-white px-2 py-0.5 rounded-full text-xs font-medium nav-badge">
{{ request.user.get_unread_message_count }}
</span>
{% endif %}
</a>
</li>
<li>
<a href="{% url 'kaauh_career' %}"
data-tooltip="{% trans 'Career Page' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'kaauh_career' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name == 'kaauh_career' %}page{% endif %}">
<i data-lucide="globe" class="w-5 h-5 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name == 'kaauh_career' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Career Page" %}</span>
</a>
</li>
</ul>
{% if request.user.is_authenticated and request.user.is_superuser %}
<div class="text-[10px] uppercase tracking-widest text-gray-600 font-bold mt-6 sm:mt-8 mb-3 sm:mb-4 px-2 section-header">
{% trans "System" %}
</div>
<ul class="space-y-1">
<li>
<a href="{% url 'settings' %}"
data-tooltip="{% trans 'Settings' %}"
class="flex items-center px-3 py-2.5 rounded-md transition nav-link {% if request.resolver_match.url_name == 'settings' %}text-white bg-temple-red/10 border-l-4 border-temple-red rounded-l-none{% else %}hover:bg-gray-800 hover:text-white{% endif %}"
aria-current="{% if request.resolver_match.url_name == 'settings' %}page{% endif %}">
<i data-lucide="settings" class="w-4 h-4 nav-icon mr-3 shrink-0 {% if request.resolver_match.url_name == 'settings' %}text-temple-red{% endif %}"></i>
<span class="nav-text">{% trans "Settings" %}</span>
</a>
</li>
</ul>
{% endif %}
</nav>
<!-- Logout Button -->
{% if request.user.is_authenticated %}
<div class="p-3 sm:p-4 border-t border-gray-800 sidebar-footer">
<form method="post" action="{% url 'account_logout'%}">
{% csrf_token %}
<button type="submit"
class="w-full flex items-center justify-center gap-2 bg-gray-800 hover:bg-gray-700 text-white py-2.5 px-4 rounded-lg text-sm transition touch-target">
<i data-lucide="log-out" class="w-4 h-4 shrink-0"></i>
<span class="nav-text">{% trans "Sign Out" %}</span>
</button>
</form>
</div>
{% endif %}
<!-- Powered By Footer -->
<div class="p-3 sm:p-4 border-t border-gray-800 text-center sidebar-footer safe-area-padding-bottom">
<a href="https://tenhal.sa/" class="text-decoration-none block" target="_blank" rel="noopener noreferrer">
<div class="text-[10px] text-gray-600 uppercase tracking-widest">
{% trans "POWERED BY" %} <span class="text-white font-bold">TENHAL</span>
</div>
</a>
</div>
</div>
</aside>
<!-- Main Content -->
<main id="main-content"
class="flex-1 flex flex-col min-h-screen smooth-transition lg:ml-64">
<!-- Header -->
<header class="sticky-header bg-white border-b px-3 sm:px-4 lg:px-8 py-3 flex justify-between items-center shadow-sm safe-area-padding-top">
<div class="flex items-center flex-1 min-w-0">
<!-- Mobile Menu Toggle -->
<button id="menu-toggle"
class="lg:hidden mr-2 sm:mr-4 p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition touch-target shrink-0"
aria-label="{% trans 'Open menu' %}"
aria-expanded="false"
aria-controls="sidebar">
<i data-lucide="menu" class="w-5 h-5"></i>
</button>
<!-- Desktop Sidebar Toggle -->
<button id="sidebar-toggle"
class="hidden lg:flex mr-4 p-2 text-gray-600 hover:text-temple-red transition shrink-0"
title="{% trans 'Toggle Sidebar' %}"
aria-label="{% trans 'Toggle sidebar' %}">
<i data-lucide="panel-left" class="w-5 h-5"></i>
</button>
<!-- Logo/Title -->
<div class="truncate">
<span class="text-temple-red font-bold text-sm sm:text-base lg:text-lg">{% trans "KAAUH ATS" %}</span>
</div>
</div>
<!-- Header Actions -->
<div class="flex items-center gap-1 sm:gap-2 lg:gap-4 shrink-0">
<!-- Fullscreen Toggle (Desktop Only) -->
<button id="fullscreen-toggle"
class="hidden lg:flex p-2 text-gray-600 hover:text-temple-red transition touch-target"
title="{% trans 'Toggle Fullscreen' %}"
aria-label="{% trans 'Toggle fullscreen' %}">
<i data-lucide="maximize" class="w-5 h-5" id="fullscreen-icon"></i>
</button>
<!-- Messages Icon -->
<a href="{% url 'message_list' %}"
class="relative p-2 text-gray-500 hover:text-temple-red transition touch-target"
aria-label="{% trans 'Messages' %}{% if request.user.get_unread_message_count > 0 %} ({{ request.user.get_unread_message_count }} {% trans 'unread' %}){% endif %}">
<i data-lucide="mail" class="w-5 h-5"></i>
{% if request.user.get_unread_message_count > 0 %}
<span class="absolute top-1 right-1 bg-temple-red border-2 border-white w-2.5 h-2.5 rounded-full" aria-hidden="true"></span>
{% endif %}
</a>
<!-- Language Switcher -->
{% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="relative">
{% csrf_token %}
<input name="language" type="hidden" value="ar">
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button class="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition touch-target"
aria-label="{% trans 'Switch to Arabic' %}">
<span aria-hidden="true">🇸🇦</span>
<span class="hidden sm:inline">{% trans "العربية" %}</span>
</button>
</form>
{% else %}
<form action="{% url 'set_language' %}" method="post" class="relative">
{% csrf_token %}
<input name="language" type="hidden" value="en">
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button class="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition touch-target"
aria-label="Switch to English">
<span aria-hidden="true">🇺🇸</span>
<span class="hidden sm:inline">English</span>
</button>
</form>
{% endif %}
<!-- User Menu Dropdown -->
<div class="dropdown relative">
<button class="flex items-center gap-2 p-1 touch-target"
id="user-menu-button"
aria-haspopup="true"
aria-expanded="false"
aria-label="{% trans 'User menu' %}">
{% if user.profile_image %}
<img src="{{ user.profile_image.url }}"
alt="{{ user.get_full_name|default:user.username }}"
class="w-8 h-8 sm:w-9 sm:h-9 rounded-full border shadow-sm object-cover">
{% else %}
<div class="w-8 h-8 sm:w-9 sm:h-9 rounded-full bg-temple-red flex items-center justify-center text-white font-bold text-sm">
{{ user.username|first|upper }}
</div>
{% endif %}
</button>
{% if request.user.is_authenticated %}
<div class="hidden absolute right-0 mt-2 w-48 sm:w-56 bg-white rounded-lg shadow-lg border border-gray-100 py-2 z-50 dropdown-menu"
id="user-menu-dropdown"
role="menu"
aria-labelledby="user-menu-button">
<div class="px-4 py-2 border-b border-gray-100">
<p class="text-sm font-bold text-gray-800 truncate">
{{ user.get_full_name|default:user.username }}
</p>
<p class="text-xs text-gray-500 truncate">{{ user.email }}</p>
</div>
<a href="{% url 'user_detail' request.user.pk %}"
class="block px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 transition"
role="menuitem">
{% trans "Profile" %}
</a>
<hr class="my-2 border-gray-100">
<form method="post" action="{% url 'account_logout'%}">
{% csrf_token %}
<button type="submit"
class="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-gray-100 transition"
role="menuitem">
{% trans "Sign Out" %}
</button>
</form>
</div>
{% endif %}
</div>
</div>
</header>
<!-- Messages/Alerts -->
<div class="p-3 sm:p-4 lg:p-8 space-y-3 sm:space-y-4 flex-1 overflow-x-hidden">
{% if messages %}
<div class="space-y-2 sm:space-y-3" role="alert" aria-live="polite">
{% for message in messages %}
<div class="alert {% if message.tags == 'error' %}bg-red-50 border-red-200 text-red-800{% elif message.tags == 'success' %}bg-green-50 border-green-200 text-green-800{% elif message.tags == 'warning' %}bg-yellow-50 border-yellow-200 text-yellow-800{% else %}bg-blue-50 border-blue-200 text-blue-800{% endif %} border rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 flex items-start sm:items-center justify-between gap-2">
<span class="flex-1 text-sm">{{ message }}</span>
<button type="button"
class="text-gray-400 hover:text-gray-600 p-1 shrink-0 touch-target"
onclick="this.parentElement.remove()"
aria-label="{% trans 'Dismiss' %}">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Page Content -->
{% block content %}{% endblock %}
</div>
<!-- Footer -->
<footer class="mt-auto p-4 sm:p-6 text-xs text-gray-400 text-center border-t border-gray-200 bg-white safe-area-padding-bottom">
&copy; {% now "Y" %} KAAUH ATS. {% trans "All rights reserved." %}
</footer>
</main>
<script>
// Initialize Lucide icons
lucide.createIcons();
// ========================================
// Mobile Sidebar Toggle
// ========================================
const menuToggle = document.getElementById('menu-toggle');
const sidebar = document.getElementById('sidebar');
const sidebarBackdrop = document.getElementById('sidebar-backdrop');
function openMobileSidebar() {
sidebar.classList.remove('-translate-x-full');
sidebar.classList.add('translate-x-0');
sidebarBackdrop.classList.remove('hidden');
document.body.style.overflow = 'hidden';
menuToggle?.setAttribute('aria-expanded', 'true');
}
function closeMobileSidebar() {
sidebar.classList.add('-translate-x-full');
sidebar.classList.remove('translate-x-0');
sidebarBackdrop.classList.add('hidden');
document.body.style.overflow = '';
menuToggle?.setAttribute('aria-expanded', 'false');
}
// Make functions available globally
window.openMobileSidebar = openMobileSidebar;
window.closeMobileSidebar = closeMobileSidebar;
if (menuToggle) {
menuToggle.addEventListener('click', openMobileSidebar);
}
// Close sidebar on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !sidebar.classList.contains('-translate-x-full')) {
closeMobileSidebar();
}
});
// ========================================
// Fullscreen Toggle (Desktop)
// ========================================
const fullscreenToggle = document.getElementById('fullscreen-toggle');
const fullscreenIcon = document.getElementById('fullscreen-icon');
if (fullscreenToggle && fullscreenIcon) {
fullscreenToggle.addEventListener('click', () => {
if (!document.fullscreenElement) {
// Enter fullscreen
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
}
} else {
// Exit fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
}
// Update fullscreen icon on change
document.addEventListener('fullscreenchange', updateFullscreenIcon);
document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
document.addEventListener('mozfullscreenchange', updateFullscreenIcon);
document.addEventListener('MSFullscreenChange', updateFullscreenIcon);
function updateFullscreenIcon() {
if (document.fullscreenElement || document.webkitFullscreenElement ||
document.mozFullScreenElement || document.msFullscreenElement) {
// In fullscreen - show minimize icon
fullscreenIcon?.setAttribute('data-lucide', 'minimize');
fullscreenToggle?.setAttribute('title', '{% trans "Exit Fullscreen" %}');
fullscreenToggle?.setAttribute('aria-label', '{% trans "Exit fullscreen" %}');
} else {
// Not in fullscreen - show maximize icon
fullscreenIcon?.setAttribute('data-lucide', 'maximize');
fullscreenToggle?.setAttribute('title', '{% trans "Toggle Fullscreen" %}');
fullscreenToggle?.setAttribute('aria-label', '{% trans "Toggle fullscreen" %}');
}
// Reinitialize lucide icons
lucide.createIcons();
}
// ========================================
// Desktop Sidebar Collapse/Expand Toggle
// ========================================
const sidebarToggle = document.getElementById('sidebar-toggle');
const mainContent = document.getElementById('main-content');
// Restore sidebar state from localStorage
let isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
// Apply saved state on page load
function applySidebarState() {
if (!sidebar || !mainContent) return;
if (isCollapsed) {
// Collapse sidebar
sidebar.style.width = '80px';
mainContent.style.marginLeft = '80px';
sidebar.classList.add('sidebar-collapsed');
// Hide all text elements
document.querySelectorAll('.sidebar-text, .nav-text, .section-header').forEach(el => {
el.style.display = 'none';
});
// Center icons and remove margins
document.querySelectorAll('.nav-link').forEach(el => {
el.classList.add('justify-center');
el.classList.remove('justify-start');
});
document.querySelectorAll('.nav-icon').forEach(el => {
el.style.marginRight = '0';
});
// Adjust headers
document.querySelectorAll('.sidebar-header').forEach(el => {
el.classList.add('justify-center', 'p-4');
el.classList.remove('p-6', 'sm:p-6');
});
// Adjust nav and footer padding
document.querySelectorAll('.sidebar-nav, .sidebar-footer').forEach(el => {
el.classList.add('px-2');
el.classList.remove('px-3', 'sm:px-4', 'p-3', 'sm:p-4', 'p-4');
});
// Reposition badges
document.querySelectorAll('.nav-badge').forEach(el => {
el.classList.add('hidden');
});
} else {
// Expand sidebar
sidebar.style.width = '256px';
mainContent.style.marginLeft = '256px';
sidebar.classList.remove('sidebar-collapsed');
// Show all text elements
document.querySelectorAll('.sidebar-text, .nav-text, .section-header').forEach(el => {
el.style.display = '';
});
// Left-align icons and add margins
document.querySelectorAll('.nav-link').forEach(el => {
el.classList.remove('justify-center');
el.classList.add('justify-start');
});
document.querySelectorAll('.nav-icon').forEach(el => {
el.style.marginRight = '';
});
// Restore headers
document.querySelectorAll('.sidebar-header').forEach(el => {
el.classList.remove('justify-center', 'p-4');
el.classList.add('p-4', 'sm:p-6');
});
// Restore nav and footer padding
document.querySelectorAll('.sidebar-nav').forEach(el => {
el.classList.remove('px-2');
el.classList.add('px-3', 'sm:px-4');
});
document.querySelectorAll('.sidebar-footer').forEach(el => {
el.classList.remove('px-2');
el.classList.add('p-3', 'sm:p-4');
});
// Restore badge visibility
document.querySelectorAll('.nav-badge').forEach(el => {
el.classList.remove('hidden');
});
}
}
// Apply state on page load (desktop only)
if (window.innerWidth >= 1024 && sidebar && mainContent) {
applySidebarState();
}
// Toggle sidebar on click
if (sidebarToggle && sidebar && mainContent) {
sidebarToggle.addEventListener('click', () => {
isCollapsed = !isCollapsed;
localStorage.setItem('sidebarCollapsed', isCollapsed);
applySidebarState();
});
}
// Reset sidebar state on window resize
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (window.innerWidth < 1024) {
// Mobile: reset inline styles
if (sidebar) {
sidebar.style.width = '';
}
if (mainContent) {
mainContent.style.marginLeft = '';
}
} else {
// Desktop: reapply sidebar state
applySidebarState();
}
}, 250);
});
// ========================================
// User Dropdown Toggle
// ========================================
const userMenuButton = document.getElementById('user-menu-button');
const userMenuDropdown = document.getElementById('user-menu-dropdown');
if (userMenuButton && userMenuDropdown) {
userMenuButton.addEventListener('click', (e) => {
e.stopPropagation();
const isHidden = userMenuDropdown.classList.contains('hidden');
userMenuDropdown.classList.toggle('hidden');
userMenuButton.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!userMenuButton.contains(e.target) && !userMenuDropdown.contains(e.target)) {
userMenuDropdown.classList.add('hidden');
userMenuButton.setAttribute('aria-expanded', 'false');
}
});
// Close dropdown on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !userMenuDropdown.classList.contains('hidden')) {
userMenuDropdown.classList.add('hidden');
userMenuButton.setAttribute('aria-expanded', 'false');
userMenuButton.focus();
}
});
}
// ========================================
// Auto-dismiss alerts after 5 seconds
// ========================================
document.querySelectorAll('.alert').forEach(alert => {
setTimeout(() => {
alert.style.opacity = '0';
alert.style.transform = 'translateY(-20px)';
setTimeout(() => alert.remove(), 300);
}, 5000);
});
// ========================================
// Prevent double-tap zoom on buttons (iOS)
// ========================================
let lastTouchEnd = 0;
document.addEventListener('touchend', (e) => {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
e.preventDefault();
}
lastTouchEnd = now;
}, false);
// ========================================
// Tooltip functionality for collapsed sidebar
// ========================================
const tooltip = document.getElementById('sidebar-tooltip');
// Setup tooltip hover events for all nav links
function setupTooltips() {
const navLinks = document.querySelectorAll('.nav-link[data-tooltip]');
navLinks.forEach(link => {
link.addEventListener('mouseenter', (e) => {
// Only show tooltip if sidebar is collapsed
if (sidebar && sidebar.classList.contains('sidebar-collapsed')) {
const tooltipText = link.getAttribute('data-tooltip');
if (tooltip && tooltipText) {
tooltip.textContent = tooltipText;
tooltip.classList.add('visible');
// Position tooltip next to hovered element
const rect = link.getBoundingClientRect();
// Use requestAnimationFrame to ensure tooltip has been rendered
requestAnimationFrame(() => {
tooltip.style.left = (rect.right + 10) + 'px';
tooltip.style.top = (rect.top + (rect.height / 2) - (tooltip.offsetHeight / 2)) + 'px';
});
}
}
});
link.addEventListener('mouseleave', () => {
if (tooltip) {
tooltip.classList.remove('visible');
}
});
link.addEventListener('mousemove', (e) => {
// Only update position if sidebar is collapsed and tooltip is visible
if (sidebar && sidebar.classList.contains('sidebar-collapsed') && tooltip && tooltip.classList.contains('visible')) {
const rect = link.getBoundingClientRect();
tooltip.style.left = (rect.right + 10) + 'px';
tooltip.style.top = (rect.top + (rect.height / 2) - (tooltip.offsetHeight / 2)) + 'px';
}
});
});
}
// Initialize tooltips after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupTooltips);
} else {
setupTooltips();
}
// ========================================
// Performance: Debounce scroll events
// ========================================
let ticking = false;
function onScroll() {
if (!ticking) {
window.requestAnimationFrame(() => {
// Add scroll-based functionality here if needed
ticking = false;
});
ticking = true;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
</script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
{% block customJS %}{% endblock %}
</body>
</html>