Files
aperonight/app/javascript/controllers/ticket_cart_controller.js

334 lines
10 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}