Switch PHP views to Twig and add 2FA/UI enhancements
Migrate many view templates from raw PHP to Twig and modernize UI/UX for 2FA and settings. Controllers updated to provide avatar data and two-factor info (ProfileController, UserController) and SettingsController now includes timezone lists, notification preset selection, cron path, cached update state and rollback availability. ErrorHandler now attempts to render error pages via a new Core\TwigService with a safe fallback to raw PHP views. TwoFactorService generation silences deprecated warnings during QR code creation. Numerous .php view files were removed and replaced with .twig equivalents (2fa setup/verify/backup-codes and many auth, dashboard, domains, errors, layout, users, tags, tld-registry, etc.), and core/TwigService was added. These changes move the app toward a Twig-based templating system, improve 2FA flows, surface avatar images in lists/profiles, and make error rendering more robust.
This commit is contained in:
@@ -1,40 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* Base Layout Template
|
||||
* Contains: HTML structure, meta tags, CSS/JS includes, global stats
|
||||
*/
|
||||
{#
|
||||
# Base Layout Template
|
||||
# Contains: HTML structure, meta tags, CSS/JS includes, global stats
|
||||
#}
|
||||
|
||||
// Get current user ID (used for both notifications and stats)
|
||||
$userId = \Core\Auth::id();
|
||||
|
||||
// Fetch notifications for top nav (available on all pages)
|
||||
if ($userId) {
|
||||
$notificationData = \App\Helpers\LayoutHelper::getNotifications($userId);
|
||||
$recentNotifications = $notificationData['items'];
|
||||
$unreadNotifications = $notificationData['unread_count'];
|
||||
// Update badge in top menu (admin only, uses cached update check data)
|
||||
$updateBadge = \Core\Auth::isAdmin() ? \App\Helpers\LayoutHelper::getUpdateBadgeInfo() : ['show' => false, 'available' => false, 'label' => ''];
|
||||
} else {
|
||||
$recentNotifications = [];
|
||||
$unreadNotifications = 0;
|
||||
$updateBadge = ['show' => false, 'available' => false, 'label' => ''];
|
||||
}
|
||||
|
||||
// Get domain stats for sidebar (available on all pages)
|
||||
if (!isset($domainStats)) {
|
||||
$domainStats = \App\Helpers\LayoutHelper::getDomainStats();
|
||||
}
|
||||
|
||||
// Get application settings from database
|
||||
if (!isset($appName)) {
|
||||
$appSettings = \App\Helpers\LayoutHelper::getAppSettings();
|
||||
$appName = $appSettings['app_name'];
|
||||
$appTimezone = $appSettings['app_timezone'];
|
||||
$appVersion = $appSettings['app_version'];
|
||||
|
||||
// Note: Timezone is now set early in public/index.php (before controllers run)
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -45,24 +13,25 @@ if (!isset($appName)) {
|
||||
<meta name="author" content="Domain Monitor">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Title -->
|
||||
<title><?= $title ?? 'Domain Monitor' ?> - <?= $appName ?></title>
|
||||
{# Title #}
|
||||
<title>{{ title|default('Domain Monitor') }} - {{ appName }}</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
{# Favicon #}
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
{# Tailwind CSS #}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Flag Icons -->
|
||||
{# Flag Icons #}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icons/7.5.0/css/flag-icons.min.css" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Font Awesome -->
|
||||
{# Font Awesome #}
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Tailwind Configuration -->
|
||||
{# Tailwind Configuration #}
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
@@ -81,37 +50,33 @@ if (!isset($appName)) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom Styles -->
|
||||
{# Theme initialization (prevent flash) #}
|
||||
<script>
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
{# Custom Styles #}
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
|
||||
<!-- Custom Page Styles (optional) -->
|
||||
<?php if (isset($customStyles)): ?>
|
||||
<style><?= $customStyles ?></style>
|
||||
<?php endif; ?>
|
||||
{# Custom Page Styles (optional) #}
|
||||
{% if customStyles is defined %}
|
||||
<style>{{ customStyles|raw }}</style>
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
/* Sidebar full height */
|
||||
.sidebar {
|
||||
height: 100vh;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transition: transform 0.3s ease-in-out, background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Mobile sidebar overlay */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 25;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
}
|
||||
.sidebar-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
@@ -133,152 +98,133 @@ if (!isset($appName)) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Active sidebar link */
|
||||
.sidebar-link.active {
|
||||
background: #374151;
|
||||
border-left: 4px solid #4A90E2;
|
||||
/* Sidebar link hover effect */
|
||||
.sidebar-link {
|
||||
position: relative;
|
||||
}
|
||||
.sidebar-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 0;
|
||||
background: linear-gradient(to bottom, #3b82f6, #6366f1);
|
||||
border-radius: 0 3px 3px 0;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
.sidebar-link:hover::before {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
/* Mobile-friendly dropdown widths */
|
||||
@media (max-width: 480px) {
|
||||
#notificationsDropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
right: -0.5rem;
|
||||
}
|
||||
#userDropdown {
|
||||
width: calc(100vw - 2rem);
|
||||
right: -0.5rem;
|
||||
}
|
||||
/* Custom scrollbar for sidebar */
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
.dark .sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 116, 139, 0.3);
|
||||
}
|
||||
.dark .sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-200">
|
||||
|
||||
<?php include __DIR__ . '/top-nav.php'; ?>
|
||||
{% include 'layout/top-nav.twig' %}
|
||||
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div id="sidebarOverlay" class="sidebar-overlay md:hidden" onclick="closeSidebar()"></div>
|
||||
{# Mobile Sidebar Overlay #}
|
||||
<div id="sidebarOverlay" class="fixed inset-0 bg-black/50 z-20 hidden md:hidden" onclick="closeSidebar()"></div>
|
||||
|
||||
<?php include __DIR__ . '/sidebar.php'; ?>
|
||||
{% include 'layout/sidebar.twig' %}
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="md:ml-64 pt-16 min-h-screen bg-gray-50">
|
||||
{# Main Content Area #}
|
||||
<main class="md:ml-64 pt-16 min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div class="p-6">
|
||||
<!-- Flash Messages -->
|
||||
<?php include __DIR__ . '/messages.php'; ?>
|
||||
{# Flash Messages #}
|
||||
{% include 'layout/messages.twig' %}
|
||||
|
||||
<!-- Page Content -->
|
||||
<?php if (isset($content)): ?>
|
||||
<?= $content ?>
|
||||
<?php endif; ?>
|
||||
{# Page Content #}
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global Scripts -->
|
||||
{# Global Scripts #}
|
||||
<script>
|
||||
// Theme toggle function
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const isDark = html.classList.contains('dark');
|
||||
|
||||
if (isDark) {
|
||||
html.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
html.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sidebar on mobile
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
sidebar.classList.toggle('open');
|
||||
overlay.classList.toggle('show');
|
||||
// Prevent body scroll when sidebar is open
|
||||
document.body.style.overflow = sidebar.classList.contains('open') ? 'hidden' : '';
|
||||
overlay.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// Close sidebar (for overlay click and link clicks)
|
||||
|
||||
// Close sidebar on mobile
|
||||
function closeSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Close sidebar when clicking a link (mobile only)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
sidebarLinks.forEach(link => {
|
||||
link.addEventListener('click', function() {
|
||||
if (window.innerWidth < 768) {
|
||||
closeSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle swipe to close sidebar on mobile
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
sidebar.addEventListener('touchstart', function(e) {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
|
||||
sidebar.addEventListener('touchend', function(e) {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeThreshold = 50;
|
||||
if (touchStartX - touchEndX > swipeThreshold) {
|
||||
// Swiped left - close sidebar
|
||||
closeSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive search placeholder
|
||||
const searchInput = document.getElementById('globalSearchInput');
|
||||
if (searchInput) {
|
||||
function updateSearchPlaceholder() {
|
||||
if (window.innerWidth < 640) {
|
||||
searchInput.placeholder = 'Search...';
|
||||
} else {
|
||||
searchInput.placeholder = 'Search domains or lookup WHOIS...';
|
||||
}
|
||||
}
|
||||
updateSearchPlaceholder();
|
||||
window.addEventListener('resize', updateSearchPlaceholder);
|
||||
}
|
||||
});
|
||||
|
||||
// Close all dropdowns except the one specified
|
||||
function closeOtherDropdowns(exceptId) {
|
||||
function closeOtherDropdowns(except) {
|
||||
['userDropdown', 'notificationsDropdown', 'quickActionsDropdown'].forEach(id => {
|
||||
if (id !== exceptId) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.remove('show');
|
||||
if (id !== except) {
|
||||
const dd = document.getElementById(id);
|
||||
if (dd) dd.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle user dropdown
|
||||
function toggleDropdown() {
|
||||
closeOtherDropdowns('userDropdown');
|
||||
document.getElementById('userDropdown').classList.toggle('show');
|
||||
}
|
||||
|
||||
// Toggle notifications dropdown
|
||||
function toggleNotifications() {
|
||||
closeOtherDropdowns('notificationsDropdown');
|
||||
document.getElementById('notificationsDropdown').classList.toggle('show');
|
||||
}
|
||||
|
||||
// Toggle quick actions dropdown
|
||||
function toggleQuickActions() {
|
||||
closeOtherDropdowns('quickActionsDropdown');
|
||||
document.getElementById('quickActionsDropdown').classList.toggle('show');
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdowns = [
|
||||
{ id: 'userDropdown', trigger: '[onclick="toggleDropdown()"]' },
|
||||
{ id: 'notificationsDropdown', trigger: '[onclick="toggleNotifications()"]' },
|
||||
{ id: 'quickActionsDropdown', trigger: '[onclick="toggleQuickActions()"]' }
|
||||
];
|
||||
|
||||
|
||||
dropdowns.forEach(({ id, trigger }) => {
|
||||
const dd = document.getElementById(id);
|
||||
if (dd && dd.classList.contains('show')) {
|
||||
@@ -344,6 +290,7 @@ if (!isset($appName)) {
|
||||
|
||||
function renderSearchResults(data) {
|
||||
let html = '';
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
if (data.domains && data.domains.length > 0) {
|
||||
html += '<div class="p-2">';
|
||||
@@ -360,11 +307,11 @@ if (!isset($appName)) {
|
||||
const colorClass = statusColors[domain.status_color] || 'text-gray-600';
|
||||
|
||||
html += `
|
||||
<a href="/domains/${domain.id}" class="block px-3 py-2 hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<a href="/domains/${domain.id}" class="block px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-900 truncate">${escapeHtml(domain.domain_name)}</p>
|
||||
<p class="text-xs text-gray-500">${escapeHtml(domain.registrar || 'Unknown registrar')}</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">${escapeHtml(domain.domain_name)}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">${escapeHtml(domain.registrar || 'Unknown registrar')}</p>
|
||||
</div>
|
||||
${domain.days_left !== null ? `
|
||||
<div class="ml-3 text-right">
|
||||
@@ -382,11 +329,11 @@ if (!isset($appName)) {
|
||||
// Show WHOIS lookup option if no results and looks like a domain
|
||||
if (data.domains.length === 0 && data.isDomainLike) {
|
||||
html += `
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Domain not in portfolio</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Perform WHOIS lookup for ${escapeHtml(data.query)}</p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Domain not in portfolio</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Perform WHOIS lookup for ${escapeHtml(data.query)}</p>
|
||||
</div>
|
||||
<button onclick="window.location.href='/search?q=${encodeURIComponent(data.query)}'" class="px-3 py-1.5 bg-primary text-white text-xs rounded-lg hover:bg-primary-dark">
|
||||
Lookup
|
||||
@@ -395,14 +342,14 @@ if (!isset($appName)) {
|
||||
</div>
|
||||
`;
|
||||
} else if (data.domains.length === 0) {
|
||||
html += '<div class="p-4 text-center text-sm text-gray-500">No results found</div>';
|
||||
html += '<div class="p-4 text-center text-sm text-gray-500 dark:text-gray-400">No results found</div>';
|
||||
}
|
||||
|
||||
// Add "View all results" link if there are results
|
||||
if (data.domains.length > 0) {
|
||||
html += `
|
||||
<div class="border-t border-gray-200 p-2">
|
||||
<a href="/search?q=${encodeURIComponent(data.query)}" class="block px-3 py-2 text-center text-sm font-medium text-primary hover:bg-gray-50 rounded-lg">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 p-2">
|
||||
<a href="/search?q=${encodeURIComponent(data.query)}" class="block px-3 py-2 text-center text-sm font-medium text-primary hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg">
|
||||
View all results →
|
||||
</a>
|
||||
</div>
|
||||
@@ -419,11 +366,12 @@ if (!isset($appName)) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Custom Page Scripts (optional) -->
|
||||
<?php if (isset($customScripts)): ?>
|
||||
<script><?= $customScripts ?></script>
|
||||
<?php endif; ?>
|
||||
{# Custom Page Scripts (optional) #}
|
||||
{% if customScripts is defined %}
|
||||
<script>{{ customScripts|raw }}</script>
|
||||
{% endif %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<!-- Toast Notifications Container -->
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm">
|
||||
|
||||
<!-- Success Toast -->
|
||||
<?php if (isset($_SESSION['success'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-green-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Success</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['success']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['success']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Error Toast -->
|
||||
<?php if (isset($_SESSION['error'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-red-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-times text-red-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Error</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['error']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['error']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Warning Toast -->
|
||||
<?php if (isset($_SESSION['warning'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-orange-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Warning</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['warning']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['warning']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Info Toast -->
|
||||
<?php if (isset($_SESSION['info'])): ?>
|
||||
<div class="toast bg-white border-l-4 border-blue-500 rounded-lg shadow-lg p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-info text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Info</p>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= htmlspecialchars($_SESSION['info']) ?></p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php unset($_SESSION['info']); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast Auto-Dismiss Script -->
|
||||
<script>
|
||||
// Auto-dismiss toasts after 5 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toasts = document.querySelectorAll('.toast');
|
||||
|
||||
toasts.forEach(toast => {
|
||||
// Add fade-out animation after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
|
||||
// Remove from DOM after animation
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
115
app/Views/layout/messages.twig
Normal file
115
app/Views/layout/messages.twig
Normal file
@@ -0,0 +1,115 @@
|
||||
{# Toast Notifications Container #}
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm">
|
||||
|
||||
{# Success Toast #}
|
||||
{% if flash.success is defined %}
|
||||
<div class="toast bg-white dark:bg-slate-800 border-l-4 border-green-500 rounded-lg shadow-lg dark:shadow-slate-900/50 p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-check text-green-600 dark:text-green-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Success</p>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">{{ flash.success }}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Error Toast #}
|
||||
{% if flash.error is defined %}
|
||||
<div class="toast bg-white dark:bg-slate-800 border-l-4 border-red-500 rounded-lg shadow-lg dark:shadow-slate-900/50 p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-times text-red-600 dark:text-red-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Error</p>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">{{ flash.error }}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Warning Toast #}
|
||||
{% if flash.warning is defined %}
|
||||
<div class="toast bg-white dark:bg-slate-800 border-l-4 border-orange-500 rounded-lg shadow-lg dark:shadow-slate-900/50 p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-600 dark:text-orange-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Warning</p>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">{{ flash.warning }}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Info Toast #}
|
||||
{% if flash.info is defined %}
|
||||
<div class="toast bg-white dark:bg-slate-800 border-l-4 border-blue-500 rounded-lg shadow-lg dark:shadow-slate-900/50 p-4 flex items-start animate-slide-in">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
|
||||
<i class="fas fa-info text-blue-600 dark:text-blue-400 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Info</p>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400 mt-0.5">{{ flash.info }}</p>
|
||||
</div>
|
||||
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300 transition-colors">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{# Toast Auto-Dismiss Script #}
|
||||
<script>
|
||||
// Auto-dismiss toasts after 5 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const toasts = document.querySelectorAll('.toast');
|
||||
|
||||
toasts.forEach(toast => {
|
||||
// Add fade-out animation after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
|
||||
// Remove from DOM after animation
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -1,136 +0,0 @@
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside id="sidebar" class="sidebar fixed left-0 top-0 w-64 bg-gray-900 text-white z-30">
|
||||
<div class="h-full overflow-y-auto flex flex-col">
|
||||
|
||||
<!-- Logo Section -->
|
||||
<div class="px-4 sm:px-5 py-4 border-b border-gray-800 flex items-center justify-between flex-shrink-0">
|
||||
<a href="/" class="flex items-center min-w-0 group">
|
||||
<img src="/assets/logo.svg" alt="Domain Monitor" class="w-9 h-9 mr-3 flex-shrink-0">
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-base font-bold text-white truncate group-hover:text-primary transition-colors">Domain Monitor</h1>
|
||||
<p class="text-xs text-gray-500 truncate">Track your domains</p>
|
||||
</div>
|
||||
</a>
|
||||
<!-- Close button for mobile -->
|
||||
<button onclick="closeSidebar()" class="md:hidden w-9 h-9 flex items-center justify-center text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors flex-shrink-0 ml-2">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="px-4 py-3">
|
||||
<div class="space-y-0.5">
|
||||
<a href="/" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= $_SERVER['REQUEST_URI'] === '/' ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-chart-line text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<a href="/domains" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/domains') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-globe text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Domains</span>
|
||||
</a>
|
||||
|
||||
<a href="/groups" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/groups') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-bell text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Notification Groups</span>
|
||||
</a>
|
||||
|
||||
<a href="/tld-registry" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/tld-registry') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-database text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">TLD Registry</span>
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] !== 'admin'): ?>
|
||||
<span class="ml-auto text-xs bg-gray-700 px-1.5 py-0.5 rounded text-gray-400">View</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<a href="/tags" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/tags') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-tags text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Tag Management</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Tools Section -->
|
||||
<div class="mt-4 pt-3 border-t border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">Tools</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/debug/whois" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/debug/whois') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-search text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">WHOIS Lookup</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Section (Admin Only) -->
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||
<div class="mt-4 pt-3 border-t border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">System</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/settings" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/settings') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-cog text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Settings</span>
|
||||
</a>
|
||||
<a href="/users" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/users') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-users text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Users</span>
|
||||
</a>
|
||||
<a href="/errors" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/errors') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-bug text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Error Logs</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Stats Cards - Pinned to Bottom -->
|
||||
<div class="mt-auto px-4 pb-3 border-t border-gray-800 pt-3">
|
||||
<div class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-2">Quick Stats</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-blue-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-globe text-blue-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Total</span>
|
||||
</div>
|
||||
<span class="text-white font-semibold text-sm"><?= $domainStats['total'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-orange-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-exclamation-triangle text-orange-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs" title="Within <?= $domainStats['expiring_threshold'] ?? 30 ?> days">Expiring</span>
|
||||
</div>
|
||||
<span class="text-orange-400 font-semibold text-sm"><?= $domainStats['expiring_soon'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-md p-2.5 border border-gray-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-7 h-7 bg-green-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-check-circle text-green-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Active</span>
|
||||
</div>
|
||||
<span class="text-green-400 font-semibold text-sm"><?= $domainStats['active'] ?? 0 ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-4 py-3 border-t border-gray-800">
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">© <?= date('Y') ?> <a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="hover:text-blue-300 transition-colors duration-150" title="Visit Domain Monitor on GitHub">Domain Monitor</a></p>
|
||||
<p class="text-xs text-gray-600 mt-0.5">v<?= $appVersion ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
124
app/Views/layout/sidebar.twig
Normal file
124
app/Views/layout/sidebar.twig
Normal file
@@ -0,0 +1,124 @@
|
||||
{# Sidebar Navigation Partial #}
|
||||
<aside id="sidebar" class="sidebar fixed left-0 top-0 w-64 z-30
|
||||
bg-white dark:bg-slate-900
|
||||
border-r border-gray-200 dark:border-slate-800
|
||||
transition-colors duration-200">
|
||||
<div class="h-full overflow-y-auto flex flex-col">
|
||||
|
||||
{# Logo Section #}
|
||||
<div class="h-16 px-4 border-b border-gray-200 dark:border-slate-800 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-3">
|
||||
<img src="{{ base_url }}/assets/logo.svg" alt="{{ appSettings.app_name|default('Domain Monitor') }}" class="h-11 w-auto">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-base font-bold text-gray-800 dark:text-white tracking-tight leading-tight">{{ appSettings.app_name|default('Domain Monitor') }}</span>
|
||||
<span class="text-xs text-gray-400 dark:text-slate-500 font-medium">Track your domains</span>
|
||||
</div>
|
||||
</a>
|
||||
{# Mobile Close Button #}
|
||||
<button onclick="closeSidebar()" class="md:hidden flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition-colors">
|
||||
<i class="fas fa-times text-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Navigation Links #}
|
||||
<nav class="px-3 py-4 flex-1">
|
||||
<div class="space-y-0.5">
|
||||
<a href="/" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active('/') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-chart-line text-sm mr-3 w-4 {{ is_active('/') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<a href="/domains" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/domains') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-globe text-sm mr-3 w-4 {{ is_active_prefix('/domains') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Domains</span>
|
||||
</a>
|
||||
|
||||
<a href="/groups" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/groups') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-bell text-sm mr-3 w-4 {{ is_active_prefix('/groups') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Notification Groups</span>
|
||||
</a>
|
||||
|
||||
<a href="/tld-registry" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/tld-registry') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-database text-sm mr-3 w-4 {{ is_active_prefix('/tld-registry') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">TLD Registry</span>
|
||||
{% if session.role is defined and session.role != 'admin' %}
|
||||
<span class="ml-auto text-xs bg-gray-200 dark:bg-slate-700 px-1.5 py-0.5 rounded text-gray-500 dark:text-slate-400">View</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<a href="/tags" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/tags') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-tags text-sm mr-3 w-4 {{ is_active_prefix('/tags') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Tag Management</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Tools Section #}
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-slate-800">
|
||||
<p class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider px-3 mb-1.5">Tools</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/debug/whois" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/debug/whois') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-search text-sm mr-3 w-4 {{ is_active_prefix('/debug/whois') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">WHOIS Lookup</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# System Section (Admin Only) #}
|
||||
{% if session.role is defined and session.role == 'admin' %}
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-slate-800">
|
||||
<p class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider px-3 mb-1.5">System</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/settings" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/settings') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-cog text-sm mr-3 w-4 {{ is_active_prefix('/settings') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Settings</span>
|
||||
</a>
|
||||
<a href="/users" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/users') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-users text-sm mr-3 w-4 {{ is_active_prefix('/users') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Users</span>
|
||||
</a>
|
||||
<a href="/errors" class="sidebar-link group flex items-center px-3 py-2 rounded-lg transition-all duration-200
|
||||
{{ is_active_prefix('/errors') ? 'bg-blue-50 dark:bg-slate-800 text-blue-700 dark:text-blue-400 font-medium' : 'text-gray-600 dark:text-slate-400 hover:bg-gray-100 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-white' }}">
|
||||
<i class="fas fa-bug text-sm mr-3 w-4 {{ is_active_prefix('/errors') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 dark:text-slate-500 group-hover:text-blue-500 dark:group-hover:text-blue-400' }}"></i>
|
||||
<span class="text-sm">Error Logs</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{# Quick Stats - Compact #}
|
||||
<div class="px-3 pb-2 border-t border-gray-200 dark:border-slate-800 pt-3">
|
||||
<div class="text-xs font-semibold text-gray-400 dark:text-slate-500 uppercase tracking-wider px-2 mb-2">Domain Stats</div>
|
||||
<div class="grid grid-cols-3 gap-1.5 px-1">
|
||||
<div class="bg-gray-50 dark:bg-slate-800 rounded-lg p-2 text-center">
|
||||
<div class="text-base font-bold text-gray-800 dark:text-white">{{ domainStats.total|default(0) }}</div>
|
||||
<div class="text-xs text-gray-400 dark:text-slate-500">Total</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-800 rounded-lg p-2 text-center">
|
||||
<div class="text-base font-bold text-orange-500 dark:text-orange-400">{{ domainStats.expiring_soon|default(0) }}</div>
|
||||
<div class="text-xs text-gray-400 dark:text-slate-500" title="Within {{ domainStats.expiring_threshold|default(30) }} days">Expiring</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-slate-800 rounded-lg p-2 text-center">
|
||||
<div class="text-base font-bold text-emerald-500 dark:text-emerald-400">{{ domainStats.active|default(0) }}</div>
|
||||
<div class="text-xs text-gray-400 dark:text-slate-500">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Footer #}
|
||||
<div class="px-4 py-3 border-t border-gray-200 dark:border-slate-800">
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500 dark:text-slate-500">© {{ "now"|date("Y") }} <a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="text-gray-500 dark:text-slate-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-150" title="Visit {{ appSettings.app_name|default('Domain Monitor') }} on GitHub">{{ appSettings.app_name|default('Domain Monitor') }}</a></p>
|
||||
<p class="text-xs text-gray-400 dark:text-slate-600 mt-0.5">v{{ appSettings.app_version|default(appVersion) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -1,396 +0,0 @@
|
||||
<!-- Top Navigation Bar -->
|
||||
<!-- Notification data ($recentNotifications, $unreadNotifications) loaded in base.php -->
|
||||
<nav class="bg-white border-b border-gray-200 fixed top-0 left-0 md:left-64 right-0 z-20">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Left: Menu button and Page Header -->
|
||||
<div class="flex items-center min-w-0">
|
||||
<button onclick="toggleSidebar()" class="text-gray-500 hover:text-gray-700 focus:outline-none focus:text-gray-700 md:hidden mr-4">
|
||||
<i class="fas fa-bars text-xl"></i>
|
||||
</button>
|
||||
|
||||
<!-- Page Title & Description -->
|
||||
<div class="hidden md:block">
|
||||
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
||||
<?php if (isset($pageIcon)): ?>
|
||||
<i class="<?= $pageIcon ?> text-primary mr-2"></i>
|
||||
<?php endif; ?>
|
||||
<?= $pageTitle ?? $title ?? 'Dashboard' ?>
|
||||
</h2>
|
||||
<?php if (isset($pageDescription)): ?>
|
||||
<p class="text-sm text-gray-600 mt-0.5"><?= $pageDescription ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Search Bar -->
|
||||
<div class="flex-1 max-w-2xl mx-2 sm:mx-4 lg:mx-8">
|
||||
<form action="/search" method="GET" class="relative" id="globalSearchForm">
|
||||
<input type="text"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
class="w-full pl-9 sm:pl-10 pr-3 sm:pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
id="globalSearchInput"
|
||||
autocomplete="off">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"></i>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div id="searchDropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 max-h-96 overflow-y-auto z-50">
|
||||
<!-- Loading state -->
|
||||
<div id="searchLoading" class="hidden p-4 text-center">
|
||||
<i class="fas fa-spinner fa-spin text-primary"></i>
|
||||
<p class="text-sm text-gray-600 mt-2">Searching...</p>
|
||||
</div>
|
||||
|
||||
<!-- Results will be inserted here -->
|
||||
<div id="searchResults"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right: Actions & User -->
|
||||
<div class="flex items-center space-x-1 sm:space-x-2">
|
||||
<!-- Update available badge (admin only, when enabled in settings) -->
|
||||
<?php if (!empty($updateBadge['show'])): ?>
|
||||
<a href="/settings#updates" class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-100 text-amber-800 hover:bg-amber-200 transition-colors text-xs font-semibold whitespace-nowrap" title="An update is available">
|
||||
<i class="fas fa-cloud-download-alt"></i>
|
||||
<span>Update<?= !empty($updateBadge['label']) ? ' ' . htmlspecialchars($updateBadge['label']) : '' ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Quick Actions Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleQuickActions()" title="Quick Actions" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<div id="quickActionsDropdown" class="dropdown-menu absolute right-0 mt-2 w-52 bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden py-1">
|
||||
<div class="px-3 py-2 border-b border-gray-100">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Quick Actions</p>
|
||||
</div>
|
||||
<a href="/domains/create" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-blue-50 hover:text-primary transition-colors">
|
||||
<div class="w-7 h-7 bg-blue-50 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-globe text-blue-600 text-xs"></i>
|
||||
</div>
|
||||
Add Domain
|
||||
</a>
|
||||
<a href="/groups/create" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-green-50 hover:text-green-700 transition-colors">
|
||||
<div class="w-7 h-7 bg-green-50 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-bell text-green-600 text-xs"></i>
|
||||
</div>
|
||||
Create Group
|
||||
</a>
|
||||
<a href="/tags" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-purple-50 hover:text-purple-700 transition-colors">
|
||||
<div class="w-7 h-7 bg-purple-50 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-tag text-purple-600 text-xs"></i>
|
||||
</div>
|
||||
Create Tag
|
||||
</a>
|
||||
<a href="/debug/whois" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-700 transition-colors">
|
||||
<div class="w-7 h-7 bg-indigo-50 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-search text-indigo-600 text-xs"></i>
|
||||
</div>
|
||||
WHOIS Lookup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleNotifications()" title="Notifications" class="relative flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-bell"></i>
|
||||
<?php if ($unreadNotifications > 0): ?>
|
||||
<span class="absolute top-1 right-1 flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-orange-500"></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
|
||||
<!-- Notifications Dropdown -->
|
||||
<div id="notificationsDropdown" class="dropdown-menu absolute right-0 mt-2 w-80 sm:w-96 bg-white rounded-lg shadow-xl border border-gray-200 max-h-[32rem] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900">Notifications</h3>
|
||||
<?php if ($unreadNotifications > 0): ?>
|
||||
<span id="dropdownHeaderBadge" class="px-2 py-0.5 bg-orange-100 text-orange-700 text-xs font-semibold rounded"><?= $unreadNotifications ?> new</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications List (Scrollable) -->
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
<?php if (!empty($recentNotifications)): ?>
|
||||
<?php foreach ($recentNotifications as $notif): ?>
|
||||
<?php
|
||||
// Build the click URL: update_available → settings#updates; domain → domain page; else mark as read only
|
||||
$hasDomain = !empty($notif['domain_id']);
|
||||
if ($notif['type'] === 'update_available') {
|
||||
$notifUrl = '/notifications/' . $notif['id'] . '/mark-read?redirect=settings';
|
||||
} elseif ($hasDomain) {
|
||||
$notifUrl = '/notifications/' . $notif['id'] . '/mark-read?redirect=domain&domain_id=' . $notif['domain_id'];
|
||||
} else {
|
||||
$notifUrl = '/notifications/' . $notif['id'] . '/mark-read';
|
||||
}
|
||||
?>
|
||||
<div class="px-4 py-3 hover:bg-gray-50 border-b border-gray-100 bg-blue-50 transition-colors notification-item" data-id="<?= $notif['id'] ?>">
|
||||
<div class="flex items-start space-x-3">
|
||||
<?php $loginData = $notif['login_data'] ?? null; ?>
|
||||
<?php if ($loginData && $notif['type'] === 'session_failed'): ?>
|
||||
<!-- Failed login notification -->
|
||||
<a href="<?= $notifUrl ?>" class="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
<?php if (($loginData['country_code'] ?? 'xx') !== 'xx'): ?>
|
||||
<span class="fi fi-<?= strtolower($loginData['country_code']) ?> text-base rounded-sm"></span>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-shield-alt text-red-600 text-sm"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<a href="<?= $notifUrl ?>" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-red-700"><?= htmlspecialchars($notif['title']) ?></p>
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-0.5">
|
||||
<?= htmlspecialchars(\App\Helpers\LayoutHelper::formatLoginDropdown($loginData)) ?>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-<?= htmlspecialchars($loginData['device_icon'] ?? 'desktop') ?> mr-0.5"></i>
|
||||
<?= htmlspecialchars($loginData['reason'] ?? 'Failed') ?> · <?= $notif['time_ago'] ?>
|
||||
</p>
|
||||
</a>
|
||||
<?php elseif ($loginData && $notif['type'] === 'session_new'): ?>
|
||||
<!-- Session notification with flag icon -->
|
||||
<a href="<?= $notifUrl ?>" class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
<?php if ($loginData['country_code'] !== 'xx'): ?>
|
||||
<span class="fi fi-<?= strtolower($loginData['country_code']) ?> text-base rounded-sm"></span>
|
||||
<?php else: ?>
|
||||
<i class="fas fa-sign-in-alt text-blue-600 text-sm"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<a href="<?= $notifUrl ?>" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($notif['title']) ?></p>
|
||||
<span class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-0.5">
|
||||
<?= htmlspecialchars(\App\Helpers\LayoutHelper::formatLoginDropdown($loginData)) ?>
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<i class="fas fa-<?= htmlspecialchars($loginData['device_icon'] ?? 'desktop') ?> mr-0.5"></i>
|
||||
<?= htmlspecialchars($loginData['method'] ?? 'Login') ?> · <?= $notif['time_ago'] ?>
|
||||
</p>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<!-- Standard notification -->
|
||||
<a href="<?= $notifUrl ?>" class="w-8 h-8 bg-<?= $notif['color'] ?>-100 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
<i class="fas fa-<?= $notif['icon'] ?> text-<?= $notif['color'] ?>-600 text-sm"></i>
|
||||
</a>
|
||||
<a href="<?= $notifUrl ?>" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-gray-900"><?= htmlspecialchars($notif['title']) ?></p>
|
||||
<span class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-0.5"><?= htmlspecialchars($notif['message']) ?></p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
<?= $notif['time_ago'] ?>
|
||||
<?php if ($hasDomain): ?>
|
||||
<span class="text-primary ml-1"><i class="fas fa-external-link-alt text-[10px]"></i> View domain</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<button onclick="event.stopPropagation(); markNotifRead(<?= $notif['id'] ?>, this)"
|
||||
class="w-7 h-7 flex items-center justify-center text-gray-400 hover:text-green-600 hover:bg-green-50 rounded transition-colors flex-shrink-0"
|
||||
title="Mark as read">
|
||||
<i class="fas fa-check text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-bell-slash text-gray-300 text-3xl mb-2"></i>
|
||||
<p class="text-sm text-gray-600">No new notifications</p>
|
||||
<p class="text-xs text-gray-400 mt-0.5">You're all caught up!</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Footer - View All Button -->
|
||||
<div class="px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<a href="/notifications" class="block text-center text-sm font-medium text-primary hover:text-primary-dark">
|
||||
View All Notifications
|
||||
<i class="fas fa-arrow-right ml-1 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="hidden md:block h-8 w-px bg-gray-300"></div>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleDropdown()" class="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded-lg transition-colors duration-150 focus:outline-none">
|
||||
<?php
|
||||
// Get user data for avatar
|
||||
$userModel = new \App\Models\User();
|
||||
$user = $userModel->find($_SESSION['user_id'] ?? 0);
|
||||
$avatar = $user ? \App\Helpers\AvatarHelper::getAvatar($user, 36) : null;
|
||||
?>
|
||||
<?php if ($avatar && ($avatar['type'] === 'uploaded' || $avatar['type'] === 'gravatar')): ?>
|
||||
<img src="<?= htmlspecialchars($avatar['url']) ?>"
|
||||
alt="<?= htmlspecialchars($avatar['alt']) ?>"
|
||||
class="w-9 h-9 rounded-full object-cover"
|
||||
loading="lazy">
|
||||
<?php else: ?>
|
||||
<div class="w-9 h-9 rounded-full bg-primary flex items-center justify-center text-white font-semibold">
|
||||
<?= strtoupper(substr($_SESSION['username'] ?? 'A', 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="hidden lg:block text-left">
|
||||
<p class="text-sm font-medium text-gray-700"><?= htmlspecialchars($_SESSION['full_name'] ?? $_SESSION['username'] ?? 'User') ?></p>
|
||||
<p class="text-xs text-gray-500"><?= htmlspecialchars($_SESSION['email'] ?? '') ?></p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 text-xs hidden md:block"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div id="userDropdown" class="dropdown-menu absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden pb-2">
|
||||
<!-- Welcome Header -->
|
||||
<div class="px-4 py-4 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-gray-100">
|
||||
<div class="text-center">
|
||||
<div class="relative w-12 h-12 mx-auto mb-2">
|
||||
<?php if ($avatar && ($avatar['type'] === 'uploaded' || $avatar['type'] === 'gravatar')): ?>
|
||||
<img src="<?= htmlspecialchars($avatar['url']) ?>"
|
||||
alt="<?= htmlspecialchars($avatar['alt']) ?>"
|
||||
class="w-12 h-12 rounded-full object-cover"
|
||||
loading="lazy">
|
||||
<?php else: ?>
|
||||
<div class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-white font-bold text-lg">
|
||||
<?= strtoupper(substr($_SESSION['username'] ?? 'A', 0, 1)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<!-- Online status dot -->
|
||||
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900">Welcome back!</p>
|
||||
<p class="text-xs text-gray-500 mt-1"><?= htmlspecialchars($_SESSION['full_name'] ?? $_SESSION['username'] ?? 'User') ?></p>
|
||||
<!-- Role indicator -->
|
||||
<div class="mt-2">
|
||||
<span class="inline-flex items-center px-2 py-1 bg-<?= $_SESSION['role'] === 'admin' ? 'amber' : 'blue' ?>-100 text-<?= $_SESSION['role'] === 'admin' ? 'amber' : 'blue' ?>-700 text-xs font-medium rounded-full">
|
||||
<i class="fas fa-<?= $_SESSION['role'] === 'admin' ? 'crown' : 'user' ?> mr-1"></i>
|
||||
<?= ucfirst($_SESSION['role'] ?? 'user') ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/profile#profile" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-user-circle w-5 text-gray-400 mr-3"></i>
|
||||
My Profile
|
||||
</a>
|
||||
|
||||
<a href="/profile#security" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-cog w-5 text-gray-400 mr-3"></i>
|
||||
Account Settings
|
||||
</a>
|
||||
|
||||
<a href="/notifications" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fas fa-bell w-5 text-gray-400 mr-3"></i>
|
||||
Notifications
|
||||
<?php if ($unreadNotifications > 0): ?>
|
||||
<span id="userDropdownNotifBadge" class="ml-auto px-2 py-0.5 bg-orange-500 text-white text-xs font-bold rounded-full">
|
||||
<?= $unreadNotifications ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
|
||||
<a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors duration-150">
|
||||
<i class="fab fa-github w-5 text-gray-400 mr-3"></i>
|
||||
Help & Support
|
||||
<i class="fas fa-external-link-alt ml-auto text-xs text-gray-400"></i>
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 my-1"></div>
|
||||
|
||||
<a href="/logout" class="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors duration-150">
|
||||
<i class="fas fa-sign-out-alt w-5 mr-3"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Notification AJAX handler -->
|
||||
<script>
|
||||
function markNotifRead(notifId, btn) {
|
||||
fetch('/notifications/' + notifId + '/mark-read?ajax=1', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('Request failed');
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
|
||||
const newCount = data.unread_count ?? 0;
|
||||
|
||||
// Remove the notification item from dropdown
|
||||
const item = btn.closest('.notification-item');
|
||||
if (item) {
|
||||
const scrollable = document.querySelector('#notificationsDropdown .max-h-96');
|
||||
const isLast = scrollable && scrollable.querySelectorAll('.notification-item').length <= 1;
|
||||
|
||||
if (isLast && scrollable) {
|
||||
scrollable.style.transition = 'opacity 0.2s';
|
||||
scrollable.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
scrollable.innerHTML = '<div class="px-4 py-8 text-center">' +
|
||||
'<i class="fas fa-bell-slash text-gray-300 text-3xl mb-2"></i>' +
|
||||
'<p class="text-sm text-gray-600">No new notifications</p>' +
|
||||
'<p class="text-xs text-gray-400 mt-0.5">You\'re all caught up!</p>' +
|
||||
'</div>';
|
||||
scrollable.style.opacity = '1';
|
||||
}, 200);
|
||||
} else {
|
||||
item.style.transition = 'opacity 0.2s, max-height 0.3s';
|
||||
item.style.opacity = '0';
|
||||
item.style.maxHeight = '0';
|
||||
item.style.overflow = 'hidden';
|
||||
item.style.padding = '0';
|
||||
item.style.margin = '0';
|
||||
setTimeout(() => item.remove(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all badges using server-returned count
|
||||
const headerBadge = document.getElementById('dropdownHeaderBadge');
|
||||
const userBadge = document.getElementById('userDropdownNotifBadge');
|
||||
const bellDot = document.querySelector('[onclick="toggleNotifications()"] .absolute.top-1');
|
||||
|
||||
if (newCount <= 0) {
|
||||
if (headerBadge) headerBadge.remove();
|
||||
if (userBadge) userBadge.remove();
|
||||
if (bellDot) bellDot.remove();
|
||||
} else {
|
||||
if (headerBadge) headerBadge.textContent = newCount + ' new';
|
||||
if (userBadge) userBadge.textContent = newCount;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/notifications/' + notifId + '/mark-read';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
373
app/Views/layout/top-nav.twig
Normal file
373
app/Views/layout/top-nav.twig
Normal file
@@ -0,0 +1,373 @@
|
||||
<!-- Top Navigation Bar -->
|
||||
<nav class="h-16 bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-800 fixed top-0 left-0 md:left-64 right-0 z-20 transition-colors duration-200">
|
||||
<div class="h-full px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-full">
|
||||
<!-- Left: Menu button and Page Header -->
|
||||
<div class="flex items-center min-w-0">
|
||||
<button onclick="toggleSidebar()" class="flex md:hidden items-center justify-center w-10 h-10 -ml-2 mr-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition-colors focus:outline-none">
|
||||
<i class="fas fa-bars text-xl"></i>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
{% if pageIcon is defined %}
|
||||
<div class="hidden sm:flex items-center justify-center w-11 h-11 bg-primary/10 dark:bg-primary/20 rounded-xl">
|
||||
<i class="{{ pageIcon }} text-primary text-xl"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-lg md:text-xl font-bold text-gray-800 dark:text-white truncate">
|
||||
{{ pageTitle|default(title)|default('Dashboard') }}
|
||||
</h2>
|
||||
{% if pageDescription is defined %}
|
||||
<p class="hidden sm:block text-sm text-gray-600 dark:text-slate-400 truncate">{{ pageDescription }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Search Bar -->
|
||||
<div class="flex-1 max-w-md mx-4 lg:mx-6">
|
||||
<form action="/search" method="GET" class="relative hidden md:block" id="globalSearchForm">
|
||||
<input type="text"
|
||||
name="q"
|
||||
placeholder="Search domains or lookup WHOIS..."
|
||||
class="w-full pl-9 pr-3 py-1.5 border border-gray-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-slate-500 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm transition-colors duration-200"
|
||||
id="globalSearchInput"
|
||||
autocomplete="off">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-slate-500 text-sm"></i>
|
||||
|
||||
<div id="searchDropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 max-h-96 overflow-y-auto z-50">
|
||||
<div id="searchLoading" class="hidden p-4 text-center">
|
||||
<i class="fas fa-spinner fa-spin text-primary"></i>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400 mt-2">Searching...</p>
|
||||
</div>
|
||||
<div id="searchResults"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Right: Actions & User -->
|
||||
<div class="flex items-center space-x-2">
|
||||
{% if updateBadge.show|default(false) %}
|
||||
<a href="/settings#updates" class="hidden sm:flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-amber-100 dark:bg-amber-500/20 text-amber-800 dark:text-amber-400 hover:bg-amber-200 dark:hover:bg-amber-500/30 transition-colors text-xs font-semibold whitespace-nowrap" title="An update is available">
|
||||
<i class="fas fa-cloud-download-alt"></i>
|
||||
<span>Update{{ updateBadge.label ? ' ' ~ updateBadge.label : '' }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleQuickActions()" title="Quick Actions" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<div id="quickActionsDropdown" class="dropdown-menu absolute right-0 mt-2 w-52 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 overflow-hidden py-1">
|
||||
<div class="px-3 py-2 border-b border-gray-100 dark:border-slate-700">
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wide">Quick Actions</p>
|
||||
</div>
|
||||
<a href="/domains/create" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-blue-50 dark:hover:bg-blue-500/10 hover:text-primary transition-colors">
|
||||
<div class="w-7 h-7 bg-blue-50 dark:bg-blue-500/20 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-globe text-blue-600 dark:text-blue-400 text-xs"></i>
|
||||
</div>
|
||||
Add Domain
|
||||
</a>
|
||||
<a href="/groups/create" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-green-50 dark:hover:bg-green-500/10 hover:text-green-700 dark:hover:text-green-400 transition-colors">
|
||||
<div class="w-7 h-7 bg-green-50 dark:bg-green-500/20 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-bell text-green-600 dark:text-green-400 text-xs"></i>
|
||||
</div>
|
||||
Create Group
|
||||
</a>
|
||||
<a href="/tags" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-purple-50 dark:hover:bg-purple-500/10 hover:text-purple-700 dark:hover:text-purple-400 transition-colors">
|
||||
<div class="w-7 h-7 bg-purple-50 dark:bg-purple-500/20 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-tag text-purple-600 dark:text-purple-400 text-xs"></i>
|
||||
</div>
|
||||
Create Tag
|
||||
</a>
|
||||
<a href="/debug/whois" class="flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-slate-300 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 hover:text-indigo-700 dark:hover:text-indigo-400 transition-colors">
|
||||
<div class="w-7 h-7 bg-indigo-50 dark:bg-indigo-500/20 rounded-md flex items-center justify-center mr-3">
|
||||
<i class="fas fa-search text-indigo-600 dark:text-indigo-400 text-xs"></i>
|
||||
</div>
|
||||
WHOIS Lookup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark/Light Mode Toggle -->
|
||||
<button onclick="toggleTheme()" id="themeToggle" title="Toggle theme" class="flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-moon dark:hidden"></i>
|
||||
<i class="fas fa-sun hidden dark:inline"></i>
|
||||
</button>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleNotifications()" title="Notifications" class="relative flex items-center justify-center w-9 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-slate-400 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition-colors duration-150">
|
||||
<i class="fas fa-bell"></i>
|
||||
{% if unreadNotifications > 0 %}
|
||||
<span class="absolute top-1 right-1 flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-orange-500"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
|
||||
<!-- Notifications Dropdown -->
|
||||
<div id="notificationsDropdown" class="dropdown-menu absolute right-0 mt-2 w-80 sm:w-96 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 max-h-[32rem] overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Notifications</h3>
|
||||
{% if unreadNotifications > 0 %}
|
||||
<span id="dropdownHeaderBadge" class="px-2 py-0.5 bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-400 text-xs font-semibold rounded">{{ unreadNotifications }} new</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-96 overflow-y-auto">
|
||||
{% if recentNotifications is not empty %}
|
||||
{% for notif in recentNotifications %}
|
||||
{% set hasDomain = notif.domain_id is defined and notif.domain_id %}
|
||||
{% if notif.type == 'update_available' %}
|
||||
{% set notifUrl = '/notifications/' ~ notif.id ~ '/mark-read?redirect=settings' %}
|
||||
{% elseif hasDomain %}
|
||||
{% set notifUrl = '/notifications/' ~ notif.id ~ '/mark-read?redirect=domain&domain_id=' ~ notif.domain_id %}
|
||||
{% else %}
|
||||
{% set notifUrl = '/notifications/' ~ notif.id ~ '/mark-read' %}
|
||||
{% endif %}
|
||||
{% set loginData = notif.login_data|default(null) %}
|
||||
<div class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-slate-700 border-b border-gray-100 dark:border-slate-700 bg-blue-50 dark:bg-blue-900/20 transition-colors notification-item" data-id="{{ notif.id }}">
|
||||
<div class="flex items-start space-x-3">
|
||||
{% if loginData and notif.type == 'session_failed' %}
|
||||
<a href="{{ notifUrl }}" class="w-8 h-8 bg-red-100 dark:bg-red-900/30 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
{% if (loginData.country_code|default('xx')) != 'xx' %}
|
||||
<span class="fi fi-{{ loginData.country_code|lower }} text-base rounded-sm"></span>
|
||||
{% else %}
|
||||
<i class="fas fa-shield-alt text-red-600 dark:text-red-400 text-sm"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="{{ notifUrl }}" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-red-700 dark:text-red-400">{{ notif.title }}</p>
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5">
|
||||
{{ format_login_dropdown(loginData) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-slate-500 mt-1">
|
||||
<i class="fas fa-{{ loginData.device_icon|default('desktop') }} mr-0.5"></i>
|
||||
{{ loginData.reason|default('Failed') }} · {{ notif.time_ago }}
|
||||
</p>
|
||||
</a>
|
||||
{% elseif loginData and notif.type == 'session_new' %}
|
||||
<a href="{{ notifUrl }}" class="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
{% if loginData.country_code != 'xx' %}
|
||||
<span class="fi fi-{{ loginData.country_code|lower }} text-base rounded-sm"></span>
|
||||
{% else %}
|
||||
<i class="fas fa-sign-in-alt text-blue-600 dark:text-blue-400 text-sm"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="{{ notifUrl }}" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ notif.title }}</p>
|
||||
<span class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5">
|
||||
{{ format_login_dropdown(loginData) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 dark:text-slate-500 mt-1">
|
||||
<i class="fas fa-{{ loginData.device_icon|default('desktop') }} mr-0.5"></i>
|
||||
{{ loginData.method|default('Login') }} · {{ notif.time_ago }}
|
||||
</p>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ notifUrl }}" class="w-8 h-8 bg-{{ notif.color }}-100 dark:bg-{{ notif.color }}-900/30 rounded-lg flex items-center justify-center flex-shrink-0 hover:opacity-80 transition-opacity">
|
||||
<i class="fas fa-{{ notif.icon }} text-{{ notif.color }}-600 dark:text-{{ notif.color }}-400 text-sm"></i>
|
||||
</a>
|
||||
<a href="{{ notifUrl }}" class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white">{{ notif.title }}</p>
|
||||
<span class="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-slate-400 mt-0.5">{{ notif.message }}</p>
|
||||
<p class="text-xs text-gray-400 dark:text-slate-500 mt-1">
|
||||
{{ notif.time_ago }}
|
||||
{% if hasDomain %}
|
||||
<span class="text-primary ml-1"><i class="fas fa-external-link-alt text-[10px]"></i> View domain</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
<button onclick="event.stopPropagation(); markNotifRead({{ notif.id }}, this)"
|
||||
class="w-7 h-7 flex items-center justify-center text-gray-400 dark:text-slate-500 hover:text-green-600 dark:hover:text-green-400 hover:bg-green-50 dark:hover:bg-green-500/10 rounded transition-colors flex-shrink-0"
|
||||
title="Mark as read">
|
||||
<i class="fas fa-check text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-bell-slash text-gray-300 dark:text-slate-600 text-3xl mb-2"></i>
|
||||
<p class="text-sm text-gray-600 dark:text-slate-400">No new notifications</p>
|
||||
<p class="text-xs text-gray-400 dark:text-slate-500 mt-0.5">You're all caught up!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 border-t border-gray-200 dark:border-slate-700 bg-gray-50 dark:bg-slate-900">
|
||||
<a href="/notifications" class="block text-center text-sm font-medium text-primary hover:text-primary-dark">
|
||||
View All Notifications
|
||||
<i class="fas fa-arrow-right ml-1 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block h-8 w-px bg-gray-300 dark:bg-slate-700"></div>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="relative">
|
||||
<button onclick="toggleDropdown()" class="flex items-center space-x-3 p-2 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-lg transition-colors duration-150 focus:outline-none">
|
||||
{% if avatar and (avatar.type == 'uploaded' or avatar.type == 'gravatar') %}
|
||||
<img src="{{ avatar.url }}"
|
||||
alt="{{ avatar.alt|default('User avatar') }}"
|
||||
class="w-9 h-9 rounded-full object-cover"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-9 h-9 rounded-full bg-primary flex items-center justify-center text-white font-semibold">
|
||||
{{ (auth.username|default('A'))|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="hidden lg:block text-left">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-white">{{ auth.fullName|default(auth.username)|default('User') }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400">{{ session.email|default('') }}</p>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-gray-400 dark:text-slate-500 text-xs hidden md:block"></i>
|
||||
</button>
|
||||
|
||||
<div id="userDropdown" class="dropdown-menu absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-lg shadow-lg border border-gray-200 dark:border-slate-700 overflow-hidden pb-2">
|
||||
<div class="px-4 py-4 border-b border-gray-200 dark:border-slate-700 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-slate-900 dark:to-slate-800">
|
||||
<div class="text-center">
|
||||
<div class="relative w-12 h-12 mx-auto mb-2">
|
||||
{% if avatar and (avatar.type == 'uploaded' or avatar.type == 'gravatar') %}
|
||||
<img src="{{ avatar.url }}"
|
||||
alt="{{ avatar.alt|default('User avatar') }}"
|
||||
class="w-12 h-12 rounded-full object-cover"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-white font-bold text-lg">
|
||||
{{ (auth.username|default('A'))|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white dark:border-slate-800 flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-white">Welcome back!</p>
|
||||
<p class="text-xs text-gray-500 dark:text-slate-400 mt-1">{{ auth.fullName|default(auth.username)|default('User') }}</p>
|
||||
<div class="mt-2">
|
||||
{{ role_badge(auth.role|default('user'), 'xs') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/profile#profile" class="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors duration-150">
|
||||
<i class="fas fa-user-circle w-5 text-gray-400 dark:text-slate-500 mr-3"></i>
|
||||
My Profile
|
||||
</a>
|
||||
|
||||
<a href="/profile#security" class="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors duration-150">
|
||||
<i class="fas fa-cog w-5 text-gray-400 dark:text-slate-500 mr-3"></i>
|
||||
Account Settings
|
||||
</a>
|
||||
|
||||
<a href="/notifications" class="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors duration-150">
|
||||
<i class="fas fa-bell w-5 text-gray-400 dark:text-slate-500 mr-3"></i>
|
||||
Notifications
|
||||
{% if unreadNotifications > 0 %}
|
||||
<span id="userDropdownNotifBadge" class="ml-auto px-2 py-0.5 bg-orange-500 text-white text-xs font-bold rounded-full">
|
||||
{{ unreadNotifications }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-slate-700 my-1"></div>
|
||||
|
||||
<a href="https://github.com/Hosteroid/domain-monitor" target="_blank" class="flex items-center px-4 py-2 text-sm text-gray-700 dark:text-slate-300 hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors duration-150">
|
||||
<i class="fab fa-github w-5 text-gray-400 dark:text-slate-500 mr-3"></i>
|
||||
Help & Support
|
||||
<i class="fas fa-external-link-alt ml-auto text-xs text-gray-400 dark:text-slate-500"></i>
|
||||
</a>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-slate-700 my-1"></div>
|
||||
|
||||
<a href="/logout" class="flex items-center px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-150">
|
||||
<i class="fas fa-sign-out-alt w-5 mr-3"></i>
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
function markNotifRead(notifId, btn) {
|
||||
fetch('/notifications/' + notifId + '/mark-read?ajax=1', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('Request failed');
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
|
||||
const newCount = data.unread_count ?? 0;
|
||||
|
||||
const item = btn.closest('.notification-item');
|
||||
if (item) {
|
||||
const scrollable = document.querySelector('#notificationsDropdown .max-h-96');
|
||||
const isLast = scrollable && scrollable.querySelectorAll('.notification-item').length <= 1;
|
||||
|
||||
if (isLast && scrollable) {
|
||||
scrollable.style.transition = 'opacity 0.2s';
|
||||
scrollable.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
scrollable.innerHTML = '<div class="px-4 py-8 text-center">' +
|
||||
'<i class="fas fa-bell-slash text-gray-300 dark:text-slate-600 text-3xl mb-2"></i>' +
|
||||
'<p class="text-sm text-gray-600 dark:text-slate-400">No new notifications</p>' +
|
||||
'<p class="text-xs text-gray-400 dark:text-slate-500 mt-0.5">You\'re all caught up!</p>' +
|
||||
'</div>';
|
||||
scrollable.style.opacity = '1';
|
||||
}, 200);
|
||||
} else {
|
||||
item.style.transition = 'opacity 0.2s, max-height 0.3s';
|
||||
item.style.opacity = '0';
|
||||
item.style.maxHeight = '0';
|
||||
item.style.overflow = 'hidden';
|
||||
item.style.padding = '0';
|
||||
item.style.margin = '0';
|
||||
setTimeout(() => item.remove(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
const headerBadge = document.getElementById('dropdownHeaderBadge');
|
||||
const userBadge = document.getElementById('userDropdownNotifBadge');
|
||||
const bellDot = document.querySelector('[onclick="toggleNotifications()"] .absolute.top-1');
|
||||
|
||||
if (newCount <= 0) {
|
||||
if (headerBadge) headerBadge.remove();
|
||||
if (userBadge) userBadge.remove();
|
||||
if (bellDot) bellDot.remove();
|
||||
} else {
|
||||
if (headerBadge) headerBadge.textContent = newCount + ' new';
|
||||
if (userBadge) userBadge.textContent = newCount;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/notifications/' + notifId + '/mark-read';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user