- Add hybrid image system supporting both file uploads and URL images - Implement Active Storage for file uploads while preserving existing URL functionality - Update Event model with both has_one_attached :image and image_url virtual attribute - Create tabbed interface in event forms for upload/URL selection - Add JavaScript preview functionality for both upload and URL inputs - Fix promotion code validation issue in tests using distinct() to prevent duplicates - Update all views to use hybrid display methods prioritizing uploads over URLs - Update seeds file to use image_url attribute for compatibility - Ensure backward compatibility with existing events using URL images 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
269 lines
14 KiB
Plaintext
269 lines
14 KiB
Plaintext
<% content_for(:title, "Créer un événement") %>
|
|
|
|
<div class="container py-8">
|
|
<div class="max-w-4xl mx-auto">
|
|
<div class="mb-8">
|
|
<div class="flex items-center space-x-4">
|
|
<%= link_to promoter_events_path, class: "text-gray-400 hover:text-gray-600 transition-colors" do %>
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
<% end %>
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">Créer un événement</h1>
|
|
<p class="text-gray-600">Remplissez les informations de votre événement</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<%= form_with model: [:promoter, @event], local: true, class: "space-y-8", data: { controller: "event-form" } do |form| %>
|
|
<% if @event.errors.any? %>
|
|
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<i data-lucide="alert-circle" class="w-5 h-5 text-red-400"></i>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800">
|
|
<%= pluralize(@event.errors.count, "erreur") %> à corriger :
|
|
</h3>
|
|
<div class="mt-2 text-sm text-red-700">
|
|
<ul class="list-disc list-inside space-y-1">
|
|
<% @event.errors.full_messages.each do |message| %>
|
|
<li><%= message %></li>
|
|
<% end %>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Basic Information -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Informations générales</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<%= form.label :name, "Nom de l'événement", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.text_field :name, 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: Soirée d'ouverture", data: { "event-form-target": "name", action: "input->event-form#generateSlug" } %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :slug, "Slug (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.text_field :slug, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "soiree-ouverture", data: { "event-form-target": "slug" } %>
|
|
<p class="mt-1 text-sm text-gray-500">Utilisé dans l'URL de l'événement</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.text_area :description, rows: 4, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "Décrivez votre événement..." %>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
|
|
<!-- Image type selection tabs -->
|
|
<div class="mb-4">
|
|
<div class="border-b border-gray-200">
|
|
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
|
<button type="button" onclick="switchImageTab('upload')" id="upload-tab" class="tab-button active border-purple-500 text-purple-600 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
|
|
<i data-lucide="upload" class="w-4 h-4 inline mr-2"></i>
|
|
Télécharger un fichier
|
|
</button>
|
|
<button type="button" onclick="switchImageTab('url')" id="url-tab" class="tab-button border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm">
|
|
<i data-lucide="link" class="w-4 h-4 inline mr-2"></i>
|
|
Utiliser une URL
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload tab content -->
|
|
<div id="upload-content" class="tab-content space-y-4">
|
|
<!-- Current image preview (for edit mode) -->
|
|
<% if @event.image.attached? %>
|
|
<div class="relative">
|
|
<%= image_tag @event.image.variant(resize_to_limit: [400, 225]), class: "w-full h-48 object-cover rounded-lg border border-gray-200" %>
|
|
<div class="absolute top-2 right-2">
|
|
<button type="button" onclick="document.getElementById('event_image').value = ''; document.getElementById('upload-preview').classList.add('hidden');" class="bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- File upload field -->
|
|
<div class="relative">
|
|
<%= form.file_field :image, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100", accept: "image/png,image/jpeg,image/jpg,image/webp", data: { action: "change->event-form#previewImage" } %>
|
|
<div class="mt-1 text-sm text-gray-500">
|
|
Formats acceptés : PNG, JPG, JPEG, WebP (max 5MB)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image preview container -->
|
|
<div id="upload-preview" class="hidden">
|
|
<div class="relative">
|
|
<img id="upload-preview-img" src="" alt="Preview" class="w-full h-48 object-cover rounded-lg border border-gray-200">
|
|
<button type="button" onclick="document.getElementById('event_image').value = ''; document.getElementById('upload-preview').classList.add('hidden');" class="absolute top-2 right-2 bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URL tab content -->
|
|
<div id="url-content" class="tab-content space-y-4 hidden">
|
|
<!-- Current URL image preview -->
|
|
<% if @event.image_url.present? && !@event.image.attached? %>
|
|
<div class="relative">
|
|
<%= image_tag @event.image_url, class: "w-full h-48 object-cover rounded-lg border border-gray-200", alt: "Current image" %>
|
|
<div class="absolute top-2 right-2">
|
|
<button type="button" onclick="document.getElementById('event_image_url').value = ''; document.getElementById('url-preview').classList.add('hidden');" class="bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- URL input field -->
|
|
<div class="relative">
|
|
<%= form.text_field :image_url, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent", placeholder: "https://example.com/image.jpg", data: { action: "input->event-form#previewImageUrl" } %>
|
|
<div class="mt-1 text-sm text-gray-500">
|
|
Entrez l'URL d'une image (JPG, PNG, GIF, WebP)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- URL preview container -->
|
|
<div id="url-preview" class="hidden">
|
|
<div class="relative">
|
|
<img id="url-preview-img" src="" alt="URL Preview" class="w-full h-48 object-cover rounded-lg border border-gray-200">
|
|
<button type="button" onclick="document.getElementById('event_image_url').value = ''; document.getElementById('url-preview').classList.add('hidden');" class="absolute top-2 right-2 bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date & Time -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Date et heure</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<%= form.label :start_time, "Date et heure de début", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.datetime_local_field :start_time, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :end_time, "Date et heure de fin", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.datetime_local_field :end_time, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Venue Information -->
|
|
<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>
|
|
|
|
<!-- 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>
|
|
<%= form.label :venue_name, "Nom du lieu", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
<%= form.text_field :venue_name, 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: Le Grand Rex" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= 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="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>
|
|
</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>
|
|
|
|
<!-- Hidden coordinate fields populated by JavaScript geocoding -->
|
|
<%= 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>
|
|
|
|
<!-- Options -->
|
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-6">Options</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-center">
|
|
<%= form.check_box :featured, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500" %>
|
|
<%= form.label :featured, "Mettre en avant sur la page d'accueil", class: "ml-2 text-sm text-gray-700" %>
|
|
</div>
|
|
<p class="text-sm text-gray-500">Les événements mis en avant apparaissent en premier sur la page d'accueil.</p>
|
|
|
|
<div class="flex items-start">
|
|
<%= form.check_box :allow_booking_during_event, class: "h-4 w-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 mt-1" %>
|
|
<div class="ml-2">
|
|
<%= form.label :allow_booking_during_event, "Autoriser la réservation pendant l'événement", class: "text-sm text-gray-700 font-medium" %>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
Si activé, les participants pourront acheter des billets même après le début de l'événement.
|
|
Si désactivé, la vente de billets s'arrêtera automatiquement à l'heure de début.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<%= link_to promoter_events_path, class: "text-gray-500 hover:text-gray-700 transition-colors" do %>
|
|
Annuler
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-3">
|
|
<%= form.submit "Créer en brouillon", class: "inline-flex items-center px-6 py-3 bg-gray-600 text-white font-medium rounded-lg hover:bg-gray-700 transition-colors duration-200" %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function switchImageTab(tab) {
|
|
// Hide all tab contents
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.add('hidden');
|
|
});
|
|
|
|
// Remove active class from all tabs
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active', 'border-purple-500', 'text-purple-600');
|
|
button.classList.add('border-transparent', 'text-gray-500');
|
|
});
|
|
|
|
// Show selected tab content
|
|
document.getElementById(tab + '-content').classList.remove('hidden');
|
|
|
|
// Add active class to selected tab
|
|
const activeTab = document.getElementById(tab + '-tab');
|
|
activeTab.classList.add('active', 'border-purple-500', 'text-purple-600');
|
|
activeTab.classList.remove('border-transparent', 'text-gray-500');
|
|
}
|
|
</script>
|