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>
195 lines
9.8 KiB
Plaintext
Executable File
195 lines
9.8 KiB
Plaintext
Executable File
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
|
<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 },
|
|
{ name: 'Événements', path: events_path },
|
|
{ name: @event.name, path: nil }
|
|
] %>
|
|
|
|
<!-- Event main wrapper -->
|
|
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
|
<!-- Event Header with Image -->
|
|
<% if @event.image.present? %>
|
|
<div class="relative h-96">
|
|
<%= image_tag @event.image, class: "w-full h-full object-cover" %>
|
|
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></div>
|
|
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
|
|
<div class="max-w-4xl mx-auto">
|
|
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2 text-center md:text-left"><%= @event.name %></h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8">
|
|
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
|
|
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
|
<div class="flex items-center">
|
|
<i data-lucide="map-pin" class="w-5 h-5 mr-2 text-purple-200"></i>
|
|
<span><%= @event.venue_name %></span>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<i data-lucide="clock" class="w-5 h-5 mr-2 text-purple-200"></i>
|
|
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Event Content -->
|
|
<div class="p-6 md:p-8">
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Left Column: Event Details -->
|
|
<div class="lg:col-span-2">
|
|
<div class="mb-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-4">Description</h2>
|
|
<div class="prose max-w-none text-gray-700">
|
|
<p class="text-lg leading-relaxed"><%= @event.description %></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
<div class="bg-gray-50 rounded-xl p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<i data-lucide="map-pin" class="w-5 h-5 mr-2 text-purple-600"></i>
|
|
Lieu
|
|
</h3>
|
|
<p class="text-gray-700 font-medium"><%= @event.venue_name %></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">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<i data-lucide="clock" class="w-5 h-5 mr-2 text-purple-600"></i>
|
|
Date & Heure
|
|
</h3>
|
|
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
|
|
<p class="text-gray-600 mt-1">À <%= @event.start_time.strftime("%H:%M") %></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-8 bg-gray-50 rounded-xl p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
|
|
<div class="flex items-center">
|
|
<div class="w-12 h-12 rounded-full bg-purple-500 flex items-center justify-center text-white font-bold">
|
|
<%= @event.user.email.first.upcase %>
|
|
</div>
|
|
<div class="ml-4">
|
|
<% if @event.user.first_name.present? && @event.user.last_name.present? %>
|
|
<p class="font-medium text-gray-900"><%= @event.user.first_name %> <%= @event.user.last_name %></p>
|
|
<% else %>
|
|
<p class="font-medium text-gray-900"><%= @event.user.email.split("@").first %></p>
|
|
<% end %>
|
|
<% if @event.user.company_name.present? %>
|
|
<p class="text-sm text-gray-500"><%= @event.user.company_name %></p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Ticket Selection -->
|
|
<div class="lg:col-span-1">
|
|
<%= form_with url: event_order_new_path(@event.slug, @event.id), method: :get, id: "checkout_form", local: true, data: {
|
|
controller: "ticket-selection",
|
|
ticket_selection_target: "form",
|
|
ticket_selection_event_slug_value: @event.slug,
|
|
ticket_selection_event_id_value: @event.id
|
|
} do |form| %>
|
|
|
|
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
|
<div class="flex justify-center sm:justify-start mb-6">
|
|
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
|
|
</div>
|
|
|
|
<div class="">
|
|
<% if @event.ticket_types.any? %>
|
|
<div class="space-y-4 mb-6">
|
|
<% @event.ticket_types.each do |ticket_type| %>
|
|
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
|
|
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
|
|
|
|
<%= render "components/ticket_card", {
|
|
id: ticket_type.id,
|
|
name: ticket_type.name,
|
|
description: ticket_type.description,
|
|
price_cents: ticket_type.price_cents,
|
|
quantity: ticket_type.quantity,
|
|
sold_out: sold_out,
|
|
remaining: remaining,
|
|
} %>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<div class="text-center py-8">
|
|
<i data-lucide="ticket" class="w-12 h-12 mx-auto text-gray-400"></i>
|
|
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
|
|
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore disponibles ou sont épuisés.</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Cart Summary -->
|
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<span class="text-gray-600">Quantité :</span>
|
|
<span class="font-medium" data-ticket-selection-target="totalQuantity">0</span>
|
|
</div>
|
|
<div class="flex justify-between items-center mb-4">
|
|
<span class="text-gray-600">Montant total :</span>
|
|
<span class="text-xl font-bold text-purple-700" data-ticket-selection-target="totalAmount">€0.00</span>
|
|
</div>
|
|
<%= form.button "Procéder au paiement", type: "submit",
|
|
data: { ticket_selection_target: "checkoutButton" },
|
|
class: "w-full btn btn-primary py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center opacity-50 cursor-not-allowed" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|