ice/public/admin.js
Deco Vander edfdeb5117 Initial commit: ICE Watch Michigan community safety tool
- Node.js/Express backend with SQLite database
- Interactive map with real-time location tracking
- MapBox API integration for fast geocoding
- Admin panel for content moderation
- 24-hour auto-expiring reports
- Deployment scripts for Debian 12 ARM64
- Caddy reverse proxy with automatic HTTPS
2025-07-02 23:27:22 -04:00

304 lines
11 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
let authToken = localStorage.getItem('adminToken');
let allLocations = [];
let editingRowId = null;
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
if (authToken) {
showAdminSection();
loadLocations();
}
// 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);
showAdminSection();
loadLocations();
} else {
showMessage(loginMessage, data.error, 'error');
}
} catch (error) {
showMessage(loginMessage, 'Login failed. Please try again.', 'error');
}
});
// Logout handler
logoutBtn.addEventListener('click', () => {
authToken = null;
localStorage.removeItem('adminToken');
showLoginSection();
});
// Refresh button handler
refreshBtn.addEventListener('click', () => {
loadLocations();
});
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';
}
async function loadLocations() {
try {
const response = await fetch('/api/admin/locations', {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.status === 401) {
// Token expired or invalid
authToken = null;
localStorage.removeItem('adminToken');
showLoginSection();
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 = '<tr><td colspan="6">Error loading locations</td></tr>';
}
}
function updateStats() {
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const activeLocations = allLocations.filter(location =>
new Date(location.created_at) > twentyFourHoursAgo
);
document.getElementById('total-count').textContent = allLocations.length;
document.getElementById('active-count').textContent = activeLocations.length;
document.getElementById('expired-count').textContent = allLocations.length - activeLocations.length;
}
function renderLocationsTable() {
if (allLocations.length === 0) {
locationsTableBody.innerHTML = '<tr><td colspan="6">No locations found</td></tr>';
return;
}
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
locationsTableBody.innerHTML = allLocations.map(location => {
const isActive = new Date(location.created_at) > twentyFourHoursAgo;
const createdDate = new Date(location.created_at);
const formattedDate = createdDate.toLocaleString();
return `
<tr data-id="${location.id}" ${editingRowId === location.id ? 'class="edit-row"' : ''}>
<td>${location.id}</td>
<td>
<span class="status-indicator ${isActive ? 'status-active' : 'status-expired'}">
${isActive ? 'ACTIVE' : 'EXPIRED'}
</span>
</td>
<td class="address-cell" title="${location.address}">${location.address}</td>
<td>${location.description || ''}</td>
<td title="${formattedDate}">${getTimeAgo(location.created_at)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-edit" onclick="editLocation(${location.id})">Edit</button>
<button class="btn btn-delete" onclick="deleteLocation(${location.id})">Delete</button>
</div>
</td>
</tr>
`;
}).join('');
}
function renderEditRow(location) {
const row = document.querySelector(`tr[data-id="${location.id}"]`);
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const isActive = new Date(location.created_at) > twentyFourHoursAgo;
const createdDate = new Date(location.created_at);
const formattedDate = createdDate.toLocaleString();
row.innerHTML = `
<td>${location.id}</td>
<td>
<span class="status-indicator ${isActive ? 'status-active' : 'status-expired'}">
${isActive ? 'ACTIVE' : 'EXPIRED'}
</span>
</td>
<td>
<input type="text" class="edit-input" id="edit-address-${location.id}"
value="${location.address}" placeholder="Address">
</td>
<td>
<input type="text" class="edit-input" id="edit-description-${location.id}"
value="${location.description || ''}" placeholder="Description">
</td>
<td title="${formattedDate}">${getTimeAgo(location.created_at)}</td>
<td>
<div class="action-buttons">
<button class="btn btn-save" onclick="saveLocation(${location.id})">Save</button>
<button class="btn btn-cancel" onclick="cancelEdit()">Cancel</button>
</div>
</td>
`;
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.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();
const reportTime = new Date(timestamp);
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();
}
});