| | 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> |
| | ); |
| | } |
| |
|
| | |
| | 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> |
| | ); |
| | } |
| |
|
| | |
| | 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> |
| | ); |
| | } |
| | |
| | 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; |
| |
|