10 Commits

Author SHA1 Message Date
kbe
e1edc1afcd fix: Re-enable ticket-selection Stimulus controller registration
- Uncomment ticket-selection controller registration in JavaScript index
- Ensure ticket selection functionality works properly on event pages
- Fix controller not being available for ticket quantity management
- Required for proper cart functionality and checkout flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:44:17 +02:00
kbe
bd6c0d5ed8 refactor: Remove legacy checkout methods from EventsController
- Remove checkout, process_names, and download_ticket methods
- Remove process_payment private method with complex Stripe logic
- Remove StripeConcern include and related authentication requirements
- Simplify EventsController to focus only on event display
- All checkout functionality now handled by OrdersController
- Clean up before_actions to match remaining functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:44:00 +02:00
kbe
5fc790cd42 fix: Resolve QR code generation errors in ticket PDF creation
- Add validation in TicketPdfGenerator to ensure QR code data integrity
- Use compact() to remove nil values from QR code data hash
- Add error handling in Ticket#generate_qr_code with fallback generation
- Validate QR code data before passing to RQRCode library
- Add proper error logging for QR code generation failures
- Prevent "data must be a String, QRSegment, or an Array" errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:43:40 +02:00
kbe
ec5095d372 fix: Resolve Stripe checkout button loading issues
- Add proper Stripe library loading checks to prevent ReferenceError
- Implement retry logic for Stripe library initialization
- Add comprehensive debugging console logs for troubleshooting
- Ensure DOM ready state handling for Turbo compatibility
- Fix async loading race conditions between Stripe CDN and local script
- Add proper error handling for checkout button initialization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:43:32 +02:00
kbe
31f5d2188d refactor: Clean up TicketsController after order migration
- Remove unused 'new' and 'create' methods moved to OrdersController
- Update controller documentation to reflect new purpose as legacy redirect handler
- Remove unused private methods (ticket_params)
- Keep only legacy redirect methods for backward compatibility
- Update before_actions to match remaining functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:43:23 +02:00
kbe
e866e259bb fix: Update event flow to use new event-scoped order routes
- Update events/show form to use event_order_new_path instead of order_new_path
- Fix JavaScript redirect in ticket_selection_controller.js to use event-scoped URL
- Ensure proper event context is maintained throughout the order flow
- Resolve routing issues that caused "Commande non trouvée" errors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:43:13 +02:00
kbe
54e99c2f7e feat: Enhance orders/new view with integrated name collection form
- Add breadcrumb navigation for better UX
- Combine order summary and ticket name collection into single page
- Add comprehensive name collection form for each ticket
- Update form to submit to event-scoped order creation route
- Improve visual design with proper sections and styling
- Remove need for separate tickets controller flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:43:04 +02:00
kbe
3ba5710d8f refactor: Move order creation to event-scoped routes and OrdersController
- Add event-scoped order routes: GET/POST /events/:slug.:id/orders
- Move ticket name collection and order creation logic from TicketsController to OrdersController
- Update OrdersController#new to handle both order summary and name collection
- Add OrdersController#create with full order and ticket creation logic
- Add set_event and order_params methods to OrdersController
- Maintain RESTful design with proper event context

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:42:54 +02:00
kbe
0f6d75b1e8 fix: Resolve tickets controller Event lookup issues
- Fix Event attribute name from starts_at to start_time in orders/new view
- Update TicketsController#set_event to use session[:event_id] as fallback when params[:id] is not available
- Remove duplicate Event.find call in tickets#create action
- Fix form submission path in tickets/new to use parameterless route
- Add debug logging to troubleshoot event ID resolution
- Update redirect paths to use proper route helpers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 01:12:59 +02:00
kbe
ee4399aa46 fix: CSS asset loading and convert footer to Tailwind
- Fix asset loading issues by removing commented footer.css import
- Convert footer component from custom CSS to Tailwind classes
- Add dark background styling to footer wrapper
- Maintain responsive grid layout and hover effects
- Remove unused CSS styles and simplify asset pipeline

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 00:57:44 +02:00
15 changed files with 316 additions and 445 deletions

View File

@@ -9,44 +9,7 @@
/* Import components */
@import "components/hero";
@import "components/flash";
@import "components/footer";
@import "components/event-finder";
/* Import pages */
@import "pages/home";
/* Base styles */
body {
font-family: var(--font-sans);
line-height: 1.6;
color: var(--color-neutral-900);
background: var(--color-neutral-50);
}
/* App wrapper */
.app-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Main content */
main {
flex: 1;
}
/* Footer */
.footer {
background: var(--color-neutral-800);
color: var(--color-neutral-300);
}
/* Flash messages */
.flash {
width: 100%;
}
/* Yield content */
.yield {
width: 100%;
}

View File

@@ -3,10 +3,9 @@
# This controller manages all events. It load events for homepage
# and display for pagination.
class EventsController < ApplicationController
include StripeConcern
before_action :authenticate_user!, only: [ :checkout, :process_names, :download_ticket ]
before_action :set_event, only: [ :show, :checkout, :process_names ]
before_action :authenticate_user!, only: [ ]
before_action :set_event, only: [ :show ]
# Display all events
def index
@@ -20,199 +19,13 @@ class EventsController < ApplicationController
# Event is set by set_event callback
end
# Handle checkout process - Collect names if needed or create Stripe session
def checkout
# Convert cart parameter to proper hash
cart_param = params[:cart]
cart_data = if cart_param.is_a?(String)
JSON.parse(cart_param)
elsif cart_param.is_a?(ActionController::Parameters)
cart_param.to_unsafe_h
else
{}
end
if cart_data.empty?
redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet"
return
end
# Check if any ticket types require names
requires_names = false
cart_data.each do |ticket_type_id, item|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
next unless ticket_type
quantity = item["quantity"].to_i
next if quantity <= 0
if ticket_type.requires_id
requires_names = true
break
end
end
# If names are required, redirect to name collection
if requires_names
session[:pending_cart] = cart_data
redirect_to event_collect_names_path(@event.slug, @event)
return
end
# Otherwise proceed directly to payment
process_payment(cart_data)
end
# Process submitted names and create Stripe session
def process_names
Rails.logger.debug "Processing names for event: #{@event.id}"
cart_data = session[:pending_cart] || {}
if cart_data.empty?
Rails.logger.debug "Cart data is empty"
redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet"
return
end
# Store names in session for later use
if params[:ticket_names].present?
# Convert ActionController::Parameters to hash
if params[:ticket_names].is_a?(ActionController::Parameters)
session[:ticket_names] = params[:ticket_names].to_unsafe_h
else
session[:ticket_names] = params[:ticket_names]
end
end
Rails.logger.debug "Proceeding to payment with cart data: #{cart_data}"
# Proceed to payment
process_payment(cart_data)
end
# Download ticket PDF
def download_ticket
@ticket = current_user.tickets.find(params[:ticket_id])
respond_to do |format|
format.pdf do
pdf = @ticket.to_pdf
send_data pdf,
filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf",
type: "application/pdf",
disposition: "attachment"
end
end
end
private
# Set the current event in the controller
#
# Expose the current @event property to method
def set_event
@event = Event.includes(:ticket_types).find(params[:id])
end
# Process payment and create Stripe session
def process_payment(cart_data)
Rails.logger.debug "Starting process_payment method"
Rails.logger.debug "Cart data: #{cart_data}"
# Create order items from cart
line_items = []
order_items = []
total_amount = 0
cart_data.each do |ticket_type_id, item|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
next unless ticket_type
quantity = item["quantity"].to_i
next if quantity <= 0
# Check availability
available = ticket_type.quantity - ticket_type.tickets.count
if quantity > available
redirect_to event_path(@event.slug, @event), alert: "Pas assez de billets disponibles pour #{ticket_type.name}"
return
end
# Create Stripe line item
line_items << {
price_data: {
currency: "eur",
product_data: {
name: "#{@event.name} - #{ticket_type.name}",
description: ticket_type.description
},
unit_amount: ticket_type.price_cents
},
quantity: quantity
}
# Store for ticket creation
order_items << {
ticket_type_id: ticket_type.id,
ticket_type_name: ticket_type.name,
quantity: quantity,
price_cents: ticket_type.price_cents
}
total_amount += ticket_type.price_cents * quantity
end
if order_items.empty?
redirect_to event_path(@event.slug, @event), alert: "Commande invalide"
return
end
# Get ticket names from session if they exist
ticket_names = session[:ticket_names] || {}
# Debug: Log Stripe configuration status
Rails.logger.debug "Stripe configuration check:"
Rails.logger.debug " Config: #{Rails.application.config.stripe}"
Rails.logger.debug " Secret key present: #{Rails.application.config.stripe[:secret_key].present?}"
Rails.logger.debug " stripe_configured? method exists: #{respond_to?(:stripe_configured?)}"
# Check if Stripe is properly configured
stripe_configured = Rails.application.config.stripe[:secret_key].present?
Rails.logger.debug " Direct stripe_configured check: #{stripe_configured}"
unless stripe_configured
Rails.logger.error "Stripe not configured properly - redirecting to event page"
redirect_to event_path(@event.slug, @event), alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
return
end
# Stripe is now initialized at application startup, no need to initialize here
Rails.logger.debug " Using globally initialized Stripe"
begin
Rails.logger.debug "Creating Stripe Checkout Session"
# Create Stripe Checkout Session
session = Stripe::Checkout::Session.create({
payment_method_types: [ "card" ],
line_items: line_items,
mode: "payment",
success_url: payment_success_url(session_id: "{CHECKOUT_SESSION_ID}"),
cancel_url: event_url(@event.slug, @event),
customer_email: current_user.email,
metadata: {
event_id: @event.id,
user_id: current_user.id,
order_items: order_items.to_json,
ticket_names: ticket_names.to_json
}
})
Rails.logger.debug "Redirecting to Stripe session URL: #{session.url}"
redirect_to session.url, allow_other_host: true
rescue Stripe::StripeError => e
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
Rails.logger.error "Stripe error: #{error_message}"
redirect_to event_path(@event.slug, @event), alert: "Erreur de traitement du paiement : #{error_message}"
end
end
end

View File

@@ -5,29 +5,98 @@
class OrdersController < ApplicationController
before_action :authenticate_user!
before_action :set_order, only: [:show, :checkout, :retry_payment, :increment_payment_attempt]
before_action :set_event, only: [:new, :create]
# Display new order form
# Display new order form with name collection
#
# On this page user can complete the tickets details (first name and last name),
# add a comment on the order.
# On this page user can see order summary and complete the tickets details
# (first name and last name) for each ticket ordered
def new
@cart_data = session[:pending_cart] || {}
if @cart_data.empty?
redirect_to root_path, alert: "Veuillez d'abord sélectionner vos billets"
redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
return
end
# Build order preview from cart data
@event_id = session[:event_id]
if @event_id.present?
@event = Event.includes(:ticket_types).find_by(id: @event_id)
redirect_to root_path, alert: "Événement non trouvé" unless @event
else
redirect_to root_path, alert: "Informations manquantes"
# Build list of tickets requiring names
@tickets_needing_names = []
@cart_data.each do |ticket_type_id, item|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
next unless ticket_type
quantity = item["quantity"].to_i
next if quantity <= 0
quantity.times do |i|
@tickets_needing_names << {
ticket_type_id: ticket_type.id,
ticket_type_name: ticket_type.name,
ticket_type_price: ticket_type.price_cents,
index: i
}
end
end
end
# Create a new order with tickets
#
# Here a new order is created with associated tickets in draft state.
# When user is ready they can proceed to payment via the order checkout
def create
@cart_data = session[:pending_cart] || {}
if @cart_data.empty?
redirect_to event_path(@event.slug, @event), alert: "Aucun billet sélectionné"
return
end
success = false
ActiveRecord::Base.transaction do
@order = current_user.orders.create!(event: @event, status: "draft")
order_params[:tickets_attributes]&.each do |index, ticket_attrs|
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
ticket = @order.tickets.build(
ticket_type: ticket_type,
first_name: ticket_attrs[:first_name],
last_name: ticket_attrs[:last_name],
status: "draft"
)
unless ticket.save
flash[:alert] = "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}"
raise ActiveRecord::Rollback
end
end
if @order.tickets.present?
@order.calculate_total!
success = true
else
flash[:alert] = "Aucun billet valide créé"
raise ActiveRecord::Rollback
end
end
# Handle redirects outside transaction
if success
session[:draft_order_id] = @order.id
session.delete(:pending_cart)
redirect_to checkout_order_path(@order)
else
redirect_to event_order_new_path(@event.slug, @event.id)
end
rescue => e
error_message = e.message.present? ? e.message : "Erreur inconnue"
flash[:alert] = "Une erreur est survenue: #{error_message}"
redirect_to event_order_new_path(@event.slug, @event.id)
end
# Display order summary
#
#
@@ -44,7 +113,7 @@ class OrdersController < ApplicationController
if @order.expired?
@order.expire_if_overdue!
return redirect_to event_path(@order.event.slug, @order.event),
alert: "Votre commande a expiré. Veuillez recommencer."
alert: "Votre commande a expiré. Veuillez recommencer."
end
@tickets = @order.tickets.includes(:ticket_type)
@@ -111,7 +180,12 @@ class OrdersController < ApplicationController
# Send confirmation emails
@order.tickets.each do |ticket|
TicketMailer.purchase_confirmation(ticket).deliver_now
begin
TicketMailer.purchase_confirmation(ticket).deliver_now
rescue => e
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
# Don't fail the entire payment process due to email/PDF generation issues
end
end
# Clear session data
@@ -160,6 +234,16 @@ class OrdersController < ApplicationController
redirect_to dashboard_path, alert: "Commande non trouvée"
end
def set_event
@event = Event.includes(:ticket_types).find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to events_path, alert: "Événement non trouvé"
end
def order_params
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
end
def create_stripe_session
line_items = @order.tickets.map do |ticket|
{

View File

@@ -1,101 +1,11 @@
# Manage tickets creation
# Legacy tickets controller - redirects to new order system
#
# This controller permit users to create a new ticket for an event,
# complete their details and proceed to payment
# This controller now primarily handles legacy redirects and backward compatibility
# Most ticket creation functionality has been moved to OrdersController
class TicketsController < ApplicationController
before_action :authenticate_user!, only: [ :new, :payment_success, :payment_cancel ]
before_action :set_event, only: [ :new, :create ]
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel ]
before_action :set_event, only: [ :checkout, :retry_payment ]
# Handle new ticket creation
#
# Once user selected ticket types he wans for an event
# he cames here where he can complete his details (first_name, last_name)
# for each ticket ordered
def new
@cart_data = session[:pending_cart] || {}
if @cart_data.empty?
redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
return
end
# Build list of tickets requiring names
@tickets_needing_names = []
@cart_data.each do |ticket_type_id, item|
ticket_type = @event.ticket_types.find_by(id: ticket_type_id)
next unless ticket_type
quantity = item["quantity"].to_i
next if quantity <= 0
quantity.times do |i|
@tickets_needing_names << {
ticket_type_id: ticket_type.id,
ticket_type_name: ticket_type.name,
index: i
}
end
end
end
# Create a new order with tickets
#
# Here a new order is created with associated tickets in draft state.
# When user is ready they can proceed to payment via the order checkout
def create
@cart_data = session[:pending_cart] || {}
if @cart_data.empty?
redirect_to event_path(params[:slug], params[:id]), alert: "Aucun billet sélectionné"
return
end
@event = Event.includes(:ticket_types).find(params[:id])
success = false
ActiveRecord::Base.transaction do
@order = current_user.orders.create!(event: @event, status: "draft")
ticket_params[:tickets_attributes]&.each do |index, ticket_attrs|
next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank?
ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id])
ticket = @order.tickets.build(
ticket_type: ticket_type,
first_name: ticket_attrs[:first_name],
last_name: ticket_attrs[:last_name],
status: "draft"
)
unless ticket.save
flash[:alert] = "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}"
raise ActiveRecord::Rollback
end
end
if @order.tickets.present?
@order.calculate_total!
success = true
else
flash[:alert] = "Aucun billet valide créé"
raise ActiveRecord::Rollback
end
end
# Handle redirects outside transaction
if success
session[:draft_order_id] = @order.id
session.delete(:pending_cart)
redirect_to checkout_order_path(@order)
else
redirect_to ticket_new_path(@event.slug, @event.id)
end
rescue => e
error_message = e.message.present? ? e.message : "Erreur inconnue"
flash[:alert] = "Une erreur est survenue: #{error_message}"
redirect_to ticket_new_path(params[:slug], params[:id])
end
# Redirect to order-based checkout
def checkout
@@ -147,12 +57,23 @@ class TicketsController < ApplicationController
private
def set_event
@event = Event.includes(:ticket_types).find(params[:id])
event_id = params[:id] || session[:event_id]
Rails.logger.debug "TicketsController#set_event - params[:id]: #{params[:id].inspect}, session[:event_id]: #{session[:event_id].inspect}"
unless event_id
Rails.logger.error "TicketsController#set_event - No event ID found"
redirect_to events_path, alert: "Aucun événement spécifié"
return
end
@event = Event.includes(:ticket_types).find(event_id)
Rails.logger.debug "TicketsController#set_event - Found event: #{@event.id} - #{@event.name}"
rescue ActiveRecord::RecordNotFound
Rails.logger.error "TicketsController#set_event - Event not found with ID: #{event_id}"
redirect_to events_path, alert: "Événement non trouvé"
end
def ticket_params
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
end
def create_stripe_session
line_items = @tickets.map do |ticket|

View File

@@ -14,7 +14,7 @@ import FlashMessageController from "./flash_message_controller";
application.register("flash-message", FlashMessageController);
import TicketSelectionController from "./ticket_selection_controller";
// application.register("ticket-selection", TicketSelectionController);
application.register("ticket-selection", TicketSelectionController);
import HeaderController from "./header_controller";
application.register("header", HeaderController);

View File

@@ -117,8 +117,8 @@ export default class extends Controller {
// Store cart data in session
await this.storeCartInSession(cartData);
// Redirect to orders/new page
const OrderNewUrl = `/orders/new`;
// Redirect to event-scoped orders/new page
const OrderNewUrl = `/events/${this.eventSlugValue}.${this.eventIdValue}/orders/new`;
window.location.href = OrderNewUrl;
} catch (error) {
console.error("Error storing cart:", error);

View File

@@ -63,6 +63,10 @@ class Ticket < ApplicationRecord
self.qr_code = SecureRandom.uuid
break unless Ticket.exists?(qr_code: qr_code)
end
rescue => e
Rails.logger.error "Failed to generate QR code for ticket: #{e.message}"
# Generate a simple fallback QR code
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
end

View File

@@ -63,12 +63,22 @@ class TicketPdfGenerator
pdf.text "Ticket QR Code", align: :center
pdf.move_down 10
# Ensure all required data is present before generating QR code
if ticket.qr_code.blank?
raise "Ticket QR code is missing"
end
qr_code_data = {
ticket_id: ticket.id,
qr_code: ticket.qr_code,
event_id: ticket.event.id,
user_id: ticket.user.id
}.to_json
event_id: ticket.event&.id,
user_id: ticket.user&.id
}.compact.to_json
# Validate QR code data before creating QR code
if qr_code_data.blank? || qr_code_data == "{}"
raise "QR code data is empty or invalid"
end
qrcode = RQRCode::QRCode.new(qr_code_data)
pdf.print_qr_code(qrcode, extent: 120, align: :center)

View File

@@ -1,41 +1,41 @@
<div class="footer-content">
<div class="footer-section">
<h3>Events</h3>
<ul class="footer-links">
<li><a href="#">Find Events</a></li>
<li><a href="#">Host an Event</a></li>
<li><a href="#">Event Categories</a></li>
<li><a href="#">Premium Events</a></li>
<div class="grid gap-6 mb-6 md:grid-cols-2 lg:grid-cols-4">
<div>
<h3 class="font-bold text-lg text-white mb-3">Events</h3>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Find Events</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Host an Event</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Event Categories</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Premium Events</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Community</h3>
<ul class="footer-links">
<li><a href="#">Join Us</a></li>
<li><a href="#">Member Benefits</a></li>
<li><a href="#">Success Stories</a></li>
<li><a href="#">Ambassador Program</a></li>
<div>
<h3 class="font-bold text-lg text-white mb-3">Community</h3>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Join Us</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Member Benefits</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Success Stories</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Ambassador Program</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Support</h3>
<ul class="footer-links">
<li><a href="#">Help Center</a></li>
<li><a href="#">Contact Us</a></li>
<li><a href="#">Safety Guidelines</a></li>
<li><a href="#">Cancellation Policy</a></li>
<div>
<h3 class="font-bold text-lg text-white mb-3">Support</h3>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Help Center</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Contact Us</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Safety Guidelines</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Cancellation Policy</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Company</h3>
<ul class="footer-links">
<li><a href="#">About Aperonight</a></li>
<li><a href="#">Careers</a></li>
<li><a href="#">Press & Media</a></li>
<li><a href="#">Partner With Us</a></li>
<div>
<h3 class="font-bold text-lg text-white mb-3">Company</h3>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">About Aperonight</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Careers</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Press & Media</a></li>
<li><a href="#" class="text-neutral-400 text-sm hover:text-accent-400 transition-colors duration-300">Partner With Us</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2024 Aperonight. All rights reserved. • <a href="#" style="color: var(--color-accent-400);">Privacy Policy</a> • <a href="#" style="color: var(--color-accent-400);">Terms of Service</a></p>
<div class="border-t border-neutral-700 pt-4 text-center text-neutral-400 text-sm">
<p>&copy; 2024 Aperonight. All rights reserved. • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Privacy Policy</a> • <a href="#" class="text-accent-400 hover:text-accent-300 transition-colors">Terms of Service</a></p>
</div>

View File

@@ -1,23 +1,11 @@
<div
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100"
>
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm">
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<svg
class="w-4 h-4 inline-block mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/>
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
Accueil
<% end %>
@@ -56,7 +44,8 @@
</ol>
</nav>
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
<!-- Event main wrapper -->
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
<!-- Event Header with Image -->
<% if @event.image.present? %>
<div class="relative h-96">
@@ -68,7 +57,7 @@
></div>
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2"><%= @event.name %></h1>
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2 text-center md:text-left"><%= @event.name %></h1>
</div>
</div>
</div>
@@ -208,8 +197,8 @@
<!-- Right Column: Ticket Selection -->
<div class="lg:col-span-1">
<%= form_with url: order_new_path, method: :get, id: "checkout_form", local: true, data: {
controller: "ticket-selection",
<%= form_with url: event_order_new_path(@event.slug, @event.id), method: :get, id: "checkout_form", local: true, data: {
controller: "ticket-selection",
ticket_selection_target: "form",
ticket_selection_event_slug_value: @event.slug,
ticket_selection_event_id_value: @event.id

View File

@@ -24,6 +24,7 @@
<link rel="apple-touch-icon" href="/icon.png">
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%# stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
@@ -41,20 +42,11 @@
</div>
</main>
<footer class="footer">
<footer class="bg-neutral-800 text-neutral-300 py-8 pb-4">
<div class="container">
<%= render "components/footer" %>
</div>
</footer>
</div>
<script>
// Initialize Lucide icons
document.addEventListener("DOMContentLoaded", function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
</body>
</html>

View File

@@ -178,9 +178,25 @@
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('<%= Rails.application.config.stripe[:publishable_key] %>');
document.getElementById('checkout-button').addEventListener('click', async function() {
// Wait for Stripe library to load and DOM to be ready
function initializeStripeCheckout() {
if (typeof Stripe === 'undefined') {
console.log('Waiting for Stripe library to load...');
setTimeout(initializeStripeCheckout, 100);
return;
}
console.log('Initializing Stripe with publishable key:', '<%= Rails.application.config.stripe[:publishable_key] %>');
const stripe = Stripe('<%= Rails.application.config.stripe[:publishable_key] %>');
const checkoutButton = document.getElementById('checkout-button');
if (!checkoutButton) {
console.error('Checkout button not found');
return;
}
checkoutButton.addEventListener('click', async function() {
console.log('Checkout button clicked');
const button = this;
button.disabled = true;
button.innerHTML = `
@@ -195,6 +211,7 @@
try {
// Increment payment attempt counter
console.log('Incrementing payment attempt for order:', '<%= @order.id %>');
const response = await fetch('<%= increment_payment_attempt_order_path(@order) %>', {
method: 'POST',
headers: {
@@ -204,9 +221,12 @@
});
if (!response.ok) {
console.error('Payment attempt increment failed:', response.status, response.statusText);
throw new Error('Failed to increment payment attempt');
}
console.log('Payment attempt incremented successfully');
// Update button text for redirect
button.innerHTML = `
<div class="flex items-center justify-center">
@@ -219,6 +239,7 @@
`;
// Redirect to Stripe
console.log('Redirecting to Stripe with session ID:', '<%= @checkout_session&.id %>');
const stripeResult = await stripe.redirectToCheckout({
sessionId: '<%= @checkout_session.id %>'
});
@@ -228,6 +249,7 @@
}
} catch (error) {
console.error('Checkout error:', error);
// Reset button on error
button.disabled = false;
button.innerHTML = `
@@ -240,7 +262,15 @@
`;
alert('Erreur: ' + error.message);
}
});
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeStripeCheckout);
} else {
initializeStripeCheckout();
}
</script>
</div>
<% else %>

View File

@@ -1,9 +1,39 @@
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<nav class="mb-8" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2 text-sm">
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
Accueil
<% end %>
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
Événements
<% end %>
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
<%= @event.name %>
<% end %>
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<li class="font-medium text-gray-900" aria-current="page">
Nouvelle commande
</li>
</ol>
</nav>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Nouvelle Commande</h1>
<p class="text-gray-600">Vérifiez vos billets avant de continuer vers le paiement</p>
<p class="text-gray-600">Vérifiez vos billets et indiquez les noms des participants</p>
</div>
<!-- Order Summary -->
@@ -14,9 +44,9 @@
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-800"><%= @event.name %></h3>
<p class="text-gray-600"><%= @event.venue_name %></p>
<% if @event.starts_at %>
<% if @event.start_time %>
<p class="text-sm text-gray-500">
<%= @event.starts_at.strftime("%d/%m/%Y à %H:%M") %>
<%= @event.start_time.strftime("%d/%m/%Y à %H:%M") %>
</p>
<% end %>
</div>
@@ -42,30 +72,69 @@
<% end %>
</div>
<!-- Continue to Tickets -->
<div class="flex justify-end">
<%= link_to ticket_new_path,
class: "inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-xl text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
<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="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
Continuer vers les détails
<% end %>
</div>
<% end %>
</div>
<!-- Back to Event -->
<div class="mt-6">
<% if @event %>
<%= link_to event_path(@event.slug, @event),
class: "inline-flex items-center text-purple-600 hover:text-purple-700 font-medium transition-colors" do %>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Retour à l'événement
<% end %>
<% end %>
</div>
<!-- Name Collection Form -->
<% if @tickets_needing_names.any? %>
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 mt-6">
<div class="p-6 md:p-8">
<div class="text-center mb-8">
<div class="mx-auto bg-purple-100 rounded-full p-3 w-16 h-16 flex items-center justify-center mb-4">
<svg class="w-8 h-8 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>
<h2 class="text-2xl font-bold text-gray-900 mb-2">Informations des participants</h2>
<p class="text-gray-600 max-w-md mx-auto">Veuillez fournir les prénoms et noms des personnes qui utiliseront les billets.</p>
</div>
<%= form_with url: event_order_create_path(@event.slug, @event.id), method: :post, local: true, class: "space-y-8" do |form| %>
<div class="space-y-6">
<div class="flex items-center justify-center mb-2">
<div class="bg-purple-600 rounded-full p-2 mr-3">
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900">Billets nécessitant une identification</h3>
</div>
<p class="text-gray-600 mb-6 text-center">Les billets suivants nécessitent que vous indiquiez le prénom et le nom de chaque participant.</p>
<% @tickets_needing_names.each_with_index do |ticket, index| %>
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl p-6 border border-purple-100">
<div class="flex items-center mb-4">
<div class="bg-purple-500 rounded-lg p-2 mr-3">
<svg class="w-5 h-5 text-white" 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>
<h4 class="text-lg font-semibold text-gray-900"><%= ticket[:ticket_type_name] %> #<%= index + 1 %></h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<%= form.label "tickets_attributes[#{index}][first_name]", "Prénom", class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.text_field "tickets_attributes[#{index}][first_name]", required: true, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-200 shadow-sm", placeholder: "Entrez le prénom" %>
<%= form.hidden_field "tickets_attributes[#{index}][ticket_type_id]", value: ticket[:ticket_type_id] %>
</div>
<div>
<%= form.label "tickets_attributes[#{index}][last_name]", "Nom", class: "block text-sm font-medium text-gray-700 mb-1" %>
<%= form.text_field "tickets_attributes[#{index}][last_name]", required: true, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-all duration-200 shadow-sm", placeholder: "Entrez le nom" %>
</div>
</div>
</div>
<% end %>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-6">
<%= link_to "Retour", event_path(@event.slug, @event), class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" %>
<%= form.submit "Procéder au paiement", class: "flex-1 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-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>

View File

@@ -98,7 +98,7 @@
les billets.</p>
</div>
<%= form_with url: ticket_create_path(@event.slug, @event), method: :post, local: true, class: "space-y-8" do |form| %>
<%= form_with url: ticket_create_path, method: :post, local: true, class: "space-y-8" do |form| %>
<% if @tickets_needing_names.any? %>
<div class="space-y-6">
<div class="flex items-center justify-center mb-2">

View File

@@ -38,9 +38,9 @@ Rails.application.routes.draw do
get "events", to: "events#index", as: "events"
get "events/:slug.:id", to: "events#show", as: "event"
# === Orders ===
get "orders/new", to: "orders#new", as: "order_new"
# === Orders (scoped to events) ===
get "events/:slug.:id/orders/new", to: "orders#new", as: "event_order_new"
post "events/:slug.:id/orders", to: "orders#create", as: "event_order_create"
resources :orders, only: [:show] do
member do
@@ -53,11 +53,7 @@ Rails.application.routes.draw do
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
# === Tickets ===
get "tickets/new", to: "tickets#new", as: "ticket_new"
post "tickets/create", to: "tickets#create", as: "ticket_create"
# Keep these for now but they redirect to order system
# Legacy ticket routes - redirect to order system
get "events/:slug.:id/tickets/checkout", to: "tickets#checkout", as: "ticket_checkout"
post "events/:slug.:id/tickets/retry", to: "tickets#retry_payment", as: "ticket_retry_payment"
get "payments/success", to: "tickets#payment_success", as: "payment_success"