// ===== Utility functions (global scope) ===== function escHtml(s) { if (s === null || s === undefined) return ''; return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escAttr(s) { return escHtml(s); } function formatDistance(meters) { if (!meters && meters !== 0) return ''; if (meters === 0) return '0 m'; if (meters >= 1000) return (meters / 1000).toFixed(2) + ' km'; return Math.round(meters) + ' m'; } function formatDate(dateStr) { if (!dateStr) return ''; const d = new Date(dateStr); if (isNaN(d.getTime())) return ''; return d.toLocaleDateString(); } let _toastTimeout; function showToast(msg, type) { const toast = document.getElementById('upload-toast'); const text = document.getElementById('upload-toast-text'); if (!toast || !text) return; text.textContent = msg; toast.className = type || ''; toast.style.display = 'block'; clearTimeout(_toastTimeout); _toastTimeout = setTimeout(() => { toast.style.display = 'none'; }, 3000); } function showUploadToast(msg) { const toast = document.getElementById('upload-toast'); const text = document.getElementById('upload-toast-text'); if (!toast || !text) return; text.textContent = msg; toast.className = ''; toast.style.display = 'block'; } function hideUploadToast() { setTimeout(() => { const toast = document.getElementById('upload-toast'); if (toast) toast.style.display = 'none'; }, 1000); } // ===== Admin Panel ===== async function loadAdminPanel() { const content = document.getElementById('admin-content'); content.innerHTML = '
Loading...
'; try { const users = await API.adminGetUsers(); let html = `
`; for (const u of users) { html += ``; } html += '
ID Login Email Admin Active Tracks Created Actions
${u.id} ${escHtml(u.login)} ${escHtml(u.email || '')} ${u.isAdmin ? '✓' : ''} ${u.isActive ? '✓' : '✗'} ${u.trackCount || 0} ${formatDate(u.createdAt)} ${!u.isActive ? `` : `` } ${!u.isAdmin ? `` : `` }
'; content.innerHTML = html; } catch (e) { content.innerHTML = '
Error: ' + escHtml(e.message) + '
'; } } async function adminActivate(userId, isActive) { try { await API.adminActivate(userId, isActive); await loadAdminPanel(); showToast(isActive ? 'User activated' : 'User deactivated', 'success'); } catch (e) { showToast('Error: ' + e.message, 'error'); } } async function adminSetAdmin(userId, isAdmin) { try { await API.adminSetAdmin(userId, isAdmin); await loadAdminPanel(); showToast(isAdmin ? 'Admin rights granted' : 'Admin rights revoked', 'success'); } catch (e) { showToast('Error: ' + e.message, 'error'); } } async function adminDeleteUser(userId) { document.getElementById('confirm-title').textContent = 'Delete User'; document.getElementById('confirm-message').textContent = 'Delete this user and ALL their data? This cannot be undone.'; const modal = document.getElementById('confirm-modal'); modal.style.display = 'flex'; document.getElementById('confirm-ok-btn').onclick = async () => { modal.style.display = 'none'; try { await API.adminDeleteUser(userId); await loadAdminPanel(); showToast('User deleted', 'success'); } catch (e) { showToast('Error: ' + e.message, 'error'); } }; } // ===== Share Page (no auth) ===== async function initSharePage(code) { document.getElementById('auth-page').style.display = 'none'; document.getElementById('app').style.display = 'flex'; document.getElementById('app').style.flexDirection = 'column'; const sidebar = document.getElementById('sidebar'); if (sidebar) sidebar.style.display = 'none'; const appName = escHtml(window.APP_CONFIG?.appName || 'GPX Visualizer'); document.getElementById('topbar').innerHTML = `
${appName}
Shared track
`; MapView.init(); const panel = document.getElementById('track-info-panel'); const infoContent = document.getElementById('track-info-content'); try { const data = await API.getShared(code); MapView.addTrack(data, 'shared'); MapView.fitTrack('shared'); panel.style.display = 'block'; infoContent.innerHTML = `

${escHtml(data.meta?.name || 'Shared Track')}

${formatDistance(data.meta?.totalDistance)}
${data.meta?.pointCount || 0}
${data.meta?.trackDate ? `
${formatDate(data.meta.trackDate)}
` : ''} `; } catch (e) { panel.style.display = 'block'; infoContent.innerHTML = '
Track not found or link has expired.
'; } document.getElementById('track-info-close').onclick = () => { panel.style.display = 'none'; }; // Update document title document.title = (window.APP_CONFIG?.appName || 'GPX Visualizer') + ' - Shared Track'; } // ===== Main Init ===== async function main() { const appName = window.APP_CONFIG?.appName || 'GPX Visualizer'; document.title = appName; document.getElementById('app-title').textContent = appName; document.getElementById('topbar-title').textContent = appName; // Check if this is a share link const path = window.location.pathname; const shareMatch = path.match(/\/share\/([a-z0-9]+)$/i); if (shareMatch) { await initSharePage(shareMatch[1]); return; } // Setup modal close buttons document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', () => { const modal = document.getElementById(btn.dataset.modal); if (modal) modal.style.display = 'none'; }); }); // Click outside modal to close document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; }); }); // Keyboard: Escape to close modals document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal').forEach(modal => { if (modal.style.display !== 'none') modal.style.display = 'none'; }); } }); // Sidebar tabs document.querySelectorAll('.sidebar-tab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.sidebar-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.querySelectorAll('.sidebar-tab-content').forEach(c => c.style.display = 'none'); const tab = btn.dataset.tab; const tabEl = document.getElementById(tab + '-tab'); if (tabEl) tabEl.style.display = 'flex'; if (tab === 'stats') Stats.init(); }); }); // Sidebar toggle const sidebar = document.getElementById('sidebar'); document.getElementById('sidebar-toggle-btn').addEventListener('click', () => { sidebar.classList.toggle('collapsed'); // Invalidate map size after transition setTimeout(() => { const m = MapView.getMap(); if (m) m.invalidateSize(); }, 250); }); // Check auth Auth.setupForms(); const loggedIn = await Auth.init(); if (!loggedIn) { document.getElementById('auth-page').style.display = 'flex'; document.getElementById('app').style.display = 'none'; return; } // Show app document.getElementById('auth-page').style.display = 'none'; document.getElementById('app').style.display = 'flex'; const user = Auth.getCurrentUser(); document.getElementById('topbar-user').textContent = user.login; document.title = appName + ' - ' + user.login; if (user.isAdmin) { const adminBtn = document.getElementById('admin-btn'); adminBtn.style.display = ''; adminBtn.addEventListener('click', () => { document.getElementById('admin-modal').style.display = 'flex'; loadAdminPanel(); }); } document.getElementById('logout-btn').addEventListener('click', Auth.logout); // Init map MapView.init(); // Init browser await Browser.init(); // Restore open track from hash const params = MapView.getHashParams(); if (params.open) { const trackId = parseInt(params.open); if (!isNaN(trackId)) { try { const data = await API.getTrackPoints(trackId); MapView.addTrack(data, trackId); // Don't fit if we already have a saved map position if (!params.map) { MapView.fitTrack(trackId); } MapView.setCurrentTrack(trackId); // Show track info panel if (data.meta) { const panel = document.getElementById('track-info-panel'); document.getElementById('track-info-content').innerHTML = `

${escHtml(data.meta.name || 'Track')}

${formatDistance(data.meta.totalDistance)}
${data.meta.pointCount || 0}
${data.meta.trackDate ? `
${formatDate(data.meta.trackDate)}
` : ''} `; panel.style.display = 'block'; document.getElementById('track-info-close').onclick = () => { panel.style.display = 'none'; }; } } catch (e) { console.warn('Could not restore track from hash:', e.message); } } } } document.addEventListener('DOMContentLoaded', main);