develop #3
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
107
app/javascript/controllers/ticket_cart_controller.js
Normal file
107
app/javascript/controllers/ticket_cart_controller.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
@@ -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,10 +12,10 @@
|
||||
<!-- 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>
|
||||
@@ -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 %>
|
||||
|
||||
72
app/views/components/_ticket_card.html.erb
Normal file
72
app/views/components/_ticket_card.html.erb
Normal 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>
|
||||
@@ -24,9 +24,11 @@
|
||||
<%= render "components/header" %>
|
||||
|
||||
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<% 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 %>
|
||||
|
||||
@@ -1,70 +1,149 @@
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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? %>
|
||||
<%= image_tag @party.image, class: "img-fluid rounded mb-3" %>
|
||||
<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 %>
|
||||
|
||||
<p><%= @party.description %></p>
|
||||
<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="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>
|
||||
<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>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Right Column: Ticket Selection -->
|
||||
<div class="w-full md:w-1/2">
|
||||
<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" />
|
||||
<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="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" />
|
||||
<!-- 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>
|
||||
</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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ Rails.application.routes.draw do
|
||||
# Parties
|
||||
get "parties", to: "parties#index", as: "parties"
|
||||
get "parties/:slug.:id", to: "parties#show", as: "party"
|
||||
post "parties/:slug.:id/checkout", to: "parties#checkout", as: "party_checkout"
|
||||
|
||||
# Routes for devise authentication Gem
|
||||
# Bind devise to user
|
||||
|
||||
Reference in New Issue
Block a user