Kaynağa Gözat

feat: Add forgot password and reset password pages

Fszontagh 13 saat önce
ebeveyn
işleme
6f5ff1a2f7

+ 4 - 0
src/App.tsx

@@ -5,6 +5,8 @@ import Layout from './components/Layout';
 import HomePage from './pages/HomePage';
 import LoginPage from './pages/LoginPage';
 import RegisterPage from './pages/RegisterPage';
+import ForgotPasswordPage from './pages/ForgotPasswordPage';
+import ResetPasswordPage from './pages/ResetPasswordPage';
 import VerifyEmailPage from './pages/VerifyEmailPage';
 import DashboardPage from './pages/DashboardPage';
 import ImageDetailPage from './pages/ImageDetailPage';
@@ -68,6 +70,8 @@ function App() {
         <Route index element={<HomePage />} />
         <Route path="login" element={<LoginPage />} />
         <Route path="register" element={<RegisterPage />} />
+        <Route path="forgot-password" element={<ForgotPasswordPage />} />
+        <Route path="reset-password" element={<ResetPasswordPage />} />
         <Route path="verify-email" element={<VerifyEmailPage />} />
 
         {/* Protected routes */}

+ 131 - 0
src/pages/ForgotPasswordPage.tsx

@@ -0,0 +1,131 @@
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useAuth } from '@picobaas/client/react';
+import LoadingSpinner from '../components/LoadingSpinner';
+
+export default function ForgotPasswordPage() {
+  const { forgotPassword } = useAuth();
+  const [email, setEmail] = useState('');
+  const [error, setError] = useState<string | null>(null);
+  const [isLoading, setIsLoading] = useState(false);
+  const [success, setSuccess] = useState(false);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError(null);
+    setIsLoading(true);
+
+    try {
+      await forgotPassword(email);
+      setSuccess(true);
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to send reset email');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  if (success) {
+    return (
+      <div className="min-h-[calc(100vh-12rem)] flex items-center justify-center py-12 px-4">
+        <div className="max-w-md w-full text-center">
+          <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-900/30 mb-4">
+            <svg className="h-8 w-8 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
+            </svg>
+          </div>
+          <h1
+            className="text-2xl font-bold mb-2"
+            style={{ color: 'var(--text-primary)' }}
+          >
+            Check your email
+          </h1>
+          <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
+            If an account exists for <strong>{email}</strong>, we've sent a password reset link.
+            The link expires in 1 hour.
+          </p>
+          <Link to="/login" className="btn btn-primary">
+            Back to Login
+          </Link>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-[calc(100vh-12rem)] flex items-center justify-center py-12 px-4">
+      <div className="max-w-md w-full">
+        <div className="text-center mb-8">
+          <h1
+            className="text-3xl font-bold"
+            style={{ color: 'var(--text-primary)' }}
+          >
+            Forgot password?
+          </h1>
+          <p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
+            Enter your email and we'll send you a reset link
+          </p>
+        </div>
+
+        <div className="card p-6">
+          <form onSubmit={handleSubmit} className="space-y-4">
+            {error && (
+              <div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
+                {error}
+              </div>
+            )}
+
+            <div>
+              <label
+                htmlFor="email"
+                className="block text-sm font-medium mb-1"
+                style={{ color: 'var(--text-secondary)' }}
+              >
+                Email
+              </label>
+              <input
+                id="email"
+                type="email"
+                value={email}
+                onChange={(e) => setEmail(e.target.value)}
+                className="input"
+                placeholder="you@example.com"
+                required
+                autoComplete="email"
+              />
+            </div>
+
+            <button
+              type="submit"
+              disabled={isLoading}
+              className="btn btn-primary w-full flex items-center justify-center gap-2"
+            >
+              {isLoading ? (
+                <>
+                  <LoadingSpinner size="sm" />
+                  Sending...
+                </>
+              ) : (
+                'Send Reset Link'
+              )}
+            </button>
+          </form>
+
+          <div
+            className="mt-6 text-center text-sm"
+            style={{ color: 'var(--text-secondary)' }}
+          >
+            Remember your password?{' '}
+            <Link
+              to="/login"
+              className="font-medium"
+              style={{ color: 'var(--accent-600)' }}
+            >
+              Sign in
+            </Link>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 10 - 0
src/pages/LoginPage.tsx

@@ -109,6 +109,16 @@ export default function LoginPage() {
               />
             </div>
 
+            <div className="flex justify-end">
+              <Link
+                to="/forgot-password"
+                className="text-sm font-medium"
+                style={{ color: 'var(--accent-600)' }}
+              >
+                Forgot password?
+              </Link>
+            </div>
+
             <button
               type="submit"
               disabled={isLoading}

+ 169 - 0
src/pages/ResetPasswordPage.tsx

@@ -0,0 +1,169 @@
+import { useState } from 'react';
+import { Link, useNavigate, useSearchParams } from 'react-router-dom';
+import { useAuth } from '@picobaas/client/react';
+import LoadingSpinner from '../components/LoadingSpinner';
+
+export default function ResetPasswordPage() {
+  const navigate = useNavigate();
+  const [searchParams] = useSearchParams();
+  const token = searchParams.get('token');
+  const { resetPassword } = useAuth();
+  const [password, setPassword] = useState('');
+  const [confirmPassword, setConfirmPassword] = useState('');
+  const [error, setError] = useState<string | null>(null);
+  const [isLoading, setIsLoading] = useState(false);
+
+  if (!token) {
+    return (
+      <div className="min-h-[calc(100vh-12rem)] flex items-center justify-center py-12 px-4">
+        <div className="max-w-md w-full text-center">
+          <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/30 mb-4">
+            <svg className="h-8 w-8 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+            </svg>
+          </div>
+          <h1
+            className="text-2xl font-bold mb-2"
+            style={{ color: 'var(--text-primary)' }}
+          >
+            Invalid reset link
+          </h1>
+          <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
+            This password reset link is invalid or has expired.
+          </p>
+          <Link to="/forgot-password" className="btn btn-primary">
+            Request a new link
+          </Link>
+        </div>
+      </div>
+    );
+  }
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError(null);
+
+    if (password.length < 8) {
+      setError('Password must be at least 8 characters');
+      return;
+    }
+    if (password !== confirmPassword) {
+      setError('Passwords do not match');
+      return;
+    }
+
+    setIsLoading(true);
+
+    try {
+      await resetPassword(token, password);
+      navigate('/login', {
+        replace: true,
+        state: { message: 'Password reset successfully. You can now sign in with your new password.' },
+      });
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'Failed to reset password');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <div className="min-h-[calc(100vh-12rem)] flex items-center justify-center py-12 px-4">
+      <div className="max-w-md w-full">
+        <div className="text-center mb-8">
+          <h1
+            className="text-3xl font-bold"
+            style={{ color: 'var(--text-primary)' }}
+          >
+            Reset your password
+          </h1>
+          <p className="mt-2" style={{ color: 'var(--text-secondary)' }}>
+            Enter your new password below
+          </p>
+        </div>
+
+        <div className="card p-6">
+          <form onSubmit={handleSubmit} className="space-y-4">
+            {error && (
+              <div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm">
+                {error}
+              </div>
+            )}
+
+            <div>
+              <label
+                htmlFor="password"
+                className="block text-sm font-medium mb-1"
+                style={{ color: 'var(--text-secondary)' }}
+              >
+                New Password
+              </label>
+              <input
+                id="password"
+                type="password"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                className="input"
+                placeholder="Enter new password"
+                required
+                autoComplete="new-password"
+              />
+              <p className="mt-1 text-xs" style={{ color: 'var(--text-muted)' }}>
+                At least 8 characters
+              </p>
+            </div>
+
+            <div>
+              <label
+                htmlFor="confirmPassword"
+                className="block text-sm font-medium mb-1"
+                style={{ color: 'var(--text-secondary)' }}
+              >
+                Confirm Password
+              </label>
+              <input
+                id="confirmPassword"
+                type="password"
+                value={confirmPassword}
+                onChange={(e) => setConfirmPassword(e.target.value)}
+                className="input"
+                placeholder="Confirm new password"
+                required
+                autoComplete="new-password"
+              />
+            </div>
+
+            <button
+              type="submit"
+              disabled={isLoading}
+              className="btn btn-primary w-full flex items-center justify-center gap-2"
+            >
+              {isLoading ? (
+                <>
+                  <LoadingSpinner size="sm" />
+                  Resetting...
+                </>
+              ) : (
+                'Reset Password'
+              )}
+            </button>
+          </form>
+
+          <div
+            className="mt-6 text-center text-sm"
+            style={{ color: 'var(--text-secondary)' }}
+          >
+            Remember your password?{' '}
+            <Link
+              to="/login"
+              className="font-medium"
+              style={{ color: 'var(--accent-600)' }}
+            >
+              Sign in
+            </Link>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}