|
@@ -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>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|