feat: Add comprehensive address-first geolocation system for events

This implementation provides automatic geocoding and map integration:

- **Event Model Enhancements:**
  - Automatic geocoding callback using OpenStreetMap Nominatim API
  - 3-tier fallback system: exact coordinates → city-based → country default
  - Fallback coordinates for major French cities (Paris, Lyon, Marseille, etc.)
  - Robust error handling that prevents event creation failures

- **User-Friendly Event Forms:**
  - Address-first approach - users just enter addresses
  - Hidden coordinate fields (auto-generated behind scenes)
  - Real-time geocoding with 1.5s debounce
  - "Ma position" button for current location with reverse geocoding
  - "Prévisualiser" button to show map links
  - Smart feedback system (loading, success, warnings, errors)

- **Enhanced Event Show Page:**
  - Map provider links (OpenStreetMap, Google Maps, Apple Plans)
  - Warning badges when approximate coordinates are used
  - Address-based URLs for better map integration

- **Comprehensive JavaScript Controller:**
  - Debounced auto-geocoding to minimize API calls
  - Multiple geocoding strategies (manual vs automatic)
  - Promise-based geolocation with proper error handling
  - Dynamic map link generation with address + coordinates

- **Failure Handling:**
  - Events never fail to save due to missing coordinates
  - Fallback to city-based coordinates when exact geocoding fails
  - User-friendly warnings when approximate locations are used
  - Maintains existing coordinates on update failures

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
kbe
2025-09-10 19:36:47 +02:00
parent 748f839346
commit 9b5d8fcf97
6 changed files with 596 additions and 35 deletions

View File

@@ -1,5 +1,5 @@
<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 -->
<%= render 'components/breadcrumb', crumbs: [
{ name: 'Accueil', path: root_path },
@@ -55,7 +55,48 @@
Lieu
</h3>
<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 class="bg-gray-50 rounded-xl p-6">
@@ -150,4 +191,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -116,26 +116,35 @@
</div>
<div>
<%= form.label :venue_address, "Adresse", 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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= 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" %>
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-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" } %>
<!-- Location Actions -->
<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">
<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>
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= 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" %>
</div>
<p class="mt-2 text-sm text-gray-500">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
</p>
</div>
<p class="text-sm text-gray-500">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
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.
</p>
<!-- Hidden coordinate fields for form submission -->
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
<!-- 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>
<% if @event.published? && @event.tickets.any? %>

View File

@@ -94,26 +94,35 @@
</div>
<div>
<%= form.label :venue_address, "Adresse", 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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= form.label :latitude, "Latitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= 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" %>
<%= form.label :venue_address, "Adresse complète", class: "block text-sm font-medium text-gray-700 mb-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" } %>
<!-- Location Actions -->
<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">
<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>
<%= form.label :longitude, "Longitude", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= 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" %>
</div>
<p class="mt-2 text-sm text-gray-500">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
Les coordonnées GPS seront automatiquement calculées à partir de cette adresse.
</p>
</div>
<p class="text-sm text-gray-500">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
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.
</p>
<!-- Hidden coordinate fields for form submission -->
<%= form.hidden_field :latitude, data: { "event-form-target": "latitude" } %>
<%= form.hidden_field :longitude, data: { "event-form-target": "longitude" } %>
<!-- 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>