feat: Implement complete event ticketing system with Stripe integration and email confirmations
- Enhanced events index page with improved visual design and better information display - Completely redesigned event show page with modern layout, ticket selection, and checkout functionality - Implemented Stripe payment processing for ticket purchases - Created ticket generation system with PDF tickets and QR codes - Added email confirmation system with ticket attachments - Updated database configuration to use SQLite for easier development setup - Fixed gem dependencies and resolved conflicts - Improved error handling throughout the checkout process - Enhanced Stimulus controller for ticket cart management - Added proper redirect handling for successful and cancelled payments
This commit is contained in:
@@ -38,14 +38,14 @@ export default class extends Controller {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then((response) => {
|
||||
// console.log(this.element.dataset.loginUrlValue); // By default, we does not return anything.
|
||||
// console.log(this.element.dataset.redirectUrlValue); // By default, we does not return anything.
|
||||
|
||||
// By default the response does not include any url.
|
||||
// Redirect to default login page (loginUrlValue)
|
||||
// Redirect to default login page (redirectUrlValue)
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else if (this.element.dataset.loginUrlValue) {
|
||||
window.location.href = this.element.dataset.loginUrlValue;
|
||||
} else if (this.element.dataset.redirectUrlValue) {
|
||||
window.location.href = this.element.dataset.redirectUrlValue;
|
||||
return;
|
||||
}
|
||||
window.location.href = "/";
|
||||
|
||||
@@ -7,11 +7,14 @@ export default class extends Controller {
|
||||
connect() {
|
||||
this.cart = {}
|
||||
this.updateCartDisplay()
|
||||
|
||||
// Check for pending cart in session storage (after login)
|
||||
this.checkForPendingCart()
|
||||
}
|
||||
|
||||
increaseQuantity(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
||||
const max = parseInt(event.currentTarget.dataset.max)
|
||||
const ticketTypeId = event.params.ticketTypeId
|
||||
const max = parseInt(event.params.max)
|
||||
const input = this.quantityTargetFor(ticketTypeId)
|
||||
|
||||
const current = parseInt(input.value) || 0
|
||||
@@ -22,7 +25,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
decreaseQuantity(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
||||
const ticketTypeId = event.params.ticketTypeId
|
||||
const input = this.quantityTargetFor(ticketTypeId)
|
||||
|
||||
const current = parseInt(input.value) || 0
|
||||
@@ -32,6 +35,22 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -59,28 +78,54 @@ export default class extends Controller {
|
||||
totalPrice += (this.cart[ticketTypeId].price * this.cart[ticketTypeId].quantity) / 100
|
||||
}
|
||||
|
||||
this.cartCountTarget.textContent = totalTickets
|
||||
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
|
||||
// Update cart count and total
|
||||
if (this.hasCartCountTarget) {
|
||||
this.cartCountTarget.textContent = totalTickets
|
||||
}
|
||||
|
||||
if (this.hasCartTotalTarget) {
|
||||
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
|
||||
}
|
||||
|
||||
const checkoutBtn = this.checkoutButtonTarget
|
||||
if (totalTickets > 0) {
|
||||
checkoutBtn.disabled = false
|
||||
} else {
|
||||
checkoutBtn.disabled = true
|
||||
// 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) {
|
||||
alert('Please select at least one ticket')
|
||||
alert('Veuillez sélectionner au moins un billet')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = document.body.dataset.userAuthenticated === "true"
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (confirm('Vous devez être connecté pour acheter des billets. Souhaitez-vous vous connecter maintenant ?')) {
|
||||
// Store cart in session storage
|
||||
sessionStorage.setItem('pending_cart', JSON.stringify({
|
||||
eventId: this.eventIdValue,
|
||||
cart: this.cart
|
||||
}))
|
||||
window.location.href = '/auth/sign_in'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create form and submit to checkout
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = `/events/${this.eventIdValue}/checkout`
|
||||
form.style.display = 'none'
|
||||
|
||||
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')
|
||||
@@ -100,6 +145,31 @@ export default class extends Controller {
|
||||
form.submit()
|
||||
}
|
||||
|
||||
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}`)
|
||||
|
||||
Reference in New Issue
Block a user