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 this.isManualGeocodingInProgress = false // 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.isManualGeocodingInProgress = true 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.isManualGeocodingInProgress = false 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 success message based on accuracy if (result.accuracy === 'exact') { this.showGeocodingSuccess("Adresse géolocalisée avec précision", result.display_name) } else { this.showGeocodingSuccess("Adresse géolocalisée approximativement", 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}"`) // Show progress for manual geocoding (not auto-geocoding) if (this.isManualGeocodingInProgress) { const strategyNames = ['adresse complète', 'rue et ville', 'ville seulement'] this.showGeocodingProgress(strategyNames[i] || `stratégie ${i + 1}`, `${i + 1}/${strategies.length}`) } try { const result = await this.tryGeocode(searchAddress) if (result) { console.log(`Geocoding successful with strategy ${i + 1}`) this.hideMessage("geocoding-progress") 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)) } } this.hideMessage("geocoding-progress") 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 `
Voir sur la carte :
${Object.entries(providers).map(([key, provider]) => ` ${provider.icon} ${provider.name} `).join('')}
` } // Clear coordinates clearCoordinates() { if (this.hasLatitudeTarget) this.latitudeTarget.value = "" if (this.hasLongitudeTarget) this.longitudeTarget.value = "" } // Clear map links clearMapLinks() { if (this.hasMapLinksContainerTarget) { this.mapLinksContainerTarget.innerHTML = "" } } // Show geocoding spinner in address input showGeocodingSpinner() { if (this.hasGeocodingSpinnerTarget) { this.geocodingSpinnerTarget.classList.remove('hidden') } } // Hide geocoding spinner in address input hideGeocodingSpinner() { if (this.hasGeocodingSpinnerTarget) { this.geocodingSpinnerTarget.classList.add('hidden') } } // Show loading state on "Ma position" button showGetCurrentLocationLoading() { if (this.hasGetCurrentLocationBtnTarget) { this.getCurrentLocationBtnTarget.disabled = true } if (this.hasGetCurrentLocationIconTarget) { this.getCurrentLocationIconTarget.innerHTML = '
' } if (this.hasGetCurrentLocationTextTarget) { this.getCurrentLocationTextTarget.textContent = 'Localisation...' } } // Hide loading state on "Ma position" button hideGetCurrentLocationLoading() { if (this.hasGetCurrentLocationBtnTarget) { this.getCurrentLocationBtnTarget.disabled = false } if (this.hasGetCurrentLocationIconTarget) { this.getCurrentLocationIconTarget.innerHTML = '' // Re-initialize Lucide icons if (window.lucide) { window.lucide.createIcons() } } if (this.hasGetCurrentLocationTextTarget) { this.getCurrentLocationTextTarget.textContent = 'Ma position' } } // Show loading state on "Prévisualiser" button showPreviewLocationLoading() { if (this.hasPreviewLocationBtnTarget) { this.previewLocationBtnTarget.disabled = true } if (this.hasPreviewLocationIconTarget) { this.previewLocationIconTarget.innerHTML = '
' } if (this.hasPreviewLocationTextTarget) { this.previewLocationTextTarget.textContent = 'Recherche...' } } // Hide loading state on "Prévisualiser" button hidePreviewLocationLoading() { if (this.hasPreviewLocationBtnTarget) { this.previewLocationBtnTarget.disabled = false } if (this.hasPreviewLocationIconTarget) { this.previewLocationIconTarget.innerHTML = '' // Re-initialize Lucide icons if (window.lucide) { window.lucide.createIcons() } } if (this.hasPreviewLocationTextTarget) { this.previewLocationTextTarget.textContent = 'Prévisualiser' } } // Show loading state showLocationLoading() { this.hideAllLocationMessages() this.showMessage("location-loading", "Géolocalisation en cours...", "loading") } // Hide loading state hideLocationLoading() { this.hideMessage("location-loading") } // Show success message showLocationSuccess(message) { this.hideAllLocationMessages() this.showMessage("location-success", message, "success") setTimeout(() => this.hideMessage("location-success"), 4000) } // Show error message showLocationError(message) { this.hideAllLocationMessages() this.showMessage("location-error", message, "error") setTimeout(() => this.hideMessage("location-error"), 6000) } // Show geocoding warning (less intrusive than error) showGeocodingWarning(address) { this.hideMessage("geocoding-warning") const message = "Les coordonnées n'ont pas pu être déterminées automatiquement. L'événement utilisera une localisation approximative." this.showMessage("geocoding-warning", message, "warning") setTimeout(() => this.hideMessage("geocoding-warning"), 8000) } // Show info about approximate location showApproximateLocationInfo(foundLocation) { this.hideMessage("approximate-location-info") const message = `Localisation approximative trouvée: ${foundLocation}` this.showMessage("approximate-location-info", message, "info") setTimeout(() => this.hideMessage("approximate-location-info"), 6000) } // Show geocoding success with location details showGeocodingSuccess(title, location) { this.hideMessage("geocoding-success") const message = `${title}
${location}` this.showMessage("geocoding-success", message, "success") setTimeout(() => this.hideMessage("geocoding-success"), 5000) } // Show geocoding progress with strategy info showGeocodingProgress(strategy, attempt) { this.hideMessage("geocoding-progress") const message = `Recherche en cours... (${attempt}/${strategy})` this.showMessage("geocoding-progress", message, "loading") } // Message template configurations getMessageTemplate(type) { const templates = { info: { bgColor: "bg-blue-50", borderColor: "border-blue-200", textColor: "text-blue-800", icon: "info", iconColor: "text-blue-500" }, success: { bgColor: "bg-green-50", borderColor: "border-green-200", textColor: "text-green-800", icon: "check-circle", iconColor: "text-green-500" }, error: { bgColor: "bg-red-50", borderColor: "border-red-200", textColor: "text-red-800", icon: "alert-circle", iconColor: "text-red-500" }, warning: { bgColor: "bg-yellow-50", borderColor: "border-yellow-200", textColor: "text-yellow-800", icon: "alert-triangle", iconColor: "text-yellow-500" }, loading: { bgColor: "bg-purple-50", borderColor: "border-purple-200", textColor: "text-purple-800", icon: "loader-2", iconColor: "text-purple-500", animated: true } } return templates[type] || templates.info } // Create dynamic message HTML using template createMessageHTML(id, message, type) { const template = this.getMessageTemplate(type) const animationClass = template.animated ? 'animate-spin' : '' return `

${message}

` } // Show a message with given type using template system showMessage(id, message, type) { // Remove existing message with same ID first this.hideMessage(id) const messageHtml = this.createMessageHTML(id, message, type) // Insert into the dedicated messages container in the venue section if (this.hasMessagesContainerTarget) { this.messagesContainerTarget.insertAdjacentHTML('beforeend', messageHtml) // Re-initialize Lucide icons for the new elements if (window.lucide) { window.lucide.createIcons() } } else { // Fallback: insert before the address input if messages container not found const addressInput = this.hasAddressTarget ? this.addressTarget.parentElement : null if (addressInput) { addressInput.insertAdjacentHTML('beforebegin', messageHtml) if (window.lucide) { window.lucide.createIcons() } } } } // Hide a specific message hideMessage(id) { const element = document.getElementById(id) if (element) { element.remove() } } // Hide all location messages hideAllLocationMessages() { this.hideMessage("location-loading") this.hideMessage("location-success") this.hideMessage("location-error") this.hideMessage("geocoding-warning") this.hideMessage("approximate-location-info") this.hideMessage("geocoding-success") this.hideMessage("geocoding-progress") } }