From a3b450de1a78a6e96bfcc451d6275bd656f24382 Mon Sep 17 00:00:00 2001 From: Deco Vander Date: Thu, 3 Jul 2025 01:17:41 -0400 Subject: [PATCH] Add mobile responsiveness and persistent reports feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced mobile responsiveness across entire site and admin panel - Optimized layouts, font sizes, and spacing for screens ≤768px and ≤480px - Made forms, tables, maps, and buttons touch-friendly - Added responsive breakpoints for better mobile experience - Added persistent reports functionality - Added 'persistent' column to database with automatic migration - Updated cleanup logic to preserve persistent reports (no auto-expiration) - Added admin panel toggle for marking reports as persistent - Added persistent report count to admin dashboard stats - Visual indicators with lock/unlock icons for persistent status - Improved admin panel UI - Standardized header button styling and sizing - Added 'Return to Homepage' button for better navigation - Enhanced mobile responsiveness for admin interface - Fixed table layouts and button arrangements for mobile devices - Backend API enhancements - New PATCH endpoint for toggling persistent status - Updated admin routes to include persistent field - Backwards compatible database migration --- public/admin.html | 153 +++++++++++++++++++++++++++++++++++++++++++--- public/admin.js | 56 ++++++++++++++++- public/style.css | 116 ++++++++++++++++++++++++++++++++++- server.js | 49 +++++++++++++-- 4 files changed, 358 insertions(+), 16 deletions(-) diff --git a/public/admin.html b/public/admin.html index 98274e5..e5858e6 100644 --- a/public/admin.html +++ b/public/admin.html @@ -122,13 +122,39 @@ margin-bottom: 20px; } - .logout-btn { - background-color: #dc3545; + .header-buttons { + display: flex; + gap: 10px; + align-items: center; + } + + .header-btn { + background-color: #6c757d; color: white; - padding: 8px 16px; + padding: 10px 16px; border: none; border-radius: 4px; cursor: pointer; + font-size: 14px; + text-decoration: none; + display: inline-block; + transition: background-color 0.2s; + } + + .header-btn:hover { + opacity: 0.9; + } + + .header-btn.btn-refresh { + background-color: #007bff; + } + + .header-btn.btn-home { + background-color: #28a745; + } + + .header-btn.btn-logout { + background-color: #dc3545; } .stats { @@ -158,6 +184,113 @@ text-overflow: ellipsis; white-space: nowrap; } + + /* Mobile responsiveness for admin panel */ + @media (max-width: 768px) { + .admin-container { + padding: 10px; + } + + .login-section { + margin: 20px auto; + padding: 20px; + } + + .admin-header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .admin-header h1 { + font-size: 1.5em; + margin: 0; + } + + .header-buttons { + flex-wrap: wrap; + justify-content: center; + } + + .header-btn { + font-size: 12px; + padding: 8px 12px; + } + + .stats { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + + .stat-card { + padding: 15px; + } + + .stat-number { + font-size: 1.5em; + } + + .locations-table { + font-size: 12px; + } + + .locations-table th, + .locations-table td { + padding: 6px 4px; + } + + .address-cell { + max-width: 120px; + font-size: 11px; + } + + .action-buttons { + flex-direction: column; + gap: 2px; + } + + .btn { + padding: 4px 6px; + font-size: 10px; + } + + .edit-input { + font-size: 12px; + padding: 2px; + } + } + + @media (max-width: 480px) { + .stats { + grid-template-columns: 1fr; + } + + .header-buttons { + flex-direction: column; + gap: 8px; + width: 100%; + } + + .header-btn { + font-size: 11px; + padding: 6px 10px; + text-align: center; + } + + .locations-table th, + .locations-table td { + padding: 4px 2px; + } + + .address-cell { + max-width: 100px; + } + + .btn { + padding: 3px 4px; + font-size: 9px; + } + } @@ -179,9 +312,10 @@

🚨 ICE Watch Admin Panel

-
- - +
+ 🏠 Homepage + +
@@ -198,6 +332,10 @@
0
Expired
+
+
0
+
Persistent
+
@@ -209,13 +347,14 @@ Status Address Description + Persistent Reported Actions - Loading... + Loading... diff --git a/public/admin.js b/public/admin.js index 725701a..bae082e 100644 --- a/public/admin.js +++ b/public/admin.js @@ -102,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => { renderLocationsTable(); } catch (error) { console.error('Error loading locations:', error); - locationsTableBody.innerHTML = 'Error loading locations'; + locationsTableBody.innerHTML = 'Error loading locations'; } } @@ -114,14 +114,17 @@ document.addEventListener('DOMContentLoaded', () => { new Date(location.created_at) > twentyFourHoursAgo ); + const persistentLocations = allLocations.filter(location => location.persistent); + document.getElementById('total-count').textContent = allLocations.length; document.getElementById('active-count').textContent = activeLocations.length; document.getElementById('expired-count').textContent = allLocations.length - activeLocations.length; + document.getElementById('persistent-count').textContent = persistentLocations.length; } function renderLocationsTable() { if (allLocations.length === 0) { - locationsTableBody.innerHTML = 'No locations found'; + locationsTableBody.innerHTML = 'No locations found'; return; } @@ -143,6 +146,13 @@ document.addEventListener('DOMContentLoaded', () => { ${location.address} ${location.description || ''} + + + ${getTimeAgo(location.created_at)}
@@ -178,6 +188,13 @@ document.addEventListener('DOMContentLoaded', () => { + + + ${getTimeAgo(location.created_at)}
@@ -262,6 +279,41 @@ document.addEventListener('DOMContentLoaded', () => { } }; + window.togglePersistent = async (id) => { + const location = allLocations.find(loc => loc.id === id); + if (!location) return; + + const newPersistentStatus = !location.persistent; + const confirmMessage = newPersistentStatus + ? 'Mark this report as persistent? It will not auto-expire after 24 hours.' + : 'Remove persistent status? This report will expire normally after 24 hours.'; + + if (!confirm(confirmMessage)) { + return; + } + + try { + const response = await fetch(`/api/admin/locations/${id}/persistent`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ persistent: newPersistentStatus }) + }); + + if (response.ok) { + loadLocations(); // Reload to get updated data + } else { + const error = await response.json(); + alert(`Error updating persistent status: ${error.error}`); + } + } catch (error) { + console.error('Error toggling persistent status:', error); + alert('Error updating persistent status. Please try again.'); + } + }; + window.deleteLocation = async (id) => { if (!confirm('Are you sure you want to delete this location report?')) { return; diff --git a/public/style.css b/public/style.css index db4fc90..f100ba2 100644 --- a/public/style.css +++ b/public/style.css @@ -282,10 +282,53 @@ footer { /* Responsive adjustments */ @media (max-width: 768px) { + .container { + padding: 10px; + } + + header h1 { + font-size: 1.5em; + } + + header p { + font-size: 0.9em; + } + + .map-section, .form-section { + padding: 10px; + margin-bottom: 15px; + } + + #map { + height: 400px; + } + + .form-group { + margin-bottom: 15px; + } + + input[type="text"], textarea { + font-size: 16px; /* Prevents zoom on iOS */ + width: calc(100% - 16px); + padding: 12px 8px; + } + + button[type="submit"] { + width: 100%; + padding: 15px; + font-size: 16px; + } + .reports-header { flex-direction: column; align-items: stretch; gap: 15px; + text-align: center; + } + + .reports-header h2 { + font-size: 1.3em; + margin-bottom: 0; } .view-toggle { @@ -294,6 +337,8 @@ footer { .toggle-btn { flex: 1; + font-size: 14px; + padding: 12px 8px; } .reports-table { @@ -302,14 +347,79 @@ footer { .reports-table th, .reports-table td { - padding: 8px; + padding: 8px 4px; } .location-cell { - max-width: 150px; + max-width: 120px; + font-size: 11px; } .details-cell { - max-width: 120px; + max-width: 100px; + font-size: 11px; + } + + .time-cell { + font-size: 10px; + } + + .remaining-cell { + font-size: 10px; + } + + .autocomplete-list { + max-height: 150px; + } + + .input-help { + font-size: 0.8em; + } + + footer { + padding: 20px 10px; + font-size: 0.9em; + } +} + +/* Extra small screens */ +@media (max-width: 480px) { + .container { + padding: 5px; + } + + header h1 { + font-size: 1.3em; + } + + .map-section, .form-section { + padding: 8px; + } + + #map { + height: 300px; + } + + .reports-table th { + font-size: 10px; + padding: 6px 2px; + } + + .reports-table td { + font-size: 10px; + padding: 6px 2px; + } + + .location-cell { + max-width: 100px; + } + + .details-cell { + max-width: 80px; + } + + .toggle-btn { + font-size: 12px; + padding: 10px 6px; } } diff --git a/server.js b/server.js index 716afb6..8ee32f7 100644 --- a/server.js +++ b/server.js @@ -28,25 +28,34 @@ db.serialize(() => { longitude REAL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, description TEXT, + persistent INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, (err) => { if (err) { console.error('Error initializing database:', err); } else { console.log('Database initialized successfully'); + // Add persistent column to existing tables if it doesn't exist + db.run(`ALTER TABLE locations ADD COLUMN persistent INTEGER DEFAULT 0`, (alterErr) => { + if (alterErr && !alterErr.message.includes('duplicate column name')) { + console.error('Error adding persistent column:', alterErr); + } else if (!alterErr) { + console.log('Added persistent column to existing table'); + } + }); } }); }); -// Clean up expired locations (older than 24 hours) +// Clean up expired locations (older than 24 hours, but not persistent ones) const cleanupExpiredLocations = () => { console.log('Running cleanup of expired locations'); const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - db.run('DELETE FROM locations WHERE created_at < ?', [twentyFourHoursAgo], function(err) { + db.run('DELETE FROM locations WHERE created_at < ? AND persistent = 0', [twentyFourHoursAgo], function(err) { if (err) { console.error('Error cleaning up expired locations:', err); } else { - console.log(`Cleaned up ${this.changes} expired locations`); + console.log(`Cleaned up ${this.changes} expired locations (persistent reports preserved)`); } }); }; @@ -163,7 +172,7 @@ app.post('/api/locations', (req, res) => { // Get all locations for admin (including expired ones) app.get('/api/admin/locations', authenticateAdmin, (req, res) => { db.all( - 'SELECT id, address, description, latitude, longitude, created_at FROM locations ORDER BY created_at DESC', + 'SELECT id, address, description, latitude, longitude, persistent, created_at FROM locations ORDER BY created_at DESC', [], (err, rows) => { if (err) { @@ -179,6 +188,7 @@ app.get('/api/admin/locations', authenticateAdmin, (req, res) => { description: row.description || '', latitude: row.latitude, longitude: row.longitude, + persistent: !!row.persistent, created_at: row.created_at, isActive: new Date(row.created_at) > new Date(Date.now() - 24 * 60 * 60 * 1000) })); @@ -218,6 +228,37 @@ app.put('/api/admin/locations/:id', authenticateAdmin, (req, res) => { ); }); +// Toggle persistent status of a location (admin only) +app.patch('/api/admin/locations/:id/persistent', authenticateAdmin, (req, res) => { + const { id } = req.params; + const { persistent } = req.body; + + if (typeof persistent !== 'boolean') { + res.status(400).json({ error: 'Persistent value must be a boolean' }); + return; + } + + db.run( + 'UPDATE locations SET persistent = ? WHERE id = ?', + [persistent ? 1 : 0, id], + function(err) { + if (err) { + console.error('Error updating persistent status:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + if (this.changes === 0) { + res.status(404).json({ error: 'Location not found' }); + return; + } + + console.log(`Location ${id} persistent status set to ${persistent}`); + res.json({ message: 'Persistent status updated successfully', persistent }); + } + ); +}); + // Delete a location (admin authentication required) app.delete('/api/admin/locations/:id', authenticateAdmin, (req, res) => { const { id } = req.params;