feat: Overhaul client-side navigation and clean up project
- Implement a unified SPA routing system in nav.js, removing all legacy and conflicting navigation scripts (router.js, inject-nav.js, fix-nav.js). - Refactor dashboard.js to delegate all navigation handling to the new nav.js module. - Create new modular JS files (auth.js, personal-player.js, logger.js) to improve code organization. - Fix all navigation-related bugs, including guest access and broken footer links. - Clean up the project root by moving development scripts and backups to a dedicated /dev directory. - Add a .gitignore file to exclude the database, logs, and other transient files from the repository.
This commit is contained in:
515
static/nav.js
515
static/nav.js
@ -7,468 +7,97 @@ function getCookie(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Check authentication status
|
||||
const isLoggedIn = !!getCookie('uid');
|
||||
|
||||
// Update body class for CSS-based visibility
|
||||
document.body.classList.toggle('logged-in', isLoggedIn);
|
||||
|
||||
// Get all main content sections
|
||||
const mainSections = Array.from(document.querySelectorAll('main > section'));
|
||||
|
||||
// Show/hide sections with smooth transitions
|
||||
const showSection = (sectionId) => {
|
||||
// Update body class to indicate current page
|
||||
document.body.className = '';
|
||||
if (sectionId) {
|
||||
document.body.classList.add(`page-${sectionId}`);
|
||||
// Determines the correct section to show based on auth status and requested section
|
||||
function getValidSection(sectionId) {
|
||||
const isLoggedIn = !!getCookie('uid');
|
||||
const protectedSections = ['me-page', 'account-page'];
|
||||
const guestOnlySections = ['login-page', 'register-page', 'magic-login-page'];
|
||||
|
||||
if (isLoggedIn) {
|
||||
// If logged in, guest-only sections are invalid, redirect to 'me-page'
|
||||
if (guestOnlySections.includes(sectionId)) {
|
||||
return 'me-page';
|
||||
}
|
||||
} else {
|
||||
document.body.classList.add('page-welcome');
|
||||
// If not logged in, protected sections are invalid, redirect to 'welcome-page'
|
||||
if (protectedSections.includes(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
}
|
||||
|
||||
// If the section doesn't exist in the DOM, default to welcome page
|
||||
if (!document.getElementById(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
|
||||
return sectionId;
|
||||
}
|
||||
|
||||
// Main function to show/hide sections
|
||||
export function showSection(sectionId) {
|
||||
const mainSections = Array.from(document.querySelectorAll('main > section'));
|
||||
|
||||
// Update body class for page-specific CSS
|
||||
document.body.className = document.body.className.replace(/page-\S+/g, '');
|
||||
document.body.classList.add(`page-${sectionId || 'welcome-page'}`);
|
||||
|
||||
// Update active state of navigation links
|
||||
document.querySelectorAll('.dashboard-nav a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if ((!sectionId && link.getAttribute('href') === '#welcome-page') ||
|
||||
(sectionId && link.getAttribute('href') === `#${sectionId}`)) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${sectionId}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
mainSections.forEach(section => {
|
||||
// Skip navigation sections
|
||||
if (section.id === 'guest-dashboard' || section.id === 'user-dashboard') {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTarget = section.id === sectionId;
|
||||
const isLegalPage = ['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId);
|
||||
const isWelcomePage = !sectionId || sectionId === 'welcome-page';
|
||||
|
||||
if (isTarget || (isLegalPage && section.id === sectionId)) {
|
||||
// Show the target section or legal page
|
||||
section.classList.add('active');
|
||||
section.hidden = false;
|
||||
|
||||
// Focus the section for accessibility with a small delay
|
||||
// Only focus if the section is focusable and in the viewport
|
||||
const focusSection = () => {
|
||||
try {
|
||||
if (section && typeof section.focus === 'function' &&
|
||||
section.offsetParent !== null && // Check if element is visible
|
||||
section.getBoundingClientRect().top < window.innerHeight &&
|
||||
section.getBoundingClientRect().bottom > 0) {
|
||||
section.focus({ preventScroll: true });
|
||||
section.hidden = section.id !== sectionId;
|
||||
});
|
||||
|
||||
// Update URL hash without causing a page scroll, this is for direct calls to showSection
|
||||
// Normal navigation is handled by the hashchange listener
|
||||
const currentHash = `#${sectionId}`;
|
||||
if (window.location.hash !== currentHash) {
|
||||
if (history.pushState) {
|
||||
if (sectionId && sectionId !== 'welcome-page') {
|
||||
history.pushState(null, null, currentHash);
|
||||
} else {
|
||||
history.pushState(null, null, window.location.pathname + window.location.search);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail if focusing isn't possible
|
||||
if (window.DEBUG_NAV || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
||||
console.debug('Could not focus section:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use requestAnimationFrame for better performance
|
||||
requestAnimationFrame(() => {
|
||||
// Only set the timeout in debug mode or local development
|
||||
if (window.DEBUG_NAV || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
||||
setTimeout(focusSection, 50);
|
||||
} else {
|
||||
focusSection();
|
||||
}
|
||||
});
|
||||
} else if (isWelcomePage && section.id === 'welcome-page') {
|
||||
// Special handling for welcome page
|
||||
section.classList.add('active');
|
||||
section.hidden = false;
|
||||
} else {
|
||||
// Hide other sections
|
||||
section.classList.remove('active');
|
||||
section.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL hash without page scroll
|
||||
if (sectionId && !['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId)) {
|
||||
if (sectionId === 'welcome-page') {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
} else {
|
||||
history.replaceState(null, '', `#${sectionId}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial page load
|
||||
const getValidSection = (sectionId) => {
|
||||
const protectedSections = ['me-page', 'register-page'];
|
||||
|
||||
// If not logged in and trying to access protected section
|
||||
if (!isLoggedIn && protectedSections.includes(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
|
||||
// If section doesn't exist, default to welcome page
|
||||
if (!document.getElementById(sectionId)) {
|
||||
return 'welcome-page';
|
||||
}
|
||||
|
||||
return sectionId;
|
||||
};
|
||||
|
||||
// Process initial page load
|
||||
const initialPage = window.location.hash.substring(1) || 'welcome-page';
|
||||
const validSection = getValidSection(initialPage);
|
||||
|
||||
// Update URL if needed
|
||||
if (validSection !== initialPage) {
|
||||
window.location.hash = validSection;
|
||||
}
|
||||
|
||||
// Show the appropriate section
|
||||
showSection(validSection);
|
||||
|
||||
const Router = {
|
||||
sections: Array.from(document.querySelectorAll("main > section")),
|
||||
|
||||
showOnly(id) {
|
||||
// Validate the section ID
|
||||
const validId = getValidSection(id);
|
||||
|
||||
// Update URL if needed
|
||||
if (validId !== id) {
|
||||
window.location.hash = validId;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the requested section
|
||||
showSection(validId);
|
||||
|
||||
// Handle the quota meter visibility - only show with 'me-page'
|
||||
const quotaMeter = document.getElementById('quota-meter');
|
||||
if (quotaMeter) {
|
||||
quotaMeter.hidden = validId !== 'me-page';
|
||||
quotaMeter.tabIndex = validId === 'me-page' ? 0 : -1;
|
||||
}
|
||||
|
||||
// Update navigation active states
|
||||
this.updateActiveNav(validId);
|
||||
},
|
||||
|
||||
updateActiveNav(activeId) {
|
||||
// Update active states for navigation links
|
||||
document.querySelectorAll('.dashboard-nav a').forEach(link => {
|
||||
const target = link.getAttribute('href').substring(1);
|
||||
if (target === activeId) {
|
||||
link.setAttribute('aria-current', 'page');
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.removeAttribute('aria-current');
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the router
|
||||
const router = Router;
|
||||
|
||||
// Handle section visibility based on authentication
|
||||
const updateSectionVisibility = (sectionId) => {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (!section) return;
|
||||
|
||||
// Skip navigation sections and quota meter
|
||||
if (['guest-dashboard', 'user-dashboard', 'quota-meter'].includes(sectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHash = window.location.hash.substring(1);
|
||||
const isLegalPage = ['terms-page', 'privacy-page', 'imprint-page'].includes(sectionId);
|
||||
|
||||
// Special handling for legal pages - always show when in hash
|
||||
if (isLegalPage) {
|
||||
const isActive = sectionId === currentHash;
|
||||
section.hidden = !isActive;
|
||||
section.tabIndex = isActive ? 0 : -1;
|
||||
if (isActive) section.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for me-page - only show to authenticated users
|
||||
if (sectionId === 'me-page') {
|
||||
section.hidden = !isLoggedIn || currentHash !== 'me-page';
|
||||
section.tabIndex = (isLoggedIn && currentHash === 'me-page') ? 0 : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for register page - only show to guests
|
||||
if (sectionId === 'register-page') {
|
||||
section.hidden = isLoggedIn || currentHash !== 'register-page';
|
||||
section.tabIndex = (!isLoggedIn && currentHash === 'register-page') ? 0 : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// For other sections, show if they match the current section ID
|
||||
const isActive = sectionId === currentHash;
|
||||
section.hidden = !isActive;
|
||||
section.tabIndex = isActive ? 0 : -1;
|
||||
|
||||
if (isActive) {
|
||||
section.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the router
|
||||
router.init = function() {
|
||||
// Update visibility for all sections
|
||||
this.sections.forEach(section => {
|
||||
updateSectionVisibility(section.id);
|
||||
});
|
||||
|
||||
// Show user-upload-area only when me-page is shown and user is logged in
|
||||
const userUpload = document.getElementById("user-upload-area");
|
||||
if (userUpload) {
|
||||
const uid = getCookie("uid");
|
||||
userUpload.style.display = (window.location.hash === '#me-page' && uid) ? '' : 'none';
|
||||
}
|
||||
|
||||
// Store the current page
|
||||
localStorage.setItem("last_page", window.location.hash.substring(1));
|
||||
|
||||
// Initialize navigation
|
||||
initNavLinks();
|
||||
initBackButtons();
|
||||
initStreamLinks();
|
||||
|
||||
// Ensure proper focus management for accessibility
|
||||
const currentSection = document.querySelector('main > section:not([hidden])');
|
||||
if (currentSection) {
|
||||
currentSection.setAttribute('tabindex', '0');
|
||||
currentSection.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the router
|
||||
router.init();
|
||||
|
||||
// Handle footer links
|
||||
document.querySelectorAll('.footer-links a').forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.target;
|
||||
if (target) {
|
||||
// Update URL hash to maintain proper history state
|
||||
window.location.hash = target;
|
||||
// Use the router to handle the navigation
|
||||
if (router && typeof router.showOnly === 'function') {
|
||||
router.showOnly(target);
|
||||
} else {
|
||||
// Fallback to showSection if router is not available
|
||||
showSection(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export the showOnly function for global access
|
||||
window.showOnly = router.showOnly.bind(router);
|
||||
|
||||
// Make router available globally for debugging
|
||||
window.appRouter = router;
|
||||
}
|
||||
|
||||
// Highlight active profile link on browser back/forward navigation
|
||||
function highlightActiveProfileLink() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const profileUid = params.get('profile');
|
||||
const ul = document.getElementById('stream-list');
|
||||
if (!ul) return;
|
||||
ul.querySelectorAll('a.profile-link').forEach(link => {
|
||||
const url = new URL(link.href, window.location.origin);
|
||||
const uidParam = url.searchParams.get('profile');
|
||||
link.classList.toggle('active', uidParam === profileUid);
|
||||
});
|
||||
}
|
||||
window.addEventListener('popstate', () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const profileUid = params.get('profile');
|
||||
const currentPage = window.location.hash.substring(1) || 'welcome-page';
|
||||
|
||||
// Prevent unauthorized access to me-page
|
||||
if ((currentPage === 'me-page' || profileUid) && !getCookie('uid')) {
|
||||
history.replaceState(null, '', '#welcome-page');
|
||||
showOnly('welcome-page');
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileUid) {
|
||||
showOnly('me-page');
|
||||
if (typeof window.showProfilePlayerFromUrl === 'function') {
|
||||
window.showProfilePlayerFromUrl();
|
||||
}
|
||||
} else {
|
||||
highlightActiveProfileLink();
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const isLoggedIn = !!getCookie('uid');
|
||||
document.body.classList.toggle('authenticated', isLoggedIn);
|
||||
|
||||
/* restore last page (unless magic‑link token present) */
|
||||
const params = new URLSearchParams(location.search);
|
||||
const token = params.get("token");
|
||||
if (!token) {
|
||||
const last = localStorage.getItem("last_page");
|
||||
if (last && document.getElementById(last)) {
|
||||
showOnly(last);
|
||||
} else if (document.getElementById("welcome-page")) {
|
||||
// Show Welcome page by default for all new/guest users
|
||||
showOnly("welcome-page");
|
||||
}
|
||||
// Highlight active link on initial load
|
||||
highlightActiveProfileLink();
|
||||
}
|
||||
// Unified click handler for SPA navigation
|
||||
document.body.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a[href^="#"]');
|
||||
// Ensure the link is not inside a component that handles its own navigation
|
||||
if (!link || link.closest('.no-global-nav')) return;
|
||||
|
||||
/* token → show magic‑login page */
|
||||
if (token) {
|
||||
document.getElementById("magic-token").value = token;
|
||||
showOnly("magic-login-page");
|
||||
const err = params.get("error");
|
||||
if (err) {
|
||||
const box = document.getElementById("magic-error");
|
||||
box.textContent = decodeURIComponent(err);
|
||||
box.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderStreamList(streams) {
|
||||
const ul = document.getElementById("stream-list");
|
||||
if (!ul) return;
|
||||
if (streams.length) {
|
||||
// Handle both array of UIDs (legacy) and array of stream objects (new)
|
||||
const streamItems = streams.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
// Legacy: array of UIDs
|
||||
return { uid: item, username: item };
|
||||
} else {
|
||||
// New: array of stream objects
|
||||
return {
|
||||
uid: item.uid || '',
|
||||
username: item.username || 'Unknown User'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
streamItems.sort((a, b) => (a.username || '').localeCompare(b.username || ''));
|
||||
ul.innerHTML = streamItems.map(stream => `
|
||||
<li><a href="/?profile=${encodeURIComponent(stream.uid)}" class="profile-link">▶ ${stream.username}</a></li>
|
||||
`).join("");
|
||||
} else {
|
||||
ul.innerHTML = "<li>No active streams.</li>";
|
||||
}
|
||||
// Ensure correct link is active after rendering
|
||||
highlightActiveProfileLink();
|
||||
}
|
||||
|
||||
// Initialize navigation listeners
|
||||
function initNavLinks() {
|
||||
const navIds = ["links", "user-dashboard", "guest-dashboard"];
|
||||
navIds.forEach(id => {
|
||||
const nav = document.getElementById(id);
|
||||
if (!nav) return;
|
||||
nav.addEventListener("click", e => {
|
||||
const a = e.target.closest("a[data-target]");
|
||||
if (!a || !nav.contains(a)) return;
|
||||
e.preventDefault();
|
||||
|
||||
// Save audio state before navigation
|
||||
const audio = document.getElementById('me-audio');
|
||||
const wasPlaying = audio && !audio.paused;
|
||||
const currentTime = audio ? audio.currentTime : 0;
|
||||
|
||||
const target = a.dataset.target;
|
||||
if (target) showOnly(target);
|
||||
|
||||
// Handle stream page specifically
|
||||
if (target === "stream-page" && typeof window.maybeLoadStreamsOnShow === "function") {
|
||||
window.maybeLoadStreamsOnShow();
|
||||
const newHash = link.getAttribute('href');
|
||||
if (window.location.hash !== newHash) {
|
||||
window.location.hash = newHash;
|
||||
}
|
||||
// Handle me-page specifically
|
||||
else if (target === "me-page" && audio) {
|
||||
// Restore audio state if it was playing
|
||||
if (wasPlaying) {
|
||||
audio.currentTime = currentTime;
|
||||
audio.play().catch(e => console.error('Play failed:', e));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add click handlers for footer links with audio state saving
|
||||
document.querySelectorAll(".footer-links a").forEach(link => {
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.target;
|
||||
if (!target) return;
|
||||
// Main routing logic on hash change
|
||||
const handleNavigation = () => {
|
||||
const sectionId = window.location.hash.substring(1) || 'welcome-page';
|
||||
const validSectionId = getValidSection(sectionId);
|
||||
|
||||
// Save audio state before navigation
|
||||
const audio = document.getElementById('me-audio');
|
||||
const wasPlaying = audio && !audio.paused;
|
||||
const currentTime = audio ? audio.currentTime : 0;
|
||||
|
||||
showOnly(target);
|
||||
|
||||
// Handle me-page specifically
|
||||
if (target === "me-page" && audio) {
|
||||
// Restore audio state if it was playing
|
||||
if (wasPlaying) {
|
||||
audio.currentTime = currentTime;
|
||||
audio.play().catch(e => console.error('Play failed:', e));
|
||||
}
|
||||
if (sectionId !== validSectionId) {
|
||||
window.location.hash = validSectionId; // This will re-trigger handleNavigation
|
||||
} else {
|
||||
showSection(validSectionId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initBackButtons() {
|
||||
document.querySelectorAll('a[data-back]').forEach(btn => {
|
||||
btn.addEventListener("click", e => {
|
||||
e.preventDefault();
|
||||
const target = btn.dataset.back;
|
||||
if (target) showOnly(target);
|
||||
// Ensure streams load instantly when stream-page is shown
|
||||
if (target === "stream-page" && typeof window.maybeLoadStreamsOnShow === "function") {
|
||||
window.maybeLoadStreamsOnShow();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleNavigation);
|
||||
|
||||
|
||||
function initStreamLinks() {
|
||||
const ul = document.getElementById("stream-list");
|
||||
if (!ul) return;
|
||||
ul.addEventListener("click", e => {
|
||||
const a = e.target.closest("a.profile-link");
|
||||
if (!a || !ul.contains(a)) return;
|
||||
e.preventDefault();
|
||||
const url = new URL(a.href, window.location.origin);
|
||||
const profileUid = url.searchParams.get("profile");
|
||||
if (profileUid && window.location.search !== `?profile=${encodeURIComponent(profileUid)}`) {
|
||||
window.profileNavigationTriggered = true;
|
||||
window.history.pushState({}, '', `/?profile=${encodeURIComponent(profileUid)}`);
|
||||
window.dispatchEvent(new Event("popstate"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Router
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
// Re-check authentication when tab becomes visible again
|
||||
if (!document.hidden && window.location.hash === '#me-page' && !getCookie('uid')) {
|
||||
window.location.hash = 'welcome-page';
|
||||
showOnly('welcome-page');
|
||||
}
|
||||
});
|
||||
|
||||
Router.init();
|
||||
// Initial page load
|
||||
handleNavigation();
|
||||
});
|
||||
|
Reference in New Issue
Block a user