diff --git a/src/App.tsx b/src/App.tsx index ad47021..2191f06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ import CompletionScreen from "./components/client/CompletionScreen"; import SessionExpired from "./components/client/SessionExpired"; import ExpiredPage from "./components/ExpiredPage"; +import ForgotPassword from "./components/forgotPassword/ForgotPassword"; const App = () => { return ( @@ -103,12 +104,16 @@ const App = () => { } /> + } /> {/* Links work wothout Login */} } /> } /> + } /> + } /> + + } /> } /> } /> @@ -117,7 +122,7 @@ const App = () => { } /> } /> } /> - + } /> {/* Links work wothout Login */} diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 0000000..cdb4d4d --- /dev/null +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -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(); + const [confirmPassword, setConfirmPassword] = useState(); + + 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 ( +
+
+ + PASSWORD RECOVERY 🔐 + + +

+ {!token && <>Forgot Password} + {token && <>Change Password} +

+ +

+ {!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.} +

+
+ + {!token && <> +
+ + + { + 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 && ( +

{error}

+ )} + + {message && ( +

{message}

+ )} +
+ + + +
+ + Back to Login + +
+ } + + {token && ( + <> +
+
+ + +
+ 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" + /> + + +
+
+ +
+ + +
+ 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" + /> + + +
+
+ +
+

+ Password Requirements +

+ +
    +
  • • Minimum 8 characters
  • +
  • • One uppercase letter
  • +
  • • One lowercase letter
  • +
  • • One number
  • +
+
+ + {error && ( +
+

+ {error} +

+
+ )} + + {message && ( +
+

+ {message} +

+ +

+ Redirecting in {countdown}s... +

+
+ )} + + +
+ + )} + + +
+ ); +}; + +export default ForgotPasswordForm; \ No newline at end of file diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index b6493d3..0f77948 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -16,9 +16,11 @@ interface FormDataState { password: string; } +interface LoginFormProps { + onSwitchToRegister: () => void; +} - -const LoginForm = ({ onSwitchToRegister }) => { +const LoginForm = ({ onSwitchToRegister }: LoginFormProps) => { const [formData, setForm] = useState({ email: "", password: "" }); const [cacheKey, setCacheKey] = useState(""); const [nextClick, setNextClick] = useState(false); @@ -123,7 +125,7 @@ const LoginForm = ({ onSwitchToRegister }) => { } catch (err) { if (err.response?.status == 400 || err.response?.status == 401) { 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 } } else { formErrorRef.current = { message: "Something went wrong. Please refresh and try again.", error: true } @@ -206,15 +208,15 @@ const LoginForm = ({ onSwitchToRegister }) => { {errors.password && (

{errors.password}

)} - {/* */} +
+ + Forgot password? + +
{/* )} */} -

{formErrorRef.current.message}

+

{formErrorRef.current.message}

+ ); diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 0c2f3f8..1470f79 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -16,7 +16,11 @@ interface FormDataState { lastName: string; } -const RegisterForm = ({ onSwitchToLogin }) => { +interface RegisterFormProps { + onSwitchToLogin: () => void +} + +const RegisterForm = ({ onSwitchToLogin }: RegisterFormProps) => { const formErrorRef = useRef<{ message: string; error: boolean }>({ message: "", error: false }); const [formData, setForm] = useState({ email: "", @@ -76,7 +80,7 @@ const RegisterForm = ({ onSwitchToLogin }) => { if (!password) return "Password is required"; if (password.length < 8) return "Password must be at least 8 characters 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); diff --git a/src/components/auth/UsertAuthUI.tsx b/src/components/auth/UsertAuthUI.tsx index fbc1718..677244b 100644 --- a/src/components/auth/UsertAuthUI.tsx +++ b/src/components/auth/UsertAuthUI.tsx @@ -9,37 +9,43 @@ import { fetchUserDetails } from "../../store/slices/userSlice"; import LoginRegisterImg from "../../assets/illustration_1_.png"; import SMELogo from "../../assets/SMELogo.png"; import api from "../../config/api"; +import ForgotPasswordForm from "./ForgotPasswordForm"; +enum PAGE_TYPE_ENUM { + LOGIN, + REGISTER, + FORGOT_PASSWORD, + EMPTY +} function UserAuthUI() { - const [isLogin, setIsLogin] = useState(true); + const [pageType, setPageType] = useState(PAGE_TYPE_ENUM.EMPTY); const navigate = useNavigate(); const location = useLocation(); // Hook to get the current URL path // Sync isLogin state with the current route useEffect(() => { const path = location.pathname; - if (path == "/login") { - setIsLogin(true); - } else if (path == "/register") { - setIsLogin(false); + if (path.includes("/login")) { + setPageType(PAGE_TYPE_ENUM.LOGIN); + } else if (path.includes("/register")) { + setPageType(PAGE_TYPE_ENUM.REGISTER); + } else if (path.includes("/forgot-password")) { + setPageType(PAGE_TYPE_ENUM.FORGOT_PASSWORD); } }, [location.pathname]); - const handleLogin = (loginData) => { - console.log("Login data:", loginData); - // Your login API call logic here - }; + // const handleLogin = (loginData) => { + // }; - const handleRegister = (registerData) => { - console.log("Register data:", registerData); - // Your register API call logic here - }; + // const handleRegister = (registerData) => { + // }; useEffect(() => { + if (pageType !== PAGE_TYPE_ENUM.LOGIN) return (async () => { await api.get(`/api/v1/user/details`); })() - }, []); + }, [pageType]); return (
@@ -70,17 +76,9 @@ function UserAuthUI() { {/* Right Side - Form */}
- {isLogin ? ( - navigate("/register")} // Navigate to /register - /> - ) : ( - navigate("/login")} // Navigate to /login - /> - )} + {pageType === PAGE_TYPE_ENUM.LOGIN && navigate("/register")} />} + {pageType === PAGE_TYPE_ENUM.REGISTER && navigate("/login")} />} + {pageType === PAGE_TYPE_ENUM.FORGOT_PASSWORD && }
); diff --git a/src/components/forgotPassword/ForgotPassword.tsx b/src/components/forgotPassword/ForgotPassword.tsx new file mode 100644 index 0000000..e5971aa --- /dev/null +++ b/src/components/forgotPassword/ForgotPassword.tsx @@ -0,0 +1,13 @@ +import UserAuthUI from "../auth/UsertAuthUI" + +function ForgotPassword() { + return ( + <> +
+ +
+ + ) +} + +export default ForgotPassword