Files
turf_saas/account.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

178 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Compte — Turf IA</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--green:#00c853;--green-d:#009624;--blue:#1e88e5;--dark:#0d1117;--dark2:#161b22;--dark3:#21262d;--text:#e6edf3;--muted:#8b949e;--border:#30363d;--radius:10px;--error:#f85149;--gold:#ffd600}
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:14px 5%;border-bottom:1px solid var(--border)}
.nav-logo{font-weight:700;font-size:1.1rem}
.nav-back{color:var(--muted);font-size:.9rem}
main{flex:1;padding:40px 5%;max-width:900px;margin:0 auto;width:100%}
h1{font-size:1.6rem;font-weight:800;margin-bottom:28px}
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:28px}
.tab-btn{padding:10px 18px;border:none;background:transparent;color:var(--muted);font-size:.9rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s;margin-bottom:-1px}
.tab-btn:hover{color:var(--text)}
.tab-btn.active{color:var(--green);border-bottom-color:var(--green)}
.tab-panel{display:none}
.tab-panel.active{display:block}
.card{background:var(--dark2);border:1px solid var(--border);border-radius:var(--radius);padding:24px;margin-bottom:20px}
.card-title{font-size:1rem;font-weight:700;margin-bottom:18px;padding-bottom:12px;border-bottom:1px solid var(--border)}
.form-group{margin-bottom:16px}
label{display:block;font-size:.83rem;font-weight:600;color:var(--muted);margin-bottom:6px}
input{width:100%;padding:10px 14px;background:var(--dark3);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:.9rem;outline:none;transition:border-color .2s}
input:focus{border-color:var(--green)}
input:disabled{opacity:.5;cursor:not-allowed}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 20px;border-radius:8px;font-size:.88rem;font-weight:600;cursor:pointer;border:none;transition:all .2s}
.btn-primary{background:var(--green);color:#000}
.btn-primary:hover{background:var(--green-d)}
.btn-primary:disabled{opacity:.6;cursor:not-allowed}
.btn-ghost{background:transparent;border:1px solid var(--border);color:var(--text)}
.btn-ghost:hover{border-color:var(--muted)}
.btn-danger{background:transparent;border:1px solid var(--error);color:var(--error)}
.btn-danger:hover{background:rgba(248,81,73,.1)}
.btn-upgrade{background:linear-gradient(135deg,var(--gold),#ff6d00);color:#000}
.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:24px}
.plan-card{background:var(--dark3);border:2px solid var(--border);border-radius:var(--radius);padding:20px;position:relative;transition:border-color .2s}
.plan-card.current{border-color:var(--green)}
.plan-current-badge{position:absolute;top:-10px;left:16px;background:var(--green);color:#000;padding:2px 10px;border-radius:10px;font-size:.7rem;font-weight:700}
.plan-name{font-weight:800;font-size:1rem;margin-bottom:6px}
.plan-price{font-size:1.5rem;font-weight:800;color:var(--green)}
.plan-price span{font-size:.85rem;font-weight:400;color:var(--muted)}
.plan-features-mini{list-style:none;margin-top:12px}
.plan-features-mini li{font-size:.8rem;color:var(--muted);padding:3px 0}
.plan-features-mini li::before{content:"✓ ";color:var(--green)}
.plan-card .btn{width:100%;justify-content:center;margin-top:14px;font-size:.82rem;padding:8px}
.alert{padding:12px 16px;border-radius:8px;font-size:.88rem;margin-bottom:14px;display:none}
.alert.show{display:block}
.alert-success{background:rgba(0,200,83,.1);border:1px solid rgba(0,200,83,.3);color:var(--green)}
.alert-error{background:rgba(248,81,73,.1);border:1px solid rgba(248,81,73,.3);color:var(--error)}
.danger-card{border-color:rgba(248,81,73,.3)}
#toast{position:fixed;bottom:24px;right:24px;z-index:999;padding:12px 20px;border-radius:10px;font-size:.88rem;font-weight:600;transform:translateY(60px);opacity:0;transition:all .3s;pointer-events:none}
#toast.show{transform:translateY(0);opacity:1}
#toast.success{background:var(--green);color:#000}
#toast.error{background:var(--error);color:#fff}
#toast.info{background:var(--blue);color:#fff}
@media(max-width:600px){.form-row{grid-template-columns:1fr}main{padding:20px 4%}}
</style>
</head>
<body>
<nav>
<a href="/" class="nav-logo">🏇 Turf IA</a>
<a href="/dashboard" class="nav-back">← Retour au dashboard</a>
</nav>
<main>
<h1>⚙️ Mon compte</h1>
<div class="tabs">
<button class="tab-btn active" data-tab="profile">Profil</button>
<button class="tab-btn" data-tab="security">Sécurité</button>
<button class="tab-btn" data-tab="upgrade">Mon plan</button>
<button class="tab-btn" data-tab="notifications">Notifications</button>
</div>
<div class="tab-panel active" id="tab-profile">
<div class="card">
<div class="card-title">Informations personnelles</div>
<div class="alert alert-success" id="profile-success">Profil mis à jour.</div>
<div class="alert alert-error" id="profile-error"></div>
<form id="profile-form">
<div class="form-row">
<div class="form-group"><label>Prénom</label><input type="text" id="p-firstname" placeholder="Jean"></div>
<div class="form-group"><label>Nom</label><input type="text" id="p-lastname" placeholder="Dupont"></div>
</div>
<div class="form-group"><label>Email</label><input type="email" id="p-email" placeholder="vous@exemple.fr"></div>
<button type="submit" class="btn btn-primary" id="profile-btn">Enregistrer</button>
</form>
</div>
<div class="card">
<div class="card-title">Informations de compte</div>
<div class="form-group"><label>Plan actuel</label><input type="text" id="p-plan-display" disabled></div>
<div class="form-group"><label>Membre depuis</label><input type="text" id="p-created" disabled></div>
</div>
</div>
<div class="tab-panel" id="tab-security">
<div class="card">
<div class="card-title">Changer le mot de passe</div>
<div class="alert alert-success" id="pwd-success">Mot de passe mis à jour.</div>
<div class="alert alert-error" id="pwd-error"></div>
<form id="password-form">
<div class="form-group"><label>Mot de passe actuel</label><input type="password" id="s-current-pwd" placeholder="••••••••" required></div>
<div class="form-group"><label>Nouveau mot de passe</label><input type="password" id="s-new-pwd" placeholder="8 caractères minimum" required></div>
<div class="form-group"><label>Confirmer</label><input type="password" id="s-confirm-pwd" placeholder="••••••••" required></div>
<button type="submit" class="btn btn-primary" id="pwd-btn">Mettre à jour</button>
</form>
</div>
<div class="card danger-card">
<div class="card-title">Zone dangereuse</div>
<p style="font-size:.9rem;color:var(--muted);margin-bottom:16px">La suppression de votre compte est irréversible.</p>
<button class="btn btn-danger" id="delete-account-btn">Supprimer mon compte</button>
</div>
</div>
<div class="tab-panel" id="tab-upgrade">
<div class="card">
<div class="card-title">Votre plan actuel</div>
<div class="plan-grid">
<div class="plan-card" id="plan-card-free"><div class="plan-name">Free</div><div class="plan-price">0€ <span>/mois</span></div><ul class="plan-features-mini"><li>1 course/jour</li><li>Aperçu Top-3</li></ul><button class="btn btn-ghost" id="select-free-btn">Plan actuel</button></div>
<div class="plan-card" id="plan-card-premium"><div class="plan-name">Premium ⭐</div><div class="plan-price">9,90€ <span>/mois</span></div><ul class="plan-features-mini"><li>Toutes les courses</li><li>Alertes Telegram</li><li>Value bets</li></ul><button class="btn btn-upgrade" id="select-premium-btn">Choisir Premium</button></div>
<div class="plan-card" id="plan-card-pro"><div class="plan-name">Pro 🚀</div><div class="plan-price">24,90€ <span>/mois</span></div><ul class="plan-features-mini"><li>Tout Premium</li><li>Export CSV</li><li>API REST</li></ul><button class="btn btn-ghost" id="select-pro-btn">Choisir Pro</button></div>
</div>
<p style="font-size:.82rem;color:var(--muted)">💳 Paiement Stripe disponible dans le Sprint 5-6.</p>
</div>
</div>
<div class="tab-panel" id="tab-notifications">
<div class="card">
<div class="card-title">Alertes Telegram</div>
<div class="alert alert-success" id="notif-success">Préférences enregistrées.</div>
<form id="notif-form">
<div class="form-group"><label>Chat ID Telegram</label><input type="text" id="n-telegram-id" placeholder="123456789"><p style="font-size:.78rem;color:var(--muted);margin-top:5px">Envoyez /start à @TurfIABot pour obtenir votre Chat ID.</p></div>
<div class="form-group">
<p style="font-size:.88rem;font-weight:600;margin-bottom:10px;color:var(--muted)">Recevoir des alertes pour :</p>
<label style="display:flex;gap:10px;align-items:center;margin-bottom:10px;cursor:pointer;font-size:.9rem"><input type="checkbox" id="n-alert-vb" style="width:auto;accent-color:var(--green)"> Value bets identifiés</label>
<label style="display:flex;gap:10px;align-items:center;margin-bottom:10px;cursor:pointer;font-size:.9rem"><input type="checkbox" id="n-alert-top1" style="width:auto;accent-color:var(--green)"> Favori IA Top-1</label>
<label style="display:flex;gap:10px;align-items:center;cursor:pointer;font-size:.9rem"><input type="checkbox" id="n-alert-quinte" style="width:auto;accent-color:var(--green)"> Quinté+ uniquement</label>
</div>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</form>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const API='/api/v1';
function getToken(){return localStorage.getItem('turf_token')}
async function fetchJson(url,opts={}){const token=getToken();const res=await fetch(url,{...opts,headers:{'Content-Type':'application/json',...(token?{Authorization:'Bearer '+token}:{}),...(opts.headers||{})}});if(res.status===401){location.href='/login';return null}return res.json().catch(()=>null)}
function showToast(msg,type='success'){const t=document.getElementById('toast');t.textContent=msg;t.className='show '+type;setTimeout(()=>t.className='',3500)}
function showAlert(id,show=true){document.getElementById(id)?.classList.toggle('show',show)}
function setAlertMsg(id,msg,show=true){const el=document.getElementById(id);if(el){el.textContent=msg;el.classList.toggle('show',show)}}
document.querySelectorAll('.tab-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById('tab-'+btn.dataset.tab)?.classList.add('active')})});
(function(){const tab=new URLSearchParams(location.search).get('tab');if(tab){document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));document.querySelectorAll('.tab-panel').forEach(p=>p.classList.toggle('active',p.id==='tab-'+tab))}})();
async function loadUser(){
if(!getToken()){location.href='/login';return}
const data=await fetchJson(API+'/auth/me');if(!data)return;
const user=data.user||data;
document.getElementById('p-firstname').value=user.firstname||'';
document.getElementById('p-lastname').value=user.lastname||'';
document.getElementById('p-email').value=user.email||'';
document.getElementById('p-plan-display').value={free:'Free',premium:'Premium',pro:'Pro'}[user.plan]||'Free';
document.getElementById('p-created').value=user.created_at?new Date(user.created_at).toLocaleDateString('fr-FR'):'—';
document.getElementById('n-telegram-id').value=user.telegram_chat_id||'';
document.getElementById('n-alert-vb').checked=user.alert_value_bets!==false;
document.getElementById('n-alert-top1').checked=user.alert_top1!==false;
document.getElementById('n-alert-quinte').checked=!!user.alert_quinte_only;
const plan=user.plan||'free';
['free','premium','pro'].forEach(p=>{const card=document.getElementById('plan-card-'+p);if(!card)return;card.classList.toggle('current',p===plan);const ex=card.querySelector('.plan-current-badge');if(p===plan&&!ex){const b=document.createElement('div');b.className='plan-current-badge';b.textContent='✓ Actuel';card.prepend(b)}else if(p!==plan&&ex)ex.remove()});
localStorage.setItem('turf_user',JSON.stringify(user));
}
document.getElementById('profile-form').addEventListener('submit',async e=>{e.preventDefault();showAlert('profile-success',false);showAlert('profile-error',false);const btn=document.getElementById('profile-btn');btn.disabled=true;btn.textContent='Enregistrement…';const res=await fetchJson(API+'/auth/update-profile',{method:'POST',body:JSON.stringify({firstname:document.getElementById('p-firstname').value.trim(),lastname:document.getElementById('p-lastname').value.trim(),email:document.getElementById('p-email').value.trim()})});btn.disabled=false;btn.textContent='Enregistrer';if(res&&res.ok!==false){showAlert('profile-success');showToast('Profil mis à jour.')}else setAlertMsg('profile-error',res?.error||'Erreur.')});
document.getElementById('password-form').addEventListener('submit',async e=>{e.preventDefault();showAlert('pwd-success',false);showAlert('pwd-error',false);const np=document.getElementById('s-new-pwd').value,cp=document.getElementById('s-confirm-pwd').value;if(np!==cp){setAlertMsg('pwd-error','Les mots de passe ne correspondent pas.');return}if(np.length<8){setAlertMsg('pwd-error','Minimum 8 caractères.');return}const btn=document.getElementById('pwd-btn');btn.disabled=true;const res=await fetchJson(API+'/auth/change-password',{method:'POST',body:JSON.stringify({current_password:document.getElementById('s-current-pwd').value,new_password:np})});btn.disabled=false;if(res&&res.ok!==false){showAlert('pwd-success');showToast('Mot de passe mis à jour.');document.getElementById('password-form').reset()}else setAlertMsg('pwd-error',res?.error||'Mot de passe actuel incorrect.')});
document.getElementById('notif-form').addEventListener('submit',async e=>{e.preventDefault();showAlert('notif-success',false);const res=await fetchJson(API+'/auth/update-preferences',{method:'POST',body:JSON.stringify({telegram_chat_id:document.getElementById('n-telegram-id').value.trim(),alert_value_bets:document.getElementById('n-alert-vb').checked,alert_top1:document.getElementById('n-alert-top1').checked,alert_quinte_only:document.getElementById('n-alert-quinte').checked})});if(res&&res.ok!==false){showAlert('notif-success');showToast('Préférences enregistrées.')}else showToast('Erreur lors de la sauvegarde.','error')});
['free','premium','pro'].forEach(p=>{document.getElementById('select-'+p+'-btn')?.addEventListener('click',()=>{showToast(p==='free'?'Vous êtes sur le plan Free.':'Paiement Stripe disponible dans le Sprint 5-6.','info')})});
document.getElementById('delete-account-btn').addEventListener('click',()=>{if(confirm('Supprimer votre compte définitivement ?')){if(confirm('Confirmez la suppression.'))fetchJson(API+'/auth/delete-account',{method:'DELETE'}).then(()=>{localStorage.clear();location.href='/'})}});
loadUser();
</script>
</body>
</html>