develop #3
@@ -43,14 +43,14 @@ class EventsController < ApplicationController
|
|||||||
# Create Stripe line item
|
# Create Stripe line item
|
||||||
line_items << {
|
line_items << {
|
||||||
price_data: {
|
price_data: {
|
||||||
currency: 'eur',
|
currency: "eur",
|
||||||
product_data: {
|
product_data: {
|
||||||
name: "#{@event.name} - #{ticket_type.name}",
|
name: "#{@event.name} - #{ticket_type.name}",
|
||||||
description: ticket_type.description,
|
description: ticket_type.description
|
||||||
},
|
},
|
||||||
unit_amount: ticket_type.price_cents,
|
unit_amount: ticket_type.price_cents
|
||||||
},
|
},
|
||||||
quantity: quantity,
|
quantity: quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store for ticket creation
|
# Store for ticket creation
|
||||||
@@ -72,10 +72,10 @@ class EventsController < ApplicationController
|
|||||||
begin
|
begin
|
||||||
# Create Stripe Checkout Session
|
# Create Stripe Checkout Session
|
||||||
session = Stripe::Checkout::Session.create({
|
session = Stripe::Checkout::Session.create({
|
||||||
payment_method_types: ['card'],
|
payment_method_types: [ "card" ],
|
||||||
line_items: line_items,
|
line_items: line_items,
|
||||||
mode: 'payment',
|
mode: "payment",
|
||||||
success_url: payment_success_url(event_id: @event.id, session_id: '{CHECKOUT_SESSION_ID}'),
|
success_url: payment_success_url(event_id: @event.id, session_id: "{CHECKOUT_SESSION_ID}"),
|
||||||
cancel_url: event_url(@event.slug, @event),
|
cancel_url: event_url(@event.slug, @event),
|
||||||
customer_email: current_user.email,
|
customer_email: current_user.email,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -99,19 +99,19 @@ class EventsController < ApplicationController
|
|||||||
begin
|
begin
|
||||||
session = Stripe::Checkout::Session.retrieve(session_id)
|
session = Stripe::Checkout::Session.retrieve(session_id)
|
||||||
|
|
||||||
if session.payment_status == 'paid'
|
if session.payment_status == "paid"
|
||||||
# Create tickets
|
# Create tickets
|
||||||
@event = Event.find(event_id)
|
@event = Event.find(event_id)
|
||||||
order_items = JSON.parse(session.metadata['order_items'])
|
order_items = JSON.parse(session.metadata["order_items"])
|
||||||
@tickets = []
|
@tickets = []
|
||||||
|
|
||||||
order_items.each do |item|
|
order_items.each do |item|
|
||||||
ticket_type = TicketType.find(item['ticket_type_id'])
|
ticket_type = TicketType.find(item["ticket_type_id"])
|
||||||
item['quantity'].times do
|
item["quantity"].times do
|
||||||
ticket = Ticket.create!(
|
ticket = Ticket.create!(
|
||||||
user: current_user,
|
user: current_user,
|
||||||
ticket_type: ticket_type,
|
ticket_type: ticket_type,
|
||||||
status: 'active'
|
status: "active"
|
||||||
)
|
)
|
||||||
@tickets << ticket
|
@tickets << ticket
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class EventsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render 'payment_success'
|
render "payment_success"
|
||||||
else
|
else
|
||||||
redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès"
|
redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès"
|
||||||
end
|
end
|
||||||
@@ -140,8 +140,8 @@ class EventsController < ApplicationController
|
|||||||
pdf = @ticket.to_pdf
|
pdf = @ticket.to_pdf
|
||||||
send_data pdf,
|
send_data pdf,
|
||||||
filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf",
|
filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf",
|
||||||
type: 'application/pdf',
|
type: "application/pdf",
|
||||||
disposition: 'attachment'
|
disposition: "attachment"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -102,7 +102,12 @@ export default class extends Controller {
|
|||||||
|
|
||||||
proceedToCheckout() {
|
proceedToCheckout() {
|
||||||
if (Object.keys(this.cart).length === 0) {
|
if (Object.keys(this.cart).length === 0) {
|
||||||
alert('Veuillez sélectionner au moins un billet')
|
this.showNotification('Veuillez sélectionner au moins un billet', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cart contents
|
||||||
|
if (!this.validateCartAvailability()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,17 +115,13 @@ export default class extends Controller {
|
|||||||
const isAuthenticated = document.body.dataset.userAuthenticated === "true"
|
const isAuthenticated = document.body.dataset.userAuthenticated === "true"
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
if (confirm('Vous devez être connecté pour acheter des billets. Souhaitez-vous vous connecter maintenant ?')) {
|
this.showLoginModal()
|
||||||
// Store cart in session storage
|
|
||||||
sessionStorage.setItem('pending_cart', JSON.stringify({
|
|
||||||
eventId: this.eventIdValue,
|
|
||||||
cart: this.cart
|
|
||||||
}))
|
|
||||||
window.location.href = '/auth/sign_in'
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
this.setCheckoutLoading(true)
|
||||||
|
|
||||||
// Create form and submit to checkout
|
// Create form and submit to checkout
|
||||||
const form = document.createElement('form')
|
const form = document.createElement('form')
|
||||||
form.method = 'POST'
|
form.method = 'POST'
|
||||||
@@ -145,6 +146,141 @@ export default class extends Controller {
|
|||||||
form.submit()
|
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() {
|
checkForPendingCart() {
|
||||||
const pendingCart = sessionStorage.getItem('pending_cart')
|
const pendingCart = sessionStorage.getItem('pending_cart')
|
||||||
if (pendingCart) {
|
if (pendingCart) {
|
||||||
|
|||||||
@@ -151,14 +151,14 @@
|
|||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div class="flex justify-between items-center mb-2">
|
||||||
<span class="text-gray-600">Total billets:</span>
|
<span class="text-gray-600">Total billets:</span>
|
||||||
<span id="cart-count" class="font-medium">0</span>
|
<span data-ticket-cart-target="cartCount" class="font-medium">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<span class="text-gray-600">Montant total:</span>
|
<span class="text-gray-600">Montant total:</span>
|
||||||
<span id="cart-total" class="text-xl font-bold text-purple-700">€0.00</span>
|
<span data-ticket-cart-target="cartTotal" class="text-xl font-bold text-purple-700">€0.00</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
id="checkout-btn"
|
data-ticket-cart-target="checkoutButton"
|
||||||
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center"
|
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center"
|
||||||
disabled
|
disabled
|
||||||
data-action="click->ticket-cart#proceedToCheckout">
|
data-action="click->ticket-cart#proceedToCheckout">
|
||||||
|
|||||||
Reference in New Issue
Block a user