// SPA Router for LibrePortal - Handles navigation without page reloads class Router { constructor() { this.routes = new Map(); this.currentRoute = null; this.loadingBar = null; this.contentContainer = null; this.init(); } init() { // Create loading bar this.createLoadingBar(); // Get content container this.contentContainer = document.getElementById('main-content') || document.querySelector('.main'); //console.log('Router: Content container found:', !!this.contentContainer, this.contentContainer); // Handle browser back/forward window.addEventListener('popstate', (e) => { if (e.state && e.state.route) { this.navigate(e.state.route, false); } }); // Don't handle initial route here - let SPA handle it after routes are registered //console.log('Router: Initialized, waiting for SPA to handle initial navigation'); } createLoadingBar() { // Create neon loading bar this.loadingBar = document.createElement('div'); this.loadingBar.className = 'neon-loading-bar'; this.loadingBar.innerHTML = `
`; // Insert after header const header = document.getElementById('topbar-container'); if (header) { header.insertAdjacentElement('afterend', this.loadingBar); } // Add CSS this.addLoadingBarStyles(); } addLoadingBarStyles() { const style = document.createElement('style'); style.textContent = ` .neon-loading-bar { position: fixed; top: 60px; left: 0; right: 0; height: 3px; background: rgba(0, 0, 0, 0.3); z-index: 1000; opacity: 0; transition: opacity 0.3s ease; } .neon-loading-bar.active { opacity: 1; } .neon-loading-bar-inner { position: relative; width: 100%; height: 100%; overflow: hidden; } .neon-loading-bar-progress { height: 100%; width: 0%; background: linear-gradient(90deg, #00d4ff, #0099ff, #00d4ff); transition: width 0.3s ease; box-shadow: 0 0 10px #00d4ff; animation: neonPulse 1.5s ease-in-out infinite; } .neon-loading-bar-glow { position: absolute; top: 0; left: 0; right: 0; height: 100%; background: linear-gradient(90deg, transparent, #00d4ff, transparent); opacity: 0.6; animation: neonGlow 2s ease-in-out infinite; } @keyframes neonPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } @keyframes neonGlow { 0%, 100% { opacity: 0.3; transform: scaleX(1); } 50% { opacity: 0.8; transform: scaleX(1.1); } } .neon-loading-bar.complete .neon-loading-bar-progress { width: 100%; animation: none; } .neon-loading-bar.complete .neon-loading-bar-glow { animation: none; opacity: 0; } `; document.head.appendChild(style); } // Register a route register(path, handler) { this.routes.set(path, handler); //console.log('Router: Registered route:', path); } // Navigate to a route async navigate(path, addToHistory = true) { if (this.currentRoute === path) return; // Show loading bar this.showLoadingBar(); // Update browser history if (addToHistory) { history.pushState({ route: path }, '', path); } this.currentRoute = path; try { //console.log('Router: Navigating to:', path); //console.log('Router: Available routes:', Array.from(this.routes.keys())); // Find matching route - simple wildcard matching let handler = null; if (this.routes.has(path)) { // Exact match handler = this.routes.get(path); } else { // Wildcard matching for (const [route, routeHandler] of this.routes) { if (route.includes('*')) { const routePattern = route.replace('*', ''); if (path.startsWith(routePattern)) { handler = routeHandler; break; } } } } if (handler) { //console.log('Router: Found handler for:', path); await handler(); } else { console.warn('Router: No handler found for:', path); console.warn('Router: Available routes were:', Array.from(this.routes.keys())); this.hideLoadingBar(); } } catch (error) { console.error('Navigation error:', error); this.hideLoadingBar(); } } showLoadingBar() { if (this.loadingBar) { this.loadingBar.classList.add('active'); // Reset progress const progress = this.loadingBar.querySelector('.neon-loading-bar-progress'); if (progress) { progress.style.width = '0%'; } } } updateProgress(percent) { if (this.loadingBar) { const progress = this.loadingBar.querySelector('.neon-loading-bar-progress'); if (progress) { progress.style.width = `${percent}%`; } } } hideLoadingBar() { if (this.loadingBar) { // Complete animation this.updateProgress(100); setTimeout(() => { this.loadingBar.classList.add('complete'); setTimeout(() => { this.loadingBar.classList.remove('active', 'complete'); const progress = this.loadingBar.querySelector('.neon-loading-bar-progress'); if (progress) { progress.style.width = '0%'; } }, 300); }, 200); } } // Load content dynamically async loadContent(content, title = null) { //console.log('Router: Loading content with title:', title); if (!this.contentContainer) { console.error('Router: No content container found'); return; } // Clear current content this.contentContainer.innerHTML = ''; // Update page title if (title) { document.title = `${title} - LibrePortal`; } // Add new content this.contentContainer.innerHTML = content; //console.log('Router: Content loaded successfully, container innerHTML length:', this.contentContainer.innerHTML.length); // Update active nav state this.updateActiveNav(); } updateActiveNav() { // Always clear all navigation first to prevent sticky highlighting if (typeof window.topbarNavigationHighlighting === 'function') { window.topbarNavigationHighlighting(); return; // Exit early - let topbar handle it } // Fallback to original logic if helper not available // Remove active class from all nav items document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); // Use path-based detection only for consistency const pathname = window.location.pathname; let activeNavId; // PRIMARY: Use path-based detection only (most reliable) if (pathname.startsWith('/app') || pathname.startsWith('/apps')) { activeNavId = 'nav-app-center'; } else if (pathname.startsWith('/admin') || pathname.startsWith('/config') || pathname.startsWith('/ssh')) { activeNavId = 'nav-config'; } else if (pathname.startsWith('/tasks')) { activeNavId = 'nav-tasks'; } else if (pathname === '/' || pathname === '/dashboard') { activeNavId = 'nav-dashboard'; } else { activeNavId = 'nav-dashboard'; // default } if (activeNavId) { const activeNav = document.getElementById(activeNavId); if (activeNav) { activeNav.classList.add('nav-active'); } } } } // Global router instance const router = new Router();