- Implement ProfileSetup component for user profile creation with username validation. - Create ProtectedRoute component to guard routes based on authentication status. - Add VerifyAuthCode component for email verification with resend functionality. - Establish AuthContext for managing user authentication state and actions. - Enhance CookieContext for consent management with server-side verification. - Set up Firebase configuration for authentication and analytics. - Update styles for authentication components and header layout. - Configure Vite proxy for API requests during development.
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Menu, X, Sun, Moon, Globe, LogIn, LogOut } from 'lucide-react';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import { useNavigate, useLocation, Link } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
interface HeaderProps {
|
|
darkMode: boolean;
|
|
toggleDarkMode: () => void;
|
|
}
|
|
|
|
const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const { language, setLanguage, t } = useLanguage();
|
|
const { user, loading, logout } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
type MenuItem = {
|
|
id: string;
|
|
name: string;
|
|
href: string;
|
|
kind: 'section' | 'route';
|
|
};
|
|
|
|
const menuItems: MenuItem[] = [
|
|
{ id: 'home', name: t('nav.home'), href: '#hero', kind: 'section' },
|
|
{ id: 'about', name: t('nav.about'), href: '#about', kind: 'section' },
|
|
{ id: 'skills', name: t('nav.skills'), href: '#skills', kind: 'section' },
|
|
{ id: 'projects', name: t('nav.projects'), href: '#projects', kind: 'section' },
|
|
{ id: 'education', name: t('nav.education'), href: '#education', kind: 'section' },
|
|
{ id: 'contact', name: t('nav.contact'), href: '#contact', kind: 'section' }
|
|
];
|
|
|
|
const handleNavigation = (item: MenuItem) => {
|
|
setIsMenuOpen(false);
|
|
if (item.kind === 'route') {
|
|
navigate(item.href);
|
|
return;
|
|
}
|
|
|
|
if (location.pathname === `/${language}` || location.pathname === `/${language}/`) {
|
|
// Already on home, just scroll
|
|
const element = document.querySelector(item.href);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
} else {
|
|
// Not on home, navigate then scroll (using a simple timeout for simplicity or hash)
|
|
navigate(`/${language}`);
|
|
// Small timeout to allow navigation to render Home before scrolling
|
|
setTimeout(() => {
|
|
const element = document.querySelector(item.href);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
const toggleLanguage = () => {
|
|
setLanguage(language === 'fr' ? 'en' : 'fr');
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await logout();
|
|
} catch {}
|
|
};
|
|
|
|
const avatarLabel = user?.displayName?.trim()?.charAt(0)?.toUpperCase() || user?.email?.trim()?.charAt(0)?.toUpperCase() || 'U';
|
|
|
|
return (
|
|
<motion.header
|
|
initial={{ y: -100 }}
|
|
animate={{ y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
className="header"
|
|
>
|
|
<nav className="nav">
|
|
<motion.div
|
|
className="nav-brand"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<Link to={`/${language}`} onClick={(e) => {
|
|
e.preventDefault();
|
|
handleNavigation({ id: 'brand', name: 'home', href: '#hero', kind: 'section' });
|
|
}}>
|
|
Dayron Van Leemput
|
|
</Link>
|
|
</motion.div>
|
|
|
|
<div className="nav-spacer" />
|
|
|
|
<div className="nav-controls">
|
|
{/* Toggle langue */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={toggleLanguage}
|
|
className="language-toggle"
|
|
aria-label={t('btn.changeLang')}
|
|
title={t('btn.changeLang')}
|
|
>
|
|
<Globe size={18} />
|
|
<span className="language-text">{language === 'fr' ? 'EN' : 'FR'}</span>
|
|
</motion.button>
|
|
|
|
{/* Toggle thème */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={toggleDarkMode}
|
|
className="theme-toggle"
|
|
aria-label={t('btn.changeTheme')}
|
|
>
|
|
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
|
|
</motion.button>
|
|
|
|
{!loading && user ? (
|
|
<>
|
|
<div className="auth-avatar" title={user.email ?? ''} aria-label={user.email ?? 'User avatar'}>
|
|
{user.photoURL ? <img src={user.photoURL} alt={user.displayName || 'Avatar utilisateur'} /> : <span>{avatarLabel}</span>}
|
|
</div>
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={handleLogout}
|
|
className="auth-toggle auth-toggle--logout"
|
|
aria-label={language === 'fr' ? 'Se déconnecter' : 'Sign out'}
|
|
title={language === 'fr' ? 'Se déconnecter' : 'Sign out'}
|
|
>
|
|
<LogOut size={16} />
|
|
<span className="logout-text">{language === 'fr' ? 'Se déconnecter' : 'Sign out'}</span>
|
|
</motion.button>
|
|
</>
|
|
) : (
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={() => navigate(`/${language}/login`)}
|
|
className="auth-toggle auth-toggle--login"
|
|
aria-label={language === 'fr' ? 'Aller à la page de connexion' : 'Go to login page'}
|
|
title={language === 'fr' ? 'Aller à la page de connexion' : 'Go to login page'}
|
|
>
|
|
<LogIn size={16} />
|
|
<span className="auth-toggle-text">{language === 'fr' ? 'Connexion' : 'Sign in'}</span>
|
|
</motion.button>
|
|
)}
|
|
|
|
{/* Menu hamburger mobile */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
className="menu-toggle"
|
|
aria-label={t('btn.menu')}
|
|
>
|
|
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</motion.button>
|
|
</div>
|
|
|
|
<div className={`nav-drawer ${isMenuOpen ? 'open' : ''}`}>
|
|
<ul className="nav-drawer-list">
|
|
{menuItems.map((item) => (
|
|
<li key={item.id}>
|
|
<a
|
|
href={item.href}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
handleNavigation(item);
|
|
}}
|
|
className={item.kind === 'route' && location.pathname === item.href ? 'is-active' : ''}
|
|
>
|
|
{item.name}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</motion.header>
|
|
);
|
|
};
|
|
|
|
export default Header;
|