feat(show parties): prepare to use ticket cart components

This commit is contained in:
kbe
2025-08-27 02:31:20 +02:00
parent 1806c875b5
commit 7c7db939a2
8 changed files with 398 additions and 85 deletions

View File

@@ -12,5 +12,6 @@ class ApplicationController < ActionController::Base
# - Badge API for notifications
# - Import maps for JavaScript modules
# - CSS nesting and :has() pseudo-class
allow_browser versions: :modern
# allow_browser versions: :modern
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
end

View File

@@ -9,4 +9,56 @@ class PartiesController < ApplicationController
def show
@party = Party.find(params[:id])
end
# Handle checkout process
def checkout
@party = Party.find(params[:id])
cart_data = JSON.parse(params[:cart] || "{}")
if cart_data.empty?
redirect_to party_path(@party), alert: "Please select at least one ticket"
return
end
# Create order items from cart
order_items = []
total_amount = 0
cart_data.each do |ticket_type_id, item|
ticket_type = @party.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 party_path(@party), alert: "Not enough tickets available for #{ticket_type.name}"
return
end
order_items << {
ticket_type: ticket_type,
quantity: quantity,
price_cents: ticket_type.price_cents
}
total_amount += ticket_type.price_cents * quantity
end
if order_items.empty?
redirect_to party_path(@party), alert: "Invalid order"
return
end
# Here you would typically:
# 1. Create an Order record
# 2. Create Ticket records for each item
# 3. Redirect to payment processing
# For now, we'll just redirect with a success message
# In a real app, you'd redirect to a payment page
redirect_to party_path(@party), notice: "Order created successfully! Proceeding to payment..."
end
end

View File

@@ -0,0 +1,107 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["quantity", "cartCount", "cartTotal", "checkoutButton"]
static values = { partyId: String }
connect() {
this.cart = {}
this.updateCartDisplay()
}
increaseQuantity(event) {
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
const max = parseInt(event.currentTarget.dataset.max)
const input = this.quantityTargetFor(ticketTypeId)
const current = parseInt(input.value) || 0
if (current < max) {
input.value = current + 1
this.updateCartItem(ticketTypeId, input)
}
}
decreaseQuantity(event) {
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
const input = this.quantityTargetFor(ticketTypeId)
const current = parseInt(input.value) || 0
if (current > 0) {
input.value = current - 1
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
}
this.cartCountTarget.textContent = totalTickets
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
const checkoutBtn = this.checkoutButtonTarget
if (totalTickets > 0) {
checkoutBtn.disabled = false
} else {
checkoutBtn.disabled = true
}
}
proceedToCheckout() {
if (Object.keys(this.cart).length === 0) {
alert('Please select at least one ticket')
return
}
const form = document.createElement('form')
form.method = 'POST'
form.action = `/parties/${this.partyIdValue}/checkout`
form.style.display = 'none'
// 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()
}
// Helper method to find quantity input by ticket type ID
quantityTargetFor(ticketTypeId) {
return document.querySelector(`#quantity_${ticketTypeId}`)
}
}

View File

@@ -1,6 +1,6 @@
<header class="shadow-sm border-b border-neutral-200">
<div class="bg-gray-800">
<nav x-data="{ open: false }" class="bg-blue border-b border-purple-700">
<header class="shadow-sm border-b border-neutral-700">
<div class="bg-neutral-900">
<nav x-data="{ open: false }" class="bg-neutral-800 border-b border-neutral-700">
<!-- Primary Navigation Menu -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
@@ -12,11 +12,11 @@
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex items-center">
<%= link_to t('header.parties'), parties_path,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
<%= link_to t('header.concerts'), "#" ,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
</div>
</div>
<!-- Authentication Links -->
@@ -26,7 +26,7 @@
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
<button
class="bg-purple-700 text-white border border-purple-800 font-medium py-2 px-4 rounded-lg hover:bg-purple-800 transition-colors duration-200 focus-ring">
class="bg-primary-700 text-white border border-primary-800 font-medium py-2 px-4 rounded-lg hover:bg-primary-800 transition-colors duration-200 focus-ring">
<div>
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
</div>
@@ -45,15 +45,15 @@
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 w-48 rounded-md shadow-lg origin-top-right right-0" style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-purple-700 py-1 bg-purple-600">
<div class="rounded-md ring-1 ring-primary-700 py-1 bg-primary-600">
<%= link_to t('header.profile') , edit_user_registration_path,
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
<%= link_to t('header.reservations') , "#" ,
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" ,
action: "click->logout#signOut" , logout_url_value: destroy_user_session_path, login_url_value:
new_user_session_path, turbo: false },
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-white hover:bg-purple-700" %>
class: "block w-full px-4 py-2 text-start text-sm leading-5 text-neutral-100 hover:bg-primary-700" %>
</div>
</div>
</div>
@@ -62,17 +62,17 @@
<!-- Login/Register Links -->
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-4 items-center">
<%= link_to t('header.login') , new_user_session_path,
class: "text-white hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
class: "text-neutral-100 hover:text-primary-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
%>
<%= link_to t('header.register') , new_user_registration_path,
class: "bg-white text-purple-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-purple-100 transition-all duration-200"
class: "bg-primary-50 text-primary-600 font-medium py-2 px-4 rounded-lg shadow-sm hover:bg-primary-100 transition-all duration-200"
%>
</div>
<% end %>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open"
class="p-2 rounded-md text-purple-200 hover:text-white hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
class="p-2 rounded-md text-neutral-300 hover:text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{ 'hidden': open, 'inline-flex': !open }" class="inline-flex" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
@@ -85,16 +85,16 @@
</div>
<!-- Responsive Navigation Menu -->
<div :class="{ 'block': open, 'hidden': !open }" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1 bg-purple-600">
<div class="pt-2 pb-3 space-y-1 bg-primary-600">
<%= link_to t('header.parties') , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
<%= link_to t('header.concerts') , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-purple-700 bg-purple-600">
<div class="pt-4 pb-1 border-t border-primary-700 bg-primary-600">
<% if user_signed_in? %>
<div class="px-4">
<% if current_user.first_name %>
@@ -108,29 +108,28 @@
<%# <div class="font-medium text-sm text-purple-200">
<%= current_user.email.length> 20 ? current_user.email[0,20] + "..." : current_user.email %>
</div>
%>
<% end %>
</div>
<div class="mt-3 space-y-1">
<%= link_to t('header.profile') , edit_user_registration_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
<%= link_to t('header.reservations') , "#" ,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
<%= link_to t('header.logout') , destroy_user_session_path, data: { controller: "logout" , action: "click->logout#signOut",
logout_url_value: destroy_user_session_path, login_url_value: new_user_session_path, turbo: false },
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
</div>
<% else %>
<div class="mt-3 space-y-1">
<%= link_to t('header.register') , new_user_registration_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
<%= link_to t('header.login') , new_user_session_path,
class: "block px-3 py-2 rounded-md text-base font-medium text-white hover:text-purple-200 hover:bg-purple-700"
class: "block px-3 py-2 rounded-md text-base font-medium text-neutral-100 hover:text-primary-200 hover:bg-primary-700"
%>
</div>
<% end %>

View File

@@ -0,0 +1,72 @@
<div class="card rounded-2xl <%= sold_out ? "border border-neutral-200 opacity-75" : "border border-neutral-200 " %>">
<div class="card-body p-6">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-xl font-bold text-primary <%= "text-slate-400" if sold_out %>"><%= name %></h3>
<p class="text-neutral-600 text-sm <%= "text-slate-400" if sold_out %>"><%= description %></p>
</div>
<div class="text-right">
<p class="text-2xl font-bold text-primary <%= "text-slate-400" if sold_out %>">
<%= number_to_currency(price_cents / 100.0, unit: "€") %>
</p>
</div>
</div>
<div class="flex justify-between items-center">
<div>
<p class="text-sm text-neutral-600 flex items-center">
<span class="inline-block"><%= quantity %> total •</span>
<% if sold_out %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 ml-2">
<svg class="-ml-0.5 mr-1.5 h-2 w-2 text-red-400" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
Sold Out
</span>
<% else %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 ml-2">
<svg class="-ml-0.5 mr-1.5 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
<%= remaining %> available
</span>
<% end %>
</p>
</div>
</div>
<div class="flex justify-between items-right mt-4">
<% if sold_out %>
<div class="text-sm text-slate-500 font-medium">
<svg class="w-5 h-5 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
Unavailable
</div>
<% else %>
<div class="flex items-center space-x-2">
<button type="button"
class="w-8 h-8 rounded-full bg-slate-200 hover:bg-slate-300 flex items-center justify-center transition-colors duration-200"
onclick="decreaseQuantity(<%= id %>)">
<span class="text-slate-600">-</span>
</button>
<input type="number"
id="quantity_<%= id %>"
min="0"
max="<%= remaining %>"
value="0"
class="w-12 text-center border border-slate-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
data-name="<%= name %>"
data-price="<%= price_cents %>"
onchange="updateCart(<%= id %>, this.dataset.name, this.dataset.price)">
<button type="button"
class="w-8 h-8 rounded-full bg-slate-200 hover:bg-slate-300 flex items-center justify-center transition-colors duration-200"
onclick="increaseQuantity(<%= id %>, <%= remaining %>)">
<span class="text-slate-600">+</span>
</button>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -24,9 +24,11 @@
<%= render "components/header" %>
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flash mx-auto px-4 sm:px-6 lg:px-8 py-8">
<%= render "shared/flash_messages" %>
</div>
<% if flash.any? %>
<div class="flash mx-auto px-4 sm:px-6 lg:px-8 py-8">
<%= render "shared/flash_messages" %>
</div>
<% end %>
<div class="yield">
<%= yield %>

View File

@@ -1,68 +1,147 @@
<div class="container mt-4">
<div class="row">
<div class="col-md-8">
<h1><%= @party.name %></h1>
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-party-id-value="<%= params[:id] %>">
<div class="max-w-7xl mx-auto md:px-4">
<% if @party.image.present? %>
<%= image_tag @party.image, class: "img-fluid rounded mb-3" %>
<% end %>
<nav class="mb-3 text-sm" aria-label="Breadcrumb">
<nav class="mb-3 text-sm" aria-label="Breadcrumb" role="navigation">
<span class="flex items-center text-slate-700" role="list">
<a href="/" class="hover:text-primary-600 transition-colors duration-200 flex items-center" role="listitem">
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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>
<span class="sr-only">Home</span>
</a>
<p><%= @party.description %></p>
<svg class="w-4 h-4 mx-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Event Details</h5>
<p class="card-text">
<strong>Venue:</strong> <%= @party.venue_name %><br>
<strong>Address:</strong> <%= @party.venue_address %><br>
<strong>Start Time:</strong> <%= @party.start_time.strftime("%B %d, %Y at %I:%M %p") %><br>
<strong>End Time:</strong> <%= @party.end_time.strftime("%B %d, %Y at %I:%M %p") %>
</p>
<a href="/parties" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
Events
</a>
<svg class="w-4 h-4 mx-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<span class="mx-2 font-medium truncate max-w-[150px] sm:max-w-[250px]" role="listitem" aria-current="page">
<%= @party.name %>
</span>
</span>
</nav>
</nav>
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 md:p-8 mb-6 sm:mb-8">
<div class="flex flex-col lg:flex-row gap-6 md:gap-8">
<!-- Left Column: Party Info & Image -->
<div class="w-full md:w-1/2">
<h1 class="text-4xl font-bold text-primary mb-4"><%= @party.name %></h1>
<% if @party.image.present? %>
<div class="relative rounded-2xl overflow-hidden mb-6">
<%= image_tag @party.image, class: "w-full h-96 object-cover" %>
<div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-50"></div>
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black">
<h2 class="text-2xl font-semibold text-white mb-2">Event Details</h2>
<div class="flex flex-wrap gap-4 text-white">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2 text-purple-400" 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"></path>
</svg>
<span><%= @party.venue_name %></span>
</div>
<div class="flex items-center">
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span><%= @party.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
</div>
</div>
</div>
</div>
<% end %>
<div class="space-y-4">
<div>
<h2 class="text-xl font-semibold text-primary mb-2">Description</h2>
<p class="text-lg text-slate-600 leading-relaxed"><%= @party.description %></p>
</div>
<div class="space-y-3">
<div class="flex items-center space-x-4">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="font-medium text-slate-800">Location:</span>
<span class="text-slate-600"><%= @party.venue_address %></span>
</div>
<div class="flex items-center space-x-4">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span class="font-medium text-slate-800">Date:</span>
<span class="text-slate-600"><%= @party.start_time.strftime("%B %d, %Y") %></span>
</div>
</div>
</div>
</div>
<!-- Right Column: Ticket Selection -->
<div class="w-full md:w-1/2">
<div class="space-y-6">
<h2 class="text-2xl font-bold text-slate-800 mb-6">Available Tickets</h2>
<div class="space-y-4">
<% @party.ticket_types.each do |ticket_type| %>
<% sold_out = ticket_type.quantity <= ticket_type.tickets.count %>
<% remaining = ticket_type.quantity - ticket_type.tickets.count %>
<%= render 'components/ticket_card', {
id: ticket_type.id,
name: ticket_type.name,
description: ticket_type.description,
price_cents: ticket_type.price_cents,
quantity: ticket_type.quantity,
sold_out: sold_out,
remaining: remaining
} %>
<% end %>
<!-- Example of a sold out ticket type for demo purposes -->
<%= render 'components/ticket_card', {
name: "Early Bird Special",
description: "Limited time offer - discounted price for early purchasers",
price_cents: 1999,
quantity: 100,
sold_out: true,
remaining: 0
} %>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="space-y-6">
<!-- Fake Ticket Types -->
<div class="card hover-lift">
<div class="card-body p-6">
<h5 class="card-title text-lg font-bold mb-4">Early Bird</h5>
<p class="text-slate-600 mb-4">€49.99 - 30 min early access</p>
<div class="flex items-center space-x-4">
<span class="text-sm font-medium">Quantity:</span>
<input type="number" min="0" max="10" value="0" class="form-input w-20 hover-glow focus-ring" />
</div>
</div>
</div>
<div class="card hover-lift">
<div class="card-body p-6">
<h5 class="card-title text-lg font-bold mb-4">Standard</h5>
<p class="text-slate-600 mb-4">€29.99 - Regular access</p>
<div class="flex items-center space-x-4">
<span class="text-sm font-medium">Quantity:</span>
<input type="number" min="0" max="10" value="0" class="form-input w-20 hover-glow focus-ring" />
</div>
</div>
</div>
<div class="card hover-lift">
<div class="card-body p-6">
<h5 class="card-title text-lg font-bold mb-4">VIP</h5>
<p class="text-slate-600 mb-4">€99.99 - Premium access</p>
<div class="flex items-center space-x-4">
<span class="text-sm font-medium">Quantity:</span>
<input type="number" min="0" max="10" value="0" class="form-input w-20 hover-glow focus-ring" />
</div>
</div>
</div>
<div class="text-center">
<button type="button" class="btn-primary w-full sm:w-auto hover-lift focus-ring transition-normal">
Proceed to Checkout
</button>
<!-- Sticky Checkout Bar -->
<div class="sticky bottom-0 bg-white border-t border-slate-200 p-6">
<div class="max-w-md mx-auto">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="text-center sm:text-left">
<p class="text-sm text-slate-600">Total: <span id="cart-count" class="font-semibold">0</span> tickets</p>
<p class="text-xl font-bold text-primary">€<span id="cart-total">0.00</span></p>
</div>
<button id="checkout-btn"
class="btn-primary w-full sm:w-auto px-8 py-3 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"
disabled
onclick="proceedToCheckout()">
<span class="flex items-center justify-center">
<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="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
Continue to Checkout
</span>
</button>
</div>
</div>
</div>