1052 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Global configuration
const CONFIG = {
DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461,
DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938,
DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11,
REFRESH_INTERVAL: 30000, // 30 seconds
MAX_ZOOM: 19,
MIN_ZOOM: 2
};
// Application state
let map = null;
let markers = [];
let userLocationMarker = null;
let isAddingLocation = false;
let refreshInterval = null;
let currentEditingLocation = null;
let currentUser = null; // Add current user state
// Initialize application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initializeMap();
checkAuthentication(); // Add authentication check
loadLocations();
setupEventListeners();
checkConfiguration();
// Set up auto-refresh
refreshInterval = setInterval(loadLocations, CONFIG.REFRESH_INTERVAL);
// Add event delegation for dynamically created edit buttons
document.addEventListener('click', function(e) {
if (e.target.classList.contains('edit-location-btn')) {
const locationId = e.target.getAttribute('data-location-id');
editLocation(locationId);
}
});
});
// Initialize Leaflet map
function initializeMap() {
// Create map instance
map = L.map('map', {
center: [CONFIG.DEFAULT_LAT, CONFIG.DEFAULT_LNG],
zoom: CONFIG.DEFAULT_ZOOM,
zoomControl: true,
attributionControl: true
});
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: CONFIG.MAX_ZOOM,
minZoom: CONFIG.MIN_ZOOM
}).addTo(map);
// Add scale control
L.control.scale({
position: 'bottomleft',
metric: true,
imperial: false
}).addTo(map);
// Hide loading overlay
document.getElementById('loading').classList.add('hidden');
}
// Set up event listeners
function setupEventListeners() {
// Geolocation button
document.getElementById('geolocate-btn').addEventListener('click', handleGeolocation);
// Add location button
document.getElementById('add-location-btn').addEventListener('click', toggleAddLocation);
// Refresh button
document.getElementById('refresh-btn').addEventListener('click', () => {
showStatus('Refreshing locations...', 'info');
loadLocations();
});
// Fullscreen button
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullscreen);
// Form submission
document.getElementById('location-form').addEventListener('submit', handleLocationSubmit);
// Edit form submission
document.getElementById('edit-location-form').addEventListener('submit', handleEditLocationSubmit);
// Map click handler for adding locations
map.on('click', handleMapClick);
// Set up geo field synchronization
setupGeoFieldSync();
// Add event listeners for buttons that were using inline onclick
document.getElementById('close-edit-footer-btn').addEventListener('click', closeEditFooter);
document.getElementById('lookup-address-edit-btn').addEventListener('click', lookupAddressForEdit);
document.getElementById('delete-location-btn').addEventListener('click', deleteLocation);
document.getElementById('close-modal-btn').addEventListener('click', closeModal);
document.getElementById('lookup-address-add-btn').addEventListener('click', lookupAddressForAdd);
document.getElementById('cancel-modal-btn').addEventListener('click', closeModal);
}
// Helper function to get color based on support level
function getSupportColor(supportLevel) {
const level = parseInt(supportLevel);
switch(level) {
case 1: return '#27ae60'; // Green - Strong support
case 2: return '#f1c40f'; // Yellow - Moderate support
case 3: return '#e67e22'; // Orange - Low support
case 4: return '#e74c3c'; // Red - No support
default: return '#95a5a6'; // Grey - Unknown/null
}
}
// Helper function to get support level text
function getSupportLevelText(level) {
const levelNum = parseInt(level);
switch(levelNum) {
case 1: return '1 - Strong Support';
case 2: return '2 - Moderate Support';
case 3: return '3 - Low Support';
case 4: return '4 - No Support';
default: return 'Not Specified';
}
}
// Set up geo field synchronization
function setupGeoFieldSync() {
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const geoLocationInput = document.getElementById('geo-location');
// Validate geo-location format
function validateGeoLocation(value) {
if (!value) return false;
// Check both formats
const patterns = [
/^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$/, // comma-separated
/^-?\d+\.?\d*\s*;\s*-?\d+\.?\d*$/ // semicolon-separated
];
return patterns.some(pattern => pattern.test(value));
}
// When lat/lng change, update geo-location
function updateGeoLocation() {
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) {
geoLocationInput.value = `${lat};${lng}`; // Use semicolon format for NocoDB
geoLocationInput.classList.remove('invalid');
geoLocationInput.classList.add('valid');
}
}
// When geo-location changes, parse and update lat/lng
function parseGeoLocation() {
const geoValue = geoLocationInput.value.trim();
if (!geoValue) {
geoLocationInput.classList.remove('valid', 'invalid');
return;
}
if (!validateGeoLocation(geoValue)) {
geoLocationInput.classList.add('invalid');
geoLocationInput.classList.remove('valid');
return;
}
// Try semicolon-separated first
let parts = geoValue.split(';');
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
// Keep semicolon format for NocoDB GeoData
geoLocationInput.value = `${lat};${lng}`;
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
return;
}
}
// Try comma-separated
parts = geoValue.split(',');
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
// Normalize to semicolon format for NocoDB GeoData
geoLocationInput.value = `${lat};${lng}`;
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
}
}
}
// Add event listeners
latInput.addEventListener('input', updateGeoLocation);
lngInput.addEventListener('input', updateGeoLocation);
geoLocationInput.addEventListener('blur', parseGeoLocation);
geoLocationInput.addEventListener('input', () => {
// Clear validation classes on input to allow real-time feedback
const geoValue = geoLocationInput.value.trim();
if (geoValue && validateGeoLocation(geoValue)) {
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
} else if (geoValue) {
geoLocationInput.classList.add('invalid');
geoLocationInput.classList.remove('valid');
} else {
geoLocationInput.classList.remove('valid', 'invalid');
}
});
}
// Check authentication and display user info
async function checkAuthentication() {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (data.authenticated && data.user) {
currentUser = data.user;
displayUserInfo();
}
} catch (error) {
console.error('Failed to check authentication:', error);
}
}
// Display user info in header
function displayUserInfo() {
const headerActions = document.querySelector('.header-actions');
// Create user info element
const userInfo = document.createElement('div');
userInfo.className = 'user-info';
userInfo.innerHTML = `
<span class="user-email">${escapeHtml(currentUser.email)}</span>
<button id="logout-btn" class="btn btn-secondary btn-sm" title="Sign out">
🚪 Logout
</button>
`;
// Insert before the location count
const locationCount = document.getElementById('location-count');
headerActions.insertBefore(userInfo, locationCount);
// Add logout event listener
document.getElementById('logout-btn').addEventListener('click', handleLogout);
}
// Handle logout
async function handleLogout() {
if (!confirm('Are you sure you want to logout?')) {
return;
}
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
window.location.href = '/login.html';
} else {
showStatus('Logout failed. Please try again.', 'error');
}
} catch (error) {
console.error('Logout error:', error);
showStatus('Logout failed. Please try again.', 'error');
}
}
// Check API configuration
async function checkConfiguration() {
try {
const response = await fetch('/api/config-check');
const data = await response.json();
if (!data.configured) {
showStatus('Warning: API not fully configured. Check your .env file.', 'warning');
}
} catch (error) {
console.error('Configuration check failed:', error);
}
}
// Load locations from API
async function loadLocations() {
try {
const response = await fetch('/api/locations');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
displayLocations(data.locations);
updateLocationCount(data.count);
} else {
throw new Error(data.error || 'Failed to load locations');
}
} catch (error) {
console.error('Error loading locations:', error);
showStatus('Failed to load locations. Check your connection.', 'error');
updateLocationCount(0);
}
}
// Display locations on map
function displayLocations(locations) {
// Clear existing markers
markers.forEach(marker => map.removeLayer(marker));
markers = [];
// Add new markers
locations.forEach(location => {
if (location.latitude && location.longitude) {
const marker = createLocationMarker(location);
markers.push(marker);
}
});
// Fit map to show all markers if there are any
if (markers.length > 0) {
const group = new L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.1));
}
}
// Create marker for location (updated to use circle markers)
function createLocationMarker(location) {
console.log('Creating marker for location:', location);
// Get color based on support level
const supportColor = getSupportColor(location['Support Level']);
// Create circle marker instead of default marker
const marker = L.circleMarker([location.latitude, location.longitude], {
radius: 8,
fillColor: supportColor,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8,
title: location.title || 'Location',
riseOnHover: true,
locationData: location // Store location data in marker options
}).addTo(map);
// Add larger radius on hover
marker.on('mouseover', function() {
this.setRadius(10);
});
marker.on('mouseout', function() {
this.setRadius(8);
});
// Create popup content
const popupContent = createPopupContent(location);
marker.bindPopup(popupContent);
return marker;
}
// Create popup content for marker
function createPopupContent(location) {
console.log('Creating popup for location:', location);
let content = '<div class="popup-content">';
// Handle name
let displayName = '';
if (location.title) {
displayName = location.title;
} else if (location['First Name'] || location['Last Name']) {
const firstName = location['First Name'] || '';
const lastName = location['Last Name'] || '';
displayName = `${firstName} ${lastName}`.trim();
}
if (displayName) {
content += `<h3>${escapeHtml(displayName)}</h3>`;
}
// Support Level with color indicator
const supportColor = getSupportColor(location['Support Level']);
const supportText = getSupportLevelText(location['Support Level']);
content += `<p><strong>Support Level:</strong> <span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${supportColor}; margin-right: 5px;"></span>${escapeHtml(supportText)}</p>`;
// Display all available fields
if (location['Email']) {
content += `<p><strong>Email:</strong> ${escapeHtml(location['Email'])}</p>`;
}
if (location['Unit Number']) {
content += `<p><strong>Unit Number:</strong> ${escapeHtml(location['Unit Number'])}</p>`;
}
if (location['Address']) {
content += `<p><strong>Address:</strong> ${escapeHtml(location['Address'])}</p>`;
}
// Sign information
if (location['Sign']) {
content += `<p><strong>Has Sign:</strong> Yes`;
if (location['Sign Size']) {
content += ` (${escapeHtml(location['Sign Size'])})`;
}
content += '</p>';
}
if (location.description) {
content += `<p><strong>Description:</strong> ${escapeHtml(location.description)}</p>`;
}
if (location.category) {
content += `<p><strong>Category:</strong> ${escapeHtml(location.category)}</p>`;
}
content += '<div class="popup-meta">';
content += `<p><strong>Coordinates:</strong> ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}</p>`;
if (location['Geo-Location']) {
content += `<p><strong>Geo-Location:</strong> ${escapeHtml(location['Geo-Location'])}</p>`;
}
if (location.created_at) {
const date = new Date(location.created_at);
content += `<p><strong>Added:</strong> ${date.toLocaleDateString()}</p>`;
}
if (location.updated_at) {
const date = new Date(location.updated_at);
content += `<p><strong>Updated:</strong> ${date.toLocaleDateString()}</p>`;
}
content += '</div>';
// Add edit button with data attribute instead of onclick
content += `<div style="margin-top: 10px; text-align: center;">`;
content += `<button class="btn btn-primary btn-sm edit-location-btn" data-location-id="${location.id || location.Id}">✏️ Edit</button>`;
content += '</div>';
content += '</div>';
return content;
}
// Handle geolocation
function handleGeolocation() {
if (!navigator.geolocation) {
showStatus('Geolocation is not supported by your browser', 'error');
return;
}
showStatus('Getting your location...', 'info');
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
// Center map on user location
map.setView([latitude, longitude], 15);
// Remove existing user marker
if (userLocationMarker) {
map.removeLayer(userLocationMarker);
}
// Add user location marker
userLocationMarker = L.marker([latitude, longitude], {
icon: L.divIcon({
html: '<div style="background-color: #2c5aa0; width: 20px; height: 20px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);"></div>',
className: 'user-location-marker',
iconSize: [20, 20],
iconAnchor: [10, 10]
}),
title: 'Your location'
}).addTo(map);
// Add accuracy circle
L.circle([latitude, longitude], {
radius: accuracy,
color: '#2c5aa0',
fillColor: '#2c5aa0',
fillOpacity: 0.1,
weight: 1
}).addTo(map);
showStatus(`Location found (±${Math.round(accuracy)}m accuracy)`, 'success');
},
(error) => {
let message = 'Unable to get your location';
switch (error.code) {
case error.PERMISSION_DENIED:
message = 'Location permission denied';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information unavailable';
break;
case error.TIMEOUT:
message = 'Location request timed out';
break;
}
showStatus(message, 'error');
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
}
// Toggle add location mode
function toggleAddLocation() {
isAddingLocation = !isAddingLocation;
const btn = document.getElementById('add-location-btn');
const crosshair = document.getElementById('crosshair');
if (isAddingLocation) {
btn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Cancel</span>';
btn.classList.remove('btn-success');
btn.classList.add('btn-secondary');
crosshair.classList.remove('hidden');
map.getContainer().style.cursor = 'crosshair';
} else {
btn.innerHTML = '<span class="btn-icon"></span><span class="btn-text">Add Location Here</span>';
btn.classList.remove('btn-secondary');
btn.classList.add('btn-success');
crosshair.classList.add('hidden');
map.getContainer().style.cursor = '';
}
}
// Handle map click
function handleMapClick(e) {
if (!isAddingLocation) return;
const { lat, lng } = e.latlng;
// Toggle off add location mode
toggleAddLocation();
// Show modal with coordinates
showAddLocationModal(lat, lng);
}
// Show add location modal
function showAddLocationModal(lat, lng) {
const modal = document.getElementById('add-modal');
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const geoLocationInput = document.getElementById('geo-location');
// Set coordinates
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
// Set geo-location field
geoLocationInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; // Use semicolon format for NocoDB
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
// Clear other fields
document.getElementById('first-name').value = '';
document.getElementById('last-name').value = '';
document.getElementById('location-email').value = '';
document.getElementById('location-unit').value = '';
document.getElementById('support-level').value = '';
const addressInput = document.getElementById('location-address');
addressInput.value = 'Looking up address...'; // Show loading message
document.getElementById('sign').checked = false;
document.getElementById('sign-size').value = '';
// Show modal
modal.classList.remove('hidden');
// Fetch address asynchronously
reverseGeocode(lat, lng).then(result => {
if (result) {
addressInput.value = result.formattedAddress || result.fullAddress;
} else {
addressInput.value = ''; // Clear if lookup fails
// Don't show warning for automatic lookups
}
}).catch(error => {
console.error('Address lookup failed:', error);
addressInput.value = '';
});
// Focus on first name input
setTimeout(() => {
document.getElementById('first-name').focus();
}, 100);
}
// Close modal
function closeModal() {
document.getElementById('add-modal').classList.add('hidden');
}
// Edit location function
function editLocation(locationId) {
// Find the location in markers data
const location = markers.find(m => {
const data = m.options.locationData;
return String(data.id || data.Id) === String(locationId);
})?.options.locationData;
if (!location) {
console.error('Location not found for ID:', locationId);
console.log('Available locations:', markers.map(m => ({
id: m.options.locationData.id || m.options.locationData.Id,
name: m.options.locationData['First Name'] + ' ' + m.options.locationData['Last Name']
})));
showStatus('Location not found', 'error');
return;
}
currentEditingLocation = location;
// Populate all the edit form fields
document.getElementById('edit-location-id').value = location.id || location.Id || '';
document.getElementById('edit-first-name').value = location['First Name'] || '';
document.getElementById('edit-last-name').value = location['Last Name'] || '';
document.getElementById('edit-location-email').value = location['Email'] || '';
document.getElementById('edit-location-unit').value = location['Unit Number'] || '';
document.getElementById('edit-support-level').value = location['Support Level'] || '';
const addressInput = document.getElementById('edit-location-address');
addressInput.value = location['Address'] || '';
// If no address exists, try to fetch it
if (!location['Address'] && location.latitude && location.longitude) {
addressInput.value = 'Looking up address...';
reverseGeocode(location.latitude, location.longitude).then(result => {
if (result && !location['Address']) {
addressInput.value = result.formattedAddress || result.fullAddress;
} else if (!location['Address']) {
addressInput.value = '';
// Don't show error - just silently fail
}
}).catch(error => {
// Handle any unexpected errors
console.error('Address lookup failed:', error);
addressInput.value = '';
});
}
// Handle checkbox
document.getElementById('edit-sign').checked = location['Sign'] === true || location['Sign'] === 'true' || location['Sign'] === 1;
document.getElementById('edit-sign-size').value = location['Sign Size'] || '';
document.getElementById('edit-location-lat').value = location.latitude || '';
document.getElementById('edit-location-lng').value = location.longitude || '';
document.getElementById('edit-geo-location').value = location['Geo-Location'] || `${location.latitude};${location.longitude}`;
// Show the edit footer
document.getElementById('edit-footer').classList.remove('hidden');
document.getElementById('map-container').classList.add('edit-mode');
// Invalidate map size after showing footer
setTimeout(() => map.invalidateSize(), 300);
// Setup geo field sync for edit form
setupEditGeoFieldSync();
}
// Close edit footer
function closeEditFooter() {
document.getElementById('edit-footer').classList.add('hidden');
document.getElementById('map-container').classList.remove('edit-mode');
currentEditingLocation = null;
// Invalidate map size after hiding footer
setTimeout(() => map.invalidateSize(), 300);
}
// Setup geo field sync for edit form
function setupEditGeoFieldSync() {
const latInput = document.getElementById('edit-location-lat');
const lngInput = document.getElementById('edit-location-lng');
const geoLocationInput = document.getElementById('edit-geo-location');
// Similar to setupGeoFieldSync but for edit form
function updateGeoLocation() {
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) {
geoLocationInput.value = `${lat};${lng}`;
geoLocationInput.classList.remove('invalid');
geoLocationInput.classList.add('valid');
}
}
function parseGeoLocation() {
const geoValue = geoLocationInput.value.trim();
if (!geoValue) {
geoLocationInput.classList.remove('valid', 'invalid');
return;
}
// Try semicolon-separated first
let parts = geoValue.split(';');
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
return;
}
}
// Try comma-separated
parts = geoValue.split(',');
if (parts.length === 2) {
const lat = parseFloat(parts[0].trim());
const lng = parseFloat(parts[1].trim());
if (!isNaN(lat) && !isNaN(lng)) {
latInput.value = lat.toFixed(8);
lngInput.value = lng.toFixed(8);
geoLocationInput.value = `${lat};${lng}`;
geoLocationInput.classList.add('valid');
geoLocationInput.classList.remove('invalid');
}
}
}
latInput.addEventListener('input', updateGeoLocation);
lngInput.addEventListener('input', updateGeoLocation);
geoLocationInput.addEventListener('blur', parseGeoLocation);
}
// Handle edit form submission
async function handleEditLocationSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
const locationId = data.id;
// Ensure Geo-Location field is included
const geoLocationInput = document.getElementById('edit-geo-location');
if (geoLocationInput.value) {
data['Geo-Location'] = geoLocationInput.value;
}
try {
const response = await fetch(`/api/locations/${locationId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('Location updated successfully!', 'success');
closeEditFooter();
// Reload locations
loadLocations();
} else {
throw new Error(result.error || 'Failed to update location');
}
} catch (error) {
console.error('Error updating location:', error);
showStatus(error.message, 'error');
}
}
// Delete location
async function deleteLocation() {
if (!currentEditingLocation) return;
const locationId = currentEditingLocation.id || currentEditingLocation.Id;
if (!confirm('Are you sure you want to delete this location?')) {
return;
}
try {
const response = await fetch(`/api/locations/${locationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('Location deleted successfully!', 'success');
closeEditFooter();
// Reload locations
loadLocations();
} else {
throw new Error(result.error || 'Failed to delete location');
}
} catch (error) {
console.error('Error deleting location:', error);
showStatus(error.message, 'error');
}
}
// Handle location form submission
async function handleLocationSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
// Validate required fields - either first name or last name should be provided
if ((!data['First Name'] || !data['First Name'].trim()) &&
(!data['Last Name'] || !data['Last Name'].trim())) {
showStatus('Either First Name or Last Name is required', 'error');
return;
}
// Ensure Geo-Location field is included
const geoLocationInput = document.getElementById('geo-location');
if (geoLocationInput.value) {
data['Geo-Location'] = geoLocationInput.value;
}
try {
const response = await fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok && result.success) {
showStatus('Location added successfully!', 'success');
closeModal();
// Reload locations
loadLocations();
// Center map on new location
map.setView([data.latitude, data.longitude], map.getZoom());
} else {
throw new Error(result.error || 'Failed to add location');
}
} catch (error) {
console.error('Error adding location:', error);
showStatus(error.message, 'error');
}
}
// Toggle fullscreen
function toggleFullscreen() {
const app = document.getElementById('app');
const btn = document.getElementById('fullscreen-btn');
if (!document.fullscreenElement) {
app.requestFullscreen().then(() => {
app.classList.add('fullscreen');
btn.innerHTML = '<span class="btn-icon">✕</span><span class="btn-text">Exit Fullscreen</span>';
// Invalidate map size after transition
setTimeout(() => map.invalidateSize(), 300);
}).catch(err => {
showStatus('Unable to enter fullscreen', 'error');
});
} else {
document.exitFullscreen().then(() => {
app.classList.remove('fullscreen');
btn.innerHTML = '<span class="btn-icon">⛶</span><span class="btn-text">Fullscreen</span>';
// Invalidate map size after transition
setTimeout(() => map.invalidateSize(), 300);
});
}
}
// Update location count
function updateLocationCount(count) {
const countElement = document.getElementById('location-count');
countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`;
}
// Show status message
function showStatus(message, type = 'info') {
const container = document.getElementById('status-container');
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
// Auto-remove after 5 seconds
setTimeout(() => {
messageDiv.remove();
}, 5000);
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
if (text === null || text === undefined) {
return '';
}
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// Handle window resize
window.addEventListener('resize', () => {
map.invalidateSize();
});
// Clean up on page unload
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
// Reverse geocode to get address from coordinates
async function reverseGeocode(lat, lng) {
try {
const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Geocoding service unavailable');
}
const result = await response.json();
if (!result.success || !result.data) {
throw new Error('Geocoding failed');
}
return result.data;
} catch (error) {
console.error('Reverse geocoding error:', error);
return null;
}
}
// Add a new function for forward geocoding (address to coordinates)
async function forwardGeocode(address) {
try {
const response = await fetch(`/api/geocode/forward?address=${encodeURIComponent(address)}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Geocoding service unavailable');
}
const result = await response.json();
if (!result.success || !result.data) {
throw new Error('Geocoding failed');
}
return result.data;
} catch (error) {
console.error('Forward geocoding error:', error);
return null;
}
}
// Manual address lookup for add form
async function lookupAddressForAdd() {
const latInput = document.getElementById('location-lat');
const lngInput = document.getElementById('location-lng');
const addressInput = document.getElementById('location-address');
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) {
addressInput.value = 'Looking up address...';
const result = await reverseGeocode(lat, lng);
if (result) {
addressInput.value = result.formattedAddress || result.fullAddress;
showStatus('Address found!', 'success');
} else {
addressInput.value = '';
showStatus('Could not find address for these coordinates', 'warning');
}
} else {
showStatus('Please enter valid coordinates first', 'warning');
}
}
// Manual address lookup for edit form
async function lookupAddressForEdit() {
const latInput = document.getElementById('edit-location-lat');
const lngInput = document.getElementById('edit-location-lng');
const addressInput = document.getElementById('edit-location-address');
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) {
addressInput.value = 'Looking up address...';
const result = await reverseGeocode(lat, lng);
if (result) {
addressInput.value = result.formattedAddress || result.fullAddress;
showStatus('Address found!', 'success');
} else {
addressInput.value = '';
showStatus('Could not find address for these coordinates', 'warning');
}
} else {
showStatus('Please enter valid coordinates first', 'warning');
}
}