develop #3
@@ -2,11 +2,13 @@ import { Controller } from "@hotwired/stimulus"
|
|||||||
import slug from 'slug'
|
import slug from 'slug'
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer"]
|
static targets = ["name", "slug", "latitude", "longitude", "address", "mapLinksContainer", "geocodingSpinner", "getCurrentLocationBtn", "getCurrentLocationIcon", "getCurrentLocationText", "previewLocationBtn", "previewLocationIcon", "previewLocationText", "messagesContainer"]
|
||||||
static values = {
|
static values = {
|
||||||
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
|
geocodeDelay: { type: Number, default: 1500 } // Delay before auto-geocoding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static lastGeocodingRequest = 0
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.geocodeTimeout = null
|
this.geocodeTimeout = null
|
||||||
|
|
||||||
@@ -43,12 +45,25 @@ export default class extends Controller {
|
|||||||
if (!address) {
|
if (!address) {
|
||||||
this.clearCoordinates()
|
this.clearCoordinates()
|
||||||
this.clearMapLinks()
|
this.clearMapLinks()
|
||||||
|
this.hideGeocodingSpinner()
|
||||||
return
|
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
|
// Debounce geocoding to avoid too many API calls
|
||||||
this.geocodeTimeout = setTimeout(() => {
|
this.geocodeTimeout = setTimeout(async () => {
|
||||||
this.geocodeAddressQuiet(address)
|
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)
|
}, this.geocodeDelayValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +74,7 @@ export default class extends Controller {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.showGetCurrentLocationLoading()
|
||||||
this.showLocationLoading()
|
this.showLocationLoading()
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -87,8 +103,10 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateMapLinks()
|
this.updateMapLinks()
|
||||||
|
this.hideGetCurrentLocationLoading()
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.hideGetCurrentLocationLoading()
|
||||||
this.hideLocationLoading()
|
this.hideLocationLoading()
|
||||||
let message = "Erreur lors de la récupération de la localisation."
|
let message = "Erreur lors de la récupération de la localisation."
|
||||||
|
|
||||||
@@ -105,6 +123,8 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.showLocationError(message)
|
this.showLocationError(message)
|
||||||
|
} finally {
|
||||||
|
this.hideGetCurrentLocationLoading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +138,20 @@ export default class extends Controller {
|
|||||||
// Reverse geocode coordinates to get address
|
// Reverse geocode coordinates to get address
|
||||||
async reverseGeocode(lat, lng) {
|
async reverseGeocode(lat, lng) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`)
|
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()
|
const data = await response.json()
|
||||||
|
console.log('Reverse geocoding response:', data) // Debug log
|
||||||
|
|
||||||
if (data && data.display_name) {
|
if (data && data.display_name) {
|
||||||
return data.display_name
|
return data.display_name
|
||||||
@@ -146,7 +178,10 @@ export default class extends Controller {
|
|||||||
this.showLocationSuccess("Liens de carte mis à jour!")
|
this.showLocationSuccess("Liens de carte mis à jour!")
|
||||||
} else {
|
} else {
|
||||||
// Otherwise geocode the address first
|
// Otherwise geocode the address first
|
||||||
this.geocodeAddress()
|
this.showPreviewLocationLoading()
|
||||||
|
this.geocodeAddress().finally(() => {
|
||||||
|
this.hidePreviewLocationLoading()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +202,12 @@ export default class extends Controller {
|
|||||||
this.latitudeTarget.value = result.lat
|
this.latitudeTarget.value = result.lat
|
||||||
this.longitudeTarget.value = result.lng
|
this.longitudeTarget.value = result.lng
|
||||||
this.updateMapLinks()
|
this.updateMapLinks()
|
||||||
this.showLocationSuccess("Coordonnées trouvées pour cette adresse!")
|
|
||||||
|
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 {
|
} else {
|
||||||
this.showLocationError("Impossible de trouver les coordonnées pour cette adresse.")
|
this.showLocationError("Impossible de trouver les coordonnées pour cette adresse.")
|
||||||
}
|
}
|
||||||
@@ -180,13 +220,26 @@ export default class extends Controller {
|
|||||||
|
|
||||||
// Geocode address quietly (no user feedback, for auto-geocoding)
|
// Geocode address quietly (no user feedback, for auto-geocoding)
|
||||||
async geocodeAddressQuiet(address) {
|
async geocodeAddressQuiet(address) {
|
||||||
|
// Skip if address is too short or invalid
|
||||||
|
if (!address || address.length < 5) {
|
||||||
|
this.clearCoordinates()
|
||||||
|
this.clearMapLinks()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.performGeocode(address)
|
const result = await this.performGeocode(address)
|
||||||
|
|
||||||
if (result) {
|
if (result && result.lat && result.lng) {
|
||||||
this.latitudeTarget.value = result.lat
|
this.latitudeTarget.value = result.lat
|
||||||
this.longitudeTarget.value = result.lng
|
this.longitudeTarget.value = result.lng
|
||||||
this.updateMapLinks()
|
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 {
|
} else {
|
||||||
// If auto-geocoding fails, show a subtle warning
|
// If auto-geocoding fails, show a subtle warning
|
||||||
this.showGeocodingWarning(address)
|
this.showGeocodingWarning(address)
|
||||||
@@ -197,17 +250,92 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the actual geocoding request
|
// Perform the actual geocoding request with fallback strategies
|
||||||
async performGeocode(address) {
|
async performGeocode(address) {
|
||||||
const encodedAddress = encodeURIComponent(address)
|
// Rate limiting: ensure at least 1 second between requests
|
||||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodedAddress}&format=json&limit=1`)
|
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()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
const result = data[0]
|
const result = data[0]
|
||||||
return {
|
return {
|
||||||
lat: parseFloat(result.lat).toFixed(6),
|
lat: parseFloat(result.lat).toFixed(6),
|
||||||
lng: parseFloat(result.lon).toFixed(6)
|
lng: parseFloat(result.lon).toFixed(6),
|
||||||
|
display_name: result.display_name,
|
||||||
|
accuracy: address === result.display_name ? 'exact' : 'approximate'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,16 +364,16 @@ export default class extends Controller {
|
|||||||
const encodedAddress = encodeURIComponent(address)
|
const encodedAddress = encodeURIComponent(address)
|
||||||
|
|
||||||
const providers = {
|
const providers = {
|
||||||
|
google: {
|
||||||
|
name: "Google Maps",
|
||||||
|
url: `https://www.google.com/maps/search/${encodedAddress},16z`,
|
||||||
|
icon: "🔍"
|
||||||
|
},
|
||||||
openstreetmap: {
|
openstreetmap: {
|
||||||
name: "OpenStreetMap",
|
name: "OpenStreetMap",
|
||||||
url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`,
|
url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`,
|
||||||
icon: "🗺️"
|
icon: "🗺️"
|
||||||
},
|
},
|
||||||
google: {
|
|
||||||
name: "Google Maps",
|
|
||||||
url: `https://www.google.com/maps/search/${encodedAddress}/@${lat},${lng},16z`,
|
|
||||||
icon: "🔍"
|
|
||||||
},
|
|
||||||
apple: {
|
apple: {
|
||||||
name: "Apple Plans",
|
name: "Apple Plans",
|
||||||
url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`,
|
url: `https://maps.apple.com/?address=${encodedAddress}&ll=${lat},${lng}`,
|
||||||
@@ -285,10 +413,84 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = '<div class="w-3 h-3 mr-1 border border-white border-t-transparent rounded-full animate-spin"></div>'
|
||||||
|
}
|
||||||
|
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 = '<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>'
|
||||||
|
// 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 = '<div class="w-3 h-3 mr-1 border border-purple-700 border-t-transparent rounded-full animate-spin"></div>'
|
||||||
|
}
|
||||||
|
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 = '<i data-lucide="map" class="w-3 h-3 mr-1"></i>'
|
||||||
|
// Re-initialize Lucide icons
|
||||||
|
if (window.lucide) {
|
||||||
|
window.lucide.createIcons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.hasPreviewLocationTextTarget) {
|
||||||
|
this.previewLocationTextTarget.textContent = 'Prévisualiser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
showLocationLoading() {
|
showLocationLoading() {
|
||||||
this.hideAllLocationMessages()
|
this.hideAllLocationMessages()
|
||||||
this.showMessage("location-loading", "Géolocalisation en cours...", "info")
|
this.showMessage("location-loading", "Géolocalisation en cours...", "loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide loading state
|
// Hide loading state
|
||||||
@@ -318,37 +520,100 @@ export default class extends Controller {
|
|||||||
setTimeout(() => this.hideMessage("geocoding-warning"), 8000)
|
setTimeout(() => this.hideMessage("geocoding-warning"), 8000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a message with given type
|
// Show info about approximate location
|
||||||
showMessage(id, message, type) {
|
showApproximateLocationInfo(foundLocation) {
|
||||||
const colors = {
|
this.hideMessage("approximate-location-info")
|
||||||
info: "bg-blue-50 border-blue-200 text-blue-800",
|
const message = `Localisation approximative trouvée: ${foundLocation}`
|
||||||
success: "bg-green-50 border-green-200 text-green-800",
|
this.showMessage("approximate-location-info", message, "info")
|
||||||
error: "bg-red-50 border-red-200 text-red-800",
|
setTimeout(() => this.hideMessage("approximate-location-info"), 6000)
|
||||||
warning: "bg-yellow-50 border-yellow-200 text-yellow-800"
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const icons = {
|
// Message template configurations
|
||||||
info: "info",
|
getMessageTemplate(type) {
|
||||||
success: "check-circle",
|
const templates = {
|
||||||
error: "alert-circle",
|
info: {
|
||||||
warning: "alert-triangle"
|
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
|
||||||
|
}
|
||||||
|
|
||||||
const messageHtml = `
|
// Create dynamic message HTML using template
|
||||||
<div id="${id}" class="flex items-center space-x-2 p-3 ${colors[type]} border rounded-lg mb-4">
|
createMessageHTML(id, message, type) {
|
||||||
<i data-lucide="${icons[type]}" class="w-4 h-4 flex-shrink-0"></i>
|
const template = this.getMessageTemplate(type)
|
||||||
<span class="text-sm font-medium">${message}</span>
|
const animationClass = template.animated ? 'animate-spin' : ''
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="${id}" class="flex items-start space-x-3 p-4 ${template.bgColor} ${template.borderColor} border rounded-lg shadow-sm transition-all duration-200 ease-in-out">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i data-lucide="${template.icon}" class="w-5 h-5 ${template.iconColor} ${animationClass}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium ${template.textColor} leading-relaxed">${message}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="this.parentElement.remove()" class="flex-shrink-0 ${template.textColor} hover:opacity-70 transition-opacity">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
|
|
||||||
// Insert after the venue section header
|
// Show a message with given type using template system
|
||||||
const venueSection = this.element.querySelector('h3')
|
showMessage(id, message, type) {
|
||||||
if (venueSection) {
|
// Remove existing message with same ID first
|
||||||
venueSection.insertAdjacentHTML('afterend', messageHtml)
|
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
|
// Re-initialize Lucide icons for the new elements
|
||||||
if (window.lucide) {
|
if (window.lucide) {
|
||||||
window.lucide.createIcons()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,5 +631,6 @@ export default class extends Controller {
|
|||||||
this.hideMessage("location-success")
|
this.hideMessage("location-success")
|
||||||
this.hideMessage("location-error")
|
this.hideMessage("location-error")
|
||||||
this.hideMessage("geocoding-warning")
|
this.hideMessage("geocoding-warning")
|
||||||
|
this.hideMessage("approximate-location-info")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,27 +23,7 @@ class Event < ApplicationRecord
|
|||||||
has_many :orders
|
has_many :orders
|
||||||
|
|
||||||
# === Callbacks ===
|
# === Callbacks ===
|
||||||
before_validation :geocode_address, if: :venue_address_changed?
|
before_validation :geocode_address, if: :should_geocode_address?
|
||||||
|
|
||||||
# === Instance Methods ===
|
|
||||||
|
|
||||||
# Check if coordinates were successfully geocoded or are fallback coordinates
|
|
||||||
def geocoding_successful?
|
|
||||||
return false if latitude.blank? || longitude.blank?
|
|
||||||
|
|
||||||
# Check if coordinates are exactly the fallback coordinates
|
|
||||||
fallback_lat = 46.603354
|
|
||||||
fallback_lng = 1.888334
|
|
||||||
|
|
||||||
!(latitude == fallback_lat && longitude == fallback_lng)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get a user-friendly status message about geocoding
|
|
||||||
def geocoding_status_message
|
|
||||||
return nil if geocoding_successful?
|
|
||||||
|
|
||||||
"Les coordonnées exactes n'ont pas pu être déterminées automatiquement. Une localisation approximative a été utilisée."
|
|
||||||
end
|
|
||||||
|
|
||||||
# Validations for Event attributes
|
# Validations for Event attributes
|
||||||
# Basic information
|
# Basic information
|
||||||
@@ -75,23 +55,79 @@ class Event < ApplicationRecord
|
|||||||
# Scope for published events ordered by start time
|
# Scope for published events ordered by start time
|
||||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||||
|
|
||||||
|
|
||||||
|
# === Instance Methods ===
|
||||||
|
|
||||||
|
# Check if coordinates were successfully geocoded or are fallback coordinates
|
||||||
|
def geocoding_successful?
|
||||||
|
coordinates_look_valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get a user-friendly status message about geocoding
|
||||||
|
def geocoding_status_message
|
||||||
|
return nil if geocoding_successful?
|
||||||
|
|
||||||
|
"Les coordonnées exactes n'ont pas pu être déterminées automatiquement. Une localisation approximative a été utilisée."
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Automatically geocode address to get latitude and longitude
|
# Determine if we should perform server-side geocoding
|
||||||
def geocode_address
|
def should_geocode_address?
|
||||||
return if venue_address.blank?
|
# Don't geocode if address is blank
|
||||||
|
return false if venue_address.blank?
|
||||||
|
|
||||||
# If we already have coordinates and this is an update, try to geocode
|
# Don't geocode if we already have valid coordinates (likely from frontend)
|
||||||
# If it fails, keep the existing coordinates
|
return false if coordinates_look_valid?
|
||||||
|
|
||||||
|
# Only geocode if address changed and we don't have coordinates
|
||||||
|
venue_address_changed?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if the current coordinates look like they were set by frontend geocoding
|
||||||
|
def coordinates_look_valid?
|
||||||
|
return false if latitude.blank? || longitude.blank?
|
||||||
|
|
||||||
|
lat_f = latitude.to_f
|
||||||
|
lng_f = longitude.to_f
|
||||||
|
|
||||||
|
# Basic sanity checks for coordinate ranges
|
||||||
|
return false if lat_f < -90 || lat_f > 90
|
||||||
|
return false if lng_f < -180 || lng_f > 180
|
||||||
|
|
||||||
|
# Check if coordinates are not the default fallback coordinates
|
||||||
|
fallback_lat = 46.603354
|
||||||
|
fallback_lng = 1.888334
|
||||||
|
|
||||||
|
# Check if coordinates are not exactly 0,0 (common invalid default)
|
||||||
|
return false if lat_f == 0.0 && lng_f == 0.0
|
||||||
|
|
||||||
|
# Coordinates are valid if they're not exactly the fallback coordinates
|
||||||
|
!(lat_f == fallback_lat && lng_f == fallback_lng)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Automatically geocode address to get latitude and longitude
|
||||||
|
# This only runs when no valid coordinates are provided (fallback for non-JS users)
|
||||||
|
def geocode_address
|
||||||
|
Rails.logger.info "Running server-side geocoding for '#{venue_address}' (no frontend coordinates provided)"
|
||||||
|
|
||||||
|
# Store original coordinates in case we need to fall back
|
||||||
original_lat = latitude
|
original_lat = latitude
|
||||||
original_lng = longitude
|
original_lng = longitude
|
||||||
|
|
||||||
begin
|
begin
|
||||||
# Use OpenStreetMap Nominatim API for geocoding
|
# Use OpenStreetMap Nominatim API for geocoding
|
||||||
encoded_address = URI.encode_www_form_component(venue_address.strip)
|
encoded_address = URI.encode_www_form_component(venue_address.strip)
|
||||||
uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1")
|
uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1&addressdetails=1")
|
||||||
|
|
||||||
response = Net::HTTP.get_response(uri)
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
http.use_ssl = true
|
||||||
|
|
||||||
|
request = Net::HTTP::Get.new(uri)
|
||||||
|
request['User-Agent'] = 'AperoNight Event Platform/1.0 (https://aperonight.com)'
|
||||||
|
request['Accept'] = 'application/json'
|
||||||
|
|
||||||
|
response = http.request(request)
|
||||||
|
|
||||||
if response.code == "200"
|
if response.code == "200"
|
||||||
data = JSON.parse(response.body)
|
data = JSON.parse(response.body)
|
||||||
@@ -100,7 +136,7 @@ class Event < ApplicationRecord
|
|||||||
result = data.first
|
result = data.first
|
||||||
self.latitude = result["lat"].to_f.round(6)
|
self.latitude = result["lat"].to_f.round(6)
|
||||||
self.longitude = result["lon"].to_f.round(6)
|
self.longitude = result["lon"].to_f.round(6)
|
||||||
Rails.logger.info "Geocoded address '#{venue_address}' to coordinates: #{latitude}, #{longitude}"
|
Rails.logger.info "Server-side geocoded '#{venue_address}' to coordinates: #{latitude}, #{longitude}"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -109,7 +145,7 @@ class Event < ApplicationRecord
|
|||||||
handle_geocoding_failure(original_lat, original_lng)
|
handle_geocoding_failure(original_lat, original_lng)
|
||||||
|
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Geocoding failed for address '#{venue_address}': #{e.message}"
|
Rails.logger.error "Server-side geocoding failed for '#{venue_address}': #{e.message}"
|
||||||
handle_geocoding_failure(original_lat, original_lng)
|
handle_geocoding_failure(original_lat, original_lng)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">Lieu de l'événement</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Lieu de l'événement</h3>
|
||||||
|
|
||||||
|
<!-- Geocoding Messages Container -->
|
||||||
|
<div data-event-form-target="messagesContainer" class="space-y-3 mb-6 empty:mb-0 empty:hidden"></div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
@@ -96,17 +99,28 @@
|
|||||||
<div>
|
<div>
|
||||||
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= form.text_field :venue_address, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris", data: { "event-form-target": "address", action: "input->event-form#addressChanged" } %>
|
<div class="relative">
|
||||||
|
<%= form.text_field :venue_address, class: "w-full px-4 py-2 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Ex: 1 Boulevard Poissonnière, 75002 Paris", data: { "event-form-target": "address", action: "input->event-form#addressChanged" } %>
|
||||||
|
|
||||||
|
<!-- Geocoding Loading Spinner -->
|
||||||
|
<div data-event-form-target="geocodingSpinner" class="absolute right-3 top-1/2 transform -translate-y-1/2 hidden">
|
||||||
|
<div class="w-5 h-5 border-2 border-purple-200 border-t-purple-600 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Location Actions -->
|
<!-- Location Actions -->
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button type="button" data-action="click->event-form#getCurrentLocation" class="inline-flex items-center px-3 py-2 text-xs font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
|
<button type="button" data-action="click->event-form#getCurrentLocation" data-event-form-target="getCurrentLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
<span data-event-form-target="getCurrentLocationIcon">
|
||||||
Ma position
|
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||||
|
</span>
|
||||||
|
<span data-event-form-target="getCurrentLocationText">Ma position</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-action="click->event-form#previewLocation" class="inline-flex items-center px-3 py-2 text-xs font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors">
|
<button type="button" data-action="click->event-form#previewLocation" data-event-form-target="previewLocationBtn" class="inline-flex items-center px-3 py-2 text-xs font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
<span data-event-form-target="previewLocationIcon">
|
||||||
Prévisualiser
|
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
||||||
|
</span>
|
||||||
|
<span data-event-form-target="previewLocationText">Prévisualiser</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +131,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden coordinate fields for form submission -->
|
<!-- Hidden coordinate fields populated by JavaScript geocoding -->
|
||||||
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
||||||
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user