mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 21:41:59 +07:00
Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack
This commit is contained in:
226
app/Views/layouts/footer_main.php
Normal file
226
app/Views/layouts/footer_main.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php if (isset($session) && !empty($session)): ?>
|
||||
</div> <!-- /.max-w-7xl (Sidebar content) -->
|
||||
</main>
|
||||
</div> <!-- /.flex-col (Main Content Wrapper) -->
|
||||
</div> <!-- /.flex h-screen (Sidebar Layout Root) -->
|
||||
<?php else: ?>
|
||||
</div> <!-- /.container (Navbar Global) -->
|
||||
|
||||
<footer class="border-t border-accents-2 bg-background mt-auto transition-colors duration-200">
|
||||
<div class="max-w-7xl mx-auto px-4 py-6 text-center text-sm text-accents-5">
|
||||
<p><?= \App\Config\SiteConfig::getFooter() ?></p>
|
||||
</div>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
// Global Theme Toggle Logic (Class-based for multiple instances)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleButtons = document.querySelectorAll('.theme-toggle');
|
||||
|
||||
// Function to update all icons based on current mode
|
||||
const updateIcons = (isDark) => {
|
||||
const darkIcons = document.querySelectorAll('.theme-toggle-dark-icon');
|
||||
const lightIcons = document.querySelectorAll('.theme-toggle-light-icon');
|
||||
|
||||
if (isDark) {
|
||||
darkIcons.forEach(el => el.classList.add('hidden'));
|
||||
lightIcons.forEach(el => el.classList.remove('hidden'));
|
||||
} else {
|
||||
darkIcons.forEach(el => el.classList.remove('hidden'));
|
||||
lightIcons.forEach(el => el.classList.add('hidden'));
|
||||
}
|
||||
};
|
||||
|
||||
// Initial Check
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
updateIcons(true);
|
||||
} else {
|
||||
updateIcons(false);
|
||||
}
|
||||
|
||||
// Click Handlers
|
||||
toggleButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// Update LocalStorage & HTML Class
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
updateIcons(false);
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
updateIcons(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sidebar Toggle Logic
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const sidebarClose = document.getElementById('sidebar-close');
|
||||
|
||||
if (sidebar && mobileMenuToggle) {
|
||||
const toggleSidebar = () => {
|
||||
const isClosed = sidebar.classList.contains('-translate-x-full');
|
||||
if (isClosed) {
|
||||
// Open
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
sidebarOverlay.classList.remove('hidden');
|
||||
// Small delay to allow display:block to apply before opacity transition
|
||||
setTimeout(() => sidebarOverlay.classList.remove('opacity-0'), 10);
|
||||
} else {
|
||||
// Close
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
sidebarOverlay.classList.add('opacity-0');
|
||||
setTimeout(() => sidebarOverlay.classList.add('hidden'), 200);
|
||||
}
|
||||
};
|
||||
|
||||
mobileMenuToggle.addEventListener('click', toggleSidebar);
|
||||
if (sidebarClose) sidebarClose.addEventListener('click', toggleSidebar);
|
||||
if (sidebarOverlay) sidebarOverlay.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
|
||||
// Initialize Lucide Icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
||||
<?php if (\App\Helpers\FlashHelper::has()): ?>
|
||||
<?php $flash = \App\Helpers\FlashHelper::get(); ?>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Map Flash Type to Lucide Icon & Color Class
|
||||
const typeMap = {
|
||||
'success': { icon: 'check-circle-2', color: 'text-success' },
|
||||
'error': { icon: 'x-circle', color: 'text-error' },
|
||||
'warning': { icon: 'alert-triangle', color: 'text-warning' },
|
||||
'info': { icon: 'info', color: 'text-info' },
|
||||
'question':{ icon: 'help-circle', color: 'text-question' }
|
||||
};
|
||||
|
||||
const type = '<?= $flash['type'] ?>';
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
let title = '<?= addslashes($flash['title']) ?>';
|
||||
let message = '<?= addslashes($flash['message'] ?? '') ?>';
|
||||
const params = <?= json_encode($flash['params'] ?? []) ?>;
|
||||
const isTranslated = <?= $flash['isTranslated'] ? 'true' : 'false' ?>;
|
||||
|
||||
const showFlash = () => {
|
||||
if (isTranslated && window.i18n) {
|
||||
title = window.i18n.t(title, params);
|
||||
message = window.i18n.t(message, params);
|
||||
}
|
||||
|
||||
// Use Toasts for all flash notifications
|
||||
Mivo.toast(type, title, message);
|
||||
};
|
||||
|
||||
if (window.i18n && window.i18n.ready) {
|
||||
window.i18n.ready.then(showFlash);
|
||||
} else {
|
||||
showFlash();
|
||||
}
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<script>
|
||||
// Global Dropdown & Sidebar Logic
|
||||
function toggleMenu(menuId, button) {
|
||||
const menu = document.getElementById(menuId);
|
||||
if (!menu) return;
|
||||
|
||||
// Handle Dropdowns (IDs start with 'lang-' or 'session-')
|
||||
if (menuId.startsWith('lang-') || menuId === 'session-dropdown') {
|
||||
if (menu.classList.contains('invisible')) {
|
||||
// Open
|
||||
menu.classList.remove('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
menu.classList.add('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
} else {
|
||||
// Close
|
||||
menu.classList.add('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
menu.classList.remove('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Collapsible (Max-Height + Fade for Navbar)
|
||||
const isOpening = menu.style.maxHeight === '0px' || menu.style.maxHeight === '';
|
||||
const chevron = button.querySelector('[data-lucide="chevron-down"]');
|
||||
const burger = button.querySelector('[data-lucide="menu"]');
|
||||
|
||||
if (isOpening) {
|
||||
menu.style.maxHeight = menu.scrollHeight + "px";
|
||||
if (chevron) chevron.classList.add('rotate-180');
|
||||
if (burger) burger.classList.add('rotate-90');
|
||||
|
||||
if (menuId === 'mobile-navbar-menu') {
|
||||
menu.classList.remove('opacity-0', 'invisible');
|
||||
menu.classList.add('opacity-100', 'visible');
|
||||
}
|
||||
} else {
|
||||
menu.style.maxHeight = "0px";
|
||||
if (chevron) chevron.classList.remove('rotate-180');
|
||||
if (burger) burger.classList.remove('rotate-90');
|
||||
|
||||
if (menuId === 'mobile-navbar-menu') {
|
||||
menu.classList.add('opacity-0', 'invisible');
|
||||
menu.classList.remove('opacity-100', 'visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdowns = document.querySelectorAll('[id^="lang-dropdown"], #session-dropdown');
|
||||
dropdowns.forEach(dropdown => {
|
||||
if (!dropdown.classList.contains('invisible')) {
|
||||
// Find the trigger button (previous sibling usually)
|
||||
// Robust way: check if click is inside dropdown OR inside the button that toggles it
|
||||
// Since button calls toggleMenu, we just need to ignore clicks inside dropdown and button?
|
||||
// Actually, simpler: just check if click is OUTSIDE dropdown.
|
||||
// But if click is on button, let button handler toggle it (don't double toggle).
|
||||
|
||||
const button = document.querySelector(`button[onclick*="'${dropdown.id}'"]`);
|
||||
|
||||
if (!dropdown.contains(event.target) && (!button || !button.contains(event.target))) {
|
||||
dropdown.classList.add('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
dropdown.classList.remove('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Helper for confirm actions
|
||||
async function confirmAction(url, message) {
|
||||
const title = message.includes('Reboot') ? 'Reboot Router?' : 'Shutdown Router?';
|
||||
const okText = message.includes('Reboot') ? 'Reboot' : 'Shutdown';
|
||||
|
||||
const confirmed = await Mivo.confirm(title, message, okText, 'Cancel');
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
Mivo.toast('success', title.replace('?', ''), 'The command has been sent to the router.');
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Action Failed',
|
||||
text: data.error || 'Unknown error occurred.',
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
backdrop: 'rgba(0,0,0,0.1)'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Mivo.toast('error', 'Connection Error', 'Failed to reach the server.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
82
app/Views/layouts/footer_public.php
Normal file
82
app/Views/layouts/footer_public.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<footer class="mt-auto py-6 text-center text-xs text-accents-5 opacity-60">
|
||||
<?= \App\Config\SiteConfig::getFooter() ?>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize Lucide Icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
|
||||
<?php if (\App\Helpers\FlashHelper::has()): ?>
|
||||
<?php $flash = \App\Helpers\FlashHelper::get(); ?>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Map Flash Type to Lucide Icon & Color Class
|
||||
const typeMap = {
|
||||
'success': { icon: 'check-circle-2', color: 'text-success' },
|
||||
'error': { icon: 'x-circle', color: 'text-error' },
|
||||
'warning': { icon: 'alert-triangle', color: 'text-warning' },
|
||||
'info': { icon: 'info', color: 'text-info' },
|
||||
'question':{ icon: 'help-circle', color: 'text-question' }
|
||||
};
|
||||
|
||||
const type = '<?= $flash['type'] ?>';
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
let title = '<?= addslashes($flash['title']) ?>';
|
||||
let message = '<?= addslashes($flash['message'] ?? '') ?>';
|
||||
const params = <?= json_encode($flash['params'] ?? []) ?>;
|
||||
const isTranslated = <?= $flash['isTranslated'] ? 'true' : 'false' ?>;
|
||||
|
||||
const showFlash = () => {
|
||||
if (isTranslated && window.i18n) {
|
||||
title = window.i18n.t(title, params);
|
||||
message = window.i18n.t(message, params);
|
||||
}
|
||||
|
||||
// Use Custom Toasts for most notifications (Success, Info, Error)
|
||||
// Only use Modal (Swal) for specific heavy warnings or questions if needed
|
||||
if (['success', 'info', 'error', 'warning'].includes(type)) {
|
||||
// Assuming Mivo.toast is available globally or via another script check
|
||||
if (window.Mivo && window.Mivo.toast) {
|
||||
Mivo.toast(type, title, message);
|
||||
} else {
|
||||
console.log('Toast:', title, message);
|
||||
}
|
||||
} else {
|
||||
// Use Swal for 'question' or fallback
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
iconHtml: `<i data-lucide="${config.icon}" class="w-12 h-12 ${config.color}"></i>`,
|
||||
title: title,
|
||||
text: message,
|
||||
confirmButtonText: 'OK',
|
||||
customClass: {
|
||||
popup: 'swal2-premium-card',
|
||||
confirmButton: 'btn btn-primary',
|
||||
cancelButton: 'btn btn-secondary',
|
||||
},
|
||||
buttonsStyling: false,
|
||||
heightAuto: false,
|
||||
didOpen: () => {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert(`${title}\n${message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.i18n && window.i18n.ready) {
|
||||
window.i18n.ready.then(showFlash);
|
||||
} else {
|
||||
showFlash();
|
||||
}
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
135
app/Views/layouts/header_main.php
Normal file
135
app/Views/layouts/header_main.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
// Initialize variables to avoid undefined notices if not set
|
||||
$hotspotname = isset($hotspotname) ? $hotspotname : \App\Config\SiteConfig::APP_NAME;
|
||||
$themecolor = isset($themecolor) ? $themecolor : '#000000';
|
||||
$theme = 'light'; // Default theme
|
||||
$title = isset($title) ? $title : \App\Config\SiteConfig::APP_NAME;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= $title; ?></title>
|
||||
<meta name="theme-color" content="<?= $themecolor ?>" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="/assets/img/favicon.png" />
|
||||
|
||||
<!-- Tailwind CSS (Local) -->
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
|
||||
<!-- Flag Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
|
||||
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('/assets/fonts/Geist-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('/assets/fonts/Geist-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('/assets/fonts/GeistMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Check local storage or system preference on load
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
<script src="/assets/js/jquery.min.js"></script>
|
||||
<script src="/assets/js/lucide.min.js"></script>
|
||||
<script src="/assets/js/custom-select.js" defer></script>
|
||||
<script src="/assets/js/datatable.js" defer></script>
|
||||
<script src="/assets/js/sweetalert2.all.min.js" defer></script>
|
||||
<script src="/assets/js/alert-helper.js" defer></script>
|
||||
<script src="/assets/js/i18n.js" defer></script>
|
||||
<script src="/assets/js/i18n.js" defer></script>
|
||||
|
||||
<style>
|
||||
/* Global Form Input Style - Matches Vercel Design System */
|
||||
.form-input, .form-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2.5rem; /* 10px */
|
||||
width: 100%;
|
||||
border-radius: 0.375rem; /* 6px */
|
||||
border: 1px solid var(--accents-2, #eaeaea);
|
||||
background-color: var(--background, #ffffff);
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.875rem; /* 14px */
|
||||
line-height: 1.25rem;
|
||||
color: var(--foreground, #000);
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
/* Input with left icon spacing */
|
||||
.form-input.pl-10, .form-control.pl-10 {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.dark .form-input {
|
||||
background-color: #000; /* or darkest gray */
|
||||
border-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--foreground);
|
||||
box-shadow: 0 0 0 1px var(--foreground);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--accents-4);
|
||||
}
|
||||
|
||||
/* Fix for DataTables or other inputs without Left Icon */
|
||||
input.form-input:not([class*="pl-"]) {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen bg-background text-foreground anti-aliased relative">
|
||||
<!-- Background Elements (Global Sci-Fi Grid) -->
|
||||
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||
<!-- Subtle Grid Pattern -->
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4zKSIvPjwvc3ZnPg==')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/20 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/20 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||
</div>
|
||||
<?php
|
||||
if (isset($session) && !empty($session)) {
|
||||
// Session Layout (Sidebar)
|
||||
include ROOT . '/app/Views/layouts/sidebar_session.php';
|
||||
} else {
|
||||
// Global Layout (Navbar)
|
||||
include ROOT . '/app/Views/layouts/navbar_main.php';
|
||||
if (!isset($no_main_container) || !$no_main_container) {
|
||||
echo '<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 flex-grow w-full flex flex-col">';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
68
app/Views/layouts/header_public.php
Normal file
68
app/Views/layouts/header_public.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?? 'MIVO' ?></title>
|
||||
<!-- Tailwind CSS (Local) -->
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
<script src="/assets/js/lucide.min.js"></script>
|
||||
<script src="/assets/js/sweetalert2.all.min.js" defer></script>
|
||||
<script src="/assets/js/alert-helper.js" defer></script>
|
||||
<script src="/assets/js/i18n.js" defer></script>
|
||||
<style>
|
||||
/* Custom Keyframes */
|
||||
@keyframes fade-in-up {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.4s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Check local storage for theme
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-foreground antialiased min-h-screen relative overflow-hidden font-sans selection:bg-accents-2 selection:text-foreground flex flex-col">
|
||||
|
||||
<!-- Background Elements (Common) -->
|
||||
<div class="absolute inset-0 z-0 pointer-events-none">
|
||||
<!-- Subtle Grid Pattern -->
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4zKSIvPjwvc3ZnPg==')] dark:bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=')] [mask-image:linear-gradient(to_bottom,white,transparent)]"></div>
|
||||
<div class="absolute -top-[20%] -left-[10%] w-[70vw] h-[70vw] rounded-full bg-blue-500/20 dark:bg-blue-500/5 blur-[120px] animate-pulse" style="animation-duration: 4s;"></div>
|
||||
<div class="absolute top-[30%] -right-[15%] w-[60vw] h-[60vw] rounded-full bg-purple-500/20 dark:bg-purple-500/5 blur-[100px] animate-pulse" style="animation-duration: 6s; animation-delay: 1s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Theme Toggle (Bottom Right) -->
|
||||
<button id="theme-toggle" class="fixed bottom-6 right-6 z-50 p-3 rounded-full bg-background border border-accents-2 shadow-lg text-accents-5 hover:text-foreground hover:border-foreground transition-all duration-300 group" style="position: fixed; bottom: 1.5rem; right: 1.5rem;">
|
||||
<i data-lucide="moon" class="w-5 h-5 block dark:hidden group-hover:scale-110 transition-transform"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 hidden dark:block group-hover:scale-110 transition-transform"></i>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
lucide.createIcons();
|
||||
|
||||
// Theme Toggle Logic
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
if(themeToggleBtn){
|
||||
themeToggleBtn.addEventListener('click', () => {
|
||||
if (htmlElement.classList.contains('dark')) {
|
||||
htmlElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
htmlElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
127
app/Views/layouts/navbar_main.php
Normal file
127
app/Views/layouts/navbar_main.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
// Determine active link state
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
?>
|
||||
<!-- Modern Navbar (Tailwind) -->
|
||||
<nav class="sticky top-0 z-50 w-full border-b border-accents-2 bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Brand & Desktop Nav -->
|
||||
<div class="flex items-center gap-8">
|
||||
<a href="/" class="flex items-center gap-2 group">
|
||||
<img src="/assets/img/logo-m.svg" alt="<?= \App\Config\SiteConfig::APP_NAME ?> Logo" class="h-6 w-auto block dark:hidden transition-transform group-hover:scale-110">
|
||||
<img src="/assets/img/logo-m-dark.svg" alt="<?= \App\Config\SiteConfig::APP_NAME ?> Logo" class="h-6 w-auto hidden dark:block transition-transform group-hover:scale-110">
|
||||
<span class="font-bold text-lg tracking-tight"><?= \App\Config\SiteConfig::APP_NAME ?></span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Navigation Links (Hidden on Mobile) -->
|
||||
<div class="hidden md:flex items-center gap-6 text-sm font-medium">
|
||||
<a href="/" class="relative py-1 <?= ($uri == '/' || $uri == '/home') ? 'text-foreground after:absolute after:bottom-0 after:left-0 after:w-full after:h-0.5 after:bg-foreground' : 'text-accents-5 hover:text-foreground transition-colors' ?>">Home</a>
|
||||
<a href="/settings" class="relative py-1 <?= (strpos($uri, '/settings') === 0) ? 'text-foreground after:absolute after:bottom-0 after:left-0 after:w-full after:h-0.5 after:bg-foreground' : 'text-accents-5 hover:text-foreground transition-colors' ?>">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side controls -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Desktop Control Pill (Hidden on Mobile) -->
|
||||
<div class="hidden md:flex control-pill scale-95 hover:scale-100 transition-transform">
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group">
|
||||
<button type="button" class="pill-lang-btn" onclick="toggleMenu('lang-dropdown-nav', this)" title="Change Language">
|
||||
<i data-lucide="languages" class="w-4 h-4 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
</button>
|
||||
<div id="lang-dropdown-nav" class="absolute right-0 top-full mt-3 w-48 bg-background/90 backdrop-blur-xl border border-accents-2 rounded-xl shadow-xl overflow-hidden transition-all duration-200 ease-out origin-top-right opacity-0 scale-95 invisible pointer-events-none z-50">
|
||||
<div class="px-3 py-2 text-[10px] font-bold text-accents-4 uppercase tracking-widest border-b border-accents-2/50 bg-accents-1/50" data-i18n="sidebar.switch_language">Select Language</div>
|
||||
<?php
|
||||
$languages = \App\Helpers\LanguageHelper::getAvailableLanguages();
|
||||
foreach ($languages as $lang):
|
||||
?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-sm shadow-sm transition-transform group-hover/lang:scale-110"></span>
|
||||
<span><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pill-divider"></div>
|
||||
|
||||
<!-- Theme Toggle (Segmented) -->
|
||||
<div class="segmented-switch theme-toggle" title="Toggle Theme">
|
||||
<div class="segmented-switch-slider"></div>
|
||||
<div class="segmented-switch-btn theme-toggle-light-icon">
|
||||
<i data-lucide="sun" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
<div class="segmented-switch-btn theme-toggle-dark-icon">
|
||||
<i data-lucide="moon" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if(isset($_SESSION['user_id'])): ?>
|
||||
<div class="pill-divider"></div>
|
||||
<a href="/logout" class="p-1.5 rounded-lg text-accents-5 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all ml-0.5" title="Logout">
|
||||
<i data-lucide="log-out" class="w-4 h-4 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Toggles -->
|
||||
<div class="flex md:hidden items-center gap-2">
|
||||
<!-- Mobile Mode Control Pill (Condensed) -->
|
||||
<div class="control-pill py-1.5 px-2">
|
||||
<div class="segmented-switch theme-toggle scale-75" title="Toggle Theme">
|
||||
<div class="segmented-switch-slider"></div>
|
||||
<div class="segmented-switch-btn theme-toggle-light-icon"><i data-lucide="sun" class="w-4 h-4" stroke-width="3.5"></i></div>
|
||||
<div class="segmented-switch-btn theme-toggle-dark-icon"><i data-lucide="moon" class="w-4 h-4" stroke-width="3.5"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="p-2 rounded-lg bg-accents-1 text-accents-5 hover:text-foreground transition-colors group" onclick="toggleMenu('mobile-navbar-menu', this)">
|
||||
<i data-lucide="menu" class="w-5 h-5 !text-black dark:!text-white transition-transform duration-300" stroke-width="2.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Navigation Drawer (Hidden by default) -->
|
||||
<div id="mobile-navbar-menu" class="md:hidden border-t border-accents-2 bg-background/95 backdrop-blur-xl transition-all duration-300 ease-in-out max-h-0 opacity-0 invisible overflow-hidden">
|
||||
<div class="px-4 pt-4 pb-6 space-y-4">
|
||||
<!-- Nav Links -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<a href="/" class="flex items-center gap-3 px-4 py-3 rounded-xl <?= ($uri == '/' || $uri == '/home') ? 'bg-foreground/5 text-foreground font-bold' : 'text-accents-5 hover:bg-accents-1' ?>">
|
||||
<i data-lucide="home" class="w-5 h-5 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="/settings" class="flex items-center gap-3 px-4 py-3 rounded-xl <?= (strpos($uri, '/settings') === 0) ? 'bg-foreground/5 text-foreground font-bold' : 'text-accents-5 hover:bg-accents-1' ?>">
|
||||
<i data-lucide="settings" class="w-5 h-5 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Controls Overlay -->
|
||||
<div class="p-4 rounded-2xl bg-accents-1/50 border border-accents-2 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-accents-4 uppercase tracking-wider">Select Language</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide snap-x">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full border border-accents-2 bg-background hover:border-foreground transition-all text-sm font-medium snap-start shadow-sm">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-full shadow-sm"></span>
|
||||
<span class="whitespace-nowrap"><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if(isset($_SESSION['user_id'])): ?>
|
||||
<div class="pt-2 border-t border-accents-2">
|
||||
<a href="/logout" class="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-xl bg-red-500/10 text-red-600 font-bold hover:bg-red-500/20 transition-all">
|
||||
<i data-lucide="log-out" class="w-5 h-5 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
<span>Logout System</span>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
475
app/Views/layouts/sidebar_session.php
Normal file
475
app/Views/layouts/sidebar_session.php
Normal file
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
// Determine active link state
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$isDashboard = strpos($uri, '/dashboard') !== false;
|
||||
$isGenerate = strpos($uri, '/hotspot/generate') !== false;
|
||||
$isTemplates = strpos($uri, '/settings/templates') !== false;
|
||||
$isSettings = ($uri === '/settings' || strpos($uri, '/settings/') !== false) && !$isTemplates;
|
||||
|
||||
// Hotspot Group Active Check
|
||||
$hotspotPages = ['/hotspot/users', '/hotspot/profiles', '/hotspot/generate', '/hotspot/cookies'];
|
||||
$isHotspotActive = false;
|
||||
foreach ($hotspotPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isHotspotActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Status Group Active Check
|
||||
$statusPages = ['/hotspot/active', '/hotspot/hosts'];
|
||||
$isStatusActive = false;
|
||||
foreach ($statusPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isStatusActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Security Group Active Check (Existing)
|
||||
$securityPages = ['/hotspot/bindings', '/hotspot/walled-garden'];
|
||||
$isSecurityActive = false;
|
||||
foreach ($securityPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isSecurityActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reports Group Active Check
|
||||
$reportsPages = ['/reports/resume', '/reports/selling', '/reports/user-log'];
|
||||
$isReportsActive = false;
|
||||
foreach ($reportsPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isReportsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Network Group Active Check
|
||||
$networkPages = ['/network/dhcp'];
|
||||
$isNetworkActive = false;
|
||||
foreach ($networkPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isNetworkActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// System Group Active Check
|
||||
$systemPages = ['/system/scheduler'];
|
||||
$isSystemActive = false;
|
||||
foreach ($systemPages as $page) {
|
||||
if (strpos($uri, $page) !== false) {
|
||||
$isSystemActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all sessions for the switcher
|
||||
$configModel = new \App\Models\Config();
|
||||
$allSessions = $configModel->getAllSessions();
|
||||
|
||||
// Find current session details to get Hotspot Name / IP
|
||||
$currentSessionDetails = [];
|
||||
foreach ($allSessions as $s) {
|
||||
if (isset($session) && $s['session_name'] === $session) {
|
||||
$currentSessionDetails = $s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Determine label: Hotspot Name > IP Address > 'MIVO'
|
||||
$sessionLabel = $currentSessionDetails['hotspot_name'] ?? $currentSessionDetails['ip_address'] ?? 'MIVO';
|
||||
if (empty($sessionLabel)) {
|
||||
$sessionLabel = $currentSessionDetails['ip_address'] ?? 'MIVO';
|
||||
}
|
||||
|
||||
// Helper for Session Initials (Kebab-friendly)
|
||||
$getInitials = function($name) {
|
||||
if (empty($name)) return 'UN';
|
||||
if (strpos($name, '-') !== false) {
|
||||
$parts = explode('-', $name);
|
||||
$initials = '';
|
||||
foreach ($parts as $part) {
|
||||
if (!empty($part)) $initials .= substr($part, 0, 1);
|
||||
}
|
||||
return strtoupper(substr($initials, 0, 2));
|
||||
}
|
||||
return strtoupper(substr($name, 0, 2));
|
||||
};
|
||||
?>
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Mobile Sidebar Overlay -->
|
||||
<div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden md:hidden transition-opacity opacity-0"></div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside id="sidebar" class="w-64 flex-shrink-0 border-r border-white/20 dark:border-white/10 bg-white/40 dark:bg-black/40 backdrop-blur-[40px] fixed md:static inset-y-0 left-0 z-40 transform -translate-x-full md:translate-x-0 transition-transform duration-200 flex flex-col h-full">
|
||||
<!-- Sidebar Header -->
|
||||
<!-- Sidebar Header -->
|
||||
<div class="group flex flex-col items-center py-5 border-b border-accents-2 flex-shrink-0 relative cursor-default overflow-hidden">
|
||||
<div class="relative w-full h-10 flex items-center justify-center">
|
||||
<!-- Brand (Slides out to the Left) -->
|
||||
<div class="flex items-center gap-2 font-bold text-2xl tracking-tighter transition-all duration-500 ease-in-out group-hover:-translate-x-full group-hover:opacity-0">
|
||||
<img src="/assets/img/logo-m.svg" alt="MIVO Logo" class="h-10 w-auto block dark:hidden">
|
||||
<img src="/assets/img/logo-m-dark.svg" alt="MIVO Logo" class="h-10 w-auto hidden dark:block">
|
||||
<span>MIVO</span>
|
||||
</div>
|
||||
|
||||
<!-- Premium Control Pill (Slides in from the Right to replace Brand) -->
|
||||
<div class="absolute inset-0 hidden md:flex items-center justify-center transition-all duration-500 ease-in-out translate-x-full opacity-0 group-hover:translate-x-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto z-10">
|
||||
<div class="control-pill scale-90 transition-transform hover:scale-100 shadow-lg bg-white/10 dark:bg-black/20 backdrop-blur-md">
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group/lang">
|
||||
<button type="button" class="pill-lang-btn" onclick="toggleMenu('lang-dropdown-sidebar', this)" title="Change Language">
|
||||
<i data-lucide="languages" class="w-4 h-4 !text-black dark:!text-white" stroke-width="2.5"></i>
|
||||
</button>
|
||||
<div id="lang-dropdown-sidebar" class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-48 bg-background/90 backdrop-blur-xl border border-accents-2 rounded-xl shadow-xl overflow-hidden transition-all duration-200 ease-out origin-top opacity-0 scale-95 invisible pointer-events-none z-50">
|
||||
<div class="px-3 py-2 text-[10px] font-bold text-accents-4 uppercase tracking-widest border-b border-accents-2/50 bg-accents-1/50" data-i18n="sidebar.switch_language">Select Language</div>
|
||||
<?php
|
||||
$languages = \App\Helpers\LanguageHelper::getAvailableLanguages();
|
||||
foreach ($languages as $lang):
|
||||
?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang-item">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-sm shadow-sm transition-transform group-hover/lang-item:scale-110"></span>
|
||||
<span><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pill-divider"></div>
|
||||
|
||||
<!-- Theme Toggle (Segmented) -->
|
||||
<div class="segmented-switch theme-toggle" title="Toggle Theme">
|
||||
<div class="segmented-switch-slider"></div>
|
||||
<div class="segmented-switch-btn theme-toggle-light-icon">
|
||||
<i data-lucide="sun" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
<div class="segmented-switch-btn theme-toggle-dark-icon">
|
||||
<i data-lucide="moon" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Close Button -->
|
||||
<button id="sidebar-close" class="md:hidden absolute top-4 right-4 text-accents-5 hover:text-foreground">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Sidebar Content -->
|
||||
<!-- Sidebar Content (RTL for left scrollbar) -->
|
||||
<div class="flex-1 overflow-y-auto" style="direction: rtl;">
|
||||
<div class="py-4 px-3 space-y-1" style="direction: ltr;">
|
||||
<!-- Session Switcher -->
|
||||
<div class="px-3 mb-6 relative">
|
||||
<button type="button" class="w-full group grid grid-cols-[auto_1fr_auto] items-center gap-3 px-4 py-2.5 rounded-xl bg-white/50 dark:bg-white/5 border border-accents-2 dark:border-white/10 hover:bg-white/80 dark:hover:bg-white/10 transition-all decoration-0 overflow-hidden shadow-sm" onclick="toggleMenu('session-dropdown', this)">
|
||||
<!-- Initials -->
|
||||
<div class="h-8 w-8 rounded-lg bg-accents-2/50 group-hover:bg-accents-2 flex items-center justify-center text-xs font-bold text-accents-6 group-hover:text-foreground transition-colors flex-shrink-0">
|
||||
<?= $getInitials($session ?? '') ?>
|
||||
</div>
|
||||
|
||||
<!-- Text Info -->
|
||||
<div class="flex flex-col text-left min-w-0">
|
||||
<span class="text-xs font-bold text-accents-6 group-hover:text-foreground transition-colors leading-none truncate"><?= htmlspecialchars($session ?? 'Select Session') ?></span>
|
||||
<span class="text-[10px] text-accents-4 leading-none mt-1 truncate" title="<?= htmlspecialchars($sessionLabel) ?>">
|
||||
<?= htmlspecialchars($sessionLabel) ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Chevron Icon -->
|
||||
<div class="h-8 w-8 flex-shrink-0 flex items-center justify-center rounded-lg bg-accents-2/50 group-hover:bg-accents-2 transition-colors">
|
||||
<i data-lucide="chevrons-up-down" class="!w-4 !h-4 !text-accents-6 dark:!text-accents-6 transition-colors"></i>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown -->
|
||||
<div id="session-dropdown" class="absolute top-full left-3 w-[calc(100%-1.5rem)] z-50 mt-1 bg-background border border-accents-2 rounded-lg shadow-lg overflow-hidden transition-all duration-200 ease-out origin-top opacity-0 scale-95 invisible pointer-events-none">
|
||||
<div class="py-1 max-h-60 overflow-y-auto">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-accents-5 uppercase tracking-wider bg-accents-1/50 border-b border-accents-2" data-i18n="sidebar.switch_session">
|
||||
Switch Session
|
||||
</div>
|
||||
<?php foreach ($allSessions as $s): ?>
|
||||
<a href="/<?= htmlspecialchars($s['session_name']) ?>/dashboard" class="flex items-center gap-3 px-3 py-2 text-sm hover:bg-accents-1 transition-colors group/item">
|
||||
<div class="h-6 w-6 rounded flex-shrink-0 bg-accents-2 flex items-center justify-center text-[10px] font-bold">
|
||||
<?= $getInitials($s['session_name']) ?>
|
||||
</div>
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span class="truncate <?= ($session === $s['session_name']) ? 'font-medium text-foreground' : 'text-accents-5 group-hover/item:text-foreground' ?>">
|
||||
<?= htmlspecialchars($s['session_name']) ?>
|
||||
</span>
|
||||
<span class="text-[10px] text-accents-4 truncate">
|
||||
<?= htmlspecialchars($s['hotspot_name'] ?: $s['ip_address']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if ($session === $s['session_name']): ?>
|
||||
<i data-lucide="check" class="w-3 h-3 ml-auto text-primary"></i>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="border-t border-accents-2 p-1 bg-accents-1/30">
|
||||
<a href="/settings/add" class="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accents-2 rounded-md transition-colors text-accents-5 hover:text-foreground">
|
||||
<i data-lucide="plus-circle" class="w-4 h-4"></i>
|
||||
<span data-i18n="settings.add_router">Connect Router</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<a href="/<?= htmlspecialchars($session) ?>/dashboard" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors <?= $isDashboard ? 'bg-white/40 dark:bg-white/5 shadow-sm text-foreground ring-1 ring-white/10' : 'text-accents-6 hover:text-foreground hover:bg-white/5' ?>">
|
||||
<i data-lucide="layout-dashboard" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.dashboard">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<!-- Quick Print -->
|
||||
<a href="/<?= htmlspecialchars($session) ?>/quick-print" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors <?= (strpos($uri, '/quick-print') !== false) ? 'bg-white/40 dark:bg-white/5 shadow-sm text-foreground ring-1 ring-white/10' : 'text-accents-6 hover:text-foreground hover:bg-white/5' ?>">
|
||||
<i data-lucide="printer" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.quick_print">Quick Print</span>
|
||||
</a>
|
||||
|
||||
<!-- Hotspots Separator -->
|
||||
<div class="pt-4 pb-1 px-3">
|
||||
<div class="text-xs font-semibold text-accents-5 uppercase tracking-wider" data-i18n="sidebar.hotspot">Hotspots</div>
|
||||
</div>
|
||||
|
||||
<!-- Hotspot Group (Collapsible) -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('hotspot-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="wifi" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.hotspot">Hotspot</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isHotspotActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="hotspot-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isHotspotActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/users" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/users') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.users">Users</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profiles" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/profile') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.profiles">User Profiles</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/generate" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/generate') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.generate">Generate</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/cookies" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/cookies') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.cookies">Cookies</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Group (Collapsible) -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('status-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="activity" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.status">Status</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isStatusActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="status-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isStatusActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/active" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/active') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.active">Active</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/hosts" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/hosts') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.hosts">Hosts</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Group (Collapsible) -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('security-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.security">Security</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isSecurityActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="security-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isSecurityActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/bindings" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/bindings') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.bindings">IP Bindings</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/walled-garden" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/hotspot/walled-garden') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="hotspot_menu.walled_garden">Walled Garden</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Reports Group -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('reports-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="file-text" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.reports">Reports</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isReportsActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="reports-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isReportsActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/reports/resume" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/reports/resume') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="reports_menu.resume">Resume</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/reports/selling" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/reports/selling') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="reports_menu.selling">Selling Report</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/reports/user-log" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/reports/user-log') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="reports_menu.user_log">User Log</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Group -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('network-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="network" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.network">Network</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isNetworkActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="network-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isNetworkActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/network/dhcp" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/network/dhcp') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="network_menu.dhcp">DHCP Leases</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Group -->
|
||||
<div class="space-y-1">
|
||||
<button type="button" class="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-5 hover:text-foreground hover:bg-accents-2/50 group" onclick="toggleMenu('system-menu', this)">
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="cpu" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.system">System</span>
|
||||
</div>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300 <?= $isSystemActive ? 'rotate-180' : '' ?>"></i>
|
||||
</button>
|
||||
|
||||
<div id="system-menu" class="space-y-1 pl-9 overflow-hidden transition-[max-height] duration-300 ease-in-out" style="max-height: <?= $isSystemActive ? '500px' : '0px' ?>">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/system/scheduler" class="block px-3 py-2 rounded-md text-sm transition-colors <?= (strpos($uri, '/system/scheduler') !== false) ? 'bg-white/40 dark:bg-white/5 text-foreground ring-1 ring-white/10 font-medium' : 'text-accents-6 hover:text-foreground' ?>">
|
||||
<span data-i18n="system_menu.scheduler">Scheduler</span>
|
||||
</a>
|
||||
<button onclick="confirmAction('/<?= htmlspecialchars($session) ?>/system/reboot', 'Reboot Router?')" class="w-full text-left block px-3 py-2 rounded-md text-sm text-accents-5 hover:text-red-500 transition-colors">
|
||||
<span data-i18n="system_menu.reboot">Reboot</span>
|
||||
</button>
|
||||
<button onclick="confirmAction('/<?= htmlspecialchars($session) ?>/system/shutdown', 'Shutdown Router?')" class="w-full text-left block px-3 py-2 rounded-md text-sm text-accents-5 hover:text-red-500 transition-colors">
|
||||
<span data-i18n="system_menu.shutdown">Shutdown</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Systems Separator -->
|
||||
<div class="pt-4 pb-1 px-3">
|
||||
<div class="text-xs font-semibold text-accents-5 uppercase tracking-wider" data-i18n="sidebar.system">Systems</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<a href="/settings" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors <?= $isSettings ? 'bg-white/40 dark:bg-white/5 shadow-sm text-foreground ring-1 ring-white/10' : 'text-accents-6 hover:text-foreground hover:bg-white/5' ?>">
|
||||
<i data-lucide="settings" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.settings">Settings</span>
|
||||
</a>
|
||||
|
||||
<!-- Voucher Templates -->
|
||||
<a href="/settings/templates" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors <?= $isTemplates ? 'bg-white/40 dark:bg-white/5 shadow-sm text-foreground ring-1 ring-white/10' : 'text-accents-6 hover:text-foreground hover:bg-white/5' ?>">
|
||||
<i data-lucide="file-code" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.templates">Templates</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="p-4 border-t border-white/10 space-y-3">
|
||||
<!-- Disconnect (Session) -->
|
||||
<a href="/" class="group flex items-center justify-between px-3 py-2.5 rounded-xl bg-white/50 dark:bg-white/5 border border-accents-2 dark:border-white/10 hover:bg-white/80 dark:hover:bg-white/10 transition-all decoration-0 shadow-sm" title="Disconnect Session">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-1.5 rounded-lg bg-accents-2/50 group-hover:bg-accents-2 transition-colors">
|
||||
<i data-lucide="cast" class="!w-4 !h-4 !text-black dark:!text-white !flex-shrink-0 transition-colors"></i>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-bold text-accents-6 group-hover:text-foreground transition-colors leading-none" data-i18n="sidebar.disconnect">Disconnect</span>
|
||||
<span class="text-[10px] text-accents-4 leading-none mt-1">Exit Session</span>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="!w-4 !h-4 !text-black dark:!text-white !flex-shrink-0 transition-colors"></i>
|
||||
</a>
|
||||
|
||||
<?php if(isset($_SESSION['user_id'])): ?>
|
||||
<!-- Logout (System) -->
|
||||
<a href="/logout" class="group flex items-center justify-between px-3 py-2.5 rounded-xl bg-white/50 dark:bg-white/5 border border-accents-2 dark:border-white/10 hover:bg-red-500/10 hover:border-red-500/20 transition-all decoration-0 shadow-sm" title="Logout from Mivo">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-1.5 rounded-lg bg-red-500/10 text-red-500 group-hover:bg-red-500/20 transition-colors">
|
||||
<i data-lucide="log-out" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-bold text-accents-6 group-hover:text-red-500 transition-colors leading-none" data-i18n="sidebar.logout">Logout</span>
|
||||
<span class="text-[10px] text-accents-4 group-hover:text-red-400/80 leading-none mt-1">Sign Out</span>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" class="!w-4 !h-4 !text-black dark:!text-white !flex-shrink-0 group-hover:!text-red-500 transition-colors"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden w-full">
|
||||
<!-- Mobile Header (Visible only on small screens) -->
|
||||
<header class="h-16 flex items-center justify-between px-4 border-b border-accents-2 bg-background/80 backdrop-blur-md md:hidden z-20 sticky top-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/assets/img/logo-m.svg" class="h-6 w-auto block dark:hidden">
|
||||
<img src="/assets/img/logo-m-dark.svg" class="h-6 w-auto hidden dark:block">
|
||||
<span class="font-bold">MIVO</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Mobile Premium Control Pill -->
|
||||
<div class="control-pill scale-90 origin-right transition-transform hover:scale-95">
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group">
|
||||
<button type="button" class="pill-lang-btn" onclick="toggleMenu('lang-dropdown-mobile', this)" title="Change Language">
|
||||
<i data-lucide="languages" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<div id="lang-dropdown-mobile" class="absolute right-0 top-full mt-3 w-48 bg-background/90 backdrop-blur-xl border border-accents-2 rounded-xl shadow-xl overflow-hidden transition-all duration-200 ease-out origin-top-right opacity-0 scale-95 invisible pointer-events-none z-50">
|
||||
<div class="px-3 py-2 text-[10px] font-bold text-accents-4 uppercase tracking-widest border-b border-accents-2/50 bg-accents-1/50" data-i18n="sidebar.switch_language">Select Language</div>
|
||||
<?php
|
||||
$languages = \App\Helpers\LanguageHelper::getAvailableLanguages();
|
||||
foreach ($languages as $lang):
|
||||
?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-sm shadow-sm transition-transform group-hover/lang:scale-110"></span>
|
||||
<span><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pill-divider"></div>
|
||||
|
||||
<!-- Theme Toggle (Segmented) -->
|
||||
<div class="segmented-switch theme-toggle" title="Toggle Theme">
|
||||
<div class="segmented-switch-slider"></div>
|
||||
<div class="segmented-switch-btn theme-toggle-light-icon">
|
||||
<i data-lucide="sun" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
<div class="segmented-switch-btn theme-toggle-dark-icon">
|
||||
<i data-lucide="moon" class="w-4 h-4" stroke-width="3.5"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="mobile-menu-toggle" class="text-accents-5 hover:text-foreground">
|
||||
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Scrollable Page Content -->
|
||||
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-background p-4 md:p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
|
||||
87
app/Views/layouts/sidebar_settings.php
Normal file
87
app/Views/layouts/sidebar_settings.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
function isActive($path, $current) {
|
||||
if ($path === '/settings') {
|
||||
// Routers is the new home. Active if exactly /settings or /settings/routers
|
||||
return $current === '/settings' || $current === '/settings/' || strpos($current, '/settings/routers') !== false;
|
||||
}
|
||||
return strpos($current, $path) !== false;
|
||||
}
|
||||
|
||||
$menu = [
|
||||
['label' => 'routers_title', 'url' => '/settings', 'namespace' => 'settings'],
|
||||
['label' => 'system', 'url' => '/settings/system', 'namespace' => 'settings'],
|
||||
['label' => 'templates_title', 'url' => '/settings/templates', 'namespace' => 'settings'],
|
||||
['label' => 'logos_title', 'url' => '/settings/logos', 'namespace' => 'settings'],
|
||||
['label' => 'api_cors_title', 'url' => '/settings/api-cors', 'namespace' => 'settings'],
|
||||
];
|
||||
?>
|
||||
<nav id="settings-sidebar" class="w-full sticky top-[64px] z-40 bg-background/95 backdrop-blur border-b border-accents-2 transition-all duration-300">
|
||||
<div class="max-w-7xl mx-auto px-4 md:px-8"> <!-- Aligned with header_main max-w-7xl -->
|
||||
<div class="relative py-2 flex items-start gap-2">
|
||||
|
||||
<!-- Menu Container (Toggles between flex-row/scroll and grid) -->
|
||||
<div id="sub-navbar-menu" class="flex-1 flex flex-row items-center overflow-x-auto no-scrollbar mask-fade-right gap-2 transition-all duration-300">
|
||||
<?php foreach($menu as $item):
|
||||
$active = isActive($item['url'], $uri);
|
||||
?>
|
||||
<a href="<?= $item['url'] ?>"
|
||||
class="sub-nav-item whitespace-nowrap px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 border border-transparent
|
||||
<?= $active ? 'bg-foreground text-background shadow-sm' : 'text-accents-5 hover:text-foreground hover:bg-accents-1' ?>"
|
||||
data-i18n="<?= ($item['namespace'] ?? 'settings') . '.' . $item['label'] ?>">
|
||||
<?= $item['label'] ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Button -->
|
||||
<button id="sub-navbar-toggle" class="flex-shrink-0 p-2 text-accents-5 hover:text-foreground hover:bg-accents-1 rounded-full transition-colors hidden sm:block" title="Expand Menu">
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggleBtn = document.getElementById('sub-navbar-toggle');
|
||||
const menu = document.getElementById('sub-navbar-menu');
|
||||
const icon = toggleBtn?.querySelector('i');
|
||||
let isExpanded = false;
|
||||
|
||||
if (toggleBtn && menu) {
|
||||
// Check if content overflows to decide if we even show the toggle initially?
|
||||
// For now, always show it on sm+ screens if desired, or we can check scrollWidth > clientWidth.
|
||||
// Let's keep it simple: always available on desktop/tablet to see full grid.
|
||||
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
isExpanded = !isExpanded;
|
||||
|
||||
if (isExpanded) {
|
||||
// Expand: Grid Layout
|
||||
menu.classList.remove('flex-row', 'overflow-x-auto', 'whitespace-nowrap', 'mask-fade-right', 'items-center');
|
||||
menu.classList.add('grid', 'grid-cols-2', 'sm:grid-cols-3', 'md:grid-cols-4', 'lg:grid-cols-5', 'gap-2', 'pb-4');
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
// Collapse: Scroll Layout
|
||||
menu.classList.add('flex-row', 'overflow-x-auto', 'whitespace-nowrap', 'mask-fade-right', 'items-center');
|
||||
menu.classList.remove('grid', 'grid-cols-2', 'sm:grid-cols-3', 'md:grid-cols-4', 'lg:grid-cols-5', 'gap-2', 'pb-4');
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
|
||||
// Reset scroll position to start? or keep?
|
||||
menu.scrollLeft = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Re-run Lucide mainly for the chevron if this is loaded via PJAX (though sidebar is usually persistent in SPA layout?
|
||||
// Wait, in PJAX we replace content, not the sidebar if it's outside.
|
||||
// BUT sidebar_settings.php is INSIDE the view in the current PHP architecture.
|
||||
// So it gets re-rendered on every navigation if we don't change that.
|
||||
// The current SPA script replaces `#settings-content-area`.
|
||||
// We need to move the sidebar OUT of the `#settings-content-area` target in the PHP files if we want it to persist...
|
||||
// OR we just re-init the script. Since it's inline, it runs on content injection.
|
||||
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
</script>
|
||||
Reference in New Issue
Block a user