🧪 **Test Infrastructure Enhancements:** - Fixed PDF generator tests by stubbing QR code generation properly - Simplified job tests by replacing complex mocking with functional testing - Added missing `expired_drafts` scope to Ticket model for job functionality - Enhanced test coverage across all components 📋 **Specific Component Fixes:** **PDF Generator Tests (17 tests):** - Added QR code mocking to avoid external dependency issues - Fixed price validation issues for zero/low price scenarios - Simplified complex mocking to focus on functional behavior - All tests now pass with proper assertions **Job Tests (14 tests):** - Replaced complex Rails logger mocking with functional testing - Fixed `expired_drafts` scope missing from Ticket model - Simplified ExpiredOrdersCleanupJob tests to focus on core functionality - Simplified CleanupExpiredDraftsJob tests to avoid brittle mocks - All job tests now pass with proper error handling **Model & Service Tests:** - Enhanced Order model tests (42 tests) with comprehensive coverage - Fixed StripeInvoiceService tests with proper Stripe API mocking - Added comprehensive validation and business logic testing - All model tests passing with edge case coverage **Infrastructure:** - Added rails-controller-testing and mocha gems for better test support - Enhanced test helpers with proper Devise integration - Fixed QR code generation in test environment - Added necessary database migrations and schema updates 🎯 **Test Coverage Summary:** - 202+ tests across the entire application - Models: Order (42 tests), Ticket, Event, User coverage - Controllers: Events (17 tests), Orders (21 tests), comprehensive actions - Services: PDF generation, Stripe integration, business logic - Jobs: Background processing, cleanup operations - All major application functionality covered 🔧 **Technical Improvements:** - Replaced fragile mocking with functional testing approaches - Added proper test data setup and teardown - Enhanced error handling and edge case coverage - Improved test maintainability and reliability 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
538 lines
29 KiB
HTML
538 lines
29 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Fête de l'Humanité 2025 - Billets</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;500;600;700&family=Fira+Code:wght@300;400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="festival_theme.css">
|
||
|
||
<style>
|
||
* {
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
box-sizing: border-box !important;
|
||
}
|
||
|
||
body {
|
||
font-family: var(--font-sans) !important;
|
||
background: var(--background) !important;
|
||
color: var(--foreground) !important;
|
||
line-height: 1.6 !important;
|
||
}
|
||
|
||
.festival-gradient {
|
||
background: linear-gradient(135deg,
|
||
oklch(0.4902 0.2314 320.7094) 0%,
|
||
oklch(0.6471 0.1686 342.5570) 50%,
|
||
oklch(0.7255 0.1451 51.2345) 100%) !important;
|
||
}
|
||
|
||
.ticket-card {
|
||
background: var(--card) !important;
|
||
border: 2px solid var(--border) !important;
|
||
border-radius: var(--radius-lg) !important;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
box-shadow: var(--shadow) !important;
|
||
}
|
||
|
||
.ticket-card:hover {
|
||
transform: translateY(-4px) !important;
|
||
box-shadow: var(--shadow-lg) !important;
|
||
border-color: var(--primary) !important;
|
||
}
|
||
|
||
.ticket-card.selected {
|
||
border-color: var(--primary) !important;
|
||
background: linear-gradient(135deg, var(--card), oklch(0.4902 0.2314 320.7094 / 0.05)) !important;
|
||
box-shadow: var(--shadow-lg) !important;
|
||
}
|
||
|
||
.quantity-control {
|
||
background: var(--muted) !important;
|
||
border: 1px solid var(--border) !important;
|
||
border-radius: var(--radius) !important;
|
||
transition: all 0.2s ease !important;
|
||
}
|
||
|
||
.quantity-control:hover {
|
||
background: var(--accent) !important;
|
||
transform: scale(1.05) !important;
|
||
}
|
||
|
||
.cart-summary {
|
||
background: linear-gradient(135deg,
|
||
var(--card),
|
||
oklch(0.4902 0.2314 320.7094 / 0.03)) !important;
|
||
border: 2px solid var(--primary) !important;
|
||
border-radius: var(--radius-xl) !important;
|
||
box-shadow: var(--shadow-md) !important;
|
||
}
|
||
|
||
.checkout-button {
|
||
background: var(--primary) !important;
|
||
color: var(--primary-foreground) !important;
|
||
border: none !important;
|
||
border-radius: var(--radius-lg) !important;
|
||
font-weight: 600 !important;
|
||
transition: all 0.3s ease !important;
|
||
box-shadow: var(--shadow) !important;
|
||
}
|
||
|
||
.checkout-button:hover:not(:disabled) {
|
||
background: oklch(0.4302 0.2314 320.7094) !important;
|
||
transform: translateY(-2px) !important;
|
||
box-shadow: var(--shadow-lg) !important;
|
||
}
|
||
|
||
.checkout-button:disabled {
|
||
background: var(--muted) !important;
|
||
color: var(--muted-foreground) !important;
|
||
cursor: not-allowed !important;
|
||
opacity: 0.5 !important;
|
||
}
|
||
|
||
.festival-info {
|
||
background: linear-gradient(45deg,
|
||
oklch(0.7255 0.1451 51.2345 / 0.1),
|
||
oklch(0.6471 0.1686 342.5570 / 0.1)) !important;
|
||
border-radius: var(--radius-lg) !important;
|
||
border: 1px solid var(--accent) !important;
|
||
}
|
||
|
||
.hero-section {
|
||
background: linear-gradient(135deg,
|
||
oklch(0.4902 0.2314 320.7094 / 0.9) 0%,
|
||
oklch(0.6471 0.1686 342.5570 / 0.9) 50%,
|
||
oklch(0.7255 0.1451 51.2345 / 0.9) 100%),
|
||
url('https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=1200&h=600&fit=crop') !important;
|
||
background-size: cover !important;
|
||
background-position: center !important;
|
||
color: white !important;
|
||
}
|
||
|
||
.animate-bounce-slow {
|
||
animation: bounce 2s infinite !important;
|
||
}
|
||
|
||
.animate-pulse-slow {
|
||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite !important;
|
||
}
|
||
|
||
.ripple-effect {
|
||
position: relative !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
.ripple-effect::after {
|
||
content: '' !important;
|
||
position: absolute !important;
|
||
top: 50% !important;
|
||
left: 50% !important;
|
||
width: 0 !important;
|
||
height: 0 !important;
|
||
border-radius: 50% !important;
|
||
background: rgba(255, 255, 255, 0.3) !important;
|
||
transform: translate(-50%, -50%) !important;
|
||
transition: width 0.4s, height 0.4s !important;
|
||
}
|
||
|
||
.ripple-effect:hover::after {
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-50">
|
||
<!-- Hero Section -->
|
||
<section class="hero-section h-96 flex items-center justify-center relative overflow-hidden">
|
||
<div class="absolute inset-0 bg-black bg-opacity-40"></div>
|
||
<div class="relative z-10 text-center max-w-4xl mx-auto px-4">
|
||
<h1 class="text-5xl md:text-6xl font-bold mb-4 font-serif animate-pulse-slow">Fête de l'Humanité 2025</h1>
|
||
<p class="text-xl md:text-2xl mb-2 opacity-90">14-16 Septembre • La Courneuve</p>
|
||
<p class="text-lg opacity-80 max-w-2xl mx-auto">Trois jours de musique, débats, culture et solidarité au cœur du plus grand festival populaire de France</p>
|
||
<div class="flex justify-center items-center mt-6 space-x-6">
|
||
<div class="flex items-center">
|
||
<i data-lucide="calendar" class="w-5 h-5 mr-2"></i>
|
||
<span>3 jours</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="music" class="w-5 h-5 mr-2"></i>
|
||
<span>100+ concerts</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="users" class="w-5 h-5 mr-2"></i>
|
||
<span>500k visiteurs</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 animate-bounce-slow">
|
||
<i data-lucide="chevron-down" class="w-8 h-8 text-white opacity-70"></i>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Main Content -->
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||
<!-- Ticket Selection Hub -->
|
||
<div class="mb-12">
|
||
<div class="text-center mb-10">
|
||
<h2 class="text-4xl font-bold text-gray-900 mb-4 font-serif">Choisissez vos billets</h2>
|
||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">Découvrez nos différentes formules pour profiter pleinement du festival</p>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
<!-- Left Column: Tickets -->
|
||
<div class="lg:col-span-2">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
|
||
<!-- Pass 3 Jours -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('pass3j', 45, 'Pass 3 jours')">
|
||
<div class="text-center">
|
||
<div class="festival-gradient w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="star" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Pass 3 Jours</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Accès complet au festival</p>
|
||
<div class="text-3xl font-bold text-primary mb-4">45€</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="pass3j-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('pass3j', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Samedi 14 -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('samedi', 18, 'Samedi 14 Sept')">
|
||
<div class="text-center">
|
||
<div class="bg-gradient-to-br from-purple-500 to-pink-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="calendar-days" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Samedi 14</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="samedi-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('samedi', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dimanche 15 -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('dimanche', 18, 'Dimanche 15 Sept')">
|
||
<div class="text-center">
|
||
<div class="bg-gradient-to-br from-orange-500 to-red-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="sun" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Dimanche 15</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="dimanche-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('dimanche', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lundi 16 -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('lundi', 18, 'Lundi 16 Sept')">
|
||
<div class="text-center">
|
||
<div class="bg-gradient-to-br from-green-500 to-blue-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="moon" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Lundi 16</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Journée complète</p>
|
||
<div class="text-3xl font-bold text-primary mb-4">18€</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="lundi-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('lundi', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tarif Réduit -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('reduit', 12, 'Tarif Réduit')">
|
||
<div class="text-center">
|
||
<div class="bg-gradient-to-br from-yellow-500 to-orange-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="percent" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Tarif Réduit</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Étudiants, -26 ans, RSA</p>
|
||
<div class="text-3xl font-bold text-primary mb-4">12€</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="reduit-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('reduit', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gratuit -12 ans -->
|
||
<div class="ticket-card p-6 cursor-pointer ripple-effect" onclick="selectTicket('gratuit', 0, 'Gratuit -12 ans')">
|
||
<div class="text-center">
|
||
<div class="bg-gradient-to-br from-green-600 to-emerald-600 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<i data-lucide="gift" class="w-8 h-8 text-white"></i>
|
||
</div>
|
||
<h3 class="text-xl font-bold text-gray-900 mb-2">Gratuit</h3>
|
||
<p class="text-sm text-gray-600 mb-4">Enfants -12 ans</p>
|
||
<div class="text-3xl font-bold text-green-600 mb-4">Gratuit</div>
|
||
<div class="text-sm text-green-600 font-medium mb-4">✓ Disponible</div>
|
||
|
||
<div class="flex items-center justify-center space-x-3">
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', -1)">
|
||
<i data-lucide="minus" class="w-4 h-4"></i>
|
||
</button>
|
||
<span class="w-8 text-center font-medium" id="gratuit-qty">0</span>
|
||
<button class="quantity-control w-10 h-10 flex items-center justify-center" onclick="event.stopPropagation(); changeQuantity('gratuit', 1)">
|
||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column: Cart & Info -->
|
||
<div class="lg:col-span-1">
|
||
<!-- Cart Summary -->
|
||
<div class="cart-summary p-6 mb-8 sticky top-4">
|
||
<h3 class="text-2xl font-bold text-gray-900 mb-6 text-center">Récapitulatif</h3>
|
||
|
||
<div id="cart-items" class="space-y-3 mb-6 min-h-[100px]">
|
||
<div class="text-center text-gray-500 py-8">
|
||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||
<p>Votre panier est vide</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border-t border-gray-200 pt-4 space-y-2">
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">Total billets:</span>
|
||
<span class="font-medium" id="total-quantity">0</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">Sous-total:</span>
|
||
<span class="font-medium" id="subtotal">€0.00</span>
|
||
</div>
|
||
<div class="flex justify-between text-sm">
|
||
<span class="text-gray-600">Frais de service:</span>
|
||
<span class="font-medium" id="service-fee">€0.00</span>
|
||
</div>
|
||
<div class="border-t border-gray-300 pt-2 mt-4">
|
||
<div class="flex justify-between text-lg font-bold">
|
||
<span>TOTAL:</span>
|
||
<span class="text-primary" id="total-amount">€0.00</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button id="checkout-btn" class="checkout-button w-full py-4 px-6 text-lg font-semibold mt-6 disabled" disabled>
|
||
<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>
|
||
Finaliser la commande
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Festival Info -->
|
||
<div class="festival-info p-6">
|
||
<h4 class="text-xl font-bold text-gray-900 mb-4 text-center">🎪 Festival Highlights</h4>
|
||
<div class="space-y-3 text-sm">
|
||
<div class="flex items-center">
|
||
<i data-lucide="music" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>100+ concerts et spectacles</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="mic" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>Débats et conférences</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="utensils" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>Village gastronomique</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="heart" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>Village solidaire</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="gamepad-2" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>Animations jeunesse</span>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<i data-lucide="train" class="w-5 h-5 mr-3 text-purple-600"></i>
|
||
<span>Accès RER B La Courneuve</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Initialize Lucide icons
|
||
lucide.createIcons();
|
||
|
||
// Cart state
|
||
let cart = {};
|
||
const serviceFeeRate = 0.05; // 5% service fee
|
||
|
||
function selectTicket(id, price, name) {
|
||
// Visual selection effect
|
||
const cards = document.querySelectorAll('.ticket-card');
|
||
cards.forEach(card => card.classList.remove('selected'));
|
||
event.currentTarget.classList.add('selected');
|
||
|
||
// Auto-add one ticket if none selected
|
||
if (!cart[id] || cart[id].quantity === 0) {
|
||
changeQuantity(id, 1, price, name);
|
||
}
|
||
}
|
||
|
||
function changeQuantity(id, delta, price, name) {
|
||
if (!cart[id]) {
|
||
cart[id] = { quantity: 0, price: price || 0, name: name || '' };
|
||
}
|
||
|
||
// Get price and name from ticket data if not provided
|
||
if (!price) {
|
||
const ticketPrices = {
|
||
'pass3j': { price: 45, name: 'Pass 3 jours' },
|
||
'samedi': { price: 18, name: 'Samedi 14 Sept' },
|
||
'dimanche': { price: 18, name: 'Dimanche 15 Sept' },
|
||
'lundi': { price: 18, name: 'Lundi 16 Sept' },
|
||
'reduit': { price: 12, name: 'Tarif Réduit' },
|
||
'gratuit': { price: 0, name: 'Gratuit -12 ans' }
|
||
};
|
||
price = ticketPrices[id].price;
|
||
name = ticketPrices[id].name;
|
||
cart[id].price = price;
|
||
cart[id].name = name;
|
||
}
|
||
|
||
cart[id].quantity = Math.max(0, cart[id].quantity + delta);
|
||
|
||
// Update quantity display
|
||
document.getElementById(id + '-qty').textContent = cart[id].quantity;
|
||
|
||
// Remove from cart if quantity is 0
|
||
if (cart[id].quantity === 0) {
|
||
delete cart[id];
|
||
}
|
||
|
||
updateCartSummary();
|
||
}
|
||
|
||
function updateCartSummary() {
|
||
const cartItemsContainer = document.getElementById('cart-items');
|
||
const totalQuantityEl = document.getElementById('total-quantity');
|
||
const subtotalEl = document.getElementById('subtotal');
|
||
const serviceFeeEl = document.getElementById('service-fee');
|
||
const totalAmountEl = document.getElementById('total-amount');
|
||
const checkoutBtn = document.getElementById('checkout-btn');
|
||
|
||
let totalQuantity = 0;
|
||
let subtotal = 0;
|
||
let cartItemsHtml = '';
|
||
|
||
// Check if cart is empty
|
||
const hasItems = Object.keys(cart).some(id => cart[id].quantity > 0);
|
||
|
||
if (!hasItems) {
|
||
cartItemsHtml = `
|
||
<div class="text-center text-gray-500 py-8">
|
||
<i data-lucide="shopping-cart" class="w-12 h-12 mx-auto mb-4 opacity-50"></i>
|
||
<p>Votre panier est vide</p>
|
||
</div>
|
||
`;
|
||
checkoutBtn.disabled = true;
|
||
checkoutBtn.classList.add('disabled');
|
||
} else {
|
||
// Build cart items
|
||
Object.keys(cart).forEach(id => {
|
||
if (cart[id].quantity > 0) {
|
||
totalQuantity += cart[id].quantity;
|
||
subtotal += cart[id].quantity * cart[id].price;
|
||
|
||
cartItemsHtml += `
|
||
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
||
<div class="flex-1">
|
||
<div class="font-medium text-sm">${cart[id].name}</div>
|
||
<div class="text-xs text-gray-500">${cart[id].quantity} × €${cart[id].price.toFixed(2)}</div>
|
||
</div>
|
||
<div class="font-medium text-sm">€${(cart[id].quantity * cart[id].price).toFixed(2)}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
});
|
||
|
||
checkoutBtn.disabled = false;
|
||
checkoutBtn.classList.remove('disabled');
|
||
}
|
||
|
||
const serviceFee = subtotal * serviceFeeRate;
|
||
const totalAmount = subtotal + serviceFee;
|
||
|
||
cartItemsContainer.innerHTML = cartItemsHtml;
|
||
totalQuantityEl.textContent = totalQuantity;
|
||
subtotalEl.textContent = `€${subtotal.toFixed(2)}`;
|
||
serviceFeeEl.textContent = `€${serviceFee.toFixed(2)}`;
|
||
totalAmountEl.textContent = `€${totalAmount.toFixed(2)}`;
|
||
|
||
// Recreate icons for newly added elements
|
||
lucide.createIcons();
|
||
}
|
||
|
||
// Checkout button click handler
|
||
document.getElementById('checkout-btn').addEventListener('click', function() {
|
||
if (this.disabled) return;
|
||
|
||
// Simulate checkout process
|
||
this.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 inline-block mr-2 animate-spin"></i>Traitement...';
|
||
this.disabled = true;
|
||
|
||
setTimeout(() => {
|
||
alert('Redirection vers le paiement sécurisé...');
|
||
this.innerHTML = '<i data-lucide="credit-card" class="w-5 h-5 inline-block mr-2"></i>Finaliser la commande';
|
||
this.disabled = Object.keys(cart).length === 0;
|
||
lucide.createIcons();
|
||
}, 2000);
|
||
});
|
||
|
||
// Initial setup
|
||
updateCartSummary();
|
||
</script>
|
||
</body>
|
||
</html> |