// ===== 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 = `
| ID |
Login |
Email |
Admin |
Active |
Tracks |
Created |
Actions |
`;
for (const u of users) {
html += `
| ${u.id} |
${escHtml(u.login)} |
${escHtml(u.email || '')} |
${u.isAdmin ? '✓' : ''} |
${u.isActive ? '✓' : '✗'} |
${u.trackCount || 0} |
${formatDate(u.createdAt)} |
${!u.isActive
? ``
: ``
}
${!u.isAdmin
? ``
: ``
}
|
`;
}
html += '
';
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);