// 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; // 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); // 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); // Map click handler for adding locations map.on('click', handleMapClick); } // 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 function createLocationMarker(location) { const marker = L.marker([location.latitude, location.longitude], { title: location.title || 'Location', riseOnHover: true }).addTo(map); // Create popup content const popupContent = createPopupContent(location); marker.bindPopup(popupContent); return marker; } // Create popup content for marker function createPopupContent(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'); // Set coordinates latInput.value = lat.toFixed(8); lngInput.value = lng.toFixed(8); // Clear other fields document.getElementById('first-name').value = ''; document.getElementById('last-name').value = ''; document.getElementById('location-email').value = ''; document.getElementById('location-unit').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'); } // 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; } 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) { const div = document.createElement('div'); div.textContent = 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;