Spaces:
Build error
Build error
| <html lang="fr" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <link rel="icon" type="image/png" href="https://i.imgur.com/7Gn3toV.png"> | |
| <title>Paiement - Nexus Pro</title> | |
| <link rel="stylesheet" | |
| href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://js.stripe.com/v3/"></script> | |
| <style> | |
| /* Variables de Thème - Couleurs Noir Foncé (Mode Sombre par défaut) */ | |
| :root, html.dark { | |
| --bg-page: #181818; /* Noir très foncé, pas de bleu marine */ | |
| --bg-card: #282828; | |
| --text-main: #EAEAEA; | |
| --text-secondary: #B0B0B0; | |
| --border-color: #404040; | |
| --shadow-color: rgba(0, 0, 0, 0.5); | |
| } | |
| /* Mode Clair */ | |
| html.light { | |
| --bg-page: #F5F5F5; | |
| --bg-card: #FFFFFF; | |
| --text-main: #202020; | |
| --text-secondary: #5C5C5C; | |
| --border-color: #E0E0E0; | |
| --shadow-color: rgba(0, 0, 0, 0.1); | |
| } | |
| /* Application des variables */ | |
| body { | |
| background-color: var(--bg-page); | |
| color: var(--text-main); | |
| transition: background-color 0.3s, color 0.3s; | |
| } | |
| .bg-gray-900 { background-color: var(--bg-page) ; } | |
| .text-white { color: var(--text-main) ; } | |
| .bg-gray-800, .bg-gray-800\/50 { | |
| background-color: var(--bg-card) ; | |
| box-shadow: 0 4px 6px -1px var(--shadow-color), 0 2px 4px -2px var(--shadow-color); | |
| } | |
| .text-gray-400 { color: var(--text-secondary) ; } | |
| .border-gray-700, .border-gray-800 { border-color: var(--border-color) ; } | |
| .text-gray-300 { color: var(--text-secondary) ; } /* Utilisation de la même couleur pour uniformité */ | |
| .bg-gray-700 { background-color: #404040 ; } | |
| .theme-switch { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--text-main); | |
| font-size: 24px; | |
| transition: color 0.2s; | |
| padding: 0.5rem; /* Espace autour du bouton */ | |
| } | |
| /* Style pour l'élément Stripe Elements (input) */ | |
| #card-element { | |
| padding: 1rem; | |
| border: 1px solid var(--border-color); | |
| background-color: var(--bg-card); | |
| border-radius: 0.5rem; | |
| margin-bottom: 1rem; | |
| color: var(--text-main); /* Assurez-vous que le texte par défaut est lisible */ | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <div class="container mx-auto p-4 sm:p-6 lg:p-8 max-w-7xl"> | |
| <header class="sticky top-0 z-50 flex flex-col sm:flex-row justify-between items-center py-4 border-b border-blue-500/50 mb-8 bg-gray-900" style="background-color: var(--bg-page); transition: background-color 0.3s;"> | |
| <a href="/" class="text-4xl font-extrabold text-blue-500 tracking-wider mb-4 sm:mb-0"> | |
| NEXUS | |
| </a> | |
| <nav class="flex items-center space-x-4"> | |
| <a href="/documentation" class="px-4 py-2 text-white hover:text-blue-300 transition duration-200 font-medium"> | |
| Documentation | |
| </a> | |
| <a href="/a-propos" class="px-4 py-2 text-white hover:text-blue-300 transition duration-200 font-medium"> | |
| À Propos | |
| </a> | |
| <button id="theme-toggle" class="theme-switch" aria-label="Basculer le thème"> | |
| <span class="material-symbols-rounded">light_mode</span> | |
| </button> | |
| {% if is_logged_in %} | |
| <a href="/dashboard" class="px-4 py-2 bg-blue-600 border border-blue-600 hover:bg-blue-700 font-medium transition duration-200"> | |
| Dashboard | |
| </a> | |
| {% else %} | |
| <a href="/connexion" class="px-4 py-2 bg-blue-600 border border-blue-600 hover:bg-blue-700 font-medium transition duration-200"> | |
| Connexion | |
| </a> | |
| {% endif %} | |
| </nav> | |
| </header> | |
| <main class="flex flex-col items-center"> | |
| <div class="max-w-xl w-full p-6 sm:p-8 bg-gray-800 shadow-2xl border-t-4 border-blue-500"> | |
| <h1 class="text-3xl font-extrabold text-white mb-6 text-center"> | |
| Finaliser votre Commande | |
| </h1> | |
| <div class="mb-8 border border-gray-700 p-4"> | |
| <h2 class="text-xl font-bold text-white mb-3 border-b border-gray-700 pb-2"> | |
| Récapitulatif | |
| </h2> | |
| <div class="space-y-2 text-gray-300"> | |
| <div class="flex justify-between"> | |
| <span>Plan Sélectionné:</span> | |
| <span class="font-semibold text-white">{{ plan.title }}</span> | |
| </div> | |
| <div class="text-sm italic text-gray-400"> | |
| {{ plan.description }} | |
| </div> | |
| <div class="flex justify-between pt-2 border-t border-gray-700 font-bold text-lg text-yellow-400"> | |
| <span>Total (TVA incluse):</span> | |
| <span>{{ plan.price_display }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <form id="payment-form"> | |
| <h2 class="text-xl font-bold text-white mb-4"> | |
| Informations de Paiement | |
| </h2> | |
| <div id="card-element" class="shadow-md"> | |
| </div> | |
| <div id="card-errors" role="alert" class="text-red-500 text-sm mb-4"></div> | |
| <button id="submit-button" class="w-full px-6 py-3 bg-blue-600 text-white text-lg font-bold shadow-md hover:bg-blue-700 transition duration-300" type="submit"> | |
| Payer maintenant {{ plan.price_display }} | |
| </button> | |
| </form> | |
| </div> | |
| </main> | |
| <footer class="mt-12 pt-6 border-t border-gray-700 text-center text-gray-500 text-sm"> | |
| © 2025 Nexus. Paiement Sécurisé par Stripe. | |
| </footer> | |
| </div> | |
| <script> | |
| // Logique de bascule du mode sombre/clair (à inclure pour l'uniformité) | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const htmlElement = document.documentElement; | |
| const iconElement = themeToggle.querySelector('.material-symbols-rounded'); | |
| const savedTheme = localStorage.getItem('theme'); | |
| if (savedTheme === 'light') { | |
| htmlElement.classList.remove('dark'); | |
| htmlElement.classList.add('light'); | |
| iconElement.textContent = 'dark_mode'; | |
| } else { | |
| htmlElement.classList.add('dark'); | |
| htmlElement.classList.remove('light'); | |
| iconElement.textContent = 'light_mode'; | |
| } | |
| themeToggle.addEventListener('click', () => { | |
| if (htmlElement.classList.contains('light')) { | |
| htmlElement.classList.remove('light'); | |
| htmlElement.classList.add('dark'); | |
| localStorage.setItem('theme', 'dark'); | |
| iconElement.textContent = 'light_mode'; | |
| } else { | |
| htmlElement.classList.add('light'); | |
| htmlElement.classList.remove('dark'); | |
| localStorage.setItem('theme', 'light'); | |
| iconElement.textContent = 'dark_mode'; | |
| } | |
| }); | |
| // ======================================================= | |
| // LOGIQUE D'INTÉGRATION STRIPE ELEMENTS | |
| // ======================================================= | |
| const planId = new URLSearchParams(window.location.search).get('plan'); | |
| const stripe = Stripe("{{ stripe_public_key }}"); // Clé publique injectée par Jinja | |
| const elements = stripe.elements(); | |
| const cardElement = elements.create('card', { | |
| style: { | |
| base: { | |
| color: htmlElement.classList.contains('light') ? '#202020' : '#EAEAEA', | |
| fontFamily: 'system-ui, sans-serif', | |
| fontSmoothing: 'antialiased', | |
| fontSize: '16px', | |
| '::placeholder': { | |
| color: htmlElement.classList.contains('light') ? '#5C5C5C' : '#B0B0B0', | |
| }, | |
| }, | |
| invalid: { | |
| color: '#EF4444', | |
| iconColor: '#EF4444', | |
| }, | |
| } | |
| }); | |
| cardElement.mount('#card-element'); | |
| const form = document.getElementById('payment-form'); | |
| const submitButton = document.getElementById('submit-button'); | |
| const cardErrors = document.getElementById('card-errors'); | |
| let clientSecret = null; // Pour stocker le Client Secret reçu du backend | |
| // 1. Récupérer le Client Secret du backend au chargement de la page | |
| async function fetchClientSecret() { | |
| try { | |
| // Requête au backend pour créer le PaymentIntent | |
| const response = await fetch('/api/create-payment-intent', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ plan_id: planId }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Erreur lors de la création du PaymentIntent.'); | |
| } | |
| const data = await response.json(); | |
| clientSecret = data.client_secret; | |
| } catch (error) { | |
| console.error('Erreur Stripe:', error); | |
| cardErrors.textContent = "Erreur: Impossible d'initialiser le paiement. Veuillez réessayer."; | |
| submitButton.disabled = true; | |
| } | |
| } | |
| // 2. Traiter la soumission du formulaire | |
| form.addEventListener('submit', async (event) => { | |
| event.preventDefault(); | |
| if (!clientSecret) { | |
| cardErrors.textContent = "Veuillez patienter pendant l'initialisation du paiement."; | |
| return; | |
| } | |
| submitButton.disabled = true; | |
| submitButton.textContent = 'Traitement en cours...'; | |
| // Confirmer le paiement côté client avec le clientSecret et l'élément Stripe | |
| const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, { | |
| payment_method: { | |
| card: cardElement, | |
| billing_details: { | |
| // Idéalement, les détails de l'utilisateur (email/nom) devraient être fournis ici | |
| }, | |
| } | |
| }); | |
| if (error) { | |
| // Afficher les erreurs (ex: carte refusée, données invalides) | |
| cardErrors.textContent = error.message; | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Payer maintenant {{ plan.price_display }}'; | |
| } else if (paymentIntent.status === 'succeeded') { | |
| // Le paiement a réussi | |
| window.location.href = `/dashboard?payment=success&intent=${paymentIntent.id}`; | |
| } | |
| }); | |
| // Démarrage : Récupérer le client secret au chargement | |
| fetchClientSecret(); | |
| </script> | |
| </body> | |
| </html> |