refactor: Simplify PDF ticket design for single-page layout
- Switch to standard A4 page size with proper 40px margins - Remove complex gradient backgrounds and card layouts for simplicity - Implement clean, minimalist design with clear visual hierarchy - Use two-column layout for efficient space utilization - Center QR code with optimal 120px size for scanning - Simplify typography with consistent font sizes and colors - Remove unnecessary visual elements (shadows, rounded corners, badges) - Ensure all content fits comfortably on single page - Maintain brand colors (purple for header) with subtle styling - Keep all essential information: event details, ticket holder, QR code - Preserve robust error handling and QR code validation Design improvements: • Single-page layout that fits within A4 margins • Clean two-column information display • Simplified color scheme with good contrast • Optimized spacing for readability • Centered QR code for easy scanning • Minimal but professional appearance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ require "rqrcode"
|
|||||||
|
|
||||||
# PDF ticket generator service using Prawn
|
# PDF ticket generator service using Prawn
|
||||||
#
|
#
|
||||||
# Generates PDF tickets with QR codes for event entry validation
|
# Generates simple, compact PDF tickets with QR codes for event entry validation
|
||||||
# Includes event details, venue information, and unique QR code for each ticket
|
# Clean, minimalist design that fits on a single page
|
||||||
class TicketPdfGenerator
|
class TicketPdfGenerator
|
||||||
# Suppress Prawn's internationalization warning for built-in fonts
|
# Suppress Prawn's internationalization warning for built-in fonts
|
||||||
Prawn::Fonts::AFM.hide_m17n_warning = true
|
Prawn::Fonts::AFM.hide_m17n_warning = true
|
||||||
@@ -16,199 +16,109 @@ class TicketPdfGenerator
|
|||||||
end
|
end
|
||||||
|
|
||||||
def generate
|
def generate
|
||||||
Prawn::Document.new(page_size: [400, 650], margin: 0) do |pdf|
|
Prawn::Document.new(page_size: "A4", margin: 40) do |pdf|
|
||||||
# Main container with modern gradient background
|
# Simple header
|
||||||
create_background(pdf)
|
create_simple_header(pdf)
|
||||||
|
|
||||||
# Header section with brand and visual hierarchy
|
# Event and ticket info in compact layout
|
||||||
create_header(pdf)
|
create_ticket_info(pdf)
|
||||||
|
|
||||||
# Event information card
|
# QR code section
|
||||||
create_event_card(pdf)
|
|
||||||
|
|
||||||
# Ticket holder information
|
|
||||||
create_holder_section(pdf)
|
|
||||||
|
|
||||||
# QR Code section with modern styling
|
|
||||||
create_qr_section(pdf)
|
create_qr_section(pdf)
|
||||||
|
|
||||||
# Footer with security elements
|
# Simple footer
|
||||||
create_footer(pdf)
|
create_simple_footer(pdf)
|
||||||
|
|
||||||
end.render
|
end.render
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_background(pdf)
|
def create_simple_header(pdf)
|
||||||
# Gradient background effect
|
# Brand name
|
||||||
pdf.fill_color "F8FAFC"
|
|
||||||
pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, pdf.bounds.height
|
|
||||||
|
|
||||||
# Top decorative band
|
|
||||||
pdf.fill_color "6366F1"
|
pdf.fill_color "6366F1"
|
||||||
pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 120
|
pdf.font "Helvetica", style: :bold, size: 24
|
||||||
|
|
||||||
# Subtle gradient effect
|
|
||||||
pdf.fill_color "8B5CF6"
|
|
||||||
pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 80
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_header(pdf)
|
|
||||||
pdf.move_cursor_to(pdf.bounds.height - 30)
|
|
||||||
|
|
||||||
# ApéroNight logo/brand
|
|
||||||
pdf.fill_color "FFFFFF"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 32
|
|
||||||
pdf.text "AperoNight", align: :center
|
pdf.text "AperoNight", align: :center
|
||||||
|
|
||||||
pdf.move_down 8
|
pdf.move_down 5
|
||||||
pdf.font "Helvetica", size: 12
|
pdf.font "Helvetica", size: 10
|
||||||
pdf.fill_color "E2E8F0"
|
pdf.fill_color "64748B"
|
||||||
pdf.text "EVENEMENT TICKET", align: :center, character_spacing: 2
|
pdf.text "Billet d'entree", align: :center
|
||||||
|
|
||||||
|
pdf.move_down 20
|
||||||
|
|
||||||
|
# Simple divider line
|
||||||
|
pdf.stroke_color "E5E7EB"
|
||||||
|
pdf.horizontal_line 0, pdf.bounds.width
|
||||||
|
pdf.move_down 20
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_event_card(pdf)
|
def create_ticket_info(pdf)
|
||||||
pdf.move_cursor_to(480)
|
# Event name - prominent
|
||||||
|
|
||||||
# Main event card with shadow effect
|
|
||||||
card_y = pdf.cursor
|
|
||||||
|
|
||||||
# Shadow effect
|
|
||||||
pdf.fill_color "E2E8F0"
|
|
||||||
pdf.rounded_rectangle [22, card_y - 2], 356, 152, 15
|
|
||||||
pdf.fill
|
|
||||||
|
|
||||||
# Main card
|
|
||||||
pdf.fill_color "FFFFFF"
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.line_width 1
|
|
||||||
pdf.rounded_rectangle [20, card_y], 360, 150, 15
|
|
||||||
pdf.fill_and_stroke
|
|
||||||
|
|
||||||
# Event name with accent
|
|
||||||
pdf.bounding_box([40, card_y - 20], width: 320, height: 110) do
|
|
||||||
pdf.fill_color "1F2937"
|
pdf.fill_color "1F2937"
|
||||||
pdf.font "Helvetica", style: :bold, size: 20
|
pdf.font "Helvetica", style: :bold, size: 18
|
||||||
pdf.text ticket.event.name, align: :center
|
pdf.text ticket.event.name, align: :center
|
||||||
|
|
||||||
pdf.move_down 15
|
pdf.move_down 15
|
||||||
|
|
||||||
# Event details grid
|
# Two-column layout for ticket details
|
||||||
create_event_details_grid(pdf)
|
pdf.bounding_box([0, pdf.cursor], width: pdf.bounds.width, height: 120) do
|
||||||
|
# Left column
|
||||||
|
pdf.bounding_box([0, pdf.cursor], width: pdf.bounds.width / 2 - 20, height: 120) do
|
||||||
|
create_info_item(pdf, "Date", ticket.event.start_time.strftime("%d %B %Y"))
|
||||||
|
create_info_item(pdf, "Heure", ticket.event.start_time.strftime("%H:%M"))
|
||||||
|
create_info_item(pdf, "Lieu", ticket.event.venue_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Right column
|
||||||
|
pdf.bounding_box([pdf.bounds.width / 2 + 20, pdf.cursor], width: pdf.bounds.width / 2 - 20, height: 120) do
|
||||||
|
create_info_item(pdf, "Type", ticket.ticket_type.name)
|
||||||
|
create_info_item(pdf, "Prix", "#{sprintf('%.2f', ticket.price_euros)} EUR")
|
||||||
|
create_info_item(pdf, "Titulaire", "#{ticket.first_name} #{ticket.last_name}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_event_details_grid(pdf)
|
pdf.move_down 30
|
||||||
details = [
|
end
|
||||||
{ label: "DATE", value: ticket.event.start_time.strftime("%d %B %Y"), icon: "[CAL]" },
|
|
||||||
{ label: "HEURE", value: ticket.event.start_time.strftime("%H:%M"), icon: "[TIME]" },
|
|
||||||
{ label: "LIEU", value: ticket.event.venue_name, icon: "[LOC]" },
|
|
||||||
{ label: "TYPE", value: ticket.ticket_type.name, icon: "[TICK]" }
|
|
||||||
]
|
|
||||||
|
|
||||||
pdf.font "Helvetica", size: 10
|
def create_info_item(pdf, label, value)
|
||||||
|
pdf.font "Helvetica", style: :bold, size: 9
|
||||||
|
pdf.fill_color "64748B"
|
||||||
|
pdf.text label.upcase
|
||||||
|
|
||||||
details.each_slice(2).with_index do |row, row_index|
|
pdf.move_down 2
|
||||||
y_offset = row_index * 35
|
pdf.font "Helvetica", size: 11
|
||||||
|
|
||||||
row.each_with_index do |detail, col_index|
|
|
||||||
x_offset = col_index * 160
|
|
||||||
|
|
||||||
pdf.bounding_box([x_offset, pdf.cursor - y_offset], width: 150, height: 30) do
|
|
||||||
# Icon and label
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 8
|
|
||||||
pdf.text "#{detail[:icon]} #{detail[:label]}", character_spacing: 1
|
|
||||||
|
|
||||||
pdf.move_down 3
|
|
||||||
|
|
||||||
# Value
|
|
||||||
pdf.fill_color "1F2937"
|
pdf.fill_color "1F2937"
|
||||||
pdf.font "Helvetica", style: :bold, size: 11
|
pdf.text value
|
||||||
pdf.text detail[:value]
|
pdf.move_down 12
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_holder_section(pdf)
|
|
||||||
pdf.move_cursor_to(280)
|
|
||||||
|
|
||||||
# Ticket holder section
|
|
||||||
pdf.bounding_box([20, pdf.cursor], width: 360, height: 60) do
|
|
||||||
# Section header with accent line
|
|
||||||
pdf.fill_color "6366F1"
|
|
||||||
pdf.fill_rectangle [0, pdf.cursor], 60, 2
|
|
||||||
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
pdf.fill_color "1F2937"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 12
|
|
||||||
pdf.text "DETENTEUR DU BILLET", character_spacing: 1
|
|
||||||
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
# Holder name with elegant styling
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 18
|
|
||||||
pdf.fill_color "374151"
|
|
||||||
pdf.text "#{ticket.first_name.upcase} #{ticket.last_name.upcase}"
|
|
||||||
|
|
||||||
pdf.move_down 5
|
|
||||||
|
|
||||||
# Price badge
|
|
||||||
create_price_badge(pdf)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_price_badge(pdf)
|
|
||||||
price_text = "€#{sprintf('%.2f', ticket.price_euros)}"
|
|
||||||
|
|
||||||
# Price badge background
|
|
||||||
pdf.fill_color "10B981"
|
|
||||||
pdf.rounded_rectangle [0, pdf.cursor], 80, 25, 12
|
|
||||||
pdf.fill
|
|
||||||
|
|
||||||
# Price text
|
|
||||||
pdf.fill_color "FFFFFF"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 12
|
|
||||||
pdf.text_box price_text, at: [0, pdf.cursor],
|
|
||||||
width: 80, height: 25,
|
|
||||||
align: :center, valign: :center
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_qr_section(pdf)
|
def create_qr_section(pdf)
|
||||||
pdf.move_cursor_to(190)
|
# Center the QR code horizontally
|
||||||
|
qr_size = 120
|
||||||
# QR Code section with modern card design
|
x_position = (pdf.bounds.width - qr_size) / 2
|
||||||
pdf.bounding_box([20, pdf.cursor], width: 360, height: 140) do
|
|
||||||
# QR background card
|
|
||||||
pdf.fill_color "F1F5F9"
|
|
||||||
pdf.stroke_color "E2E8F0"
|
|
||||||
pdf.rounded_rectangle [0, pdf.cursor], 360, 130, 15
|
|
||||||
pdf.fill_and_stroke
|
|
||||||
|
|
||||||
|
pdf.bounding_box([x_position, pdf.cursor], width: qr_size, height: qr_size + 40) do
|
||||||
# QR Code title
|
# QR Code title
|
||||||
pdf.move_down 15
|
|
||||||
pdf.fill_color "475569"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 12
|
pdf.font "Helvetica", style: :bold, size: 12
|
||||||
pdf.text "CODE D'ENTREE", align: :center, character_spacing: 2
|
pdf.fill_color "1F2937"
|
||||||
|
pdf.text "Code d'entree", align: :center
|
||||||
pdf.move_down 10
|
pdf.move_down 10
|
||||||
|
|
||||||
# Generate and place QR code
|
# Generate QR code
|
||||||
generate_qr_code(pdf)
|
generate_simple_qr_code(pdf, qr_size)
|
||||||
|
|
||||||
pdf.move_down 10
|
pdf.move_down 10
|
||||||
|
|
||||||
# QR code ID
|
# QR code ID
|
||||||
pdf.font "Helvetica", size: 8
|
pdf.font "Helvetica", size: 8
|
||||||
pdf.fill_color "64748B"
|
pdf.fill_color "64748B"
|
||||||
pdf.text "ID: #{ticket.qr_code[0..11]}...", align: :center, character_spacing: 0.5
|
pdf.text "ID: #{ticket.qr_code[0..15]}...", align: :center
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_qr_code(pdf)
|
pdf.move_down 40
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_simple_qr_code(pdf, size)
|
||||||
# Ensure all required data is present before generating QR code
|
# Ensure all required data is present before generating QR code
|
||||||
if ticket.qr_code.blank?
|
if ticket.qr_code.blank?
|
||||||
raise "Ticket QR code is missing"
|
raise "Ticket QR code is missing"
|
||||||
@@ -229,37 +139,31 @@ class TicketPdfGenerator
|
|||||||
raise "QR code data must be a valid string"
|
raise "QR code data must be a valid string"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create QR code with white background
|
|
||||||
pdf.bounding_box([130, pdf.cursor], width: 100, height: 100) do
|
|
||||||
pdf.fill_color "FFFFFF"
|
|
||||||
pdf.rounded_rectangle [0, pdf.cursor], 100, 100, 8
|
|
||||||
pdf.fill
|
|
||||||
|
|
||||||
# Generate QR code
|
# Generate QR code
|
||||||
pdf.print_qr_code(qr_code_data, extent: 85, align: :center)
|
pdf.print_qr_code(qr_code_data, extent: size, align: :center)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_footer(pdf)
|
def create_simple_footer(pdf)
|
||||||
pdf.move_cursor_to(40)
|
|
||||||
|
|
||||||
# Security notice
|
# Security notice
|
||||||
pdf.font "Helvetica", size: 8
|
pdf.font "Helvetica", size: 8
|
||||||
pdf.fill_color "6B7280"
|
pdf.fill_color "64748B"
|
||||||
pdf.text "[!] Ce billet est valable pour une seule entree", align: :center
|
pdf.text "Ce billet est valable pour une seule entree.", align: :center
|
||||||
pdf.text "Presentez ce billet a l'entree de l'evenement", align: :center
|
pdf.text "Presentez ce code QR a l'entree de l'evenement.", align: :center
|
||||||
|
|
||||||
pdf.move_down 8
|
pdf.move_down 10
|
||||||
|
|
||||||
# Generation timestamp with modern styling
|
# Divider line
|
||||||
|
pdf.stroke_color "E5E7EB"
|
||||||
|
pdf.horizontal_line 0, pdf.bounds.width
|
||||||
|
pdf.move_down 5
|
||||||
|
|
||||||
|
# Generation timestamp
|
||||||
pdf.font "Helvetica", size: 7
|
pdf.font "Helvetica", size: 7
|
||||||
pdf.fill_color "9CA3AF"
|
pdf.fill_color "9CA3AF"
|
||||||
timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}"
|
timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}"
|
||||||
pdf.text timestamp, align: :center, character_spacing: 0.3
|
pdf.text timestamp, align: :center
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def build_qr_code_data(ticket)
|
def build_qr_code_data(ticket)
|
||||||
# Try multiple approaches to get valid QR code data
|
# Try multiple approaches to get valid QR code data
|
||||||
begin
|
begin
|
||||||
|
|||||||
Reference in New Issue
Block a user