diff --git a/src/assets/illustration_1_.png b/src/assets/illustration_1_.png new file mode 100644 index 0000000..b81f51d Binary files /dev/null and b/src/assets/illustration_1_.png differ diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index 733933c..730d857 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,3 +1,4 @@ + // import OAuthGoogle from "./OAuthGoogle"; import axios from "axios"; import { useState } from "react"; @@ -8,6 +9,8 @@ import { Bounce, toast } from "react-toastify"; // import Cookies from "js-cookie"; import { useDispatch } from "react-redux"; import { AppDispatch } from "../../store/store"; +import { BsEye } from "react-icons/bs"; +import { FiEyeOff } from "react-icons/fi"; // import api from "../../config/api"; interface FormDataState { @@ -16,43 +19,167 @@ interface FormDataState { } -function LoginForm() { - const [formData, setForm] = useState({ email: "", password: "" }); - const [cacheKey, setCacheKey] = useState(''); - const [nextClick, setNextClick] = useState(false); - const dispatch = useDispatch(); - const navigate = useNavigate(); +const LoginForm = ({ onSwitchToRegister }) => { + 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 = () => { - - // Validation Starts - if (formData.email === "") { - return; + 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@]+$/; - 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; } - // Validation Ends - // Checking user exists getting cacheKey - axios.get(`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`, { headers: { "ngrok-skip-browser-warning": "69420" } }) - .then((res) => { + setIsLoading(true); - - if (res.status === 200) { - toast.dismiss(); - setCacheKey(res.data); - setNextClick(true); + try { + const res = await axios.get( + `${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`, + { + headers: { + "ngrok-skip-browser-warning": "69420", + } } - }) - .catch((err) => { + ); + + if (res.status === 200) { toast.dismiss(); - toast.error("User Does Not Exists", { + setCacheKey(res.data); + setNextClick(true); + } + } catch (err: any) { + toast.dismiss(); + toast.error("User Does Not Exist", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + theme: "light", + transition: Bounce, + }); + } finally { + setIsLoading(false); + } + }; + + + const handleFormLoginClick = async () => { + const emailError = validateEmail(formData.email); + const passwordError = validatePassword(formData.password); + + if (emailError || passwordError) { + setErrors({ email: emailError, password: passwordError }); + return; + } + + setIsLoading(true); + setErrors({ email: "", password: "" }); + + try { + const res = await axios.post( + `${import.meta.env.VITE_BASE_URI}/api/auth/login`, + { email: formData.email, password: formData.password }, + { + withCredentials: true, + headers: { + "ngrok-skip-browser-warning": "69420", + "Access-Control-Allow-Origin": "*", + }, + } + ); + + if (res.status === 200) { + toast.dismiss(); + + // Example if you want cookies / redux: + // Cookies.set("authorization", res.headers.authorization, { + // path: "/", + // secure: true, + // sameSite: "Lax", + // }); + // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); + + navigate("/"); // Redirect after 1 second + + toast.success("Login Successful", { position: "top-center", autoClose: 5000, hideProgressBar: false, @@ -63,140 +190,157 @@ function LoginForm() { theme: "light", transition: Bounce, }); - }); - - } - - const handleFormLoginClick = () => { - - // Validation Starts - if (formData.email === "" || formData.password === "") { - return; - } - - // Validate Email Format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(formData.email)) { - return; - } - // Validation Ends - - // Login API Call - axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/login`, - { cacheKey: cacheKey, password: formData.password }, - { - withCredentials: true, - headers: { - "ngrok-skip-browser-warning": "69420", - "Access-Control-Allow-Origin": "*" - } } - ) - .then((res) => { - if (res.status === 200) { - - toast.dismiss(); - // Cookies.set("authorization", res.headers.authorization, { - // path: "/", - // secure: true, // Required for HTTPS - // sameSite: "Lax", // Required for cross-origin requests - // }); + } catch (err) { + toast.dismiss(); - // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); - setTimeout(() => navigate('/'), 1000); // Redirect after 1 second - toast.success("Login Successful", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - }) - .catch((err) => { - if (err.status === 401) { - toast.dismiss(); - toast.error("Credentials Not Match", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } else { - toast.dismiss(); - toast.error("Some Error Occurred", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - }); - } + if (err.response?.status === 400 || err.response?.status === 401) { + toast.error("Credentials Not Match", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } else { + toast.error("Some Error Occurred", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } + } finally { + setIsLoading(false); + } + }; + + + const handleGoogleLogin = () => { + // Your Google OAuth logic here + console.log("Google login clicked"); + }; return ( -
-

Login

- - -
{ - e.preventDefault(); - }}> - -
-
- setForm({ ...formData, email: e.target.value })} /> - -
- -
-
- - -
{ - e.preventDefault(); - handleFormLoginClick(); - }}> -
-
- setForm({ ...formData, password: e.target.value })} - /> - -
- - -
-
- -

or login with social platforms

- -
- +
+
+ WELCOME BACK 👋 +

Login to your Account

-
-

Create an Account. Register

+
+ +
+ +
+
+ or +
+
+ +
+
+ + + {errors.email && ( +

{errors.email}

+ )} +
+ + {/* {nextClick && ( */} +
+ +
+ + +
+ {errors.password && ( +

{errors.password}

+ )} + {/* */} +
+ {/* )} */} + + {/* {!nextClick ? ( + + ) : ( */} + + {/* )} */} + +
+ Create an Account?{" "} + +
- ) -} + ); +}; -export default LoginForm +export default LoginForm \ No newline at end of file diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 6e02a69..67656c4 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -10,6 +10,8 @@ import { Bounce, toast } from "react-toastify"; import { updateUser, UserState } from "../../store/slices/userSlice"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch, RootState } from "../../store/store"; +import { BsEye } from "react-icons/bs"; +import { FiEyeOff } from "react-icons/fi"; // import api from "../../config/api"; interface FormDataState { @@ -21,158 +23,410 @@ interface FormDataState { } -function RegisterForm() { - const [formData, setForm] = useState({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "" }); +const RegisterForm = ({ onSwitchToLogin }) => { + 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(); - const navigate = useNavigate(); + const validateFirstName = (name) => { + const trimmedName = name.trim(); - const handleFormRegisterClick = () => { - formData.firstName = formData.firstName.trim(); - formData.lastName = formData.lastName.trim(); - // Validation Starts - if (formData.email === "" || formData.password === "" || formData.confirmPassword === "" || formData.firstName === "" || formData.lastName === "") { - console.log('All Fields are Required'); - return; + if (!trimmedName) { + return "First name is required"; + } + + if (trimmedName.length < 2) { + return "First name must be at least 2 characters long"; + } + + 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@]+$/; - if (!emailRegex.test(formData.email)) { - console.log("Invalid email format"); - return; + if (!emailRegex.test(trimmedEmail)) { + return "Please enter a valid email address"; } - // Validation Ends + return ""; + }; - // Register API Call - console.dir(formData); + const validatePassword = (password) => { + if (!password) { + return "Password is required"; + } - axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/signup`, { firstName: formData.firstName, lastName: formData.lastName, email: formData.email, password: formData.password }, { - withCredentials: true, - headers: { - "ngrok-skip-browser-warning": "69420", - "Access-Control-Allow-Origin": "*" + 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: "" }); } - }) - .then((res) => { - if (res.status === 201) { + } + }; - toast.dismiss(); - // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); - // dispatch(updateUser({ ...res.data, authorized: true })); - navigate('/') - } - }) - .catch((err) => { - if (err.status === 409) { - toast.dismiss(); - toast.error("User Already Exists", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } else { - toast.dismiss(); - toast.error("Some Error Occurred", { - position: "top-center", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: "light", - transition: Bounce, - }); - } - }); + 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; + } + + setIsLoading(true); + + const trimmedData = { + firstName: formData.firstName.trim(), + lastName: formData.lastName.trim(), + email: formData.email.trim(), + password: formData.password, + confirmPassword: formData.confirmPassword, + }; + + 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, + headers: { + "ngrok-skip-browser-warning": "69420", + "Access-Control-Allow-Origin": "*", + }, + } + ); + + if (res.status === 201) { + toast.dismiss(); + // dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization })); + // dispatch(updateUser({ ...res.data, authorized: true })); + navigate("/"); } + } catch (err: any) { + toast.dismiss(); + + if (err.response?.status === 409) { + toast.error("User Already Exists", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + theme: "light", + transition: Bounce, + }); + } else { + toast.error("Some Error Occurred", { + position: "top-center", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + theme: "light", + transition: Bounce, + }); + } + } finally { + setIsLoading(false); + } +}; + + + const handleGoogleLogin = () => { + // Your Google OAuth logic here + console.log("Google signup clicked"); + }; return ( -
-
{ - e.preventDefault(); - handleFormRegisterClick(); - }}> -

Registration

- {/*
- - -
*/} -
-
- setForm({ ...formData, firstName: e.target.value })} - type="text" placeholder="First Name" pattern="^(?!\s*$).+" - required - title="Input cannot be empty or just spaces" - /> - -
+
+
+

Create an Account

+
-
- setForm({ ...formData, lastName: e.target.value })} - type="text" - placeholder="Last Name" - pattern="^(?!\s*$).+" - required - title="Input cannot be empty or just spaces" /> - +
+ +
+ +
+
+ or +
+
+ +
+
+ +
+
+ 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 && ( +

{errors.firstName}

+ )} +
+
+ 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 && ( +

{errors.lastName}

+ )} +
-
+
+ setForm({ ...formData, email: e.target.value })} - type="email" placeholder="Email" required /> - -
-
- setForm({ ...formData, password: e.target.value })} - type="password" placeholder="Password" - pattern="^.{8,}$" - title="Password must be at least 8 digits" - required /> - -
-
- setForm({ ...formData, confirmPassword: e.target.value })} - type="password" - placeholder="Confirm Password" - style={{ - border: (formData.confirmPassword != "" && formData.password !== formData.confirmPassword) ? "1px solid red" : "none" - }} - required + 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 && ( +

{errors.email}

+ )}
- +
+ {errors.password && ( +

{errors.password}

+ )} +
+ +
+ +
+ 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} + /> + +
+ {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
+ + -

or register with social platforms

-
- +
+ Already have an Account?{" "} +
- - -
-

Already! have an Account. Login

- ) -} + ); +}; export default RegisterForm diff --git a/src/components/auth/UsertAuthUI.tsx b/src/components/auth/UsertAuthUI.tsx index 12693ff..44224dc 100644 --- a/src/components/auth/UsertAuthUI.tsx +++ b/src/components/auth/UsertAuthUI.tsx @@ -1,87 +1,82 @@ -import { useEffect, useLayoutEffect, useRef } from "react"; - -import "../styles/style.css" -import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import "../styles/style.css"; +import { useNavigate, useLocation } from "react-router-dom"; import LoginForm from "./LoginForm"; import RegisterForm from "./RegisterForm"; import { useDispatch } from "react-redux"; import { AppDispatch } from "../../store/store"; 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 location = useLocation(); // Hook to get the current URL path - useLayoutEffect(() => { - dispatch(fetchUserDetails()); - }, []); - - const dispatch = useDispatch(); - const elementRef = useRef(null); - + // Sync isLogin state with the current route useEffect(() => { - if (pg === 'login') { - elementRef.current?.classList.add('active') - setTimeout(() => { - elementRef.current?.classList.remove('active') - }, 0) - } else { - setTimeout(() => { - elementRef.current?.classList.add('active') - }, 0) + const path = location.pathname; + if (path === "/login") { + setIsLogin(true); + } else if (path === "/register") { + setIsLogin(false); } - }, []) + }, [location.pathname]); - // const [activeToggle, setActiveToggle] = useState(''); + const handleLogin = (loginData) => { + console.log("Login data:", loginData); + // Your login API call logic here + }; - // const [page, setPage] = useState(pg || 'login'); - function handleToggleForm(val: string) { - navigate(`/${val}`); - // 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]) + const handleRegister = (registerData) => { + console.log("Register data:", registerData); + // Your register API call logic here + }; return ( - <> -
-
-
- +
+ {/* Left Side - Branding */} +
+
+ {/* Logos */} +
+ SME Logo + Illustration
-
- -
- -
-
-

Sign Made Easy

-

Don't have an account?

- -
- -
-

Welcome Back!

-

Already have an account?

- -
+ {/* Text Section */} +
+

Create, eSign, and Connect!

+

+ A Powerful eSign solution for Salesforce, powered by Adobe Advanced + Trust List (AATL), to elevate your digital signature experience and + foster agreements closures. +

- - ) + + {/* Right Side - Form */} +
+ {isLogin ? ( + navigate("/register")} // Navigate to /register + /> + ) : ( + navigate("/login")} // Navigate to /login + /> + )} +
+
+ ); } -export default UserAuthUI +export default UserAuthUI; \ No newline at end of file diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx index ddf7096..3ab73e6 100644 --- a/src/components/login/Login.tsx +++ b/src/components/login/Login.tsx @@ -3,7 +3,7 @@ import UserAuthUI from "../auth/UsertAuthUI" function Login() { return ( <> -
+
diff --git a/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx b/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx index f9eeafb..b13e260 100644 --- a/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx +++ b/src/components/process/CompletionScreen/DocumentCompletionScreen.tsx @@ -529,13 +529,13 @@ const DocumentSubmissionOptions = () => {
*/} -
-
+
+
-
-

Document Send Successfully!

-
+
+
+

Document Send Successfully!

diff --git a/src/components/process/processStepOne/ProcessStepOne.tsx b/src/components/process/processStepOne/ProcessStepOne.tsx index b3742ae..0b3dbe0 100644 --- a/src/components/process/processStepOne/ProcessStepOne.tsx +++ b/src/components/process/processStepOne/ProcessStepOne.tsx @@ -373,7 +373,7 @@ const ProcessStepOne = () => { className='flex items-center gap-3'> {transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && - - {/* Delete Button */}
- - )}
- ) - )} + ))}
) : ( @@ -240,28 +451,24 @@ const RecipientCards = ({ userEmail, getMinRoutingOrder, setEditRecipient, setEd
)} - {signingOrderModal.open && + {signingOrderModal.open && (

Signing Order

setSigningOrderModal({ ...signingOrderModal, open: false })} - className="text-xl cursor-pointer hover:text-gray-200 transition-all"> + className="text-xl cursor-pointer hover:text-gray-200 transition-all" + >
- } - - - - - + )}
); }; -export default RecipientCards; +export default RecipientCards; \ No newline at end of file diff --git a/src/components/process/processStepOne/SelectedDocuments.tsx b/src/components/process/processStepOne/SelectedDocuments.tsx index b78a6ea..b479bb1 100644 --- a/src/components/process/processStepOne/SelectedDocuments.tsx +++ b/src/components/process/processStepOne/SelectedDocuments.tsx @@ -14,16 +14,17 @@ import { useParams } from 'react-router-dom'; import { toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE } from '../../../store/slices/selectedRecipientsSlice'; -const SelectedDocuments = ({transactionTemplateToggle}) => { +const SelectedDocuments = ({ transactionTemplateToggle }) => { // let transactionTemplateToggle = TRANSACTION_TEMPLATE_TOGGLE.TRANSACTIONS; // const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); // await sleep(5000); const documents = useSelector((state: RootState) => state.documents.documents); + const docCount = documents.length; useEffect(() => { - + console.log("Documents updated:", documents); }, [documents]); @@ -50,7 +51,7 @@ const SelectedDocuments = ({transactionTemplateToggle}) => { { name: 'Dropbox', icon: }, ]; - const handleUpload = (filesToUpload: File[]) => { + const handleUpload = (filesToUpload: File[]) => { const formData = new FormData(); filesToUpload.forEach(file => { @@ -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 ( <> {/* Upload Card First */} -
- {/* Uploaded documents */} +
+
+ {/* Upload Document Card - Always First */}
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`} >

Upload Document

- {/* Document Show */} + {/* First Row Documents */} ({ ...doc }))} - setList={(newState) => dispatch(setDocuments(newState))} + className="contents" // Use 'contents' to make children participate in parent grid + list={documents.slice(0, firstRowDocCount).map(doc => ({ ...doc }))} + setList={(newState) => { + const updatedDocs = [ + ...newState, + ...documents.slice(firstRowDocCount) + ]; + dispatch(setDocuments(updatedDocs)); + }} handle=".drag-handle" animation={200} ghostClass="sortable-ghost" disabled={false} > - {documents.map((doc) => ( - + {documents.slice(0, firstRowDocCount).map((doc) => ( +
+ +
))}
- -
+ {/* Additional Rows - Remaining Documents (5th, 6th, 7th, etc.) */} + {remainingDocs > 0 && ( + ({ ...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) => ( +
+ +
+ ))} +
+ )} + + {/* Modal */} {isModalOpen && (
@@ -181,6 +253,10 @@ const SelectedDocuments = ({transactionTemplateToggle}) => {
)} + +
+ + ); }; diff --git a/src/components/register/Register.tsx b/src/components/register/Register.tsx index adb8365..fe9ae53 100644 --- a/src/components/register/Register.tsx +++ b/src/components/register/Register.tsx @@ -3,7 +3,7 @@ import UserAuthUI from "../auth/UsertAuthUI" function Register() { return ( <> -
+
diff --git a/src/components/setting/AsideSection.tsx b/src/components/setting/AsideSection.tsx index 0cf696e..cd20e24 100644 --- a/src/components/setting/AsideSection.tsx +++ b/src/components/setting/AsideSection.tsx @@ -37,7 +37,7 @@ const AsideSection = ({ e.preventDefault(); // Prevent default anchor behavior onSubSectionClick(subSection.id); }} - className={`block w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${ + className={`block w-full text-left px-3 py-2.5 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${ activeSubSection === subSection.id ? "bg-blue-50 text-blue-600 border-l-4 border-blue-600 shadow-sm" : "text-gray-600 hover:text-gray-800 hover:bg-gray-50" diff --git a/src/components/setting/ContactForm.tsx b/src/components/setting/ContactForm.tsx index dbf5e01..e81732c 100644 --- a/src/components/setting/ContactForm.tsx +++ b/src/components/setting/ContactForm.tsx @@ -130,7 +130,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => { }; return ( -
+

Add Contact

diff --git a/src/components/setting/CreateAndInvite.tsx b/src/components/setting/CreateAndInvite.tsx index 7925a26..5ec4561 100644 --- a/src/components/setting/CreateAndInvite.tsx +++ b/src/components/setting/CreateAndInvite.tsx @@ -158,9 +158,9 @@ const CreateAndInvite = ({ ); return ( -
+
{/* Create Section */} -
+

Create Account

{/* Invite Section */} -
+

Invite

{/* Account Name Dropdown */} diff --git a/src/components/setting/Setting.tsx b/src/components/setting/Setting.tsx index 9fd8fb9..3ce7b7b 100644 --- a/src/components/setting/Setting.tsx +++ b/src/components/setting/Setting.tsx @@ -224,7 +224,7 @@ const Setting = () => { } return ( -
+
{currentSubSections.map((subSection) => (
{
{/* Tabs */}
-
+
{tabs.map((tab) => tab.type === page1 && (
{/* Responsive Layout */} -
+
{/* Sidebar (hidden on small, shown on lg) */} {activeTab === 'userProfile' && ( @@ -448,8 +448,8 @@ const Setting = () => { {/* Sidebar as horizontal nav (only on mobile) */} {activeTab === 'userProfile' && ( -
-
+
+
{getSubSections(activeTab).map((sub) => (
@@ -253,10 +254,11 @@ const DrawSignatureCanvas: React.FC = ({ onSignatureChange, initialSignat
diff --git a/src/components/setting/UserProfile.tsx b/src/components/setting/UserProfile.tsx index 1b8e654..cbbe56a 100644 --- a/src/components/setting/UserProfile.tsx +++ b/src/components/setting/UserProfile.tsx @@ -20,7 +20,7 @@ interface UserProfileProps { const UserProfile: React.FC = ({ userData }) => { return ( -
+
{/* Profile Picture */}
diff --git a/src/components/signature/SignCon.tsx b/src/components/signature/SignCon.tsx index 1270dba..00660f7 100644 --- a/src/components/signature/SignCon.tsx +++ b/src/components/signature/SignCon.tsx @@ -47,8 +47,8 @@ interface Signature { } interface AddSignatureModalProps { - // setShowAddSignature?: (value: boolean) => void; - // getAllSignatures: () => Promise; + setOpenSign?: (value: boolean) => void; + getAllSignatures: () => Promise; // editingSignature?: Signature | null; recipientId?: string; @@ -87,7 +87,8 @@ const AddSignatureModal: React.FC = ({ signTagId }) => { - const [activeTab, setActiveTab] = useState<'TYPED' | 'DRAW' | 'UPLOAD' | 'SAVED'>('TYPED'); + console.log("savedModal", recipient) + const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD' | 'SAVED'>('SELECT'); const [fullName, setFullName] = useState(''); const [initials, setInitials] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); @@ -130,13 +131,15 @@ const AddSignatureModal: React.FC = ({ // Store the preview URL const [preview1, setPreview1] = useState(null); const [preview2, setPreview2] = useState(null); + const [checked, setChecked] = useState(false); + const toggleCheckbox = () => setChecked(!checked); const [preSaveSignatures, setPreSaveSignatures] = useState([]) // Navigation items const navigationItems = [ - { id: 'TYPED', label: 'TYPED', icon: }, + { id: 'SELECT', label: 'SELECT', icon: }, { id: 'DRAW', label: 'DRAW', icon: }, { id: 'UPLOAD', label: 'UPLOAD', icon: }, { id: 'SAVED', label: 'SAVED', icon: }, @@ -314,32 +317,283 @@ const AddSignatureModal: React.FC = ({ } }; + // const renderContent = () => { + // switch (activeTab) { + // case 'SELECT': + // return ( + //
+ //
+ //

Choose Signature Style

+ //

Select from pre-designed signature styles

+ //
+ + //
+ //
+ // {signatureOptions.map((option) => ( + //
+ // setSelectedSignature({ id: option.id, style: option.style })} + // className="w-4 h-4 text-blue-600 focus:ring-blue-500" + // /> + //
+ //
+ //
Signed by:
+ //
+ // {fullName || option.name} + //
+ //
+ // dca3e646-62cd-4cc... + //
+ //
+ //
+ //
DS
+ //
+ // {initials || option.initials} + //
+ //
+ //
+ //
+ // ))} + //
+ //
+ //
+ // ); + + // case 'DRAW': + // return ( + // + // ); + + // case 'UPLOAD': + // return ( + //
+ //
+ //

Upload Signature

+ //
+ //
+ //
+ //
+ // {/* Upload 1 */} + //
+ // + // + // handleImageChange(e, setFile1, setPreview1, "fullSignature")} + // /> + //
+ + // {/* Upload 2 */} + //
+ // + // + // handleImageChange(e, setFile2, setPreview2, "initials")} + // /> + //
+ //
+ //
+ //
+ //
+ // ); + // case 'SAVED': + // return ( + //
+ //
+ //

Saved Signature's

+ // {/*

Select from pre-designed signature styles

*/} + //
+ + //
+ //
+ // {preSaveSignatures.map((signature) => ( + //
+ // {/* Signature Column */} + //
+ //
+ //
+ // Signed by: + //
+ + // {/* Signature Image or Fallback Text */} + //
+ // {signature?.signatureBlob ? ( + // Signature + // ) : ( + //
No Signature
+ // )} + //
+ + // {/* Name */} + //
+ // {signature?.signatureHash ?? ' '} + //
+ //
+ //
+ + // {/* Initial Column */} + //
+ //
+ //
+ // Initials + //
+ + // {/* Initials Image */} + //
+ // {signature?.initialsBlob ? ( + // Initials + // ) : ( + //
No Initials
+ // )} + //
+ + // {/* Optional initials hash or font */} + //
+ // {signature?.initialsHash || " "} + //
+ //
+ //
+ //
+ // ))} + + //
+ //
+ //
+ // ); + + // default: + // return null; + // } + // }; + + + const renderSignatureText = (type, font, firstName, lastName, mode = 'name') => { + if (type !== 'SELECT') return null; + + const selectedFont = signatureOptions.find(opt => opt.style === font); + const fontClass = `${selectedFont?.style || 'print'}`; + console.log("fontClass",fontClass) + + + if (mode === 'initials') { + const initials = `${(firstName?.[0] ?? '')}${(lastName?.[0] ?? '')}`.toUpperCase(); + return ( +
+ {initials} +
+ ); + } + + const fullName = `${firstName ?? ''} ${lastName ?? ''}`.trim(); + return ( +
+
{fullName}
+
+ ); + }; + + + // Fixed renderContent function for SELECT case with proper radio button styling const renderContent = () => { switch (activeTab) { - case 'TYPED': + case 'SELECT': return ( -
-
-

Choose Signature Style

-

Select from pre-designed signature styles

+
+
+

Choose Signature Style

+

Select from pre-designed signature styles

-
-
+
+
{signatureOptions.map((option) => ( -
+
setSelectedSignature({ id: option.id, style: option.style })} + > + {/* Custom Radio Button */} +
+
+ {selectedSignature.id === option.id && ( +
+ )} +
+
+ + {/* Hidden native radio button for accessibility */} setSelectedSignature({ id: option.id, style: option.style })} - className="w-4 h-4 text-blue-600 focus:ring-blue-500" + className="sr-only" /> -
-
-
Signed by:
-
+
+
Signed by:
+
= ({ }`}> {fullName || option.name}
-
+
dca3e646-62cd-4cc...
-
-
DS
-
+
DS
+
= ({ case 'DRAW': return ( - + +
); case 'UPLOAD': return (
-
-

Upload Signature

+
+

Upload Signature

+

Upload your signature images

-
-
-
- {/* Upload 1 */} -
- - - handleImageChange(e, setFile1, setPreview1, "fullSignature")} - /> -
+
+
+ {/* Upload 1 - Full Signature */} +
+ + + handleImageChange(e, setFile1, setPreview1, "fullSignature")} + /> +
- {/* Upload 2 */} -
- - - handleImageChange(e, setFile2, setPreview2, "initials")} - /> -
+ {/* Upload 2 - Initials */} +
+ + + handleImageChange(e, setFile2, setPreview2, "initials")} + />
); - case 'SAVED': + + case 'SAVED': return (
@@ -457,16 +714,16 @@ const AddSignatureModal: React.FC = ({ {/* Signature Image or Fallback Text */}
- {signature?.signatureBlob ? ( - Signature - ) : ( -
No Signature
- )} -
+ {signature?.signatureType !== 'SELECT' ? ( + Signature + ) : ( + renderSignatureText(signature?.signatureType, signature?.font, signature?.firstName, signature?.lastName, 'name') + )} +
{/* Name */}
@@ -484,16 +741,16 @@ const AddSignatureModal: React.FC = ({ {/* Initials Image */}
- {signature?.initialsBlob ? ( - Initials - ) : ( -
No Initials
- )} -
+ {signature?.signatureType !== 'SELECT' ? ( + Initials + ) : ( + renderSignatureText(signature?.signatureType, signature?.font, signature?.firstName, signature?.lastName, 'initials') + )} +
{/* Optional initials hash or font */}
@@ -513,7 +770,6 @@ const AddSignatureModal: React.FC = ({ return null; } }; - const getUserDetails = async () => { try { // let res = await axios.get(`/api/user/details`, { @@ -536,122 +792,250 @@ const AddSignatureModal: React.FC = ({ }, []); return ( -
-
- - {/* Sidebar Navigation */} -
- {/* Header */} -
-
-
-

Add Signature Sign

-

Create your digital signature

+
+
+ + {/* Mobile Navigation - Fixed at top on mobile only */} +
+ + {/* Scrollable area (mobile: inputs + nav + content) */} +
+
+

+ Add Signature +

+
+ {/* Mobile Inputs */} +
+
+
+ + setFullName(e.target.value)} + placeholder="Enter full name" + className="w-full px-2 py-2 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setInitials(e.target.value)} + placeholder="Enter initials" + className="w-full px-2 py-2 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+
+ + {/* Mobile Navigation - Sticky inside scroll */} +
+
+ +
+
+ + {/* Main Content */} +
+
+ {renderContent()} +
+
-
-
- - {/* Form Inputs */} -
-
-
-
-
- - {/* Navigation Menu */} -
-
*/} + + {/* Footer Buttons - Fixed */} +
- ))} - + +
+
+ + + {/* Desktop Sidebar Navigation */} +
+ {/* Header - Fixed */} +
+

+ Add Signature +

+

+ Create your digital signature +

+
+ + {/* Form Inputs - Fixed */} +
+
+
+ + setFullName(e.target.value)} + placeholder="Enter full name" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + setInitials(e.target.value)} + placeholder="Enter initials" + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+
+ + {/* Navigation Menu - Fixed */} +
+ +
+ + {/* Spacer for desktop */} +
+
+ + {/* Main Content Area */} +
+ {/* Content - Scrollable */} +
+
+ {renderContent()} +
+
+ + {/* Checkbox - Fixed */} + {/*
+ +
*/} + + {/* Footer Buttons - Fixed */} +
+ + +
+
-
- - {/* Main Content Area */} -
- {/* Header with close button */} - - {/* Content */} -
- {/* */} - {renderContent()} -
- - {/* Footer Buttons in Main Area */} -
- - -
- -
-
-
); }; diff --git a/src/components/styles/style.css b/src/components/styles/style.css index 21f3e0e..310944f 100644 --- a/src/components/styles/style.css +++ b/src/components/styles/style.css @@ -30,13 +30,13 @@ html { .auth-ui-con .container-l { position: relative; - width: 850px; - height: 550px; + width: 1280px; + height: 599px; background: #fff; /* margin-top: 20px; */ margin-left: 50%; transform: translateX(-50%); - border-radius: 30px; + /* border-radius: 30px; */ box-shadow: 0 0 30px rgba(0, 0, 0, .2); overflow-y: auto; overflow-x: hidden;