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:
parent
3581ea219d
commit
5e56d59bbd
3 changed files with 334 additions and 7 deletions
|
@ -23,11 +23,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const addressInput = document.getElementById('address');
|
const addressInput = document.getElementById('address');
|
||||||
const autocompleteList = document.getElementById('autocomplete-list');
|
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 autocompleteTimeout;
|
||||||
let selectedIndex = -1;
|
let selectedIndex = -1;
|
||||||
let autocompleteResults = [];
|
let autocompleteResults = [];
|
||||||
let currentMarkers = [];
|
let currentMarkers = [];
|
||||||
let updateInterval;
|
let updateInterval;
|
||||||
|
let currentLocations = [];
|
||||||
|
let currentView = 'map'; // 'map' or 'table'
|
||||||
|
|
||||||
const clearMarkers = () => {
|
const clearMarkers = () => {
|
||||||
currentMarkers.forEach(marker => {
|
currentMarkers.forEach(marker => {
|
||||||
|
@ -64,14 +74,112 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
return 'over a day ago';
|
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 = () => {
|
const refreshLocations = () => {
|
||||||
fetch('/api/locations')
|
fetch('/api/locations')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(locations => {
|
.then(locations => {
|
||||||
|
currentLocations = locations;
|
||||||
|
|
||||||
|
// Update map view
|
||||||
showMarkers(locations);
|
showMarkers(locations);
|
||||||
const countElement = document.getElementById('location-count');
|
const countElement = document.getElementById('location-count');
|
||||||
countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`;
|
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 now = new Date();
|
||||||
const timeStr = now.toLocaleTimeString('en-US', {
|
const timeStr = now.toLocaleTimeString('en-US', {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
|
@ -84,6 +192,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Error fetching locations:', err);
|
console.error('Error fetching locations:', err);
|
||||||
document.getElementById('location-count').textContent = 'Error loading locations';
|
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', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
if (updateInterval) {
|
if (updateInterval) {
|
||||||
clearInterval(updateInterval);
|
clearInterval(updateInterval);
|
||||||
|
|
|
@ -45,12 +45,46 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-section">
|
<div class="map-section">
|
||||||
<h2>Current Reports</h2>
|
<div class="reports-header">
|
||||||
<div id="map"></div>
|
<h2>Current Reports</h2>
|
||||||
<div class="map-info">
|
<div class="view-toggle">
|
||||||
<p><strong>🔴 Red markers:</strong> ICE activity reported</p>
|
<button id="map-view-btn" class="toggle-btn active">📍 Map View</button>
|
||||||
<p><strong>⏰ Auto-cleanup:</strong> Reports disappear after 24 hours</p>
|
<button id="table-view-btn" class="toggle-btn">📋 Table View</button>
|
||||||
<p id="location-count">Loading locations...</p>
|
</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>
|
||||||
|
<p><strong>⏰ Auto-cleanup:</strong> Reports disappear after 24 hours</p>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
180
public/style.css
180
public/style.css
|
@ -131,7 +131,185 @@ footer {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
disclaimer {
|
.disclaimer {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #777;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue