Allow users to edit the station itself when clicking a station (description, etc). Humanize refill/empty date/time formats. Remove last

updated line on the popup. On the select city page only show the add new city section and heading when logged in, adjust the login link
  based on logged-in status, and say click to update on each city button when logged in.
This commit is contained in:
Will Bradley 2025-07-16 13:16:16 -07:00
parent 429663d80c
commit 275c91f4e1
3 changed files with 188 additions and 13 deletions

View File

@ -194,7 +194,7 @@
<div class="loading">Loading cities...</div> <div class="loading">Loading cities...</div>
</div> </div>
<div class="add-city-form"> <div class="add-city-form" id="addCitySection" style="display: none;">
<h3>Add New City</h3> <h3>Add New City</h3>
<form id="addCityForm"> <form id="addCityForm">
<div class="form-group"> <div class="form-group">
@ -227,12 +227,31 @@
const data = await response.json(); const data = await response.json();
user = data.user; user = data.user;
if (!user) { updateUIBasedOnAuth();
document.getElementById('addCityForm').style.display = 'none';
}
} catch (error) { } catch (error) {
console.error('Auth check failed:', error); console.error('Auth check failed:', error);
document.getElementById('addCityForm').style.display = 'none'; updateUIBasedOnAuth();
}
}
function updateUIBasedOnAuth() {
const addCitySection = document.getElementById('addCitySection');
const authLinks = document.querySelector('.auth-links');
if (user) {
addCitySection.style.display = 'block';
authLinks.innerHTML = `
<a href="/dashboard">Dashboard</a>
<span>|</span>
<a href="/city/salem">View Salem (Default)</a>
`;
} else {
addCitySection.style.display = 'none';
authLinks.innerHTML = `
<a href="/login">Login</a>
<span>|</span>
<a href="/city/salem">View Salem (Default)</a>
`;
} }
} }
@ -256,10 +275,11 @@
return; return;
} }
const clickText = user ? 'Click to update' : 'Click to view';
cityList.innerHTML = cities.map(city => ` cityList.innerHTML = cities.map(city => `
<div class="city-item" onclick="selectCity('${city.name}')"> <div class="city-item" onclick="selectCity('${city.name}')">
<div class="city-name">${city.display_name}</div> <div class="city-name">${city.display_name}</div>
<div class="city-stats">Click to view</div> <div class="city-stats">${clickText}</div>
</div> </div>
`).join(''); `).join('');
} }

View File

@ -322,6 +322,34 @@
</div> </div>
</div> </div>
<!-- Edit Station Modal -->
<div id="editModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditModal()">&times;</span>
<h2>Edit Station</h2>
<div id="edit-message"></div>
<form id="editStationForm">
<div class="form-group">
<label for="editStationName">Station Name</label>
<input type="text" id="editStationName" name="name" required>
</div>
<div class="form-group">
<label for="editStationDescription">Description</label>
<textarea id="editStationDescription" name="description" placeholder="e.g., Public fountain in park"></textarea>
</div>
<div class="form-group">
<label>Location</label>
<input type="text" id="editCoordinates" readonly>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">Cancel</button>
</form>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script> <script>
let map; let map;
@ -333,6 +361,52 @@
let contextMenuPosition = null; let contextMenuPosition = null;
let longPressTimer = null; let longPressTimer = null;
// Helper function to format dates in a human-readable way
function formatTimeAgo(dateString) {
if (!dateString) return 'Never';
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) {
return 'Just now';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
} else if (diffInSeconds < 604800) {
const days = Math.floor(diffInSeconds / 86400);
return `${days} day${days !== 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString() + ' at ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
}
}
// Helper function to format estimated empty time
function formatTimeUntil(dateString) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((date - now) / 1000);
if (diffInSeconds <= 0) {
return 'Already empty';
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60);
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600);
return `${hours} hour${hours !== 1 ? 's' : ''}`;
} else {
const days = Math.floor(diffInSeconds / 86400);
return `${days} day${days !== 1 ? 's' : ''}`;
}
}
function initDashboard() { function initDashboard() {
// Get city name from URL // Get city name from URL
const pathParts = window.location.pathname.split('/'); const pathParts = window.location.pathname.split('/');
@ -457,6 +531,8 @@
// Expose functions to global scope for onclick handlers (for modal close buttons) // Expose functions to global scope for onclick handlers (for modal close buttons)
window.closeUpdateModal = closeUpdateModal; window.closeUpdateModal = closeUpdateModal;
window.closeAddModal = closeAddModal; window.closeAddModal = closeAddModal;
window.closeEditModal = closeEditModal;
window.openEditModal = openEditModal;
async function loadStations() { async function loadStations() {
try { try {
@ -522,11 +598,8 @@
} }
function createPopupContent(station) { function createPopupContent(station) {
const refillTime = station.last_refill_time ? const refillTime = formatTimeAgo(station.last_refill_time);
new Date(station.last_refill_time).toLocaleString() : 'Never'; const estimatedEmpty = formatTimeUntil(station.estimated_empty_time);
const estimatedEmpty = station.estimated_empty_time ?
new Date(station.estimated_empty_time).toLocaleString() : 'Unknown';
return ` return `
<div style="min-width: 200px;"> <div style="min-width: 200px;">
@ -534,10 +607,12 @@
<p><strong>Description:</strong> ${station.latest_description || 'No description'}</p> <p><strong>Description:</strong> ${station.latest_description || 'No description'}</p>
<p><strong>Last Refill:</strong> ${refillTime}</p> <p><strong>Last Refill:</strong> ${refillTime}</p>
<p><strong>Estimated Empty:</strong> ${estimatedEmpty}</p> <p><strong>Estimated Empty:</strong> ${estimatedEmpty}</p>
<p><strong>Last Updated:</strong> ${station.last_updated ? new Date(station.last_updated).toLocaleString() : 'Never'}</p>
<p><strong>Last Updated By:</strong> ${station.updated_by_name || 'Unknown'}</p> <p><strong>Last Updated By:</strong> ${station.updated_by_name || 'Unknown'}</p>
<div style="margin-top: 10px;">
<button class="popup-btn" onclick="openEditModal(${station.id})">Edit Station</button>
<button class="popup-btn" onclick="openUpdateModal(${station.id})">Update Status</button> <button class="popup-btn" onclick="openUpdateModal(${station.id})">Update Status</button>
</div> </div>
</div>
`; `;
} }
@ -582,10 +657,32 @@
} }
} }
function openEditModal(stationId) {
const station = stations.find(s => s.id === stationId);
if (!station) return;
selectedStation = station;
document.getElementById('editModal').style.display = 'block';
document.getElementById('editStationName').value = station.name;
document.getElementById('editStationDescription').value = station.description || '';
document.getElementById('editCoordinates').value = `${station.latitude.toFixed(6)}, ${station.longitude.toFixed(6)}`;
// Center map on selected station
map.setView([station.latitude, station.longitude], 16);
}
function closeEditModal() {
selectedStation = null;
document.getElementById('editModal').style.display = 'none';
document.getElementById('editStationForm').reset();
document.getElementById('edit-message').innerHTML = '';
}
// Close modal when clicking outside // Close modal when clicking outside
window.onclick = function(event) { window.onclick = function(event) {
const updateModal = document.getElementById('updateModal'); const updateModal = document.getElementById('updateModal');
const addModal = document.getElementById('addModal'); const addModal = document.getElementById('addModal');
const editModal = document.getElementById('editModal');
const contextMenu = document.getElementById('contextMenu'); const contextMenu = document.getElementById('contextMenu');
if (event.target === updateModal) { if (event.target === updateModal) {
@ -594,6 +691,9 @@
if (event.target === addModal) { if (event.target === addModal) {
closeAddModal(); closeAddModal();
} }
if (event.target === editModal) {
closeEditModal();
}
if (!contextMenu.contains(event.target)) { if (!contextMenu.contains(event.target)) {
hideContextMenu(); hideContextMenu();
} }
@ -706,6 +806,44 @@
} }
}); });
document.getElementById('editStationForm').addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedStation) {
showMessage('edit-message', 'Please select a station first', 'error');
return;
}
const formData = new FormData(e.target);
const data = {
name: formData.get('name'),
description: formData.get('description')
};
try {
const response = await fetch(`/api/stations/${selectedStation.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
showMessage('edit-message', 'Station updated successfully!');
setTimeout(() => {
closeEditModal();
loadStations();
}, 1500);
} else {
const result = await response.json();
showMessage('edit-message', result.error || 'Failed to update station', 'error');
}
} catch (error) {
showMessage('edit-message', 'Failed to update station', 'error');
}
});
async function logout() { async function logout() {
try { try {
await fetch('/auth/logout', { method: 'POST' }); await fetch('/auth/logout', { method: 'POST' });

View File

@ -411,6 +411,23 @@ app.post('/api/stations/:id/update', (req, res) => {
); );
}); });
app.put('/api/stations/:id', (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { name, description } = req.body;
const stationId = req.params.id;
db.run('UPDATE water_stations SET name = ?, description = ? WHERE id = ?',
[name, description, stationId],
function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ success: true });
}
);
});
app.get('/dashboard', (req, res) => { app.get('/dashboard', (req, res) => {
if (!req.user) { if (!req.user) {
return res.redirect('/login'); return res.redirect('/login');