256 lines
12 KiB
Twig
256 lines
12 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
|
<script>
|
|
tailwind = { config: { devtools: false } };
|
|
</script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
{% block styles %}{% endblock %}
|
|
<style>
|
|
.gradient-bg {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
.mobile-menu {
|
|
display: none;
|
|
}
|
|
.mobile-menu.active {
|
|
display: block;
|
|
}
|
|
.language-dropdown {
|
|
display: none;
|
|
}
|
|
.language-dropdown.active {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50">
|
|
{% if user %}
|
|
<!-- Navigation -->
|
|
<nav class="gradient-bg shadow-lg">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0 flex items-center">
|
|
<i class="fas fa-shield-alt text-white text-2xl mr-2"></i>
|
|
<span class="text-white text-xl font-bold">{{ app_name }}</span>
|
|
</div>
|
|
<!-- Desktop Menu -->
|
|
<div class="hidden md:ml-6 md:flex md:space-x-8">
|
|
<a href="/dashboard" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
|
|
<i class="fas fa-tachometer-alt mr-2"></i>{{ t('menu.dashboard') }}
|
|
</a>
|
|
<a href="/servers" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
|
|
<i class="fas fa-server mr-2"></i>{{ t('menu.servers') }}
|
|
</a>
|
|
{% if user.role == 'admin' %}
|
|
<a href="/settings" class="text-white hover:text-gray-200 inline-flex items-center px-1 pt-1 text-sm font-medium">
|
|
<i class="fas fa-cog mr-2"></i>{{ t('menu.settings') }}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<!-- Language Selector -->
|
|
<div class="relative mr-4">
|
|
<button onclick="toggleLanguageDropdown()" class="text-white hover:text-gray-200 flex items-center focus:outline-none">
|
|
{% set currentLang = current_language %}
|
|
{% for lang in languages %}
|
|
{% if lang.code == currentLang %}
|
|
<span class="mr-2">{{ getFlag(lang.code) }}</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
<i class="fas fa-chevron-down ml-1 text-xs"></i>
|
|
</button>
|
|
<div id="languageDropdown" class="language-dropdown absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-50">
|
|
{% for lang in languages %}
|
|
<form method="POST" action="/language/change" class="block">
|
|
<input type="hidden" name="language" value="{{ lang.code }}">
|
|
<input type="hidden" name="redirect" value="{{ current_uri }}">
|
|
<button type="submit" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center
|
|
{% if lang.code == current_language %}bg-purple-50 font-medium{% endif %}">
|
|
<span class="mr-2">{{ getFlag(lang.code) }}</span>
|
|
<span>{{ lang.name }}</span>
|
|
{% if lang.code == current_language %}
|
|
<i class="fas fa-check text-purple-600 ml-auto"></i>
|
|
{% endif %}
|
|
</button>
|
|
</form>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Menu (Desktop) -->
|
|
<div class="hidden md:flex items-center">
|
|
<span class="text-white mr-4">
|
|
<i class="fas fa-user mr-1"></i>{{ user.name }}
|
|
</span>
|
|
<a href="/logout" class="text-white hover:text-gray-200">
|
|
<i class="fas fa-sign-out-alt mr-1"></i>{{ t('menu.logout') }}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Mobile menu button -->
|
|
<div class="md:hidden flex items-center ml-4">
|
|
<button onclick="toggleMobileMenu()" type="button" class="text-white hover:text-gray-200 focus:outline-none">
|
|
<i class="fas fa-bars text-xl"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile Menu -->
|
|
<div id="mobileMenu" class="mobile-menu md:hidden">
|
|
<div class="px-2 pt-2 pb-3 space-y-1">
|
|
<a href="/dashboard" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
|
|
<i class="fas fa-tachometer-alt mr-2"></i>{{ t('menu.dashboard') }}
|
|
</a>
|
|
<a href="/servers" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
|
|
<i class="fas fa-server mr-2"></i>{{ t('menu.servers') }}
|
|
</a>
|
|
{% if user.role == 'admin' %}
|
|
<a href="/settings" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
|
|
<i class="fas fa-cog mr-2"></i>{{ t('menu.settings') }}
|
|
</a>
|
|
{% endif %}
|
|
<div class="border-t border-purple-500 my-2"></div>
|
|
<div class="text-white px-3 py-2">
|
|
<i class="fas fa-user mr-1"></i>{{ user.name }}
|
|
</div>
|
|
<a href="/logout" class="text-white hover:bg-purple-700 block px-3 py-2 rounded-md text-base font-medium">
|
|
<i class="fas fa-sign-out-alt mr-1"></i>{{ t('menu.logout') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
<!-- Main Content -->
|
|
<main class="{% if user %}py-10{% endif %}">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
<!-- Custom Confirmation Modal -->
|
|
<div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-[9999]" style="display:none;">
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
|
<div class="mt-3 text-center">
|
|
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
|
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
|
|
</div>
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mt-4" id="confirmModalTitle">Подтверждение</h3>
|
|
<div class="mt-2 px-7 py-3">
|
|
<p class="text-sm text-gray-500" id="confirmModalMessage">Вы уверены?</p>
|
|
</div>
|
|
<div class="items-center px-4 py-3 flex justify-center gap-4">
|
|
<button id="confirmModalCancel" class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
|
Отмена
|
|
</button>
|
|
<button id="confirmModalOk" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
|
|
Удалить
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="bg-white border-t border-gray-200 mt-auto">
|
|
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
|
<p class="text-center text-gray-500 text-sm">
|
|
{{ app_name }} © 2025 | Open Source VPN Management Panel
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
function toggleMobileMenu() {
|
|
const menu = document.getElementById('mobileMenu');
|
|
menu.classList.toggle('active');
|
|
}
|
|
|
|
function toggleLanguageDropdown() {
|
|
const dropdown = document.getElementById('languageDropdown');
|
|
dropdown.classList.toggle('active');
|
|
}
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', function(event) {
|
|
const languageButton = event.target.closest('button[onclick="toggleLanguageDropdown()"]');
|
|
const dropdown = document.getElementById('languageDropdown');
|
|
const isInsideDropdown = dropdown && dropdown.contains(event.target);
|
|
|
|
// Don't close if clicking the button or inside dropdown (except submit buttons)
|
|
if (!languageButton && !isInsideDropdown && dropdown) {
|
|
dropdown.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Ensure dropdown stays open when form is being submitted
|
|
document.addEventListener('submit', function(event) {
|
|
if (event.target.closest('#languageDropdown')) {
|
|
// Form will submit normally, dropdown will close on page reload
|
|
}
|
|
});
|
|
|
|
// Custom confirmation modal function (replaces native confirm)
|
|
window.showConfirmModal = function(message, title) {
|
|
return new Promise((resolve) => {
|
|
const modal = document.getElementById('confirmModal');
|
|
const titleEl = document.getElementById('confirmModalTitle');
|
|
const msgEl = document.getElementById('confirmModalMessage');
|
|
const okBtn = document.getElementById('confirmModalOk');
|
|
const cancelBtn = document.getElementById('confirmModalCancel');
|
|
|
|
if (!modal) {
|
|
// Fallback to native confirm if modal not found
|
|
resolve(confirm(message));
|
|
return;
|
|
}
|
|
|
|
titleEl.textContent = title || 'Подтверждение';
|
|
msgEl.textContent = message || 'Вы уверены?';
|
|
|
|
modal.style.display = 'flex';
|
|
modal.classList.remove('hidden');
|
|
|
|
function cleanup() {
|
|
modal.style.display = 'none';
|
|
modal.classList.add('hidden');
|
|
okBtn.removeEventListener('click', onOk);
|
|
cancelBtn.removeEventListener('click', onCancel);
|
|
modal.removeEventListener('click', onBackdrop);
|
|
}
|
|
|
|
function onOk() {
|
|
cleanup();
|
|
resolve(true);
|
|
}
|
|
|
|
function onCancel() {
|
|
cleanup();
|
|
resolve(false);
|
|
}
|
|
|
|
function onBackdrop(e) {
|
|
if (e.target === modal) {
|
|
cleanup();
|
|
resolve(false);
|
|
}
|
|
}
|
|
|
|
okBtn.addEventListener('click', onOk);
|
|
cancelBtn.addEventListener('click', onCancel);
|
|
modal.addEventListener('click', onBackdrop);
|
|
});
|
|
};
|
|
</script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|