Add map/table toggle view for current reports

 New Features:
- Toggle between map and table view for current reports
- Table view shows location, details, reported time, and time remaining
- Color-coded time remaining: urgent (red), warning (orange), normal (green)
- Responsive design with mobile-optimized table layout
- Real-time updates work in both map and table views
- Sorted by most recent reports first

🎨 UI Improvements:
- Professional toggle buttons with active state
- Clean table design with hover effects
- Accessibility-friendly with proper titles and tooltips
- Mobile-responsive layout adjustments

🚀 Better UX:
- Easy switching between visual map and detailed table
- Time remaining countdown helps prioritize urgent reports
- Searchable and scannable table format for quick review
- Maintains all existing functionality while adding new view
This commit is contained in:
Deco Vander 2025-07-03 01:07:17 -04:00
parent 3581ea219d
commit 5e56d59bbd
3 changed files with 334 additions and 7 deletions

View file

@ -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 = '<tr><td colspan="4" class="loading">No active reports</td></tr>';
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 `
<tr>
<td class="location-cell" title="${location.address}">${location.address}</td>
<td class="details-cell" title="${location.description || 'No additional details'}">
${location.description || '<em>No additional details</em>'}
</td>
<td class="time-cell" title="${reportedTime}">${timeAgo}</td>
<td class="remaining-cell ${remainingClass}">${timeRemaining}</td>
</tr>
`;
}).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 = '<tr><td colspan="4" class="loading">Error loading reports</td></tr>';
}
});
};
@ -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);

View file

@ -45,7 +45,15 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
</div>
<div class="map-section">
<div class="reports-header">
<h2>Current Reports</h2>
<div class="view-toggle">
<button id="map-view-btn" class="toggle-btn active">📍 Map View</button>
<button id="table-view-btn" class="toggle-btn">📋 Table View</button>
</div>
</div>
<div id="map-view" class="view-container">
<div id="map"></div>
<div class="map-info">
<p><strong>🔴 Red markers:</strong> ICE activity reported</p>
@ -53,6 +61,32 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
<p id="location-count">Loading locations...</p>
</div>
</div>
<div id="table-view" class="view-container" style="display: none;">
<div class="table-controls">
<div class="table-info">
<p id="table-location-count">Loading locations...</p>
</div>
</div>
<div class="table-container">
<table class="reports-table">
<thead>
<tr>
<th>Location</th>
<th>Details</th>
<th>Reported</th>
<th>Time Remaining</th>
</tr>
</thead>
<tbody id="reports-tbody">
<tr>
<td colspan="4" class="loading">Loading reports...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<footer>

View file

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