import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate, useParams } from 'react-router-dom'; import { useEffect, useState, createContext, useContext } from 'react'; import { Users, PlayCircle, CheckCircle, Lightbulb, Download, BookOpen, Plus, Edit2, Trash2, ChevronRight, X, Save, BarChart2, DollarSign, ArrowLeft, Mic, Activity } from 'lucide-react'; import LiveFeed from './pages/LiveFeed'; import TrainingLab from './pages/TrainingLab'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; const SESSION_KEY = 'edtech_admin_key'; export const AuthContext = createContext<{ apiKey: string | null; login: (k: string) => void; logout: () => void; }>({ apiKey: null, login: () => { }, logout: () => { } }); function AuthProvider({ children }: { children: React.ReactNode }) { const [apiKey, setApiKey] = useState(() => sessionStorage.getItem(SESSION_KEY)); const login = (k: string) => { sessionStorage.setItem(SESSION_KEY, k); setApiKey(k); }; const logout = () => { sessionStorage.removeItem(SESSION_KEY); setApiKey(null); }; return {children}; } export const useAuth = () => useContext(AuthContext); const ah = (k: string) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json' }); function ProtectedRoute({ children }: { children: React.ReactNode }) { const { apiKey } = useAuth(); if (!apiKey) return ; return <>{children}; } function LoginPage() { const { login, apiKey } = useAuth(); const navigate = useNavigate(); const [key, setKey] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { if (apiKey) navigate('/', { replace: true }); }, [apiKey, navigate]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { const res = await fetch(`${API_URL}/v1/admin/stats`, { headers: { 'Authorization': `Bearer ${key}` } }); if (res.ok) { login(key); navigate('/', { replace: true }); } else setError('Clé API invalide.'); } catch { setError('Impossible de joindre le serveur.'); } finally { setLoading(false); } }; return (
🔐

Admin Access

Entrez votre ADMIN_API_KEY

setKey(e.target.value)} className="w-full border border-slate-200 rounded-xl px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-slate-400" /> {error &&

{error}

}
); } function Dashboard() { const { apiKey, logout } = useAuth(); const [stats, setStats] = useState(null); const [enrollments, setEnrollments] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { (async () => { try { const h = { 'Authorization': `Bearer ${apiKey}` }; const [sRes, eRes] = await Promise.all([ fetch(`${API_URL}/v1/admin/stats`, { headers: h }), fetch(`${API_URL}/v1/admin/enrollments`, { headers: h }) ]); if (sRes.status === 401) { logout(); return; } setStats(await sRes.json()); setEnrollments(await eRes.json()); } finally { setLoading(false); } })(); }, [apiKey, logout]); const exportCSV = () => { if (!enrollments.length) return alert('Aucune inscription.'); const rows = enrollments.map((e: any) => [e.id, e.user?.phone, e.track?.title, e.status, e.currentDay, e.startedAt]); const csv = [['ID', 'Phone', 'Track', 'Status', 'Day', 'Started'].join(','), ...rows.map(r => r.join(','))].join('\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); a.download = `enrollments_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); }; if (loading) return
Chargement...
; const statCards = [ { icon: , label: 'Utilisateurs', value: stats?.totalUsers || 0, color: 'text-slate-900' }, { icon: , label: 'Actifs', value: stats?.activeEnrollments || 0, color: 'text-blue-600' }, { icon: , label: 'Complétés', value: stats?.completedEnrollments || 0, color: 'text-green-600' }, { icon: , label: 'Parcours', value: stats?.totalTracks || 0, color: 'text-purple-600' }, { icon: , label: 'Revenus', value: `${(stats?.totalRevenue || 0).toLocaleString()} XOF`, color: 'text-emerald-600' }, ]; return (

Dashboard

{statCards.map((s, i) => (
{s.icon}

{s.label}

{s.value}

))}

Inscriptions récentes

{['Téléphone', 'Parcours', 'Statut', 'Jour', 'Date'].map(h => )} {enrollments.map((e: any) => ( ))} {!enrollments.length && }
{h}
{e.user?.phone || '—'} {e.track?.title || '—'} {e.status} Jour {e.currentDay} {new Date(e.startedAt).toLocaleDateString('fr-FR')}
Aucune inscription
); } function TrackList() { const { apiKey } = useAuth(); const navigate = useNavigate(); const [tracks, setTracks] = useState([]); const [loading, setLoading] = useState(true); const load = async () => { const r = await fetch(`${API_URL}/v1/admin/tracks`, { headers: ah(apiKey!) }); setTracks(await r.json()); setLoading(false); }; useEffect(() => { load(); }, []); const del = async (id: string) => { if (!confirm('Supprimer ce parcours ?')) return; await fetch(`${API_URL}/v1/admin/tracks/${id}`, { method: 'DELETE', headers: ah(apiKey!) }); load(); }; if (loading) return
Chargement...
; return (

Parcours

{tracks.map((t: any) => (

{t.title}

{t.isPremium && Premium} {t.language}

{t._count?.days || 0} jours · {t._count?.enrollments || 0} inscrits · {t.duration}j

))} {!tracks.length &&

Aucun parcours. Créez-en un !

}
); } function TrackForm() { const { apiKey } = useAuth(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const isNew = id === 'new'; const [form, setForm] = useState({ title: '', description: '', duration: 7, language: 'FR', isPremium: false, priceAmount: 0, stripePriceId: '' }); const [saving, setSaving] = useState(false); useEffect(() => { if (!isNew) fetch(`${API_URL}/v1/admin/tracks/${id}`, { headers: ah(apiKey!) }).then(r => r.json()).then(t => setForm({ title: t.title, description: t.description || '', duration: t.duration, language: t.language, isPremium: t.isPremium, priceAmount: t.priceAmount || 0, stripePriceId: t.stripePriceId || '' })); }, [id]); const inp = "w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300"; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSaving(true); const url = isNew ? `${API_URL}/v1/admin/tracks` : `${API_URL}/v1/admin/tracks/${id}`; await fetch(url, { method: isNew ? 'POST' : 'PUT', headers: ah(apiKey!), body: JSON.stringify({ ...form, priceAmount: form.priceAmount || undefined, stripePriceId: form.stripePriceId || undefined }) }); navigate('/content'); }; return (

{isNew ? 'Nouveau parcours' : 'Modifier le parcours'}

setForm(f => ({ ...f, title: e.target.value }))} />