mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 13:31:56 +07:00
Chore: Bump version to v1.1.0 and implement automated release system
This commit is contained in:
@@ -6,9 +6,26 @@
|
||||
<?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>
|
||||
<footer class="border-t border-accents-2 bg-background mt-auto transition-colors duration-200 py-8 text-center space-y-4">
|
||||
<!-- Links Row -->
|
||||
<div class="flex justify-center items-center gap-6 text-sm font-medium text-accents-5">
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span>Repo</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Copyright Row -->
|
||||
<div class="text-xs text-accents-4 opacity-50">
|
||||
<?= \App\Config\SiteConfig::getFooter() ?>
|
||||
</div>
|
||||
</footer>
|
||||
<?php endif; ?>
|
||||
@@ -129,20 +146,67 @@
|
||||
</script>
|
||||
<script>
|
||||
// Global Dropdown & Sidebar Logic
|
||||
let menuTimeout;
|
||||
|
||||
function toggleMenu(menuId, button) {
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
|
||||
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')) {
|
||||
// Handle Dropdowns (IDs start with 'lang-', 'session-', or is 'notification-')
|
||||
if (menuId.startsWith('lang-') || menuId === 'session-dropdown' || menuId === 'notification-dropdown') {
|
||||
const sidebarHeader = document.getElementById('sidebar-header');
|
||||
const isOpening = menu.classList.contains('invisible');
|
||||
|
||||
if (isOpening) {
|
||||
// Smart Positioning Logic
|
||||
// 1. Reset to base state (remove specific overrides to measure natural/preferred state)
|
||||
// But we want to preserve 'absolute' etc. The HTML has 'left-1/2 -translate-x-1/2' by default for sidebar.
|
||||
// We'll calculate based on button rect and assumed menu width (w-48 = 12rem = 192px approx, or measure)
|
||||
|
||||
const btnRect = button.getBoundingClientRect();
|
||||
const menuWidth = 192; // Approx w-48 standard. Better to measure if possible, but invisible elements have width.
|
||||
// Actually, if we make it visible but opacity-0 first, we can measure.
|
||||
// But simpler math:
|
||||
const centerX = btnRect.left + (btnRect.width / 2);
|
||||
const leftEdge = centerX - (menuWidth / 2);
|
||||
const rightEdge = centerX + (menuWidth / 2);
|
||||
|
||||
// Remove conflicting positioning classes first to ensure a clean slate if we need to override
|
||||
menu.classList.remove('left-0', 'right-0', 'left-1/2', '-translate-x-1/2', 'origin-top-left', 'origin-top-right', 'origin-top', 'left-3');
|
||||
|
||||
// Decision Tree
|
||||
if (leftEdge < 10) {
|
||||
// overflow left -> Align Left
|
||||
menu.classList.add('left-0', 'origin-top-left');
|
||||
} else if (rightEdge > window.innerWidth - 10) {
|
||||
// overflow right -> Align Right
|
||||
menu.classList.add('right-0', 'origin-top-right');
|
||||
} else {
|
||||
// Safe to Center
|
||||
menu.classList.add('left-1/2', '-translate-x-1/2', 'origin-top');
|
||||
}
|
||||
|
||||
// Open
|
||||
menu.classList.remove('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
menu.classList.add('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
|
||||
// Special Case: Sidebar Lang Dropdown needs overflow visible on header
|
||||
if (menuId === 'lang-dropdown-sidebar' && sidebarHeader) {
|
||||
sidebarHeader.classList.remove('overflow-hidden');
|
||||
sidebarHeader.classList.add('overflow-visible');
|
||||
}
|
||||
} else {
|
||||
// Close
|
||||
menu.classList.add('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
menu.classList.remove('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
|
||||
// Revert Overflow
|
||||
if (menuId === 'lang-dropdown-sidebar' && sidebarHeader) {
|
||||
sidebarHeader.classList.add('overflow-hidden');
|
||||
sidebarHeader.classList.remove('overflow-visible');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -175,20 +239,22 @@
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdowns = document.querySelectorAll('[id^="lang-dropdown"], #session-dropdown');
|
||||
const dropdowns = document.querySelectorAll('[id^="lang-dropdown"], #session-dropdown, #notification-dropdown');
|
||||
dropdowns.forEach(dropdown => {
|
||||
const sidebarHeader = document.getElementById('sidebar-header');
|
||||
|
||||
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');
|
||||
|
||||
// Revert Sidebar Overflow if needed
|
||||
if (dropdown.id === 'lang-dropdown-sidebar' && sidebarHeader) {
|
||||
sidebarHeader.classList.add('overflow-hidden');
|
||||
sidebarHeader.classList.remove('overflow-visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -209,18 +275,36 @@
|
||||
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)'
|
||||
});
|
||||
Mivo.alert('error', 'Action Failed', data.error || 'Unknown error occurred.');
|
||||
}
|
||||
} catch (err) {
|
||||
Mivo.toast('error', 'Connection Error', 'Failed to reach the server.');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Close Helper with Debounce
|
||||
function closeMenu(menuId) {
|
||||
if (menuTimeout) clearTimeout(menuTimeout);
|
||||
|
||||
// Notification dropdown is more "sticky" (800ms vs 300ms elsewhere)
|
||||
const delay = (menuId === 'notification-dropdown') ? 800 : 300;
|
||||
|
||||
menuTimeout = setTimeout(() => {
|
||||
const menu = document.getElementById(menuId);
|
||||
const sidebarHeader = document.getElementById('sidebar-header');
|
||||
|
||||
if (menu && !menu.classList.contains('invisible')) {
|
||||
menu.classList.add('opacity-0', 'scale-95', 'invisible', 'pointer-events-none');
|
||||
menu.classList.remove('opacity-100', 'scale-100', 'visible', 'pointer-events-auto');
|
||||
|
||||
// Revert Overflow if needed
|
||||
if (menuId === 'lang-dropdown-sidebar' && sidebarHeader) {
|
||||
sidebarHeader.classList.add('overflow-hidden');
|
||||
sidebarHeader.classList.remove('overflow-visible');
|
||||
}
|
||||
}
|
||||
}, 300); // 300ms delay to prevent accidental closure
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
<footer class="mt-auto py-6 text-center text-xs text-accents-5 opacity-60">
|
||||
<?= \App\Config\SiteConfig::getFooter() ?>
|
||||
<footer class="mt-auto py-8 text-center space-y-4">
|
||||
<div class="flex justify-center items-center gap-6 text-sm font-medium text-accents-5">
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span>Repo</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Copyright Row -->
|
||||
<div class="text-xs text-accents-4 opacity-50">
|
||||
<?= \App\Config\SiteConfig::getFooter() ?>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -38,34 +56,17 @@
|
||||
|
||||
// Use Custom Toasts for most notifications (Success, Info, Error)
|
||||
// Only use Modal (Swal) for specific heavy warnings or questions if needed
|
||||
// Use Toasts for standard notifications
|
||||
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}`);
|
||||
// For questions or other types, use Modal Alert
|
||||
if (window.Mivo && window.Mivo.alert) {
|
||||
Mivo.alert(type || 'info', title, message);
|
||||
} else if (typeof Swal !== 'undefined') {
|
||||
Swal.fire(title, message, type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,12 +54,16 @@ $title = isset($title) ? $title : \App\Config\SiteConfig::APP_NAME;
|
||||
</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>
|
||||
window.currentVersion = '<?= \App\Config\SiteConfig::APP_VERSION ?>';
|
||||
</script>
|
||||
<script src="/assets/js/mivo.js" defer></script>
|
||||
<script src="/assets/js/modules/updater.js" defer></script>
|
||||
<script src="/assets/js/components/select.js" defer></script>
|
||||
<script src="/assets/js/components/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>
|
||||
<script src="/assets/js/modules/alert.js" defer></script>
|
||||
<script src="/assets/js/modules/i18n.js" defer></script>
|
||||
|
||||
<style>
|
||||
/* Global Form Input Style - Matches Vercel Design System */
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
<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>
|
||||
<script src="/assets/js/mivo.js" defer></script>
|
||||
<script src="/assets/js/modules/alert.js" defer></script>
|
||||
<script src="/assets/js/modules/i18n.js" defer></script>
|
||||
<style>
|
||||
/* Custom Keyframes */
|
||||
@keyframes fade-in-up {
|
||||
@@ -39,30 +40,126 @@
|
||||
<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>
|
||||
<!-- Top Right Controls (Pill Theme Toggle & Lang Switcher) -->
|
||||
<div class="fixed top-4 right-4 z-50 flex items-center space-x-3">
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group">
|
||||
<button onclick="toggleMenu('lang-dropdown-public', this)" class="h-9 px-3 rounded-full bg-background/50 backdrop-blur-md border border-accents-2 hover:border-foreground/20 text-accents-5 hover:text-foreground transition-all flex items-center shadow-sm">
|
||||
<i data-lucide="globe" class="w-4 h-4 mr-2"></i>
|
||||
<span class="text-xs font-semibold uppercase tracking-wider" id="current-lang-label">EN</span>
|
||||
<i data-lucide="chevron-down" class="w-3 h-3 ml-2 opacity-50"></i>
|
||||
</button>
|
||||
<!-- Dropdown -->
|
||||
<div id="lang-dropdown-public" class="hidden absolute right-0 mt-2 w-32 bg-background/95 backdrop-blur-2xl border border-white/10 rounded-xl shadow-2xl py-1 z-50 transform origin-top-right transition-all duration-200" onmouseleave="closeMenu('lang-dropdown-public')">
|
||||
<button onclick="changeLanguage('en')" class="w-full text-left px-4 py-2 text-xs font-medium text-accents-5 hover:text-foreground hover:bg-white/5 flex items-center group">
|
||||
<span class="mr-2 text-lg">🇺🇸</span> English
|
||||
</button>
|
||||
<button onclick="changeLanguage('id')" class="w-full text-left px-4 py-2 text-xs font-medium text-accents-5 hover:text-foreground hover:bg-white/5 flex items-center group">
|
||||
<span class="mr-2 text-lg">🇮🇩</span> Indonesia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Toggle Pill -->
|
||||
<div class="h-9 p-1 bg-accents-2/50 backdrop-blur-md border border-accents-2 rounded-full flex items-center relative" id="theme-pill">
|
||||
<!-- Gliding Background -->
|
||||
<div class="absolute top-1 bottom-1 w-[calc(50%-4px)] bg-background rounded-full shadow-sm transition-all duration-300 ease-spring" id="theme-glider" style="left: 4px;"></div>
|
||||
|
||||
<button onclick="setTheme('light')" class="relative z-10 w-8 h-full flex items-center justify-center text-accents-5 hover:text-foreground transition-colors rounded-full" id="btn-light">
|
||||
<i data-lucide="sun" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button onclick="setTheme('dark')" class="relative z-10 w-8 h-full flex items-center justify-center text-accents-5 hover:text-foreground transition-colors rounded-full" id="btn-dark">
|
||||
<i data-lucide="moon" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle Menu Helper (Reuse or define for public if main footer not loaded)
|
||||
// Public footer includes site config footer, but maybe not main JS.
|
||||
// Let's define simple toggle for public page to be safe and independent.
|
||||
function toggleMenu(id, btn) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const isHidden = el.classList.contains('hidden');
|
||||
|
||||
// Close others if needed (optional)
|
||||
|
||||
if (isHidden) {
|
||||
el.classList.remove('hidden', 'scale-95', 'opacity-0');
|
||||
el.classList.add('scale-100', 'opacity-100');
|
||||
} else {
|
||||
closeMenu(id);
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el && !el.classList.contains('hidden')) {
|
||||
el.classList.remove('scale-100', 'opacity-100');
|
||||
el.classList.add('hidden', 'scale-95', 'opacity-0');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
lucide.createIcons();
|
||||
|
||||
// Theme Toggle Logic
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
// Theme Logic
|
||||
const glider = document.getElementById('theme-glider');
|
||||
const btnLight = document.getElementById('btn-light');
|
||||
const btnDark = document.getElementById('btn-dark');
|
||||
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';
|
||||
}
|
||||
});
|
||||
window.setTheme = (theme) => {
|
||||
if (theme === 'dark') {
|
||||
htmlElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
glider.style.transform = 'translateX(100%)';
|
||||
// adjustment: logic depends on width.
|
||||
// container is w-8+w-8+padding.
|
||||
// simplest is just left/right toggle classes or transform.
|
||||
// using transform translateX(100%) works if width is exactly 50% parent minus padding.
|
||||
// padding is 1 (4px). buttons are w-8 (32px).
|
||||
// let's use explicit left style or class-based positioning if easier.
|
||||
// Tailwind 'translate-x-full' moves 100% of own width.
|
||||
// If glider is w-[calc(50%-4px)], moving 100% of itself is almost correct but includes gap.
|
||||
// Let's rely on simple pixel math or percentage relative to parent?
|
||||
// actually `left: 4px` vs `left: calc(100% - width - 4px)`.
|
||||
glider.style.left = 'auto';
|
||||
glider.style.right = '4px';
|
||||
} else {
|
||||
htmlElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
glider.style.right = 'auto';
|
||||
glider.style.left = '4px';
|
||||
}
|
||||
|
||||
// Update Active Colors
|
||||
if (theme === 'dark') {
|
||||
btnDark.classList.add('text-foreground');
|
||||
btnLight.classList.remove('text-foreground');
|
||||
} else {
|
||||
btnLight.classList.add('text-foreground');
|
||||
btnDark.classList.remove('text-foreground');
|
||||
}
|
||||
};
|
||||
|
||||
// Init
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
|
||||
// Language Init (Mock)
|
||||
const currentLang = localStorage.getItem('mivo_lang') || 'en';
|
||||
const langLabel = document.getElementById('current-lang-label');
|
||||
if(langLabel) langLabel.innerText = currentLang.toUpperCase();
|
||||
|
||||
window.changeLanguage = (lang) => {
|
||||
localStorage.setItem('mivo_lang', lang);
|
||||
// Reload or use i18n module to reload
|
||||
location.reload();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -15,28 +15,46 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
</a>
|
||||
|
||||
<!-- Desktop Navigation Links (Hidden on Mobile) -->
|
||||
<?php if(isset($_SESSION['user_id'])): ?>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
<!-- Notification Bell -->
|
||||
<div class="relative group" onmouseleave="closeMenu('notification-dropdown')">
|
||||
<button id="notification-bell" type="button" class="pill-lang-btn relative" onclick="toggleMenu('notification-dropdown', this)" title="Notifications">
|
||||
<i data-lucide="bell" class="w-4 h-4"></i>
|
||||
<span id="update-badge" class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full hidden animate-pulse"></span>
|
||||
</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 id="notification-dropdown" class="absolute right-0 top-full mt-3 w-64 bg-background/95 backdrop-blur-2xl 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 dropdown-bridge">
|
||||
<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="notifications.title">Notifications</div>
|
||||
<div id="notification-content" class="p-4 text-sm text-accents-5 text-center" data-i18n="notifications.empty">
|
||||
No new notifications
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pill-divider"></div>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group" onmouseleave="closeMenu('lang-dropdown-desktop')">
|
||||
<button type="button" class="pill-lang-btn" onclick="toggleMenu('lang-dropdown-desktop', this)" title="Change Language">
|
||||
<i data-lucide="languages" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<div id="lang-dropdown-desktop" class="absolute right-0 top-full mt-3 w-48 bg-background/95 backdrop-blur-2xl 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 dropdown-bridge">
|
||||
<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">
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $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>
|
||||
@@ -44,8 +62,6 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
</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>
|
||||
@@ -88,6 +104,7 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<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 -->
|
||||
<?php if(isset($_SESSION['user_id'])): ?>
|
||||
<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>
|
||||
@@ -98,6 +115,7 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Mobile Controls Overlay -->
|
||||
<div class="p-4 rounded-2xl bg-accents-1/50 border border-accents-2 space-y-4">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$isDashboard = strpos($uri, '/dashboard') !== false;
|
||||
$isGenerate = strpos($uri, '/hotspot/generate') !== false;
|
||||
$isTemplates = strpos($uri, '/settings/templates') !== false;
|
||||
$isTemplates = strpos($uri, '/settings/voucher-templates') !== false;
|
||||
$isSettings = ($uri === '/settings' || strpos($uri, '/settings/') !== false) && !$isTemplates;
|
||||
|
||||
// Hotspot Group Active Check
|
||||
@@ -106,7 +106,7 @@ $getInitials = function($name) {
|
||||
<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 id="sidebar-header" 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">
|
||||
@@ -119,17 +119,19 @@ $getInitials = function($name) {
|
||||
<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">
|
||||
<!-- Language Switcher (Mivo Component) -->
|
||||
<!-- Language Switcher -->
|
||||
<div class="relative group/lang" onmouseleave="closeMenu('lang-dropdown-sidebar')">
|
||||
<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 id="lang-dropdown-sidebar" class="absolute left-1/2 -translate-x-1/2 top-full mt-3 w-48 bg-background/95 backdrop-blur-2xl 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 dropdown-bridge" onmouseenter="if(typeof menuTimeout !== 'undefined') clearTimeout(menuTimeout)">
|
||||
<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">
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $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>
|
||||
@@ -163,7 +165,7 @@ $getInitials = function($name) {
|
||||
<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">
|
||||
<div class="px-3 mb-6 relative" onmouseleave="closeMenu('session-dropdown')">
|
||||
<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">
|
||||
@@ -185,7 +187,7 @@ $getInitials = function($name) {
|
||||
</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 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 dropdown-bridge" onmouseenter="if(typeof menuTimeout !== 'undefined') clearTimeout(menuTimeout)">
|
||||
<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
|
||||
@@ -377,11 +379,37 @@ $getInitials = function($name) {
|
||||
</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' ?>">
|
||||
<a href="/settings/voucher-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>
|
||||
|
||||
<!-- Support Separator -->
|
||||
<div class="pt-4 pb-1 px-3">
|
||||
<div class="text-xs font-semibold text-accents-5 uppercase tracking-wider" data-i18n="sidebar.support">Support</div>
|
||||
</div>
|
||||
|
||||
<!-- Docs -->
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.docs">Documentation</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
</a>
|
||||
|
||||
<!-- Community -->
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.community">Community</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
</a>
|
||||
|
||||
<!-- Repo -->
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.repo">Repository</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
@@ -435,7 +463,7 @@ $getInitials = function($name) {
|
||||
<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 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 dropdown-bridge" onmouseenter="if(typeof menuTimeout !== 'undefined') clearTimeout(menuTimeout)">
|
||||
<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();
|
||||
|
||||
@@ -11,7 +11,7 @@ function isActive($path, $current) {
|
||||
$menu = [
|
||||
['label' => 'routers_title', 'url' => '/settings', 'namespace' => 'settings'],
|
||||
['label' => 'system', 'url' => '/settings/system', 'namespace' => 'settings'],
|
||||
['label' => 'templates_title', 'url' => '/settings/templates', 'namespace' => 'settings'],
|
||||
['label' => 'templates_title', 'url' => '/settings/voucher-templates', 'namespace' => 'settings'],
|
||||
['label' => 'logos_title', 'url' => '/settings/logos', 'namespace' => 'settings'],
|
||||
['label' => 'api_cors_title', 'url' => '/settings/api-cors', 'namespace' => 'settings'],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user