feat: Implement complete ticket purchasing flow with new TicketsController
- Create new TicketsController with actions for name collection, creation, and checkout - Add dedicated ticket views (new.html.erb, checkout.html.erb, show.html.erb) - Update ticket_selection_controller.js to handle form submission via AJAX - Add store_cart endpoint in EventsController for session-based cart management - Update routes to support new ticket flow: /tickets/new, /create, /checkout - Fix attribute name consistency across views (title→name, starts_at→start_time) - Add Stripe checkout integration with proper error handling - Remove deprecated collect_names flow in favor of streamlined approach The flow is now: Event selection → AJAX cart storage → Name collection → Checkout → Payment
This commit is contained in:
@@ -18,7 +18,7 @@ export default class extends Controller {
|
||||
// Auto-dismiss after 2 seconds
|
||||
this.timeout = setTimeout(() => {
|
||||
this.close()
|
||||
}, 2000)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// Clean up the timeout when the controller disconnects
|
||||
@@ -32,7 +32,7 @@ export default class extends Controller {
|
||||
close() {
|
||||
// Add opacity transition classes
|
||||
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
||||
|
||||
|
||||
// Remove element after transition completes
|
||||
setTimeout(() => {
|
||||
this.element.remove()
|
||||
|
||||
@@ -4,37 +4,37 @@ import { Controller } from "@hotwired/stimulus"
|
||||
// Manages mobile menu toggle and user dropdown menu
|
||||
export default class extends Controller {
|
||||
static targets = ["mobileMenu", "mobileMenuButton", "userMenu", "userMenuButton"]
|
||||
|
||||
|
||||
connect() {
|
||||
// Initialize menu states
|
||||
this.mobileMenuOpen = false
|
||||
this.userMenuOpen = false
|
||||
|
||||
|
||||
// Add click outside listener for user menu
|
||||
this.clickOutsideHandler = this.handleClickOutside.bind(this)
|
||||
document.addEventListener("click", this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
|
||||
disconnect() {
|
||||
// Clean up event listener
|
||||
document.removeEventListener("click", this.clickOutsideHandler)
|
||||
}
|
||||
|
||||
|
||||
// Toggle mobile menu visibility
|
||||
toggleMobileMenu() {
|
||||
this.mobileMenuOpen = !this.mobileMenuOpen
|
||||
this.mobileMenuTarget.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||
|
||||
|
||||
// Update button icon based on state
|
||||
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||
|
||||
|
||||
if (iconOpen && iconClose) {
|
||||
iconOpen.classList.toggle("hidden", this.mobileMenuOpen)
|
||||
iconClose.classList.toggle("hidden", !this.mobileMenuOpen)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Toggle user dropdown menu visibility
|
||||
toggleUserMenu() {
|
||||
this.userMenuOpen = !this.userMenuOpen
|
||||
@@ -42,32 +42,32 @@ export default class extends Controller {
|
||||
this.userMenuTarget.classList.toggle("hidden", !this.userMenuOpen)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close menus when clicking outside
|
||||
handleClickOutside(event) {
|
||||
// Close user menu if clicked outside
|
||||
if (this.userMenuOpen && this.hasUserMenuTarget &&
|
||||
!this.userMenuTarget.contains(event.target) &&
|
||||
!this.userMenuButtonTarget.contains(event.target)) {
|
||||
if (this.userMenuOpen && this.hasUserMenuTarget &&
|
||||
!this.userMenuTarget.contains(event.target) &&
|
||||
!this.userMenuButtonTarget.contains(event.target)) {
|
||||
this.userMenuOpen = false
|
||||
this.userMenuTarget.classList.add("hidden")
|
||||
}
|
||||
|
||||
|
||||
// Close mobile menu if clicked outside
|
||||
if (this.mobileMenuOpen &&
|
||||
!this.mobileMenuTarget.contains(event.target) &&
|
||||
!this.mobileMenuButtonTarget.contains(event.target)) {
|
||||
if (this.mobileMenuOpen &&
|
||||
!this.mobileMenuTarget.contains(event.target) &&
|
||||
!this.mobileMenuButtonTarget.contains(event.target)) {
|
||||
this.mobileMenuOpen = false
|
||||
this.mobileMenuTarget.classList.add("hidden")
|
||||
|
||||
|
||||
// Update button icon
|
||||
const iconOpen = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="open"]')
|
||||
const iconClose = this.mobileMenuButtonTarget.querySelector('[data-menu-icon="close"]')
|
||||
|
||||
|
||||
if (iconOpen && iconClose) {
|
||||
iconOpen.classList.remove("hidden")
|
||||
iconClose.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class extends Controller {
|
||||
// Log when the controller is mounted
|
||||
connect() {
|
||||
// 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
|
||||
|
||||
@@ -3,11 +3,20 @@ 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"]
|
||||
static targets = ["quantityInput", "totalQuantity", "totalAmount", "checkoutButton", "form"]
|
||||
static values = { eventSlug: String, eventId: String }
|
||||
|
||||
// Initialize the controller and update the cart summary
|
||||
connect() {
|
||||
this.updateCartSummary()
|
||||
this.bindFormSubmission()
|
||||
}
|
||||
|
||||
// Bind form submission to handle cart storage
|
||||
bindFormSubmission() {
|
||||
if (this.hasFormTarget) {
|
||||
this.formTarget.addEventListener('submit', this.submitCart.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the quantity for a specific ticket type
|
||||
@@ -76,4 +85,66 @@ export default class extends Controller {
|
||||
this.checkoutButtonTarget.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission - store cart in session before proceeding
|
||||
async submitCart(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const cartData = this.buildCartData()
|
||||
|
||||
if (Object.keys(cartData).length === 0) {
|
||||
alert('Veuillez sélectionner au moins un billet')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Store cart data in session
|
||||
await this.storeCartInSession(cartData)
|
||||
|
||||
// Redirect to tickets/new page
|
||||
const ticketNewUrl = `/events/${this.eventSlugValue}.${this.eventIdValue}/tickets/new`
|
||||
window.location.href = ticketNewUrl
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error storing cart:', error)
|
||||
alert('Une erreur est survenue. Veuillez réessayer.')
|
||||
}
|
||||
}
|
||||
|
||||
// Build cart data from current form state
|
||||
buildCartData() {
|
||||
const cartData = {}
|
||||
|
||||
this.quantityInputTargets.forEach(input => {
|
||||
const quantity = parseInt(input.value) || 0
|
||||
if (quantity > 0) {
|
||||
const ticketTypeId = input.dataset.target
|
||||
cartData[ticketTypeId] = {
|
||||
quantity: quantity
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cartData
|
||||
}
|
||||
|
||||
// Store cart data in session via AJAX
|
||||
async storeCartInSession(cartData) {
|
||||
const storeCartUrl = `/events/${this.eventSlugValue}.${this.eventIdValue}/store_cart`
|
||||
|
||||
const response = await fetch(storeCartUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ cart: cartData })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to store cart data: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user