1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["latitude", "longitude", "accuracy", "gpsStatus", "gpsButton", 5 "gpsForm", "mapContainer", "locationAccuracy", "fallbackOptions", 6 "regionSelect", "districtSelect", "districtsData"] 7 8 connect() { 9 // Load districts data 10 if (this.hasDistrictsDataTarget) { 11 this.districtsData = JSON.parse(this.districtsDataTarget.textContent) 12 } 13 14 this.map = null 15 this.marker = null 16 this.accuracyCircle = null 17 } 18 19 disconnect() { 20 if (this.map) { 21 this.map.remove() 22 } 23 this.accuracyCircle = null 24 this.marker = null 25 } 26 27 requestGPS(event) { 28 event.preventDefault() 29 30 if (!navigator.geolocation) { 31 this.showGPSStatus("GPS not supported by your browser", "error") 32 this.showFallbackOptions() 33 return 34 } 35 36 // Disable button and show loading 37 this.gpsButtonTarget.disabled = true 38 this.gpsButtonTarget.innerHTML = ` 39 <svg class="animate-spin h-5 w-5 mr-3 inline" fill="none" viewBox="0 0 24 24"> 40 <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> 41 <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> 42 </svg> 43 Getting your location... 44 ` 45 46 navigator.geolocation.getCurrentPosition( 47 (position) => this.handleGPSSuccess(position), 48 (error) => this.handleGPSError(error), 49 { 50 enableHighAccuracy: true, 51 timeout: 15000, 52 maximumAge: 0 53 } 54 ) 55 } 56 57 handleGPSSuccess(position) { 58 const lat = position.coords.latitude 59 const lon = position.coords.longitude 60 const accuracy = position.coords.accuracy 61 62 this.latitudeTarget.value = lat 63 this.longitudeTarget.value = lon 64 this.accuracyTarget.value = accuracy 65 66 // Hide button and status 67 this.gpsButtonTarget.classList.add("hidden") 68 this.gpsStatusTarget.classList.add("hidden") 69 70 // Show map with location 71 this.showMapPreview(lat, lon, accuracy) 72 } 73 74 handleGPSError(error) { 75 let message = "Could not get your location" 76 77 switch(error.code) { 78 case error.PERMISSION_DENIED: 79 message = "Location permission denied." 80 break 81 case error.POSITION_UNAVAILABLE: 82 message = "Location information unavailable." 83 break 84 case error.TIMEOUT: 85 message = "Location request timed out." 86 break 87 } 88 89 this.showGPSStatus(message, "error") 90 91 // Re-enable button 92 this.gpsButtonTarget.disabled = false 93 this.gpsButtonTarget.innerHTML = ` 94 <svg class="w-6 h-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 95 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> 96 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> 97 </svg> 98 Share My Location 99 ` 100 101 // Show fallback options 102 this.showFallbackOptions() 103 } 104 105 showMapPreview(lat, lon, accuracy) { 106 this.mapContainerTarget.classList.remove("hidden") 107 this.locationAccuracyTarget.textContent = `Accuracy: ±${Math.round(accuracy)}m` 108 109 // Check if Leaflet is available 110 if (typeof L === 'undefined') { 111 this.showGPSStatus("Map library not loaded. Please refresh the page.", "error") 112 return 113 } 114 115 // Initialize Leaflet map 116 if (!this.map) { 117 this.map = L.map('location-map').setView([lat, lon], 13) 118 119 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 120 attribution: '© OpenStreetMap contributors', 121 maxZoom: 19 122 }).addTo(this.map) 123 } else { 124 this.map.setView([lat, lon], 13) 125 } 126 127 // Add marker 128 if (this.marker) { 129 this.marker.setLatLng([lat, lon]) 130 } else { 131 this.marker = L.marker([lat, lon]).addTo(this.map) 132 .bindPopup('ð Your Location') 133 .openPopup() 134 } 135 136 // Remove old accuracy circle if it exists 137 if (this.accuracyCircle) { 138 this.map.removeLayer(this.accuracyCircle) 139 } 140 141 // Add new accuracy circle 142 this.accuracyCircle = L.circle([lat, lon], { 143 radius: accuracy, 144 color: '#3b82f6', 145 fillColor: '#3b82f6', 146 fillOpacity: 0.1 147 }).addTo(this.map) 148 } 149 150 confirmLocation(event) { 151 event.preventDefault() 152 // Submit the form 153 this.gpsFormTarget.requestSubmit() 154 } 155 156 retryGPS(event) { 157 event.preventDefault() 158 159 // Reset UI 160 this.mapContainerTarget.classList.add("hidden") 161 this.gpsButtonTarget.classList.remove("hidden") 162 this.gpsButtonTarget.disabled = false 163 this.fallbackOptionsTarget.classList.add("hidden") 164 165 // Clear map and all resources 166 if (this.map) { 167 this.map.remove() 168 this.map = null 169 this.marker = null 170 this.accuracyCircle = null 171 } 172 } 173 174 showFallbackOptions() { 175 this.fallbackOptionsTarget.classList.remove("hidden") 176 } 177 178 showGPSStatus(message, type) { 179 this.gpsStatusTarget.textContent = message 180 this.gpsStatusTarget.classList.remove("hidden", "text-blue-600", "text-green-600", "text-red-600") 181 182 if (type === "loading") { 183 this.gpsStatusTarget.classList.add("text-blue-600", "dark:text-blue-400") 184 } else if (type === "success") { 185 this.gpsStatusTarget.classList.add("text-green-600", "dark:text-green-400") 186 } else if (type === "error") { 187 this.gpsStatusTarget.classList.add("text-red-600", "dark:text-red-400") 188 } 189 } 190 191 updateDistricts(event) { 192 const regionId = parseInt(event.target.value) 193 const districtSelect = this.districtSelectTarget 194 195 // Clear current options 196 districtSelect.innerHTML = '<option value="">Select a district...</option>' 197 198 if (!regionId) { 199 districtSelect.disabled = true 200 return 201 } 202
203 // Find the selected region 204 const region = this.districtsData.find(r => r.id === regionId) 205 206 if (region && region.districts) { 207 // Add district options 208 region.districts.forEach(district => { 209 const option = document.createElement('option') 210 option.value = district.id 211 option.textContent = district.name 212 districtSelect.appendChild(option) 213 }) 214 215 districtSelect.disabled = false 216 } 217 } 218}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.