Replace the Admin area's ?= query URLs with clean, hierarchical paths that mirror the breadcrumb: /admin -> Overview /admin/config/<category>-> Config / <category> /admin/tools/ssh-access -> Tools / SSH Access New /admin (+ /admin*) SPA route -> handleAdmin, which parses the path via the shared window.adminPath / window.adminCategoryFromPath helpers and renders through the existing ConfigManager. Legacy /config, /config?=<x> and /ssh now redirect into the matching /admin path, so old links/bookmarks keep working (server already serves index.html for any depth). Sidebar, Admin Overview, dashboard link and top-nav now build /admin paths; active-nav + config data loading recognise /admin across spa.js, topbar.js, router.js, data-loader.js. Scope: Admin area only — /app, /apps, /tasks, /backup keep their existing ?= URLs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
288 lines
7.7 KiB
JavaScript
Executable File
288 lines
7.7 KiB
JavaScript
Executable File
// 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 = `
|
|
<div class="neon-loading-bar-inner">
|
|
<div class="neon-loading-bar-progress"></div>
|
|
<div class="neon-loading-bar-glow"></div>
|
|
</div>
|
|
`;
|
|
|
|
// 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();
|