feat: add profile setup, verification, and authentication context

- 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.
This commit is contained in:
Van Leemput Dayron
2026-02-24 08:25:37 +01:00
parent d3e0570214
commit 46e6edcd9c
30 changed files with 3469 additions and 324 deletions

View File

@@ -1,8 +1,9 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Menu, X, Sun, Moon, Globe } from 'lucide-react';
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;
@@ -12,24 +13,36 @@ interface HeaderProps {
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();
const menuItems = [
{ id: 'home', name: t('nav.home'), href: '#hero' },
{ id: 'about', name: t('nav.about'), href: '#about' },
{ id: 'skills', name: t('nav.skills'), href: '#skills' },
{ id: 'projects', name: t('nav.projects'), href: '#projects' },
{ id: 'education', name: t('nav.education'), href: '#education' },
{ id: 'contact', name: t('nav.contact'), href: '#contact' }
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 = (href: string) => {
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(href);
const element = document.querySelector(item.href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
@@ -38,7 +51,7 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
navigate(`/${language}`);
// Small timeout to allow navigation to render Home before scrolling
setTimeout(() => {
const element = document.querySelector(href);
const element = document.querySelector(item.href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
@@ -50,6 +63,14 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
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 }}
@@ -65,33 +86,13 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
>
<Link to={`/${language}`} onClick={(e) => {
e.preventDefault();
handleNavigation('#hero');
handleNavigation({ id: 'brand', name: 'home', href: '#hero', kind: 'section' });
}}>
Dayron Van Leemput
</Link>
</motion.div>
{/* Navigation desktop */}
<ul className="nav-menu desktop-menu">
{menuItems.map((item, index) => (
<motion.li
key={item.id}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</motion.li>
))}
</ul>
<div className="nav-spacer" />
<div className="nav-controls">
{/* Toggle langue */}
@@ -118,6 +119,37 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
{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 }}
@@ -130,30 +162,27 @@ const Header = ({ darkMode, toggleDarkMode }: HeaderProps) => {
</motion.button>
</div>
{/* Navigation mobile */}
<motion.ul
className={`nav-menu mobile-menu ${isMenuOpen ? 'open' : ''}`}
initial={false}
animate={isMenuOpen ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
transition={{ duration: 0.3 }}
>
{menuItems.map((item) => (
<li key={item.id}>
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavigation(item.href);
}}
>
{item.name}
</a>
</li>
))}
</motion.ul>
<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;
export default Header;