@@ -1,333 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "quantity" , "cartCount" , "cartTotal" , "checkoutButton" ]
static values = { eventId : String }
connect ( ) {
this . cart = { }
this . updateCartDisplay ( )
// Check for pending cart in session storage (after login)
this . checkForPendingCart ( )
}
increaseQuantity ( event ) {
const ticketTypeId = event . params . ticketTypeId
const max = parseInt ( event . params . max )
const input = this . quantityTargetFor ( ticketTypeId )
if ( ! input ) {
console . error ( ` Could not find input for ticket type ID: ${ ticketTypeId } ` )
return
}
const current = parseInt ( input . value ) || 0
if ( current < max ) {
input . value = current + 1
this . updateCartItem ( ticketTypeId , input )
}
}
decreaseQuantity ( event ) {
const ticketTypeId = event . params . ticketTypeId
const input = this . quantityTargetFor ( ticketTypeId )
if ( ! input ) {
console . error ( ` Could not find input for ticket type ID: ${ ticketTypeId } ` )
return
}
const current = parseInt ( input . value ) || 0
if ( current > 0 ) {
input . value = current - 1
this . updateCartItem ( ticketTypeId , input )
}
}
updateQuantityFromInput ( event ) {
const input = event . target
const ticketTypeId = input . dataset . ticketTypeId
if ( ! ticketTypeId ) {
console . error ( 'Missing ticket type ID on input element' )
return
}
const max = parseInt ( input . max )
const quantity = parseInt ( input . value ) || 0
// Validate input
if ( quantity < 0 ) {
input . value = 0
} else if ( quantity > max ) {
input . value = max
}
this . updateCartItem ( ticketTypeId , input )
}
updateCartItem ( ticketTypeId , input ) {
const name = input . dataset . name
const price = parseInt ( input . dataset . price )
const quantity = parseInt ( input . value ) || 0
if ( quantity > 0 ) {
this . cart [ ticketTypeId ] = {
name : name ,
price : price ,
quantity : quantity
}
} else {
delete this . cart [ ticketTypeId ]
}
this . updateCartDisplay ( )
}
updateCartDisplay ( ) {
let totalTickets = 0
let totalPrice = 0
for ( let ticketTypeId in this . cart ) {
totalTickets += this . cart [ ticketTypeId ] . quantity
totalPrice += ( this . cart [ ticketTypeId ] . price * this . cart [ ticketTypeId ] . quantity ) / 100
}
// Update cart count and total
if ( this . hasCartCountTarget ) {
this . cartCountTarget . textContent = totalTickets
}
if ( this . hasCartTotalTarget ) {
this . cartTotalTarget . textContent = totalPrice . toFixed ( 2 )
}
// Update checkout button state
if ( this . hasCheckoutButtonTarget ) {
const checkoutBtn = this . checkoutButtonTarget
if ( totalTickets > 0 ) {
checkoutBtn . disabled = false
checkoutBtn . classList . remove ( 'opacity-50' , 'cursor-not-allowed' )
} else {
checkoutBtn . disabled = true
checkoutBtn . classList . add ( 'opacity-50' , 'cursor-not-allowed' )
}
}
}
proceedToCheckout ( ) {
if ( Object . keys ( this . cart ) . length === 0 ) {
this . showNotification ( 'Veuillez sélectionner au moins un billet' , 'warning' )
return
}
// Validate cart contents
if ( ! this . validateCartAvailability ( ) ) {
return
}
// Check if user is authenticated
const isAuthenticated = document . body . dataset . userAuthenticated === "true"
if ( ! isAuthenticated ) {
this . showLoginModal ( )
return
}
// Show loading state
this . setCheckoutLoading ( true )
// Create form and submit to checkout
const form = document . createElement ( 'form' )
form . method = 'POST'
form . action = ` /events/ ${ document . body . dataset . eventSlug } . ${ this . eventIdValue } /checkout `
// Add CSRF token
const csrfToken = document . querySelector ( 'meta[name="csrf-token"]' ) . content
const csrfInput = document . createElement ( 'input' )
csrfInput . type = 'hidden'
csrfInput . name = 'authenticity_token'
csrfInput . value = csrfToken
form . appendChild ( csrfInput )
// Add cart data
const cartInput = document . createElement ( 'input' )
cartInput . type = 'hidden'
cartInput . name = 'cart'
cartInput . value = JSON . stringify ( this . cart )
form . appendChild ( cartInput )
document . body . appendChild ( form )
form . submit ( )
}
validateCartAvailability ( ) {
// Check each ticket type availability before checkout
for ( let ticketTypeId in this . cart ) {
const input = this . quantityTargetFor ( ticketTypeId )
if ( input ) {
const maxAvailable = parseInt ( input . max )
const requested = this . cart [ ticketTypeId ] . quantity
if ( requested > maxAvailable ) {
this . showNotification ( ` Seulement ${ maxAvailable } billets disponibles pour ${ this . cart [ ticketTypeId ] . name } ` , 'error' )
// Adjust cart to maximum available
input . value = maxAvailable
this . updateCartItem ( ticketTypeId , input )
return false
}
}
}
return true
}
showLoginModal ( ) {
// Create and show modern login modal
const modal = document . createElement ( 'div' )
modal . className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'
modal . innerHTML = `
<div class="bg-white rounded-2xl p-8 max-w-md mx-4 shadow-2xl">
<div class="text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-purple-100 mb-4">
<svg class="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Connexion requise</h3>
<p class="text-sm text-gray-500 mb-6">Vous devez être connecté pour acheter des billets. Votre panier sera conservé.</p>
<div class="flex flex-col sm:flex-row gap-3">
<button id="login-btn" class="flex-1 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg font-medium transition-colors">
Se connecter
</button>
<button id="cancel-login" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg font-medium transition-colors">
Annuler
</button>
</div>
</div>
</div>
`
document . body . appendChild ( modal )
// Handle login button
modal . querySelector ( '#login-btn' ) . addEventListener ( 'click' , ( ) => {
// Store cart in session storage
sessionStorage . setItem ( 'pending_cart' , JSON . stringify ( {
eventId : this . eventIdValue ,
cart : this . cart
} ) )
window . location . href = '/auth/sign_in'
} )
// Handle cancel button
modal . querySelector ( '#cancel-login' ) . addEventListener ( 'click' , ( ) => {
document . body . removeChild ( modal )
} )
// Handle backdrop click
modal . addEventListener ( 'click' , ( e ) => {
if ( e . target === modal ) {
document . body . removeChild ( modal )
}
} )
}
setCheckoutLoading ( loading ) {
const checkoutBtn = this . checkoutButtonTarget
if ( loading ) {
checkoutBtn . disabled = true
checkoutBtn . innerHTML = `
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Redirection...
`
} else {
checkoutBtn . disabled = false
checkoutBtn . innerHTML = `
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" 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" />
</svg>
Procéder au paiement
`
}
}
showNotification ( message , type = 'info' ) {
// Create toast notification
const toast = document . createElement ( 'div' )
const colors = {
success : 'bg-green-50 text-green-800 border-green-200' ,
error : 'bg-red-50 text-red-800 border-red-200' ,
warning : 'bg-yellow-50 text-yellow-800 border-yellow-200' ,
info : 'bg-blue-50 text-blue-800 border-blue-200'
}
toast . className = ` fixed top-4 right-4 z-50 max-w-sm p-4 border rounded-lg shadow-lg ${ colors [ type ] } transform transition-all duration-300 translate-x-full `
toast . innerHTML = `
<div class="flex items-center">
<div class="flex-1">
<p class="text-sm font-medium"> ${ message } </p>
</div>
<button class="ml-3 text-sm font-medium opacity-70 hover:opacity-100" onclick="this.parentElement.parentElement.remove()">
×
</button>
</div>
`
document . body . appendChild ( toast )
// Animate in
setTimeout ( ( ) => {
toast . classList . remove ( 'translate-x-full' )
} , 10 )
// Auto remove after 5 seconds
setTimeout ( ( ) => {
if ( document . body . contains ( toast ) ) {
toast . classList . add ( 'translate-x-full' )
setTimeout ( ( ) => {
if ( document . body . contains ( toast ) ) {
document . body . removeChild ( toast )
}
} , 300 )
}
} , 5000 )
}
checkForPendingCart ( ) {
const pendingCart = sessionStorage . getItem ( 'pending_cart' )
if ( pendingCart ) {
try {
const cartData = JSON . parse ( pendingCart )
if ( cartData . eventId == this . eventIdValue ) {
this . cart = cartData . cart
this . updateCartDisplay ( )
// Restore quantities in inputs
for ( let ticketTypeId in this . cart ) {
const input = this . quantityTargetFor ( ticketTypeId )
if ( input ) {
input . value = this . cart [ ticketTypeId ] . quantity
}
}
}
sessionStorage . removeItem ( 'pending_cart' )
} catch ( e ) {
console . error ( 'Error restoring pending cart:' , e )
sessionStorage . removeItem ( 'pending_cart' )
}
}
}
// Helper method to find quantity input by ticket type ID
quantityTargetFor ( ticketTypeId ) {
const element = document . querySelector ( ` #quantity_ ${ ticketTypeId } ` )
if ( ! element ) {
console . warn ( ` Could not find quantity input for ticket type ID: ${ ticketTypeId } ` )
}
return element
}
}