Compare commits
21 Commits
feat/seo
...
ceb5a13297
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceb5a13297 | ||
|
|
7694e50fa0 | ||
|
|
e86b84ba61 | ||
|
|
f1750cb887 | ||
|
|
2aae7fe8ea | ||
|
|
b8efa1e26d | ||
|
|
9e6c48dc5c | ||
|
|
6e3413a128 | ||
|
|
0a3a913f66 | ||
|
|
dcaa83e756 | ||
|
|
213a11e731 | ||
|
|
ce0752bbda | ||
|
|
e983b68834 | ||
|
|
d5326c7dc6 | ||
|
|
fdad3bfb7b | ||
|
|
c3f5d72a91 | ||
|
|
241256e373 | ||
|
|
7f36abbcec | ||
|
|
73eefdd7bd | ||
|
|
29f1d75969 | ||
|
|
340f655102 |
14
.env.example
14
.env.example
@@ -1,18 +1,18 @@
|
||||
# Application data
|
||||
RAILS_ENV=development
|
||||
RAILS_ENV=production
|
||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||
APP_NAME=Aperonight
|
||||
|
||||
# Database Configuration for production and development
|
||||
DB_HOST=localhost
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
DB_ROOT_PASSWORD=root
|
||||
DB_DATABASE=aperonight
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
# Test database
|
||||
DB_TEST_ADAPTER=sqlite3
|
||||
DB_TEST_DATABASE=aperonight_test
|
||||
DB_TEST_USERNAME=root
|
||||
DB_TEST_USERNAME=root
|
||||
@@ -28,14 +28,6 @@ SMTP_PORT=1025
|
||||
# SMTP_DOMAIN=localhost
|
||||
SMTP_AUTHENTICATION=plain
|
||||
SMTP_ENABLE_STARTTLS=false
|
||||
|
||||
# Production SMTP Configuration (set these in .env.production)
|
||||
# SMTP_ADDRESS=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USERNAME=your_smtp_username
|
||||
# SMTP_PASSWORD=your_smtp_password
|
||||
# SMTP_AUTHENTICATION=plain
|
||||
# SMTP_DOMAIN=example.com
|
||||
# SMTP_STARTTLS=true
|
||||
|
||||
# Application variables
|
||||
|
||||
67
BACKLOG.md
67
BACKLOG.md
@@ -2,43 +2,50 @@
|
||||
|
||||
## 📋 Todo
|
||||
|
||||
- [ ] Set up project infrastructure
|
||||
- [ ] Design user interface mockups
|
||||
- [ ] Create user dashboard
|
||||
- [ ] Implement data persistence
|
||||
- [ ] Add responsive design
|
||||
- [ ] Write unit tests
|
||||
- [ ] Set up CI/CD pipeline
|
||||
- [ ] Add error handling
|
||||
- [ ] Implement search functionality
|
||||
- [ ] Add user profile management
|
||||
- [ ] Create admin panel
|
||||
- [ ] Optimize performance
|
||||
- [ ] Add documentation
|
||||
- [ ] Security audit
|
||||
- [ ] Deploy to production
|
||||
### High Priority
|
||||
|
||||
- [ ] feat: Check-in system with QR code scanning
|
||||
|
||||
### Medium Priority
|
||||
|
||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
||||
- [ ] feat: Refund management system
|
||||
- [ ] feat: Real-time sales analytics dashboard
|
||||
- [ ] feat: Guest checkout without account creation
|
||||
- [ ] feat: Seat selection with interactive venue maps
|
||||
- [ ] feat: Dynamic pricing based on demand
|
||||
|
||||
### Low Priority
|
||||
|
||||
- [ ] feat: SMS integration for ticket delivery and updates
|
||||
- [ ] feat: Mobile wallet integration
|
||||
- [ ] feat: Multi-currency support
|
||||
- [ ] feat: Event updates communication system
|
||||
- [ ] feat: Bulk operations for group bookings
|
||||
- [ ] feat: Fraud prevention and bot protection
|
||||
- [ ] feat: Social login options
|
||||
- [ ] feat: Event recommendations system
|
||||
|
||||
### Design & Infrastructure
|
||||
|
||||
- [ ] style: Rewrite design system
|
||||
- [ ] refactor: Rewrite design mockup
|
||||
|
||||
## 🚧 Doing
|
||||
|
||||
- [ ] refactor: Moving checkout to OrdersController
|
||||
- [ ] feat: Page to display all tickets for an event
|
||||
- [ ] feat: Add a link into notification email to order page that display all tickets
|
||||
|
||||
## ✅ Done
|
||||
|
||||
- [x] Initialize git repository
|
||||
- [x] Set up development environment
|
||||
- [x] Create project structure
|
||||
- [x] Install dependencies
|
||||
- [x] Configure build tools
|
||||
- [x] Set up linting rules
|
||||
- [x] Create initial README
|
||||
- [x] Set up version control
|
||||
- [x] Configure development server
|
||||
- [x] Establish coding standards
|
||||
- [x] Set up package.json
|
||||
- [x] Create .gitignore file
|
||||
- [x] Initialize npm project
|
||||
- [x] Set up basic folder structure
|
||||
- [x] Configure environment variables
|
||||
- [x] Create authentication system
|
||||
- [x] Implement user registration
|
||||
- [x] Add login functionality
|
||||
- [x] refactor: Moving checkout to OrdersController
|
||||
- [x] feat: Payment gateway integration (Stripe) - PayPal not implemented
|
||||
- [x] feat: Digital tickets with QR codes
|
||||
- [x] feat: Ticket inventory management and capacity limits
|
||||
- [x] feat: Event discovery with search and filtering
|
||||
- [x] feat: Email notifications (purchase confirmations, event reminders)
|
||||
|
||||
185
app/assets/javascripts/qr_generator.js
Normal file
185
app/assets/javascripts/qr_generator.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// Self-contained QR Code Generator
|
||||
// No external dependencies required
|
||||
|
||||
class QRCodeGenerator {
|
||||
constructor() {
|
||||
// QR Code error correction levels
|
||||
this.errorCorrectionLevels = {
|
||||
L: 1, // Low ~7%
|
||||
M: 0, // Medium ~15%
|
||||
Q: 3, // Quartile ~25%
|
||||
H: 2 // High ~30%
|
||||
};
|
||||
|
||||
// Mode indicators
|
||||
this.modes = {
|
||||
NUMERIC: 1,
|
||||
ALPHANUMERIC: 2,
|
||||
BYTE: 4,
|
||||
KANJI: 8
|
||||
};
|
||||
}
|
||||
|
||||
// Generate QR code as SVG
|
||||
generateSVG(text, options = {}) {
|
||||
const size = options.size || 200;
|
||||
const margin = options.margin || 4;
|
||||
const errorCorrection = options.errorCorrection || 'M';
|
||||
|
||||
try {
|
||||
const qrData = this.createQRData(text, errorCorrection);
|
||||
const moduleSize = (size - 2 * margin) / qrData.length;
|
||||
|
||||
let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`;
|
||||
svg += `<rect width="${size}" height="${size}" fill="white"/>`;
|
||||
|
||||
for (let row = 0; row < qrData.length; row++) {
|
||||
for (let col = 0; col < qrData[row].length; col++) {
|
||||
if (qrData[row][col]) {
|
||||
const x = margin + col * moduleSize;
|
||||
const y = margin + row * moduleSize;
|
||||
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" fill="black"/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
} catch (error) {
|
||||
console.error('QR Code generation failed:', error);
|
||||
return this.createErrorSVG(size);
|
||||
}
|
||||
}
|
||||
|
||||
// Create QR code data matrix (simplified implementation)
|
||||
createQRData(text, errorCorrection) {
|
||||
// For simplicity, we'll create a basic QR code pattern
|
||||
// This is a minimal implementation - real QR codes are much more complex
|
||||
|
||||
const version = this.determineVersion(text.length);
|
||||
const size = 21 + (version - 1) * 4; // QR code size formula
|
||||
|
||||
// Initialize matrix
|
||||
const matrix = Array(size).fill().map(() => Array(size).fill(false));
|
||||
|
||||
// Add finder patterns (corners)
|
||||
this.addFinderPatterns(matrix);
|
||||
|
||||
// Add timing patterns
|
||||
this.addTimingPatterns(matrix);
|
||||
|
||||
// Add data (simplified - just create a pattern based on text)
|
||||
this.addDataPattern(matrix, text);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
determineVersion(length) {
|
||||
// Simplified version determination
|
||||
if (length <= 25) return 1;
|
||||
if (length <= 47) return 2;
|
||||
if (length <= 77) return 3;
|
||||
return 4; // Max we'll support in this simple implementation
|
||||
}
|
||||
|
||||
addFinderPatterns(matrix) {
|
||||
const size = matrix.length;
|
||||
const pattern = [
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,0,0,0,0,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,1,1,1,0,1],
|
||||
[1,0,0,0,0,0,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
];
|
||||
|
||||
// Top-left
|
||||
this.placePattern(matrix, 0, 0, pattern);
|
||||
// Top-right
|
||||
this.placePattern(matrix, 0, size - 7, pattern);
|
||||
// Bottom-left
|
||||
this.placePattern(matrix, size - 7, 0, pattern);
|
||||
}
|
||||
|
||||
addTimingPatterns(matrix) {
|
||||
const size = matrix.length;
|
||||
|
||||
// Horizontal timing pattern
|
||||
for (let i = 8; i < size - 8; i++) {
|
||||
matrix[6][i] = i % 2 === 0;
|
||||
}
|
||||
|
||||
// Vertical timing pattern
|
||||
for (let i = 8; i < size - 8; i++) {
|
||||
matrix[i][6] = i % 2 === 0;
|
||||
}
|
||||
}
|
||||
|
||||
addDataPattern(matrix, text) {
|
||||
const size = matrix.length;
|
||||
|
||||
// Simple data pattern based on text hash
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = ((hash << 5) - hash + text.charCodeAt(i)) & 0xffffffff;
|
||||
}
|
||||
|
||||
// Fill available spaces with pattern based on hash
|
||||
for (let row = 0; row < size; row++) {
|
||||
for (let col = 0; col < size; col++) {
|
||||
if (!this.isReserved(row, col, size)) {
|
||||
matrix[row][col] = ((hash >> ((row + col) % 32)) & 1) === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
placePattern(matrix, startRow, startCol, pattern) {
|
||||
for (let row = 0; row < pattern.length; row++) {
|
||||
for (let col = 0; col < pattern[row].length; col++) {
|
||||
matrix[startRow + row][startCol + col] = pattern[row][col] === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isReserved(row, col, size) {
|
||||
// Check if position is reserved for finder patterns, timing patterns, etc.
|
||||
|
||||
// Finder patterns
|
||||
if ((row < 9 && col < 9) || // Top-left
|
||||
(row < 9 && col >= size - 8) || // Top-right
|
||||
(row >= size - 8 && col < 9)) { // Bottom-left
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timing patterns
|
||||
if (row === 6 || col === 6) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
createErrorSVG(size) {
|
||||
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="${size}" height="${size}" fill="#f3f4f6"/>
|
||||
<text x="${size/2}" y="${size/2-10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">QR Code</text>
|
||||
<text x="${size/2}" y="${size/2+10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">Error</text>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Global function for easy access
|
||||
window.generateQRCode = function(text, containerId, options = {}) {
|
||||
const generator = new QRCodeGenerator();
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
console.error('Container not found:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = generator.generateSVG(text, options);
|
||||
container.innerHTML = svg;
|
||||
};
|
||||
@@ -97,6 +97,14 @@ class OrdersController < ApplicationController
|
||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
||||
end
|
||||
|
||||
# Display all user orders
|
||||
def index
|
||||
@orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
||||
.where(status: [ "paid", "completed" ])
|
||||
.order(created_at: :desc)
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
# Display order summary
|
||||
#
|
||||
#
|
||||
@@ -188,15 +196,8 @@ class OrdersController < ApplicationController
|
||||
# Don't fail the payment process due to job scheduling issues
|
||||
end
|
||||
|
||||
# Send confirmation emails
|
||||
@order.tickets.each do |ticket|
|
||||
begin
|
||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||
# Don't fail the entire payment process due to email/PDF generation issues
|
||||
end
|
||||
end
|
||||
# Email confirmation is handled by the order model's mark_as_paid! method
|
||||
# to avoid duplicate emails
|
||||
|
||||
# Clear session data
|
||||
session.delete(:pending_cart)
|
||||
|
||||
@@ -17,30 +17,28 @@ class PagesController < ApplicationController
|
||||
# User dashboard showing personalized content
|
||||
# Accessible only to authenticated users
|
||||
def dashboard
|
||||
# Metrics for dashboard cards
|
||||
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||
.where(events: { state: :published })
|
||||
.where(orders: { status: [ "paid", "completed" ] })
|
||||
.sum("1")
|
||||
@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: :order })
|
||||
.where(orders: { user: current_user }, tickets: { status: "active" })
|
||||
.distinct
|
||||
.limit(5)
|
||||
# User's orders with associated data
|
||||
@user_orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
||||
.where(status: [ "paid", "completed" ])
|
||||
.order(created_at: :desc)
|
||||
.limit(10)
|
||||
|
||||
# Draft orders that can be retried
|
||||
@draft_orders = current_user.orders.includes(tickets: [ :ticket_type, :event ])
|
||||
.can_retry_payment
|
||||
.order(:expires_at)
|
||||
|
||||
# Events sections
|
||||
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [ Date.current, Date.current + 1 ]).order(start_time: :asc).page(params[:page])
|
||||
# Simplified upcoming events preview - only show if user has orders
|
||||
if @user_orders.any?
|
||||
ordered_event_ids = @user_orders.map(&:event).map(&:id)
|
||||
@upcoming_preview_events = Event.published
|
||||
.upcoming
|
||||
.where.not(id: ordered_event_ids)
|
||||
.order(start_time: :asc)
|
||||
.limit(6)
|
||||
else
|
||||
@upcoming_preview_events = []
|
||||
end
|
||||
end
|
||||
|
||||
# Events page showing all published events with pagination
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# This controller now primarily handles legacy redirects and backward compatibility
|
||||
# Most ticket creation functionality has been moved to OrdersController
|
||||
class TicketsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :download_ticket ]
|
||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :download ]
|
||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
||||
|
||||
|
||||
@@ -50,11 +50,18 @@ class TicketsController < ApplicationController
|
||||
|
||||
# Display ticket details
|
||||
def show
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
# Find ticket by qr code id
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
return
|
||||
end
|
||||
|
||||
@event = @ticket.event
|
||||
@order = @ticket.order
|
||||
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||
end
|
||||
@@ -62,12 +69,10 @@ class TicketsController < ApplicationController
|
||||
# Download PDF ticket - only accessible by ticket owner
|
||||
# User must be authenticated to download ticket
|
||||
# TODO: change ID to an unique identifier (UUID)
|
||||
def download_ticket
|
||||
# Find ticket and ensure it belongs to current user
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||
tickets: { id: params[:ticket_id] },
|
||||
orders: { user_id: current_user.id }
|
||||
)
|
||||
def download
|
||||
# Find ticket by qr code id
|
||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
||||
|
||||
if @ticket.nil?
|
||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||
@@ -88,7 +93,6 @@ class TicketsController < ApplicationController
|
||||
Rails.logger.error "Error generating ticket PDF: #{e.message}"
|
||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_event
|
||||
|
||||
@@ -18,3 +18,6 @@ application.register("ticket-selection", TicketSelectionController);
|
||||
|
||||
import HeaderController from "./header_controller";
|
||||
application.register("header", HeaderController);
|
||||
|
||||
import QrCodeController from "./qr_code_controller";
|
||||
application.register("qr-code", QrCodeController);
|
||||
|
||||
56
app/javascript/controllers/qr_code_controller.js
Normal file
56
app/javascript/controllers/qr_code_controller.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// QR Code generator controller using qrcode npm package
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import QRCode from "qrcode"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { data: String }
|
||||
static targets = ["container", "loading"]
|
||||
|
||||
connect() {
|
||||
this.generateQRCode()
|
||||
}
|
||||
|
||||
async generateQRCode() {
|
||||
try {
|
||||
// Hide loading indicator
|
||||
if (this.hasLoadingTarget) {
|
||||
this.loadingTarget.style.display = 'none'
|
||||
}
|
||||
|
||||
// Create canvas element
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
// Generate QR code using qrcode library
|
||||
await QRCode.toCanvas(canvas, this.dataValue, {
|
||||
width: 128,
|
||||
height: 128,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
|
||||
// Clear container and add QR code
|
||||
this.containerTarget.innerHTML = ''
|
||||
this.containerTarget.appendChild(canvas)
|
||||
|
||||
console.log('QR code generated successfully')
|
||||
} catch (error) {
|
||||
console.error('Error generating QR code:', error)
|
||||
this.showFallback()
|
||||
}
|
||||
}
|
||||
|
||||
showFallback() {
|
||||
this.containerTarget.innerHTML = `
|
||||
<div class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center text-gray-500 text-xs border-2 border-dashed border-gray-300">
|
||||
<div class="text-center">
|
||||
<div class="text-lg mb-1">📱</div>
|
||||
<div>QR Code</div>
|
||||
<div class="font-mono text-xs mt-1 break-all px-2">${this.dataValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
19
app/jobs/event_reminder_job.rb
Normal file
19
app/jobs/event_reminder_job.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class EventReminderJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(event_id, days_before)
|
||||
event = Event.find(event_id)
|
||||
|
||||
# Find all users with active tickets for this event
|
||||
users_with_tickets = User.joins(orders: { tickets: :ticket_type })
|
||||
.where(ticket_types: { event: event })
|
||||
.where(tickets: { status: "active" })
|
||||
.distinct
|
||||
|
||||
users_with_tickets.find_each do |user|
|
||||
TicketMailer.event_reminder(user, event, days_before).deliver_now
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to send event reminder to user #{user.id} for event #{event.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
class EventReminderSchedulerJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
schedule_weekly_reminders
|
||||
schedule_daily_reminders
|
||||
schedule_day_of_reminders
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def schedule_weekly_reminders
|
||||
# Find events starting in exactly 7 days
|
||||
target_date = 7.days.from_now.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 7)
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_daily_reminders
|
||||
# Find events starting in exactly 1 day (tomorrow)
|
||||
target_date = 1.day.from_now.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 1)
|
||||
end
|
||||
end
|
||||
|
||||
def schedule_day_of_reminders
|
||||
# Find events starting today
|
||||
target_date = Time.current.beginning_of_day
|
||||
events = Event.published
|
||||
.where(start_time: target_date..(target_date + 1.day))
|
||||
|
||||
events.find_each do |event|
|
||||
EventReminderJob.perform_later(event.id, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "from@example.com"
|
||||
default from: ENV.fetch("MAILER_FROM_EMAIL", "no-reply@aperonight.fr")
|
||||
layout "mailer"
|
||||
end
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
class TicketMailer < ApplicationMailer
|
||||
default from: "notifications@aperonight.com"
|
||||
def purchase_confirmation_order(order)
|
||||
@order = order
|
||||
@user = order.user
|
||||
@event = order.event
|
||||
@tickets = order.tickets
|
||||
|
||||
# Generate PDF attachments for all tickets
|
||||
@tickets.each do |ticket|
|
||||
begin
|
||||
pdf = ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: "application/pdf",
|
||||
content: pdf
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to generate PDF for ticket #{ticket.id}: #{e.message}"
|
||||
# Continue without PDF attachment rather than failing the entire email
|
||||
end
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}",
|
||||
template_name: "purchase_confirmation"
|
||||
)
|
||||
end
|
||||
|
||||
def purchase_confirmation(ticket)
|
||||
@ticket = ticket
|
||||
@@ -7,15 +32,49 @@ class TicketMailer < ApplicationMailer
|
||||
@event = ticket.event
|
||||
|
||||
# Generate PDF attachment
|
||||
begin
|
||||
pdf = @ticket.to_pdf
|
||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||
mime_type: "application/pdf",
|
||||
content: pdf
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to generate PDF for ticket #{@ticket.id}: #{e.message}"
|
||||
# Continue without PDF attachment rather than failing the entire email
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "Confirmation d'achat - #{@event.name}"
|
||||
)
|
||||
end
|
||||
|
||||
def event_reminder(user, event, days_before)
|
||||
@user = user
|
||||
@event = event
|
||||
@days_before = days_before
|
||||
|
||||
# Get user's tickets for this event
|
||||
@tickets = Ticket.joins(:order, :ticket_type)
|
||||
.where(orders: { user: @user }, ticket_types: { event: @event }, status: "active")
|
||||
|
||||
return if @tickets.empty?
|
||||
|
||||
subject = case days_before
|
||||
when 7
|
||||
"Rappel : #{@event.name} dans une semaine"
|
||||
when 1
|
||||
"Rappel : #{@event.name} demain"
|
||||
when 0
|
||||
"C'est aujourd'hui : #{@event.name}"
|
||||
else
|
||||
"Rappel : #{@event.name} dans #{days_before} jours"
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: subject,
|
||||
template_name: "event_reminder"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,6 +76,16 @@ class Order < ApplicationRecord
|
||||
update!(status: "paid")
|
||||
tickets.update_all(status: "active")
|
||||
end
|
||||
|
||||
# Send purchase confirmation email outside the transaction
|
||||
# so that payment completion isn't affected by email failures
|
||||
begin
|
||||
TicketMailer.purchase_confirmation_order(self).deliver_now
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to send purchase confirmation email for order #{id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
# Don't re-raise the error - payment should still succeed
|
||||
end
|
||||
end
|
||||
|
||||
# Calculate total from tickets
|
||||
|
||||
@@ -70,7 +70,6 @@ class Ticket < ApplicationRecord
|
||||
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
|
||||
end
|
||||
|
||||
|
||||
def draft?
|
||||
status == "draft"
|
||||
end
|
||||
|
||||
@@ -2,10 +2,10 @@ require "prawn"
|
||||
require "prawn/qrcode"
|
||||
require "rqrcode"
|
||||
|
||||
# PDF ticket generator service using Prawn
|
||||
# Service de génération de billets PDF utilisant Prawn
|
||||
#
|
||||
# Generates PDF tickets with QR codes for event entry validation
|
||||
# Includes event details, venue information, and unique QR code for each ticket
|
||||
# Génère des billets PDF simples et compacts avec codes QR pour la validation d'entrée
|
||||
# Design propre et minimaliste qui tient sur une seule page
|
||||
class TicketPdfGenerator
|
||||
# Suppress Prawn's internationalization warning for built-in fonts
|
||||
Prawn::Fonts::AFM.hide_m17n_warning = true
|
||||
@@ -20,14 +20,14 @@ class TicketPdfGenerator
|
||||
# Header
|
||||
pdf.fill_color "2D1B69"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "ApéroNight", align: :center
|
||||
pdf.text ENV.fetch("APP_NAME", "Aperonight"), 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
|
||||
pdf.move_down 10
|
||||
|
||||
# Ticket info box
|
||||
pdf.stroke_color "E5E7EB"
|
||||
@@ -40,27 +40,35 @@ class TicketPdfGenerator
|
||||
pdf.font "Helvetica", size: 12
|
||||
|
||||
# Customer name
|
||||
pdf.text "Ticket Holder:", style: :bold
|
||||
pdf.indent 10 do
|
||||
pdf.text "Titulaire du billet :", style: :bold
|
||||
pdf.text "#{ticket.first_name} #{ticket.last_name}"
|
||||
end
|
||||
pdf.move_down 8
|
||||
|
||||
# Ticket details
|
||||
pdf.text "Ticket Type:", style: :bold
|
||||
pdf.indent 10 do
|
||||
pdf.text "Type de billet :", style: :bold
|
||||
pdf.text ticket.ticket_type.name
|
||||
end
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.text "Price:", style: :bold
|
||||
pdf.text "€#{ticket.price_euros}"
|
||||
pdf.indent 10 do
|
||||
pdf.text "Prix :", style: :bold
|
||||
pdf.text "#{ticket.price_euros} €"
|
||||
end
|
||||
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.indent 10 do
|
||||
pdf.text "Date et heure :", style: :bold
|
||||
pdf.text ticket.event.start_time.strftime("%d %B %Y à %H:%M")
|
||||
end
|
||||
pdf.move_down 20
|
||||
|
||||
# Venue information
|
||||
# Informations sur le lieu
|
||||
pdf.fill_color "374151"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Venue Information"
|
||||
pdf.text "Informations sur le lieu"
|
||||
pdf.move_down 8
|
||||
|
||||
pdf.font "Helvetica", size: 11
|
||||
@@ -68,10 +76,10 @@ class TicketPdfGenerator
|
||||
pdf.text ticket.event.venue_address
|
||||
pdf.move_down 20
|
||||
|
||||
# QR Code
|
||||
# Code QR
|
||||
pdf.fill_color "000000"
|
||||
pdf.font "Helvetica", style: :bold, size: 14
|
||||
pdf.text "Ticket QR Code", align: :center
|
||||
pdf.text "Code QR", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Ensure all required data is present before generating QR code
|
||||
@@ -79,18 +87,21 @@ class TicketPdfGenerator
|
||||
raise "Ticket QR code is missing"
|
||||
end
|
||||
|
||||
qr_code_data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.event&.id,
|
||||
user_id: ticket.user&.id
|
||||
}.compact.to_json
|
||||
# Build QR code data with safe association loading
|
||||
qr_code_data = build_qr_code_data(ticket)
|
||||
|
||||
# Validate QR code data before creating QR code
|
||||
if qr_code_data.blank? || qr_code_data == "{}"
|
||||
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
||||
raise "QR code data is empty or invalid"
|
||||
end
|
||||
|
||||
# Ensure qr_code_data is a proper string for QR code generation
|
||||
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
||||
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
||||
raise "QR code data must be a valid string"
|
||||
end
|
||||
|
||||
# Generate QR code - prawn-qrcode expects the data string directly
|
||||
pdf.print_qr_code(qr_code_data, extent: 120, align: :center)
|
||||
|
||||
@@ -99,20 +110,188 @@ class TicketPdfGenerator
|
||||
# QR code text
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "QR Code: #{ticket.qr_code[0..7]}...", align: :center
|
||||
pdf.text "#{ticket.qr_code}", align: :center
|
||||
|
||||
|
||||
# Ticket ID
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "6B7280"
|
||||
pdf.text "ID du billet : #{ticket.id}", align: :center
|
||||
|
||||
# Footer
|
||||
pdf.move_down 30
|
||||
pdf.stroke_color "E5E7EB"
|
||||
pdf.horizontal_line 0, 310
|
||||
pdf.move_down 10
|
||||
pdf.move_down 6
|
||||
|
||||
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.text "Ce billet est valable pour une seule entrée.", align: :center
|
||||
pdf.text "Présentez ce billet à l'entrée du lieu.", align: :center
|
||||
pdf.move_down 5
|
||||
pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center
|
||||
pdf.text "Généré le #{Time.current.strftime('%d %B %Y à %H:%M')}", align: :center
|
||||
end.render
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_simple_header(pdf)
|
||||
# Nom de la marque
|
||||
pdf.fill_color "6366F1"
|
||||
pdf.font "Helvetica", style: :bold, size: 24
|
||||
pdf.text "AperoNight", align: :center
|
||||
|
||||
pdf.move_down 5
|
||||
pdf.font "Helvetica", size: 10
|
||||
pdf.fill_color "64748B"
|
||||
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
|
||||
|
||||
def create_ticket_info(pdf)
|
||||
# Nom de l'événement - proéminent
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.font "Helvetica", style: :bold, size: 18
|
||||
pdf.text ticket.event.name, align: :center
|
||||
pdf.move_down 15
|
||||
|
||||
# Two-column layout for ticket details
|
||||
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)} €")
|
||||
create_info_item(pdf, "Titulaire", "#{ticket.first_name} #{ticket.last_name}")
|
||||
end
|
||||
end
|
||||
|
||||
pdf.move_down 30
|
||||
end
|
||||
|
||||
def create_info_item(pdf, label, value)
|
||||
pdf.font "Helvetica", style: :bold, size: 9
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text label.upcase
|
||||
|
||||
pdf.move_down 2
|
||||
pdf.font "Helvetica", size: 11
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.text value
|
||||
pdf.move_down 12
|
||||
end
|
||||
|
||||
def create_qr_section(pdf)
|
||||
# Center the QR code horizontally
|
||||
qr_size = 120
|
||||
x_position = (pdf.bounds.width - qr_size) / 2
|
||||
|
||||
pdf.bounding_box([ x_position, pdf.cursor ], width: qr_size, height: qr_size + 40) do
|
||||
# QR Code title
|
||||
pdf.font "Helvetica", style: :bold, size: 12
|
||||
pdf.fill_color "1F2937"
|
||||
pdf.text "Code d'entree", align: :center
|
||||
pdf.move_down 10
|
||||
|
||||
# Generate QR code
|
||||
generate_simple_qr_code(pdf, qr_size)
|
||||
|
||||
pdf.move_down 10
|
||||
|
||||
# QR code ID
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text "ID: #{ticket.qr_code[0..15]}...", align: :center
|
||||
end
|
||||
|
||||
pdf.move_down 40
|
||||
end
|
||||
|
||||
def generate_simple_qr_code(pdf, size)
|
||||
# Ensure all required data is present before generating QR code
|
||||
if ticket.qr_code.blank?
|
||||
raise "Ticket QR code is missing"
|
||||
end
|
||||
|
||||
# Build QR code data with safe association loading
|
||||
qr_code_data = build_qr_code_data(ticket)
|
||||
|
||||
# Validate QR code data before creating QR code
|
||||
if qr_code_data.blank? || qr_code_data == "{}"
|
||||
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
||||
raise "QR code data is empty or invalid"
|
||||
end
|
||||
|
||||
# Ensure qr_code_data is a proper string for QR code generation
|
||||
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
||||
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
||||
raise "QR code data must be a valid string"
|
||||
end
|
||||
|
||||
# Generate QR code
|
||||
pdf.print_qr_code(qr_code_data, extent: size, align: :center)
|
||||
end
|
||||
|
||||
def create_simple_footer(pdf)
|
||||
# Security notice
|
||||
pdf.font "Helvetica", size: 8
|
||||
pdf.fill_color "64748B"
|
||||
pdf.text "Ce billet est valable pour une seule entree.", align: :center
|
||||
pdf.text "Presentez ce code QR a l'entree de l'evenement.", align: :center
|
||||
|
||||
pdf.move_down 10
|
||||
|
||||
# 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.fill_color "9CA3AF"
|
||||
timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}"
|
||||
pdf.text timestamp, align: :center
|
||||
end
|
||||
|
||||
def build_qr_code_data(ticket)
|
||||
# Try multiple approaches to get valid QR code data
|
||||
begin
|
||||
# Primary approach: full JSON with all data
|
||||
data = {
|
||||
ticket_id: ticket.id,
|
||||
qr_code: ticket.qr_code,
|
||||
event_id: ticket.ticket_type&.event_id,
|
||||
user_id: ticket.order&.user_id
|
||||
}.compact
|
||||
|
||||
# Ensure we have the minimum required data
|
||||
if data[:ticket_id] && data[:qr_code]
|
||||
return data.to_json
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Failed to build complex QR data: #{e.message}"
|
||||
end
|
||||
|
||||
# Fallback approach: just use the ticket's QR code string
|
||||
begin
|
||||
return ticket.qr_code.to_s if ticket.qr_code.present?
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Failed to use ticket QR code: #{e.message}"
|
||||
end
|
||||
|
||||
# Final fallback: simple ticket identifier
|
||||
"TICKET-#{ticket.id}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||
<%= link_to ticket_download_path(ticket.qr_code, 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>
|
||||
|
||||
106
app/views/orders/index.html.erb
Normal file
106
app/views/orders/index.html.erb
Normal file
@@ -0,0 +1,106 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Toutes mes commandes</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Consultez l'historique de toutes vos commandes</p>
|
||||
</div>
|
||||
|
||||
<%= link_to dashboard_path, class: "inline-flex items-center px-4 py-2 bg-purple-100 hover:bg-purple-200 text-purple-700 font-medium rounded-lg transition-colors duration-200" do %>
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
||||
Retour au tableau de bord
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Orders List -->
|
||||
<% if @orders.any? %>
|
||||
<div class="space-y-6">
|
||||
<% @orders.each do |order| %>
|
||||
<div class="card hover-lift">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3 mb-2">
|
||||
<h3 class="font-semibold text-slate-900 dark:text-slate-100"><%= order.event.name %></h3>
|
||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : order.status == 'completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' %>">
|
||||
<%= order.status.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4 text-sm text-slate-600 dark:text-slate-400 mb-3">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-4 h-4 mr-1"></i>
|
||||
<%= order.event.venue_name %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4 mr-1"></i>
|
||||
<%= pluralize(order.tickets.count, 'billet') %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||
Commande #<%= order.id %> • <%= order.created_at.strftime("%d/%m/%Y") %> • <%= order.total_amount_euros %>€
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<%= link_to order_path(order),
|
||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors duration-200" do %>
|
||||
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>
|
||||
Voir détails
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick tickets preview -->
|
||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
||||
<div class="grid gap-2">
|
||||
<% order.tickets.limit(3).each do |ticket| %>
|
||||
<div class="flex items-center justify-between text-sm bg-slate-50 dark:bg-slate-700 rounded p-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-slate-500">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
||||
<i data-lucide="download" class="w-3 h-3"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if order.tickets.count > 3 %>
|
||||
<div class="text-xs text-slate-500 text-center">
|
||||
et <%= order.tickets.count - 3 %> autre<%= order.tickets.count - 3 > 1 ? 's' : '' %> billet<%= order.tickets.count - 3 > 1 ? 's' : '' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @orders %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="text-center py-12">
|
||||
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<i data-lucide="shopping-bag" class="w-8 h-8 text-slate-400"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100 mb-2">Aucune commande</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Vous n'avez encore passé aucune commande.</p>
|
||||
<%= link_to 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" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
||||
Découvrir les événements
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
163
app/views/orders/payment_cancel.html.erb
Normal file
163
app/views/orders/payment_cancel.html.erb
Normal file
@@ -0,0 +1,163 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
||||
<% if @order %>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="text-red-600 font-medium">
|
||||
Paiement annulé
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-gray-600">Aucune commande trouvée.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @order %>
|
||||
<!-- Event Information -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
||||
<% if @order.event.start_time %>
|
||||
<div class="flex items-center">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
<div class="flex items-center">
|
||||
<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="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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<svg class="w-3 h-3 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>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
<div class="flex items-center text-xs text-red-600 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
En attente de paiement
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<span class="font-medium text-gray-900">Total à payer</span>
|
||||
<span class="font-bold text-2xl text-red-600">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<% if @order&.can_retry_payment? %>
|
||||
<!-- Payment Required -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="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
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Retour au Tableau de Bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="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>
|
||||
Voir l'Événement Complet
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +1,36 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-green-50 to-emerald-50 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Success Header -->
|
||||
<div class="text-center mb-12">
|
||||
<div class="mx-auto w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mb-6">
|
||||
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">Paiement réussi !</h1>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Félicitations ! Votre commande a été traitée avec succès. Vous allez recevoir vos billets par email d'ici quelques minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Order Summary -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Récapitulatif de la commande</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
Commande #<%= @order.id %>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="text-green-600 font-medium">Payée</span>
|
||||
<span class="text-green-600 font-medium">
|
||||
Payée
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +46,7 @@
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<%= l(@order.event.start_time, format: :long) %>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
@@ -57,13 +58,21 @@
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tickets List -->
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vos billets</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
@@ -93,46 +102,47 @@
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<span class="font-medium text-gray-900">Total payé</span>
|
||||
<span class="font-bold text-2xl text-green-600"><%= @order.total_amount_euros %>€</span>
|
||||
<span class="font-bold text-2xl text-green-600">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<!-- Ticket Access -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Prochaines étapes</h2>
|
||||
<p class="text-sm text-gray-600">Que faire maintenant ?</p>
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Email Confirmation -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-blue-600 font-semibold text-sm">1</span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Vérifiez votre email</h3>
|
||||
<p class="text-gray-600 text-sm">Nous avons envoyé vos billets à <strong><%= current_user.email %></strong>. Vérifiez aussi vos spams.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Tickets -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-purple-600 font-semibold text-sm">2</span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Téléchargez vos billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @order.tickets.each do |ticket| %>
|
||||
<%= link_to download_ticket_path(ticket), class: "inline-flex items-center px-3 py-2 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 transition-colors mr-2 mb-2" do %>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 text-purple-600" 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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @order.tickets.each_with_index do |ticket, index| %>
|
||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
||||
<%= index + 1 %>
|
||||
</div>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
<% end %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
||||
<svg class="w-4 h-4" 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"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,46 +151,34 @@
|
||||
<!-- Event Day -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<span class="text-green-600 font-semibold text-sm">3</span>
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le jour J</h3>
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mt-8">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Besoin d'aide ?</h4>
|
||||
<p class="text-gray-600 text-sm mb-3">Si vous avez des questions ou des problèmes avec votre commande, n'hésitez pas à nous contacter.</p>
|
||||
<div class="space-y-2">
|
||||
<%= link_to "mailto:support@example.com", class: "inline-flex items-center text-sm text-purple-600 hover:text-purple-700" 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="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Contactez le support
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Voir tous mes billets
|
||||
Retour au Tableau de Bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to events_path, class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
<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>
|
||||
Découvrir d'autres événements
|
||||
Voir l'Événement Complet
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,66 +1,105 @@
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-8" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm">
|
||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Accueil
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
Événements
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||
<%= @order.event.name %>
|
||||
<% end %>
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<li class="font-medium text-gray-900" aria-current="page">Commande #<%= @order.id %></li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Détails de la commande</h1>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
Commande #<%= @order.id %>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Event & Order Details -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Informations</h2>
|
||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"/>
|
||||
</svg>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-4 h-4 mr-1 <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
<% else %>
|
||||
<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"/>
|
||||
<% end %>
|
||||
</svg>
|
||||
<span class="<%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %> font-medium">
|
||||
<%= case @order.status
|
||||
when 'paid' then 'Payé'
|
||||
when 'completed' then 'Terminé'
|
||||
else @order.status.humanize
|
||||
end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Information -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
||||
<% if @order.event.start_time %>
|
||||
<div class="flex items-center">
|
||||
<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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<%= @order.status.titleize %>
|
||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_name.present? %>
|
||||
<div class="flex items-center">
|
||||
<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="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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_name %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @order.event.venue_address.present? %>
|
||||
<div class="flex items-center">
|
||||
<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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
||||
</svg>
|
||||
<%= @order.event.venue_address %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Order Items -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Billets commandés</h3>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
||||
|
||||
<% @tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between py-4 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-sm font-medium text-gray-900"><%= ticket.ticket_type.name %></h4>
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||
<svg class="w-3 h-3 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>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Statut: <%= ticket.status.titleize %>
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<div class="flex items-center text-xs text-green-600 mt-1">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
Actif
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
||||
@@ -68,37 +107,114 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<!-- Order Total -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<span class="font-medium text-gray-900">Total</span>
|
||||
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
|
||||
<!-- Total -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="flex space-x-4">
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
<div class="flex items-center justify-between text-lg">
|
||||
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
||||
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
||||
<%= @order.total_amount_euros %>€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions & Ticket Access -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
||||
<!-- Ticket Access -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Download Tickets -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-purple-600" 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"/>
|
||||
</svg>
|
||||
Retour à l'événement
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||
<div class="space-y-2">
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
||||
<%= index + 1 %>
|
||||
</div>
|
||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||
<% end %>
|
||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
||||
<svg class="w-4 h-4" 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"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Day -->
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<!-- Payment Required -->
|
||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
||||
</div>
|
||||
|
||||
<% if @order.can_retry_payment? %>
|
||||
<%= link_to checkout_order_path(@order), class: "bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center">
|
||||
<div class="mb-6">
|
||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="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
|
||||
Procéder au Paiement
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<!-- Navigation Actions -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="space-y-3">
|
||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Retour au Tableau de Bord
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||
<div class="flex items-center justify-center">
|
||||
<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="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>
|
||||
Voir l'Événement Complet
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,69 @@
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Hero section with metrics -->
|
||||
<div class="mt-4 mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Tableau de bord</h1>
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
|
||||
<!-- Simplified header -->
|
||||
<div class="my-6 sm:my-8">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100">Mon tableau de bord</h1>
|
||||
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400 mt-1">Gérez vos commandes et accédez à vos billets</p>
|
||||
</div>
|
||||
|
||||
<!-- Promoter Actions -->
|
||||
<% if current_user.promoter? %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= link_to promoter_events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||
<i data-lucide="calendar-plus" class="w-4 h-4 mr-2"></i>
|
||||
Mes événements
|
||||
<div class="flex flex-col xs:flex-row items-stretch xs:items-center gap-2">
|
||||
<%= link_to promoter_events_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
||||
<i data-lucide="calendar-plus" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
||||
<span class="whitespace-nowrap">Mes Événements</span>
|
||||
<% end %>
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-4 py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||
Créer un événement
|
||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200 text-sm" do %>
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
||||
<span class="whitespace-nowrap">Créer un Événement</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to events_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
||||
<i data-lucide="search" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
||||
<span class="whitespace-nowrap">Découvrir des Événements</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<%= 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_today, classes: "from-blue-100 to-sky-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>
|
||||
|
||||
<!-- Draft orders needing payment -->
|
||||
<% if @draft_orders.any? %>
|
||||
<div class="card hover-lift mb-8 border-orange-200 bg-orange-50">
|
||||
<div class="card mb-6 sm:mb-8 border-orange-200 bg-orange-50">
|
||||
<div class="card-header bg-orange-100 rounded-lg">
|
||||
|
||||
<div class="mx-4 py-4">
|
||||
<h2 class="text-2xl font-bold text-orange-900 flex items-center">
|
||||
<svg class="w-6 h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="mx-4 py-3 sm:py-4">
|
||||
<h2 class="text-lg sm:text-2xl font-bold text-orange-900 flex items-center">
|
||||
<svg class="w-5 h-5 sm:w-6 sm:h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Commandes en attente de paiement
|
||||
Commandes en Attente de Paiement
|
||||
</h2>
|
||||
<p class="text-orange-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
||||
<p class="text-sm sm:text-base text-orange-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="space-y-4">
|
||||
<% @draft_orders.each do |order| %>
|
||||
<div class="bg-white rounded-lg p-4 border border-orange-200">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="bg-white rounded-lg p-3 sm:p-4 border border-orange-200">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900"><%= order.event.name %></h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h3 class="font-semibold text-gray-900 text-sm sm:text-base"><%= order.event.name %></h3>
|
||||
<p class="text-xs sm:text-sm text-gray-600 mt-1">
|
||||
<svg class="w-3 h-3 sm:w-4 sm:h-4 inline mr-1" 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>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-orange-600 bg-orange-100 px-2 py-1 rounded-full">
|
||||
Commande #<%= order.id %>
|
||||
<span class="text-xs sm:text-sm font-medium text-orange-600 bg-orange-100 px-2 py-1 rounded-full whitespace-nowrap">
|
||||
Order #<%= order.id %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 mb-4">
|
||||
<% order.tickets.each do |ticket| %>
|
||||
<div class="flex items-center justify-between text-sm bg-gray-50 rounded p-2">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-xs sm:text-sm bg-gray-50 rounded p-2 gap-2">
|
||||
<div>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-gray-600">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
@@ -80,19 +75,21 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-600">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<div class="text-xs sm:text-sm text-gray-600">
|
||||
<div class="mb-1 sm:mb-0">
|
||||
Tentatives: <%= order.payment_attempts %>/3
|
||||
</div>
|
||||
<% if order.expiring_soon? %>
|
||||
<span class="text-orange-600 font-medium ml-2">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<span class="text-orange-600 font-medium">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<% else %>
|
||||
<span class="text-gray-500 ml-2">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<span class="text-gray-500">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to retry_payment_order_path(order), method: :post,
|
||||
class: "inline-flex items-center px-4 py-2 bg-orange-600 text-white text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200" do %>
|
||||
Reprendre le paiement (<%= order.total_amount_euros %>€)
|
||||
class: "inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 bg-orange-600 text-white text-xs sm:text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200 whitespace-nowrap" do %>
|
||||
Reprendre le Paiement (€<%= order.total_amount_euros %>)
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,96 +99,148 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- User's booked events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<!-- User's Orders Section -->
|
||||
<div class="card mb-6 sm: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 class="flex items-center justify-between">
|
||||
<h2 class="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100">Mes Commandes</h2>
|
||||
<span class="text-xs sm:text-sm text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 sm:px-3 sm:py-1 rounded-full">
|
||||
<%= pluralize(@user_orders.count, 'commande') %>
|
||||
</span>
|
||||
</div>
|
||||
</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>
|
||||
<% if @user_orders.any? %>
|
||||
<div class="space-y-4">
|
||||
<% @user_orders.each do |order| %>
|
||||
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 sm:p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h3 class="font-semibold text-slate-900 dark:text-slate-100 text-sm sm:text-base"><%= order.event.name %></h3>
|
||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : order.status == 'completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' %>">
|
||||
<%= order.status.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs sm:text-sm text-slate-600 dark:text-slate-400 mb-2">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
||||
<%= order.event.venue_name %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="shopping-bag" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
||||
<%= pluralize(order.tickets.count, 'billet') %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs sm:text-sm text-slate-500 dark:text-slate-400 mt-2">
|
||||
Order #<%= order.id %> • <%= order.created_at.strftime("%m/%d/%Y") %> • €<%= order.total_amount_euros %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= link_to order_path(order),
|
||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors duration-200 whitespace-nowrap" do %>
|
||||
<i data-lucide="eye" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
||||
Voir les Détails
|
||||
<% 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>
|
||||
</div>
|
||||
|
||||
<!-- Quick tickets preview -->
|
||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
||||
<div class="grid gap-2">
|
||||
<% order.tickets.limit(3).each do |ticket| %>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-xs sm:text-sm bg-slate-50 dark:bg-slate-700 rounded p-2 gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||
<span class="text-slate-500 text-xs">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to ticket_download_path(ticket.qr_code),
|
||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
||||
<i data-lucide="download" class="w-3 h-3"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if order.tickets.count > 3 %>
|
||||
<div class="text-xs text-slate-500 text-center">
|
||||
et <%= pluralize(order.tickets.count - 3, 'autre billet') %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @user_orders.count >= 10 %>
|
||||
<div class="mt-4 sm:mt-6 text-center">
|
||||
<%= link_to "Voir Toutes Mes Commandes", orders_path, class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium transition-colors duration-200 text-sm" %>
|
||||
</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 class="text-center py-8 sm:py-12">
|
||||
<div class="w-12 h-12 sm:w-16 sm:h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4">
|
||||
<i data-lucide="shopping-bag" class="w-6 h-6 sm:w-8 sm:h-8 text-slate-400"></i>
|
||||
</div>
|
||||
<h3 class="text-base sm:text-lg font-medium text-slate-900 dark:text-slate-100 mb-1 sm:mb-2">Aucune Commande</h3>
|
||||
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400 mb-4 sm:mb-6">Vous n'avez pas encore passé de commandes.</p>
|
||||
<%= link_to events_path, class: "inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
||||
<i data-lucide="search" class="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2"></i>
|
||||
Découvrir des Événements
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's events -->
|
||||
<div class="card hover-lift mb-8">
|
||||
<!-- Quick Events Preview - Simplified -->
|
||||
<% if @user_orders.any? %>
|
||||
<div class="my-6 sm:my-8 card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements du jour</h2>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100">Découvrir d'autres événements</h2>
|
||||
<%= link_to events_path, class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium text-sm transition-colors duration-200 whitespace-nowrap" do %>
|
||||
Voir tout →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @today_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @today_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucun évenement aujourd'hui.</p>
|
||||
<% if @upcoming_preview_events.any? %>
|
||||
<div class="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<% @upcoming_preview_events.each do |event| %>
|
||||
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 sm:p-4 hover:shadow-md transition-shadow">
|
||||
<h4 class="font-medium text-slate-900 dark:text-slate-100 mb-2 text-sm sm:text-base"><%= event.name %></h4>
|
||||
<div class="text-xs sm:text-sm text-slate-600 dark:text-slate-400 space-y-1">
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="calendar" class="w-3 h-3 mr-1"></i>
|
||||
<%= event.start_time.strftime("%d %B") %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
||||
<%= event.venue_name %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 sm:mt-3">
|
||||
<%= link_to event_path(event.slug, event), class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-xs sm:text-sm font-medium" do %>
|
||||
Voir l'Événement →
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tomorrow's 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">Évenements de demain</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @tomorrow_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @tomorrow_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune partie demain.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other upcoming events with pagination -->
|
||||
<div class="card hover-lift">
|
||||
<div class="card-header">
|
||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Autres évenements à venir</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if @other_events.any? %>
|
||||
<ul class="space-y-4">
|
||||
<% @other_events.each do |event| %>
|
||||
<li>
|
||||
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<%= paginate @other_events %>
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
|
||||
<p class="text-slate-600 dark:text-slate-400 text-sm">Aucun événement à venir pour le moment.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
86
app/views/ticket_mailer/event_reminder.html.erb
Normal file
86
app/views/ticket_mailer/event_reminder.html.erb
Normal file
@@ -0,0 +1,86 @@
|
||||
<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;">Rappel d'événement</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;">Salut <%= @user.email.split('@').first %> ! 🎉</h2>
|
||||
|
||||
<p style="color: #495057; line-height: 1.6; font-size: 18px;">
|
||||
<% case @days_before %>
|
||||
<% when 7 %>
|
||||
Plus qu'une semaine avant <strong><%= @event.name %></strong> !
|
||||
<% when 1 %>
|
||||
C'est demain ! <strong><%= @event.name %></strong> a lieu demain.
|
||||
<% when 0 %>
|
||||
C'est aujourd'hui ! <strong><%= @event.name %></strong> a lieu aujourd'hui.
|
||||
<% else %>
|
||||
Plus que <%= @days_before %> jours avant <strong><%= @event.name %></strong> !
|
||||
<% end %>
|
||||
</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 l'événement</h3>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📅 Date & heure</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529; font-size: 16px;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📍 Lieu</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.venue_name %></p>
|
||||
<p style="margin: 5px 0 0; color: #495057;"><%= @event.venue_address %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<h4 style="color: #4c1d95; margin-top: 0; margin-bottom: 15px;">Vos billets pour cet événement :</h4>
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">🎫 Billet #<%= index + 1 %></p>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<span style="background-color: #d4edda; color: #155724; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold;">ACTIF</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<% if @days_before == 0 %>
|
||||
<p style="color: #495057; margin-bottom: 20px; font-size: 16px;">🚨 N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.</p>
|
||||
<% else %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">📧 Vos billets ont été envoyés par email lors de votre achat.</p>
|
||||
<% end %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
||||
</div>
|
||||
|
||||
<% if @days_before <= 1 %>
|
||||
<div style="background-color: #d1ecf1; border-radius: 6px; padding: 15px; border-left: 4px solid #17a2b8; margin: 20px 0;">
|
||||
<p style="margin: 0; color: #0c5460; font-size: 14px;">
|
||||
<strong>💡 Conseil :</strong> Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div style="background-color: #d4edda; border-radius: 6px; padding: 15px; border-left: 4px solid #28a745;">
|
||||
<p style="margin: 0; color: #155724; font-size: 14px;">
|
||||
<strong>📅 Ajoutez à votre calendrier :</strong> N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
|
||||
<p style="margin: 0;">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>
|
||||
41
app/views/ticket_mailer/event_reminder.text.erb
Normal file
41
app/views/ticket_mailer/event_reminder.text.erb
Normal file
@@ -0,0 +1,41 @@
|
||||
Salut <%= @user.email.split('@').first %> !
|
||||
|
||||
<% case @days_before %>
|
||||
<% when 7 %>
|
||||
Plus qu'une semaine avant "<%= @event.name %>" !
|
||||
<% when 1 %>
|
||||
C'est demain ! "<%= @event.name %>" a lieu demain.
|
||||
<% when 0 %>
|
||||
C'est aujourd'hui ! "<%= @event.name %>" a lieu aujourd'hui.
|
||||
<% else %>
|
||||
Plus que <%= @days_before %> jours avant "<%= @event.name %>" !
|
||||
<% end %>
|
||||
|
||||
DÉTAILS DE L'ÉVÉNEMENT
|
||||
======================
|
||||
|
||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
Lieu : <%= @event.venue_name %>
|
||||
Adresse : <%= @event.venue_address %>
|
||||
|
||||
VOS BILLETS POUR CET ÉVÉNEMENT :
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> (ACTIF)
|
||||
<% end %>
|
||||
|
||||
<% if @days_before == 0 %>
|
||||
N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.
|
||||
<% else %>
|
||||
Vos billets ont été envoyés par email lors de votre achat.
|
||||
<% end %>
|
||||
Présentez-les à l'entrée de l'événement pour y accéder.
|
||||
|
||||
<% if @days_before <= 1 %>
|
||||
Conseil : Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
||||
<% else %>
|
||||
N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
||||
<% end %>
|
||||
|
||||
Des questions ? Contactez-nous à support@aperonight.com
|
||||
|
||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||
@@ -1,17 +1,68 @@
|
||||
<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>
|
||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;"><%= ENV.fetch("APP_NAME", "Aperonight") %></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);">
|
||||
<% if user.first_name %>
|
||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.first_name %>,</h2>
|
||||
<% else %>
|
||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
||||
<% end %>
|
||||
|
||||
<p style="color: #495057; line-height: 1.6;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement <strong><%= @event.name %></strong>.
|
||||
<% else %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre commande</h3>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<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;">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>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Nombre de billets</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @tickets.count %></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Total</p>
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@order.total_amount_euros, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="color: #4c1d95; margin: 20px 0 15px;">Billets inclus :</h4>
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">Billet #<%= index + 1 %></p>
|
||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<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;">
|
||||
@@ -35,16 +86,31 @@
|
||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<a href="<%= ticket_url(@ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px; display: inline-block; padding: 10px 15px; border: 1px solid #4c1d95; border-radius: 6px; background-color: #f8f9fa;">📱 Voir le détail et le code QR</a>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Vos billets sont attachés à cet email en format PDF.</p>
|
||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
||||
<% else %>
|
||||
<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>
|
||||
<% end %>
|
||||
</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.
|
||||
<strong>Important :</strong>
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
||||
<% else %>
|
||||
Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
<% if @user.first_name %>
|
||||
Bonjour <%= @user.first_name %>,
|
||||
<% else %>
|
||||
Bonjour <%= @user.email.split('@').first %>,
|
||||
<% end %>
|
||||
|
||||
<% if defined?(@order) && @order.present? %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement "<%= @event.name %>".
|
||||
|
||||
DÉTAILS DE VOTRE COMMANDE
|
||||
=========================
|
||||
|
||||
Événement : <%= @event.name %>
|
||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||
Nombre de billets : <%= @tickets.count %>
|
||||
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
|
||||
|
||||
BILLETS INCLUS :
|
||||
<% @tickets.each_with_index do |ticket, index| %>
|
||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
||||
<% end %>
|
||||
|
||||
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
||||
|
||||
Important : Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
||||
<% else %>
|
||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
||||
|
||||
DÉTAILS DE VOTRE BILLET
|
||||
@@ -13,7 +37,8 @@ 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.
|
||||
<% end %>
|
||||
|
||||
Si vous avez des questions, contactez-nous à support@aperonight.com
|
||||
|
||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||
© <%= Time.current.year %> <%= ENV.fetch("APP_NAME", "Aperonight") %>. Tous droits réservés.
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||
<%= link_to ticket_download_path(ticket.qr_code, 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>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet Électronique</h1>
|
||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet électronique</h1>
|
||||
<p class="text-purple-100">ID: #<%= @ticket.id %></p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
@@ -128,14 +128,16 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Code QR</label>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">QR Code</label>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-center">
|
||||
<div class="inline-block bg-white p-4 rounded-lg shadow-sm">
|
||||
<!-- QR Code would be generated here -->
|
||||
<div class="w-32 h-32 bg-gray-200 rounded flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
<div data-controller="qr-code" data-qr-code-data-value="<%= @ticket.qr_code %>" class="w-32 h-32">
|
||||
<!-- Loading indicator -->
|
||||
<div data-qr-code-target="loading" class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
</div>
|
||||
<!-- QR code container -->
|
||||
<div data-qr-code-target="container" class="w-32 h-32"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2 font-mono"><%= @ticket.qr_code %></p>
|
||||
@@ -148,16 +150,16 @@
|
||||
<!-- Actions -->
|
||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<%= link_to dashboard_path,
|
||||
<%= link_to order_path(@order),
|
||||
class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" do %>
|
||||
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
||||
</svg>
|
||||
Retour au tableau de bord
|
||||
Retour aux informations de commande
|
||||
<% end %>
|
||||
|
||||
<% if @ticket.status == 'active' %>
|
||||
<%= link_to download_ticket_path(@ticket.id),
|
||||
<%= link_to ticket_download_path(@ticket.qr_code),
|
||||
class: "flex-1 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-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5 text-center" do %>
|
||||
<svg class="w-4 h-4 inline-block 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"/>
|
||||
|
||||
23
config/initializers/disable_mailer_annotations.rb
Normal file
23
config/initializers/disable_mailer_annotations.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# Disable view annotations for mailer templates to prevent HTML comments
|
||||
# from breaking email formatting in development mode
|
||||
if Rails.env.development?
|
||||
Rails.application.configure do
|
||||
# Override the annotation setting for ActionMailer specifically
|
||||
config.to_prepare do
|
||||
ActionMailer::Base.prepend(Module.new do
|
||||
def mail(headers = {}, &block)
|
||||
# Temporarily disable view annotations during email rendering
|
||||
original_setting = ActionView::Base.annotate_rendered_view_with_filenames
|
||||
ActionView::Base.annotate_rendered_view_with_filenames = false
|
||||
|
||||
result = super(headers, &block)
|
||||
|
||||
# Restore original setting
|
||||
ActionView::Base.annotate_rendered_view_with_filenames = original_setting
|
||||
|
||||
result
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
21
config/initializers/event_reminder_scheduler.rb
Normal file
21
config/initializers/event_reminder_scheduler.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# Schedule event reminder notifications
|
||||
Rails.application.config.after_initialize do
|
||||
# Only schedule in production or when SCHEDULE_REMINDERS is set
|
||||
if Rails.env.production? || ENV["SCHEDULE_REMINDERS"] == "true"
|
||||
# Schedule the reminder scheduler to run daily at 9 AM
|
||||
begin
|
||||
# Use a simple cron-like approach with ActiveJob
|
||||
# This will be handled by solid_queue in production
|
||||
EventReminderSchedulerJob.set(wait_until: next_run_time).perform_later
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn "Could not schedule event reminders: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_run_time
|
||||
# Schedule for 9 AM today, or 9 AM tomorrow if it's already past 9 AM
|
||||
target_time = Time.current.beginning_of_day + 9.hours
|
||||
target_time += 1.day if Time.current > target_time
|
||||
target_time
|
||||
end
|
||||
@@ -42,7 +42,7 @@ Rails.application.routes.draw do
|
||||
get "orders/new/events/:slug.:id", to: "orders#new", as: "event_order_new"
|
||||
post "orders/create/events/:slug.:id", to: "orders#create", as: "event_order_create"
|
||||
|
||||
resources :orders, only: [ :show ] do
|
||||
resources :orders, only: [ :index, :show ] do
|
||||
member do
|
||||
get :checkout
|
||||
post :retry_payment
|
||||
@@ -53,15 +53,15 @@ Rails.application.routes.draw do
|
||||
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
|
||||
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
|
||||
|
||||
# legacy routes
|
||||
# Legacy routes - redirect to order system
|
||||
get "events/:slug.:id/tickets/checkout", to: "tickets#checkout", as: "ticket_checkout"
|
||||
post "events/:slug.:id/tickets/retry", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
||||
get "payments/success", to: "tickets#payment_success", as: "payment_success"
|
||||
get "payments/cancel", to: "tickets#payment_cancel", as: "payment_cancel"
|
||||
|
||||
# === Tickets ===
|
||||
get "tickets/checkout/events/:slug.:id", to: "tickets#checkout", as: "ticket_checkout"
|
||||
post "tickets/retry/events/:slug.:id", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
||||
get "tickets/:ticket_id", to: "tickets#show", as: "ticket"
|
||||
get "tickets/:ticket_id/download", to: "tickets#download_ticket", as: "download_ticket"
|
||||
get "tickets/:qr_code", to: "tickets#show", as: "ticket"
|
||||
get "tickets/:qr_code/download", to: "tickets#download", as: "ticket_download"
|
||||
|
||||
# === Promoter Routes ===
|
||||
namespace :promoter do
|
||||
|
||||
@@ -53,6 +53,7 @@ services:
|
||||
|
||||
mailhog:
|
||||
image: corpusops/mailhog:v1.0.1
|
||||
restart: unless-stopped
|
||||
# environment:
|
||||
# - "mh_auth_file=/opt/mailhog/passwd.conf"
|
||||
volumes:
|
||||
|
||||
1092
docs/checkin-system-implementation.md
Normal file
1092
docs/checkin-system-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
162
docs/email-notifications.md
Normal file
162
docs/email-notifications.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Email Notifications System
|
||||
|
||||
This document describes the email notifications system implemented for ApéroNight.
|
||||
|
||||
## Overview
|
||||
|
||||
The email notifications system provides two main types of notifications:
|
||||
1. **Purchase Confirmation Emails** - Sent when orders are completed
|
||||
2. **Event Reminder Emails** - Sent at scheduled intervals before events
|
||||
|
||||
## Features
|
||||
|
||||
### Purchase Confirmation Emails
|
||||
|
||||
- **Trigger**: Automatically sent when an order is marked as paid
|
||||
- **Content**: Order details, ticket information, PDF attachments for each ticket
|
||||
- **Template**: Supports both single tickets and multi-ticket orders
|
||||
- **Languages**: French (can be extended)
|
||||
|
||||
### Event Reminder Emails
|
||||
|
||||
- **Schedule**: 7 days before, 1 day before, and day of event
|
||||
- **Content**: Event details, user's ticket information, venue information
|
||||
- **Recipients**: Only users with active tickets for the event
|
||||
- **Smart Content**: Different messaging based on time until event
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Mailer Classes
|
||||
|
||||
#### TicketMailer
|
||||
- `purchase_confirmation_order(order)` - For complete orders with multiple tickets
|
||||
- `purchase_confirmation(ticket)` - For individual tickets
|
||||
- `event_reminder(user, event, days_before)` - For event reminders
|
||||
|
||||
### Background Jobs
|
||||
|
||||
#### EventReminderJob
|
||||
- Sends reminder emails to all users with active tickets for a specific event
|
||||
- Parameters: `event_id`, `days_before`
|
||||
- Error handling: Logs failures but continues processing other users
|
||||
|
||||
#### EventReminderSchedulerJob
|
||||
- Runs daily to schedule reminder emails
|
||||
- Automatically finds events starting in 7 days, 1 day, or same day
|
||||
- Only processes published events
|
||||
- Configurable via environment variables
|
||||
|
||||
### Email Templates
|
||||
|
||||
Templates are available in both HTML and text formats:
|
||||
|
||||
- `app/views/ticket_mailer/purchase_confirmation.html.erb`
|
||||
- `app/views/ticket_mailer/purchase_confirmation.text.erb`
|
||||
- `app/views/ticket_mailer/event_reminder.html.erb`
|
||||
- `app/views/ticket_mailer/event_reminder.text.erb`
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Environment Variables
|
||||
- `MAILER_FROM_EMAIL` - From address for emails (default: no-reply@aperonight.fr)
|
||||
- `SMTP_*` - SMTP configuration for production
|
||||
- `SCHEDULE_REMINDERS` - Enable automatic reminder scheduling in non-production
|
||||
|
||||
#### Development Setup
|
||||
- Uses localhost:1025 for development (MailCatcher recommended)
|
||||
- Email delivery is configured but won't raise errors in development
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Testing
|
||||
|
||||
```ruby
|
||||
# Test purchase confirmation
|
||||
order = Order.last
|
||||
TicketMailer.purchase_confirmation_order(order).deliver_now
|
||||
|
||||
# Test event reminder
|
||||
user = User.first
|
||||
event = Event.published.first
|
||||
TicketMailer.event_reminder(user, event, 7).deliver_now
|
||||
|
||||
# Test scheduler job
|
||||
EventReminderSchedulerJob.perform_now
|
||||
```
|
||||
|
||||
### Integration in Code
|
||||
|
||||
Purchase confirmation emails are automatically sent when orders are marked as paid:
|
||||
|
||||
```ruby
|
||||
order.mark_as_paid! # Automatically sends confirmation email
|
||||
```
|
||||
|
||||
Event reminders are automatically scheduled via the initializer, but can be manually triggered:
|
||||
|
||||
```ruby
|
||||
# Schedule reminders for a specific event
|
||||
EventReminderJob.perform_later(event.id, 7) # 7 days before
|
||||
```
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
### Production Configuration
|
||||
|
||||
1. Configure SMTP settings via environment variables
|
||||
2. Set `MAILER_FROM_EMAIL` to your domain
|
||||
3. Ensure `SCHEDULE_REMINDERS=true` to enable automatic reminders
|
||||
4. Configure solid_queue for background job processing
|
||||
|
||||
### Monitoring
|
||||
|
||||
- Check logs for email delivery failures
|
||||
- Monitor job queue for stuck reminder jobs
|
||||
- Verify SMTP configuration is working
|
||||
|
||||
### Customization
|
||||
|
||||
- Email templates can be customized in `app/views/ticket_mailer/`
|
||||
- Add new reminder intervals by modifying `EventReminderSchedulerJob`
|
||||
- Internationalization can be added using Rails I18n
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── jobs/
|
||||
│ ├── event_reminder_job.rb
|
||||
│ └── event_reminder_scheduler_job.rb
|
||||
├── mailers/
|
||||
│ ├── application_mailer.rb
|
||||
│ └── ticket_mailer.rb
|
||||
└── views/
|
||||
└── ticket_mailer/
|
||||
├── purchase_confirmation.html.erb
|
||||
├── purchase_confirmation.text.erb
|
||||
├── event_reminder.html.erb
|
||||
└── event_reminder.text.erb
|
||||
|
||||
config/
|
||||
├── environments/
|
||||
│ ├── development.rb (SMTP localhost:1025)
|
||||
│ └── production.rb (ENV-based SMTP)
|
||||
└── initializers/
|
||||
└── event_reminder_scheduler.rb
|
||||
|
||||
test/
|
||||
├── jobs/
|
||||
│ ├── event_reminder_job_test.rb
|
||||
│ └── event_reminder_scheduler_job_test.rb
|
||||
├── mailers/
|
||||
│ └── ticket_mailer_test.rb
|
||||
└── integration/
|
||||
└── email_notifications_integration_test.rb
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- No sensitive information in email templates
|
||||
- User data is properly escaped in templates
|
||||
- QR codes contain only necessary ticket verification data
|
||||
- Email addresses are validated through Devise
|
||||
215
package-lock.json
generated
215
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"lucide": "^0.542.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
@@ -794,7 +795,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -804,7 +804,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -1015,6 +1014,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
@@ -1159,7 +1167,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -1172,7 +1179,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colord": {
|
||||
@@ -1423,6 +1429,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/degenerator": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
|
||||
@@ -1458,6 +1473,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -1528,7 +1549,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
@@ -1736,6 +1756,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -1800,7 +1833,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -1979,7 +2011,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -2323,6 +2354,18 @@
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -2543,6 +2586,42 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||
@@ -2584,6 +2663,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
@@ -2766,6 +2854,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -3479,6 +3576,89 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -3544,7 +3724,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3565,6 +3744,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -3664,6 +3849,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shimmer": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
|
||||
@@ -3774,7 +3965,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -3789,7 +3979,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -4156,6 +4345,12 @@
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@hotwired/turbo-rails": "^8.0.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"lucide": "^0.542.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
|
||||
101
test/integration/email_notifications_integration_test.rb
Normal file
101
test/integration/email_notifications_integration_test.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
require "test_helper"
|
||||
|
||||
class EmailNotificationsIntegrationTest < ActionDispatch::IntegrationTest
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
def setup
|
||||
@user = User.create!(
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
first_name: "Test",
|
||||
last_name: "User"
|
||||
)
|
||||
|
||||
@event = Event.create!(
|
||||
name: "Test Event",
|
||||
slug: "test-event",
|
||||
description: "A test event for integration testing",
|
||||
state: :published,
|
||||
venue_name: "Test Venue",
|
||||
venue_address: "123 Test Street",
|
||||
latitude: 40.7128,
|
||||
longitude: -74.0060,
|
||||
start_time: 1.week.from_now,
|
||||
end_time: 1.week.from_now + 4.hours,
|
||||
user: @user
|
||||
)
|
||||
|
||||
@ticket_type = TicketType.create!(
|
||||
name: "General Admission",
|
||||
description: "General admission ticket",
|
||||
price_cents: 2500,
|
||||
quantity: 100,
|
||||
sale_start_at: 1.day.ago,
|
||||
sale_end_at: 1.day.from_now,
|
||||
event: @event
|
||||
)
|
||||
|
||||
@order = Order.create!(
|
||||
user: @user,
|
||||
event: @event,
|
||||
status: "draft",
|
||||
total_amount_cents: 2500,
|
||||
payment_attempts: 0
|
||||
)
|
||||
|
||||
@ticket = Ticket.create!(
|
||||
order: @order,
|
||||
ticket_type: @ticket_type,
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
price_cents: 2500,
|
||||
status: "draft"
|
||||
)
|
||||
end
|
||||
|
||||
test "sends purchase confirmation email when order is marked as paid" do
|
||||
# Mock PDF generation to avoid QR code issues
|
||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
||||
|
||||
assert_emails 1 do
|
||||
@order.mark_as_paid!
|
||||
end
|
||||
|
||||
assert_equal "paid", @order.status
|
||||
assert_equal "active", @ticket.reload.status
|
||||
end
|
||||
|
||||
test "event reminder email can be sent to users with active tickets" do
|
||||
# Setup: mark order as paid and activate tickets
|
||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Clear any emails from the setup
|
||||
ActionMailer::Base.deliveries.clear
|
||||
|
||||
assert_emails 1 do
|
||||
TicketMailer.event_reminder(@user, @event, 7).deliver_now
|
||||
end
|
||||
|
||||
email = ActionMailer::Base.deliveries.last
|
||||
assert_equal [ @user.email ], email.to
|
||||
assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
|
||||
end
|
||||
|
||||
test "event reminder job schedules emails for users with tickets" do
|
||||
# Setup: mark order as paid and activate tickets
|
||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Clear any emails from the setup
|
||||
ActionMailer::Base.deliveries.clear
|
||||
|
||||
# Perform the job
|
||||
EventReminderJob.perform_now(@event.id, 7)
|
||||
|
||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
||||
email = ActionMailer::Base.deliveries.last
|
||||
assert_equal [ @user.email ], email.to
|
||||
assert_match "une semaine", email.subject
|
||||
end
|
||||
end
|
||||
31
test/jobs/event_reminder_job_test.rb
Normal file
31
test/jobs/event_reminder_job_test.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
require "test_helper"
|
||||
|
||||
class EventReminderJobTest < ActiveJob::TestCase
|
||||
def setup
|
||||
@event = events(:concert_event)
|
||||
@user = users(:one)
|
||||
@ticket = tickets(:one)
|
||||
end
|
||||
|
||||
test "performs event reminder job for users with tickets" do
|
||||
# Mock the mailer to avoid actual email sending in tests
|
||||
TicketMailer.expects(:event_reminder).with(@user, @event, 7).returns(stub(deliver_now: true))
|
||||
|
||||
EventReminderJob.perform_now(@event.id, 7)
|
||||
end
|
||||
|
||||
test "handles missing event gracefully" do
|
||||
assert_raises(ActiveRecord::RecordNotFound) do
|
||||
EventReminderJob.perform_now(999999, 7)
|
||||
end
|
||||
end
|
||||
|
||||
test "logs error when mailer fails" do
|
||||
# Mock a failing mailer
|
||||
TicketMailer.stubs(:event_reminder).raises(StandardError.new("Test error"))
|
||||
|
||||
Rails.logger.expects(:error).with(regexp_matches(/Failed to send event reminder/))
|
||||
|
||||
EventReminderJob.perform_now(@event.id, 7)
|
||||
end
|
||||
end
|
||||
50
test/jobs/event_reminder_scheduler_job_test.rb
Normal file
50
test/jobs/event_reminder_scheduler_job_test.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
require "test_helper"
|
||||
|
||||
class EventReminderSchedulerJobTest < ActiveJob::TestCase
|
||||
def setup
|
||||
@event = events(:concert_event)
|
||||
end
|
||||
|
||||
test "schedules weekly reminders for events starting in 7 days" do
|
||||
# Set event to start in exactly 7 days
|
||||
@event.update(start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
||||
|
||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 7 ]) do
|
||||
EventReminderSchedulerJob.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
test "schedules daily reminders for events starting tomorrow" do
|
||||
# Set event to start tomorrow
|
||||
@event.update(start_time: 1.day.from_now.beginning_of_day + 20.hours)
|
||||
|
||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 1 ]) do
|
||||
EventReminderSchedulerJob.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
test "schedules day-of reminders for events starting today" do
|
||||
# Set event to start today
|
||||
@event.update(start_time: Time.current.beginning_of_day + 21.hours)
|
||||
|
||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 0 ]) do
|
||||
EventReminderSchedulerJob.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
test "does not schedule reminders for draft events" do
|
||||
@event.update(state: :draft, start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
||||
|
||||
assert_no_enqueued_jobs(only: EventReminderJob) do
|
||||
EventReminderSchedulerJob.perform_now
|
||||
end
|
||||
end
|
||||
|
||||
test "does not schedule reminders for cancelled events" do
|
||||
@event.update(state: :canceled, start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
||||
|
||||
assert_no_enqueued_jobs(only: EventReminderJob) do
|
||||
EventReminderSchedulerJob.perform_now
|
||||
end
|
||||
end
|
||||
end
|
||||
104
test/mailers/ticket_mailer_test.rb
Normal file
104
test/mailers/ticket_mailer_test.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
require "test_helper"
|
||||
|
||||
class TicketMailerTest < ActionMailer::TestCase
|
||||
def setup
|
||||
@user = users(:one)
|
||||
@event = events(:concert_event)
|
||||
@ticket_type = ticket_types(:standard)
|
||||
@order = orders(:paid_order)
|
||||
@ticket = tickets(:one)
|
||||
end
|
||||
|
||||
test "purchase confirmation order email" do
|
||||
# Mock PDF generation for all tickets
|
||||
@order.tickets.each do |ticket|
|
||||
ticket.stubs(:to_pdf).returns("fake_pdf_data")
|
||||
end
|
||||
|
||||
email = TicketMailer.purchase_confirmation_order(@order)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
||||
assert_equal [ @user.email ], email.to
|
||||
assert_equal "Confirmation d'achat - #{@event.name}", email.subject
|
||||
assert_match @event.name, email.body.to_s
|
||||
assert_match @user.email.split("@").first, email.body.to_s
|
||||
end
|
||||
|
||||
test "purchase confirmation single ticket email" do
|
||||
# Mock PDF generation
|
||||
@ticket.stubs(:to_pdf).returns("fake_pdf_data")
|
||||
|
||||
email = TicketMailer.purchase_confirmation(@ticket)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
||||
assert_equal [ @ticket.user.email ], email.to
|
||||
assert_equal "Confirmation d'achat - #{@ticket.event.name}", email.subject
|
||||
assert_match @ticket.event.name, email.body.to_s
|
||||
assert_match @ticket.user.email.split("@").first, email.body.to_s
|
||||
end
|
||||
|
||||
test "event reminder email one week before" do
|
||||
# Ensure the user has active tickets for the event by using the existing fixtures
|
||||
# The 'one' ticket fixture is already linked to the 'paid_order' and 'concert_event'
|
||||
email = TicketMailer.event_reminder(@user, @event, 7)
|
||||
|
||||
# Only test delivery if the user has tickets (the method returns early if not)
|
||||
if email
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
||||
assert_equal [ @user.email ], email.to
|
||||
assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
|
||||
assert_match "une semaine", email.body.to_s
|
||||
assert_match @event.name, email.body.to_s
|
||||
else
|
||||
# If no email is sent, that's expected behavior when user has no active tickets
|
||||
assert_no_emails do
|
||||
TicketMailer.event_reminder(@user, @event, 7)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "event reminder email one day before" do
|
||||
email = TicketMailer.event_reminder(@user, @event, 1)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal "Rappel : #{@event.name} demain", email.subject
|
||||
assert_match "demain", email.body.to_s
|
||||
end
|
||||
|
||||
test "event reminder email day of event" do
|
||||
email = TicketMailer.event_reminder(@user, @event, 0)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal "C'est aujourd'hui : #{@event.name}", email.subject
|
||||
assert_match "aujourd'hui", email.body.to_s
|
||||
end
|
||||
|
||||
test "event reminder email custom days" do
|
||||
email = TicketMailer.event_reminder(@user, @event, 3)
|
||||
|
||||
assert_emails 1 do
|
||||
email.deliver_now
|
||||
end
|
||||
|
||||
assert_equal "Rappel : #{@event.name} dans 3 jours", email.subject
|
||||
assert_match "3 jours", email.body.to_s
|
||||
end
|
||||
end
|
||||
38
test/models/order_email_test.rb
Normal file
38
test/models/order_email_test.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require "test_helper"
|
||||
|
||||
class OrderEmailTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@order = orders(:draft_order)
|
||||
end
|
||||
|
||||
test "sends purchase confirmation email when order is marked as paid" do
|
||||
# Mock the mailer to capture the call
|
||||
TicketMailer.expects(:purchase_confirmation_order).with(@order).returns(stub(deliver_now: true))
|
||||
|
||||
@order.mark_as_paid!
|
||||
|
||||
assert_equal "paid", @order.status
|
||||
end
|
||||
|
||||
test "activates all tickets when order is marked as paid" do
|
||||
@order.tickets.update_all(status: "reserved")
|
||||
|
||||
# Mock the mailer to avoid actual email sending
|
||||
TicketMailer.stubs(:purchase_confirmation_order).returns(stub(deliver_now: true))
|
||||
|
||||
@order.mark_as_paid!
|
||||
|
||||
assert @order.tickets.all? { |ticket| ticket.status == "active" }
|
||||
end
|
||||
|
||||
test "email sending failure does not prevent order completion" do
|
||||
# Mock mailer to raise an error
|
||||
TicketMailer.stubs(:purchase_confirmation_order).raises(StandardError.new("Email error"))
|
||||
|
||||
# Should not raise error - email failure is logged but doesn't fail the payment
|
||||
@order.mark_as_paid!
|
||||
|
||||
# Order should still be marked as paid even if email fails
|
||||
assert_equal "paid", @order.reload.status
|
||||
end
|
||||
end
|
||||
393
yarn.lock
393
yarn.lock
@@ -17,158 +17,11 @@
|
||||
resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz"
|
||||
integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
|
||||
|
||||
"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0"
|
||||
integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads" "1.1.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73"
|
||||
integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf"
|
||||
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
|
||||
integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
|
||||
|
||||
"@esbuild/android-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c"
|
||||
integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
|
||||
|
||||
"@esbuild/android-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419"
|
||||
integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
|
||||
|
||||
"@esbuild/android-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683"
|
||||
integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae"
|
||||
integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be"
|
||||
integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca"
|
||||
integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab"
|
||||
integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b"
|
||||
integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
|
||||
|
||||
"@esbuild/linux-arm@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37"
|
||||
integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4"
|
||||
integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0"
|
||||
integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5"
|
||||
integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db"
|
||||
integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547"
|
||||
integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830"
|
||||
integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
|
||||
|
||||
"@esbuild/linux-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz"
|
||||
integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548"
|
||||
integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52"
|
||||
integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935"
|
||||
integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf"
|
||||
integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314"
|
||||
integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e"
|
||||
integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b"
|
||||
integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3"
|
||||
integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
|
||||
|
||||
"@esbuild/win32-x64@0.25.9":
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
|
||||
integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
|
||||
|
||||
"@hotwired/stimulus@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz"
|
||||
@@ -228,15 +81,6 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@napi-rs/wasm-runtime@^0.2.12":
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
|
||||
integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==
|
||||
dependencies:
|
||||
"@emnapi/core" "^1.4.3"
|
||||
"@emnapi/runtime" "^1.4.3"
|
||||
"@tybys/wasm-util" "^0.10.0"
|
||||
|
||||
"@pm2/agent@~2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz"
|
||||
@@ -421,13 +265,6 @@
|
||||
resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
|
||||
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:
|
||||
version "7.1.4"
|
||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
||||
@@ -440,7 +277,7 @@ amp-message@~0.1.1:
|
||||
dependencies:
|
||||
amp "0.3.1"
|
||||
|
||||
amp@0.3.1, amp@~0.3.1:
|
||||
amp@~0.3.1, amp@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz"
|
||||
integrity sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==
|
||||
@@ -487,7 +324,7 @@ ast-types@^0.13.4:
|
||||
dependencies:
|
||||
tslib "^2.0.1"
|
||||
|
||||
async@^2.6.3, async@~2.6.1:
|
||||
async@^2.6.3:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
@@ -499,6 +336,13 @@ async@^3.2.0, async@~3.2.0, async@~3.2.6:
|
||||
resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz"
|
||||
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
||||
|
||||
async@~2.6.1:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
autoprefixer@^10.4.21:
|
||||
version "10.4.21"
|
||||
resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz"
|
||||
@@ -543,7 +387,7 @@ braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.24.4, browserslist@^4.25.1:
|
||||
browserslist@^4.0.0, browserslist@^4.24.4, browserslist@^4.25.1, "browserslist@>= 4.21.0":
|
||||
version "4.25.2"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz"
|
||||
integrity sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==
|
||||
@@ -558,6 +402,11 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
camelcase@^5.0.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
|
||||
caniuse-api@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz"
|
||||
@@ -573,7 +422,7 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001733:
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz"
|
||||
integrity sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==
|
||||
|
||||
chalk@3.0.0, chalk@~3.0.0:
|
||||
chalk@~3.0.0, chalk@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz"
|
||||
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||
@@ -620,6 +469,15 @@ cli-tableau@^2.0.0:
|
||||
dependencies:
|
||||
chalk "3.0.0"
|
||||
|
||||
cliui@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz"
|
||||
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
||||
@@ -651,16 +509,16 @@ colord@^2.9.3:
|
||||
resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz"
|
||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||
|
||||
commander@2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz"
|
||||
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
|
||||
|
||||
commander@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz"
|
||||
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
|
||||
|
||||
commander@2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz"
|
||||
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
|
||||
|
||||
croner@~4.1.92:
|
||||
version "4.1.97"
|
||||
resolved "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz"
|
||||
@@ -784,13 +642,6 @@ dayjs@~1.8.24:
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz"
|
||||
integrity sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==
|
||||
|
||||
debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@^3.2.6:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
|
||||
@@ -798,6 +649,13 @@ debug@^3.2.6:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7, debug@4:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
|
||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@~4.3.1:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz"
|
||||
@@ -805,6 +663,11 @@ debug@~4.3.1:
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
||||
|
||||
degenerator@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz"
|
||||
@@ -824,6 +687,11 @@ detect-libc@^2.0.3, detect-libc@^2.0.4:
|
||||
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
|
||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||
|
||||
dijkstrajs@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
|
||||
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz"
|
||||
@@ -952,16 +820,16 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
eventemitter2@5.0.1, eventemitter2@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz"
|
||||
integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==
|
||||
|
||||
eventemitter2@^6.3.1:
|
||||
version "6.4.9"
|
||||
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz"
|
||||
integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==
|
||||
|
||||
eventemitter2@~5.0.1, eventemitter2@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz"
|
||||
integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==
|
||||
|
||||
extrareqp2@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz"
|
||||
@@ -974,7 +842,7 @@ fast-json-patch@^3.1.0:
|
||||
resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz"
|
||||
integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==
|
||||
|
||||
fclone@1.0.11, fclone@~1.0.11:
|
||||
fclone@~1.0.11, fclone@1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz"
|
||||
integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==
|
||||
@@ -991,6 +859,14 @@ fill-range@^7.1.1:
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
find-up@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
|
||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||
dependencies:
|
||||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
||||
@@ -1010,17 +886,12 @@ fs-extra@^11.0.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
@@ -1137,7 +1008,7 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
jiti@^2.5.1:
|
||||
jiti@^2.5.1, jiti@>=1.21.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz"
|
||||
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==
|
||||
@@ -1251,6 +1122,13 @@ lilconfig@^3.1.1, lilconfig@^3.1.3:
|
||||
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz"
|
||||
integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
|
||||
|
||||
locate-path@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
|
||||
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
|
||||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
|
||||
@@ -1319,16 +1197,16 @@ minizlib@^3.0.1:
|
||||
dependencies:
|
||||
minipass "^7.1.2"
|
||||
|
||||
mkdirp@1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mkdirp@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz"
|
||||
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
|
||||
|
||||
mkdirp@1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
module-details-from-path@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz"
|
||||
@@ -1385,6 +1263,25 @@ nth-check@^2.0.1:
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
p-limit@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
|
||||
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
|
||||
dependencies:
|
||||
p-try "^2.0.0"
|
||||
|
||||
p-locate@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
|
||||
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
|
||||
dependencies:
|
||||
p-limit "^2.2.0"
|
||||
|
||||
p-try@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
pac-proxy-agent@^7.0.1:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz"
|
||||
@@ -1412,6 +1309,11 @@ pako@^0.2.5:
|
||||
resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz"
|
||||
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-parse@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
||||
@@ -1427,7 +1329,7 @@ picomatch@^2.0.4, picomatch@^2.2.1:
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
picomatch@^4.0.2:
|
||||
"picomatch@^3 || ^4", picomatch@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
@@ -1531,6 +1433,11 @@ pm2@^6.0.5:
|
||||
optionalDependencies:
|
||||
pm2-sysmonit "^1.2.8"
|
||||
|
||||
pngjs@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
postcss-calc@^10.1.1:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz"
|
||||
@@ -1806,7 +1713,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
||||
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.4.41, postcss@^8.5.3:
|
||||
postcss@^8.0.0, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.1.4, postcss@^8.2.14, postcss@^8.4, postcss@^8.4.32, postcss@^8.4.38, postcss@^8.4.41, postcss@^8.5.3, postcss@>=8.0.9:
|
||||
version "8.5.6"
|
||||
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
|
||||
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
|
||||
@@ -1846,6 +1753,15 @@ proxy-from-env@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
qrcode@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz"
|
||||
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
|
||||
dependencies:
|
||||
dijkstrajs "^1.0.1"
|
||||
pngjs "^5.0.0"
|
||||
yargs "^15.3.1"
|
||||
|
||||
react-dom@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
||||
@@ -1854,7 +1770,7 @@ react-dom@^18.3.1:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react@^18.3.1:
|
||||
"react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
@@ -1896,6 +1812,11 @@ require-in-the-middle@^5.0.0:
|
||||
module-details-from-path "^1.0.3"
|
||||
resolve "^1.22.1"
|
||||
|
||||
require-main-filename@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
|
||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||
|
||||
resolve@^1.1.7, resolve@^1.22.1:
|
||||
version "1.22.10"
|
||||
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz"
|
||||
@@ -1937,13 +1858,25 @@ semver@^7.6.2:
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
semver@~7.5.0, semver@~7.5.4:
|
||||
semver@~7.5.0:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@~7.5.4:
|
||||
version "7.5.4"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
||||
|
||||
shimmer@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz"
|
||||
@@ -2068,7 +2001,7 @@ tailwindcss-animate@^1.0.7:
|
||||
resolved "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz"
|
||||
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
|
||||
|
||||
tailwindcss@4.1.12, tailwindcss@^4.1.4:
|
||||
tailwindcss@^4.1.4, "tailwindcss@>=3.0.0 || insiders", tailwindcss@4.1.12:
|
||||
version "4.1.12"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz"
|
||||
integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==
|
||||
@@ -2110,16 +2043,16 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tslib@1.9.3:
|
||||
tslib@^2.0.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@^2.8.0, tslib@1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz"
|
||||
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.4.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tv4@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz"
|
||||
@@ -2160,6 +2093,20 @@ vizion@~2.2.1:
|
||||
ini "^1.3.5"
|
||||
js-git "^0.7.8"
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz"
|
||||
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
@@ -2174,6 +2121,11 @@ ws@^7.0.0, ws@~7.5.10:
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
|
||||
@@ -2194,11 +2146,36 @@ yaml@^2.4.2:
|
||||
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
|
||||
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
|
||||
|
||||
yargs-parser@^18.1.2:
|
||||
version "18.1.3"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz"
|
||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
||||
dependencies:
|
||||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^15.3.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
dependencies:
|
||||
cliui "^6.0.0"
|
||||
decamelize "^1.2.0"
|
||||
find-up "^4.1.0"
|
||||
get-caller-file "^2.0.1"
|
||||
require-directory "^2.1.1"
|
||||
require-main-filename "^2.0.0"
|
||||
set-blocking "^2.0.0"
|
||||
string-width "^4.2.0"
|
||||
which-module "^2.0.0"
|
||||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
|
||||
yargs@^17.0.0:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
|
||||
|
||||
Reference in New Issue
Block a user