develop #3

Merged
kbe merged 227 commits from develop into main 2025-09-16 14:35:23 +00:00
14 changed files with 428 additions and 284 deletions
Showing only changes of commit 56b0a45719 - Show all commits

View File

@@ -1,3 +1,8 @@
// Entry point for the build script in your package.json // Entry point for the build script in your package.json
// This file initializes the Rails application with Turbo and Stimulus controllers
// Import Turbo Rails for SPA-like navigation
import "@hotwired/turbo-rails"; import "@hotwired/turbo-rails";
// Import all Stimulus controllers
import "./controllers"; import "./controllers";

View File

@@ -4,6 +4,7 @@ import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
// Define button styles using class-variance-authority for consistent styling
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
@@ -35,6 +36,7 @@ const buttonVariants = cva(
} }
) )
// Button component that can render as a regular button or as a Slot (for composition)
function Button({ function Button({
className, className,
variant, variant,
@@ -42,6 +44,7 @@ function Button({
asChild = false, asChild = false,
...props ...props
}) { }) {
// Use Slot component if asChild is true, otherwise render as a regular button
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (

View File

@@ -1,12 +1,19 @@
// Main Stimulus application controller
// Initializes the Stimulus framework and makes it available globally
import { Application } from "@hotwired/stimulus"; import { Application } from "@hotwired/stimulus";
// Create and start the Stimulus application
const application = Application.start(); const application = Application.start();
// Configure Stimulus development experience // Configure Stimulus development experience
// Set to false in production to avoid unnecessary logging
application.debug = false; application.debug = false;
// Make Stimulus globally available for debugging purposes
window.Stimulus = application; window.Stimulus = application;
// import Alpine from 'alpinejs'; // Configure Alpine js (commented out as it's not currently used)
// import Alpine from "alpinejs";
// window.Alpine = Alpine; // window.Alpine = Alpine;
// Alpine.start(); // Alpine.start();

View File

@@ -1,31 +1,41 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
// Counter controller for animating number increments
// Used for statistics and numerical displays that animate when they come into view
export default class extends Controller { export default class extends Controller {
// Define controller values with defaults
static values = { static values = {
target: { type: Number, default: 0 }, target: { type: Number, default: 0 }, // Target number to count to
decimal: { type: Boolean, default: false }, decimal: { type: Boolean, default: false }, // Whether to display decimal values
duration: { type: Number, default: 2000 } duration: { type: Number, default: 2000 } // Animation duration in milliseconds
} }
// Set up the intersection observer when the controller connects
connect() { connect() {
// Create an intersection observer to trigger animation when element is visible
this.observer = new IntersectionObserver((entries) => { this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
// Start animation when element is 50% visible
if (entry.isIntersecting) { if (entry.isIntersecting) {
this.animate() this.animate()
// Stop observing after animation starts
this.observer.unobserve(this.element) this.observer.unobserve(this.element)
} }
}) })
}, { threshold: 0.5 }) }, { threshold: 0.5 })
// Begin observing this element
this.observer.observe(this.element) this.observer.observe(this.element)
} }
// Clean up the observer when the controller disconnects
disconnect() { disconnect() {
if (this.observer) { if (this.observer) {
this.observer.disconnect() this.observer.disconnect()
} }
} }
// Animate the counter from 0 to the target value
animate() { animate() {
// Find the target element with data-target-value // Find the target element with data-target-value
const targetElement = this.element.querySelector('.stat-number'); const targetElement = this.element.querySelector('.stat-number');
@@ -37,15 +47,17 @@ export default class extends Controller {
const startValue = 0; const startValue = 0;
const startTime = performance.now(); const startTime = performance.now();
// Update counter function using requestAnimationFrame for smooth animation
const updateCounter = (currentTime) => { const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime; const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / this.durationValue, 1); const progress = Math.min(elapsedTime / this.durationValue, 1);
// Easing function for smooth animation // Easing function for smooth animation (ease-out quartic)
const easeOutQuart = 1 - Math.pow(1 - progress, 4); const easeOutQuart = 1 - Math.pow(1 - progress, 4);
let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart; let currentValue = startValue + (this.targetValue - startValue) * easeOutQuart;
// Format value based on decimal setting
if (this.decimalValue && this.targetValue < 10) { if (this.decimalValue && this.targetValue < 10) {
currentValue = currentValue.toFixed(1); currentValue = currentValue.toFixed(1);
} else { } else {
@@ -55,9 +67,11 @@ export default class extends Controller {
// Update only the text content of the target element // Update only the text content of the target element
targetElement.textContent = currentValue; targetElement.textContent = currentValue;
// Continue animation until complete
if (progress < 1) { if (progress < 1) {
requestAnimationFrame(updateCounter); requestAnimationFrame(updateCounter);
} else { } else {
// Ensure final value is exactly the target
const finalValue = this.decimalValue && this.targetValue < 10 const finalValue = this.decimalValue && this.targetValue < 10
? this.targetValue.toFixed(1) ? this.targetValue.toFixed(1)
: this.targetValue; : this.targetValue;
@@ -65,6 +79,7 @@ export default class extends Controller {
} }
} }
// Start the animation
requestAnimationFrame(updateCounter); requestAnimationFrame(updateCounter);
} }
} }

View File

@@ -1,34 +1,47 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
// Controller for handling animations of featured event cards
// Uses intersection observer to trigger animations when cards come into view
export default class extends Controller { export default class extends Controller {
// Define targets for the controller
static targets = ["card"] static targets = ["card"]
// Define CSS classes that can be used with this controller
static classes = ["visible"] static classes = ["visible"]
// Define configurable values with defaults
static values = { static values = {
threshold: { type: Number, default: 0.1 }, threshold: { type: Number, default: 0.1 }, // Percentage of element visibility needed to trigger animation
rootMargin: { type: String, default: '0px 0px -50px 0px' }, rootMargin: { type: String, default: '0px 0px -50px 0px' }, // Margin around root element for intersection detection
staggerDelay: { type: Number, default: 0.2 } staggerDelay: { type: Number, default: 0.2 } // Delay between card animations in seconds
} }
// Initialize the controller when it connects to the DOM
connect() { connect() {
console.log("FeaturedEventController connected") console.log("FeaturedEventController connected")
this.setupIntersectionObserver() this.setupIntersectionObserver()
this.setupStaggeredAnimations() this.setupStaggeredAnimations()
} }
// Clean up observers when the controller disconnects
disconnect() { disconnect() {
if (this.observer) { if (this.observer) {
this.observer.disconnect() this.observer.disconnect()
} }
} }
// Set up intersection observer to detect when cards come into view
setupIntersectionObserver() { setupIntersectionObserver() {
// Configure observer options
const observerOptions = { const observerOptions = {
threshold: this.thresholdValue, threshold: this.thresholdValue,
rootMargin: this.rootMarginValue rootMargin: this.rootMarginValue
} }
// Create intersection observer
this.observer = new IntersectionObserver((entries) => { this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
// Add visible class when card comes into view
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add('visible') entry.target.classList.add('visible')
} }
@@ -43,6 +56,7 @@ export default class extends Controller {
}) })
} }
// Set up staggered animations for cards with progressive delays
setupStaggeredAnimations() { setupStaggeredAnimations() {
console.log("Setting up staggered animations") console.log("Setting up staggered animations")
console.log("Card targets:", this.cardTargets) console.log("Card targets:", this.cardTargets)

View File

@@ -1,12 +1,16 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
// Controller for handling flash messages
// Automatically dismisses messages after a timeout and handles manual closing
export default class extends Controller { export default class extends Controller {
// Define targets for the controller
static targets = ["message"] static targets = ["message"]
// Initialize the controller when it connects to the DOM
connect() { connect() {
console.log("FlashMessageController mounted", this.element); console.log("FlashMessageController mounted", this.element);
// Initialize Lucide icons for this element // Initialize Lucide icons for this element if available
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined') {
lucide.createIcons({ within: this.element }); lucide.createIcons({ within: this.element });
} }
@@ -17,14 +21,19 @@ export default class extends Controller {
}, 2000) }, 2000)
} }
// Clean up the timeout when the controller disconnects
disconnect() { disconnect() {
if (this.timeout) { if (this.timeout) {
clearTimeout(this.timeout) clearTimeout(this.timeout)
} }
} }
// Close the flash message with a fade-out animation
close() { close() {
// Add opacity transition classes
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300') this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
// Remove element after transition completes
setTimeout(() => { setTimeout(() => {
this.element.remove() this.element.remove()
}, 300) }, 300)

View File

@@ -2,22 +2,25 @@
// Run that command whenever you add a new controller or create them with // Run that command whenever you add a new controller or create them with
// ./bin/rails generate stimulus controllerName // ./bin/rails generate stimulus controllerName
// Import the main Stimulus application
import { application } from "./application" import { application } from "./application"
import LogoutController from "./logout_controller" // Import all controllers
import FlashMessageController from "./flash_message_controller" import LogoutController from "./logout_controller";
application.register("logout", LogoutController);
import CounterController from "./counter_controller" import CounterController from "./counter_controller"
// import FeaturedEventController from "./featured_event_controller" application.register("counter", CounterController);
import FlashMessageController from "./flash_message_controller"
application.register("flash-message", FlashMessageController);
import TicketSelectionController from "./ticket_selection_controller"
application.register("ticket-selection", TicketSelectionController);
// application.register("logout", LogoutController) // Allow logout using js
// application.register("flash-message", FlashMessageController) // Dismiss notification after 5 secondes
// application.register("counter", CounterController) // Simple counter for homepage
// application.register("featured-event", FeaturedEventController) // Featured event controller for homepage
// import ShadcnTestController from "./shadcn_test_controller"
// application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn

View File

@@ -1,15 +1,20 @@
import { Controller } from "@hotwired/stimulus"; import { Controller } from "@hotwired/stimulus";
// Controller for handling user logout functionality
// Sends a DELETE request to the server to sign out the user
export default class extends Controller { export default class extends Controller {
// Define controller values
static values = { static values = {
url: String, url: String, // Optional URL for logout endpoint
}; };
// Log when the controller is mounted
connect() { connect() {
// Display a message when the controller is mounted // Display a message when the controller is mounted
console.log("LogoutController mounted", this.element); console.log("LogoutController mounted", this.element);
} }
// Handle the sign out action
signOut(event) { signOut(event) {
event.preventDefault(); event.preventDefault();
console.log("User clicked on logout button."); console.log("User clicked on logout button.");
@@ -17,7 +22,7 @@ export default class extends Controller {
// Ensure user wants to disconnect with a confirmation request // Ensure user wants to disconnect with a confirmation request
// if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; } // if (this.hasUrlValue && !confirm(this.element.dataset.confirm)) { return; }
// Retrieve the csrf token from header // Retrieve the csrf token from header for security
const csrfToken = document.querySelector("[name='csrf-token']").content; const csrfToken = document.querySelector("[name='csrf-token']").content;
// Define url to redirect user when action is valid // Define url to redirect user when action is valid

View File

@@ -3,15 +3,19 @@ import React from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { Button } from "@/components/button" import { Button } from "@/components/button"
// Connects to data-controller="shadcn-test" // Controller for testing shadcn/ui React components within a Stimulus context
// Renders a React button component to verify the PostCSS and component setup
export default class extends Controller { export default class extends Controller {
// Define targets for the controller
static targets = ["container"] static targets = ["container"]
// Initialize and render the React component when the controller connects
connect() { connect() {
console.log("Shadcn Button Test Controller connected") console.log("Shadcn Button Test Controller connected")
this.renderButton() this.renderButton()
} }
// Render the React button component inside the target container
renderButton() { renderButton() {
const container = this.containerTarget const container = this.containerTarget
const root = createRoot(container) const root = createRoot(container)
@@ -32,6 +36,7 @@ export default class extends Controller {
) )
} }
// Handle button click events
handleClick = () => { handleClick = () => {
alert("✅ Le bouton shadcn fonctionne avec PostCSS !") alert("✅ Le bouton shadcn fonctionne avec PostCSS !")
console.log("Shadcn button clicked - PostCSS compilation successful") console.log("Shadcn button clicked - PostCSS compilation successful")

View File

@@ -0,0 +1,79 @@
import { Controller } from "@hotwired/stimulus"
// Controller for handling ticket selection on the event show page
// Manages quantity inputs, calculates totals, and enables/disables the checkout button
export default class extends Controller {
static targets = ["quantityInput", "totalQuantity", "totalAmount", "checkoutButton"]
// Initialize the controller and update the cart summary
connect() {
this.updateCartSummary()
}
// Increment the quantity for a specific ticket type
increment(event) {
const ticketTypeId = event.currentTarget.dataset.target
const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId)
const value = parseInt(input.value) || 0
const max = parseInt(input.max) || 0
if (value < max) {
input.value = value + 1
this.updateCartSummary()
}
}
// Decrement the quantity for a specific ticket type
decrement(event) {
const ticketTypeId = event.currentTarget.dataset.target
const input = this.quantityInputTargets.find(input => input.dataset.target === ticketTypeId)
const value = parseInt(input.value) || 0
if (value > 0) {
input.value = value - 1
this.updateCartSummary()
}
}
// Update quantity when directly edited in the input field
updateQuantity(event) {
const input = event.currentTarget
let value = parseInt(input.value) || 0
const max = parseInt(input.max) || 0
// Ensure value is within valid range (0 to max available)
if (value < 0) value = 0
if (value > max) value = max
input.value = value
this.updateCartSummary()
}
// Calculate and update the cart summary (total quantity and amount)
updateCartSummary() {
let totalQuantity = 0
let totalAmount = 0
// Sum up quantities and calculate total amount
this.quantityInputTargets.forEach(input => {
const quantity = parseInt(input.value) || 0
const price = parseInt(input.dataset.price) || 0
totalQuantity += quantity
totalAmount += quantity * price
})
// Update the displayed total quantity and amount
this.totalQuantityTarget.textContent = totalQuantity
this.totalAmountTarget.textContent = `${(totalAmount / 100).toFixed(2)}`
// Enable/disable checkout button based on whether any tickets are selected
if (totalQuantity > 0) {
this.checkoutButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed')
this.checkoutButtonTarget.disabled = false
} else {
this.checkoutButtonTarget.classList.add('opacity-50', 'cursor-not-allowed')
this.checkoutButtonTarget.disabled = true
}
}
}

View File

@@ -1,6 +1,9 @@
import { clsx } from "clsx" import { clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
// Utility function for conditionally joining CSS classes
// Combines clsx (for conditional classes) with twMerge (for Tailwind CSS conflicts)
// Usage: cn("class1", "class2", conditionalClass && "class3")
export function cn(...inputs) { export function cn(...inputs) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }

View File

@@ -15,7 +15,7 @@ class Event < ApplicationRecord
# === Relations === # === Relations ===
belongs_to :user belongs_to :user
has_many :ticket_types, dependent: :destroy has_many :ticket_types
has_many :tickets, through: :ticket_types has_many :tickets, through: :ticket_types
# Validations for Event attributes # Validations for Event attributes

View File

@@ -35,18 +35,25 @@
<div class="flex items-center space-x-2 order-1 sm:order-2"> <div class="flex items-center space-x-2 order-1 sm:order-2">
<button type="button" <button type="button"
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200" class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
disabled> data-action="ticket-selection#decrement"
data-target="<%= id %>">
<span class="text-gray-600 font-bold">-</span> <span class="text-gray-600 font-bold">-</span>
</button> </button>
<input type="number" <input type="number"
id="ticket_quantity_<%= id %>"
name="cart[<%= id %>][quantity]"
min="0" min="0"
max="<%= remaining %>" max="<%= remaining %>"
value="0" value="0"
class="w-12 text-center border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1" class="w-12 text-center border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1 ticket-quantity-input"
disabled> data-ticket-selection-target="quantityInput"
data-action="change->ticket-selection#updateQuantity"
data-price="<%= price_cents %>"
data-target="<%= id %>">
<button type="button" <button type="button"
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200" class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
disabled> data-action="ticket-selection#increment"
data-target="<%= id %>">
<span class="text-gray-600 font-bold">+</span> <span class="text-gray-600 font-bold">+</span>
</button> </button>
</div> </div>

View File

@@ -1,13 +1,28 @@
<div <div
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100" class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"
> >
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb"> <nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm"> <ol class="flex items-center space-x-2 text-sm">
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %> <%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<svg
class="w-4 h-4 inline-block mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
</svg>
Accueil
<% end %>
<svg <svg
class="w-4 h-4 inline-block mr-1" class="w-4 h-4 text-gray-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -16,232 +31,109 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" d="M9 5l7 7-7 7"
/> />
</svg> </svg>
Accueil <%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<% end %> Événements
<svg <% end %>
class="w-4 h-4 text-gray-400" <svg
fill="none" class="w-4 h-4 text-gray-400"
stroke="currentColor" fill="none"
viewBox="0 0 24 24" stroke="currentColor"
> viewBox="0 0 24 24"
<path >
stroke-linecap="round" <path
stroke-linejoin="round" stroke-linecap="round"
stroke-width="2" stroke-linejoin="round"
d="M9 5l7 7-7 7" stroke-width="2"
/> d="M9 5l7 7-7 7"
</svg> />
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %> </svg>
Événements <li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
<% end %> <%= @event.name %>
<svg </li>
class="w-4 h-4 text-gray-400" </ol>
fill="none" </nav>
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
<%= @event.name %>
</li>
</ol>
</nav>
<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.present? %> <% if @event.image.present? %>
<div class="relative h-96"> <div class="relative h-96">
<%= image_tag @event.image, class: "w-full h-full object-cover" %> <%= image_tag @event.image, class: "w-full h-full object-cover" %>
<div <div
class=" class="
absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent
" "
></div> ></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">
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2"><%= @event.name %></h1> <h1 class="text-3xl md:text-4xl font-bold text-white mb-2"><%= @event.name %></h1>
</div>
</div> </div>
</div> </div>
</div> <% else %>
<% else %> <div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8">
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8"> <h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1> <div class="flex flex-wrap items-center gap-4 text-white/90">
<div class="flex flex-wrap items-center gap-4 text-white/90">
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
<span><%= @event.venue_name %></span>
</div>
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
</div>
</div>
</div>
<% end %>
<!-- Event Content -->
<div class="p-6 md:p-8">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Event Details -->
<div class="lg:col-span-2">
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Description</h2>
<div class="prose max-w-none text-gray-700">
<p class="text-lg leading-relaxed"><%= @event.description %></p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
Lieu
</h3>
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
</div>
<div class="bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
Date & Heure
</h3>
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
<p class="text-gray-600 mt-1">À
<%= @event.start_time.strftime("%H:%M") %></p>
</div>
</div>
<div class="mb-8 bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
<div class="flex items-center"> <div class="flex items-center">
<div <svg
class=" class="w-5 h-5 mr-2 text-purple-200"
w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-indigo-600 flex fill="none"
items-center justify-center text-white font-bold stroke="currentColor"
" viewBox="0 0 24 24"
> >
<%= @event.user.email.first.upcase %> <path
</div> stroke-linecap="round"
<div class="ml-4"> stroke-linejoin="round"
<% if @event.user.first_name.present? && @event.user.last_name.present? %> stroke-width="2"
<p class="font-medium text-gray-900"><%= @event.user.first_name %> d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
<%= @event.user.last_name %></p> ></path>
<% else %> <path
<p class="font-medium text-gray-900"><%= @event.user.email.split("@").first %></p> stroke-linecap="round"
<% end %> stroke-linejoin="round"
<% if @event.user.company_name.present? %> stroke-width="2"
<p class="text-sm text-gray-500"><%= @event.user.company_name %></p> d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
<% end %> ></path>
</div> </svg>
<span><%= @event.venue_name %></span>
</div>
<div class="flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
</div> </div>
</div> </div>
</div><!-- Left Column: Event Details --> </div>
<% end %>
<!-- Right Column: Ticket Selection --> <!-- Event Content -->
<div class="lg:col-span-1"> <div class="p-6 md:p-8">
<div class=""> <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div <!-- Left Column: Event Details -->
class=" <div class="lg:col-span-2">
bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border <div class="mb-8">
border-purple-100 p-6 shadow-sm <h2 class="text-2xl font-bold text-gray-900 mb-4">Description</h2>
" <div class="prose max-w-none text-gray-700">
> <p class="text-lg leading-relaxed"><%= @event.description %></p>
<div class="flex justify-center sm:justify-start mb-6">
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
</div> </div>
</div>
<div class="space-y-4 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<% if @event.ticket_types.any? %> <div class="bg-gray-50 rounded-xl p-6">
<div class="space-y-4 mb-6"> <h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<% @event.ticket_types.each do |ticket_type| %>
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
<%= render "components/ticket_card",
{
id: ticket_type.id,
name: ticket_type.name,
description: ticket_type.description,
price_cents: ticket_type.price_cents,
quantity: ticket_type.quantity,
sold_out: sold_out,
remaining: remaining,
} %>
<% end %>
</div>
<% else %>
<div class="text-center py-8">
<svg <svg
class="w-12 h-12 mx-auto text-gray-400" class="w-5 h-5 mr-2 text-purple-600"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -250,49 +142,146 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/> ></path>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore
disponibles ou sont épuisés.</p>
</div>
<% end %>
<!-- Cart Summary -->
<div class="border-t border-gray-200 pt-6 mt-6">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">Quantité :</span>
<span class="font-medium">0</span>
</div>
<div class="flex justify-between items-center mb-4">
<span class="text-gray-600">Montant total:</span>
<span class="text-xl font-bold text-purple-700">€0.00</span>
</div>
<button
disabled
class="
w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium
py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none
focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center
justify-center opacity-50 cursor-not-allowed
"
>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/> ></path>
</svg> </svg>
Procéder au paiement Lieu
</button> </h3>
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
</div>
<div class="bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg
class="w-5 h-5 mr-2 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
Date & Heure
</h3>
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
<p class="text-gray-600 mt-1">À
<%= @event.start_time.strftime("%H:%M") %></p>
</div> </div>
</div> </div>
</div>
</div><!-- Right Column: Ticket Selection --> <div class="mb-8 bg-gray-50 rounded-xl p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
<div class="flex items-center">
<div
class="
w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-indigo-600 flex
items-center justify-center text-white font-bold
"
>
<%= @event.user.email.first.upcase %>
</div>
<div class="ml-4">
<% if @event.user.first_name.present? && @event.user.last_name.present? %>
<p class="font-medium text-gray-900"><%= @event.user.first_name %>
<%= @event.user.last_name %></p>
<% else %>
<p class="font-medium text-gray-900"><%= @event.user.email.split("@").first %></p>
<% end %>
<% if @event.user.company_name.present? %>
<p class="text-sm text-gray-500"><%= @event.user.company_name %></p>
<% end %>
</div>
</div>
</div>
</div><!-- Left Column: Event Details -->
<!-- Right Column: Ticket Selection -->
<div class="lg:col-span-1">
<%= form_with url: event_checkout_path(@event.slug, @event), method: :post, id: "checkout_form", local: true, data: { controller: "ticket-selection" } do |form| %>
<div class="">
<div
class="
bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border
border-purple-100 p-6 shadow-sm
"
>
<div class="flex justify-center sm:justify-start mb-6">
<h2 class="text-lg font-bold text-gray-900">Billets disponibles</h2>
</div>
<div class="">
<% if @event.ticket_types.any? %>
<div class="space-y-4 mb-6">
<% @event.ticket_types.each do |ticket_type| %>
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
<%= render "components/ticket_card",
{
id: ticket_type.id,
name: ticket_type.name,
description: ticket_type.description,
price_cents: ticket_type.price_cents,
quantity: ticket_type.quantity,
sold_out: sold_out,
remaining: remaining,
} %>
<% end %>
</div>
<% else %>
<div class="text-center py-8">
<svg
class="w-12 h-12 mx-auto text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"
/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore
disponibles ou sont épuisés.</p>
</div>
<% end %>
<!-- Cart Summary -->
<div class="border-t border-gray-200 pt-6 mt-6">
<div class="flex justify-between items-center mb-2">
<span class="text-gray-600">Quantité :</span>
<span class="font-medium" data-ticket-selection-target="totalQuantity">0</span>
</div>
<div class="flex justify-between items-center mb-4">
<span class="text-gray-600">Montant total :</span>
<span class="text-xl font-bold text-purple-700" data-ticket-selection-target="totalAmount">€0.00</span>
</div>
<%= form.submit "Procéder au paiement",
data: { ticket_selection_target: "checkoutButton" },
class: "w-full bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center opacity-50 cursor-not-allowed",
disabled: true %>
</div>
</div>
</div>
<% end %>
</div><!-- Right Column: Ticket Selection -->
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>