From 2a2c249a549564da30458c5a60da7a337a828a38 Mon Sep 17 00:00:00 2001 From: kbe Date: Thu, 28 Aug 2025 21:20:33 +0200 Subject: [PATCH] feat(show): wip to checkout --- app/javascript/application.js | 4 + app/javascript/controllers/application.js | 5 - app/javascript/controllers/index.js | 2 - .../controllers/ticket_cart_controller.js | 333 ------------- app/views/components/_ticket_card.html.erb | 61 ++- app/views/events/show.html.erb | 447 ++++++++++++++++-- 6 files changed, 457 insertions(+), 395 deletions(-) delete mode 100755 app/javascript/controllers/ticket_cart_controller.js diff --git a/app/javascript/application.js b/app/javascript/application.js index 6d426e6..41fd25e 100755 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,7 @@ // Entry point for the build script in your package.json import "@hotwired/turbo-rails"; +import Alpine from 'alpinejs'; import "./controllers"; + +window.Alpine = Alpine; +Alpine.start(); diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js index e297a52..d6fe5eb 100755 --- a/app/javascript/controllers/application.js +++ b/app/javascript/controllers/application.js @@ -1,5 +1,4 @@ import { Application } from "@hotwired/stimulus"; -import Alpine from "alpinejs"; const application = Application.start(); @@ -7,8 +6,4 @@ const application = Application.start(); application.debug = false; window.Stimulus = application; -// Configure and load Alpine -window.Alpine = Alpine; -Alpine.start(); - export { application }; diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index fb8f55a..93abc2b 100755 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -8,14 +8,12 @@ import LogoutController from "./logout_controller" import FlashMessageController from "./flash_message_controller" import CounterController from "./counter_controller" import FeaturedEventController from "./featured_event_controller" -import TicketCartController from "./ticket_cart_controller" application.register("logout", LogoutController) // Allow logout using js application.register("flash-message", FlashMessageController) // Dismiss notification after 5 secondes application.register("counter", CounterController) // Simple counter for homepage application.register("featured-event", FeaturedEventController) // Featured event controller for homepage -application.register("ticket-cart-controller", TicketCartController) // Handle ticket checkout diff --git a/app/javascript/controllers/ticket_cart_controller.js b/app/javascript/controllers/ticket_cart_controller.js deleted file mode 100755 index e995dc6..0000000 --- a/app/javascript/controllers/ticket_cart_controller.js +++ /dev/null @@ -1,333 +0,0 @@ -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 = ` -
-
-
- - - -
-

Connexion requise

-

Vous devez être connecté pour acheter des billets. Votre panier sera conservé.

-
- - -
-
-
- ` - - 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 = ` - - - - - Redirection... - ` - } else { - checkoutBtn.disabled = false - checkoutBtn.innerHTML = ` - - - - 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 = ` -
-
-

${message}

-
- -
- ` - - 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 - } -} diff --git a/app/views/components/_ticket_card.html.erb b/app/views/components/_ticket_card.html.erb index 38b9e18..bf2c567 100755 --- a/app/views/components/_ticket_card.html.erb +++ b/app/views/components/_ticket_card.html.erb @@ -1,4 +1,41 @@ -
shadow-sm overflow-hidden"> +
shadow-sm overflow-hidden" + x-data="{ + quantity: 0, + max: <%= remaining %>, + ticketTypeId: <%= id %>, + name: '<%= name %>', + price: <%= price_cents %> + }" + x-init=" + $watch('quantity', (value) => { + $dispatch('update-cart-item', { + ticketTypeId: ticketTypeId, + name: name, + price: price, + quantity: value, + max: max + }); + }); + + $nextTick(() => { + $dispatch('cart-initialized', { ticketTypeId: ticketTypeId }); + }); + " + @update-cart-item.window=" + if ($event.detail.ticketTypeId === ticketTypeId) { + quantity = $event.detail.quantity; + } + " + @cart-change.window=" + if (!$event.detail.cart[ticketTypeId]) { + quantity = 0; + } + " + @restore-cart.window=" + if ($event.detail.cart[ticketTypeId]) { + quantity = $event.detail.cart[ticketTypeId].quantity; + } + ">
@@ -35,27 +72,19 @@
+ :max="max" + x-model="quantity" + class="w-12 text-center border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1">
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index e60860d..e95e3f7 100755 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -1,22 +1,56 @@ -
+