إنجاز: تفعيل خاصية استعادة كلمة المرور وإعادة تعيينها

- إضافة نقاط نهاية لخاصيتي استعادة كلمة المرور وإعادة تعيينها في الواجهة الخلفية.

- إنشاء اختبارات لخاصية إعادة تعيين كلمة المرور لضمان كفاءتها وأمانها.

- تطوير صفحات واجهة المستخدم لخاصيتي استعادة كلمة المرور وإعادة تعيينها مع معالجة النماذج.

- دمج حدود تحميل ديناميكية لأنواع ملفات مختلفة بناءً على خطط المستخدمين.

- تقديم أداة جديدة لتغيير حجم الصور مع إمكانية تعديل الأبعاد وإعدادات الجودة.

- تحديث نظام التوجيه والتنقل ليشمل أدوات جديدة وميزات مصادقة.

- تحسين تجربة المستخدم من خلال معالجة الأخطاء ورسائل التغذية الراجعة المناسبة.

- إضافة دعم التدويل للميزات الجديدة باللغات الإنجليزية والعربية والفرنسية.
This commit is contained in:
Your Name
2026-03-07 14:23:50 +02:00
parent 0ad2ba0f02
commit 71f7d0382d
27 changed files with 1460 additions and 7 deletions

View File

@@ -633,6 +633,14 @@ export default function AccountPage() {
<button type="submit" className="btn-primary w-full" disabled={authLoading}>
{mode === 'login' ? t('account.submitLogin') : t('account.submitRegister')}
</button>
{mode === 'login' && (
<p className="text-center text-sm">
<a href="/forgot-password" className="text-primary-600 hover:underline dark:text-primary-400">
{t('auth.forgotPassword.link')}
</a>
</p>
)}
</form>
</div>
</section>

View File

@@ -0,0 +1,95 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async';
import { Mail } from 'lucide-react';
const API_BASE = import.meta.env.VITE_API_URL || '';
export default function ForgotPasswordPage() {
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/auth/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});
if (!res.ok) throw new Error('Request failed');
setSubmitted(true);
} catch {
setError(t('auth.forgotPassword.error'));
} finally {
setLoading(false);
}
};
return (
<>
<Helmet>
<title>{t('auth.forgotPassword.title')} {t('common.appName')}</title>
</Helmet>
<div className="mx-auto max-w-md">
<div className="mb-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-100 dark:bg-primary-900/30">
<Mail className="h-8 w-8 text-primary-600 dark:text-primary-400" />
</div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">
{t('auth.forgotPassword.title')}
</h1>
<p className="mt-2 text-slate-500 dark:text-slate-400">
{t('auth.forgotPassword.subtitle')}
</p>
</div>
{submitted ? (
<div className="rounded-2xl bg-green-50 p-6 text-center ring-1 ring-green-200 dark:bg-green-900/20 dark:ring-green-800">
<p className="text-sm text-green-700 dark:text-green-400">
{t('auth.forgotPassword.sent')}
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.email')}
</label>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('account.emailPlaceholder')}
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{error && (
<div className="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>
)}
<button
type="submit"
disabled={loading}
className="btn-primary w-full disabled:opacity-50"
>
{loading ? t('common.loading') : t('auth.forgotPassword.submit')}
</button>
</form>
)}
</div>
</>
);
}

View File

@@ -19,6 +19,7 @@ import {
ListOrdered,
PenLine,
GitBranch,
Scaling,
} from 'lucide-react';
import ToolCard from '@/components/shared/ToolCard';
import HeroUploadZone from '@/components/shared/HeroUploadZone';
@@ -50,6 +51,7 @@ const pdfTools: ToolInfo[] = [
const otherTools: ToolInfo[] = [
{ key: 'imageConvert', path: '/tools/image-converter', icon: <ImageIcon className="h-6 w-6 text-purple-600" />, bgColor: 'bg-purple-50' },
{ key: 'imageResize', path: '/tools/image-resize', icon: <Scaling className="h-6 w-6 text-teal-600" />, bgColor: 'bg-teal-50' },
{ key: 'videoToGif', path: '/tools/video-to-gif', icon: <Film className="h-6 w-6 text-emerald-600" />, bgColor: 'bg-emerald-50' },
{ key: 'wordCounter', path: '/tools/word-counter', icon: <Hash className="h-6 w-6 text-blue-600" />, bgColor: 'bg-blue-50' },
{ key: 'textCleaner', path: '/tools/text-cleaner', icon: <Eraser className="h-6 w-6 text-indigo-600" />, bgColor: 'bg-indigo-50' },

View File

@@ -0,0 +1,130 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import { KeyRound } from 'lucide-react';
const API_BASE = import.meta.env.VITE_API_URL || '';
export default function ResetPasswordPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const token = searchParams.get('token') || '';
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (password.length < 8) {
setError(t('auth.resetPassword.tooShort'));
return;
}
if (password !== confirm) {
setError(t('account.passwordMismatch'));
return;
}
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/auth/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Reset failed');
setSuccess(true);
setTimeout(() => navigate('/account'), 3000);
} catch (err) {
setError(err instanceof Error ? err.message : t('auth.resetPassword.error'));
} finally {
setLoading(false);
}
};
if (!token) {
return (
<div className="mx-auto max-w-md text-center">
<p className="text-slate-500 dark:text-slate-400">{t('auth.resetPassword.noToken')}</p>
</div>
);
}
return (
<>
<Helmet>
<title>{t('auth.resetPassword.title')} {t('common.appName')}</title>
</Helmet>
<div className="mx-auto max-w-md">
<div className="mb-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-primary-100 dark:bg-primary-900/30">
<KeyRound className="h-8 w-8 text-primary-600 dark:text-primary-400" />
</div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">
{t('auth.resetPassword.title')}
</h1>
</div>
{success ? (
<div className="rounded-2xl bg-green-50 p-6 text-center ring-1 ring-green-200 dark:bg-green-900/20 dark:ring-green-800">
<p className="text-sm text-green-700 dark:text-green-400">
{t('auth.resetPassword.success')}
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('auth.resetPassword.newPassword')}
</label>
<input
type="password"
required
minLength={8}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('account.confirmPassword')}
</label>
<input
type="password"
required
minLength={8}
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{error && (
<div className="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>
)}
<button
type="submit"
disabled={loading}
className="btn-primary w-full disabled:opacity-50"
>
{loading ? t('common.loading') : t('auth.resetPassword.submit')}
</button>
</form>
)}
</div>
</>
);
}