feat: Implement event image upload system for promoters

- Add Active Storage migrations for file attachments
- Update Event model to handle image uploads with validation
- Replace image URL fields with file upload in forms
- Add client-side image preview with validation
- Update all views to display uploaded images properly
- Fix JSON serialization to prevent stack overflow in API
- Add custom image validation methods for format and size
- Include image processing variants for different display sizes
- Fix promotion code test infrastructure and Stripe configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kbe
2025-09-30 00:41:03 +02:00
parent be7b3d5c18
commit 6be8b95ed3
22 changed files with 450 additions and 36 deletions

View File

@@ -67,9 +67,41 @@
</div>
<div class="mt-6">
<%= form.label :image, "Image (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.url_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", placeholder: "https://example.com/image.jpg" %>
<p class="mt-1 text-sm text-gray-500">URL de l'image de couverture de l'événement</p>
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div class="space-y-4">
<!-- Current image preview -->
<% 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="this.closest('div').querySelector('input[type=file]').click()" 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)
<% if @event.image.attached? %>
<br>Laissez vide pour conserver l'image actuelle
<% end %>
</div>
</div>
<!-- Image preview container -->
<div id="image-preview" class="hidden">
<div class="relative">
<img id="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('image-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>

View File

@@ -60,9 +60,38 @@
</div>
<div class="mt-6">
<%= form.label :image, "Image (URL)", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= form.url_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", placeholder: "https://example.com/image.jpg" %>
<p class="mt-1 text-sm text-gray-500">URL de l'image de couverture de l'événement</p>
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div class="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="this.closest('div').previousElementSibling.querySelector('input[type=file]').click()" 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="image-preview" class="hidden">
<div class="relative">
<img id="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('image-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>

View File

@@ -174,9 +174,9 @@
<!-- Main content -->
<div class="lg:col-span-2 space-y-6 lg:space-y-8">
<!-- Event image -->
<% if @event.image.present? %>
<% if @event.image.attached? %>
<div class="aspect-video bg-gray-100 rounded-2xl overflow-hidden">
<img src="<%= @event.image %>" alt="<%= @event.name %>" class="w-full h-full object-cover">
<%= image_tag @event.event_image_variant(:large), alt: @event.name, class: "w-full h-full object-cover" %>
</div>
<% end %>