document.addEventListener('DOMContentLoaded', () => { let authToken = localStorage.getItem('adminToken'); let allLocations = []; let editingRowId = null; let sessionTimeout = null; let warningTimeout = null; // Session timeout settings (in milliseconds) const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes const WARNING_TIME = 5 * 60 * 1000; // Show warning 5 minutes before expiry const loginSection = document.getElementById('login-section'); const adminSection = document.getElementById('admin-section'); const loginForm = document.getElementById('login-form'); const loginMessage = document.getElementById('login-message'); const logoutBtn = document.getElementById('logout-btn'); const refreshBtn = document.getElementById('refresh-btn'); const locationsTableBody = document.getElementById('locations-tbody'); // Check if already logged in and session is still valid if (authToken) { const loginTime = localStorage.getItem('adminLoginTime'); const now = Date.now(); if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) { showAdminSection(); loadLocations(); startSessionTimer(); } else { // Session expired logout('Session expired. Please log in again.'); } } // Login form handler loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const password = document.getElementById('password').value; try { const response = await fetch('/api/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await response.json(); if (response.ok) { authToken = data.token; localStorage.setItem('adminToken', authToken); localStorage.setItem('adminLoginTime', Date.now().toString()); showAdminSection(); loadLocations(); startSessionTimer(); } else { showMessage(loginMessage, data.error, 'error'); } } catch (error) { showMessage(loginMessage, 'Login failed. Please try again.', 'error'); } }); // Logout handler logoutBtn.addEventListener('click', () => { logout('Logged out successfully.'); }); // Refresh button handler refreshBtn.addEventListener('click', () => { loadLocations(); }); // Tab navigation logic const tabs = document.querySelectorAll('.tab-btn'); const contents = document.querySelectorAll('.tab-content'); tabs.forEach(tab => { tab.addEventListener('click', function () { tabs.forEach(t => t.classList.remove('active')); contents.forEach(c => c.classList.remove('active')); this.classList.add('active'); document.querySelector(`#${this.dataset.tab}-tab`).classList.add('active'); // Load data for the active tab if (this.dataset.tab === 'profanity') { loadProfanityWords(); } }); }); // Profanity management handlers const addProfanityForm = document.getElementById('add-profanity-form'); const testProfanityForm = document.getElementById('test-profanity-form'); const profanityTableBody = document.getElementById('profanity-tbody'); if (addProfanityForm) { addProfanityForm.addEventListener('submit', async (e) => { e.preventDefault(); const word = document.getElementById('new-word').value.trim(); const severity = document.getElementById('new-severity').value; const category = document.getElementById('new-category').value.trim() || 'custom'; if (!word) return; await addProfanityWord(word, severity, category); }); } if (testProfanityForm) { testProfanityForm.addEventListener('submit', async (e) => { e.preventDefault(); const text = document.getElementById('test-text').value.trim(); if (!text) return; await testProfanityFilter(text); }); } function showLoginSection() { loginSection.style.display = 'block'; adminSection.style.display = 'none'; document.getElementById('password').value = ''; hideMessage(loginMessage); } function showAdminSection() { loginSection.style.display = 'none'; adminSection.style.display = 'block'; } function showMessage(element, message, type) { element.textContent = message; element.className = `message ${type}`; element.style.display = 'block'; setTimeout(() => hideMessage(element), 5000); } function hideMessage(element) { element.style.display = 'none'; } // Session management functions function startSessionTimer() { clearSessionTimers(); // Set warning timer (5 minutes before expiry) warningTimeout = setTimeout(() => { showSessionWarning(); }, SESSION_DURATION - WARNING_TIME); // Set logout timer (30 minutes) sessionTimeout = setTimeout(() => { logout('Session expired due to inactivity.'); }, SESSION_DURATION); } function clearSessionTimers() { if (sessionTimeout) { clearTimeout(sessionTimeout); sessionTimeout = null; } if (warningTimeout) { clearTimeout(warningTimeout); warningTimeout = null; } } function showSessionWarning() { const extend = confirm('Your admin session will expire in 5 minutes. Click OK to extend your session, or Cancel to log out now.'); if (extend) { extendSession(); } else { logout('Session ended by user.'); } } function extendSession() { // Update login time and restart timers localStorage.setItem('adminLoginTime', Date.now().toString()); startSessionTimer(); console.log('Admin session extended for 30 minutes'); } function logout(message = 'Logged out.') { clearSessionTimers(); authToken = null; localStorage.removeItem('adminToken'); localStorage.removeItem('adminLoginTime'); showLoginSection(); if (message) { showMessage(loginMessage, message, 'error'); } } // Reset session timer on user activity function resetSessionTimer() { if (authToken && adminSection.style.display !== 'none') { const loginTime = localStorage.getItem('adminLoginTime'); const now = Date.now(); // Only reset if we're within the session duration if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) { localStorage.setItem('adminLoginTime', now.toString()); startSessionTimer(); } } } // Listen for user activity to reset session timer const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']; let activityTimer = null; activityEvents.forEach(event => { document.addEventListener(event, () => { // Throttle activity detection to once per minute if (!activityTimer) { activityTimer = setTimeout(() => { resetSessionTimer(); activityTimer = null; }, 60000); // 1 minute throttle } }, true); }); // Check session validity periodically setInterval(() => { if (authToken) { const loginTime = localStorage.getItem('adminLoginTime'); const now = Date.now(); if (!loginTime || (now - parseInt(loginTime)) >= SESSION_DURATION) { logout('Session expired.'); } } }, 60000); // Check every minute async function loadLocations() { try { const response = await fetch('/api/admin/locations', { headers: { 'Authorization': `Bearer ${authToken}` } }); if (response.status === 401) { // Token expired or invalid logout('Session expired. Please log in again.'); return; } if (!response.ok) { throw new Error('Failed to load locations'); } allLocations = await response.json(); updateStats(); renderLocationsTable(); } catch (error) { console.error('Error loading locations:', error); locationsTableBody.innerHTML = 'Error loading locations'; } } function updateStats() { const now = new Date(); const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000); const activeLocations = allLocations.filter(location => new Date(location.created_at) > fortyEightHoursAgo ); const persistentLocations = allLocations.filter(location => location.persistent); document.getElementById('total-count').textContent = allLocations.length; document.getElementById('active-count').textContent = activeLocations.length; document.getElementById('expired-count').textContent = allLocations.length - activeLocations.length; document.getElementById('persistent-count').textContent = persistentLocations.length; } function renderLocationsTable() { if (allLocations.length === 0) { locationsTableBody.innerHTML = 'No locations found'; return; } const now = new Date(); const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000); locationsTableBody.innerHTML = allLocations.map(location => { const isActive = new Date(location.created_at) > fortyEightHoursAgo; const createdDate = new Date(location.created_at); const formattedDate = createdDate.toLocaleString(); return ` ${location.id} ${isActive ? 'ACTIVE' : 'EXPIRED'} ${location.address} ${location.description || ''} ${getTimeAgo(location.created_at)}
`; }).join(''); } function renderEditRow(location) { const row = document.querySelector(`tr[data-id="${location.id}"]`); const now = new Date(); const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000); const isActive = new Date(location.created_at) > fortyEightHoursAgo; const createdDate = new Date(location.created_at); const formattedDate = createdDate.toLocaleString(); row.innerHTML = ` ${location.id} ${isActive ? 'ACTIVE' : 'EXPIRED'} ${getTimeAgo(location.created_at)}
`; row.className = 'edit-row'; } window.editLocation = (id) => { if (editingRowId) { cancelEdit(); } editingRowId = id; const location = allLocations.find(loc => loc.id === id); if (location) { renderEditRow(location); } }; window.cancelEdit = () => { editingRowId = null; renderLocationsTable(); }; window.saveLocation = async (id) => { const address = document.getElementById(`edit-address-${id}`).value.trim(); const description = document.getElementById(`edit-description-${id}`).value.trim(); if (!address) { alert('Address is required'); return; } try { // Geocode the address if it was changed let latitude = null; let longitude = null; const originalLocation = allLocations.find(loc => loc.id === id); if (address !== originalLocation.address) { try { const geoResponse = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`); const geoData = await geoResponse.json(); if (geoData && geoData.length > 0) { latitude = parseFloat(geoData[0].lat); longitude = parseFloat(geoData[0].lon); } } catch (geoError) { console.warn('Geocoding failed, keeping original coordinates'); latitude = originalLocation.latitude; longitude = originalLocation.longitude; } } else { latitude = originalLocation.latitude; longitude = originalLocation.longitude; } const response = await fetch(`/api/admin/locations/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ address, latitude, longitude, description }) }); if (response.ok) { editingRowId = null; loadLocations(); // Reload to get updated data } else { const error = await response.json(); alert(`Error updating location: ${error.error}`); } } catch (error) { console.error('Error saving location:', error); alert('Error saving location. Please try again.'); } }; window.togglePersistent = async (id) => { const location = allLocations.find(loc => loc.id === id); if (!location) return; const newPersistentStatus = !location.persistent; const confirmMessage = newPersistentStatus ? 'Mark this report as persistent? It will not auto-expire after 48 hours.' : 'Remove persistent status? This report will expire normally after 48 hours.'; if (!confirm(confirmMessage)) { return; } try { const response = await fetch(`/api/admin/locations/${id}/persistent`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ persistent: newPersistentStatus }) }); if (response.ok) { loadLocations(); // Reload to get updated data } else { const error = await response.json(); alert(`Error updating persistent status: ${error.error}`); } } catch (error) { console.error('Error toggling persistent status:', error); alert('Error updating persistent status. Please try again.'); } }; window.deleteLocation = async (id) => { if (!confirm('Are you sure you want to delete this location report?')) { return; } try { const response = await fetch(`/api/admin/locations/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}` } }); if (response.ok) { loadLocations(); // Reload the table } else { const error = await response.json(); alert(`Error deleting location: ${error.error}`); } } catch (error) { console.error('Error deleting location:', error); alert('Error deleting location. Please try again.'); } }; function getTimeAgo(timestamp) { const now = new Date(); // Ensure timestamp is treated as UTC if it doesn't have timezone info const reportTime = new Date(timestamp.includes('T') ? timestamp : timestamp + 'Z'); const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60)); if (diffInMinutes < 1) return 'just now'; if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; const diffInHours = Math.floor(diffInMinutes / 60); if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; const diffInDays = Math.floor(diffInHours / 24); if (diffInDays < 7) return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`; return reportTime.toLocaleDateString(); } // Profanity management functions async function loadProfanityWords() { if (!profanityTableBody) return; try { const response = await fetch('/api/admin/profanity-words', { headers: { 'Authorization': `Bearer ${authToken}` } }); if (response.status === 401) { logout('Session expired. Please log in again.'); return; } const data = await response.json(); if (response.ok) { displayProfanityWords(data || []); } else { console.error('Failed to load profanity words:', data.error); profanityTableBody.innerHTML = 'Failed to load words'; } } catch (error) { console.error('Error loading profanity words:', error); profanityTableBody.innerHTML = 'Error loading words'; } } function displayProfanityWords(words) { if (!profanityTableBody) return; if (words.length === 0) { profanityTableBody.innerHTML = 'No custom words added yet'; return; } profanityTableBody.innerHTML = words.map(word => ` ${word.id} ${escapeHtml(word.word)} ${word.severity} ${word.category || 'N/A'} ${new Date(word.created_at).toLocaleDateString()} `).join(''); } async function addProfanityWord(word, severity, category) { try { const response = await fetch('/api/admin/profanity-words', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ word, severity, category }) }); const data = await response.json(); if (response.ok) { // Clear form document.getElementById('new-word').value = ''; document.getElementById('new-category').value = 'custom'; document.getElementById('new-severity').value = 'medium'; // Reload words list loadProfanityWords(); console.log('Word added successfully'); } else { alert('Failed to add word: ' + data.error); } } catch (error) { console.error('Error adding profanity word:', error); alert('Error adding word. Please try again.'); } } async function testProfanityFilter(text) { const resultsDiv = document.getElementById('test-results'); if (!resultsDiv) return; try { const response = await fetch('/api/admin/test-profanity', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ text }) }); const data = await response.json(); if (response.ok) { if (data.analysis && data.analysis.hasProfanity) { resultsDiv.className = 'test-results profane'; resultsDiv.innerHTML = ` ⚠️ Profanity Detected!
Detected words: ${data.analysis.matches.map(m => `${escapeHtml(m.word)}`).join(', ')}
Severity: ${data.analysis.severity}
Filtered text: "${escapeHtml(data.filtered)}" `; } else { resultsDiv.className = 'test-results clean'; resultsDiv.innerHTML = '✅ Text is clean!
No profanity detected.'; } } else { resultsDiv.className = 'test-results empty'; resultsDiv.innerHTML = 'Error testing text: ' + data.error; } } catch (error) { console.error('Error testing profanity filter:', error); resultsDiv.className = 'test-results empty'; resultsDiv.innerHTML = 'Error testing text. Please try again.'; } } // Make deleteProfanityWord available globally for onclick handlers window.deleteProfanityWord = async function(wordId) { if (!confirm('Are you sure you want to delete this word?')) return; try { const response = await fetch(`/api/admin/profanity-words/${wordId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}` } }); if (response.ok) { loadProfanityWords(); console.log('Word deleted successfully'); } else { const data = await response.json(); alert('Failed to delete word: ' + data.error); } } catch (error) { console.error('Error deleting profanity word:', error); alert('Error deleting word. Please try again.'); } }; function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize theme toggle initializeTheme(); }); // Theme toggle functionality (shared with main page) function initializeTheme() { const themeToggle = document.getElementById('theme-toggle'); const themeIcon = document.querySelector('.theme-icon'); if (!themeToggle || !themeIcon) { console.warn('Theme toggle elements not found'); return; } // Check for saved theme preference or default to auto (follows system) const savedTheme = localStorage.getItem('theme') || 'auto'; applyTheme(savedTheme); // Update icon based on current theme updateThemeIcon(savedTheme, themeIcon); // Add click listener for cycling through themes themeToggle.addEventListener('click', () => { const currentTheme = localStorage.getItem('theme') || 'auto'; let newTheme; // Cycle: auto → light → dark → auto switch(currentTheme) { case 'auto': newTheme = 'light'; break; case 'light': newTheme = 'dark'; break; case 'dark': newTheme = 'auto'; break; default: newTheme = 'auto'; } localStorage.setItem('theme', newTheme); applyTheme(newTheme); updateThemeIcon(newTheme, themeIcon); console.log(`Theme switched to: ${newTheme}`); }); // Listen for system theme changes when in auto mode window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const currentTheme = localStorage.getItem('theme') || 'auto'; if (currentTheme === 'auto') { applyTheme('auto'); } }); } function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } function updateThemeIcon(theme, iconElement) { switch(theme) { case 'auto': iconElement.textContent = '🌍'; // Globe (auto) break; case 'light': iconElement.textContent = '☀️'; // Sun (light) break; case 'dark': iconElement.textContent = '🌙'; // Moon (dark) break; } }