edtech / apps /web /src /App.tsx
CognxSafeTrack
feat: Genspark-Standard upgrade, MLOps audit fixes, and XAMLÉ branding
eac938a
import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useSearchParams, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { BookOpen, FileText, Smartphone, ArrowRight, Phone, CheckCircle, Download, AlertCircle } from 'lucide-react';
import PrivacyPolicy from './PrivacyPolicy';
const WA_NUMBER = import.meta.env.VITE_WHATSAPP_NUMBER || '221771234567';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
function Navbar() {
return (
<nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-20">
<Link to="/" className="flex items-center space-x-2 group">
<div className="bg-primary/10 p-2 rounded-xl group-hover:bg-primary/20 transition-colors">
<BookOpen className="h-6 w-6 text-primary" />
</div>
<span className="font-heading font-bold text-2xl text-secondary tracking-tight">Xamlé<span className="text-primary"> Studio</span></span>
</Link>
<div className="flex items-center space-x-4">
<Link to="/student" className="text-secondary font-medium hover:text-primary transition-colors">Mon espace</Link>
<a href={`https://wa.me/${WA_NUMBER}?text=INSCRIPTION`} target="_blank" rel="noreferrer"
className="bg-primary text-white px-5 py-2.5 rounded-full font-medium hover:bg-emerald-700 hover:shadow-lg hover:shadow-primary/30 transition-all active:scale-95 hidden sm:inline-flex items-center">
Commencer <ArrowRight className="ml-2 w-4 h-4" />
</a>
</div>
</div>
</div>
</nav>
);
}
function Hero() {
return (
<div className="relative overflow-hidden bg-slate-50 pt-24 pb-32">
<div className="absolute top-0 right-0 -translate-y-12 translate-x-1/3"><div className="w-96 h-96 bg-accent/20 rounded-full blur-3xl"></div></div>
<div className="absolute bottom-0 left-0 translate-y-1/3 -translate-x-1/3"><div className="w-[500px] h-[500px] bg-primary/10 rounded-full blur-3xl"></div></div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-blue-50 text-secondary font-medium text-sm mb-8 border border-blue-100 shadow-sm">
<span className="flex h-2 w-2 rounded-full bg-blue-600 mr-2 animate-pulse"></span>
Formation directement sur WhatsApp
</div>
<h1 className="font-heading font-extrabold text-5xl md:text-7xl text-secondary mb-6 leading-tight tracking-tight">
Maîtrise ton business,<br className="hidden md:block" /> un message à la fois.
</h1>
<p className="max-w-2xl mx-auto text-xl text-gray-600 mb-10 font-sans">
Des formations audio interactives sur WhatsApp pour les entrepreneurs du Sénégal. Reçois ta leçon chaque matin et génère ton dossier business en IA.
</p>
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
<a href={`https://wa.me/${WA_NUMBER}?text=INSCRIPTION`} target="_blank" rel="noreferrer"
className="w-full sm:w-auto bg-primary text-white text-lg px-8 py-4 rounded-full font-bold shadow-xl shadow-primary/30 hover:bg-emerald-700 hover:scale-105 transition-all flex items-center justify-center group">
<Phone className="mr-2 w-5 h-5 group-hover:rotate-12 transition-transform" />S'inscrire sur WhatsApp
</a>
<Link to="/student" className="w-full sm:w-auto bg-white text-secondary border-2 border-gray-200 text-lg px-8 py-4 rounded-full font-bold hover:border-secondary hover:bg-gray-50 transition-all flex items-center justify-center">
Mon espace étudiant
</Link>
</div>
</div>
</div>
);
}
function Features() {
const features = [
{ icon: <Smartphone className="w-7 h-7 text-primary" />, title: 'WhatsApp natif', desc: 'Pas d\'application à installer. Reçois tes leçons audio et exercices directement dans WhatsApp.' },
{ icon: <BookOpen className="w-7 h-7 text-accent" />, title: 'Parcours structurés', desc: 'Des formations multi-jours conçues pour les entrepreneurs de l\'informel au Sénégal.' },
{ icon: <FileText className="w-7 h-7 text-secondary" />, title: 'Dossier IA', desc: 'À la fin de ta formation, l\'IA génère automatiquement ton One-Pager PDF et ton Pitch Deck.' },
];
return (
<div className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="font-heading font-bold text-3xl md:text-4xl text-secondary mb-4">Comment ça marche</h2>
<p className="text-lg text-gray-500 max-w-2xl mx-auto">Une expérience d'apprentissage conçue pour les entrepreneurs mobiles.</p>
</div>
<div className="grid md:grid-cols-3 gap-12">
{features.map((f, i) => (
<div key={i} className="group bg-slate-50 p-8 rounded-3xl border border-slate-100 hover:border-primary/30 hover:shadow-xl hover:shadow-primary/5 transition-all">
<div className="bg-white w-14 h-14 rounded-2xl flex items-center justify-center shadow-sm mb-6 group-hover:scale-110 transition-transform">{f.icon}</div>
<h3 className="font-heading font-bold text-xl text-secondary mb-3">{f.title}</h3>
<p className="text-gray-600 leading-relaxed text-sm">{f.desc}</p>
</div>
))}
</div>
</div>
</div>
);
}
// ── Student Portal ────────────────────────────────────────────────────────────────
function StudentPortal() {
const navigate = useNavigate();
const [phone, setPhone] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(''); setLoading(true);
const cleaned = phone.replace(/\s+/g, '').replace(/^\+/, '');
try {
const res = await fetch(`${API_URL}/v1/student/me?phone=${cleaned}`);
if (res.ok) {
navigate(`/student/${cleaned}`);
} else if (res.status === 404) {
setError('Numéro non trouvé. Envoie INSCRIPTION sur WhatsApp pour t\'inscrire.');
} else {
setError('Erreur serveur. Réessaie dans un moment.');
}
} catch {
setError('Impossible de joindre le serveur.');
} finally { setLoading(false); }
};
return (
<div className="min-h-[80vh] flex flex-col justify-center py-12 sm:px-6 lg:px-8 bg-slate-50">
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center">
<div className="inline-flex bg-primary/10 p-4 rounded-full mb-4"><BookOpen className="w-10 h-10 text-primary" /></div>
<h2 className="text-center text-3xl font-heading font-extrabold text-secondary">Mon espace étudiant</h2>
<p className="mt-2 text-center text-sm text-gray-600">Entre ton numéro WhatsApp pour voir ta progression.</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-2xl sm:px-10 border border-gray-100">
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700">Numéro WhatsApp</label>
<div className="mt-1 relative rounded-md shadow-sm">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Phone className="h-5 w-5 text-gray-400" />
</div>
<input id="phone" name="phone" type="tel" required value={phone} onChange={e => setPhone(e.target.value)}
className="focus:ring-primary focus:border-primary block w-full pl-10 sm:text-sm border-gray-300 rounded-xl py-3 border outline-none transition-colors"
placeholder="+221 77 123 45 67" />
</div>
</div>
{error && <div className="flex items-start gap-2 p-3 bg-red-50 rounded-xl text-sm text-red-700"><AlertCircle className="w-4 h-4 shrink-0 mt-0.5" />{error}</div>}
<button type="submit" disabled={loading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-secondary hover:bg-blue-900 focus:outline-none transition-all active:scale-95 disabled:opacity-50">
{loading ? 'Recherche...' : 'Voir ma progression'}
</button>
</form>
<p className="mt-6 text-center text-xs text-gray-400">
Pas encore inscrit ? <a href={`https://wa.me/${WA_NUMBER}?text=INSCRIPTION`} className="text-primary font-medium">Envoie INSCRIPTION sur WhatsApp</a>
</p>
</div>
</div>
</div>
);
}
// ── Student Dashboard ─────────────────────────────────────────────────────────────
function StudentDashboard() {
const { phone } = useParams<{ phone: string }>();
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetch(`${API_URL}/v1/student/me?phone=${phone}`)
.then(r => { if (!r.ok) throw new Error('not found'); return r.json(); })
.then(setData)
.catch(() => setError('Impossible de charger tes données.'))
.finally(() => setLoading(false));
}, [phone]);
if (loading) return <div className="min-h-screen flex items-center justify-center text-slate-400">Chargement...</div>;
if (error) return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4">
<AlertCircle className="w-10 h-10 text-red-400" />
<p className="text-slate-600">{error}</p>
<Link to="/student" className="text-primary font-medium">← Retour</Link>
</div>
);
return (
<div className="min-h-screen bg-slate-50 py-12">
<div className="max-w-3xl mx-auto px-4">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-slate-800">{data.name || 'Mon espace'}</h1>
<p className="text-sm text-slate-500">{data.phone} · {data.language} · {data.activity || 'Secteur non défini'}</p>
</div>
<Link to="/student" className="text-sm text-slate-400 hover:text-slate-600">← Changer de compte</Link>
</div>
{/* Enrollments */}
<h2 className="text-lg font-bold text-slate-700 mb-4">Mes formations</h2>
{data.enrollments.length === 0 ? (
<div className="bg-white rounded-2xl border border-slate-100 p-8 text-center text-slate-400 mb-8">
<BookOpen className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p>Tu n'es inscrit à aucune formation.</p>
<a href={`https://wa.me/${WA_NUMBER}?text=FORMATION`} target="_blank" rel="noreferrer"
className="mt-4 inline-block bg-primary text-white px-5 py-2 rounded-full text-sm font-medium hover:bg-emerald-700 transition">
Voir les formations disponibles
</a>
</div>
) : (
<div className="grid gap-4 mb-8">
{data.enrollments.map((e: any) => (
<div key={e.id} className="bg-white rounded-2xl border border-slate-100 p-6 shadow-sm">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-bold text-slate-800">{e.trackTitle}</h3>
<p className="text-sm text-slate-500 mt-1">Jour {e.currentDay} sur {e.totalDays}</p>
</div>
<span className={`text-xs font-medium px-2.5 py-1 rounded-full ${e.status === 'ACTIVE' ? 'bg-blue-100 text-blue-700' : e.status === 'COMPLETED' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'}`}>
{e.status === 'ACTIVE' ? '🟢 En cours' : e.status === 'COMPLETED' ? '✅ Terminé' : e.status}
</span>
</div>
{/* Progress bar */}
<div className="w-full bg-slate-100 rounded-full h-2 mb-2">
<div className="bg-primary h-2 rounded-full transition-all" style={{ width: `${e.progressPercent}%` }}></div>
</div>
<p className="text-xs text-slate-400 text-right">{e.progressPercent}% complété</p>
{e.status === 'ACTIVE' && (
<a href={`https://wa.me/${WA_NUMBER}?text=SUITE`} target="_blank" rel="noreferrer"
className="mt-4 flex items-center justify-center gap-2 bg-primary/10 text-primary font-medium text-sm py-2.5 rounded-xl hover:bg-primary/20 transition">
<Phone className="w-4 h-4" /> Continuer sur WhatsApp
</a>
)}
</div>
))}
</div>
)}
{/* Payments */}
{data.payments.length > 0 && (
<>
<h2 className="text-lg font-bold text-slate-700 mb-4">Mes paiements</h2>
<div className="bg-white rounded-2xl border border-slate-100 p-5 shadow-sm">
{data.payments.map((p: any) => (
<div key={p.id} className="flex items-center justify-between py-3 border-b border-slate-50 last:border-0">
<div className="flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-green-500" />
<div>
<p className="text-sm font-medium text-slate-800">{p.amount.toLocaleString()} {p.currency}</p>
<p className="text-xs text-slate-400">{new Date(p.createdAt).toLocaleDateString('fr-FR')}</p>
</div>
</div>
<span className="text-xs text-green-600 font-medium bg-green-50 px-2 py-1 rounded-full">Payé</span>
</div>
))}
</div>
</>
)}
</div>
</div>
);
}
// ── Payment Success ───────────────────────────────────────────────────────────────
function PaymentSuccess() {
const [searchParams] = useSearchParams();
const phone = searchParams.get('phone') || '';
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
<div className="bg-white rounded-3xl shadow-xl p-10 max-w-md w-full text-center">
<div className="inline-flex bg-green-100 p-5 rounded-full mb-6">
<CheckCircle className="w-12 h-12 text-green-500" />
</div>
<h1 className="text-3xl font-heading font-bold text-slate-800 mb-3">Paiement réussi !</h1>
<p className="text-gray-500 mb-8">Ta formation a été débloquée. Tu vas recevoir ta première leçon sur WhatsApp dans quelques minutes.</p>
<div className="bg-slate-50 rounded-2xl p-5 mb-8">
<p className="text-sm text-slate-500 mb-4">Continue ton apprentissage :</p>
<a href={`https://wa.me/${WA_NUMBER}?text=SUITE`} target="_blank" rel="noreferrer"
className="flex items-center justify-center gap-2 bg-primary text-white font-bold py-3.5 px-6 rounded-xl hover:bg-emerald-700 transition shadow-lg shadow-primary/30">
<Phone className="w-5 h-5" /> Ouvrir WhatsApp
</a>
</div>
{phone && (
<Link to={`/student/${phone.replace(/^\+/, '')}`} className="text-sm text-primary font-medium hover:underline flex items-center justify-center gap-1">
<Download className="w-4 h-4" /> Voir mon espace étudiant
</Link>
)}
<Link to="/" className="block mt-4 text-sm text-slate-400 hover:text-slate-600">← Retour à l'accueil</Link>
</div>
</div>
);
}
function Footer() {
return (
<footer className="bg-secondary text-white py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid md:grid-cols-4 gap-8">
<div className="col-span-2">
<Link to="/" className="flex items-center space-x-2 mb-4">
<BookOpen className="h-6 w-6 text-primary" />
<span className="font-heading font-bold text-xl tracking-tight">EdTech<span className="text-primary">.sn</span></span>
</Link>
<p className="text-gray-400 text-sm max-w-sm">Former les entrepreneurs d'Afrique grâce à une éducation accessible, mobile-first et propulsée par l'IA.</p>
</div>
<div><h4 className="font-bold mb-4 font-heading">Plateforme</h4>
<ul className="space-y-2 text-sm text-gray-400">
<li><a href={`https://wa.me/${WA_NUMBER}?text=INSCRIPTION`} className="hover:text-white transition">S'inscrire</a></li>
<li><Link to="/student" className="hover:text-white transition">Mon espace</Link></li>
</ul>
</div>
<div><h4 className="font-bold mb-4 font-heading">Entreprise</h4>
<ul className="space-y-2 text-sm text-gray-400">
<li><a href="#" className="hover:text-white transition">À propos</a></li>
<li><a href="#" className="hover:text-white transition">Contact</a></li>
<li><Link to="/privacy" className="hover:text-white transition">Confidentialité</Link></li>
</ul>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 mt-12 pt-8 border-t border-white/10 text-center text-sm text-gray-400">
© {new Date().getFullYear()} EdTech.sn. Tous droits réservés.
</div>
</footer>
);
}
function App() {
return (
<Router>
<div className="min-h-screen font-sans flex flex-col">
<Navbar />
<main className="flex-grow">
<Routes>
<Route path="/" element={<><Hero /><Features /></>} />
<Route path="/student" element={<StudentPortal />} />
<Route path="/student/:phone" element={<StudentDashboard />} />
<Route path="/payment/success" element={<PaymentSuccess />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
</Routes>
</main>
<Footer />
</div>
</Router>
);
}
export default App;