|
|
import React, { useState, useEffect } from 'react'; |
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom'; |
|
|
import { Menu, X, GraduationCap, BookOpen, Users, Award, Brain, Sun, Moon, Globe, LogIn, UserPlus, MessageCircle, Bell, LogOut, Wrench, LayoutDashboard, Settings } from 'lucide-react'; |
|
|
import { useTheme } from '../context/ThemeContext'; |
|
|
import { useAuth } from '../context/AuthContext'; |
|
|
import AskQuestionModal from './AskQuestionModal'; |
|
|
import { UserRole } from '../types'; |
|
|
import { socialLinkService, SocialLink } from '../services/socials'; |
|
|
import { Facebook, Twitter, Linkedin, Instagram, Youtube, Mail } from 'lucide-react'; |
|
|
|
|
|
interface LayoutProps { |
|
|
children: React.ReactNode; |
|
|
} |
|
|
|
|
|
const Layout: React.FC<LayoutProps> = ({ children }) => { |
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); |
|
|
const { isDark, toggleTheme } = useTheme(); |
|
|
const { user, isAuthenticated, logout, notifications } = useAuth(); |
|
|
const location = useLocation(); |
|
|
const navigate = useNavigate(); |
|
|
|
|
|
|
|
|
const unreadCount = notifications.filter(n => !n.isRead).length; |
|
|
const [socialLinks, setSocialLinks] = useState<SocialLink[]>([]); |
|
|
|
|
|
useEffect(() => { |
|
|
const fetchSocials = async () => { |
|
|
try { |
|
|
const data = await socialLinkService.getSocialLinks(); |
|
|
setSocialLinks(data); |
|
|
} catch (error) { |
|
|
console.error("Failed to fetch social links", error); |
|
|
} |
|
|
}; |
|
|
fetchSocials(); |
|
|
}, []); |
|
|
|
|
|
const getSocialIcon = (iconName: string) => { |
|
|
switch (iconName) { |
|
|
case 'Facebook': return <Facebook size={18} />; |
|
|
case 'Twitter': return <Twitter size={18} />; |
|
|
case 'Linkedin': return <Linkedin size={18} />; |
|
|
case 'Instagram': return <Instagram size={18} />; |
|
|
case 'Youtube': return <Youtube size={18} />; |
|
|
case 'Mail': return <Mail size={18} />; |
|
|
default: return <Globe size={18} />; |
|
|
} |
|
|
}; |
|
|
|
|
|
const navItems = [ |
|
|
{ path: '/', label: 'Accueil', icon: <Globe size={20} /> }, |
|
|
{ path: '/questions', label: 'Questions', icon: <BookOpen size={20} /> }, |
|
|
{ path: '/mentors', label: 'Mentors', icon: <Users size={20} /> }, |
|
|
{ path: '/opportunities', label: 'Opportunités', icon: <GraduationCap size={20} /> }, |
|
|
{ path: '/tools', label: 'Outils Pratiques', icon: <Wrench size={20} /> }, |
|
|
{ path: '/chat', label: 'Messages', icon: <MessageCircle size={20} /> }, |
|
|
{ path: '/ai-tutor', label: 'Tuteur IA', icon: <Brain size={20} /> }, |
|
|
]; |
|
|
|
|
|
const isActive = (path: string) => location.pathname === path; |
|
|
|
|
|
const handleLogout = () => { |
|
|
logout(); |
|
|
setIsMobileMenuOpen(false); |
|
|
navigate('/login'); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen flex flex-col bg-edu-bg dark:bg-edu-darkbg text-edu-primary dark:text-gray-100 transition-colors duration-300"> |
|
|
{/* Header */} |
|
|
<header className="sticky top-0 z-50 bg-edu-primary text-white shadow-lg"> |
|
|
<div className="container mx-auto px-4 max-w-7xl h-16 flex items-center justify-between"> |
|
|
{/* Logo */} |
|
|
<Link to="/" className="flex items-center space-x-2 group shrink-0"> |
|
|
<img src="/logo.png" alt="EduLab Africa" className="h-14 w-auto object-contain group-hover:scale-105 transition-transform" /> |
|
|
</Link> |
|
|
|
|
|
{/* Desktop Nav - Optimized spacing for xl screens (Tablet/Laptop uses hamburger) */} |
|
|
<nav className="hidden xl:flex items-center gap-0.5 xl:gap-1"> |
|
|
{navItems.map((item) => ( |
|
|
<Link |
|
|
key={item.path} |
|
|
to={item.path} |
|
|
className={`flex items-center space-x-1 px-1.5 py-1.5 rounded-full transition-all duration-200 whitespace-nowrap ${isActive(item.path) |
|
|
? 'bg-edu-secondary text-white shadow-md' |
|
|
: 'text-gray-300 hover:text-white hover:bg-white/10' |
|
|
}`} |
|
|
> |
|
|
{/* Scale icon down slightly on medium screens */} |
|
|
<span className="scale-75 xl:scale-90">{item.icon}</span> |
|
|
<span className="text-[10px] xl:text-xs font-medium">{item.label}</span> |
|
|
</Link> |
|
|
))} |
|
|
|
|
|
{/* Mentor Dashboard Link (Desktop) */} |
|
|
{user?.role === UserRole.MENTOR && ( |
|
|
<Link |
|
|
to="/dashboard" |
|
|
className={`flex items-center space-x-1 px-1.5 py-1.5 rounded-full transition-all duration-200 whitespace-nowrap border border-edu-accent ${isActive('/dashboard') |
|
|
? 'bg-edu-accent text-edu-primary shadow-md font-bold' |
|
|
: 'text-edu-accent hover:bg-edu-accent hover:text-edu-primary' |
|
|
}`} |
|
|
> |
|
|
<LayoutDashboard size={16} /> |
|
|
<span className="text-[10px] xl:text-xs font-medium">Tableau de bord</span> |
|
|
</Link> |
|
|
)} |
|
|
</nav> |
|
|
|
|
|
{/* User & Actions */} |
|
|
<div className="hidden md:flex items-center gap-2 lg:gap-4 shrink-0"> |
|
|
<button |
|
|
onClick={toggleTheme} |
|
|
className="p-2 rounded-full hover:bg-white/10 text-edu-accent transition-colors" |
|
|
title="Changer le thème" |
|
|
> |
|
|
{isDark ? <Sun size={20} /> : <Moon size={20} />} |
|
|
</button> |
|
|
|
|
|
{isAuthenticated && ( |
|
|
<Link |
|
|
to="/notifications" |
|
|
className="p-2 rounded-full hover:bg-white/10 text-gray-300 hover:text-white transition-colors relative" |
|
|
title="Notifications" |
|
|
> |
|
|
<Bell size={20} /> |
|
|
{unreadCount > 0 && ( |
|
|
<span className="absolute top-1.5 right-2 w-4 h-4 bg-red-500 rounded-full border border-edu-primary flex items-center justify-center text-[9px] font-bold"> |
|
|
{unreadCount > 9 ? '9+' : unreadCount} |
|
|
</span> |
|
|
)} |
|
|
</Link> |
|
|
)} |
|
|
|
|
|
{isAuthenticated && user ? ( |
|
|
<div className="flex items-center gap-2"> |
|
|
<Link to="/badges" className="flex items-center space-x-2 lg:space-x-3 bg-white/10 px-2 lg:px-3 py-1.5 rounded-full border border-white/10 hover:bg-white/20 transition-colors"> |
|
|
<div className="text-right hidden lg:block"> |
|
|
<div className="text-xs text-gray-300">{user.points} pts</div> |
|
|
<div className="text-sm font-semibold leading-none max-w-[100px] truncate">{user.name}</div> |
|
|
</div> |
|
|
<img src={user.avatar} alt="Profile" className="w-8 h-8 rounded-full border-2 border-edu-accent" /> |
|
|
</Link> |
|
|
<Link |
|
|
to="/settings" |
|
|
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-colors" |
|
|
title="Paramètres" |
|
|
> |
|
|
<Settings size={20} /> |
|
|
</Link> |
|
|
<button |
|
|
onClick={handleLogout} |
|
|
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-colors" |
|
|
title="Se déconnecter" |
|
|
> |
|
|
<LogOut size={20} /> |
|
|
</button> |
|
|
</div> |
|
|
) : ( |
|
|
<div className="flex items-center gap-2 lg:gap-3"> |
|
|
<Link |
|
|
to="/login" |
|
|
className="px-2 lg:px-4 py-2 text-xs lg:text-sm font-medium text-white hover:text-edu-accent transition-colors whitespace-nowrap" |
|
|
> |
|
|
Se connecter |
|
|
</Link> |
|
|
<Link |
|
|
to="/register" |
|
|
className="px-3 lg:px-4 py-2 text-xs lg:text-sm font-bold bg-edu-accent text-edu-primary rounded-full hover:bg-white transition-colors whitespace-nowrap" |
|
|
> |
|
|
S'inscrire |
|
|
</Link> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Mobile/Tablet Menu Button - Visible until XL */} |
|
|
<button |
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} |
|
|
className="xl:hidden p-2 text-white" |
|
|
> |
|
|
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />} |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
{/* Mobile/Tablet Navigation Drawer - Visible until XL */} |
|
|
{ |
|
|
isMobileMenuOpen && ( |
|
|
<div className="xl:hidden bg-edu-primary border-t border-white/10 animate-in slide-in-from-top-5"> |
|
|
<div className="p-4 space-y-2"> |
|
|
{navItems.map((item) => ( |
|
|
<Link |
|
|
key={item.path} |
|
|
to={item.path} |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className={`flex items-center space-x-3 px-4 py-3 rounded-xl ${isActive(item.path) ? 'bg-edu-secondary text-white' : 'text-gray-300 hover:bg-white/5' |
|
|
}`} |
|
|
> |
|
|
{item.icon} |
|
|
<span>{item.label}</span> |
|
|
</Link> |
|
|
))} |
|
|
|
|
|
{/* Mentor Dashboard Link (Mobile) */} |
|
|
{user?.role === UserRole.MENTOR && ( |
|
|
<Link |
|
|
to="/dashboard" |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className={`flex items-center space-x-3 px-4 py-3 rounded-xl border border-edu-accent ${isActive('/dashboard') ? 'bg-edu-accent text-edu-primary' : 'text-edu-accent hover:bg-white/5' |
|
|
}`} |
|
|
> |
|
|
<LayoutDashboard size={20} /> |
|
|
<span className="font-bold">Tableau de bord Mentor</span> |
|
|
</Link> |
|
|
)} |
|
|
|
|
|
{/* Hide Auth Actions on Tablet (md) since they are visible in header */} |
|
|
<div className="border-t border-white/10 my-2 pt-4 flex flex-col gap-3 md:hidden"> |
|
|
{!isAuthenticated ? ( |
|
|
<> |
|
|
<Link |
|
|
to="/login" |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className="flex items-center justify-center gap-2 w-full py-2 rounded-lg border border-white/20 text-white hover:bg-white/10" |
|
|
> |
|
|
<LogIn size={18} /> |
|
|
Connexion |
|
|
</Link> |
|
|
<Link |
|
|
to="/register" |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className="flex items-center justify-center gap-2 w-full py-2 rounded-lg bg-edu-accent text-edu-primary font-bold hover:bg-white" |
|
|
> |
|
|
<UserPlus size={18} /> |
|
|
Inscription |
|
|
</Link> |
|
|
</> |
|
|
) : ( |
|
|
<> |
|
|
<div className="flex items-center gap-3 px-4 py-2"> |
|
|
<img src={user?.avatar} alt="profile" className="w-8 h-8 rounded-full" /> |
|
|
<div> |
|
|
<div className="text-white font-bold">{user?.name}</div> |
|
|
<div className="text-xs text-edu-accent">{user?.points} points</div> |
|
|
</div> |
|
|
</div> |
|
|
<Link |
|
|
to="/badges" |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-300 hover:bg-white/5" |
|
|
> |
|
|
<Award size={20} /> |
|
|
<span>Mes Badges</span> |
|
|
</Link> |
|
|
<Link |
|
|
to="/settings" |
|
|
onClick={() => setIsMobileMenuOpen(false)} |
|
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-300 hover:bg-white/5" |
|
|
> |
|
|
<Settings size={20} /> |
|
|
<span>Paramètres</span> |
|
|
</Link> |
|
|
<button |
|
|
onClick={handleLogout} |
|
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-red-400 hover:bg-white/5 w-full text-left" |
|
|
> |
|
|
<LogOut size={20} /> |
|
|
<span>Déconnexion</span> |
|
|
</button> |
|
|
</> |
|
|
)} |
|
|
|
|
|
<div className="flex justify-between items-center px-2 mt-2"> |
|
|
{isAuthenticated && ( |
|
|
<Link to="/notifications" onClick={() => setIsMobileMenuOpen(false)} className="flex items-center gap-2 text-gray-400"> |
|
|
<div className="relative"> |
|
|
<Bell size={18} /> |
|
|
{unreadCount > 0 && <span className="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full"></span>} |
|
|
</div> |
|
|
<span className="text-sm">Notifications</span> |
|
|
</Link> |
|
|
)} |
|
|
|
|
|
<div className="flex items-center gap-3 ml-auto"> |
|
|
<span className="text-sm text-gray-400">Mode Sombre</span> |
|
|
<button |
|
|
onClick={toggleTheme} |
|
|
className="p-2 rounded-full bg-white/10 text-edu-accent" |
|
|
> |
|
|
{isDark ? <Sun size={18} /> : <Moon size={18} />} |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
</header> |
|
|
|
|
|
{} |
|
|
<main className="flex-grow container mx-auto px-4 py-6 max-w-7xl"> |
|
|
{children} |
|
|
</main> |
|
|
|
|
|
{} |
|
|
<AskQuestionModal /> |
|
|
|
|
|
{} |
|
|
<footer className="bg-edu-primary text-gray-400 py-8 border-t border-white/10"> |
|
|
<div className="container mx-auto px-4 text-center md:text-left"> |
|
|
<div className="grid md:grid-cols-3 gap-8 mb-8"> |
|
|
<div> |
|
|
<h3 className="text-white font-bold text-lg mb-4">Une initiative Hypee</h3> |
|
|
<p className="text-sm mb-4"> |
|
|
Développée au <strong>Bénin</strong>, EduLab pallie le manque de matériel dans le système éducatif africain. |
|
|
</p> |
|
|
<p className="text-sm text-edu-accent italic"> |
|
|
"Nous transformons la théorie en savoir-faire concret pour la jeunesse active." |
|
|
</p> |
|
|
</div> |
|
|
<div> |
|
|
<h3 className="text-white font-bold text-lg mb-4">Accès Direct</h3> |
|
|
<ul className="space-y-2 text-sm"> |
|
|
<li><Link to="/tools" className="hover:text-edu-accent">Outils Scientifiques</Link></li> |
|
|
<li><Link to="/questions" className="hover:text-edu-accent">Questions récentes</Link></li> |
|
|
<li><Link to="/mentors" className="hover:text-edu-accent">Mentorat</Link></li> |
|
|
</ul> |
|
|
</div> |
|
|
<div> |
|
|
<h3 className="text-white font-bold text-lg mb-4">Hypee Community</h3> |
|
|
<p className="text-sm mb-2">L'innovation sociale au service de l'éducation.</p> |
|
|
<div className="flex justify-center md:justify-start space-x-4"> |
|
|
{socialLinks.map(link => ( |
|
|
<a |
|
|
key={link.id} |
|
|
href={link.url} |
|
|
target="_blank" |
|
|
rel="noopener noreferrer" |
|
|
className="w-8 h-8 bg-white/10 rounded-full hover:bg-edu-accent hover:text-edu-primary transition-colors cursor-pointer flex items-center justify-center" |
|
|
title={link.name} |
|
|
> |
|
|
{getSocialIcon(link.icon)} |
|
|
</a> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div className="text-xs text-center border-t border-white/5 pt-6 flex flex-col md:flex-row justify-between items-center gap-2"> |
|
|
<span>© 2025 EduLab Africa. Fièrement propulsé par Hypee (Bénin).</span> |
|
|
<div className="flex gap-4"> |
|
|
<Link to="/terms" className="hover:text-edu-accent transition-colors">Conditions</Link> |
|
|
<Link to="/privacy" className="hover:text-edu-accent transition-colors">Confidentialité</Link> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</footer> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default Layout; |