feat: Complete hybrid image upload system with URL compatibility #8

Open
kbe wants to merge 7 commits from feat/image-upload into develop
12 changed files with 316 additions and 40 deletions
Showing only changes of commit ef3f05661e - Show all commits

View File

@@ -687,8 +687,8 @@ export default class extends Controller {
// Show preview // Show preview
const reader = new FileReader() const reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
const previewContainer = document.getElementById('image-preview') const previewContainer = document.getElementById('upload-preview')
const previewImg = document.getElementById('preview-img') const previewImg = document.getElementById('upload-preview-img')
if (previewContainer && previewImg) { if (previewContainer && previewImg) {
previewImg.src = e.target.result previewImg.src = e.target.result
@@ -697,4 +697,54 @@ export default class extends Controller {
} }
reader.readAsDataURL(file) 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
}
}
} }

View File

@@ -24,6 +24,9 @@ class Event < ApplicationRecord
has_many :promotion_codes has_many :promotion_codes
has_one_attached :image has_one_attached :image
# === Virtual attribute for backward compatibility with image URLs ===
attr_accessor :image_url
# === Callbacks === # === Callbacks ===
before_validation :geocode_address, if: :should_geocode_address? 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 :slug, presence: true, length: { minimum: 3, maximum: 100 }
validates :description, presence: true, length: { minimum: 10, maximum: 2000 } validates :description, presence: true, length: { minimum: 10, maximum: 2000 }
validates :state, presence: true, inclusion: { in: states.keys } validates :state, presence: true, inclusion: { in: states.keys }
# Image validation - handles both attachments and URLs
validate :image_format, if: -> { image.attached? } validate :image_format, if: -> { image.attached? }
validate :image_size, if: -> { image.attached? } validate :image_size, if: -> { image.attached? }
validate :image_url_format, if: -> { image_url.present? && !image.attached? }
# Venue information # Venue information
validates :venue_name, presence: true, length: { maximum: 100 } validates :venue_name, presence: true, length: { maximum: 100 }
@@ -61,8 +67,9 @@ class Event < ApplicationRecord
# === Instance Methods === # === 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) def event_image_variant(size = :medium)
if image.attached?
case size case size
when :large when :large
image.variant(resize_to_limit: [1200, 630]) image.variant(resize_to_limit: [1200, 630])
@@ -73,6 +80,24 @@ class Event < ApplicationRecord
else else
image image
end 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 end
# Check if coordinates were successfully geocoded or are fallback coordinates # Check if coordinates were successfully geocoded or are fallback coordinates
@@ -167,6 +192,15 @@ class Event < ApplicationRecord
end end
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 private
# Determine if we should perform server-side geocoding # Determine if we should perform server-side geocoding

View File

@@ -194,8 +194,14 @@ class Order < ApplicationRecord
# Prevent duplicate promotion codes on the same order # Prevent duplicate promotion codes on the same order
def no_duplicate_promotion_codes def no_duplicate_promotion_codes
promotion_code_ids = promotion_codes.map(&:id) return if promotion_codes.empty?
if promotion_code_ids.size != promotion_code_ids.uniq.size
# 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") errors.add(:promotion_codes, "ne peuvent pas contenir de codes en double")
end end
end end

View File

@@ -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 %> <%= 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 %>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0"> <div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
<%= 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 %>
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200"> <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 group-hover:text-purple-600 dark:group-hover:text-purple-400 transition-colors duration-200">

View File

@@ -22,9 +22,13 @@
<% @events.each do |event| %> <% @events.each do |event| %>
<article class="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden transform hover:-translate-y-1"> <article class="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden transform hover:-translate-y-1">
<%= link_to event_path(event.slug, event), class: "block" do %> <%= link_to event_path(event.slug, event), class: "block" do %>
<% if event.image.attached? %> <% if event.has_image? %>
<div class="relative overflow-hidden aspect-[4/3]"> <div class="relative overflow-hidden aspect-[4/3]">
<% 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" %> <%= 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 %>
<!-- Event featured badge --> <!-- Event featured badge -->
<% if event.featured? %> <% if event.featured? %>
<div class="absolute top-4 left-4"> <div class="absolute top-4 left-4">

View File

@@ -10,9 +10,13 @@
<!-- Event main wrapper --> <!-- Event main wrapper -->
<div class="bg-white rounded-2xl shadow-xl overflow-hidden"> <div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<!-- Event Header with Image --> <!-- Event Header with Image -->
<% if @event.image.attached? %> <% if @event.has_image? %>
<div class="relative h-96"> <div class="relative h-96">
<% if @event.image.attached? %>
<%= image_tag @event.event_image_variant(:large), class: "w-full h-full object-cover" %> <%= 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 %>
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></div> <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="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">

View File

@@ -89,8 +89,12 @@
<div class="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden"> <div class="bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 overflow-hidden">
<!-- Event Image --> <!-- Event Image -->
<div class="relative overflow-hidden aspect-[4/3]"> <div class="relative overflow-hidden aspect-[4/3]">
<% if event.has_image? %>
<% if event.image.attached? %> <% 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" %> <%= 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 %> <% else %>
<div class="w-full h-full bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"> <div class="w-full h-full bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center">
<i data-lucide="calendar" class="w-16 h-16 text-white"></i> <i data-lucide="calendar" class="w-16 h-16 text-white"></i>

View File

@@ -68,16 +68,37 @@
<div class="mt-6"> <div class="mt-6">
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %> <%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div class="space-y-4">
<!-- 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 --> <!-- Current image preview -->
<% if @event.image.attached? %> <% if @event.image.attached? %>
<div class="relative"> <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" %> <%= 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"> <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"> <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> <i data-lucide="trash-2" class="w-4 h-4"></i>
</button> </button>
</div> </div>
<div class="absolute bottom-2 left-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-xs">
Image actuelle
</div>
</div> </div>
<% end %> <% end %>
@@ -93,12 +114,57 @@
</div> </div>
<!-- Image preview container --> <!-- Image preview container -->
<div id="image-preview" class="hidden"> <div id="upload-preview" class="hidden">
<div class="relative"> <div class="relative">
<img id="preview-img" src="" alt="Preview" class="w-full h-48 object-cover rounded-lg border border-gray-200"> <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('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"> <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> <i data-lucide="x" class="w-4 h-4"></i>
</button> </button>
<div class="absolute bottom-2 left-2 bg-purple-600 text-white px-2 py-1 rounded text-xs">
Nouvelle image
</div>
</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 URL 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 class="absolute bottom-2 left-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-xs">
URL actuelle
</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", value: @event.image_url, 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)
<% if @event.image_url.present? %>
<br>Laissez vide pour conserver l'URL actuelle
<% end %>
</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 class="absolute bottom-2 left-2 bg-purple-600 text-white px-2 py-1 rounded text-xs">
Nouvelle URL
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -236,3 +302,26 @@
<% end %> <% end %>
</div> </div>
</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>

View File

@@ -61,13 +61,31 @@
<div class="mt-6"> <div class="mt-6">
<%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %> <%= form.label :image, "Image de couverture", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div class="space-y-4">
<!-- 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) --> <!-- Current image preview (for edit mode) -->
<% if @event.image.attached? %> <% if @event.image.attached? %>
<div class="relative"> <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" %> <%= 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"> <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"> <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> <i data-lucide="trash-2" class="w-4 h-4"></i>
</button> </button>
</div> </div>
@@ -83,10 +101,43 @@
</div> </div>
<!-- Image preview container --> <!-- Image preview container -->
<div id="image-preview" class="hidden"> <div id="upload-preview" class="hidden">
<div class="relative"> <div class="relative">
<img id="preview-img" src="" alt="Preview" class="w-full h-48 object-cover rounded-lg border border-gray-200"> <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('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"> <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> <i data-lucide="x" class="w-4 h-4"></i>
</button> </button>
</div> </div>
@@ -192,3 +243,26 @@
<% end %> <% end %>
</div> </div>
</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>

View File

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

1
db/schema.rb generated
View File

@@ -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.boolean "allow_booking_during_event", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "image_url"
t.index ["featured"], name: "index_events_on_featured" t.index ["featured"], name: "index_events_on_featured"
t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude" t.index ["latitude", "longitude"], name: "index_events_on_latitude_and_longitude"
t.index ["state"], name: "index_events_on_state" t.index ["state"], name: "index_events_on_state"

View File

@@ -44,7 +44,7 @@ events_data = [
start_time: 1.day.from_now, start_time: 1.day.from_now,
end_time: 1.day.from_now + 6.hours, end_time: 1.day.from_now + 6.hours,
featured: true, 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 user: users.first
}, },
{ {
@@ -58,7 +58,7 @@ events_data = [
start_time: 3.days.from_now, start_time: 3.days.from_now,
end_time: 3.days.from_now + 4.hours, end_time: 3.days.from_now + 4.hours,
featured: true, 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 user: users.second
}, },
{ {
@@ -72,7 +72,7 @@ events_data = [
start_time: 1.week.from_now, start_time: 1.week.from_now,
end_time: 1.week.from_now + 8.hours, end_time: 1.week.from_now + 8.hours,
featured: false, 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 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.start_time = 3.days.from_now
e.end_time = 3.days.from_now + 8.hours e.end_time = 3.days.from_now + 8.hours
e.featured = false 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.user = promoter
e.allow_booking_during_event = true e.allow_booking_during_event = true
end 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.start_time = Time.parse("2025-10-03 19:00:00")
e.end_time = Time.parse("2025-10-03 23:00:00") e.end_time = Time.parse("2025-10-03 23:00:00")
e.featured = false 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.user = promoter
e.state = :published e.state = :published
end 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.start_time = Time.parse("2025-10-03 23:00:00")
e.end_time = Time.parse("2025-10-04 05:00:00") e.end_time = Time.parse("2025-10-04 05:00:00")
e.featured = false 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.user = promoter
e.state = :published e.state = :published
end 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.start_time = Time.parse("2025-10-04 18:00:00")
e.end_time = Time.parse("2025-10-05 02:00:00") e.end_time = Time.parse("2025-10-05 02:00:00")
e.featured = false 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.user = promoter
e.state = :published e.state = :published
e.allow_booking_during_event = true e.allow_booking_during_event = true