297 lines
14 KiB
JavaScript
297 lines
14 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, Link, router } from '@inertiajs/react';
|
|
import {
|
|
KeyRound,
|
|
Eye,
|
|
EyeOff,
|
|
Loader2,
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
ArrowLeft
|
|
} from 'lucide-react';
|
|
import axios from 'axios';
|
|
|
|
export default function ForgotPassword() {
|
|
const [step, setStep] = useState(1); // 1 = Request Code, 2 = Verify & Reset, 3 = Success
|
|
const [email, setEmail] = useState('');
|
|
const [otp, setOtp] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [successMessage, setSuccessMessage] = useState('');
|
|
|
|
const handleSendCode = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!email.trim()) {
|
|
setError('Please enter your email address.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await axios.post('/employer/forgot-password', { email });
|
|
if (response.data.success) {
|
|
setStep(2);
|
|
setSuccessMessage('A 6-digit verification code has been sent to your email.');
|
|
} else {
|
|
setError(response.data.message || 'Failed to send reset code. Please try again.');
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.message || 'Failed to send reset code. Please verify your email address.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleResetPassword = async (e) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!otp.trim() || otp.length !== 6) {
|
|
setError('Please enter the 6-digit verification code.');
|
|
return;
|
|
}
|
|
|
|
if (!password) {
|
|
setError('Please enter a new password.');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
setError('Password must be at least 8 characters long.');
|
|
return;
|
|
}
|
|
|
|
if (password !== passwordConfirmation) {
|
|
setError('Passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
// Validate password strength: 1 uppercase, 1 lowercase, 1 number, 1 special character
|
|
const hasUppercase = /[A-Z]/.test(password);
|
|
const hasLowercase = /[a-z]/.test(password);
|
|
const hasNumber = /[0-9]/.test(password);
|
|
const hasSpecial = /[^A-Za-z0-9]/.test(password);
|
|
|
|
if (!hasUppercase || !hasLowercase || !hasNumber || !hasSpecial) {
|
|
setError('Password must contain at least 1 uppercase, 1 lowercase, 1 number, and 1 special character.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await axios.post('/employer/reset-password', {
|
|
email,
|
|
otp,
|
|
password,
|
|
password_confirmation: passwordConfirmation
|
|
});
|
|
|
|
if (response.data.success) {
|
|
setStep(3);
|
|
} else {
|
|
setError(response.data.message || 'Failed to reset password. Please check your verification code.');
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.message || 'Failed to reset password. Please check your inputs and try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col justify-center items-center px-4 bg-slate-50 font-sans">
|
|
<Head title="Reset Password - Sponsor Portal" />
|
|
|
|
<div className="w-full max-w-md bg-white rounded-2xl shadow-sm border border-slate-200 p-8 space-y-6">
|
|
|
|
{/* Header Icon & Title */}
|
|
{step !== 3 && (
|
|
<div className="text-center space-y-4">
|
|
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto border border-blue-200">
|
|
<KeyRound className="w-8 h-8 text-[#185FA5]" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">
|
|
{step === 1 ? 'Forgot Password' : 'Reset Password'}
|
|
</h1>
|
|
<p className="text-sm text-gray-600">
|
|
{step === 1
|
|
? "Enter your registration email address and we'll send you a verification code."
|
|
: `Enter the 6-digit code sent to ${email} and your new password.`
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Status/Error Messages */}
|
|
{error && (
|
|
<div className="flex items-start space-x-3 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-xs font-medium">
|
|
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{successMessage && step === 2 && (
|
|
<div className="flex items-start space-x-3 p-4 bg-emerald-50 border border-emerald-200 rounded-xl text-emerald-800 text-xs font-medium animate-pulse">
|
|
<CheckCircle2 className="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" />
|
|
<span>{successMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 1: Request Code Form */}
|
|
{step === 1 && (
|
|
<form onSubmit={handleSendCode} className="space-y-5">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Email Address</label>
|
|
<input
|
|
type="email"
|
|
placeholder="name@company.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
autoFocus
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center disabled:opacity-70 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : 'Send Reset Code'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
{/* Step 2: Verification and Reset Form */}
|
|
{step === 2 && (
|
|
<form onSubmit={handleResetPassword} className="space-y-5">
|
|
{/* OTP Input */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">6-Digit Code</label>
|
|
<input
|
|
type="text"
|
|
placeholder="123456"
|
|
maxLength={6}
|
|
value={otp}
|
|
onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))}
|
|
className="w-full px-3.5 py-2.5 rounded-xl border border-slate-300 text-sm font-semibold tracking-wider text-center focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
autoFocus
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Password Input */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">New Password</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 focus:outline-none"
|
|
>
|
|
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
<p className="mt-1 text-[10px] text-gray-500 leading-normal">
|
|
Must be at least 8 characters with 1 uppercase, 1 lowercase, 1 number, and 1 special symbol.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Confirm Password Input */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">Confirm Password</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
placeholder="••••••••"
|
|
value={passwordConfirmation}
|
|
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
|
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-slate-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 focus:border-[#185FA5]"
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 focus:outline-none"
|
|
>
|
|
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center disabled:opacity-70 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? <Loader2 className="w-5 h-5 animate-spin mr-2" /> : 'Reset Password'}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setStep(1);
|
|
setError('');
|
|
setSuccessMessage('');
|
|
}}
|
|
className="w-full border border-slate-300 hover:bg-slate-50 text-gray-700 rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm flex items-center justify-center"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" /> Back
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
{/* Step 3: Success Screen */}
|
|
{step === 3 && (
|
|
<div className="text-center space-y-6 py-4">
|
|
<div className="w-16 h-16 bg-emerald-50 rounded-full flex items-center justify-center mx-auto border border-emerald-200">
|
|
<CheckCircle2 className="w-8 h-8 text-emerald-600" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">Password Reset Success</h2>
|
|
<p className="text-sm text-gray-600">
|
|
Your password has been successfully updated. You can now use your new password to sign in.
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => router.visit('/employer/login')}
|
|
className="w-full bg-[#185FA5] hover:bg-[#0C447C] active:bg-[#08305c] text-white rounded-xl h-11 font-semibold text-sm transition-colors shadow-sm"
|
|
>
|
|
Sign In
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Footer Back Link */}
|
|
{step !== 3 && (
|
|
<div className="pt-2 text-center">
|
|
<Link href="/employer/login" className="text-xs text-[#185FA5] font-bold hover:underline">
|
|
Return to Sign in
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|