diff --git a/public/app-mapbox.js b/public/app-mapbox.js
index c7cd7eb..bb109a2 100644
--- a/public/app-mapbox.js
+++ b/public/app-mapbox.js
@@ -23,11 +23,21 @@ document.addEventListener('DOMContentLoaded', async () => {
const addressInput = document.getElementById('address');
const autocompleteList = document.getElementById('autocomplete-list');
+ // View toggle elements
+ const mapViewBtn = document.getElementById('map-view-btn');
+ const tableViewBtn = document.getElementById('table-view-btn');
+ const mapView = document.getElementById('map-view');
+ const tableView = document.getElementById('table-view');
+ const reportsTableBody = document.getElementById('reports-tbody');
+ const tableLocationCount = document.getElementById('table-location-count');
+
let autocompleteTimeout;
let selectedIndex = -1;
let autocompleteResults = [];
let currentMarkers = [];
let updateInterval;
+ let currentLocations = [];
+ let currentView = 'map'; // 'map' or 'table'
const clearMarkers = () => {
currentMarkers.forEach(marker => {
@@ -64,14 +74,112 @@ document.addEventListener('DOMContentLoaded', async () => {
return 'over a day ago';
};
+ // Toggle between map and table view
+ const switchView = (viewType) => {
+ currentView = viewType;
+
+ if (viewType === 'map') {
+ mapView.style.display = 'block';
+ tableView.style.display = 'none';
+ mapViewBtn.classList.add('active');
+ tableViewBtn.classList.remove('active');
+
+ // Invalidate map size after showing
+ setTimeout(() => map.invalidateSize(), 100);
+ } else {
+ mapView.style.display = 'none';
+ tableView.style.display = 'block';
+ mapViewBtn.classList.remove('active');
+ tableViewBtn.classList.add('active');
+
+ // Render table with current data
+ renderTable(currentLocations);
+ }
+ };
+
+ // Render table view
+ const renderTable = (locations) => {
+ if (!locations || locations.length === 0) {
+ reportsTableBody.innerHTML = '
No active reports |
';
+ tableLocationCount.textContent = '0 active reports';
+ return;
+ }
+
+ // Sort by most recent first
+ const sortedLocations = [...locations].sort((a, b) =>
+ new Date(b.created_at) - new Date(a.created_at)
+ );
+
+ const tableHTML = sortedLocations.map(location => {
+ const timeAgo = getTimeAgo(location.created_at);
+ const timeRemaining = getTimeRemaining(location.created_at);
+ const remainingClass = getRemainingClass(location.created_at);
+ const reportedTime = new Date(location.created_at).toLocaleString();
+
+ return `
+
+ ${location.address} |
+
+ ${location.description || 'No additional details'}
+ |
+ ${timeAgo} |
+ ${timeRemaining} |
+
+ `;
+ }).join('');
+
+ reportsTableBody.innerHTML = tableHTML;
+ tableLocationCount.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`;
+ };
+
+ // Calculate time remaining until 24-hour expiration
+ const getTimeRemaining = (timestamp) => {
+ const now = new Date();
+ const reportTime = new Date(timestamp);
+ const expirationTime = new Date(reportTime.getTime() + 24 * 60 * 60 * 1000);
+ const remaining = expirationTime - now;
+
+ if (remaining <= 0) return 'Expired';
+
+ const hours = Math.floor(remaining / (1000 * 60 * 60));
+ const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ } else {
+ return `${minutes}m`;
+ }
+ };
+
+ // Get CSS class for time remaining
+ const getRemainingClass = (timestamp) => {
+ const now = new Date();
+ const reportTime = new Date(timestamp);
+ const expirationTime = new Date(reportTime.getTime() + 24 * 60 * 60 * 1000);
+ const remaining = expirationTime - now;
+ const hoursRemaining = remaining / (1000 * 60 * 60);
+
+ if (hoursRemaining <= 1) return 'urgent';
+ if (hoursRemaining <= 6) return 'warning';
+ return 'normal';
+ };
+
const refreshLocations = () => {
fetch('/api/locations')
.then(res => res.json())
.then(locations => {
+ currentLocations = locations;
+
+ // Update map view
showMarkers(locations);
const countElement = document.getElementById('location-count');
countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`;
+ // Update table view if it's currently visible
+ if (currentView === 'table') {
+ renderTable(locations);
+ }
+
const now = new Date();
const timeStr = now.toLocaleTimeString('en-US', {
hour12: false,
@@ -84,6 +192,9 @@ document.addEventListener('DOMContentLoaded', async () => {
.catch(err => {
console.error('Error fetching locations:', err);
document.getElementById('location-count').textContent = 'Error loading locations';
+ if (currentView === 'table') {
+ reportsTableBody.innerHTML = 'Error loading reports |
';
+ }
});
};
@@ -423,6 +534,10 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
+ // View toggle event listeners
+ mapViewBtn.addEventListener('click', () => switchView('map'));
+ tableViewBtn.addEventListener('click', () => switchView('table'));
+
window.addEventListener('beforeunload', () => {
if (updateInterval) {
clearInterval(updateInterval);
diff --git a/public/index.html b/public/index.html
index bd6f6ae..259d77b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -45,12 +45,46 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
-
Current Reports
-
-
-
🔴 Red markers: ICE activity reported
-
⏰ Auto-cleanup: Reports disappear after 24 hours
-
Loading locations...
+
+
+
+
+
+
🔴 Red markers: ICE activity reported
+
⏰ Auto-cleanup: Reports disappear after 24 hours
+
Loading locations...
+
+
+
+
+
+
+
+
+
+ Location |
+ Details |
+ Reported |
+ Time Remaining |
+
+
+
+
+ Loading reports... |
+
+
+
+
diff --git a/public/style.css b/public/style.css
index abe4d15..db4fc90 100644
--- a/public/style.css
+++ b/public/style.css
@@ -131,7 +131,185 @@ footer {
clear: both;
}
-disclaimer {
+.disclaimer {
font-size: 0.8em;
color: #777;
}
+
+/* Reports header and toggle */
+.reports-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.view-toggle {
+ display: flex;
+ gap: 10px;
+}
+
+.toggle-btn {
+ background-color: #f8f9fa;
+ border: 2px solid #dee2e6;
+ color: #495057;
+ padding: 8px 16px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.toggle-btn:hover {
+ background-color: #e9ecef;
+ border-color: #adb5bd;
+}
+
+.toggle-btn.active {
+ background-color: #007bff;
+ border-color: #007bff;
+ color: white;
+}
+
+.toggle-btn.active:hover {
+ background-color: #0056b3;
+ border-color: #0056b3;
+}
+
+/* View containers */
+.view-container {
+ width: 100%;
+}
+
+/* Table view styles */
+.table-container {
+ overflow-x: auto;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.reports-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: white;
+ font-size: 14px;
+}
+
+.reports-table th {
+ background-color: #f8f9fa;
+ color: #495057;
+ font-weight: 600;
+ padding: 12px;
+ text-align: left;
+ border-bottom: 2px solid #dee2e6;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.reports-table td {
+ padding: 12px;
+ border-bottom: 1px solid #dee2e6;
+ vertical-align: top;
+}
+
+.reports-table tr:hover {
+ background-color: #f8f9fa;
+}
+
+.reports-table tr:last-child td {
+ border-bottom: none;
+}
+
+.table-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.table-info {
+ color: #666;
+ font-size: 14px;
+}
+
+/* Table cell specific styles */
+.location-cell {
+ font-weight: 500;
+ color: #495057;
+ max-width: 250px;
+}
+
+.details-cell {
+ color: #6c757d;
+ font-style: italic;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.time-cell {
+ font-size: 12px;
+ color: #868e96;
+}
+
+.remaining-cell {
+ font-weight: 500;
+ font-size: 12px;
+}
+
+.remaining-cell.urgent {
+ color: #dc3545;
+ font-weight: 600;
+}
+
+.remaining-cell.warning {
+ color: #fd7e14;
+ font-weight: 600;
+}
+
+.remaining-cell.normal {
+ color: #28a745;
+}
+
+.loading {
+ text-align: center;
+ color: #6c757d;
+ font-style: italic;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .reports-header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 15px;
+ }
+
+ .view-toggle {
+ justify-content: center;
+ }
+
+ .toggle-btn {
+ flex: 1;
+ }
+
+ .reports-table {
+ font-size: 12px;
+ }
+
+ .reports-table th,
+ .reports-table td {
+ padding: 8px;
+ }
+
+ .location-cell {
+ max-width: 150px;
+ }
+
+ .details-cell {
+ max-width: 120px;
+ }
+}