feat: implement dark mode support and enhance UI components for better accessibility and insert new lang french.

This commit is contained in:
Your Name
2026-03-03 10:53:52 +02:00
parent 31f1e4b312
commit 071c66d3b1
13 changed files with 412 additions and 74 deletions

View File

@@ -1,50 +1,174 @@
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { FileText, Globe } from 'lucide-react';
import { FileText, Globe, Moon, Sun, Menu, X, ChevronDown } from 'lucide-react';
interface LangOption {
code: string;
label: string;
flag: string;
}
const languages: LangOption[] = [
{ code: 'en', label: 'English', flag: '🇺🇸' },
{ code: 'ar', label: 'العربية', flag: '🇸🇦' },
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
];
function useDarkMode() {
const [isDark, setIsDark] = useState(() => {
if (typeof window === 'undefined') return false;
const stored = localStorage.getItem('theme');
if (stored) return stored === 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
const root = document.documentElement;
if (isDark) {
root.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
root.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [isDark]);
return { isDark, toggle: () => setIsDark((v) => !v) };
}
export default function Header() {
const { t, i18n } = useTranslation();
const { isDark, toggle: toggleDark } = useDarkMode();
const [langOpen, setLangOpen] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const langRef = useRef<HTMLDivElement>(null);
const toggleLanguage = () => {
const newLang = i18n.language === 'ar' ? 'en' : 'ar';
i18n.changeLanguage(newLang);
const currentLang = languages.find((l) => l.code === i18n.language) ?? languages[0];
// Close language dropdown on outside click
useEffect(() => {
function handleClick(e: MouseEvent) {
if (langRef.current && !langRef.current.contains(e.target as Node)) {
setLangOpen(false);
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
const switchLang = (code: string) => {
i18n.changeLanguage(code);
setLangOpen(false);
};
return (
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg">
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/80 backdrop-blur-lg dark:border-slate-700 dark:bg-slate-900/80">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600">
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-primary-600 dark:text-primary-400">
<FileText className="h-7 w-7" />
<span>{t('common.appName')}</span>
</Link>
{/* Navigation */}
{/* Desktop Navigation */}
<nav className="hidden items-center gap-6 md:flex">
<Link
to="/"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
>
{t('common.home')}
</Link>
<Link
to="/about"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600"
className="text-sm font-medium text-slate-600 transition-colors hover:text-primary-600 dark:text-slate-300 dark:hover:text-primary-400"
>
{t('common.about')}
</Link>
</nav>
{/* Language Toggle */}
<button
onClick={toggleLanguage}
className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100"
aria-label={t('common.language')}
>
<Globe className="h-4 w-4" />
<span>{i18n.language === 'ar' ? 'English' : 'العربية'}</span>
</button>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Dark Mode Toggle */}
<button
onClick={toggleDark}
className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-800"
aria-label={isDark ? t('common.lightMode') : t('common.darkMode')}
title={isDark ? t('common.lightMode') : t('common.darkMode')}
>
{isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
{/* Language Dropdown */}
<div className="relative" ref={langRef}>
<button
onClick={() => setLangOpen((v) => !v)}
className="flex items-center gap-1.5 rounded-xl px-3 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"
aria-label={t('common.language')}
aria-expanded={langOpen}
aria-haspopup="listbox"
>
<span className="text-lg leading-none">{currentLang.flag}</span>
<span className="hidden sm:inline">{currentLang.label}</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${langOpen ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown Menu */}
{langOpen && (
<div className="absolute end-0 top-full z-50 mt-2 w-44 origin-top-right animate-in fade-in slide-in-from-top-2 rounded-xl border border-slate-200 bg-white p-1 shadow-lg dark:border-slate-700 dark:bg-slate-800">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => switchLang(lang.code)}
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
lang.code === i18n.language
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-700'
}`}
role="option"
aria-selected={lang.code === i18n.language}
>
<span className="text-lg leading-none">{lang.flag}</span>
<span>{lang.label}</span>
{lang.code === i18n.language && (
<span className="ms-auto text-primary-600 dark:text-primary-400"></span>
)}
</button>
))}
</div>
)}
</div>
{/* Mobile Menu Toggle */}
<button
onClick={() => setMobileOpen((v) => !v)}
className="flex items-center justify-center rounded-xl p-2.5 text-slate-500 transition-colors hover:bg-slate-100 md:hidden dark:text-slate-400 dark:hover:bg-slate-800"
aria-label="Menu"
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div>
{/* Mobile Navigation */}
{mobileOpen && (
<nav className="border-t border-slate-200 bg-white px-4 pb-4 pt-2 md:hidden dark:border-slate-700 dark:bg-slate-900">
<Link
to="/"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.home')}
</Link>
<Link
to="/about"
onClick={() => setMobileOpen(false)}
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:bg-slate-50 dark:text-slate-300 dark:hover:bg-slate-800"
>
{t('common.about')}
</Link>
</nav>
)}
</header>
);
}