314 lines
10 KiB
JavaScript
Executable File
314 lines
10 KiB
JavaScript
Executable File
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)
|
||
|
||
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)
|
||
|
||
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
|
||
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) {
|
||
return document.querySelector(`#quantity_${ticketTypeId}`)
|
||
}
|
||
}
|