develop #3
375
app/javascript/controllers/event_form_controller.js
Normal file
375
app/javascript/controllers/event_form_controller.js
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
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
|
||||||
|
const slug = name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||||
|
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||||
|
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||||
|
|
||||||
|
this.slugTarget.value = slug
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,3 +21,6 @@ application.register("header", HeaderController);
|
|||||||
|
|
||||||
import QrCodeController from "./qr_code_controller";
|
import QrCodeController from "./qr_code_controller";
|
||||||
application.register("qr-code", QrCodeController);
|
application.register("qr-code", QrCodeController);
|
||||||
|
|
||||||
|
import EventFormController from "./event_form_controller";
|
||||||
|
application.register("event-form", EventFormController);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Event model representing nightlife events and events
|
# Event model representing nightlife events and events
|
||||||
# Manages event details, location data, and publication state
|
# Manages event details, location data, and publication state
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
class Event < ApplicationRecord
|
class Event < ApplicationRecord
|
||||||
# Define states for Event lifecycle management
|
# Define states for Event lifecycle management
|
||||||
# draft: Initial state when Event is being created
|
# draft: Initial state when Event is being created
|
||||||
@@ -19,6 +22,29 @@ class Event < ApplicationRecord
|
|||||||
has_many :tickets, through: :ticket_types
|
has_many :tickets, through: :ticket_types
|
||||||
has_many :orders
|
has_many :orders
|
||||||
|
|
||||||
|
# === Callbacks ===
|
||||||
|
before_validation :geocode_address, if: :venue_address_changed?
|
||||||
|
|
||||||
|
# === 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
|
||||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||||
@@ -48,4 +74,102 @@ 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) }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Automatically geocode address to get latitude and longitude
|
||||||
|
def geocode_address
|
||||||
|
return if venue_address.blank?
|
||||||
|
|
||||||
|
# If we already have coordinates and this is an update, try to geocode
|
||||||
|
# If it fails, keep the existing coordinates
|
||||||
|
original_lat = latitude
|
||||||
|
original_lng = longitude
|
||||||
|
|
||||||
|
begin
|
||||||
|
# Use OpenStreetMap Nominatim API for geocoding
|
||||||
|
encoded_address = URI.encode_www_form_component(venue_address.strip)
|
||||||
|
uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1")
|
||||||
|
|
||||||
|
response = Net::HTTP.get_response(uri)
|
||||||
|
|
||||||
|
if response.code == '200'
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
|
||||||
|
if data.any?
|
||||||
|
result = data.first
|
||||||
|
self.latitude = result['lat'].to_f.round(6)
|
||||||
|
self.longitude = result['lon'].to_f.round(6)
|
||||||
|
Rails.logger.info "Geocoded address '#{venue_address}' to coordinates: #{latitude}, #{longitude}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If we reach here, geocoding failed
|
||||||
|
handle_geocoding_failure(original_lat, original_lng)
|
||||||
|
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Geocoding failed for address '#{venue_address}': #{e.message}"
|
||||||
|
handle_geocoding_failure(original_lat, original_lng)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle geocoding failure with fallback strategies
|
||||||
|
def handle_geocoding_failure(original_lat, original_lng)
|
||||||
|
# Strategy 1: Keep existing coordinates if this is an update
|
||||||
|
if original_lat.present? && original_lng.present?
|
||||||
|
self.latitude = original_lat
|
||||||
|
self.longitude = original_lng
|
||||||
|
Rails.logger.warn "Geocoding failed for '#{venue_address}', keeping existing coordinates: #{latitude}, #{longitude}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Strategy 2: Try to extract country/city and use approximate coordinates
|
||||||
|
fallback_coordinates = get_fallback_coordinates_from_address
|
||||||
|
if fallback_coordinates
|
||||||
|
self.latitude = fallback_coordinates[:lat]
|
||||||
|
self.longitude = fallback_coordinates[:lng]
|
||||||
|
Rails.logger.warn "Using fallback coordinates for '#{venue_address}': #{latitude}, #{longitude}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Strategy 3: Use default coordinates (center of France) as last resort
|
||||||
|
# This ensures the event can still be created
|
||||||
|
self.latitude = 46.603354 # Center of France
|
||||||
|
self.longitude = 1.888334
|
||||||
|
Rails.logger.warn "Using default coordinates for '#{venue_address}' due to geocoding failure: #{latitude}, #{longitude}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extract country/city from address and return approximate coordinates
|
||||||
|
def get_fallback_coordinates_from_address
|
||||||
|
address_lower = venue_address.downcase
|
||||||
|
|
||||||
|
# Common French cities with approximate coordinates
|
||||||
|
french_cities = {
|
||||||
|
'paris' => { lat: 48.8566, lng: 2.3522 },
|
||||||
|
'lyon' => { lat: 45.7640, lng: 4.8357 },
|
||||||
|
'marseille' => { lat: 43.2965, lng: 5.3698 },
|
||||||
|
'toulouse' => { lat: 43.6047, lng: 1.4442 },
|
||||||
|
'nice' => { lat: 43.7102, lng: 7.2620 },
|
||||||
|
'nantes' => { lat: 47.2184, lng: -1.5536 },
|
||||||
|
'montpellier' => { lat: 43.6110, lng: 3.8767 },
|
||||||
|
'strasbourg' => { lat: 48.5734, lng: 7.7521 },
|
||||||
|
'bordeaux' => { lat: 44.8378, lng: -0.5792 },
|
||||||
|
'lille' => { lat: 50.6292, lng: 3.0573 }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if any known city is mentioned in the address
|
||||||
|
french_cities.each do |city, coords|
|
||||||
|
if address_lower.include?(city)
|
||||||
|
return coords
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for common country indicators
|
||||||
|
if address_lower.include?('france') || address_lower.include?('french')
|
||||||
|
return { lat: 46.603354, lng: 1.888334 } # Center of France
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<%= render 'components/breadcrumb', crumbs: [
|
<%= render 'components/breadcrumb', crumbs: [
|
||||||
{ name: 'Accueil', path: root_path },
|
{ name: 'Accueil', path: root_path },
|
||||||
@@ -55,7 +55,48 @@
|
|||||||
Lieu
|
Lieu
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
|
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
|
||||||
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
|
<p class="text-gray-600 mt-2 mb-4"><%= @event.venue_address %></p>
|
||||||
|
|
||||||
|
<% if @event.latitude.present? && @event.longitude.present? %>
|
||||||
|
<div class="border-t border-gray-200 pt-4">
|
||||||
|
<% if @event.geocoding_status_message %>
|
||||||
|
<div class="mb-3 p-2 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i data-lucide="alert-triangle" class="w-4 h-4 text-yellow-600 mr-2"></i>
|
||||||
|
<p class="text-xs text-yellow-800"><%= @event.geocoding_status_message %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<p class="text-sm font-medium text-gray-700 mb-2">Ouvrir dans :</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<%
|
||||||
|
encoded_address = URI.encode_www_form_component(@event.venue_address)
|
||||||
|
lat = @event.latitude
|
||||||
|
lng = @event.longitude
|
||||||
|
|
||||||
|
map_providers = {
|
||||||
|
"OpenStreetMap" => "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=16/#{lat}/#{lng}",
|
||||||
|
"Google Maps" => "https://www.google.com/maps/search/#{encoded_address}/@#{lat},#{lng},16z",
|
||||||
|
"Apple Plans" => "https://maps.apple.com/?address=#{encoded_address}&ll=#{lat},#{lng}"
|
||||||
|
}
|
||||||
|
|
||||||
|
icons = {
|
||||||
|
"OpenStreetMap" => "🗺️",
|
||||||
|
"Google Maps" => "🔍",
|
||||||
|
"Apple Plans" => "🍎"
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
||||||
|
<% map_providers.each do |name, url| %>
|
||||||
|
<%= link_to 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" do %>
|
||||||
|
<span class="mr-1"><%= icons[name] %></span>
|
||||||
|
<%= name %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-50 rounded-xl p-6">
|
<div class="bg-gray-50 rounded-xl p-6">
|
||||||
|
|||||||
@@ -116,26 +116,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :venue_address, "Adresse", 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" %>
|
||||||
<%= 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" %>
|
<div class="space-y-2">
|
||||||
</div>
|
<%= 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="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<!-- Location Actions -->
|
||||||
<div>
|
<div class="flex flex-wrap gap-2">
|
||||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-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">
|
||||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||||
|
Ma position
|
||||||
|
</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">
|
||||||
|
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
||||||
|
Prévisualiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-500">
|
<!-- Hidden coordinate fields for form submission -->
|
||||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
||||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
||||||
</p>
|
|
||||||
|
<!-- Map Links Container (shown when address is valid) -->
|
||||||
|
<div data-event-form-target="mapLinksContainer" class="empty:hidden bg-gray-50 rounded-lg p-3 border border-gray-200"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if @event.published? && @event.tickets.any? %>
|
<% if @event.published? && @event.tickets.any? %>
|
||||||
|
|||||||
@@ -94,26 +94,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :venue_address, "Adresse", 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" %>
|
||||||
<%= 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" %>
|
<div class="space-y-2">
|
||||||
</div>
|
<%= 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="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<!-- Location Actions -->
|
||||||
<div>
|
<div class="flex flex-wrap gap-2">
|
||||||
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-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">
|
||||||
<%= form.number_field :latitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "48.8566" %>
|
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||||
|
Ma position
|
||||||
|
</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">
|
||||||
|
<i data-lucide="map" class="w-3 h-3 mr-1"></i>
|
||||||
|
Prévisualiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||||
<%= form.number_field :longitude, step: :any, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "2.3522" %>
|
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-500">
|
<!-- Hidden coordinate fields for form submission -->
|
||||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
|
||||||
Utilisez un service comme <a href="https://www.latlong.net/" target="_blank" class="text-purple-600 hover:text-purple-800">latlong.net</a> pour obtenir les coordonnées GPS.
|
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
|
||||||
</p>
|
|
||||||
|
<!-- Map Links Container (shown when address is valid) -->
|
||||||
|
<div data-event-form-target="mapLinksContainer" class="empty:hidden bg-gray-50 rounded-lg p-3 border border-gray-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user