| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- // ===== Utility functions (global scope) =====
- function escHtml(s) {
- if (s === null || s === undefined) return '';
- return String(s)
- .replace(/&/g, '&')
- .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 = '<div class="loading">Loading...</div>';
- try {
- const users = await API.adminGetUsers();
- let html = `<div style="overflow-x:auto">
- <table class="admin-table">
- <thead>
- <tr>
- <th>ID</th>
- <th>Login</th>
- <th>Email</th>
- <th>Admin</th>
- <th>Active</th>
- <th>Tracks</th>
- <th>Created</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>`;
- for (const u of users) {
- html += `<tr>
- <td>${u.id}</td>
- <td>${escHtml(u.login)}</td>
- <td>${escHtml(u.email || '')}</td>
- <td>${u.isAdmin ? '✓' : ''}</td>
- <td>${u.isActive ? '✓' : '✗'}</td>
- <td>${u.trackCount || 0}</td>
- <td>${formatDate(u.createdAt)}</td>
- <td>
- ${!u.isActive
- ? `<button class="btn-small btn-primary" onclick="adminActivate(${u.id}, true)">Activate</button>`
- : `<button class="btn-small btn-secondary" onclick="adminActivate(${u.id}, false)">Deactivate</button>`
- }
- ${!u.isAdmin
- ? `<button class="btn-small btn-secondary" onclick="adminSetAdmin(${u.id}, true)">Make Admin</button>`
- : `<button class="btn-small btn-secondary" onclick="adminSetAdmin(${u.id}, false)">Revoke Admin</button>`
- }
- <button class="btn-small btn-danger" onclick="adminDeleteUser(${u.id})">Delete</button>
- </td>
- </tr>`;
- }
- html += '</tbody></table></div>';
- content.innerHTML = html;
- } catch (e) {
- content.innerHTML = '<div class="error-msg">Error: ' + escHtml(e.message) + '</div>';
- }
- }
- 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 = `
- <div id="topbar-left">
- <span id="topbar-title">${appName}</span>
- </div>
- <div id="topbar-right">
- <span style="color:rgba(255,255,255,0.6);font-size:13px">Shared track</span>
- </div>
- `;
- 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 = `
- <h3>${escHtml(data.meta?.name || 'Shared Track')}</h3>
- <div class="info-row"><label>Distance</label><span>${formatDistance(data.meta?.totalDistance)}</span></div>
- <div class="info-row"><label>Points</label><span>${data.meta?.pointCount || 0}</span></div>
- ${data.meta?.trackDate ? `<div class="info-row"><label>Date</label><span>${formatDate(data.meta.trackDate)}</span></div>` : ''}
- `;
- } catch (e) {
- panel.style.display = 'block';
- infoContent.innerHTML = '<div class="error-msg">Track not found or link has expired.</div>';
- }
- 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 = `
- <h3>${escHtml(data.meta.name || 'Track')}</h3>
- <div class="info-row"><label>Distance</label><span>${formatDistance(data.meta.totalDistance)}</span></div>
- <div class="info-row"><label>Points</label><span>${data.meta.pointCount || 0}</span></div>
- ${data.meta.trackDate ? `<div class="info-row"><label>Date</label><span>${formatDate(data.meta.trackDate)}</span></div>` : ''}
- `;
- 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);
|