feat(auth): add password reset page component
- Add ResetPasswordPage with token validation from URL params - Implement password confirmation validation - Display success/error states with proper feedback - Include accessible form labels and navigation buttons - Export ResetPasswordPage from auth components index
This commit is contained in:
188
components/auth/ResetPasswordPage.tsx
Normal file
188
components/auth/ResetPasswordPage.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { authService } from '../../services/auth/auth.service';
|
||||
import { PillIcon } from '../icons/Icons';
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
const ResetPasswordPage: React.FC = () => {
|
||||
const token = useMemo(() => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get('token');
|
||||
}, []);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'success'>(
|
||||
'idle'
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 px-4'>
|
||||
<div className='max-w-md w-full bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8 text-center'>
|
||||
<h1 className='text-2xl font-semibold text-slate-800 dark:text-slate-100 mb-4'>
|
||||
Password Reset Link Invalid
|
||||
</h1>
|
||||
<p className='text-slate-600 dark:text-slate-300 mb-6'>
|
||||
We could not find a valid reset token. Please return to the sign in
|
||||
page and request a new password reset email.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}}
|
||||
className='w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md'
|
||||
>
|
||||
Back to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
setError('Password must be at least 6 characters long.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setStatus('submitting');
|
||||
await authService.resetPassword(token, password);
|
||||
setStatus('success');
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: 'Unable to reset password. Please try again.'
|
||||
);
|
||||
setStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 px-4'>
|
||||
<div className='max-w-md w-full bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8 text-center'>
|
||||
<div className='inline-block bg-emerald-100 dark:bg-emerald-900/60 p-3 rounded-full mb-4'>
|
||||
<PillIcon className='w-8 h-8 text-emerald-500 dark:text-emerald-300' />
|
||||
</div>
|
||||
<h1 className='text-2xl font-semibold text-slate-800 dark:text-slate-100 mb-2'>
|
||||
Password Updated
|
||||
</h1>
|
||||
<p className='text-slate-600 dark:text-slate-300 mb-6'>
|
||||
Your password has been reset successfully. You can now sign in with
|
||||
your new credentials.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}}
|
||||
className='w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md'
|
||||
>
|
||||
Go to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 px-4'>
|
||||
<div className='w-full max-w-md'>
|
||||
<div className='text-center mb-8'>
|
||||
<div className='inline-block bg-indigo-600 p-3 rounded-xl mb-4'>
|
||||
<PillIcon className='w-8 h-8 text-white' />
|
||||
</div>
|
||||
<h1 className='text-3xl font-bold text-slate-800 dark:text-slate-100'>
|
||||
Reset Password
|
||||
</h1>
|
||||
<p className='text-slate-500 dark:text-slate-400 mt-1'>
|
||||
Choose a new password for your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='bg-white dark:bg-slate-800 rounded-lg shadow-lg p-8'>
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='password'
|
||||
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
id='password'
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={event => setPassword(event.target.value)}
|
||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
|
||||
placeholder='Enter a new password'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor='confirmPassword'
|
||||
className='block text-sm font-medium text-slate-700 dark:text-slate-300'
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
id='confirmPassword'
|
||||
type='password'
|
||||
value={confirmPassword}
|
||||
onChange={event => setConfirmPassword(event.target.value)}
|
||||
className='mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
|
||||
placeholder='Re-enter your password'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className='text-sm text-red-600 bg-red-50 dark:bg-red-900/40 border border-red-200 dark:border-red-800 rounded-md px-3 py-2'>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type='submit'
|
||||
disabled={status === 'submitting'}
|
||||
className='w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200'
|
||||
>
|
||||
{status === 'submitting'
|
||||
? 'Updating Password...'
|
||||
: 'Update Password'}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}}
|
||||
className='mt-6 w-full text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'
|
||||
>
|
||||
Back to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordPage;
|
||||
@@ -2,3 +2,4 @@
|
||||
export { default as AuthPage } from './AuthPage';
|
||||
export { default as AvatarDropdown } from './AvatarDropdown';
|
||||
export { default as ChangePasswordModal } from './ChangePasswordModal';
|
||||
export { default as ResetPasswordPage } from './ResetPasswordPage';
|
||||
|
||||
Reference in New Issue
Block a user