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>
<div class="add-city-form">
<div class="add-city-form" id="addCitySection" style="display: none;">
<h3>Add New City</h3>
<form id="addCityForm">
<div class="form-group">
@ -227,12 +227,31 @@
const data = await response.json();
user = data.user;
if (!user) {
document.getElementById('addCityForm').style.display = 'none';
}
updateUIBasedOnAuth();
} catch (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;
}
const clickText = user ? 'Click to update' : 'Click to view';
cityList.innerHTML = cities.map(city => `
<div class="city-item" onclick="selectCity('${city.name}')">
<div class="city-name">${city.display_name}</div>
<div class="city-stats">Click to view</div>
<div class="city-stats">${clickText}</div>
</div>
`).join('');
}

View File

@ -322,6 +322,34 @@
</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>
let map;
@ -333,6 +361,52 @@
let contextMenuPosition = 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() {
// Get city name from URL
const pathParts = window.location.pathname.split('/');
@ -457,6 +531,8 @@
// Expose functions to global scope for onclick handlers (for modal close buttons)
window.closeUpdateModal = closeUpdateModal;
window.closeAddModal = closeAddModal;
window.closeEditModal = closeEditModal;
window.openEditModal = openEditModal;
async function loadStations() {
try {
@ -522,11 +598,8 @@
}
function createPopupContent(station) {
const refillTime = station.last_refill_time ?
new Date(station.last_refill_time).toLocaleString() : 'Never';
const estimatedEmpty = station.estimated_empty_time ?
new Date(station.estimated_empty_time).toLocaleString() : 'Unknown';
const refillTime = formatTimeAgo(station.last_refill_time);
const estimatedEmpty = formatTimeUntil(station.estimated_empty_time);
return `
<div style="min-width: 200px;">
@ -534,10 +607,12 @@
<p><strong>Description:</strong> ${station.latest_description || 'No description'}</p>
<p><strong>Last Refill:</strong> ${refillTime}</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>
<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>
</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
window.onclick = function(event) {
const updateModal = document.getElementById('updateModal');
const addModal = document.getElementById('addModal');
const editModal = document.getElementById('editModal');
const contextMenu = document.getElementById('contextMenu');
if (event.target === updateModal) {
@ -594,6 +691,9 @@
if (event.target === addModal) {
closeAddModal();
}
if (event.target === editModal) {
closeEditModal();
}
if (!contextMenu.contains(event.target)) {
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() {
try {
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) => {
if (!req.user) {
return res.redirect('/login');