// 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; // Add this line // Initialize application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { initializeMap(); loadLocations(); setupEventListeners(); checkConfiguration(); // Set up auto-refresh refreshInterval = setInterval(loadLocations, CONFIG.REFRESH_INTERVAL); }); // 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: '© OpenStreetMap 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); // Add legend addLegend(); // 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(); } // 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'; } } // Add legend to the map function addLegend() { const legend = L.control({ position: 'bottomright' }); legend.onAdd = function(map) { const div = L.DomUtil.create('div', 'map-legend'); div.innerHTML = `

Support Levels

1 - Strong Support
2 - Moderate Support
3 - Low Support
4 - No Support
Not Specified
`; return div; }; legend.addTo(map); } // 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 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 = ''; 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: '
', 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.textContent = '✕ Cancel'; btn.classList.remove('btn-success'); btn.classList.add('btn-secondary'); crosshair.classList.remove('hidden'); map.getContainer().style.cursor = 'crosshair'; } else { btn.textContent = '➕ Add Location Here'; 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 = ''; document.getElementById('location-address').value = ''; document.getElementById('sign').checked = false; document.getElementById('sign-size').value = ''; // Show modal modal.classList.remove('hidden'); // 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 => m.options.locationData.id === locationId || m.options.locationData.Id === locationId)?.options.locationData; if (!location) { 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'] || ''; document.getElementById('edit-location-address').value = location['Address'] || ''; // 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.textContent = '✕ Exit Fullscreen'; // 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.textContent = '⛶ Fullscreen'; // 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); } }); // Make closeModal function global for onclick handler window.closeModal = closeModal; window.editLocation = editLocation; window.closeEditFooter = closeEditFooter; window.deleteLocation = deleteLocation;