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 %>
- <%= image_tag event.event_image_variant(:small), alt: event.name, class: "w-full h-full object-cover" if event.image.attached? %> + <% if event.has_image? %> + <% if event.image.attached? %> + <%= image_tag event.event_image_variant(:small), alt: event.name, class: "w-full h-full object-cover" %> + <% else %> + <%= image_tag event.image_url, alt: event.name, class: "w-full h-full object-cover" %> + <% end %> + <% end %>

diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 768511a..426776c 100755 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -22,9 +22,13 @@ <% @events.each do |event| %>
<%= link_to event_path(event.slug, event), class: "block" do %> - <% if event.image.attached? %> + <% if event.has_image? %>
- <%= image_tag event.event_image_variant(:medium), alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% if event.image.attached? %> + <%= image_tag event.event_image_variant(:medium), alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% else %> + <%= image_tag event.image_url, alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% end %> <% if event.featured? %>
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index ad7c236..46c3f15 100755 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -10,9 +10,13 @@
- <% if @event.image.attached? %> + <% if @event.has_image? %>
- <%= image_tag @event.event_image_variant(:large), class: "w-full h-full object-cover" %> + <% if @event.image.attached? %> + <%= image_tag @event.event_image_variant(:large), class: "w-full h-full object-cover" %> + <% else %> + <%= image_tag @event.image_url, class: "w-full h-full object-cover", alt: @event.name %> + <% end %>
diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index f663a9a..fb0bb7e 100755 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -89,8 +89,12 @@
- <% if event.image.attached? %> - <%= image_tag event.event_image_variant(:medium), alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% if event.has_image? %> + <% if event.image.attached? %> + <%= image_tag event.event_image_variant(:medium), alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% else %> + <%= image_tag event.image_url, alt: event.name, class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" %> + <% end %> <% else %>
diff --git a/app/views/promoter/events/edit.html.erb b/app/views/promoter/events/edit.html.erb index 58ba4c6..037fccd 100644 --- a/app/views/promoter/events/edit.html.erb +++ b/app/views/promoter/events/edit.html.erb @@ -68,16 +68,37 @@
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %> -
+ + +
+
+ +
+
+ + +
<% if @event.image.attached? %>
<%= image_tag @event.image.variant(resize_to_limit: [400, 225]), class: "w-full h-48 object-cover rounded-lg border border-gray-200" %>
-
+
+ Image actuelle +
<% end %> @@ -93,12 +114,57 @@
- + + + @@ -235,4 +301,27 @@
<% end %>
-
\ No newline at end of file +
+ + \ No newline at end of file diff --git a/app/views/promoter/events/new.html.erb b/app/views/promoter/events/new.html.erb index 7d6ef4d..20eb59d 100644 --- a/app/views/promoter/events/new.html.erb +++ b/app/views/promoter/events/new.html.erb @@ -61,13 +61,31 @@
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %> -
+ + +
+
+ +
+
+ + +
<% if @event.image.attached? %>
<%= image_tag @event.image.variant(resize_to_limit: [400, 225]), class: "w-full h-48 object-cover rounded-lg border border-gray-200" %>
-
@@ -83,10 +101,43 @@
- + + + + + diff --git a/app/views/promoter/events/show.html.erb b/app/views/promoter/events/show.html.erb index 6596c20..f4b9711 100644 --- a/app/views/promoter/events/show.html.erb +++ b/app/views/promoter/events/show.html.erb @@ -174,9 +174,13 @@
- <% if @event.image.attached? %> + <% if @event.has_image? %>
- <%= image_tag @event.event_image_variant(:large), alt: @event.name, class: "w-full h-full object-cover" %> + <% if @event.image.attached? %> + <%= image_tag @event.event_image_variant(:large), alt: @event.name, class: "w-full h-full object-cover" %> + <% else %> + <%= image_tag @event.image_url, alt: @event.name, class: "w-full h-full object-cover" %> + <% end %>
<% end %> diff --git a/db/schema.rb b/db/schema.rb index 5cc56c2..7e08fec 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -56,6 +56,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_29_222616) do t.boolean "allow_booking_during_event", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "image_url" t.index ["featured"], name: "index_events_on_featured" t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude" t.index ["state"], name: "index_events_on_state" diff --git a/db/seeds.rb b/db/seeds.rb index 09af41d..6975d87 100755 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -44,7 +44,7 @@ events_data = [ start_time: 1.day.from_now, end_time: 1.day.from_now + 6.hours, featured: true, - image: "https://fastly.picsum.photos/id/407/300/200.jpg?hmac=9EhoXMZ1QdwJue90vzxcjBg2YzsZsAWCjJ7oxOhtcU0", + image_url: "https://fastly.picsum.photos/id/407/300/200.jpg?hmac=9EhoXMZ1QdwJue90vzxcjBg2YzsZsAWCjJ7oxOhtcU0", user: users.first }, { @@ -58,7 +58,7 @@ events_data = [ start_time: 3.days.from_now, end_time: 3.days.from_now + 4.hours, featured: true, - image: "https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image_url: "https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", user: users.second }, { @@ -72,7 +72,7 @@ events_data = [ start_time: 1.week.from_now, end_time: 1.week.from_now + 8.hours, featured: false, - image: "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image_url: "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", user: users.third } ] @@ -147,7 +147,7 @@ belle_epoque_event = Event.find_or_create_by!(name: "LA BELLE ÉPOQUE PAR SISLEY e.start_time = 3.days.from_now e.end_time = 3.days.from_now + 8.hours e.featured = false - e.image = "https://data.bizouk.com/cache1/events/images/10/78/87/b801a9a43266b4cc54bdda73bf34eec8_700_800_auto_97.jpg" + e.image_url = "https://data.bizouk.com/cache1/events/images/10/78/87/b801a9a43266b4cc54bdda73bf34eec8_700_800_auto_97.jpg" e.user = promoter e.allow_booking_during_event = true end @@ -201,7 +201,7 @@ konpa_event = Event.find_or_create_by!(name: "Konpa With Bev - Cours De Konpa Go e.start_time = Time.parse("2025-10-03 19:00:00") e.end_time = Time.parse("2025-10-03 23:00:00") e.featured = false - e.image = "https://data.bizouk.com/cache1/events/images/10/79/61/081f38b583ac651f3a0930c5d8f13458_800_600_auto_97.png" + e.image_url = "https://data.bizouk.com/cache1/events/images/10/79/61/081f38b583ac651f3a0930c5d8f13458_800_600_auto_97.png" e.user = promoter e.state = :published end @@ -216,7 +216,7 @@ caribbean_groove_event = Event.find_or_create_by!(name: "La Plus Grosse Soirée e.start_time = Time.parse("2025-10-03 23:00:00") e.end_time = Time.parse("2025-10-04 05:00:00") e.featured = false - e.image = "https://data.bizouk.com/cache1/events/images/10/83/15/fa5d43f0b1998f691181cfda8fe35213_800_600_auto_97.png" + e.image_url = "https://data.bizouk.com/cache1/events/images/10/83/15/fa5d43f0b1998f691181cfda8fe35213_800_600_auto_97.png" e.user = promoter e.state = :published end @@ -231,7 +231,7 @@ belle_epoque_october_event = Event.find_or_create_by!(name: "LA BELLE ÉPOQUE PA e.start_time = Time.parse("2025-10-04 18:00:00") e.end_time = Time.parse("2025-10-05 02:00:00") e.featured = false - e.image = "https://data.bizouk.com/cache1/events/images/10/92/72/351e61b55603a4d142b43486216457c1_800_600_auto_97.jpg" + e.image_url = "https://data.bizouk.com/cache1/events/images/10/92/72/351e61b55603a4d142b43486216457c1_800_600_auto_97.jpg" e.user = promoter e.state = :published e.allow_booking_during_event = true