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...

+
+

Current Reports

+
+ + +
+
+ +
+
+
+

🔴 Red markers: ICE activity reported

+

⏰ Auto-cleanup: Reports disappear after 24 hours

+

Loading locations...

+
+
+ +
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; + } +}