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:
William Valentin
2025-10-16 13:15:25 -07:00
parent a183aca4d8
commit 25e25d92bc
2 changed files with 189 additions and 0 deletions

View 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;

View File

@@ -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';