develop #3
3
.gitignore
vendored
3
.gitignore
vendored
@@ -43,3 +43,6 @@
|
|||||||
|
|
||||||
# Ignore generated reports
|
# Ignore generated reports
|
||||||
/test/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)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
chunky_png (1.4.0)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.3)
|
connection_pool (2.5.3)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@@ -181,7 +182,6 @@ GEM
|
|||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
mysql2 (0.5.6)
|
|
||||||
net-imap (0.5.9)
|
net-imap (0.5.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
@@ -215,8 +215,16 @@ GEM
|
|||||||
parser (3.3.9.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
|
pdf-core (0.10.0)
|
||||||
pp (0.6.2)
|
pp (0.6.2)
|
||||||
prettyprint
|
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)
|
prettyprint (0.2.0)
|
||||||
prism (1.4.0)
|
prism (1.4.0)
|
||||||
propshaft (1.2.1)
|
propshaft (1.2.1)
|
||||||
@@ -279,8 +287,12 @@ GEM
|
|||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.1)
|
rexml (3.4.2)
|
||||||
rubocop (1.80.0)
|
rqrcode (3.1.0)
|
||||||
|
chunky_png (~> 1.0)
|
||||||
|
rqrcode_core (~> 2.0)
|
||||||
|
rqrcode_core (2.0.0)
|
||||||
|
rubocop (1.80.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -349,11 +361,14 @@ GEM
|
|||||||
stimulus-rails (1.3.4)
|
stimulus-rails (1.3.4)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.1.7)
|
stringio (3.1.7)
|
||||||
|
stripe (15.5.0)
|
||||||
thor (1.4.0)
|
thor (1.4.0)
|
||||||
thruster (0.1.15)
|
thruster (0.1.15)
|
||||||
thruster (0.1.15-aarch64-linux)
|
thruster (0.1.15-aarch64-linux)
|
||||||
thruster (0.1.15-x86_64-linux)
|
thruster (0.1.15-x86_64-linux)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
|
ttfunk (1.8.0)
|
||||||
|
bigdecimal (~> 3.1)
|
||||||
turbo-rails (2.0.16)
|
turbo-rails (2.0.16)
|
||||||
actionpack (>= 7.1.0)
|
actionpack (>= 7.1.0)
|
||||||
railties (>= 7.1.0)
|
railties (>= 7.1.0)
|
||||||
@@ -402,10 +417,12 @@ DEPENDENCIES
|
|||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kaminari-tailwind (~> 0.1.0)
|
kaminari-tailwind (~> 0.1.0)
|
||||||
minitest-reporters (~> 1.7)
|
minitest-reporters (~> 1.7)
|
||||||
mysql2 (~> 0.5)
|
prawn (~> 2.5)
|
||||||
|
prawn-qrcode (~> 0.5)
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.0.2, >= 8.0.2.1)
|
rails (~> 8.0.2, >= 8.0.2.1)
|
||||||
|
rqrcode (~> 3.1)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
solid_cable
|
solid_cable
|
||||||
@@ -413,6 +430,7 @@ DEPENDENCIES
|
|||||||
solid_queue
|
solid_queue
|
||||||
sqlite3 (~> 2.7)
|
sqlite3 (~> 2.7)
|
||||||
stimulus-rails
|
stimulus-rails
|
||||||
|
stripe (~> 15.5)
|
||||||
thruster
|
thruster
|
||||||
turbo-rails
|
turbo-rails
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
|||||||
205
README.md
205
README.md
@@ -4,76 +4,209 @@
|
|||||||
|
|
||||||
## 🌃 Overview
|
## 🌃 Overview
|
||||||
|
|
||||||
**Aperonight** is a two-sided marketplace connecting event-goers with nightlife promoters in Paris. The platform allows:
|
**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.
|
||||||
|
|
||||||
- **Customers** to discover/book tickets for upcoming events
|
|
||||||
- **Promoters** to create/manage events and validate tickets at venue entrances
|
|
||||||
|
|
||||||
## 🎯 Key Features
|
## 🎯 Key Features
|
||||||
|
|
||||||
### For Event-Goers
|
### For Event-Goers
|
||||||
✔ Browse upcoming events with filters (date, location, music genre)
|
✅ **User Dashboard** - Personalized metrics showing booked events, upcoming events, and event statistics
|
||||||
✔ Book tickets with multiple bundle options (VIP, group passes, etc.)
|
✅ **Event Discovery** - Browse upcoming events with detailed information and venue details
|
||||||
✔ Secure payment processing (credit cards, Apple/Google Pay)
|
✅ **Secure Booking** - Multiple ticket types per event with quantity selection
|
||||||
✔ Mobile-friendly e-tickets with QR codes
|
✅ **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
|
### For Event Organizers
|
||||||
✔ Event creation dashboard with ticket type customization
|
✅ **Event Management** - Create and manage events with detailed information
|
||||||
✔ Real-time ticket validation via mobile scanning
|
✅ **Ticket Type Configuration** - Set up multiple ticket types with different pricing
|
||||||
✔ Sales analytics and attendee tracking
|
✅ **Sales Tracking** - Monitor ticket sales and availability
|
||||||
✔ Automatic aggregation of events from partner platforms
|
✅ **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
|
## 🛠 Technical Stack
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- **Ruby on Rails 7** (API mode)
|
- **Ruby on Rails 8.0+** with Hotwire for reactive UI
|
||||||
- **MariaDB** database
|
- **MySQL** database with comprehensive migrations
|
||||||
<!--- **Redis** for caching/background jobs-->
|
- **Devise** for user authentication and session management
|
||||||
- **ActiveJob** for background processing
|
- **Kaminari** for pagination
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- **Hotwire (Turbo + Stimulus)** for reactive UI
|
- **Hotwire (Turbo + Stimulus)** for interactive JavaScript behavior
|
||||||
- **Tailwind CSS** for styling
|
- **Tailwind CSS** for responsive styling and modern UI
|
||||||
- **React Native** for promoter mobile app (ticket scanning)
|
- **JavaScript Controllers** for cart management and checkout flow
|
||||||
|
|
||||||
### Key Integrations
|
### Key Integrations
|
||||||
- **Stripe Connect** for payments & promoter payouts
|
- **Stripe** for secure payment processing and checkout sessions
|
||||||
- **Shogun/Bizouk/Weezevent APIs** for event aggregation
|
- **Prawn & Prawn-QRCode** for PDF ticket generation
|
||||||
<!--- **Twilio** for SMS ticket delivery-->
|
- **RQRCode** for unique QR code generation per ticket
|
||||||
<!--- **AWS S3** for media storage-->
|
|
||||||
|
|
||||||
## 📊 Database Schema (Simplified)
|
## 📊 Database Schema
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
USER ||--o{ BOOKING : makes
|
USER ||--o{ EVENT : creates
|
||||||
|
USER ||--o{ TICKET : purchases
|
||||||
USER {
|
USER {
|
||||||
integer id
|
integer id
|
||||||
string email
|
string email
|
||||||
string encrypted_password
|
string encrypted_password
|
||||||
}
|
string first_name
|
||||||
PROMOTER ||--o{ EVENT : creates
|
string last_name
|
||||||
PROMOTER {
|
|
||||||
integer id
|
|
||||||
string stripe_account_id
|
|
||||||
}
|
}
|
||||||
EVENT ||--o{ TICKET_TYPE : has
|
EVENT ||--o{ TICKET_TYPE : has
|
||||||
EVENT {
|
EVENT {
|
||||||
integer id
|
integer id
|
||||||
|
integer user_id
|
||||||
|
string name
|
||||||
|
string slug
|
||||||
|
text description
|
||||||
|
string venue_name
|
||||||
|
string venue_address
|
||||||
|
decimal latitude
|
||||||
|
decimal longitude
|
||||||
datetime start_time
|
datetime start_time
|
||||||
}
|
datetime end_time
|
||||||
BOOKING ||--o{ TICKET : generates
|
string state
|
||||||
BOOKING {
|
boolean featured
|
||||||
integer id
|
string image
|
||||||
decimal total_price
|
|
||||||
}
|
}
|
||||||
TICKET_TYPE ||--o{ TICKET : defines
|
TICKET_TYPE ||--o{ TICKET : defines
|
||||||
TICKET_TYPE {
|
TICKET_TYPE {
|
||||||
integer id
|
integer id
|
||||||
|
integer event_id
|
||||||
string name
|
string name
|
||||||
|
text description
|
||||||
|
integer price_cents
|
||||||
|
integer quantity
|
||||||
|
datetime sale_start_at
|
||||||
|
datetime sale_end_at
|
||||||
|
boolean requires_id
|
||||||
|
integer minimum_age
|
||||||
}
|
}
|
||||||
TICKET {
|
TICKET {
|
||||||
integer id
|
integer id
|
||||||
|
integer user_id
|
||||||
|
integer ticket_type_id
|
||||||
string qr_code
|
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
|
class EventsController < ApplicationController
|
||||||
|
before_action :authenticate_user!, only: [:checkout, :payment_success, :download_ticket]
|
||||||
|
before_action :set_event, only: [:show, :checkout]
|
||||||
|
|
||||||
# Display all events
|
# Display all events
|
||||||
def index
|
def index
|
||||||
@events = Event.includes(:user).upcoming.page(params[:page]).per(1)
|
@events = Event.includes(:user).upcoming.page(params[:page]).per(12)
|
||||||
# @events = Event.page(params[:page]).per(12)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display desired event
|
# Display desired event
|
||||||
def show
|
def show
|
||||||
@event = Event.find(params[:id])
|
# Event is set by set_event callback
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle checkout process
|
# Handle checkout process - Create Stripe session
|
||||||
def checkout
|
def checkout
|
||||||
@event = Event.find(params[:id])
|
|
||||||
cart_data = JSON.parse(params[:cart] || "{}")
|
cart_data = JSON.parse(params[:cart] || "{}")
|
||||||
|
|
||||||
if cart_data.empty?
|
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create order items from cart
|
# Create order items from cart
|
||||||
|
line_items = []
|
||||||
order_items = []
|
order_items = []
|
||||||
total_amount = 0
|
total_amount = 0
|
||||||
|
|
||||||
@@ -34,12 +36,27 @@ class EventsController < ApplicationController
|
|||||||
# Check availability
|
# Check availability
|
||||||
available = ticket_type.quantity - ticket_type.tickets.count
|
available = ticket_type.quantity - ticket_type.tickets.count
|
||||||
if quantity > available
|
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
|
return
|
||||||
end
|
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 << {
|
order_items << {
|
||||||
ticket_type: ticket_type,
|
ticket_type_id: ticket_type.id,
|
||||||
|
ticket_type_name: ticket_type.name,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
price_cents: ticket_type.price_cents
|
price_cents: ticket_type.price_cents
|
||||||
}
|
}
|
||||||
@@ -48,17 +65,90 @@ class EventsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if order_items.empty?
|
if order_items.empty?
|
||||||
redirect_to event_path(@event), alert: "Invalid order"
|
redirect_to event_path(@event.slug, @event), alert: "Commande invalide"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Here you would typically:
|
begin
|
||||||
# 1. Create an Order record
|
# Create Stripe Checkout Session
|
||||||
# 2. Create Ticket records for each item
|
session = Stripe::Checkout::Session.create({
|
||||||
# 3. Redirect to payment processing
|
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
|
redirect_to session.url, allow_other_host: true
|
||||||
# In a real app, you'd redirect to a payment page
|
rescue Stripe::StripeError => e
|
||||||
redirect_to event_path(@event), notice: "Order created successfully! Proceeding to payment..."
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,8 +18,19 @@ class PagesController < ApplicationController
|
|||||||
# User dashboard showing personalized content
|
# User dashboard showing personalized content
|
||||||
# Accessible only to authenticated users
|
# Accessible only to authenticated users
|
||||||
def dashboard
|
def dashboard
|
||||||
@available_events = Event.published.count
|
# Metrics for dashboard cards
|
||||||
@events_this_week = Event.published.where("start_time BETWEEN ? AND ?", Date.current.beginning_of_week, Date.current.end_of_week).count
|
@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)
|
@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)
|
@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])
|
@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",
|
credentials: "same-origin",
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.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.
|
// By default the response does not include any url.
|
||||||
// Redirect to default login page (loginUrlValue)
|
// Redirect to default login page (redirectUrlValue)
|
||||||
if (response.redirected) {
|
if (response.redirected) {
|
||||||
window.location.href = response.url;
|
window.location.href = response.url;
|
||||||
} else if (this.element.dataset.loginUrlValue) {
|
} else if (this.element.dataset.redirectUrlValue) {
|
||||||
window.location.href = this.element.dataset.loginUrlValue;
|
window.location.href = this.element.dataset.redirectUrlValue;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ export default class extends Controller {
|
|||||||
connect() {
|
connect() {
|
||||||
this.cart = {}
|
this.cart = {}
|
||||||
this.updateCartDisplay()
|
this.updateCartDisplay()
|
||||||
|
|
||||||
|
// Check for pending cart in session storage (after login)
|
||||||
|
this.checkForPendingCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
increaseQuantity(event) {
|
increaseQuantity(event) {
|
||||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
const ticketTypeId = event.params.ticketTypeId
|
||||||
const max = parseInt(event.currentTarget.dataset.max)
|
const max = parseInt(event.params.max)
|
||||||
const input = this.quantityTargetFor(ticketTypeId)
|
const input = this.quantityTargetFor(ticketTypeId)
|
||||||
|
|
||||||
const current = parseInt(input.value) || 0
|
const current = parseInt(input.value) || 0
|
||||||
@@ -22,7 +25,7 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
decreaseQuantity(event) {
|
decreaseQuantity(event) {
|
||||||
const ticketTypeId = event.currentTarget.dataset.ticketTypeId
|
const ticketTypeId = event.params.ticketTypeId
|
||||||
const input = this.quantityTargetFor(ticketTypeId)
|
const input = this.quantityTargetFor(ticketTypeId)
|
||||||
|
|
||||||
const current = parseInt(input.value) || 0
|
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) {
|
updateCartItem(ticketTypeId, input) {
|
||||||
const name = input.dataset.name
|
const name = input.dataset.name
|
||||||
const price = parseInt(input.dataset.price)
|
const price = parseInt(input.dataset.price)
|
||||||
@@ -59,28 +78,54 @@ export default class extends Controller {
|
|||||||
totalPrice += (this.cart[ticketTypeId].price * this.cart[ticketTypeId].quantity) / 100
|
totalPrice += (this.cart[ticketTypeId].price * this.cart[ticketTypeId].quantity) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cartCountTarget.textContent = totalTickets
|
// Update cart count and total
|
||||||
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
|
if (this.hasCartCountTarget) {
|
||||||
|
this.cartCountTarget.textContent = totalTickets
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasCartTotalTarget) {
|
||||||
|
this.cartTotalTarget.textContent = totalPrice.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
const checkoutBtn = this.checkoutButtonTarget
|
// Update checkout button state
|
||||||
if (totalTickets > 0) {
|
if (this.hasCheckoutButtonTarget) {
|
||||||
checkoutBtn.disabled = false
|
const checkoutBtn = this.checkoutButtonTarget
|
||||||
} else {
|
if (totalTickets > 0) {
|
||||||
checkoutBtn.disabled = true
|
checkoutBtn.disabled = false
|
||||||
|
checkoutBtn.classList.remove('opacity-50', 'cursor-not-allowed')
|
||||||
|
} else {
|
||||||
|
checkoutBtn.disabled = true
|
||||||
|
checkoutBtn.classList.add('opacity-50', 'cursor-not-allowed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proceedToCheckout() {
|
proceedToCheckout() {
|
||||||
if (Object.keys(this.cart).length === 0) {
|
if (Object.keys(this.cart).length === 0) {
|
||||||
alert('Please select at least one ticket')
|
alert('Veuillez sélectionner au moins un billet')
|
||||||
return
|
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')
|
const form = document.createElement('form')
|
||||||
form.method = 'POST'
|
form.method = 'POST'
|
||||||
form.action = `/events/${this.eventIdValue}/checkout`
|
form.action = `/events/${document.body.dataset.eventSlug}.${this.eventIdValue}/checkout`
|
||||||
form.style.display = 'none'
|
|
||||||
|
|
||||||
// Add CSRF token
|
// Add CSRF token
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
|
||||||
const csrfInput = document.createElement('input')
|
const csrfInput = document.createElement('input')
|
||||||
@@ -100,6 +145,31 @@ export default class extends Controller {
|
|||||||
form.submit()
|
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
|
// Helper method to find quantity input by ticket type ID
|
||||||
quantityTargetFor(ticketTypeId) {
|
quantityTargetFor(ticketTypeId) {
|
||||||
return document.querySelector(`#quantity_${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] }
|
validates :status, presence: true, inclusion: { in: %w[active used expired refunded] }
|
||||||
|
|
||||||
before_validation :set_price_from_ticket_type, on: :create
|
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
|
private
|
||||||
|
|
||||||
@@ -19,4 +30,13 @@ class Ticket < ApplicationRecord
|
|||||||
return unless ticket_type
|
return unless ticket_type
|
||||||
self.price_cents = ticket_type.price_cents
|
self.price_cents = ticket_type.price_cents
|
||||||
end
|
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
|
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">
|
<div class="hidden sm:flex space-x-6">
|
||||||
<%= link_to t("header.parties"), events_path,
|
<%= 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"), "#",
|
<%= 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-700" %>
|
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,
|
<%= link_to t("header.logout"), destroy_user_session_path,
|
||||||
data: { controller: "logout", action: "click->logout#signOut",
|
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" %>
|
class: "block px-4 py-2 text-sm bg-black text-gray-100 hover:bg-purple-700 last:rounded-b-md" %>
|
||||||
</div>
|
</div>
|
||||||
</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="bg-white rounded-xl border <%= sold_out ? "border-gray-200 opacity-75" : "border-purple-200" %> shadow-sm overflow-hidden">
|
||||||
<div class="card-body p-6">
|
<div class="p-5">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-primary <%= "text-slate-400" if sold_out %>"><%= name %></h3>
|
<h3 class="text-lg font-bold text-gray-900 <%= "text-gray-400" if sold_out %>"><%= name %></h3>
|
||||||
<p class="text-neutral-600 text-sm <%= "text-slate-400" if sold_out %>"><%= description %></p>
|
<p class="text-gray-600 text-sm <%= "text-gray-400" if sold_out %>"><%= description %></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<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: "€") %>
|
<%= number_to_currency(price_cents / 100.0, unit: "€") %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center mt-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-neutral-600 flex items-center">
|
<% if sold_out %>
|
||||||
<% 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">
|
||||||
<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 h-2 w-2 text-red-400" fill="currentColor" viewBox="0 0 8 8">
|
||||||
<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" />
|
||||||
<circle cx="4" cy="4" r="3" />
|
</svg>
|
||||||
</svg>
|
Épuisé
|
||||||
Sold Out
|
</span>
|
||||||
</span>
|
<% else %>
|
||||||
<% 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">
|
||||||
<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 h-2 w-2 text-green-400" fill="currentColor" viewBox="0 0 8 8">
|
||||||
<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" />
|
||||||
<circle cx="4" cy="4" r="3" />
|
</svg>
|
||||||
</svg>
|
<%= remaining %> disponibles
|
||||||
<%= remaining %> available
|
</span>
|
||||||
</span>
|
<% end %>
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% unless sold_out %>
|
<% unless sold_out %>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<button type="button"
|
<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"
|
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||||
onclick="decreaseQuantity(<%= id %>)">
|
data-action="click->ticket-cart#decreaseQuantity"
|
||||||
<span class="text-slate-600">-</span>
|
data-ticket-cart-ticket-type-id-param="<%= id %>"
|
||||||
|
data-ticket-cart-max-param="<%= remaining %>">
|
||||||
|
<span class="text-gray-600 font-bold">-</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
id="quantity_<%= id %>"
|
id="quantity_<%= id %>"
|
||||||
min="0"
|
min="0"
|
||||||
max="<%= remaining %>"
|
max="<%= remaining %>"
|
||||||
value="0"
|
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-name="<%= name %>"
|
||||||
data-price="<%= price_cents %>"
|
data-price="<%= price_cents %>">
|
||||||
onchange="updateCart(<%= id %>, this.dataset.name, this.dataset.price)">
|
|
||||||
<button type="button"
|
<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"
|
class="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 flex items-center justify-center transition-colors duration-200"
|
||||||
onclick="increaseQuantity(<%= id %>, <%= remaining %>)">
|
data-action="click->ticket-cart#increaseQuantity"
|
||||||
<span class="text-slate-600">+</span>
|
data-ticket-cart-ticket-type-id-param="<%= id %>"
|
||||||
|
data-ticket-cart-max-param="<%= remaining %>">
|
||||||
|
<span class="text-gray-600 font-bold">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% 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">
|
<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>
|
<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>
|
</svg>
|
||||||
Unavailable
|
Indisponible
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,53 +1,91 @@
|
|||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<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? %>
|
<% if @events.any? %>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<% @events.each do |event| %>
|
<% @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="p-6">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-bold text-gray-900"><%= event.name %></h2>
|
<h2 class="text-xl font-bold text-gray-900 line-clamp-1"><%= event.name %></h2>
|
||||||
<p class="text-sm text-gray-500 mt-1"><%= event.user.email %></p>
|
<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>
|
</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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mb-4">
|
||||||
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
<p class="text-gray-600 text-sm line-clamp-2"><%= event.description.truncate(100) %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<% if event.ticket_types.any? %>
|
<% if event.ticket_types.any? %>
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
À partir de <%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
À partir de <%= format_price(event.ticket_types.minimum(:price_cents)) %>€
|
||||||
</p>
|
</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 %>
|
<% else %>
|
||||||
<p class="text-sm text-gray-500">Pas de billets disponibles</p>
|
<p class="text-sm text-gray-500">Pas de billets disponibles</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8 flex justify-center">
|
||||||
<%# paginate @events, theme: 'twitter_bootstrap' %>
|
<%= paginate @events, theme: "tailwind" %>
|
||||||
<%= paginate @events %>
|
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-16">
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="mx-auto max-w-md">
|
||||||
<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" />
|
<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>
|
<svg class="w-12 h-12 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Aucun événement disponible</h3>
|
<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" />
|
||||||
<p class="mt-1 text-sm text-gray-500">Il n'y a aucun événement à venir pour le moment.</p>
|
</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>
|
</div>
|
||||||
<% end %>
|
<% 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="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 md:px-4">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
<nav class="mb-3 text-sm" aria-label="Breadcrumb">
|
<nav class="mb-6" aria-label="Breadcrumb">
|
||||||
<nav class="mb-3 text-sm" aria-label="Breadcrumb" role="navigation">
|
<ol class="flex items-center space-x-2 text-sm">
|
||||||
<span class="flex items-center text-slate-700" role="list">
|
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<a href="/" class="hover:text-primary-600 transition-colors duration-200 flex items-center" role="listitem">
|
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<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" />
|
||||||
<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>
|
</svg>
|
||||||
|
Accueil
|
||||||
<a href="/events" class="hover:text-primary-600 transition-colors duration-200 mx-2" role="listitem">
|
<% end %>
|
||||||
Events
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</a>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
<svg class="w-4 h-4 mx-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
Événements
|
||||||
</svg>
|
<% end %>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<span class="mx-2 font-medium truncate max-w-[150px] sm:max-w-[250px]" role="listitem" aria-current="page">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
<%= @event.name %>
|
</svg>
|
||||||
</span>
|
<li class="font-medium text-gray-900 truncate max-w-xs" aria-current="page">
|
||||||
</span>
|
<%= @event.name %>
|
||||||
</nav>
|
</li>
|
||||||
|
</ol>
|
||||||
</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="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||||
<div class="flex flex-col lg:flex-row gap-6 md:gap-8">
|
<!-- Event Header with Image -->
|
||||||
<!-- Left Column: Event Info & Image -->
|
<% if @event.image.present? %>
|
||||||
<div class="w-full md:w-1/2">
|
<div class="relative h-96">
|
||||||
<h1 class="text-4xl font-bold text-primary mb-4"><%= @event.name %></h1>
|
<%= 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>
|
||||||
<% if @event.image.present? %>
|
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
|
||||||
<div class="relative rounded-2xl overflow-hidden mb-6">
|
<div class="max-w-4xl mx-auto">
|
||||||
<%= image_tag @event.image, class: "w-full h-96 object-cover" %>
|
<h1 class="text-3xl md:text-4xl font-bold text-white mb-2"><%= @event.name %></h1>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-50"></div>
|
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black">
|
<div class="flex items-center">
|
||||||
<h2 class="text-2xl font-semibold text-white mb-2">Event Details</h2>
|
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div class="flex flex-wrap gap-4 text-white">
|
<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>
|
||||||
<div class="flex items-center">
|
<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 class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</svg>
|
||||||
<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>
|
<span><%= @event.venue_name %></span>
|
||||||
</svg>
|
</div>
|
||||||
<span><%= @event.venue_name %></span>
|
<div class="flex items-center">
|
||||||
</div>
|
<svg class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div class="flex items-center">
|
<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 class="w-5 h-5 mr-2 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</svg>
|
||||||
<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>
|
<span><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></span>
|
||||||
</svg>
|
|
||||||
<span><%= @event.start_time.strftime("%B %d, %Y at %I:%M %p") %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-semibold text-primary mb-2">Description</h2>
|
|
||||||
<p class="text-lg text-slate-600 leading-relaxed"><%= @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>
|
||||||
</div>
|
</div>
|
||||||
|
<% else %>
|
||||||
<!-- Right Column: Ticket Selection -->
|
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 p-8">
|
||||||
<div class="w-full md:w-1/2">
|
<h1 class="text-3xl md:text-4xl font-bold text-white mb-4"><%= @event.name %></h1>
|
||||||
<div class="space-y-6">
|
<div class="flex flex-wrap items-center gap-4 text-white/90">
|
||||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Available Tickets</h2>
|
<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">
|
||||||
<div class="space-y-4">
|
<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>
|
||||||
<% @event.ticket_types.each do |ticket_type| %>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
<% 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>
|
|
||||||
</svg>
|
</svg>
|
||||||
Continue to Checkout
|
<span><%= @event.venue_name %></span>
|
||||||
</span>
|
</div>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-user-authenticated="<%= user_signed_in? %>" data-event-slug="<%= @event&.slug %>">
|
||||||
<div class="app-wrapper">
|
<div class="app-wrapper">
|
||||||
<%= render "components/header" %>
|
<%= render "components/header" %>
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,44 @@
|
|||||||
<!-- Hero section with metrics -->
|
<!-- Hero section with metrics -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-6">Tableau de bord</h1>
|
<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>
|
||||||
</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"
|
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
get "events", to: "events#index", as: "events"
|
get "events", to: "events#index", as: "events"
|
||||||
get "events/:slug.:id", to: "events#show", as: "event"
|
get "events/:slug.:id", to: "events#show", as: "event"
|
||||||
post "events/:slug.:id/checkout", to: "events#checkout", as: "event_checkout"
|
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
|
# Routes for devise authentication Gem
|
||||||
# Bind devise to user
|
# 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.
|
# 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
|
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 "name", null: false
|
||||||
t.string "slug", null: false
|
t.string "slug", null: false
|
||||||
t.string "image"
|
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"
|
t.index ["user_id"], name: "index_events_on_user_id"
|
||||||
end
|
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.string "name"
|
||||||
t.text "description"
|
t.text "description"
|
||||||
t.integer "price_cents"
|
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"
|
t.index ["sale_start_at"], name: "index_ticket_types_on_sale_start_at"
|
||||||
end
|
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.string "qr_code"
|
||||||
t.integer "price_cents"
|
t.integer "price_cents"
|
||||||
t.string "status", default: "active"
|
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"
|
t.index ["user_id"], name: "index_tickets_on_user_id"
|
||||||
end
|
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 "email", default: "", null: false
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string "encrypted_password", default: "", null: false
|
||||||
t.string "reset_password_token"
|
t.string "reset_password_token"
|
||||||
|
|||||||
Reference in New Issue
Block a user