import { Controller } from "@hotwired/stimulus" import slug from 'slug' export default class extends Controller { static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer", "geocodingSpinner", "getCurrentLocationBtn", "getCurrentLocationIcon", "getCurrentLocationText", "previewLocationBtn", "previewLocationIcon", "previewLocationText", "messagesContainer"] static values = { geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding } static lastGeocodingRequest = 0 connect() { this.geocodeTimeout = null // Initialize map links if we have an address and coordinates already exist if (this.hasAddressTarget && this.addressTarget.value.trim() && this.hasLatitudeTarget && this.hasLongitudeTarget && this.latitudeTarget.value && this.longitudeTarget.value) { this.updateMapLinks() } } disconnect() { if (this.geocodeTimeout) { clearTimeout(this.geocodeTimeout) } } // Generate slug from name generateSlug() { const name = this.nameTarget.value this.slugTarget.value = slug(name) } // Handle address changes with debounced geocoding addressChanged() { // Clear any existing timeout if (this.geocodeTimeout) { clearTimeout(this.geocodeTimeout) } const address = this.addressTarget.value.trim() if (!address) { this.clearCoordinates() this.clearMapLinks() this.hideGeocodingSpinner() return } // Show spinner after a brief delay to avoid flickering for very short typing const showSpinnerTimeout = setTimeout(() => { this.showGeocodingSpinner() }, 300) // Debounce geocoding to avoid too many API calls this.geocodeTimeout = setTimeout(async () => { clearTimeout(showSpinnerTimeout) // Cancel spinner delay if still pending this.showGeocodingSpinner() // Show spinner for sure now try { await this.geocodeAddressQuiet(address) } finally { this.hideGeocodingSpinner() } }, this.geocodeDelayValue) } // Get user's current location and reverse geocode to address async getCurrentLocation() { if (!navigator.geolocation) { this.showLocationError("La géolocalisation n'est pas supportée par ce navigateur.") return } this.showGetCurrentLocationLoading() this.showLocationLoading() const options = { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } try { const position = await this.getCurrentPositionPromise(options) const lat = position.coords.latitude const lng = position.coords.longitude // Set coordinates first this.latitudeTarget.value = lat.toFixed(6) this.longitudeTarget.value = lng.toFixed(6) // Then reverse geocode to get address const address = await this.reverseGeocode(lat, lng) if (address) { this.addressTarget.value = address this.showLocationSuccess("Position actuelle détectée et adresse mise à jour!") } else { this.showLocationSuccess("Position actuelle détectée!") } this.updateMapLinks() this.hideGetCurrentLocationLoading() } catch (error) { this.hideGetCurrentLocationLoading() this.hideLocationLoading() let message = "Erreur lors de la récupération de la localisation." switch(error.code) { case error.PERMISSION_DENIED: message = "L'accès à la localisation a été refusé." break case error.POSITION_UNAVAILABLE: message = "Les informations de localisation ne sont pas disponibles." break case error.TIMEOUT: message = "La demande de localisation a expiré." break } this.showLocationError(message) } finally { this.hideGetCurrentLocationLoading() } } // Promise wrapper for geolocation getCurrentPositionPromise(options) { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, options) }) } // Reverse geocode coordinates to get address async reverseGeocode(lat, lng) { try { const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&addressdetails=1`, { method: 'GET', headers: { 'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)', 'Accept': 'application/json' } }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() console.log('Reverse geocoding response:', data) // Debug log if (data && data.display_name) { return data.display_name } return null } catch (error) { console.log("Reverse geocoding failed:", error) return null } } // Preview location - same as updating map links but with user feedback previewLocation() { if (!this.hasAddressTarget || !this.addressTarget.value.trim()) { this.showLocationError("Veuillez saisir une adresse pour la prévisualiser.") return } // If we already have coordinates, just update map links if (this.hasLatitudeTarget && this.hasLongitudeTarget && this.latitudeTarget.value && this.longitudeTarget.value) { this.updateMapLinks() this.showLocationSuccess("Liens de carte mis à jour!") } else { // Otherwise geocode the address first this.showPreviewLocationLoading() this.geocodeAddress().finally(() => { this.hidePreviewLocationLoading() }) } } // Geocode address manually (with user feedback) async geocodeAddress() { if (!this.hasAddressTarget || !this.addressTarget.value.trim()) { this.showLocationError("Veuillez saisir une adresse.") return } const address = this.addressTarget.value.trim() try { this.showLocationLoading() const result = await this.performGeocode(address) if (result) { this.latitudeTarget.value = result.lat this.longitudeTarget.value = result.lng this.updateMapLinks() if (result.accuracy === 'exact') { this.showLocationSuccess("Coordonnées exactes trouvées pour cette adresse!") } else { this.showLocationSuccess(`Coordonnées approximatives trouvées: ${result.display_name}`) } } else { this.showLocationError("Impossible de trouver les coordonnées pour cette adresse.") } } catch (error) { this.showLocationError("Erreur lors de la recherche de l'adresse.") } finally { this.hideLocationLoading() } } // Geocode address quietly (no user feedback, for auto-geocoding) async geocodeAddressQuiet(address) { // Skip if address is too short or invalid if (!address || address.length < 5) { this.clearCoordinates() this.clearMapLinks() return } try { const result = await this.performGeocode(address) if (result && result.lat && result.lng) { this.latitudeTarget.value = result.lat this.longitudeTarget.value = result.lng this.updateMapLinks() console.log(`Auto-geocoded "${address}" to ${result.lat}, ${result.lng}`) // Show info if coordinates are approximate if (result.accuracy === 'approximate') { this.showApproximateLocationInfo(result.display_name) } } else { // If auto-geocoding fails, show a subtle warning this.showGeocodingWarning(address) } } catch (error) { console.log("Auto-geocoding failed:", error) this.showGeocodingWarning(address) } } // Perform the actual geocoding request with fallback strategies async performGeocode(address) { // Rate limiting: ensure at least 1 second between requests const now = Date.now() const timeSinceLastRequest = now - (this.constructor.lastGeocodingRequest || 0) if (timeSinceLastRequest < 1000) { await new Promise(resolve => setTimeout(resolve, 1000 - timeSinceLastRequest)) } this.constructor.lastGeocodingRequest = Date.now() // Try multiple geocoding strategies const strategies = [ // Strategy 1: Exact address address, // Strategy 2: Street name + city (remove house number) address.replace(/^\d+\s*/, ''), // Strategy 3: Just city and postal code this.extractCityAndPostalCode(address) ].filter(Boolean) // Remove null/undefined values for (let i = 0; i < strategies.length; i++) { const searchAddress = strategies[i] console.log(`Geocoding attempt ${i + 1}: "${searchAddress}"`) try { const result = await this.tryGeocode(searchAddress) if (result) { console.log(`Geocoding successful with strategy ${i + 1}`) return result } } catch (error) { console.log(`Strategy ${i + 1} failed:`, error.message) } // Add small delay between attempts if (i < strategies.length - 1) { await new Promise(resolve => setTimeout(resolve, 500)) } } console.log('All geocoding strategies failed') return null } // Extract city and postal code from address extractCityAndPostalCode(address) { // Look for French postal code pattern (5 digits) + city const match = address.match(/(\d{5})\s+([^,]+)/); if (match) { return `${match[1]} ${match[2].trim()}` } // Fallback: extract last part after comma (assume it's city) const parts = address.split(',') if (parts.length > 1) { return parts[parts.length - 1].trim() } return null } // Try a single geocoding request async tryGeocode(address) { const encodedAddress = encodeURIComponent(address.trim()) const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1&addressdetails=1`, { method: 'GET', headers: { 'User-Agent': 'AperoNight Event Platform/1.0 (https://aperonight.com)', 'Accept': 'application/json' } }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data = await response.json() if (data && data.length > 0) { const result = data[0] return { lat: parseFloat(result.lat).toFixed(6), lng: parseFloat(result.lon).toFixed(6), display_name: result.display_name, accuracy: address === result.display_name ? 'exact' : 'approximate' } } return null } // Update map links based on current coordinates updateMapLinks() { if (!this.hasMapLinksContainerTarget) return const lat = parseFloat(this.latitudeTarget.value) const lng = parseFloat(this.longitudeTarget.value) const address = this.hasAddressTarget ? this.addressTarget.value.trim() : "" if (isNaN(lat) || isNaN(lng) || !address) { this.clearMapLinks() return } const links = this.generateMapLinks(lat, lng, address) this.mapLinksContainerTarget.innerHTML = links } // Generate map links HTML generateMapLinks(lat, lng, address) { const encodedAddress = encodeURIComponent(address) const providers = { google: { name: "Google Maps", url: `https://www.google.com/maps/search/${encodedAddress},16z`, icon: "🔍" }, openstreetmap: { name: "OpenStreetMap", url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`, icon: "🗺️" }, apple: { name: "Apple Plans", url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`, icon: "🍎" } } return `
${message}