- 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
371 lines
11 KiB
JavaScript
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")
|
|
}
|
|
}
|