Added Forgot Password UI and Functionality

This commit is contained in:
Bhagwan Singh 2026-05-29 18:48:54 +05:30
parent 21313da24a
commit 13b506960b
6 changed files with 380 additions and 38 deletions

View File

@ -27,6 +27,7 @@ import CompletionScreen from "./components/client/CompletionScreen";
import SessionExpired from "./components/client/SessionExpired"; import SessionExpired from "./components/client/SessionExpired";
import ExpiredPage from "./components/ExpiredPage"; import ExpiredPage from "./components/ExpiredPage";
import ForgotPassword from "./components/forgotPassword/ForgotPassword";
const App = () => { const App = () => {
return ( return (
@ -103,12 +104,16 @@ const App = () => {
} /> } />
<Route path="/completion" element={ <Route path="/completion" element={
<CompletionScreen /> <CompletionScreen />
} /> } />
{/* Links work wothout Login */} {/* Links work wothout Login */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/forgot-password/:token" element={<ForgotPassword />} />
<Route path="/sign/:recipientId/:signType" element={<SignDocument />} /> <Route path="/sign/:recipientId/:signType" element={<SignDocument />} />
<Route path="/sign/:recipientId/" element={<SignDocument />} /> <Route path="/sign/:recipientId/" element={<SignDocument />} />
<Route path="/view" element={<DocView />} /> <Route path="/view" element={<DocView />} />

View File

@ -0,0 +1,319 @@
import { useState } from "react";
import axios from "axios";
import { Link, useNavigate, useParams, } from "react-router-dom";
import { FiEyeOff } from "react-icons/fi";
import { BsEye } from "react-icons/bs";
interface ForgotPasswordFormProps {
onSwitchToLogin: () => void
}
const ForgotPasswordForm = () => {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [password, setPassword] = useState<string>();
const [confirmPassword, setConfirmPassword] = useState<string>();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [countdown, setCountdown] = useState(3);
const { token } = useParams<{ token: string }>();
const validateEmail = (email: string) => {
if (!email.trim()) {
return "Email is required";
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return "Please enter a valid email";
}
return "";
};
const validatePassword = () => {
if (!password?.trim()) {
return "Password is required";
}
if (password.length < 8) {
return "Password must be at least 8 characters";
}
if (!/[A-Z]/.test(password)) {
return "Password must contain at least one uppercase letter";
}
if (!/[a-z]/.test(password)) {
return "Password must contain at least one lowercase letter";
}
if (!/\d/.test(password)) {
return "Password must contain at least one number";
}
if (password !== confirmPassword) {
return "Passwords do not match";
}
return "";
};
const handleSubmit = async () => {
const emailError = validateEmail(email);
if (emailError) {
setError(emailError);
return;
}
setLoading(true);
setError("");
setMessage("");
try {
await axios.post(
`${import.meta.env.VITE_BASE_URI}/api/v1/auth/forgot-password`,
{ email }
);
setMessage(
"If an account exists with this email, password reset instructions have been sent."
);
} catch (err) {
setError("Unable to process your request. Please try again.");
} finally {
setLoading(false);
}
};
const handleUpdatePassword = async () => {
const validationError = validatePassword();
if (validationError) {
setError(validationError);
return;
}
setLoading(true);
setError("");
setMessage("");
try {
await axios.post(
`${import.meta.env.VITE_BASE_URI}/api/v1/auth/reset-password`,
{
token,
password,
}
);
setMessage(
"🎉 Password updated successfully! Redirecting to login in 3 seconds..."
);
let seconds = 3;
const timer = setInterval(() => {
seconds -= 1;
setCountdown(seconds);
if (seconds <= 0) {
clearInterval(timer);
navigate("/login");
}
}, 1000);
} catch (err: any) {
setError(
err?.response?.data?.message ||
"Unable to update password. Please try again."
);
} finally {
setLoading(false);
}
};
return (
<div className="bg-white p-8 rounded-lg w-full max-w-md">
<div className="text-center mb-6">
<span className="text-orange-500 text-sm">
PASSWORD RECOVERY 🔐
</span>
<h2 className="text-2xl font-bold text-gray-800 mt-2">
{!token && <>Forgot Password</>}
{token && <>Change Password</>}
</h2>
<p className="text-sm text-gray-500 mt-2">
{!token && <>Enter your email address and we'll send you instructions to reset your password.</>}
{token && <>Create a new password for your account. Enter your new password below and confirm it to continue.</>}
</p>
</div>
{!token && <>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError("");
}}
placeholder="Enter your email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
/>
{error && (
<p className="text-red-500 text-sm mt-2">{error}</p>
)}
{message && (
<p className="text-green-600 text-sm mt-2">{message}</p>
)}
</div>
<button
onClick={handleSubmit}
disabled={loading}
className={`w-full mt-5 py-2 rounded-lg text-white font-medium ${loading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600"
}`}
>
{loading ? "Sending..." : "Send Reset Link"}
</button>
<div className="text-center mt-5">
<Link
to="/login"
className="text-sm text-blue-600 hover:underline"
>
Back to Login
</Link>
</div>
</>}
{token && (
<>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
New Password
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter new password"
className="w-full px-4 py-2 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
/>
<button
type="button"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
{showPassword ? <FiEyeOff size={18} /> : <BsEye size={18} />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirm Password
</label>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
className="w-full px-4 py-2 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none"
/>
<button
type="button"
onClick={() => setShowConfirmPassword((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
{showConfirmPassword ? <FiEyeOff size={18} /> : <BsEye size={18} />}
</button>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<p className="text-xs font-semibold text-gray-700 mb-2">
Password Requirements
</p>
<ul className="text-xs text-gray-500 space-y-1">
<li>• Minimum 8 characters</li>
<li>• One uppercase letter</li>
<li>• One lowercase letter</li>
<li>• One number</li>
</ul>
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-red-600 text-sm">
{error}
</p>
</div>
)}
{message && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-green-700 text-sm font-medium">
{message}
</p>
<p className="text-green-600 text-xs mt-1">
Redirecting in {countdown}s...
</p>
</div>
)}
<button
onClick={handleUpdatePassword}
disabled={loading}
className={`w-full py-3 rounded-lg text-white font-medium transition-all duration-200 ${loading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 active:scale-[0.98]"
}`}
>
{loading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Updating...
</div>
) : (
"Update Password"
)}
</button>
</div>
</>
)}
</div>
);
};
export default ForgotPasswordForm;

View File

@ -16,9 +16,11 @@ interface FormDataState {
password: string; password: string;
} }
interface LoginFormProps {
onSwitchToRegister: () => void;
}
const LoginForm = ({ onSwitchToRegister }: LoginFormProps) => {
const LoginForm = ({ onSwitchToRegister }) => {
const [formData, setForm] = useState({ email: "", password: "" }); const [formData, setForm] = useState({ email: "", password: "" });
const [cacheKey, setCacheKey] = useState(""); const [cacheKey, setCacheKey] = useState("");
const [nextClick, setNextClick] = useState(false); const [nextClick, setNextClick] = useState(false);
@ -123,7 +125,7 @@ const LoginForm = ({ onSwitchToRegister }) => {
} catch (err) { } catch (err) {
if (err.response?.status == 400 || err.response?.status == 401) { if (err.response?.status == 400 || err.response?.status == 401) {
formErrorRef.current = { message: "The email or password you entered is invalid.", error: true } formErrorRef.current = { message: "The email or password you entered is invalid.", error: true }
} else if(err.response?.status == 404) { } else if (err.response?.status == 404) {
formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true } formErrorRef.current = { message: "Email not found. Please sign up to continue.", error: true }
} else { } else {
formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true } formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true }
@ -206,15 +208,15 @@ const LoginForm = ({ onSwitchToRegister }) => {
{errors.password && ( {errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p> <p className="text-red-500 text-sm mt-1">{errors.password}</p>
)} )}
{/* <div className="text-right mt-2"> <div className="text-right mt-2">
<a href="#" className="text-blue-500 text-sm hover:underline"> <Link to="/forgot-password" className="text-blue-500 text-sm hover:underline">
Forgot password? Forgot password?
</a> </Link>
</div> */} </div>
</div> </div>
{/* )} */} {/* )} */}
<p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p> <p className={`${formErrorRef.current.error ? 'text-red-500 text-center' : 'text-green-500 text-center'} `}>{formErrorRef.current.message}</p>
<button <button
@ -238,6 +240,7 @@ const LoginForm = ({ onSwitchToRegister }) => {
Register Register
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); );

View File

@ -16,7 +16,11 @@ interface FormDataState {
lastName: string; lastName: string;
} }
const RegisterForm = ({ onSwitchToLogin }) => { interface RegisterFormProps {
onSwitchToLogin: () => void
}
const RegisterForm = ({ onSwitchToLogin }: RegisterFormProps) => {
const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false }); const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false });
const [formData, setForm] = useState<FormDataState>({ const [formData, setForm] = useState<FormDataState>({
email: "", email: "",
@ -76,7 +80,7 @@ const RegisterForm = ({ onSwitchToLogin }) => {
if (!password) return "Password is required"; if (!password) return "Password is required";
if (password.length < 8) return "Password must be at least 8 characters long"; if (password.length < 8) return "Password must be at least 8 characters long";
if (password.length > 60) return "Password is too long"; if (password.length > 60) return "Password is too long";
if (/\s/.test(password)) return "Password cannot contain spaces"; if (/\s/.test(password)) return "Password cannot contain spaces";
const hasUpperCase = /[A-Z]/.test(password); const hasUpperCase = /[A-Z]/.test(password);

View File

@ -9,37 +9,43 @@ import { fetchUserDetails } from "../../store/slices/userSlice";
import LoginRegisterImg from "../../assets/illustration_1_.png"; import LoginRegisterImg from "../../assets/illustration_1_.png";
import SMELogo from "../../assets/SMELogo.png"; import SMELogo from "../../assets/SMELogo.png";
import api from "../../config/api"; import api from "../../config/api";
import ForgotPasswordForm from "./ForgotPasswordForm";
enum PAGE_TYPE_ENUM {
LOGIN,
REGISTER,
FORGOT_PASSWORD,
EMPTY
}
function UserAuthUI() { function UserAuthUI() {
const [isLogin, setIsLogin] = useState(true); const [pageType, setPageType] = useState<PAGE_TYPE_ENUM>(PAGE_TYPE_ENUM.EMPTY);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); // Hook to get the current URL path const location = useLocation(); // Hook to get the current URL path
// Sync isLogin state with the current route // Sync isLogin state with the current route
useEffect(() => { useEffect(() => {
const path = location.pathname; const path = location.pathname;
if (path == "/login") { if (path.includes("/login")) {
setIsLogin(true); setPageType(PAGE_TYPE_ENUM.LOGIN);
} else if (path == "/register") { } else if (path.includes("/register")) {
setIsLogin(false); setPageType(PAGE_TYPE_ENUM.REGISTER);
} else if (path.includes("/forgot-password")) {
setPageType(PAGE_TYPE_ENUM.FORGOT_PASSWORD);
} }
}, [location.pathname]); }, [location.pathname]);
const handleLogin = (loginData) => { // const handleLogin = (loginData) => {
console.log("Login data:", loginData); // };
// Your login API call logic here
};
const handleRegister = (registerData) => { // const handleRegister = (registerData) => {
console.log("Register data:", registerData); // };
// Your register API call logic here
};
useEffect(() => { useEffect(() => {
if (pageType !== PAGE_TYPE_ENUM.LOGIN) return
(async () => { (async () => {
await api.get(`/api/v1/user/details`); await api.get(`/api/v1/user/details`);
})() })()
}, []); }, [pageType]);
return ( return (
<div className="max-h-screen bg-white flex"> <div className="max-h-screen bg-white flex">
@ -70,17 +76,9 @@ function UserAuthUI() {
{/* Right Side - Form */} {/* Right Side - Form */}
<div className="flex-1 flex items-center justify-center p-8 lg:p-12"> <div className="flex-1 flex items-center justify-center p-8 lg:p-12">
{isLogin ? ( {pageType === PAGE_TYPE_ENUM.LOGIN && <LoginForm onSwitchToRegister={() => navigate("/register")} />}
<LoginForm {pageType === PAGE_TYPE_ENUM.REGISTER && <RegisterForm onSwitchToLogin={() => navigate("/login")} />}
onLogin={handleLogin} {pageType === PAGE_TYPE_ENUM.FORGOT_PASSWORD && <ForgotPasswordForm />}
onSwitchToRegister={() => navigate("/register")} // Navigate to /register
/>
) : (
<RegisterForm
onRegister={handleRegister}
onSwitchToLogin={() => navigate("/login")} // Navigate to /login
/>
)}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,13 @@
import UserAuthUI from "../auth/UsertAuthUI"
function ForgotPassword() {
return (
<>
<div className="">
<UserAuthUI pg={"forgot-password"} />
</div>
</>
)
}
export default ForgotPassword