feat: implement dark mode support and enhance UI components for better accessibility and insert new lang french.
This commit is contained in:
@@ -6,11 +6,11 @@ export default function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-slate-200 bg-slate-50">
|
||||
<footer className="border-t border-slate-200 bg-slate-50 dark:border-slate-700 dark:bg-slate-900">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
|
||||
<FileText className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">
|
||||
© {new Date().getFullYear()} {t('common.appName')}
|
||||
@@ -21,19 +21,19 @@ export default function Footer() {
|
||||
<div className="flex items-center gap-6">
|
||||
<Link
|
||||
to="/privacy"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
|
||||
>
|
||||
{t('common.privacy')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/terms"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
|
||||
>
|
||||
{t('common.terms')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/about"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600"
|
||||
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
|
||||
>
|
||||
{t('common.about')}
|
||||
</Link>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
|
||||
if (!result.download_url) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl bg-emerald-50 p-6 ring-1 ring-emerald-200">
|
||||
<div className="rounded-2xl bg-emerald-50 p-6 ring-1 ring-emerald-200 dark:bg-emerald-900/20 dark:ring-emerald-800">
|
||||
{/* Success header */}
|
||||
<div className="mb-4 text-center">
|
||||
<p className="text-lg font-semibold text-emerald-800">
|
||||
<p className="text-lg font-semibold text-emerald-800 dark:text-emerald-300">
|
||||
{t('result.conversionComplete')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-emerald-600">
|
||||
<p className="mt-1 text-sm text-emerald-600 dark:text-emerald-400">
|
||||
{t('result.downloadReady')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -31,24 +31,24 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
|
||||
{(result.original_size || result.compressed_size) && (
|
||||
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{result.original_size && (
|
||||
<div className="rounded-lg bg-white p-3 text-center">
|
||||
<p className="text-xs text-slate-500">{t('result.originalSize')}</p>
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
<div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{t('result.originalSize')}</p>
|
||||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{formatFileSize(result.original_size)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{result.compressed_size && (
|
||||
<div className="rounded-lg bg-white p-3 text-center">
|
||||
<p className="text-xs text-slate-500">{t('result.newSize')}</p>
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
<div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{t('result.newSize')}</p>
|
||||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{formatFileSize(result.compressed_size)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{result.reduction_percent !== undefined && (
|
||||
<div className="rounded-lg bg-white p-3 text-center">
|
||||
<p className="text-xs text-slate-500">{t('result.reduction')}</p>
|
||||
<div className="rounded-lg bg-white p-3 text-center dark:bg-slate-800">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{t('result.reduction')}</p>
|
||||
<p className="text-sm font-semibold text-emerald-600">
|
||||
{result.reduction_percent}%
|
||||
</p>
|
||||
@@ -70,7 +70,7 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
|
||||
</a>
|
||||
|
||||
{/* Expiry notice */}
|
||||
<div className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500">
|
||||
<div className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
{t('result.linkExpiry')}
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@ export default function DownloadButton({ result, onStartOver }: DownloadButtonPr
|
||||
{/* Start over */}
|
||||
<button
|
||||
onClick={onStartOver}
|
||||
className="mt-4 flex w-full items-center justify-center gap-2 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700"
|
||||
className="mt-4 flex w-full items-center justify-center gap-2 text-sm font-medium text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
{t('common.startOver')}
|
||||
|
||||
@@ -69,13 +69,13 @@ export default function FileUploader({
|
||||
isDragActive ? 'text-primary-500' : 'text-slate-400'
|
||||
}`}
|
||||
/>
|
||||
<p className="mb-2 text-base font-medium text-slate-700">
|
||||
<p className="mb-2 text-base font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.dragDrop')}
|
||||
</p>
|
||||
{acceptLabel && (
|
||||
<p className="text-sm text-slate-500">{acceptLabel}</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{acceptLabel}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">
|
||||
{t('common.maxSize', { size: maxSizeMB })}
|
||||
</p>
|
||||
</div>
|
||||
@@ -83,13 +83,13 @@ export default function FileUploader({
|
||||
|
||||
{/* Selected File */}
|
||||
{file && !isUploading && (
|
||||
<div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4 ring-1 ring-primary-200">
|
||||
<File className="h-8 w-8 flex-shrink-0 text-primary-600" />
|
||||
<div className="flex items-center gap-3 rounded-xl bg-primary-50 p-4 ring-1 ring-primary-200 dark:bg-primary-900/20 dark:ring-primary-800">
|
||||
<File className="h-8 w-8 flex-shrink-0 text-primary-600 dark:text-primary-400" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-slate-900">
|
||||
<p className="truncate text-sm font-medium text-slate-900 dark:text-slate-100">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">{formatFileSize(file.size)}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{formatFileSize(file.size)}</p>
|
||||
</div>
|
||||
{onReset && (
|
||||
<button
|
||||
@@ -105,12 +105,12 @@ export default function FileUploader({
|
||||
|
||||
{/* Upload Progress */}
|
||||
{isUploading && (
|
||||
<div className="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200">
|
||||
<div className="rounded-xl bg-slate-50 p-4 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-700">
|
||||
<span className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{t('common.upload')}...
|
||||
</span>
|
||||
<span className="text-sm text-slate-500">{uploadProgress}%</span>
|
||||
<span className="text-sm text-slate-500 dark:text-slate-400">{uploadProgress}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-200">
|
||||
<div
|
||||
@@ -123,8 +123,8 @@ export default function FileUploader({
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="mt-3 rounded-xl bg-red-50 p-3 ring-1 ring-red-200">
|
||||
<p className="text-sm text-red-700">{error}</p>
|
||||
<div className="mt-3 rounded-xl bg-red-50 p-3 ring-1 ring-red-200 dark:bg-red-900/20 dark:ring-red-800">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,17 +15,17 @@ export default function ProgressBar({ state, message }: ProgressBarProps) {
|
||||
const isComplete = state === 'SUCCESS';
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-slate-50 p-5 ring-1 ring-slate-200">
|
||||
<div className="rounded-xl bg-slate-50 p-5 ring-1 ring-slate-200 dark:bg-slate-800 dark:ring-slate-700">
|
||||
<div className="flex items-center gap-3">
|
||||
{isActive && (
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary-600" />
|
||||
<Loader2 className="h-6 w-6 animate-spin text-primary-600 dark:text-primary-400" />
|
||||
)}
|
||||
{isComplete && (
|
||||
<CheckCircle2 className="h-6 w-6 text-emerald-600" />
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{message || t('common.processing')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@ export default function ProgressBar({ state, message }: ProgressBarProps) {
|
||||
|
||||
{/* Animated progress bar for active states */}
|
||||
{isActive && (
|
||||
<div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200">
|
||||
<div className="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700">
|
||||
<div className="progress-bar-animated h-full w-2/3 rounded-full bg-primary-500 transition-all" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -30,10 +30,10 @@ export default function ToolCard({
|
||||
{icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-semibold text-slate-900 group-hover:text-primary-600 transition-colors">
|
||||
<h3 className="text-base font-semibold text-slate-900 group-hover:text-primary-600 transition-colors dark:text-slate-100 dark:group-hover:text-primary-400">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500 line-clamp-2">
|
||||
<p className="mt-1 text-sm text-slate-500 line-clamp-2 dark:text-slate-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user