Files
aperonight/app/javascript/controllers/event_form_controller.js
kbe 10c93fff2f
Some checks failed
Ruby on Rails Test / rails-test (push) Failing after 1m50s
feat: Implement the promoter event creation
- Promoter can now create an event in draft mode
- Place is found based on address and long/lat are automatically
  deducted from it
- Slug is forged using the *slug* npm package instead of custom code
2025-09-10 20:49:06 +02:00

371 lines
11 KiB
JavaScript

import { Controller } from "@hotwired/stimulus"
import slug from 'slug'
export default class extends Controller {
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer"]
static values = {
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
}
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()
return
}
// Debounce geocoding to avoid too many API calls
this.geocodeTimeout = setTimeout(() => {
this.geocodeAddressQuiet(address)
}, 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.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()
} catch (error) {
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)
}
}
// 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`)
const data = await response.json()
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.geocodeAddress()
}
}
// 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()
this.showLocationSuccess("Coordonnées trouvées pour cette adresse!")
} 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) {
try {
const result = await this.performGeocode(address)
if (result) {
this.latitudeTarget.value = result.lat
this.longitudeTarget.value = result.lng
this.updateMapLinks()
} 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
async performGeocode(address) {
const encodedAddress = encodeURIComponent(address)
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1`)
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)
}
}
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 = {
openstreetmap: {
name: "OpenStreetMap",
url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`,
icon: "🗺️"
},
google: {
name: "Google Maps",
url: `https://www.google.com/maps/search/${encodedAddress}/@${lat},${lng},16z`,
icon: "🔍"
},
apple: {
name: "Apple Plans",
url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`,
icon: "🍎"
}
}
return `
<div class="space-y-2">
<div class="flex items-center space-x-2">
<i data-lucide="map-pin" class="w-4 h-4 text-gray-500"></i>
<span class="text-sm font-medium text-gray-700">Voir sur la carte :</span>
</div>
<div class="flex flex-wrap gap-2">
${Object.entries(providers).map(([key, provider]) => `
<a href="${provider.url}" target="_blank" rel="noopener"
class="inline-flex items-center px-3 py-2 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
<span class="mr-2">${provider.icon}</span>
${provider.name}
</a>
`).join('')}
</div>
</div>
`
}
// 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 loading state
showLocationLoading() {
this.hideAllLocationMessages()
this.showMessage("location-loading", "Géolocalisation en cours...", "info")
}
// 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 a message with given type
showMessage(id, message, type) {
const colors = {
info: "bg-blue-50 border-blue-200 text-blue-800",
success: "bg-green-50 border-green-200 text-green-800",
error: "bg-red-50 border-red-200 text-red-800",
warning: "bg-yellow-50 border-yellow-200 text-yellow-800"
}
const icons = {
info: "info",
success: "check-circle",
error: "alert-circle",
warning: "alert-triangle"
}
const messageHtml = `
<div id="${id}" class="flex items-center space-x-2 p-3 ${colors[type]} border rounded-lg mb-4">
<i data-lucide="${icons[type]}" class="w-4 h-4 flex-shrink-0"></i>
<span class="text-sm font-medium">${message}</span>
</div>
`
// Insert after the venue section header
const venueSection = this.element.querySelector('h3')
if (venueSection) {
venueSection.insertAdjacentHTML('afterend', messageHtml)
// Re-initialize Lucide icons for the new elements
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")
}
}