Files
turf_saas/register.html
DevOps Engineer 41a9e36166 feat(sprint4-5): Landing page + onboarding SaaS — HRT-30
Frontend pages:
- landing.html: marketing page — hero, pricing (Free/9.90e/24.90e), features, FAQ, footer, mobile-first responsive, LCP < 2.5s friendly
- login.html: JWT auth login with JS validation, error handling, redirect-after-login
- register.html: registration with plan selection preview sidebar, password strength meter
- dashboard_saas.html: role-based dashboard (Free/Premium/Pro) with locked sections, race prediction cards, detailed table, stats row
- onboarding.html: 3-step wizard — plan confirm + Telegram alerts config + first prediction preview
- account.html: tabbed account management — profile, security (change-password, delete), plan upgrade, notification preferences

Backend:
- saas_auth.py: Flask Blueprint /api/v1/auth/* — register, login, token auth, profile/password/plan/preferences update, logout, delete-account
- saas_api_v1.py: Flask Blueprint /api/v1/* — stats/summary, predictions/today (plan-gated), value-bets (Premium+), CSV export (Pro)

Server:
- portal_server.py: register blueprints, serve all new SaaS routes at /login /register /dashboard /onboarding /account

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 18:04:19 +02:00

129 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inscription — Turf IA</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--green:#00c853;--green-d:#009624;--dark:#0d1117;--dark2:#161b22;--dark3:#21262d;--text:#e6edf3;--muted:#8b949e;--border:#30363d;--error:#f85149;--radius:10px}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--dark);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
a{color:inherit;text-decoration:none}
nav{display:flex;align-items:center;justify-content:space-between;padding:16px 5%;border-bottom:1px solid var(--border)}
.nav-logo{font-weight:700;font-size:1.1rem}
.nav-link{color:var(--muted);font-size:.9rem}
main{flex:1;display:flex;align-items:flex-start;justify-content:center;padding:40px 20px;gap:40px;flex-wrap:wrap}
.auth-card{width:100%;max-width:440px;background:var(--dark2);border:1px solid var(--border);border-radius:14px;padding:40px}
.plan-preview{width:100%;max-width:300px}
.auth-title{font-size:1.5rem;font-weight:800;margin-bottom:6px}
.auth-subtitle{color:var(--muted);font-size:.9rem;margin-bottom:28px}
.form-group{margin-bottom:16px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
label{display:block;font-size:.85rem;font-weight:600;color:var(--muted);margin-bottom:6px}
input,select{width:100%;padding:11px 14px;background:var(--dark3);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:.95rem;outline:none;transition:border-color .2s}
input:focus,select:focus{border-color:var(--green)}
input.error{border-color:var(--error)}
select option{background:var(--dark3)}
.field-error{color:var(--error);font-size:.8rem;margin-top:4px;display:none}
.field-error.show{display:block}
.btn{width:100%;padding:12px;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;transition:all .2s;margin-top:4px}
.btn-primary{background:var(--green);color:#000}
.btn-primary:hover{background:var(--green-d)}
.btn-primary:disabled{opacity:.6;cursor:not-allowed}
.auth-footer{text-align:center;margin-top:20px;color:var(--muted);font-size:.9rem}
.auth-footer a{color:var(--green);font-weight:600}
.alert{padding:12px 16px;border-radius:10px;font-size:.88rem;margin-bottom:18px;display:none}
.alert.show{display:block}
.alert-error{background:rgba(248,81,73,.12);border:1px solid rgba(248,81,73,.3);color:#f85149}
.loader{display:inline-block;width:16px;height:16px;border:2px solid rgba(0,0,0,.3);border-top-color:#000;border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:6px}
@keyframes spin{to{transform:rotate(360deg)}}
.terms{font-size:.8rem;color:var(--muted);line-height:1.6}
.terms a{color:var(--green)}
.password-strength{height:4px;border-radius:2px;margin-top:6px;background:var(--border);overflow:hidden}
.strength-bar{height:100%;width:0;transition:width .3s,background .3s}
.plan-card{background:var(--dark2);border:2px solid var(--border);border-radius:14px;padding:24px;margin-bottom:16px;transition:border-color .3s}
.plan-card.selected{border-color:var(--green)}
.plan-card-name{font-weight:700;font-size:1rem;display:flex;justify-content:space-between;align-items:center}
.plan-card-price{font-size:1.5rem;font-weight:800;margin:8px 0}
.plan-card-features{list-style:none;margin-top:12px}
.plan-card-features li{font-size:.83rem;color:var(--muted);padding:4px 0}
.plan-card-features li::before{content:"✓ ";color:var(--green)}
.plan-badge{background:var(--green);color:#000;padding:2px 8px;border-radius:10px;font-size:.7rem;font-weight:700}
footer{text-align:center;padding:20px;color:var(--muted);font-size:.8rem;border-top:1px solid var(--border)}
@media(max-width:768px){.plan-preview{display:none}}
</style>
</head>
<body>
<nav>
<a href="/" class="nav-logo">🏇 Turf IA</a>
<a href="/login" class="nav-link">Déjà un compte ? Se connecter →</a>
</nav>
<main>
<div class="auth-card">
<h1 class="auth-title">Créer votre compte</h1>
<p class="auth-subtitle">Commencez gratuitement, sans carte bancaire.</p>
<div class="alert alert-error" id="alert-error"></div>
<form id="register-form" novalidate>
<div class="form-row">
<div class="form-group"><label for="firstname">Prénom</label><input type="text" id="firstname" placeholder="Jean" required><div class="field-error" id="firstname-error">Requis.</div></div>
<div class="form-group"><label for="lastname">Nom</label><input type="text" id="lastname" placeholder="Dupont" required><div class="field-error" id="lastname-error">Requis.</div></div>
</div>
<div class="form-group"><label for="email">Email</label><input type="email" id="email" placeholder="vous@exemple.fr" required><div class="field-error" id="email-error">Email invalide.</div></div>
<div class="form-group"><label for="password">Mot de passe</label><input type="password" id="password" placeholder="8 caractères minimum" required><div class="password-strength"><div class="strength-bar" id="strength-bar"></div></div><div class="field-error" id="password-error">8 caractères minimum.</div></div>
<div class="form-group"><label for="plan">Plan de départ</label><select id="plan"><option value="free">Free — Gratuit</option><option value="premium">Premium — 9,90€/mois</option><option value="pro">Pro — 24,90€/mois</option></select></div>
<div class="form-group"><p class="terms">En vous inscrivant, vous acceptez nos <a href="/legal/cgu">CGU</a> et notre <a href="/legal/privacy">Politique de Confidentialité</a>. Vous devez avoir 18 ans ou plus.</p></div>
<button type="submit" class="btn btn-primary" id="submit-btn">Créer mon compte gratuit</button>
</form>
<div class="auth-footer">Déjà un compte ? <a href="/login">Se connecter</a></div>
</div>
<div class="plan-preview">
<div id="plan-card-free" class="plan-card selected"><div class="plan-card-name">Free</div><div class="plan-card-price">0€<span style="font-size:.9rem;font-weight:400;color:var(--muted)">/mois</span></div><ul class="plan-card-features"><li>Aperçu Top-3 du jour</li><li>1 course complète/jour</li></ul></div>
<div id="plan-card-premium" class="plan-card" style="display:none"><div class="plan-card-name">Premium <span class="plan-badge"></span></div><div class="plan-card-price">9,90€<span style="font-size:.9rem;font-weight:400;color:var(--muted)">/mois</span></div><ul class="plan-card-features"><li>Toutes les courses</li><li>Alertes Telegram</li><li>Value bets</li></ul></div>
<div id="plan-card-pro" class="plan-card" style="display:none"><div class="plan-card-name">Pro</div><div class="plan-card-price">24,90€<span style="font-size:.9rem;font-weight:400;color:var(--muted)">/mois</span></div><ul class="plan-card-features"><li>Tout Premium</li><li>Export CSV</li><li>API REST</li></ul></div>
<p style="font-size:.78rem;color:var(--muted);text-align:center;">⚡ Pas de carte bancaire pour le plan Free</p>
</div>
</main>
<footer>© 2026 Turf IA — H3R7 Tech. Jouez de façon responsable. 18+</footer>
<script>
const API='/api/v1';
const form=document.getElementById('register-form');
const submitBtn=document.getElementById('submit-btn');
const alertBox=document.getElementById('alert-error');
const planSelect=document.getElementById('plan');
const urlPlan=new URLSearchParams(location.search).get('plan');
if(urlPlan&&['free','premium','pro'].includes(urlPlan))planSelect.value=urlPlan;
function updatePlanPreview(){['free','premium','pro'].forEach(p=>{const el=document.getElementById('plan-card-'+p);if(!el)return;if(planSelect.value===p){el.style.display='';el.classList.add('selected')}else{el.style.display='none';el.classList.remove('selected')}})}
planSelect.addEventListener('change',updatePlanPreview);updatePlanPreview();
function showError(m){alertBox.textContent=m;alertBox.classList.add('show')}
function hideError(){alertBox.classList.remove('show')}
function setLoading(on){submitBtn.disabled=on;submitBtn.innerHTML=on?'<span class="loader"></span>Création…':'Créer mon compte gratuit'}
function validateField(input,errId,cond,msg){const e=document.getElementById(errId);if(cond){input.classList.add('error');e.textContent=msg;e.classList.add('show');return false}input.classList.remove('error');e.classList.remove('show');return true}
const passInput=document.getElementById('password');
const bar=document.getElementById('strength-bar');
passInput.addEventListener('input',()=>{const v=passInput.value;let s=0;if(v.length>=8)s++;if(/[A-Z]/.test(v))s++;if(/[0-9]/.test(v))s++;if(/[^A-Za-z0-9]/.test(v))s++;const c=['#f85149','#e3b341','#58a6ff','#00c853'];bar.style.width=(s*25)+'%';bar.style.background=c[s-1]||'#30363d'});
form.addEventListener('submit',async e=>{
e.preventDefault();hideError();
const firstname=document.getElementById('firstname').value.trim();
const lastname=document.getElementById('lastname').value.trim();
const email=document.getElementById('email').value.trim();
const password=passInput.value;
const plan=planSelect.value;
let ok=true;
ok=validateField(document.getElementById('firstname'),'firstname-error',!firstname,'Prénom requis.')&&ok;
ok=validateField(document.getElementById('lastname'),'lastname-error',!lastname,'Nom requis.')&&ok;
ok=validateField(document.getElementById('email'),'email-error',!email||!email.includes('@'),'Email invalide.')&&ok;
ok=validateField(passInput,'password-error',password.length<8,'8 caractères minimum.')&&ok;
if(!ok)return;
setLoading(true);
try{
const res=await fetch(`${API}/auth/register`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({firstname,lastname,email,password,plan})});
const data=await res.json();
if(!res.ok){showError(data.error||'Erreur lors de la création du compte.')}
else{localStorage.setItem('turf_token',data.token);localStorage.setItem('turf_user',JSON.stringify(data.user));localStorage.setItem('turf_onboarding','pending');location.href='/onboarding'}
}catch(_){showError('Erreur de connexion. Vérifiez votre réseau.')}
finally{setLoading(false)}
});
document.querySelectorAll('input').forEach(el=>el.addEventListener('input',()=>{el.classList.remove('error');document.getElementById(el.id+'-error')?.classList.remove('show');hideError()}));
(function(){if(localStorage.getItem('turf_token'))location.href='/dashboard'})();
</script>
</body>
</html>