diff --git a/app/javascript/controllers/event_form_controller.js b/app/javascript/controllers/event_form_controller.js index 0b7f431..a9d4248 100644 --- a/app/javascript/controllers/event_form_controller.js +++ b/app/javascript/controllers/event_form_controller.js @@ -687,8 +687,8 @@ export default class extends Controller { // Show preview const reader = new FileReader() reader.onload = (e) => { - const previewContainer = document.getElementById('image-preview') - const previewImg = document.getElementById('preview-img') + const previewContainer = document.getElementById('upload-preview') + const previewImg = document.getElementById('upload-preview-img') if (previewContainer && previewImg) { previewImg.src = e.target.result @@ -697,4 +697,54 @@ export default class extends Controller { } reader.readAsDataURL(file) } + + // Preview image from URL + previewImageUrl(event) { + const url = event.target.value.trim() + const previewContainer = document.getElementById('url-preview') + const previewImg = document.getElementById('url-preview-img') + + if (!url) { + if (previewContainer) { + previewContainer.classList.add('hidden') + } + return + } + + // Basic URL validation + if (!this.isValidImageUrl(url)) { + if (previewContainer) { + previewContainer.classList.add('hidden') + } + return + } + + // Show preview with error handling + if (previewImg) { + previewImg.onload = () => { + if (previewContainer) { + previewContainer.classList.remove('hidden') + } + } + + previewImg.onerror = () => { + if (previewContainer) { + previewContainer.classList.add('hidden') + } + } + + previewImg.src = url + } + } + + // Validate image URL format + isValidImageUrl(url) { + try { + new URL(url) + // Check if it looks like an image URL + return /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url) + } catch { + return false + } + } } diff --git a/app/models/event.rb b/app/models/event.rb index 5038539..7a07beb 100755 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -24,7 +24,10 @@ class Event < ApplicationRecord has_many :promotion_codes has_one_attached :image - + # === Virtual attribute for backward compatibility with image URLs === + attr_accessor :image_url + + # === Callbacks === before_validation :geocode_address, if: :should_geocode_address? @@ -34,8 +37,11 @@ class Event < ApplicationRecord validates :slug, presence: true, length: { minimum: 3, maximum: 100 } validates :description, presence: true, length: { minimum: 10, maximum: 2000 } validates :state, presence: true, inclusion: { in: states.keys } + + # Image validation - handles both attachments and URLs validate :image_format, if: -> { image.attached? } validate :image_size, if: -> { image.attached? } + validate :image_url_format, if: -> { image_url.present? && !image.attached? } # Venue information validates :venue_name, presence: true, length: { maximum: 100 } @@ -61,17 +67,36 @@ class Event < ApplicationRecord # === Instance Methods === - # Get image variants for different display sizes + # Get image for display - handles both uploaded files and URLs def event_image_variant(size = :medium) - case size - when :large - image.variant(resize_to_limit: [1200, 630]) - when :medium - image.variant(resize_to_limit: [800, 450]) - when :small - image.variant(resize_to_limit: [400, 225]) + if image.attached? + case size + when :large + image.variant(resize_to_limit: [1200, 630]) + when :medium + image.variant(resize_to_limit: [800, 450]) + when :small + image.variant(resize_to_limit: [400, 225]) + else + image + end else + # Fallback to URL-based image + image_url.presence + end + end + + # Check if event has any image (uploaded or URL) + def has_image? + image.attached? || image_url.present? + end + + # Get display image source (uploaded or URL) + def display_image + if image.attached? image + else + image_url end end @@ -167,6 +192,15 @@ class Event < ApplicationRecord end end + # Validate image URL format + def image_url_format + return unless image_url.present? + + unless image_url.match?(/\Ahttps?:\/\/.+\.(jpg|jpeg|png|gif|webp)(\?.*)?\z/i) + errors.add(:image_url, "doit être une URL valide vers une image (JPG, PNG, GIF, WebP)") + end + end + private # Determine if we should perform server-side geocoding diff --git a/app/models/order.rb b/app/models/order.rb index 7034161..1124ae0 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -194,8 +194,14 @@ class Order < ApplicationRecord # Prevent duplicate promotion codes on the same order def no_duplicate_promotion_codes - promotion_code_ids = promotion_codes.map(&:id) - if promotion_code_ids.size != promotion_code_ids.uniq.size + return if promotion_codes.empty? + + # Use distinct to avoid association loading issues + unique_codes = promotion_codes.distinct + code_counts = unique_codes.group_by(&:code).transform_values(&:count) + duplicates = code_counts.select { |_, count| count > 1 } + + if duplicates.any? errors.add(:promotion_codes, "ne peuvent pas contenir de codes en double") end end diff --git a/app/views/components/_event_item.html.erb b/app/views/components/_event_item.html.erb index 4fa25f1..8a02f12 100755 --- a/app/views/components/_event_item.html.erb +++ b/app/views/components/_event_item.html.erb @@ -1,7 +1,13 @@ <%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>