jai #21

Merged
jaipandey merged 7 commits from jai into main 2025-08-28 11:55:32 +00:00
8 changed files with 835 additions and 366 deletions
Showing only changes of commit d9f8cd58b0 - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -1,3 +1,4 @@
// import OAuthGoogle from "./OAuthGoogle"; // import OAuthGoogle from "./OAuthGoogle";
import axios from "axios"; import axios from "axios";
import { useState } from "react"; import { useState } from "react";
@ -8,6 +9,8 @@ import { Bounce, toast } from "react-toastify";
// import Cookies from "js-cookie"; // import Cookies from "js-cookie";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store/store"; import { AppDispatch } from "../../store/store";
import { BsEye } from "react-icons/bs";
import { FiEyeOff } from "react-icons/fi";
// import api from "../../config/api"; // import api from "../../config/api";
interface FormDataState { interface FormDataState {
@ -16,94 +19,166 @@ interface FormDataState {
} }
function LoginForm() {
const [formData, setForm] = useState<FormDataState>({ email: "", password: "" });
const [cacheKey, setCacheKey] = useState<string>('');
const [nextClick, setNextClick] = useState<boolean>(false);
const dispatch = useDispatch<AppDispatch>(); const LoginForm = ({ onSwitchToRegister }) => {
const navigate = useNavigate(); const [formData, setForm] = useState({ email: "", password: "" });
const [cacheKey, setCacheKey] = useState("");
const [nextClick, setNextClick] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState({ email: "", password: "" });
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate()
const validateEmail = (email) => {
const trimmedEmail = email.trim();
const handleNextClick = () => { if (!trimmedEmail) {
return "Email address is required";
// Validation Starts }
if (formData.email === "") {
return; if (trimmedEmail.length > 254) {
return "Email address is too long";
} }
// Validate Email Format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) { if (!emailRegex.test(trimmedEmail)) {
return "Please enter a valid email address";
}
// Check for common typos
const commonDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'];
const domain = trimmedEmail.split('@')[1];
const commonTypos = {
'gmial.com': 'gmail.com',
'gmai.com': 'gmail.com',
'yahooo.com': 'yahoo.com',
'hotmial.com': 'hotmail.com'
};
if (commonTypos[domain]) {
return `Did you mean ${trimmedEmail.replace(domain, commonTypos[domain])}?`;
}
return "";
};
const validatePassword = (password) => {
if (!password) {
return "Password is required";
}
// if (password.length < 6) {
// return "Password must be at least 6 characters long";
// }
// if (password.length > 128) {
// return "Password is too long";
// }
return "";
};
const handleEmailChange = (e) => {
const email = e.target.value;
setForm({ ...formData, email });
// Clear error when user starts typing
if (errors.email) {
setErrors({ ...errors, email: "" });
}
};
const handlePasswordChange = (e) => {
const password = e.target.value;
setForm({ ...formData, password });
// Clear error when user starts typing
if (errors.password) {
setErrors({ ...errors, password: "" });
}
};
const handleNextClick = async () => {
const emailError = validateEmail(formData.email);
if (emailError) {
setErrors({ ...errors, email: emailError });
return; return;
} }
// Validation Ends
// Checking user exists getting cacheKey setIsLoading(true);
axios.get(`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
.then((res) => {
try {
const res = await axios.get(
`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`,
{
headers: {
"ngrok-skip-browser-warning": "69420",
}
}
);
if (res.status === 200) { if (res.status === 200) {
toast.dismiss(); toast.dismiss();
setCacheKey(res.data); setCacheKey(res.data);
setNextClick(true); setNextClick(true);
} }
}) } catch (err: any) {
.catch((err) => {
toast.dismiss(); toast.dismiss();
toast.error("User Does Not Exists", { toast.error("User Does Not Exist", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: false, closeOnClick: false,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "light", theme: "light",
transition: Bounce, transition: Bounce,
}); });
}); } finally {
setIsLoading(false);
} }
};
const handleFormLoginClick = () => {
// Validation Starts const handleFormLoginClick = async () => {
if (formData.email === "" || formData.password === "") { const emailError = validateEmail(formData.email);
const passwordError = validatePassword(formData.password);
if (emailError || passwordError) {
setErrors({ email: emailError, password: passwordError });
return; return;
} }
// Validate Email Format setIsLoading(true);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; setErrors({ email: "", password: "" });
if (!emailRegex.test(formData.email)) {
return;
}
// Validation Ends
// Login API Call try {
axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/login`, const res = await axios.post(
{ cacheKey: cacheKey, password: formData.password }, `${import.meta.env.VITE_BASE_URI}/api/auth/login`,
{ email: formData.email, password: formData.password },
{ {
withCredentials: true, withCredentials: true,
headers: { headers: {
"ngrok-skip-browser-warning": "69420", "ngrok-skip-browser-warning": "69420",
"Access-Control-Allow-Origin": "*" "Access-Control-Allow-Origin": "*",
},
} }
} );
)
.then((res) => {
if (res.status === 200) {
if (res.status === 200) {
toast.dismiss(); toast.dismiss();
// Example if you want cookies / redux:
// Cookies.set("authorization", res.headers.authorization, { // Cookies.set("authorization", res.headers.authorization, {
// path: "/", // path: "/",
// secure: true, // Required for HTTPS // secure: true,
// sameSite: "Lax", // Required for cross-origin requests // sameSite: "Lax",
// }); // });
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
setTimeout(() => navigate('/'), 1000); // Redirect after 1 second
navigate("/"); // Redirect after 1 second
toast.success("Login Successful", { toast.success("Login Successful", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@ -116,10 +191,10 @@ function LoginForm() {
transition: Bounce, transition: Bounce,
}); });
} }
}) } catch (err) {
.catch((err) => {
if (err.status === 401) {
toast.dismiss(); toast.dismiss();
if (err.response?.status === 400 || err.response?.status === 401) {
toast.error("Credentials Not Match", { toast.error("Credentials Not Match", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@ -132,7 +207,6 @@ function LoginForm() {
transition: Bounce, transition: Bounce,
}); });
} else { } else {
toast.dismiss();
toast.error("Some Error Occurred", { toast.error("Some Error Occurred", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@ -145,58 +219,128 @@ function LoginForm() {
transition: Bounce, transition: Bounce,
}); });
} }
}); } finally {
setIsLoading(false);
} }
};
const handleGoogleLogin = () => {
// Your Google OAuth logic here
console.log("Google login clicked");
};
return ( 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">WELCOME BACK 👋</span>
<h2 className="text-2xl font-bold text-gray-800 mt-2">Login to your Account</h2>
</div>
<div className="flex justify-center">
<OAuthGoogle onGoogleLogin={handleGoogleLogin} />
</div>
<div className="flex items-center my-6">
<hr className="flex-1 border-gray-300" />
<span className="px-4 text-gray-500 text-sm">or</span>
<hr className="flex-1 border-gray-300" />
</div>
<div className="space-y-4">
<div> <div>
<h1>Login</h1> <label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<form action="#" onSubmit={(e) => { <input
e.preventDefault(); type="email"
}}> value={formData.email}
onChange={handleEmailChange}
<div className={`${nextClick ? 'hidden' : ''}`}> placeholder="Enter your email"
<div className="input-box"> className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email
<input type="email" placeholder="Email" required ? "border-red-500 focus:ring-red-500"
onChange={(e) => setForm({ ...formData, email: e.target.value })} /> : "border-gray-300 focus:ring-blue-500"
<i className='bx bxs-user'></i> }`}
</div> disabled={nextClick || isLoading}
<button type="submit" className="btn" onClick={handleNextClick}>Next</button>
</div>
</form>
<form action="#" onSubmit={(e) => {
e.preventDefault();
handleFormLoginClick();
}}>
<div className={`${!nextClick ? 'hidden' : ''}`}>
<div className="input-box">
<input type="password" placeholder="Password" required
onChange={(e) => setForm({ ...formData, password: e.target.value })}
/> />
<i className='bx bxs-lock-alt' ></i> {errors.email && (
</div> <p className="text-red-500 text-sm mt-1">{errors.email}</p>
<div className="forgot-link"> )}
<a href="#">Forgot Password?</a>
</div>
<button type="submit" className="btn">Login</button>
</div>
</form>
<p>or login with social platforms</p>
<div className="social-icons">
<OAuthGoogle isLogin={true} />
</div> </div>
{/* {nextClick && ( */}
<div> <div>
<p>Create an Account. <Link to={'/register'} className="text-blue-400">Register</Link></p> <label className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handlePasswordChange}
placeholder="Enter your password"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
disabled={isLoading}
>
{showPassword ? <FiEyeOff size={20} /> : <BsEye size={20} />}
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
{/* <div className="text-right mt-2">
<a href="#" className="text-blue-500 text-sm hover:underline">
Forgot password?
</a>
</div> */}
</div>
{/* )} */}
{/* {!nextClick ? (
<button
onClick={handleNextClick}
disabled={isLoading}
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white"
}`}
>
{isLoading ? "Verifying..." : "Next"}
</button>
) : ( */}
<button
onClick={handleFormLoginClick}
disabled={isLoading}
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white"
}`}
>
{isLoading ? "Logging in..." : "Login"}
</button>
{/* )} */}
<div className="text-center text-sm text-gray-600">
Create an Account?{" "}
<button
onClick={onSwitchToRegister}
className="text-blue-500 font-medium hover:underline"
>
Register
</button>
</div> </div>
</div> </div>
) </div>
} );
};
export default LoginForm export default LoginForm

View File

@ -10,6 +10,8 @@ import { Bounce, toast } from "react-toastify";
import { updateUser, UserState } from "../../store/slices/userSlice"; import { updateUser, UserState } from "../../store/slices/userSlice";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../../store/store"; import { AppDispatch, RootState } from "../../store/store";
import { BsEye } from "react-icons/bs";
import { FiEyeOff } from "react-icons/fi";
// import api from "../../config/api"; // import api from "../../config/api";
interface FormDataState { interface FormDataState {
@ -21,52 +23,217 @@ interface FormDataState {
} }
function RegisterForm() { const RegisterForm = ({ onSwitchToLogin }) => {
const [formData, setForm] = useState<FormDataState>({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "" }); const [formData, setForm] = useState({
email: "",
password: "",
confirmPassword: "",
firstName: "",
lastName: ""
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState({
firstName: "",
lastName: "",
email: "",
password: "",
confirmPassword: ""
});
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate()
const dispatch = useDispatch<AppDispatch>(); const validateFirstName = (name) => {
const navigate = useNavigate(); const trimmedName = name.trim();
const handleFormRegisterClick = () => { if (!trimmedName) {
formData.firstName = formData.firstName.trim(); return "First name is required";
formData.lastName = formData.lastName.trim(); }
// Validation Starts
if (formData.email === "" || formData.password === "" || formData.confirmPassword === "" || formData.firstName === "" || formData.lastName === "") { if (trimmedName.length < 2) {
console.log('All Fields are Required'); return "First name must be at least 2 characters long";
return; }
if (trimmedName.length > 50) {
return "First name is too long";
}
const nameRegex = /^[a-zA-Z\s'-]+$/;
if (!nameRegex.test(trimmedName)) {
return "First name can only contain letters, spaces, hyphens, and apostrophes";
}
return "";
};
const validateLastName = (name) => {
const trimmedName = name.trim();
if (!trimmedName) {
return "Last name is required";
}
if (trimmedName.length < 2) {
return "Last name must be at least 2 characters long";
}
if (trimmedName.length > 50) {
return "Last name is too long";
}
const nameRegex = /^[a-zA-Z\s'-]+$/;
if (!nameRegex.test(trimmedName)) {
return "Last name can only contain letters, spaces, hyphens, and apostrophes";
}
return "";
};
const validateEmail = (email) => {
const trimmedEmail = email.trim();
if (!trimmedEmail) {
return "Email address is required";
}
if (trimmedEmail.length > 254) {
return "Email address is too long";
} }
// Validate Email Format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) { if (!emailRegex.test(trimmedEmail)) {
console.log("Invalid email format"); return "Please enter a valid email address";
}
return "";
};
const validatePassword = (password) => {
if (!password) {
return "Password is required";
}
if (password.length < 8) {
return "Password must be at least 8 characters long";
}
if (password.length > 128) {
return "Password is too long";
}
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
// const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
// if (!hasUpperCase) {
// return "Password must contain at least one uppercase letter";
// }
// if (!hasLowerCase) {
// return "Password must contain at least one lowercase letter";
// }
// if (!hasNumbers) {
// return "Password must contain at least one number";
// }
// if (!hasSpecialChar) {
// return "Password must contain at least one special character";
// }
return "";
};
const validateConfirmPassword = (confirmPassword, password) => {
if (!confirmPassword) {
return "Please confirm your password";
}
if (confirmPassword !== password) {
return "Passwords do not match";
}
return "";
};
const handleInputChange = (field, value) => {
setForm({ ...formData, [field]: value });
// Clear error when user starts typing
if (errors[field]) {
setErrors({ ...errors, [field]: "" });
}
// If confirming password, also check if it matches
if (field === "password" && formData.confirmPassword && errors.confirmPassword) {
const confirmError = validateConfirmPassword(formData.confirmPassword, value);
if (!confirmError) {
setErrors({ ...errors, [field]: "", confirmPassword: "" });
}
}
};
const handleFormRegisterClick = async () => {
const firstNameError = validateFirstName(formData.firstName);
const lastNameError = validateLastName(formData.lastName);
const emailError = validateEmail(formData.email);
const passwordError = validatePassword(formData.password);
const confirmPasswordError = validateConfirmPassword(formData.confirmPassword, formData.password);
const newErrors = {
firstName: firstNameError,
lastName: lastNameError,
email: emailError,
password: passwordError,
confirmPassword: confirmPasswordError,
};
setErrors(newErrors);
// If any errors exist, stop
if (Object.values(newErrors).some((error) => error !== "")) {
return; return;
} }
// Validation Ends
setIsLoading(true);
// Register API Call const trimmedData = {
console.dir(formData); firstName: formData.firstName.trim(),
lastName: formData.lastName.trim(),
email: formData.email.trim(),
password: formData.password,
confirmPassword: formData.confirmPassword,
};
axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/signup`, { firstName: formData.firstName, lastName: formData.lastName, email: formData.email, password: formData.password }, { try {
const res = await axios.post(
`${import.meta.env.VITE_BASE_URI}/api/auth/signup`,
{
firstName: trimmedData.firstName,
lastName: trimmedData.lastName,
email: trimmedData.email,
password: trimmedData.password,
},
{
withCredentials: true, withCredentials: true,
headers: { headers: {
"ngrok-skip-browser-warning": "69420", "ngrok-skip-browser-warning": "69420",
"Access-Control-Allow-Origin": "*" "Access-Control-Allow-Origin": "*",
},
} }
}) );
.then((res) => {
if (res.status === 201) {
if (res.status === 201) {
toast.dismiss(); toast.dismiss();
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
// dispatch(updateUser({ ...res.data, authorized: true })); // dispatch(updateUser({ ...res.data, authorized: true }));
navigate('/') navigate("/");
} }
}) } catch (err: any) {
.catch((err) => {
if (err.status === 409) {
toast.dismiss(); toast.dismiss();
if (err.response?.status === 409) {
toast.error("User Already Exists", { toast.error("User Already Exists", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@ -74,12 +241,10 @@ function RegisterForm() {
closeOnClick: false, closeOnClick: false,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "light", theme: "light",
transition: Bounce, transition: Bounce,
}); });
} else { } else {
toast.dismiss();
toast.error("Some Error Occurred", { toast.error("Some Error Occurred", {
position: "top-center", position: "top-center",
autoClose: 5000, autoClose: 5000,
@ -87,92 +252,181 @@ function RegisterForm() {
closeOnClick: false, closeOnClick: false,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "light", theme: "light",
transition: Bounce, transition: Bounce,
}); });
} }
}); } finally {
setIsLoading(false);
} }
};
const handleGoogleLogin = () => {
// Your Google OAuth logic here
console.log("Google signup clicked");
};
return ( return (
<div> <div className="bg-white p-5 rounded-lg w-full max-w-lg">
<form action="#" onSubmit={(e) => { <div className="text-center mb-2">
e.preventDefault(); <h2 className="text-2xl font-bold text-gray-800">Create an Account</h2>
handleFormRegisterClick();
}}>
<h1>Registration</h1>
{/* <div className="input-box">
<input type="text" placeholder="Username" required />
<i className='bx bxs-user'></i>
</div> */}
<div className="fl-container flex gap-2">
<div className="input-box">
<input
onChange={(e) => setForm({ ...formData, firstName: e.target.value })}
type="text" placeholder="First Name" pattern="^(?!\s*$).+"
required
title="Input cannot be empty or just spaces"
/>
<i className='bx bxs-envelope' ></i>
</div> </div>
<div className="input-box"> <div className="flex justify-center">
<input
onChange={(e) => setForm({ ...formData, lastName: e.target.value })}
type="text"
placeholder="Last Name"
pattern="^(?!\s*$).+"
required
title="Input cannot be empty or just spaces" />
<i className='bx bxs-envelope' ></i>
</div>
</div>
<div className="input-box">
<input
onChange={(e) => setForm({ ...formData, email: e.target.value })}
type="email" placeholder="Email" required />
<i className='bx bxs-envelope' ></i>
</div>
<div className="input-box">
<input
onChange={(e) => setForm({ ...formData, password: e.target.value })}
type="password" placeholder="Password"
pattern="^.{8,}$"
title="Password must be at least 8 digits"
required />
<i className='bx bxs-lock-alt' ></i>
</div>
<div className="input-box">
<input
onChange={(e) => setForm({ ...formData, confirmPassword: e.target.value })}
type="password"
placeholder="Confirm Password"
style={{
border: (formData.confirmPassword != "" && formData.password !== formData.confirmPassword) ? "1px solid red" : "none"
}}
required
/>
<i className='bx bxs-lock-alt' ></i>
</div>
<button type="submit"
className={`btn ${(formData.email != "" && formData.password != "" && formData.password === formData.confirmPassword) ? "opacity-100" : "opacity-60"} transition-all duration-150`}>
Register
</button>
<p>or register with social platforms</p>
<div className="social-icons">
<OAuthGoogle isLogin={false} /> <OAuthGoogle isLogin={false} />
</div> </div>
</form>
<div className="flex items-center my-1.5">
<hr className="flex-1 border-gray-300" />
<span className="px-4 text-gray-500 text-sm">or</span>
<hr className="flex-1 border-gray-300" />
</div>
<div className="space-y-1.5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<div className="grid grid-cols-2 gap-3">
<div>
<input
type="text"
value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
placeholder="First name"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.firstName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
{errors.firstName && (
<p className="text-red-500 text-xs mt-0.5">{errors.firstName}</p>
)}
</div>
<div>
<input
type="text"
value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
placeholder="Last name"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.lastName
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
{errors.lastName && (
<p className="text-red-500 text-xs mt-0.5">{errors.lastName}</p>
)}
</div>
</div>
</div>
<div> <div>
<p>Already! have an Account. <Link to={'/login'} className="text-blue-400">Login</Link></p> <label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="Enter your email"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none ${errors.email
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
placeholder="Enter your password"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.password
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
disabled={isLoading}
>
{showPassword ? <FiEyeOff size={20} /> : <BsEye size={20} />}
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</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={formData.confirmPassword}
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
placeholder="Confirm your password"
className={`w-full px-4 py-1.5 border rounded-lg focus:ring-2 focus:border-transparent outline-none pr-12 ${errors.confirmPassword
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
disabled={isLoading}
>
{showConfirmPassword ? <FiEyeOff size={20} /> : <BsEye size={20} />}
</button>
</div>
{errors.confirmPassword && (
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</div>
<button
onClick={handleFormRegisterClick}
disabled={isLoading}
className={`w-full py-2 rounded-lg font-medium transition-colors ${isLoading
? "bg-gray-400 cursor-not-allowed"
: "bg-blue-500 hover:bg-blue-600 text-white"
}`}
>
{isLoading ? "Creating Account..." : "Create Account"}
</button>
<div className="text-center text-sm text-gray-600">
Already have an Account?{" "}
<button
onClick={onSwitchToLogin}
className="text-blue-500 font-medium hover:underline"
>
Login
</button>
</div> </div>
</div> </div>
) </div>
} );
};
export default RegisterForm export default RegisterForm

View File

@ -1,87 +1,82 @@
import { useEffect, useLayoutEffect, useRef } from "react"; import { useEffect, useState } from "react";
import "../styles/style.css";
import "../styles/style.css" import { useNavigate, useLocation } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import LoginForm from "./LoginForm"; import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm"; import RegisterForm from "./RegisterForm";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store/store"; import { AppDispatch } from "../../store/store";
import { fetchUserDetails } from "../../store/slices/userSlice"; import { fetchUserDetails } from "../../store/slices/userSlice";
import LoginRegisterImg from "../../assets/illustration_1_.png";
import SMELogo from "../../assets/SMELogo.png";
function UserAuthUI({ pg }: { pg: string }) { function UserAuthUI() {
const [isLogin, setIsLogin] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); // Hook to get the current URL path
useLayoutEffect(() => { // Sync isLogin state with the current route
dispatch(fetchUserDetails());
}, []);
const dispatch = useDispatch<AppDispatch>();
const elementRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
if (pg === 'login') { const path = location.pathname;
elementRef.current?.classList.add('active') if (path === "/login") {
setTimeout(() => { setIsLogin(true);
elementRef.current?.classList.remove('active') } else if (path === "/register") {
}, 0) setIsLogin(false);
} else {
setTimeout(() => {
elementRef.current?.classList.add('active')
}, 0)
} }
}, []) }, [location.pathname]);
// const [activeToggle, setActiveToggle] = useState<string>(''); const handleLogin = (loginData) => {
console.log("Login data:", loginData);
// Your login API call logic here
};
// const [page, setPage] = useState(pg || 'login'); const handleRegister = (registerData) => {
function handleToggleForm(val: string) { console.log("Register data:", registerData);
navigate(`/${val}`); // Your register API call logic here
// setPage((prev) => prev === "login" ? "register" : "login") };
}
// const user: UserState = useSelector((state: RootState) => state.user);
// useEffect(() => {
// if (page === 'register') {
// setActiveToggle('active')
// } else {
// setActiveToggle('')
// }
// }, [page])
// useEffect(() => {
// setPage(pg);
// }, [pg])
return ( return (
<> <div className="max-h-screen bg-white flex">
<div className="auth-ui-con main-container-l"> {/* Left Side - Branding */}
<div ref={elementRef} className={`container-l`}> <div className="hidden lg:flex lg:w-1/2 bg-[var(--primary)] text-white p-12">
<div className={`form-box login flex-col`}> <div className="flex flex-col items-center justify-center text-center w-full h-full gap-6">
<LoginForm /> {/* Logos */}
<div className="flex flex-col items-center gap-2 space-y-3">
<img src={SMELogo} alt="SME Logo" className="h-10 w-auto mt-10" />
<img
src={LoginRegisterImg}
alt="Illustration"
className="h-[50vh] w-auto object-contain"
/>
</div> </div>
<div className={`form-box register flex-col`}> {/* Text Section */}
<RegisterForm /> <div className="flex flex-col items-center max-w-md gap-4">
<h2 className="text-xl font-bold">Create, eSign, and Connect!</h2>
<p className="text-blue-100 text-xs leading-relaxed">
A Powerful eSign solution for Salesforce, powered by Adobe Advanced
Trust List (AATL), to elevate your digital signature experience and
foster agreements closures.
</p>
</div>
</div>
</div> </div>
<div className="toggle-box"> {/* Right Side - Form */}
<div className="toggle-panel toggle-left"> <div className="flex-1 flex items-center justify-center p-8 lg:p-12">
<h1>Sign Made Easy</h1> {isLogin ? (
<p>Don't have an account?</p> <LoginForm
<button className="btn register-btn" onClick={() => handleToggleForm("register")}>Register</button> onLogin={handleLogin}
</div> onSwitchToRegister={() => navigate("/register")} // Navigate to /register
/>
<div className="toggle-panel toggle-right"> ) : (
<h1>Welcome Back!</h1> <RegisterForm
<p>Already have an account?</p> onRegister={handleRegister}
<button className="btn login-btn" onClick={() => handleToggleForm("login")}>Login</button> onSwitchToLogin={() => navigate("/login")} // Navigate to /login
/>
)}
</div> </div>
</div> </div>
</div> );
</div>
</>
)
} }
export default UserAuthUI export default UserAuthUI;

View File

@ -3,7 +3,7 @@ import UserAuthUI from "../auth/UsertAuthUI"
function Login() { function Login() {
return ( return (
<> <>
<div className="flex justify-center items-center w-full h-[100vh]"> <div className="">
<UserAuthUI pg={"login"} /> <UserAuthUI pg={"login"} />
</div> </div>
</> </>

View File

@ -21,6 +21,7 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
// await sleep(5000); // await sleep(5000);
const documents = useSelector((state: RootState) => state.documents.documents); const documents = useSelector((state: RootState) => state.documents.documents);
const docCount = documents.length;
useEffect(() => { useEffect(() => {
@ -87,39 +88,110 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
} }
}; };
// Get proper Tailwind class names for upload card span
const getUploadCardSpanClass = () => {
switch (docCount) {
case 0: return 'col-span-5'; // Full width when no docs
case 1: return 'col-span-4'; // 4 cols for upload, 1 for doc
case 2: return 'col-span-3'; // 3 cols for upload, 2 for docs
case 3: return 'col-span-2'; // 2 cols for upload, 3 for docs
case 4: return 'col-span-1'; // 1 col for upload, 4 for docs
default: return 'col-span-1'; // 1 col for upload, rest for docs
}
};
// Calculate how many documents should be in the first row
const getFirstRowDocCount = () => {
if (docCount === 0) return 0;
if (docCount <= 4) return docCount; // All docs fit in first row
return 4; // Max 4 docs in first row when upload card takes 1 column
};
const firstRowDocCount = getFirstRowDocCount();
const remainingDocs = Math.max(0, docCount - firstRowDocCount);
return ( return (
<> <>
{/* Upload Card First */} {/* Upload Card First */}
<div <div className='space-y-3'>
id='upload-doc-step-one' <div id='upload-doc-step-one' className="grid grid-cols-5 gap-2">
className="flex flex-wrap gap-3"> {/* Upload Document Card - Always First */}
{/* Uploaded documents */}
<div <div
onClick={() => setModalOpen(true)} onClick={() => setModalOpen(true)}
className="cursor-pointer w-[160px] h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md" className={`${getUploadCardSpanClass()} cursor-pointer h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md`}
> >
<FaPlus className="text-[var(--primary)] text-3xl mb-2" /> <FaPlus className="text-[var(--primary)] text-3xl mb-2" />
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p> <p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
</div> </div>
{/* Document Show */} {/* First Row Documents */}
<ReactSortable <ReactSortable
className="flex flex-wrap gap-3" className="contents" // Use 'contents' to make children participate in parent grid
list={documents.map(doc => ({ ...doc }))} list={documents.slice(0, firstRowDocCount).map(doc => ({ ...doc }))}
setList={(newState) => dispatch(setDocuments(newState))} setList={(newState) => {
const updatedDocs = [
...newState,
...documents.slice(firstRowDocCount)
];
dispatch(setDocuments(updatedDocs));
}}
handle=".drag-handle" handle=".drag-handle"
animation={200} animation={200}
ghostClass="sortable-ghost" ghostClass="sortable-ghost"
disabled={false} disabled={false}
> >
{documents.map((doc) => ( {documents.slice(0, firstRowDocCount).map((doc) => (
<DocumentCard key={doc.id} id={doc.id} name={doc.name} isCompleted={("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED")) ? true : false} transactionTemplateToggle={transactionTemplateToggle}/> <div key={doc.id} className="col-span-1">
<DocumentCard
id={doc.id}
name={doc.name}
isCompleted={
("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED"))
? true
: false
}
transactionTemplateToggle={transactionTemplateToggle}
/>
</div>
))} ))}
</ReactSortable> </ReactSortable>
</div> </div>
{/* Additional Rows - Remaining Documents (5th, 6th, 7th, etc.) */}
{remainingDocs > 0 && (
<ReactSortable
className="grid grid-cols-5 gap-2"
list={documents.slice(firstRowDocCount).map(doc => ({ ...doc }))}
setList={(newState) => {
const updatedDocs = [
...documents.slice(0, firstRowDocCount),
...newState
];
dispatch(setDocuments(updatedDocs));
}}
handle=".drag-handle"
animation={200}
ghostClass="sortable-ghost"
disabled={false}
>
{documents.slice(firstRowDocCount).map((doc) => (
<div key={doc.id} className="col-span-1">
<DocumentCard
id={doc.id}
name={doc.name}
isCompleted={
("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED"))
? true
: false
}
transactionTemplateToggle={transactionTemplateToggle}
/>
</div>
))}
</ReactSortable>
)}
{/* Modal */} {/* Modal */}
{isModalOpen && ( {isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-4">
@ -181,6 +253,10 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
</div> </div>
</div> </div>
)} )}
</div>
</> </>
); );
}; };

View File

@ -3,7 +3,7 @@ import UserAuthUI from "../auth/UsertAuthUI"
function Register() { function Register() {
return ( return (
<> <>
<div className="flex justify-center items-center w-full h-[100vh]"> <div className="">
<UserAuthUI pg={"register"} /> <UserAuthUI pg={"register"} />
</div> </div>
</> </>

View File

@ -30,13 +30,13 @@ html {
.auth-ui-con .container-l { .auth-ui-con .container-l {
position: relative; position: relative;
width: 850px; width: 1280px;
height: 550px; height: 599px;
background: #fff; background: #fff;
/* margin-top: 20px; */ /* margin-top: 20px; */
margin-left: 50%; margin-left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
border-radius: 30px; /* border-radius: 30px; */
box-shadow: 0 0 30px rgba(0, 0, 0, .2); box-shadow: 0 0 30px rgba(0, 0, 0, .2);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;