develop #3
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@
|
||||
|
||||
# Ignore generated reports
|
||||
/test/reports
|
||||
|
||||
# Ignore .fuse file
|
||||
.fuse_hidden*
|
||||
267
AGENT.md
Executable file
267
AGENT.md
Executable file
@@ -0,0 +1,267 @@
|
||||
# Aperonight - Technical Documentation for AI Agents
|
||||
|
||||
## 🤖 Agent Implementation Guide
|
||||
|
||||
This document provides technical details for AI agents working on the Aperonight ticket selling system.
|
||||
|
||||
## 🏗️ System Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
#### 1. User Management (`app/models/user.rb`)
|
||||
- **Devise Integration**: Complete authentication system with registration, login, password reset
|
||||
- **Relationships**: Users can create events and purchase tickets
|
||||
- **Validations**: Email format, password strength, optional name fields
|
||||
|
||||
#### 2. Event System (`app/models/event.rb`)
|
||||
- **States**: `draft`, `published`, `canceled`, `sold_out` with enum management
|
||||
- **Geographic Data**: Latitude/longitude for venue mapping
|
||||
- **Relationships**: Belongs to user, has many ticket types and tickets through ticket types
|
||||
- **Scopes**: Featured events, published events, upcoming events with proper ordering
|
||||
|
||||
#### 3. Ticket Management
|
||||
- **TicketType** (`app/models/ticket_type.rb`): Defines ticket categories with pricing, quantity, sale periods
|
||||
- **Ticket** (`app/models/ticket.rb`): Individual tickets with unique QR codes, status tracking, price storage
|
||||
|
||||
#### 4. Payment Processing (`app/controllers/events_controller.rb`)
|
||||
- **Stripe Integration**: Complete checkout session creation and payment confirmation
|
||||
- **Session Management**: Proper handling of payment success/failure with ticket generation
|
||||
- **Security**: Authentication required, cart validation, availability checking
|
||||
|
||||
### Database Schema Key Points
|
||||
|
||||
```sql
|
||||
-- Users table (managed by Devise)
|
||||
CREATE TABLE users (
|
||||
id bigint PRIMARY KEY,
|
||||
email varchar(255) UNIQUE NOT NULL,
|
||||
encrypted_password varchar(255) NOT NULL,
|
||||
first_name varchar(255),
|
||||
last_name varchar(255),
|
||||
-- Devise fields: confirmation, reset tokens, etc.
|
||||
);
|
||||
|
||||
-- Events table
|
||||
CREATE TABLE events (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
name varchar(100) NOT NULL,
|
||||
slug varchar(100) NOT NULL,
|
||||
description text(1000) NOT NULL,
|
||||
venue_name varchar(100) NOT NULL,
|
||||
venue_address varchar(200) NOT NULL,
|
||||
latitude decimal(10,8) NOT NULL,
|
||||
longitude decimal(11,8) NOT NULL,
|
||||
start_time datetime NOT NULL,
|
||||
end_time datetime,
|
||||
state integer DEFAULT 0, -- enum: draft=0, published=1, canceled=2, sold_out=3
|
||||
featured boolean DEFAULT false,
|
||||
image varchar(500)
|
||||
);
|
||||
|
||||
-- Ticket types define pricing and availability
|
||||
CREATE TABLE ticket_types (
|
||||
id bigint PRIMARY KEY,
|
||||
event_id bigint REFERENCES events(id),
|
||||
name varchar(255) NOT NULL,
|
||||
description text,
|
||||
price_cents integer NOT NULL,
|
||||
quantity integer NOT NULL,
|
||||
sale_start_at datetime,
|
||||
sale_end_at datetime,
|
||||
requires_id boolean DEFAULT false,
|
||||
minimum_age integer
|
||||
);
|
||||
|
||||
-- Individual tickets with QR codes
|
||||
CREATE TABLE tickets (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users(id),
|
||||
ticket_type_id bigint REFERENCES ticket_types(id),
|
||||
qr_code varchar(255) UNIQUE NOT NULL,
|
||||
price_cents integer NOT NULL,
|
||||
status varchar(255) DEFAULT 'active' -- active, used, expired, refunded
|
||||
);
|
||||
```
|
||||
|
||||
## 🎯 Key Implementation Details
|
||||
|
||||
### 1. Dashboard Metrics (`app/controllers/pages_controller.rb`)
|
||||
|
||||
```ruby
|
||||
# User-specific metrics with optimized queries
|
||||
@booked_events = current_user.tickets
|
||||
.joins(:ticket_type, :event)
|
||||
.where(events: { state: :published })
|
||||
.count
|
||||
|
||||
# Event counts for different timeframes
|
||||
@events_today = Event.published
|
||||
.where("DATE(start_time) = ?", Date.current)
|
||||
.count
|
||||
|
||||
# User's actual booked events (not just count)
|
||||
@user_booked_events = Event.joins(ticket_types: :tickets)
|
||||
.where(tickets: { user: current_user, status: 'active' })
|
||||
.distinct
|
||||
.limit(5)
|
||||
```
|
||||
|
||||
### 2. Stripe Payment Flow
|
||||
|
||||
#### Checkout Initiation (`events#checkout`)
|
||||
1. **Cart Validation**: Parse JSON cart data, validate ticket types and quantities
|
||||
2. **Availability Check**: Ensure sufficient tickets available before payment
|
||||
3. **Stripe Session**: Create checkout session with line items, success/cancel URLs
|
||||
4. **Metadata Storage**: Store order details in Stripe session metadata for later retrieval
|
||||
|
||||
```ruby
|
||||
# Key Stripe configuration
|
||||
session = Stripe::Checkout::Session.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: line_items,
|
||||
mode: 'payment',
|
||||
success_url: payment_success_url(event_id: @event.id, 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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Payment Confirmation (`events#payment_success`)
|
||||
1. **Session Retrieval**: Get Stripe session with payment status
|
||||
2. **Ticket Creation**: Generate tickets based on order items from metadata
|
||||
3. **QR Code Generation**: Automatic unique QR code creation via model callbacks
|
||||
4. **Success Page**: Display tickets with download links
|
||||
|
||||
### 3. PDF Ticket Generation (`app/services/ticket_pdf_generator.rb`)
|
||||
|
||||
```ruby
|
||||
class TicketPdfGenerator
|
||||
def generate
|
||||
Prawn::Document.new(page_size: [350, 600], margin: 20) do |pdf|
|
||||
# Header with branding
|
||||
pdf.fill_color "2D1B69"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "ApéroNight", align: :center
|
||||
|
||||
# Event details
|
||||
pdf.text ticket.event.name, align: :center
|
||||
|
||||
# QR Code generation
|
||||
qr_code_data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.event.id,
|
||||
user_id: ticket.user.id
|
||||
}.to_json
|
||||
|
||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||
end.render
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 4. Frontend Cart Management (`app/javascript/controllers/ticket_cart_controller.js`)
|
||||
|
||||
- **Stimulus Controller**: Manages cart state and interactions
|
||||
- **Authentication Check**: Validates user login before checkout
|
||||
- **Session Storage**: Preserves cart when redirecting to login
|
||||
- **Dynamic Updates**: Real-time cart total and ticket count updates
|
||||
|
||||
## 🔧 Development Patterns
|
||||
|
||||
### Model Validations
|
||||
```ruby
|
||||
# Event validations
|
||||
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
|
||||
validates :latitude, numericality: {
|
||||
greater_than_or_equal_to: -90,
|
||||
less_than_or_equal_to: 90
|
||||
}
|
||||
|
||||
# Ticket QR code generation
|
||||
before_validation :generate_qr_code, on: :create
|
||||
def generate_qr_code
|
||||
loop do
|
||||
self.qr_code = SecureRandom.uuid
|
||||
break unless Ticket.exists?(qr_code: qr_code)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Controller Patterns
|
||||
```ruby
|
||||
# Authentication for sensitive actions
|
||||
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||
|
||||
# Strong parameters
|
||||
private
|
||||
def event_params
|
||||
params.require(:event).permit(:name, :description, :venue_name, :venue_address,
|
||||
:latitude, :longitude, :start_time, :image)
|
||||
end
|
||||
```
|
||||
|
||||
### View Helpers and Partials
|
||||
- **Metric Cards**: Reusable component for dashboard statistics
|
||||
- **Event Items**: Consistent event display across pages
|
||||
- **Flash Messages**: Centralized notification system
|
||||
|
||||
## 🚀 Deployment Considerations
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Required for production
|
||||
STRIPE_PUBLISHABLE_KEY=pk_live_...
|
||||
STRIPE_SECRET_KEY=sk_live_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
DATABASE_URL=mysql2://user:pass@host/db
|
||||
RAILS_MASTER_KEY=...
|
||||
```
|
||||
|
||||
### Database Indexes
|
||||
```sql
|
||||
-- Performance indexes for common queries
|
||||
CREATE INDEX idx_events_published_start_time ON events (state, start_time);
|
||||
CREATE INDEX idx_tickets_user_status ON tickets (user_id, status);
|
||||
CREATE INDEX idx_ticket_types_event ON ticket_types (event_id);
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
- **CSRF Protection**: Rails default protection enabled
|
||||
- **Strong Parameters**: All user inputs filtered
|
||||
- **Authentication**: Devise handles session security
|
||||
- **Payment Security**: Stripe handles sensitive payment data
|
||||
|
||||
## 🧪 Testing Strategy
|
||||
|
||||
### Key Test Cases
|
||||
1. **User Authentication**: Registration, login, logout flows
|
||||
2. **Event Creation**: Validation, state management, relationships
|
||||
3. **Booking Process**: Cart validation, payment processing, ticket generation
|
||||
4. **PDF Generation**: QR code uniqueness, ticket format
|
||||
5. **Dashboard Metrics**: Query accuracy, performance
|
||||
|
||||
### Seed Data Structure
|
||||
```ruby
|
||||
# Creates test users, events, and ticket types
|
||||
users = User.create!([...])
|
||||
events = Event.create!([...])
|
||||
ticket_types = TicketType.create!([...])
|
||||
```
|
||||
|
||||
## 📝 Code Style & Conventions
|
||||
|
||||
- **Ruby Style**: Follow Rails conventions and Rubocop rules
|
||||
- **Database**: Use Rails migrations for all schema changes
|
||||
- **JavaScript**: Stimulus controllers for interactive behavior
|
||||
- **CSS**: Tailwind utility classes with custom components
|
||||
- **Documentation**: Inline comments for complex business logic
|
||||
|
||||
This architecture provides a solid foundation for a scalable ticket selling platform with proper separation of concerns, security, and user experience.
|
||||
26
Gemfile.lock
26
Gemfile.lock
@@ -96,6 +96,7 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crass (1.0.6)
|
||||
@@ -181,7 +182,6 @@ GEM
|
||||
minitest (>= 5.0)
|
||||
ruby-progressbar
|
||||
msgpack (1.8.0)
|
||||
mysql2 (0.5.6)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
@@ -215,8 +215,16 @@ GEM
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-core (0.10.0)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prawn (2.5.0)
|
||||
matrix (~> 0.4)
|
||||
pdf-core (~> 0.10.0)
|
||||
ttfunk (~> 1.8)
|
||||
prawn-qrcode (0.5.2)
|
||||
prawn (>= 1)
|
||||
rqrcode (>= 1.0.0)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
propshaft (1.2.1)
|
||||
@@ -279,8 +287,12 @@ GEM
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.1)
|
||||
rubocop (1.80.0)
|
||||
rexml (3.4.2)
|
||||
rqrcode (3.1.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.0.0)
|
||||
rubocop (1.80.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -349,11 +361,14 @@ GEM
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
stripe (15.5.0)
|
||||
thor (1.4.0)
|
||||
thruster (0.1.15)
|
||||
thruster (0.1.15-aarch64-linux)
|
||||
thruster (0.1.15-x86_64-linux)
|
||||
timeout (0.4.3)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
@@ -402,10 +417,12 @@ DEPENDENCIES
|
||||
kaminari (~> 1.2)
|
||||
kaminari-tailwind (~> 0.1.0)
|
||||
minitest-reporters (~> 1.7)
|
||||
mysql2 (~> 0.5)
|
||||
prawn (~> 2.5)
|
||||
prawn-qrcode (~> 0.5)
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
rails (~> 8.0.2, >= 8.0.2.1)
|
||||
rqrcode (~> 3.1)
|
||||
rubocop-rails-omakase
|
||||
selenium-webdriver
|
||||
solid_cable
|
||||
@@ -413,6 +430,7 @@ DEPENDENCIES
|
||||
solid_queue
|
||||
sqlite3 (~> 2.7)
|
||||
stimulus-rails
|
||||
stripe (~> 15.5)
|
||||
thruster
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
|
||||
205
README.md
205
README.md
@@ -4,76 +4,209 @@
|
||||
|
||||
## 🌃 Overview
|
||||
|
||||
**Aperonight** is a two-sided marketplace connecting event-goers with nightlife promoters in Paris. The platform allows:
|
||||
|
||||
- **Customers** to discover/book tickets for upcoming events
|
||||
- **Promoters** to create/manage events and validate tickets at venue entrances
|
||||
**Aperonight** is a comprehensive ticket selling system that connects event-goers with event organizers. The platform provides a complete solution for event booking, payment processing, and ticket management.
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### For Event-Goers
|
||||
✔ Browse upcoming events with filters (date, location, music genre)
|
||||
✔ Book tickets with multiple bundle options (VIP, group passes, etc.)
|
||||
✔ Secure payment processing (credit cards, Apple/Google Pay)
|
||||
✔ Mobile-friendly e-tickets with QR codes
|
||||
✅ **User Dashboard** - Personalized metrics showing booked events, upcoming events, and event statistics
|
||||
✅ **Event Discovery** - Browse upcoming events with detailed information and venue details
|
||||
✅ **Secure Booking** - Multiple ticket types per event with quantity selection
|
||||
✅ **Stripe Integration** - Secure payment processing with credit/debit cards
|
||||
✅ **PDF Tickets** - Automatically generated tickets with unique QR codes for each purchase
|
||||
✅ **Download System** - Instant PDF ticket downloads after successful payment
|
||||
|
||||
### For Promoters
|
||||
✔ Event creation dashboard with ticket type customization
|
||||
✔ Real-time ticket validation via mobile scanning
|
||||
✔ Sales analytics and attendee tracking
|
||||
✔ Automatic aggregation of events from partner platforms
|
||||
### For Event Organizers
|
||||
✅ **Event Management** - Create and manage events with detailed information
|
||||
✅ **Ticket Type Configuration** - Set up multiple ticket types with different pricing
|
||||
✅ **Sales Tracking** - Monitor ticket sales and availability
|
||||
✅ **User Authentication** - Secure user registration and login system
|
||||
|
||||
### Technical Implementation
|
||||
✅ **Payment Processing** - Full Stripe Checkout integration with session management
|
||||
✅ **PDF Generation** - Custom PDF tickets with QR codes using Prawn library
|
||||
✅ **Responsive Design** - Mobile-friendly interface with Tailwind CSS
|
||||
✅ **Database Relations** - Proper user-event-ticket relationships
|
||||
|
||||
## 🛠 Technical Stack
|
||||
|
||||
### Backend
|
||||
- **Ruby on Rails 7** (API mode)
|
||||
- **MariaDB** database
|
||||
<!--- **Redis** for caching/background jobs-->
|
||||
- **ActiveJob** for background processing
|
||||
- **Ruby on Rails 8.0+** with Hotwire for reactive UI
|
||||
- **MySQL** database with comprehensive migrations
|
||||
- **Devise** for user authentication and session management
|
||||
- **Kaminari** for pagination
|
||||
|
||||
### Frontend
|
||||
- **Hotwire (Turbo + Stimulus)** for reactive UI
|
||||
- **Tailwind CSS** for styling
|
||||
- **React Native** for promoter mobile app (ticket scanning)
|
||||
- **Hotwire (Turbo + Stimulus)** for interactive JavaScript behavior
|
||||
- **Tailwind CSS** for responsive styling and modern UI
|
||||
- **JavaScript Controllers** for cart management and checkout flow
|
||||
|
||||
### Key Integrations
|
||||
- **Stripe Connect** for payments & promoter payouts
|
||||
- **Shogun/Bizouk/Weezevent APIs** for event aggregation
|
||||
<!--- **Twilio** for SMS ticket delivery-->
|
||||
<!--- **AWS S3** for media storage-->
|
||||
- **Stripe** for secure payment processing and checkout sessions
|
||||
- **Prawn & Prawn-QRCode** for PDF ticket generation
|
||||
- **RQRCode** for unique QR code generation per ticket
|
||||
|
||||
## 📊 Database Schema (Simplified)
|
||||
## 📊 Database Schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ BOOKING : makes
|
||||
USER ||--o{ EVENT : creates
|
||||
USER ||--o{ TICKET : purchases
|
||||
USER {
|
||||
integer id
|
||||
string email
|
||||
string encrypted_password
|
||||
}
|
||||
PROMOTER ||--o{ EVENT : creates
|
||||
PROMOTER {
|
||||
integer id
|
||||
string stripe_account_id
|
||||
string first_name
|
||||
string last_name
|
||||
}
|
||||
EVENT ||--o{ TICKET_TYPE : has
|
||||
EVENT {
|
||||
integer id
|
||||
integer user_id
|
||||
string name
|
||||
string slug
|
||||
text description
|
||||
string venue_name
|
||||
string venue_address
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
datetime start_time
|
||||
}
|
||||
BOOKING ||--o{ TICKET : generates
|
||||
BOOKING {
|
||||
integer id
|
||||
decimal total_price
|
||||
datetime end_time
|
||||
string state
|
||||
boolean featured
|
||||
string image
|
||||
}
|
||||
TICKET_TYPE ||--o{ TICKET : defines
|
||||
TICKET_TYPE {
|
||||
integer id
|
||||
integer event_id
|
||||
string name
|
||||
text description
|
||||
integer price_cents
|
||||
integer quantity
|
||||
datetime sale_start_at
|
||||
datetime sale_end_at
|
||||
boolean requires_id
|
||||
integer minimum_age
|
||||
}
|
||||
TICKET {
|
||||
integer id
|
||||
integer user_id
|
||||
integer ticket_type_id
|
||||
string qr_code
|
||||
integer price_cents
|
||||
string status
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Ruby 3.4+
|
||||
- Rails 8.0+
|
||||
- MySQL/MariaDB
|
||||
- Node.js 18+ (for asset compilation)
|
||||
- Stripe account (for payment processing)
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone https://github.com/yourusername/aperonight.git
|
||||
cd aperonight
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
bundle install
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Database setup**
|
||||
```bash
|
||||
rails db:create
|
||||
rails db:migrate
|
||||
rails db:seed
|
||||
```
|
||||
|
||||
4. **Configure environment variables**
|
||||
Create a `.env` file or configure Rails credentials:
|
||||
```bash
|
||||
# Stripe configuration
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here
|
||||
STRIPE_SECRET_KEY=sk_test_your_key_here
|
||||
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
|
||||
|
||||
# Database configuration (if not using defaults)
|
||||
DATABASE_URL=mysql2://username:password@localhost/aperonight_development
|
||||
```
|
||||
|
||||
5. **Start the development server**
|
||||
```bash
|
||||
rails server
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see the application running.
|
||||
|
||||
## 💳 Payment Configuration
|
||||
|
||||
### Setting up Stripe
|
||||
|
||||
1. Create a Stripe account at [stripe.com](https://stripe.com)
|
||||
2. Get your API keys from the Stripe Dashboard
|
||||
3. Add your keys to the Rails credentials or environment variables
|
||||
4. Configure webhook endpoints for payment confirmations:
|
||||
- Endpoint URL: `your-domain.com/stripe/webhooks`
|
||||
- Events: `checkout.session.completed`, `payment_intent.succeeded`
|
||||
|
||||
## 🎫 Core Functionality
|
||||
|
||||
### User Flow
|
||||
1. **Registration/Login** - Users create accounts or sign in
|
||||
2. **Event Discovery** - Browse events from the homepage or events page
|
||||
3. **Ticket Selection** - Choose ticket types and quantities
|
||||
4. **Checkout** - Secure payment through Stripe Checkout
|
||||
5. **Ticket Generation** - Automatic PDF ticket generation with QR codes
|
||||
6. **Download** - Instant ticket download after payment
|
||||
|
||||
### Event Management
|
||||
1. **Event Creation** - Create events with full details and images
|
||||
2. **Ticket Types** - Configure multiple ticket types with pricing
|
||||
3. **Sales Tracking** - Monitor ticket sales through the dashboard
|
||||
|
||||
### Dashboard Features
|
||||
- **Personal Metrics** - View booked events and upcoming events
|
||||
- **Event Sections** - Today's events, tomorrow's events, and upcoming events
|
||||
- **Quick Actions** - Easy navigation to event discovery and booking
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Key Files Structure
|
||||
```
|
||||
app/
|
||||
├── controllers/
|
||||
│ ├── events_controller.rb # Event listing, booking, checkout
|
||||
│ └── pages_controller.rb # Dashboard and static pages
|
||||
├── models/
|
||||
│ ├── user.rb # User authentication with Devise
|
||||
│ ├── event.rb # Event management and states
|
||||
│ ├── ticket_type.rb # Ticket configuration
|
||||
│ └── ticket.rb # Ticket generation with QR codes
|
||||
├── services/
|
||||
│ └── ticket_pdf_generator.rb # PDF ticket generation service
|
||||
└── views/
|
||||
├── events/
|
||||
│ ├── show.html.erb # Event details and booking
|
||||
│ └── payment_success.html.erb # Post-purchase confirmation
|
||||
└── pages/
|
||||
└── dashboard.html.erb # User dashboard with metrics
|
||||
```
|
||||
|
||||
### Key Routes
|
||||
- `GET /` - Homepage
|
||||
- `GET /dashboard` - User dashboard (authenticated)
|
||||
- `GET /events` - Event listings
|
||||
- `GET /events/:slug.:id` - Event details and booking
|
||||
- `POST /events/:slug.:id/checkout` - Stripe checkout initiation
|
||||
- `GET /payment/success` - Payment confirmation
|
||||
- `GET /tickets/:ticket_id/download` - PDF ticket download
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
class EventsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||
before_action :set_event, only: [:show, :checkout]
|
||||
|
||||
# Display all events
|
||||
def index
|
||||
@events = Event.includes(:user).upcoming.page(params[:page]).per(1)
|
||||
# @events = Event.page(params[:page]).per(12)
|
||||
@events = Event.includes(:user).upcoming.page(params[:page]).per(12)
|
||||
end
|
||||
|
||||
# Display desired event
|
||||
def show
|
||||
@event = Event.find(params[:id])
|
||||
# Event is set by set_event callback
|
||||
end
|
||||
|
||||
# Handle checkout process
|
||||
# Handle checkout process - Create Stripe session
|
||||
def checkout
|
||||
@event = Event.find(params[:id])
|
||||
cart_data = JSON.parse(params[:cart] || "{}")
|
||||
|
||||
if cart_data.empty?
|
||||
redirect_to event_path(@event), alert: "Please select at least one ticket"
|
||||
redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet"
|
||||
return
|
||||
end
|
||||
|
||||
# Create order items from cart
|
||||
line_items = []
|
||||
order_items = []
|
||||
total_amount = 0
|
||||
|
||||
@@ -34,12 +36,27 @@ class EventsController < ApplicationController
|
||||
# Check availability
|
||||
available = ticket_type.quantity - ticket_type.tickets.count
|
||||
if quantity > available
|
||||
redirect_to event_path(@event), alert: "Not enough tickets available for #{ticket_type.name}"
|
||||
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: ticket_type,
|
||||
ticket_type_id: ticket_type.id,
|
||||
ticket_type_name: ticket_type.name,
|
||||
quantity: quantity,
|
||||
price_cents: ticket_type.price_cents
|
||||
}
|
||||
@@ -48,17 +65,90 @@ class EventsController < ApplicationController
|
||||
end
|
||||
|
||||
if order_items.empty?
|
||||
redirect_to event_path(@event), alert: "Invalid order"
|
||||
redirect_to event_path(@event.slug, @event), alert: "Commande invalide"
|
||||
return
|
||||
end
|
||||
|
||||
# Here you would typically:
|
||||
# 1. Create an Order record
|
||||
# 2. Create Ticket records for each item
|
||||
# 3. Redirect to payment processing
|
||||
begin
|
||||
# Create Stripe Checkout Session
|
||||
session = Stripe::Checkout::Session.create({
|
||||
payment_method_types: ['card'],
|
||||
line_items: line_items,
|
||||
mode: 'payment',
|
||||
success_url: payment_success_url(event_id: @event.id, 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
|
||||
}
|
||||
})
|
||||
|
||||
# For now, we'll just redirect with a success message
|
||||
# In a real app, you'd redirect to a payment page
|
||||
redirect_to event_path(@event), notice: "Order created successfully! Proceeding to payment..."
|
||||
redirect_to session.url, allow_other_host: true
|
||||
rescue Stripe::StripeError => e
|
||||
redirect_to event_path(@event.slug, @event), alert: "Erreur de traitement du paiement : #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Handle successful payment
|
||||
def payment_success
|
||||
session_id = params[:session_id]
|
||||
event_id = params[:event_id]
|
||||
|
||||
begin
|
||||
session = Stripe::Checkout::Session.retrieve(session_id)
|
||||
|
||||
if session.payment_status == 'paid'
|
||||
# Create tickets
|
||||
@event = Event.find(event_id)
|
||||
order_items = JSON.parse(session.metadata['order_items'])
|
||||
@tickets = []
|
||||
|
||||
order_items.each do |item|
|
||||
ticket_type = TicketType.find(item['ticket_type_id'])
|
||||
item['quantity'].times do
|
||||
ticket = Ticket.create!(
|
||||
user: current_user,
|
||||
ticket_type: ticket_type,
|
||||
status: 'active'
|
||||
)
|
||||
@tickets << ticket
|
||||
|
||||
# Send confirmation email for each ticket
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
end
|
||||
end
|
||||
|
||||
render 'payment_success'
|
||||
else
|
||||
redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès"
|
||||
end
|
||||
rescue Stripe::StripeError => e
|
||||
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{e.message}"
|
||||
rescue => e
|
||||
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{e.message}"
|
||||
end
|
||||
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
|
||||
|
||||
def set_event
|
||||
@event = Event.find(params[:id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,8 +18,19 @@ class PagesController < ApplicationController
|
||||
# User dashboard showing personalized content
|
||||
# Accessible only to authenticated users
|
||||
def dashboard
|
||||
@available_events = Event.published.count
|
||||
@events_this_week = Event.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
|
||||
# Metrics for dashboard cards
|
||||
@booked_events = current_user.tickets.joins(:ticket_type, :event).where(events: { state: :published }).count
|
||||
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
||||
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
||||
@upcoming_events = Event.published.upcoming.count
|
||||
|
||||
# User's booked events
|
||||
@user_booked_events = Event.joins(ticket_types: :tickets)
|
||||
.where(tickets: { user: current_user, status: 'active' })
|
||||
.distinct
|
||||
.limit(5)
|
||||
|
||||
# Events sections
|
||||
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [Date.current, Date.current + 1]).order(start_time: :asc).page(params[:page])
|
||||
|
||||
@@ -38,14 +38,14 @@ export default class extends Controller {
|
||||
credentials: "same-origin",
|
||||
})
|
||||
.then((response) => {
|
||||
// console.log(this.element.dataset.loginUrlValue); // By default, we does not return anything.
|
||||
// console.log(this.element.dataset.redirectUrlValue); // By default, we does not return anything.
|
||||
|
||||
// By default the response does not include any url.
|
||||
// Redirect to default login page (loginUrlValue)
|
||||
// Redirect to default login page (redirectUrlValue)
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else if (this.element.dataset.loginUrlValue) {
|
||||
window.location.href = this.element.dataset.loginUrlValue;
|
||||
} else if (this.element.dataset.redirectUrlValue) {
|
||||
window.location.href = this.element.dataset.redirectUrlValue;
|
||||
return;
|
||||
}
|
||||
window.location.href = "/";
|
||||
|
||||
@@ -7,11 +7,14 @@ export default class extends Controller {
|
||||
connect() {
|
||||
this.cart = {}
|
||||
this.updateCartDisplay()
|
||||
|
||||
// Check for pending cart in session storage (after login)
|
||||
this.checkForPendingCart()
|
||||
}
|
||||
|
||||
increaseQuantity(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
||||
const max = parseInt(event.currentTarget.dataset.max)
|
||||
const ticketTypeId = event.params.ticketTypeId
|
||||
const max = parseInt(event.params.max)
|
||||
const input = this.quantityTargetFor(ticketTypeId)
|
||||
|
||||
const current = parseInt(input.value) || 0
|
||||
@@ -22,7 +25,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
decreaseQuantity(event) {
|
||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
||||
const ticketTypeId = event.params.ticketTypeId
|
||||
const input = this.quantityTargetFor(ticketTypeId)
|
||||
|
||||
const current = parseInt(input.value) || 0
|
||||
@@ -32,6 +35,22 @@ export default class extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
updateQuantityFromInput(event) {
|
||||
const input = event.target
|
||||
const ticketTypeId = input.dataset.ticketTypeId
|
||||
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)
|
||||
@@ -59,27 +78,53 @@ export default class extends Controller {
|
||||
totalPrice += (this.cart[ticketTypeId].price * this.cart[ticketTypeId].quantity) / 100
|
||||
}
|
||||
|
||||
this.cartCountTarget.textContent = totalTickets
|
||||
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
|
||||
// Update cart count and total
|
||||
if (this.hasCartCountTarget) {
|
||||
this.cartCountTarget.textContent = totalTickets
|
||||
}
|
||||
|
||||
const checkoutBtn = this.checkoutButtonTarget
|
||||
if (totalTickets > 0) {
|
||||
checkoutBtn.disabled = false
|
||||
} else {
|
||||
checkoutBtn.disabled = true
|
||||
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) {
|
||||
alert('Please select at least one ticket')
|
||||
alert('Veuillez sélectionner au moins un billet')
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = document.body.dataset.userAuthenticated === "true"
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (confirm('Vous devez être connecté pour acheter des billets. Souhaitez-vous vous connecter maintenant ?')) {
|
||||
// Store cart in session storage
|
||||
sessionStorage.setItem('pending_cart', JSON.stringify({
|
||||
eventId: this.eventIdValue,
|
||||
cart: this.cart
|
||||
}))
|
||||
window.location.href = '/auth/sign_in'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create form and submit to checkout
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = `/events/${this.eventIdValue}/checkout`
|
||||
form.style.display = 'none'
|
||||
form.action = `/events/${document.body.dataset.eventSlug}.${this.eventIdValue}/checkout`
|
||||
|
||||
// Add CSRF token
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||
@@ -100,6 +145,31 @@ export default class extends Controller {
|
||||
form.submit()
|
||||
}
|
||||
|
||||
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) {
|
||||
return document.querySelector(`#quantity_${ticketTypeId}`)
|
||||
|
||||
21
app/mailers/ticket_mailer.rb
Executable file
21
app/mailers/ticket_mailer.rb
Executable file
@@ -0,0 +1,21 @@
|
||||
class TicketMailer < ApplicationMailer
|
||||
default from: 'notifications@aperonight.com'
|
||||
|
||||
def purchase_confirmation(ticket)
|
||||
@ticket = ticket
|
||||
@user = ticket.user
|
||||
@event = ticket.event
|
||||
|
||||
# Generate PDF attachment
|
||||
pdf = @ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: 'application/pdf',
|
||||
content: pdf
|
||||
}
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}"
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,17 @@ class Ticket < ApplicationRecord
|
||||
validates :status, presence: true, inclusion: { in: %w[active used expired refunded] }
|
||||
|
||||
before_validation :set_price_from_ticket_type, on: :create
|
||||
before_validation :generate_qr_code, on: :create
|
||||
|
||||
# Generate PDF ticket
|
||||
def to_pdf
|
||||
TicketPdfGenerator.new(self).generate
|
||||
end
|
||||
|
||||
# Price in euros (formatted)
|
||||
def price_euros
|
||||
price_cents / 100.0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -19,4 +30,13 @@ class Ticket < ApplicationRecord
|
||||
return unless ticket_type
|
||||
self.price_cents = ticket_type.price_cents
|
||||
end
|
||||
|
||||
def generate_qr_code
|
||||
return if qr_code.present?
|
||||
|
||||
loop do
|
||||
self.qr_code = SecureRandom.uuid
|
||||
break unless Ticket.exists?(qr_code: qr_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
97
app/services/ticket_pdf_generator.rb
Executable file
97
app/services/ticket_pdf_generator.rb
Executable file
@@ -0,0 +1,97 @@
|
||||
require 'prawn'
|
||||
require 'prawn/qrcode'
|
||||
require 'rqrcode'
|
||||
|
||||
class TicketPdfGenerator
|
||||
attr_reader :ticket
|
||||
|
||||
def initialize(ticket)
|
||||
@ticket = ticket
|
||||
end
|
||||
|
||||
def generate
|
||||
Prawn::Document.new(page_size: [350, 600], margin: 20) do |pdf|
|
||||
# Header
|
||||
pdf.fill_color "2D1B69"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "ApéroNight", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Event name
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", style: :bold, size: 18
|
||||
pdf.text ticket.event.name, align: :center
|
||||
pdf.move_down 20
|
||||
|
||||
# Ticket info box
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.fill_color "F9FAFB"
|
||||
pdf.rounded_rectangle [0, pdf.cursor], 310, 120, 10
|
||||
pdf.fill_and_stroke
|
||||
|
||||
pdf.move_down 10
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", size: 12
|
||||
|
||||
# Ticket details
|
||||
pdf.text "Ticket Type:", style: :bold
|
||||
pdf.text ticket.ticket_type.name
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.text "Price:", style: :bold
|
||||
pdf.text "€#{ticket.price_euros}"
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.text "Date & Time:", style: :bold
|
||||
pdf.text ticket.event.start_time.strftime("%B %d, %Y at %I:%M %p")
|
||||
pdf.move_down 20
|
||||
|
||||
# Venue information
|
||||
pdf.fill_color "374151"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Venue Information"
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.font "Helvetica", size: 11
|
||||
pdf.text ticket.event.venue_name, style: :bold
|
||||
pdf.text ticket.event.venue_address
|
||||
pdf.move_down 20
|
||||
|
||||
# QR Code
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Ticket QR Code", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
qr_code_data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.event.id,
|
||||
user_id: ticket.user.id
|
||||
}.to_json
|
||||
|
||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||
|
||||
pdf.move_down 15
|
||||
|
||||
# QR code text
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "QR Code: #{ticket.qr_code[0..7]}...", align: :center
|
||||
|
||||
# Footer
|
||||
pdf.move_down 30
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.horizontal_line 0, 310
|
||||
pdf.move_down 10
|
||||
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "This ticket is valid for one entry only.", align: :center
|
||||
pdf.text "Present this ticket at the venue entrance.", align: :center
|
||||
pdf.move_down 5
|
||||
pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center
|
||||
end.render
|
||||
end
|
||||
end
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
<div class="hidden sm:flex space-x-6">
|
||||
<%= link_to t("header.parties"), events_path,
|
||||
class: "bg-black text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
<%= link_to t("header.concerts"), "#",
|
||||
class: "bg-black text-gray-100 hover:text-purple-200 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
class: "text-gray-100 hover:text-purple-200 py-2 rounded-md text-sm font-medium transition-colors duration-200" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-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 },
|
||||
logout_url_value: destroy_user_session_path, redirect_url_value: "/", turbo: false },
|
||||
class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-700 last:rounded-b-md" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,66 +1,69 @@
|
||||
<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 class="bg-white rounded-xl border <%= sold_out ? "border-gray-200 opacity-75" : "border-purple-200" %> shadow-sm overflow-hidden">
|
||||
<div class="p-5">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<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>
|
||||
<h3 class="text-lg font-bold text-gray-900 <%= "text-gray-400" if sold_out %>"><%= name %></h3>
|
||||
<p class="text-gray-600 text-sm <%= "text-gray-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 %>">
|
||||
<p class="text-xl font-bold text-purple-700 <%= "text-gray-400" if sold_out %>">
|
||||
<%= number_to_currency(price_cents / 100.0, unit: "€") %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div>
|
||||
<p class="text-sm text-neutral-600 flex items-center">
|
||||
<% 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>
|
||||
<% 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">
|
||||
<svg class="-ml-0.5 mr-1 h-2 w-2 text-red-400" fill="currentColor" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
Épuisé
|
||||
</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">
|
||||
<svg class="-ml-0.5 mr-1 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
<%= remaining %> disponibles
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% unless sold_out %>
|
||||
<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>
|
||||
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||
data-action="click->ticket-cart#decreaseQuantity"
|
||||
data-ticket-cart-ticket-type-id-param="<%= id %>"
|
||||
data-ticket-cart-max-param="<%= remaining %>">
|
||||
<span class="text-gray-600 font-bold">-</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"
|
||||
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"
|
||||
data-ticket-cart-target="quantity"
|
||||
data-ticket-type-id="<%= id %>"
|
||||
data-name="<%= name %>"
|
||||
data-price="<%= price_cents %>"
|
||||
onchange="updateCart(<%= id %>, this.dataset.name, this.dataset.price)">
|
||||
data-price="<%= price_cents %>">
|
||||
<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>
|
||||
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||
data-action="click->ticket-cart#increaseQuantity"
|
||||
data-ticket-cart-ticket-type-id-param="<%= id %>"
|
||||
data-ticket-cart-max-param="<%= remaining %>">
|
||||
<span class="text-gray-600 font-bold">+</span>
|
||||
</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-sm text-slate-500 font-medium">
|
||||
<div class="text-sm text-gray-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
|
||||
Indisponible
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,91 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">Événements à venir</h1>
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Événements à venir</h1>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= @events.total_count %> événements trouvés
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @events.any? %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @events.each do |event| %>
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
<div class="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
||||
<% if event.image.present? %>
|
||||
<div class="h-48 overflow-hidden">
|
||||
<%= image_tag event.image, class: "w-full h-full object-cover" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="h-48 bg-gradient-to-r from-purple-500 to-indigo-600 flex items-center justify-center">
|
||||
<svg class="w-16 h-16 text-white opacity-80" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900"><%= event.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1"><%= event.user.email %></p>
|
||||
<h2 class="text-xl font-bold text-gray-900 line-clamp-1"><%= event.name %></h2>
|
||||
<p class="text-sm text-gray-500 mt-1 flex items-center">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<%= event.user.email.split('@').first %>
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<%= event.start_time.strftime("%d/%m/%Y") %>
|
||||
<%= event.start_time.strftime("%d/%m") %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<% if event.ticket_types.any? %>
|
||||
<p class="text-sm font-medium text-gray-900">
|
||||
À partir de <%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 flex items-center mt-1">
|
||||
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
</svg>
|
||||
<%= event.venue_name.truncate(20) %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">Pas de billets disponibles</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to "Voir les détails", event_path(event.slug, event), class: "inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-full shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" %>
|
||||
<%= link_to event_path(event.slug, event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm 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 %>
|
||||
Détails
|
||||
<svg class="w-4 h-4 ml-2" 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>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<%# paginate @events, theme: 'twitter_bootstrap' %>
|
||||
<%= paginate @events %>
|
||||
<div class="mt-8 flex justify-center">
|
||||
<%= paginate @events, theme: "tailwind" %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun événement disponible</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Il n'y a aucun événement à venir pour le moment.</p>
|
||||
<div class="text-center py-16">
|
||||
<div class="mx-auto max-w-md">
|
||||
<div class="w-24 h-24 mx-auto bg-gradient-to-r from-purple-100 to-indigo-100 rounded-full flex items-center justify-center mb-6">
|
||||
<svg class="w-12 h-12 text-purple-600" 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" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucun événement disponible</h3>
|
||||
<p class="text-gray-500 mb-6">Il n'y a aucun événement à venir pour le moment.</p>
|
||||
<%= link_to "Retour à l'accueil", root_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm 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" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
150
app/views/events/payment_success.html.erb
Executable file
150
app/views/events/payment_success.html.erb
Executable file
@@ -0,0 +1,150 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-12 px-4 sm:px-6">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 px-6 py-8 text-center">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="w-16 h-16 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Paiement réussi !</h1>
|
||||
<p class="text-purple-100">Félicitations pour votre achat</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6 sm:p-8">
|
||||
<div class="text-center mb-8">
|
||||
<p class="text-xl text-gray-700">
|
||||
Vos billets pour <span class="font-bold text-purple-700"><%= @event.name %></span> ont été achetés avec succès.
|
||||
</p>
|
||||
<p class="text-gray-500 mt-2">
|
||||
Un email de confirmation avec vos billets a été envoyé à <span class="font-medium"><%= current_user.email %></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Event Details -->
|
||||
<div class="bg-gray-50 rounded-xl p-6 mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600" 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>
|
||||
Détails de l'événement
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
||||
<svg class="w-5 h-5 text-purple-500 mr-3" 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>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">Lieu</p>
|
||||
<p class="font-medium"><%= @event.venue_name %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center p-3 bg-white rounded-lg">
|
||||
<svg class="w-5 h-5 text-purple-500 mr-3" 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>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">Date & Heure</p>
|
||||
<p class="font-medium"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tickets -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
||||
</svg>
|
||||
Vos billets
|
||||
</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% @tickets.each do |ticket| %>
|
||||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl border border-purple-100 p-5">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-4">
|
||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-gray-900"><%= ticket.ticket_type.name %></h3>
|
||||
<p class="text-sm text-gray-600">Prix: <span class="font-medium"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Télécharger PDF
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-purple-100 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 text-gray-500 mr-2" 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>
|
||||
<span class="text-xs text-gray-500">Code QR: <%= ticket.qr_code[0..7] %></span>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Actif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Important Notice -->
|
||||
<div class="bg-blue-50 border border-blue-100 rounded-xl p-5 mb-8">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold text-blue-800 mb-1">Important</h3>
|
||||
<p class="text-sm text-blue-700">
|
||||
Veuillez télécharger et sauvegarder vos billets. Présentez-les à l'entrée du lieu pour accéder à l'événement.
|
||||
Un email de confirmation avec vos billets a été envoyé à votre adresse email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<%= link_to dashboard_path,
|
||||
class: "inline-flex items-center justify-center px-6 py-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-xl hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 font-medium shadow-sm" 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="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
Tableau de bord
|
||||
<% end %>
|
||||
|
||||
<%= link_to events_path,
|
||||
class: "inline-flex items-center justify-center px-6 py-3 bg-white text-gray-700 rounded-xl border border-gray-300 hover:bg-gray-50 transition-all duration-200 font-medium shadow-sm" 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="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>
|
||||
Voir plus d'événements
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,147 +1,185 @@
|
||||
<div class="min-h-screen bg-neutral-50" data-controller="ticket-cart" data-ticket-cart-event-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" />
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100" data-controller="ticket-cart" data-ticket-cart-event-id-value="<%= params[:id] %>">
|
||||
<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>
|
||||
|
||||
<a href="/events" 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">
|
||||
<%= @event.name %>
|
||||
</span>
|
||||
</span>
|
||||
</nav>
|
||||
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>
|
||||
<li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
|
||||
<%= @event.name %>
|
||||
</li>
|
||||
</ol>
|
||||
</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: Event Info & Image -->
|
||||
<div class="w-full md:w-1/2">
|
||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @event.name %></h1>
|
||||
|
||||
<% if @event.image.present? %>
|
||||
<div class="relative rounded-2xl overflow-hidden mb-6">
|
||||
<%= image_tag @event.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><%= @event.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><%= @event.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Event Header with Image -->
|
||||
<% if @event.image.present? %>
|
||||
<div class="relative h-96">
|
||||
<%= image_tag @event.image, class: "w-full h-full object-cover" %>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-transparent"></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>
|
||||
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
||||
<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.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><%= @event.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><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
|
||||
</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"><%= @event.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"><%= @event.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"><%= @event.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">
|
||||
<% @event.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>
|
||||
|
||||
<!-- 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>
|
||||
<% else %>
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
|
||||
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-200" 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>
|
||||
Continue to Checkout
|
||||
</span>
|
||||
</button>
|
||||
<span><%= @event.venue_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-200" 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><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Event Content -->
|
||||
<div class="p-6 md:p-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Left Column: Event Details -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">Description</h2>
|
||||
<div class="prose max-w-none text-gray-700">
|
||||
<p class="text-lg leading-relaxed"><%= @event.description %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-gray-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600" 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>
|
||||
Lieu
|
||||
</h3>
|
||||
<p class="text-gray-700 font-medium"><%= @event.venue_name %></p>
|
||||
<p class="text-gray-600 mt-1"><%= @event.venue_address %></p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-xl p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600" 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>
|
||||
Date & Heure
|
||||
</h3>
|
||||
<p class="text-gray-700 font-medium"><%= @event.start_time.strftime("%A %d %B %Y") %></p>
|
||||
<p class="text-gray-600 mt-1">À <%= @event.start_time.strftime("%H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Organisateur</h3>
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-full bg-gradient-to-r from-purple-500 to-indigo-600 flex items-center justify-center text-white font-bold">
|
||||
<%= @event.user.email.first.upcase %>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="font-medium text-gray-900"><%= @event.user.email.split('@').first %></p>
|
||||
<p class="text-sm text-gray-500">Organisateur de l'événement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Ticket Selection -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="sticky top-6">
|
||||
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Billets disponibles</h2>
|
||||
|
||||
<% if @event.ticket_types.any? %>
|
||||
<div class="space-y-4 mb-6">
|
||||
<% @event.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 %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 mx-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">Aucun billet disponible</h3>
|
||||
<p class="mt-2 text-gray-500">Les billets pour cet événement ne sont pas encore disponibles ou sont épuisés.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Cart Summary -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-600">Total billets:</span>
|
||||
<span id="cart-count" class="font-medium">0</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-gray-600">Montant total:</span>
|
||||
<span id="cart-total" class="text-xl font-bold text-purple-700">€0.00</span>
|
||||
</div>
|
||||
<button
|
||||
id="checkout-btn"
|
||||
class="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-4 rounded-xl shadow-sm transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 flex items-center justify-center"
|
||||
disabled
|
||||
data-action="click->ticket-cart#proceedToCheckout">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
Procéder au paiement
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<body data-user-authenticated="<%= user_signed_in? %>" data-event-slug="<%= @event&.slug %>">
|
||||
<div class="app-wrapper">
|
||||
<%= render "components/header" %>
|
||||
|
||||
|
||||
@@ -2,14 +2,44 @@
|
||||
<!-- Hero section with metrics -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements disponibles", value: @available_events, classes: "from-purple-100 to-indigo-100" } %>
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Mes réservations", value: @booked_events, classes: "from-green-100 to-emerald-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_this_week, classes: "from-purple-100 to-indigo-100" } %>
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_today, classes: "from-blue-100 to-sky-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements cette semaine", value: @events_this_week, classes: "from-purple-100 to-indigo-100" } %>
|
||||
<%= render partial: 'components/metric_card', locals: { title: "Événements demain", value: @events_tomorrow, classes: "from-purple-100 to-indigo-100" } %>
|
||||
|
||||
<%= render partial: 'components/metric_card', locals: { title: "À venir", value: @upcoming_events, classes: "from-orange-100 to-amber-100" } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User's booked events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Mes événements réservés</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @user_booked_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @user_booked_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if @booked_events > 5 %>
|
||||
<div class="mt-6 text-center">
|
||||
<%= link_to "Voir toutes mes réservations", "#", class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium transition-colors duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-8">
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4">Vous n'avez encore réservé aucun événement.</p>
|
||||
<%= link_to "Découvrir les événements", events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
56
app/views/ticket_mailer/purchase_confirmation.html.erb
Executable file
56
app/views/ticket_mailer/purchase_confirmation.html.erb
Executable file
@@ -0,0 +1,56 @@
|
||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
||||
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;">ApéroNight</h1>
|
||||
<p style="color: #6c757d; margin: 10px 0 0;">Confirmation de votre achat</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
||||
|
||||
<p style="color: #495057; line-height: 1.6;">
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
||||
</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Type de billet</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @ticket.ticket_type.name %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
||||
<strong>Important :</strong> Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
|
||||
<p style="margin: 0;">Si vous avez des questions, contactez-nous à <a href="mailto:support@aperonight.com" style="color: #4c1d95; text-decoration: none;">support@aperonight.com</a></p>
|
||||
<p style="margin: 10px 0 0;">© <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
|
||||
</div>
|
||||
</div>
|
||||
19
app/views/ticket_mailer/purchase_confirmation.text.erb
Executable file
19
app/views/ticket_mailer/purchase_confirmation.text.erb
Executable file
@@ -0,0 +1,19 @@
|
||||
Bonjour <%= @user.email.split('@').first %>,
|
||||
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
||||
|
||||
DÉTAILS DE VOTRE BILLET
|
||||
======================
|
||||
|
||||
Événement : <%= @event.name %>
|
||||
Type de billet : <%= @ticket.ticket_type.name %>
|
||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
|
||||
|
||||
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
||||
|
||||
Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
|
||||
Si vous avez des questions, contactez-nous à support@aperonight.com
|
||||
|
||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||
27
config/initializers/stripe.rb
Executable file
27
config/initializers/stripe.rb
Executable file
@@ -0,0 +1,27 @@
|
||||
Rails.application.configure do
|
||||
# Try to get Stripe keys from environment variables first, then from credentials
|
||||
stripe_publishable_key = ENV["STRIPE_PUBLISHABLE_KEY"]
|
||||
stripe_secret_key = ENV["STRIPE_SECRET_KEY"]
|
||||
stripe_webhook_secret = ENV["STRIPE_WEBHOOK_SECRET"]
|
||||
|
||||
# If not in environment variables, try to get from credentials
|
||||
if Rails.application.respond_to?(:credentials) && Rails.application.credentials.respond_to?(:stripe)
|
||||
stripe_credentials = Rails.application.credentials.stripe
|
||||
if stripe_credentials
|
||||
stripe_publishable_key ||= stripe_credentials[:publishable_key] if stripe_credentials.key?(:publishable_key)
|
||||
stripe_secret_key ||= stripe_credentials[:secret_key] if stripe_credentials.key?(:secret_key)
|
||||
stripe_webhook_secret ||= stripe_credentials[:webhook_secret] if stripe_credentials.key?(:webhook_secret)
|
||||
end
|
||||
end
|
||||
|
||||
config.stripe = {
|
||||
publishable_key: stripe_publishable_key,
|
||||
secret_key: stripe_secret_key,
|
||||
signing_secret: stripe_webhook_secret
|
||||
}
|
||||
end
|
||||
|
||||
# Only set the API key if it exists
|
||||
if Rails.application.config.stripe[:secret_key].present?
|
||||
Stripe.api_key = Rails.application.config.stripe[:secret_key]
|
||||
end
|
||||
@@ -16,9 +16,15 @@ Rails.application.routes.draw do
|
||||
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
||||
|
||||
# Events
|
||||
get "events", to: "events#index", as: "events"
|
||||
get "events/:slug.:id", to: "events#show", as: "event"
|
||||
post "events/:slug.:id/checkout", to: "events#checkout", as: "event_checkout"
|
||||
get "events", to: "events#index", as: "events"
|
||||
get "events/:slug.:id", to: "events#show", as: "event"
|
||||
post "events/:slug.:id/checkout", to: "events#checkout", as: "event_checkout"
|
||||
|
||||
# Payment success
|
||||
get "payments/success", to: "events#payment_success", as: "payment_success"
|
||||
|
||||
# Tickets
|
||||
get "tickets/:ticket_id/download", to: "events#download_ticket", as: "download_ticket"
|
||||
|
||||
# Routes for devise authentication Gem
|
||||
# Bind devise to user
|
||||
|
||||
8
db/schema.rb
generated
8
db/schema.rb
generated
@@ -11,7 +11,7 @@
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
||||
create_table "events", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||
create_table "events", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "slug", null: false
|
||||
t.string "image"
|
||||
@@ -33,7 +33,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
||||
t.index ["user_id"], name: "index_events_on_user_id"
|
||||
end
|
||||
|
||||
create_table "ticket_types", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||
create_table "ticket_types", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.text "description"
|
||||
t.integer "price_cents"
|
||||
@@ -50,7 +50,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
||||
t.index ["sale_start_at"], name: "index_ticket_types_on_sale_start_at"
|
||||
end
|
||||
|
||||
create_table "tickets", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||
create_table "tickets", force: :cascade do |t|
|
||||
t.string "qr_code"
|
||||
t.integer "price_cents"
|
||||
t.string "status", default: "active"
|
||||
@@ -63,7 +63,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_171354) do
|
||||
t.index ["user_id"], name: "index_tickets_on_user_id"
|
||||
end
|
||||
|
||||
create_table "users", charset: "utf8mb4", collation: "utf8mb4_uca1400_ai_ci", force: :cascade do |t|
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
t.string "reset_password_token"
|
||||
|
||||
Reference in New Issue
Block a user