Merge pull request 'jai' (#21) from jai into main
Reviewed-on: http://git.hicglobalsolutions.in/BhawanSingh/SME-Web/pulls/21
This commit is contained in:
commit
f3b1fc021f
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -529,13 +529,13 @@ const DocumentSubmissionOptions = () => {
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="text-center pt-2 max-h-[25vh]">
|
<div className="text-center pt-1 max-h-[25vh]">
|
||||||
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
|
<div className="w-20 h-20 bg-gradient-to-br from-teal-500 to-green-400 rounded-full flex items-center justify-center mt-3 mx-auto shadow-lg ">
|
||||||
<FaCheck className="text-white text-4xl" />
|
<FaCheck className="text-white text-4xl" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className='text-center py-1'>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-800 mb-2">Document Send Successfully!</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className='border border-b border-gray-50'></div>
|
<div className='border border-b border-gray-50'></div>
|
||||||
<div className=' min-h-[30vh]'>
|
<div className=' min-h-[30vh]'>
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,7 @@ const ProcessStepOne = () => {
|
||||||
className='flex items-center gap-3'>
|
className='flex items-center gap-3'>
|
||||||
|
|
||||||
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button
|
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && <button
|
||||||
onClick={() => handleNextClick(false)}
|
onClick={() => handleNextClick(true)}
|
||||||
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
|
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition cursor-pointer"
|
||||||
>
|
>
|
||||||
Save & Close
|
Save & Close
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
removeRecipientAsync,
|
removeRecipientAsync,
|
||||||
removeSelectedRecipient,
|
removeSelectedRecipient,
|
||||||
// setSigningOrder,
|
|
||||||
updateRecipientOrder,
|
updateRecipientOrder,
|
||||||
|
TRANSACTION_TEMPLATE_TOGGLE,
|
||||||
|
updateRecipient,
|
||||||
|
recipientType,
|
||||||
} from "../../../store/slices/selectedRecipientsSlice";
|
} from "../../../store/slices/selectedRecipientsSlice";
|
||||||
import { AppDispatch, RootState } from "../../../store/store";
|
import { AppDispatch, RootState } from "../../../store/store";
|
||||||
|
|
||||||
import { ImBin } from "react-icons/im";
|
import { ImBin } from "react-icons/im";
|
||||||
import { FaUsersLine } from "react-icons/fa6";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
|
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
|
||||||
import { IoCloseOutline } from "react-icons/io5";
|
import { IoCloseOutline } from "react-icons/io5";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { FiEdit } from "react-icons/fi";
|
import { FiEdit } from "react-icons/fi";
|
||||||
import { RiPencilFill } from "react-icons/ri";
|
import { FaUsersLine } from "react-icons/fa6";
|
||||||
|
|
||||||
const getInitials = (name: string) => {
|
const getInitials = (name: string) => {
|
||||||
if (!name) return "";
|
if (!name) return "";
|
||||||
|
|
@ -29,84 +29,212 @@ type CardsData = {
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
getMinRoutingOrder: () => number;
|
getMinRoutingOrder: () => number;
|
||||||
editRecipient: any;
|
editRecipient: any;
|
||||||
setEditRecipient: (recipient: any) => void; // Replace `any` with the actual type if known
|
setEditRecipient: (recipient: any) => void;
|
||||||
setEditCase: (string: any) => void;
|
setEditCase: (string: any) => void;
|
||||||
transactionTemplateToggle: string
|
transactionTemplateToggle: string;
|
||||||
editRecipient: any
|
editRecipientCurrentEmail: string;
|
||||||
setEditRecipientCurrentEmail: any,
|
setEditRecipientCurrentEmail: (email: string) => void;
|
||||||
editRecipientCurrentEmail: string
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RecipientCards = ({
|
||||||
const RecipientCards = ({ userEmail, getMinRoutingOrder, setEditRecipient, setEditCase, transactionTemplateToggle, editRecipient, editCase, setEditRecipientCurrentEmail, editRecipientCurrentEmail }: CardsData) => {
|
setEditRecipient,
|
||||||
|
setEditCase,
|
||||||
|
transactionTemplateToggle,
|
||||||
|
editRecipient,
|
||||||
|
editRecipientCurrentEmail,
|
||||||
|
setEditRecipientCurrentEmail,
|
||||||
|
}: CardsData) => {
|
||||||
const templateId = useParams<{ templateId: string }>().templateId;
|
const templateId = useParams<{ templateId: string }>().templateId;
|
||||||
let { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
let { accountId, transactionId } = useParams<{ accountId: string; transactionId: string }>();
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
if (!transactionId) {
|
if (!transactionId) {
|
||||||
transactionId = templateId;
|
transactionId = templateId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
console.log("recipients.length", recipients)
|
|
||||||
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
|
|
||||||
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
|
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
|
||||||
|
|
||||||
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
|
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
|
||||||
|
const [editingField, setEditingField] = useState<{ recipientId: string; fieldName: string } | null>(null);
|
||||||
|
const [localChanges, setLocalChanges] = useState<Record<string, any>>({});
|
||||||
|
const [errors, setErrors] = useState<Record<string, { hostName: boolean; hostEmail: boolean }>>({});
|
||||||
|
|
||||||
|
const getRecipientIdentifier = (recipient: any) => {
|
||||||
|
return recipient.id || recipient.uuid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditable = (recipient: any) => {
|
||||||
|
return transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
|
||||||
|
? true
|
||||||
|
: recipient.newOne || recipient.allowSenderToEdit;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInPersonSignerFields = (recipient: any, updatedFields: any) => {
|
||||||
|
if (recipient.type !== recipientType.INPERSONSIGNER) return true;
|
||||||
|
const hostName = updatedFields.hostName ?? getCurrentValue(recipient, "hostName");
|
||||||
|
const hostEmail = updatedFields.hostEmail ?? getCurrentValue(recipient, "hostEmail");
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
const newErrors = { hostName: !hostName, hostEmail: !hostEmail };
|
||||||
|
setErrors((prev) => ({ ...prev, [identifier]: newErrors }));
|
||||||
|
return !newErrors.hostName && !newErrors.hostEmail;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldFocus = (recipientId: string, fieldName: string) => {
|
||||||
|
setEditingField({ recipientId, fieldName });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldBlur = (recipient: any, fieldName: string, value: any) => {
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
const updatedRecipient = { ...recipient, [fieldName]: value, uuid: identifier };
|
||||||
|
|
||||||
|
if (recipient.type === recipientType.INPERSONSIGNER && (fieldName === "hostName" || fieldName === "hostEmail")) {
|
||||||
|
if (!validateInPersonSignerFields(recipient, { [fieldName]: value })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(updateRecipient(updatedRecipient));
|
||||||
|
setLocalChanges((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${identifier}_${fieldName}`]: undefined,
|
||||||
|
}));
|
||||||
|
setEditingField(null);
|
||||||
|
setErrors((prev) => ({ ...prev, [identifier]: { hostName: false, hostEmail: false } }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFieldChange = (recipient: any, fieldName: string, value: any) => {
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
setLocalChanges((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[`${identifier}_${fieldName}`]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentValue = (recipient: any, fieldName: string) => {
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
const localValue = localChanges[`${identifier}_${fieldName}`];
|
||||||
|
return localValue !== undefined ? localValue : recipient[fieldName];
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasValue = (recipient: any, fieldName: string) => {
|
||||||
|
const currentValue = getCurrentValue(recipient, fieldName);
|
||||||
|
return currentValue && currentValue.toString().trim() !== "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEditableField = (
|
||||||
|
recipient: any,
|
||||||
|
fieldName: string,
|
||||||
|
placeholder: string,
|
||||||
|
className = "",
|
||||||
|
type = "text"
|
||||||
|
) => {
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
const currentValue = getCurrentValue(recipient, fieldName);
|
||||||
|
const isEditing = editingField?.recipientId === identifier && editingField?.fieldName === fieldName;
|
||||||
|
const showValue = hasValue(recipient, fieldName);
|
||||||
|
const error = errors[identifier]?.[fieldName];
|
||||||
|
const editable = isEditable(recipient);
|
||||||
|
|
||||||
|
if (isEditing && editable) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={currentValue || ""}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoFocus
|
||||||
|
onFocus={() => handleFieldFocus(identifier, fieldName)}
|
||||||
|
onBlur={(e) => handleFieldBlur(recipient, fieldName, e.target.value)}
|
||||||
|
onChange={(e) => handleFieldChange(recipient, fieldName, e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full px-3 py-1 border ${error ? "border-red-500 placeholder-red-500 text-xs" : "border-gray-300"
|
||||||
|
} rounded-lg text-sm focus:outline-none focus:ring-0 focus:ring-gray-300 ${className}`}
|
||||||
|
/>
|
||||||
|
{/* {error && (
|
||||||
|
<span className="text-red-500 text-xs absolute left-0">{`${placeholder} is required`}</span>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
onClick={editable ? () => handleFieldFocus(identifier, fieldName) : undefined}
|
||||||
|
className={`${editable
|
||||||
|
? "cursor-pointer hover:bg-gray-100 rounded px-1 transition-colors"
|
||||||
|
: "cursor-not-allowed"
|
||||||
|
} ${showValue ? "text-gray-800" : "text-gray-400 italic"} ${className}`}
|
||||||
|
title={editable ? "Click to edit" : "Editing disabled"}
|
||||||
|
>
|
||||||
|
{showValue ? currentValue : placeholder}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOrderChange = (id: string, value: number) => {
|
const handleOrderChange = (id: string, value: number) => {
|
||||||
dispatch(updateRecipientOrder({ id, routingOrder: value }));
|
dispatch(updateRecipientOrder({ id, routingOrder: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditRecipient = (recipient) => {
|
const handleEditRecipient = (recipient: any) => {
|
||||||
|
setEditCase("Edit");
|
||||||
|
setEditRecipient(recipient);
|
||||||
|
setEditRecipientCurrentEmail(recipient.id || recipient.uuid);
|
||||||
|
};
|
||||||
|
|
||||||
const provideOwnTags = userEmail === recipient.email && (recipient.routingOrder ?? 1) === getMinRoutingOrder() ? true : false;
|
const handleRemoveRecipient = (recipient: any) => {
|
||||||
// alert(JSON.stringify(recipient))
|
|
||||||
setEditCase('Edit')
|
|
||||||
setEditRecipient({...recipient, provideOwnTags})
|
|
||||||
|
|
||||||
if (recipient.uuid) {
|
|
||||||
setEditRecipientCurrentEmail(recipient.uuid)
|
|
||||||
} else {
|
|
||||||
setEditRecipientCurrentEmail(recipient.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveRecipient = (recipient) => {
|
|
||||||
if (!accountId || !transactionId) return;
|
if (!accountId || !transactionId) return;
|
||||||
// dispatch(removeSelectedRecipient(recipient))
|
|
||||||
// dispatch(removeRecipientAsync([{
|
|
||||||
// recipientId: recipient.id,
|
|
||||||
// email: recipient.email,
|
|
||||||
// accountId: accountId,
|
|
||||||
// transactionId: transactionId,
|
|
||||||
// }]))
|
|
||||||
// =======
|
|
||||||
if (recipient.uuid) {
|
if (recipient.uuid) {
|
||||||
dispatch(removeSelectedRecipient(recipient))
|
dispatch(removeSelectedRecipient(recipient));
|
||||||
} else {
|
} else {
|
||||||
dispatch(removeRecipientAsync({
|
dispatch(
|
||||||
|
removeRecipientAsync({
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
transactionId: transactionId,
|
transactionId: transactionId,
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipients.length === 2) {
|
if (recipients.length === 2) {
|
||||||
let leftRecipient = recipients.filter((obj) => {
|
const leftRecipient = recipients.filter((obj) => {
|
||||||
if ("id" in obj && "id" in recipient) {
|
if ("id" in obj && "id" in recipient) {
|
||||||
if (obj.id !== recipient.id) return true
|
return obj.id !== recipient.id;
|
||||||
}
|
}
|
||||||
if ("email" in obj && "email" in recipient) {
|
if ("email" in obj && "email" in recipient) {
|
||||||
if (obj.email !== recipient.email) return true
|
return obj.email !== recipient.email;
|
||||||
}
|
}
|
||||||
return false
|
return false;
|
||||||
})
|
});
|
||||||
dispatch(updateRecipientOrder({ id: leftRecipient[0].id || leftRecipient[0].email, routingOrder: 1 }));
|
dispatch(
|
||||||
|
updateRecipientOrder({ id: leftRecipient[0].id || leftRecipient[0].email, routingOrder: 1 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeChange = (recipient: any, type: string) => {
|
||||||
|
const identifier = getRecipientIdentifier(recipient);
|
||||||
|
const updatedRecipient = { ...recipient, type, uuid: identifier };
|
||||||
|
|
||||||
|
if (type === recipientType.SIGNER || type === recipientType.CC) {
|
||||||
|
updatedRecipient.hostName = "";
|
||||||
|
updatedRecipient.hostEmail = "";
|
||||||
|
setErrors((prev) => ({ ...prev, [identifier]: { hostName: false, hostEmail: false } }));
|
||||||
|
} else if (type === recipientType.INPERSONSIGNER) {
|
||||||
|
if (!validateInPersonSignerFields(recipient, updatedRecipient)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(updateRecipient(updatedRecipient));
|
||||||
|
setEditingField(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
{recipients.length > 0 ? (
|
{recipients.length > 0 ? (
|
||||||
|
|
@ -116,23 +244,8 @@ const RecipientCards = ({ userEmail, getMinRoutingOrder, setEditRecipient, setEd
|
||||||
<h3 className="text-sm font-semibold text-gray-700">
|
<h3 className="text-sm font-semibold text-gray-700">
|
||||||
Recipients ({recipients.length})
|
Recipients ({recipients.length})
|
||||||
</h3>
|
</h3>
|
||||||
{/* <label className="flex items-center cursor-pointer select-none">
|
<label
|
||||||
<span className="mr-2 text-xs font-medium text-gray-600">Signing Order</span>
|
className="flex items-center gap-2 cursor-pointer select-none"
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={signingOrder}
|
|
||||||
onChange={() => dispatch(setSigningOrder(!signingOrder))}
|
|
||||||
className="sr-only peer"
|
|
||||||
/>
|
|
||||||
<div className="w-9 h-5 bg-gray-300 rounded-full peer-checked:bg-[var(--primary)] relative transition-colors duration-300">
|
|
||||||
<span
|
|
||||||
className={`absolute left-1 top-1/2 w-3.5 h-3.5 bg-white rounded-full shadow transform transition-transform duration-300 -translate-y-1/2 ${signingOrder ? "translate-x-4" : "translate-x-0"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label> */}
|
|
||||||
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer select-none"
|
|
||||||
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
|
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
|
||||||
>
|
>
|
||||||
<span className="text-xs font-medium text-gray-600">Signing Order</span>
|
<span className="text-xs font-medium text-gray-600">Signing Order</span>
|
||||||
|
|
@ -145,93 +258,191 @@ const RecipientCards = ({ userEmail, getMinRoutingOrder, setEditRecipient, setEd
|
||||||
{recipients.map((recipient, idx) => (
|
{recipients.map((recipient, idx) => (
|
||||||
<div
|
<div
|
||||||
key={recipient.email + idx}
|
key={recipient.email + idx}
|
||||||
className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail ? "bg-neutral-100 opacity-40 " : ""}`}
|
className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors ${(recipient.uuid || recipient.id) === editRecipientCurrentEmail
|
||||||
|
? "bg-neutral-100 opacity-40"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{/* {signingOrder && ( */}
|
{recipients.length > 1 ? (
|
||||||
{recipients.length > 1 ? <div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={recipient.routingOrder ? recipient.routingOrder : ""}
|
value={recipient.routingOrder || 1}
|
||||||
// min={1}
|
min={1}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
let val = e.target.value.replace(/^0+(?=\d)/, "");
|
handleOrderChange(
|
||||||
console.log(val, "Line 155")
|
recipient.id || recipient.email,
|
||||||
handleOrderChange(recipient.id || recipient.email, Number(val))
|
Number(e.target.value)
|
||||||
}}
|
)
|
||||||
|
}
|
||||||
className="w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400 bg-slate-100"
|
className="w-10 h-6 text-center rounded text-sm text-gray-600 focus:outline-none focus:ring-[var(--primary)] border border-dashed border-gray-400 bg-slate-100"
|
||||||
/>
|
/>
|
||||||
{/* <RiPencilFill className="absolute top-1.5 right-0 text-[14px] text-gray-500" /> */}
|
|
||||||
</div>
|
</div>
|
||||||
: <input
|
) : (
|
||||||
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled
|
disabled
|
||||||
value={recipient.routingOrder}
|
value={recipient.routingOrder}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
|
handleOrderChange(
|
||||||
|
recipient.id || recipient.email,
|
||||||
|
Number(e.target.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
|
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
|
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
|
||||||
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-semibold text-gray-800 truncate text-sm">
|
<p className="font-semibold text-gray-800 truncate text-xs flex items-center gap-0.5">
|
||||||
{recipient.firstName} {recipient.lastName || ""}
|
{renderEditableField(recipient, "firstName", "First Name")}
|
||||||
|
{renderEditableField(recipient, "lastName", "Last Name")}
|
||||||
|
{transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES && (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="text-gray-400 ">| Permissions granted :</span>
|
||||||
|
<FiEdit
|
||||||
|
className={`cursor-pointer h-3 w-3 ${recipient.allowSenderToEdit
|
||||||
|
? "text-blue-500"
|
||||||
|
: "text-gray-400"
|
||||||
|
} hover:text-blue-800`}
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(
|
||||||
|
updateRecipient({
|
||||||
|
...recipient,
|
||||||
|
allowSenderToEdit: !recipient.allowSenderToEdit,
|
||||||
|
uuid: getRecipientIdentifier(recipient),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ImBin
|
||||||
|
className={`cursor-pointer h-3 w-3 ${recipient.allowSenderToDelete
|
||||||
|
? "text-red-500"
|
||||||
|
: "text-gray-400"
|
||||||
|
} hover:text-red-700`}
|
||||||
|
onClick={() =>
|
||||||
|
dispatch(
|
||||||
|
updateRecipient({
|
||||||
|
...recipient,
|
||||||
|
allowSenderToDelete: !recipient.allowSenderToDelete,
|
||||||
|
uuid: getRecipientIdentifier(recipient),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 truncate">
|
<p className="text-xs text-gray-500 truncate flex items-center gap-1">
|
||||||
{recipient.email} | {recipient.type} {recipient.roleName ? `| Role : ${recipient.roleName}` : ""}
|
{renderEditableField(recipient, "email", "Recipient Email", "", "email")} |
|
||||||
|
<span
|
||||||
|
onClick={
|
||||||
|
isEditable(recipient)
|
||||||
|
? () => handleFieldFocus(getRecipientIdentifier(recipient), "type")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className={`${isEditable(recipient)
|
||||||
|
? "cursor-pointer hover:bg-gray-100"
|
||||||
|
: "cursor-not-allowed"
|
||||||
|
} rounded px-1 transition-colors`}
|
||||||
|
>
|
||||||
|
{recipient.type}
|
||||||
|
</span>
|
||||||
|
{(hasValue(recipient, "roleName") ||
|
||||||
|
transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES) && (
|
||||||
|
<span>
|
||||||
|
{" | Role: "}
|
||||||
|
{renderEditableField(recipient, "roleName", "Role name")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{recipient.type === recipientType.INPERSONSIGNER && (
|
||||||
|
<>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span>{" | Host Name: "}</span>
|
||||||
|
{renderEditableField(recipient, "hostName", "Host Name")}
|
||||||
|
<span>{" | Host Email: "}</span>
|
||||||
|
{renderEditableField(recipient, "hostEmail", "Host Email", "", "email")}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
{editingField?.recipientId === getRecipientIdentifier(recipient) &&
|
||||||
|
editingField?.fieldName === "type" &&
|
||||||
|
isEditable(recipient) && (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<label className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
|
||||||
|
value={recipientType.SIGNER}
|
||||||
|
checked={recipient.type === recipientType.SIGNER}
|
||||||
|
onChange={() =>
|
||||||
|
handleTypeChange(recipient, recipientType.SIGNER)
|
||||||
|
}
|
||||||
|
onBlur={() => setEditingField(null)}
|
||||||
|
className="accent-[var(--primary)]"
|
||||||
|
disabled={!isEditable(recipient)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">Signer</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
|
||||||
|
value={recipientType.INPERSONSIGNER}
|
||||||
|
checked={recipient.type === recipientType.INPERSONSIGNER}
|
||||||
|
onChange={() =>
|
||||||
|
handleTypeChange(recipient, recipientType.INPERSONSIGNER)
|
||||||
|
}
|
||||||
|
onBlur={() => setEditingField(null)}
|
||||||
|
className="accent-[var(--primary)]"
|
||||||
|
disabled={!isEditable(recipient)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">In Person Signer</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`recipient-type-${getRecipientIdentifier(recipient)}`}
|
||||||
|
value={recipientType.CC}
|
||||||
|
checked={recipient.type === recipientType.CC}
|
||||||
|
onChange={() =>
|
||||||
|
handleTypeChange(recipient, recipientType.CC)
|
||||||
|
}
|
||||||
|
onBlur={() => setEditingField(null)}
|
||||||
|
className="accent-[var(--primary)]"
|
||||||
|
disabled={!isEditable(recipient)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">CC</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{signType !== "self-sign" && (
|
{signType !== "self-sign" && (
|
||||||
<div className="flex justify-between gap-2 items-center">
|
<div className="flex justify-between gap-2 items-center">
|
||||||
{/* Edit Button */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Edit recipient"
|
|
||||||
aria-label={`Edit recipient ${recipient.firstName}`}
|
|
||||||
onClick={() => handleEditRecipient(recipient)}
|
|
||||||
disabled={
|
|
||||||
// For templates: always enabled (never disabled)
|
|
||||||
['templates'].includes(transactionTemplateToggle)
|
|
||||||
? false
|
|
||||||
: // For transactions: disabled if not newOne AND not allowed to edit
|
|
||||||
(!recipient.newOne && !recipient.allowSenderToEdit)
|
|
||||||
}
|
|
||||||
className="text-gray-400 hover:text-blue-600 transition-colors cursor-pointer disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<FiEdit size={16} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Delete Button */}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Remove recipient"
|
title="Remove recipient"
|
||||||
aria-label={`Remove recipient ${recipient.firstName}`}
|
aria-label={`Remove recipient ${recipient.firstName}`}
|
||||||
onClick={() => handleRemoveRecipient(recipient)}
|
onClick={() => handleRemoveRecipient(recipient)}
|
||||||
disabled={
|
disabled={
|
||||||
// For templates: always enabled (never disabled)
|
transactionTemplateToggle === TRANSACTION_TEMPLATE_TOGGLE.TEMPLATES
|
||||||
['templates'].includes(transactionTemplateToggle)
|
|
||||||
? false
|
? false
|
||||||
: // For transactions: disabled if not newOne AND not allowed to delete
|
: !recipient.newOne && !recipient.allowSenderToDelete
|
||||||
(!recipient.newOne && !recipient.allowSenderToDelete)
|
|
||||||
}
|
}
|
||||||
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50"
|
className="text-gray-400 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ImBin size={16} />
|
<ImBin size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -240,26 +451,22 @@ const RecipientCards = ({ userEmail, getMinRoutingOrder, setEditRecipient, setEd
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{signingOrderModal.open &&
|
{signingOrderModal.open && (
|
||||||
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center">
|
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center">
|
||||||
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto">
|
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto">
|
||||||
<div className="p-2 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
|
<div className="p-2 rounded-t-lg text-white flex justify-between items-center bg-[var(--primary)]">
|
||||||
<p className="font-semibold">Signing Order</p>
|
<p className="font-semibold">Signing Order</p>
|
||||||
<div
|
<div
|
||||||
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
|
onClick={() => 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"
|
||||||
|
>
|
||||||
<IoCloseOutline />
|
<IoCloseOutline />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SigningOrderDiagram signers={recipients} />
|
<SigningOrderDiagram signers={recipients} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const AsideSection = ({
|
||||||
e.preventDefault(); // Prevent default anchor behavior
|
e.preventDefault(); // Prevent default anchor behavior
|
||||||
onSubSectionClick(subSection.id);
|
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
|
activeSubSection === subSection.id
|
||||||
? "bg-blue-50 text-blue-600 border-l-4 border-blue-600 shadow-sm"
|
? "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"
|
: "text-gray-600 hover:text-gray-800 hover:bg-gray-50"
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ const ContactForm = ({ onSubmit, loading }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ms-2 space-y-2 max-w-[60vw]">
|
<div className="px-3 py-3 space-y-2 max-w-[60vw] bg-white rounded-lg">
|
||||||
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
|
|
||||||
|
|
@ -158,9 +158,9 @@ const CreateAndInvite = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ms-2 space-y-3">
|
<div className=" space-y-1.5">
|
||||||
{/* Create Section */}
|
{/* Create Section */}
|
||||||
<section className="border-gray-200 border-b">
|
<section className="border-gray-200 border-b bg-white px-3 py-3 rounded-md">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
|
||||||
<div className="flex flex-col mb-6">
|
<div className="flex flex-col mb-6">
|
||||||
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
|
||||||
|
|
@ -197,7 +197,7 @@ const CreateAndInvite = ({
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Invite Section */}
|
{/* Invite Section */}
|
||||||
<section className="border-gray-300">
|
<section className="border-gray-200 border-b bg-white px-3 py-3 rounded-lg">
|
||||||
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
|
||||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||||
{/* Account Name Dropdown */}
|
{/* Account Name Dropdown */}
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ const Setting = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-2">
|
||||||
{currentSubSections.map((subSection) => (
|
{currentSubSections.map((subSection) => (
|
||||||
<div
|
<div
|
||||||
key={subSection.id}
|
key={subSection.id}
|
||||||
|
|
@ -430,7 +430,7 @@ const Setting = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Responsive Layout */}
|
{/* Responsive Layout */}
|
||||||
<div className="flex-1 min-h-0 flex flex-col lg:flex-row px-4 md:px-6 lg:px-8 mt-3 gap-6">
|
<div className="flex-1 min-h-0 flex flex-col lg:flex-row px-4 md:px-6 lg:px-7 pt-2 lg:gap-6 ">
|
||||||
|
|
||||||
{/* Sidebar (hidden on small, shown on lg) */}
|
{/* Sidebar (hidden on small, shown on lg) */}
|
||||||
{activeTab === 'userProfile' && (
|
{activeTab === 'userProfile' && (
|
||||||
|
|
@ -448,8 +448,8 @@ const Setting = () => {
|
||||||
|
|
||||||
{/* Sidebar as horizontal nav (only on mobile) */}
|
{/* Sidebar as horizontal nav (only on mobile) */}
|
||||||
{activeTab === 'userProfile' && (
|
{activeTab === 'userProfile' && (
|
||||||
<section className="lg:hidden mb-4">
|
<section className="lg:hidden mb-2">
|
||||||
<div className="flex overflow-x-auto no-scrollbar gap-4 border-b pb-2">
|
<div className="flex overflow-x-auto no-scrollbar gap-4 border-b border-gray-200 pb-2">
|
||||||
{getSubSections(activeTab).map((sub) => (
|
{getSubSections(activeTab).map((sub) => (
|
||||||
<button
|
<button
|
||||||
key={sub.id}
|
key={sub.id}
|
||||||
|
|
|
||||||
|
|
@ -238,10 +238,11 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef1}
|
ref={canvasRef1}
|
||||||
className="w-full h-51 cursor-crosshair"
|
className="w-full h-51 cursor-crosshair"
|
||||||
onMouseDown={handleMouseDown}
|
onPointerDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onPointerMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onPointerUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseUp}
|
onPointerLeave={handleMouseUp}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -253,10 +254,11 @@ const DrawSignatureCanvas: React.FC<Props> = ({ onSignatureChange, initialSignat
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef2}
|
ref={canvasRef2}
|
||||||
className="w-full h-51 cursor-crosshair"
|
className="w-full h-51 cursor-crosshair"
|
||||||
onMouseDown={handleMouseDown}
|
onPointerDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onPointerMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onPointerUp={handleMouseUp}
|
||||||
onMouseLeave={handleMouseUp}
|
onPointerLeave={handleMouseUp}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ interface UserProfileProps {
|
||||||
|
|
||||||
const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-md shadow-sm flex flex-col sm:flex-row items-center sm:items-start w-full p-6 gap-6">
|
<div className="bg-white rounded-md shadow-sm flex flex-col sm:flex-row items-center sm:items-start w-full p-6 lg:gap-6 ">
|
||||||
{/* Profile Picture */}
|
{/* Profile Picture */}
|
||||||
<div className="flex-shrink-0 text-center">
|
<div className="flex-shrink-0 text-center">
|
||||||
<div className="w-20 h-20 bg-gradient-to-br from-[var(--primary)] to-blue-600 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
<div className="w-20 h-20 bg-gradient-to-br from-[var(--primary)] to-blue-600 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ interface Signature {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddSignatureModalProps {
|
interface AddSignatureModalProps {
|
||||||
// setShowAddSignature?: (value: boolean) => void;
|
setOpenSign?: (value: boolean) => void;
|
||||||
// getAllSignatures: () => Promise<void>;
|
getAllSignatures: () => Promise<void>;
|
||||||
// editingSignature?: Signature | null;
|
// editingSignature?: Signature | null;
|
||||||
|
|
||||||
recipientId?: string;
|
recipientId?: string;
|
||||||
|
|
@ -87,7 +87,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
signTagId
|
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 [fullName, setFullName] = useState('');
|
||||||
const [initials, setInitials] = useState('');
|
const [initials, setInitials] = useState('');
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
|
@ -130,13 +131,15 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
// Store the preview URL
|
// Store the preview URL
|
||||||
const [preview1, setPreview1] = useState<string | null>(null);
|
const [preview1, setPreview1] = useState<string | null>(null);
|
||||||
const [preview2, setPreview2] = useState<string | null>(null);
|
const [preview2, setPreview2] = useState<string | null>(null);
|
||||||
|
const [checked, setChecked] = useState(false);
|
||||||
|
const toggleCheckbox = () => setChecked(!checked);
|
||||||
|
|
||||||
const [preSaveSignatures, setPreSaveSignatures] = useState([])
|
const [preSaveSignatures, setPreSaveSignatures] = useState([])
|
||||||
|
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ id: 'TYPED', label: 'TYPED', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
{ id: 'SELECT', label: 'SELECT', icon: <GrCheckboxSelected className="w-4 h-4" /> },
|
||||||
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
{ id: 'DRAW', label: 'DRAW', icon: <AiOutlineEdit className="w-5 h-5" /> },
|
||||||
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
{ id: 'UPLOAD', label: 'UPLOAD', icon: <AiOutlineUpload className="w-5 h-5" /> },
|
||||||
{ id: 'SAVED', label: 'SAVED', icon: <CiSaveDown1 className="w-5 h-5" /> },
|
{ id: 'SAVED', label: 'SAVED', icon: <CiSaveDown1 className="w-5 h-5" /> },
|
||||||
|
|
@ -314,32 +317,283 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const renderContent = () => {
|
||||||
|
// switch (activeTab) {
|
||||||
|
// case 'SELECT':
|
||||||
|
// return (
|
||||||
|
// <div className="h-full flex flex-col ">
|
||||||
|
// <div className="mb-6">
|
||||||
|
// <h2 className="text-xl font-semibold text-gray-900 mb-2">Choose Signature Style</h2>
|
||||||
|
// <p className="text-sm text-gray-600">Select from pre-designed signature styles</p>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="flex-1 overflow-y-auto">
|
||||||
|
// <div className="space-y-4">
|
||||||
|
// {signatureOptions.map((option) => (
|
||||||
|
// <div key={option.id} className="flex items-center space-x-4 px-2 py-1 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
||||||
|
// <input
|
||||||
|
// type="radio"
|
||||||
|
// id={`signature-${option.id}`}
|
||||||
|
// name="signature"
|
||||||
|
// checked={selectedSignature.id === option.id}
|
||||||
|
// onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
||||||
|
// className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||||
|
// />
|
||||||
|
// <div className="flex-1 grid grid-cols-2 gap-2">
|
||||||
|
// <div className=" border-gray-300 rounded-lg px-2 py-1 bg-white ">
|
||||||
|
// <div className="text-xs text-gray-500 mb-2">Signed by:</div>
|
||||||
|
// <div className={`text-lg ${option.style === 'cursive' ? 'font-serif italic' :
|
||||||
|
// option.style === 'script' ? 'font-mono' :
|
||||||
|
// option.style === 'elegant' ? 'font-serif' :
|
||||||
|
// option.style === 'modern' ? 'font-sans tracking-wide' :
|
||||||
|
// 'font-sans'
|
||||||
|
// }`}>
|
||||||
|
// {fullName || option.name}
|
||||||
|
// </div>
|
||||||
|
// <div className="text-xs text-gray-400 mt-2">
|
||||||
|
// dca3e646-62cd-4cc...
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// <div className=" rounded-lg px-2 py-2 bg-white ">
|
||||||
|
// <div className="text-xs text-gray-500 mb-2">DS</div>
|
||||||
|
// <div className={`text-2xl ${option.style === 'cursive' ? 'font-serif italic' :
|
||||||
|
// option.style === 'script' ? 'font-mono' :
|
||||||
|
// option.style === 'elegant' ? 'font-serif' :
|
||||||
|
// option.style === 'modern' ? 'font-sans tracking-wide' :
|
||||||
|
// 'font-sans'
|
||||||
|
// }`}>
|
||||||
|
// {initials || option.initials}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// case 'DRAW':
|
||||||
|
// return (
|
||||||
|
// <DrawSignatureCanvas
|
||||||
|
// onSignatureChange={handleSignatureChange}
|
||||||
|
// // initialSignatures={isEditMode ? signatures : undefined}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
|
||||||
|
// case 'UPLOAD':
|
||||||
|
// return (
|
||||||
|
// <div className="h-full flex flex-col">
|
||||||
|
// <div className="mb-6">
|
||||||
|
// <h2 className="text-xl font-semibold text-gray-900 mb-2">Upload Signature</h2>
|
||||||
|
// </div>
|
||||||
|
// <div className="flex-1 overflow-y-auto">
|
||||||
|
// <div>
|
||||||
|
// <div className="grid grid-cols-12 gap-6">
|
||||||
|
// {/* Upload 1 */}
|
||||||
|
// <div className="col-span-8">
|
||||||
|
// <label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
||||||
|
// <label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
|
// {preview1 ? (
|
||||||
|
// <img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
||||||
|
// ) : (
|
||||||
|
// <div className="text-center">
|
||||||
|
// <MdCloudUpload className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
||||||
|
// <p className="text-sm text-gray-500 mb-1">Click to upload or drag and drop</p>
|
||||||
|
// <p className="text-xs text-gray-400">PNG, JPG, GIF up to 10MB</p>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </label>
|
||||||
|
// <input
|
||||||
|
// id="upload1"
|
||||||
|
// type="file"
|
||||||
|
// accept="image/*"
|
||||||
|
// className="hidden"
|
||||||
|
// onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Upload 2 */}
|
||||||
|
// <div className="col-span-4">
|
||||||
|
// <label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
||||||
|
// <label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
|
// {preview2 ? (
|
||||||
|
// <img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
||||||
|
// ) : (
|
||||||
|
// <div className="text-center">
|
||||||
|
// <MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
||||||
|
// <p className="text-xs text-gray-500">Upload initials</p>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </label>
|
||||||
|
// <input
|
||||||
|
// id="upload2"
|
||||||
|
// type="file"
|
||||||
|
// accept="image/*"
|
||||||
|
// className="hidden"
|
||||||
|
// onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// case 'SAVED':
|
||||||
|
// return (
|
||||||
|
// <div className="h-full flex flex-col ">
|
||||||
|
// <div className="mb-6">
|
||||||
|
// <h2 className="text-xl font-semibold text-gray-900 mb-2">Saved Signature's</h2>
|
||||||
|
// {/* <p className="text-sm text-gray-600">Select from pre-designed signature styles</p> */}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="flex-1 overflow-y-auto">
|
||||||
|
// <div className="space-y-4">
|
||||||
|
// {preSaveSignatures.map((signature) => (
|
||||||
|
// <div key={signature?.id} className="grid grid-cols-12 gap-4 px-6 py-9 hover:bg-gray-50 transition-colors duration-150">
|
||||||
|
// {/* Signature Column */}
|
||||||
|
// <div className="col-span-5">
|
||||||
|
// <div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-blue-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
|
// <div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
|
// Signed by:
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Signature Image or Fallback Text */}
|
||||||
|
// <div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
||||||
|
// {signature?.signatureBlob ? (
|
||||||
|
// <img
|
||||||
|
// src={`data:${signature?.signatureMimeType || "image/png"};base64,${signature?.signatureBlob}`}
|
||||||
|
// className="h-[50px] w-[250px] object-contain"
|
||||||
|
// alt="Signature"
|
||||||
|
// />
|
||||||
|
// ) : (
|
||||||
|
// <div className="text-gray-400 italic text-sm">No Signature</div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Name */}
|
||||||
|
// <div className="text-xs text-gray-500 truncate">
|
||||||
|
// {signature?.signatureHash ?? ' '}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Initial Column */}
|
||||||
|
// <div className="col-span-5">
|
||||||
|
// <div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl">
|
||||||
|
// <div className="text-sm text-gray-600 font-medium mb-1">
|
||||||
|
// Initials
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Initials Image */}
|
||||||
|
// <div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
||||||
|
// {signature?.initialsBlob ? (
|
||||||
|
// <img
|
||||||
|
// src={`data:image/png;base64,${signature?.initialsBlob}`}
|
||||||
|
// className="h-[50px] object-contain"
|
||||||
|
// alt="Initials"
|
||||||
|
// />
|
||||||
|
// ) : (
|
||||||
|
// <div className="text-gray-400 italic text-sm">No Initials</div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Optional initials hash or font */}
|
||||||
|
// <div className="text-xs text-gray-500 truncate">
|
||||||
|
// {signature?.initialsHash || " "}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className={`text-center text-[24px]`} style={{fontFamily:selectedFont?.style}}>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullName = `${firstName ?? ''} ${lastName ?? ''}`.trim();
|
||||||
|
return (
|
||||||
|
<div className={`text-center text-[20px]`}style={{fontFamily:selectedFont?.style}}>
|
||||||
|
<div>{fullName}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Fixed renderContent function for SELECT case with proper radio button styling
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'TYPED':
|
case 'SELECT':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="mb-6">
|
<div className="mb-1 md:mb-4 lg:mb-6 flex-shrink-0">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Choose Signature Style</h2>
|
<h2 className="text-base md:text-lg lg:text-xl font-semibold text-gray-900 mb-1">Choose Signature Style</h2>
|
||||||
<p className="text-sm text-gray-600">Select from pre-designed signature styles</p>
|
<p className="text-xs md:text-sm text-gray-600 hidden md:block">Select from pre-designed signature styles</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 min-h-0 overflow-y-auto no-scrollbar">
|
||||||
<div className="space-y-4">
|
<div className="space-y-2 md:space-y-3 pb-2">
|
||||||
{signatureOptions.map((option) => (
|
{signatureOptions.map((option) => (
|
||||||
<div key={option.id} className="flex items-center space-x-4 px-2 py-1 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
|
<div
|
||||||
|
key={option.id}
|
||||||
|
className={`flex items-center space-x-2 md:space-x-3 p-2 md:p-3 border rounded-lg transition-all duration-200 cursor-pointer ${selectedSignature.id === option.id
|
||||||
|
? 'border-gray-100 shadow-sm'
|
||||||
|
: 'border-gray-200 hover:bg-gray-50 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedSignature({ id: option.id, style: option.style })}
|
||||||
|
>
|
||||||
|
{/* Custom Radio Button */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 md:w-3 md:h-3 rounded-full border-2 flex items-center justify-center transition-all duration-200 ${selectedSignature.id === option.id
|
||||||
|
? 'border-gray-300 bg-blue-600'
|
||||||
|
: 'border-gray-300 bg-white hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedSignature.id === option.id && (
|
||||||
|
<div className="w-2 h-2 md:w-3 md:h-2 rounded-full bg-blue-500"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden native radio button for accessibility */}
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id={`signature-${option.id}`}
|
id={`signature-${option.id}`}
|
||||||
name="signature"
|
name="signature"
|
||||||
checked={selectedSignature.id === option.id}
|
checked={selectedSignature.id === option.id}
|
||||||
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
|
||||||
className="w-4 h-4 text-blue-600 focus:ring-blue-500"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 grid grid-cols-2 gap-2">
|
|
||||||
<div className=" border-gray-300 rounded-lg px-2 py-1 bg-white ">
|
<div className="flex-1 min-w-0 grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-3">
|
||||||
<div className="text-xs text-gray-500 mb-2">Signed by:</div>
|
<div className="border border-gray-300 rounded-lg px-2 md:px-3 py-2 md:py-3 bg-white min-w-0">
|
||||||
<div className={`text-lg ${option.style === 'cursive' ? 'font-serif italic' :
|
<div className="text-xs text-gray-500 mb-1">Signed by:</div>
|
||||||
|
<div className={`text-sm md:text-base truncate ${option.style === 'cursive' ? 'font-serif italic' :
|
||||||
option.style === 'script' ? 'font-mono' :
|
option.style === 'script' ? 'font-mono' :
|
||||||
option.style === 'elegant' ? 'font-serif' :
|
option.style === 'elegant' ? 'font-serif' :
|
||||||
option.style === 'modern' ? 'font-sans tracking-wide' :
|
option.style === 'modern' ? 'font-sans tracking-wide' :
|
||||||
|
|
@ -347,13 +601,13 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
}`}>
|
}`}>
|
||||||
{fullName || option.name}
|
{fullName || option.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 mt-2">
|
<div className="text-xs text-gray-400 mt-1 truncate">
|
||||||
dca3e646-62cd-4cc...
|
dca3e646-62cd-4cc...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className=" rounded-lg px-2 py-2 bg-white ">
|
<div className="border border-gray-300 rounded-lg px-2 md:px-3 py-2 md:py-3 bg-white min-w-0">
|
||||||
<div className="text-xs text-gray-500 mb-2">DS</div>
|
<div className="text-xs text-gray-500 mb-1">DS</div>
|
||||||
<div className={`text-2xl ${option.style === 'cursive' ? 'font-serif italic' :
|
<div className={`text-lg md:text-xl truncate ${option.style === 'cursive' ? 'font-serif italic' :
|
||||||
option.style === 'script' ? 'font-mono' :
|
option.style === 'script' ? 'font-mono' :
|
||||||
option.style === 'elegant' ? 'font-serif' :
|
option.style === 'elegant' ? 'font-serif' :
|
||||||
option.style === 'modern' ? 'font-sans tracking-wide' :
|
option.style === 'modern' ? 'font-sans tracking-wide' :
|
||||||
|
|
@ -372,31 +626,33 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
case 'DRAW':
|
case 'DRAW':
|
||||||
return (
|
return (
|
||||||
|
<div className="h-full">
|
||||||
<DrawSignatureCanvas
|
<DrawSignatureCanvas
|
||||||
onSignatureChange={handleSignatureChange}
|
onSignatureChange={handleSignatureChange}
|
||||||
// initialSignatures={isEditMode ? signatures : undefined}
|
// initialSignatures={isEditMode ? signatures : undefined}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'UPLOAD':
|
case 'UPLOAD':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="mb-6">
|
<div className="mb-3 md:mb-4 lg:mb-6 flex-shrink-0">
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Upload Signature</h2>
|
<h2 className="text-base md:text-lg lg:text-xl font-semibold text-gray-900 mb-1">Upload Signature</h2>
|
||||||
|
<p className="text-xs md:text-sm text-gray-600 hidden md:block">Upload your signature images</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
<div>
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-3 md:gap-4 pb-2">
|
||||||
<div className="grid grid-cols-12 gap-6">
|
{/* Upload 1 - Full Signature */}
|
||||||
{/* Upload 1 */}
|
<div className="lg:col-span-8">
|
||||||
<div className="col-span-8">
|
<label className="block text-xs md:text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
|
<label htmlFor="upload1" className="block border-2 border-dashed border-gray-300 rounded-lg h-32 md:h-40 lg:h-48 xl:h-56 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
<label htmlFor="upload1" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
|
||||||
{preview1 ? (
|
{preview1 ? (
|
||||||
<img src={preview1} alt="Signature Preview" className="max-h-44 object-contain" />
|
<img src={preview1} alt="Signature Preview" className="max-h-full max-w-full object-contain p-2" />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center px-4">
|
||||||
<MdCloudUpload className="mx-auto h-12 w-12 text-gray-400 mb-3" />
|
<MdCloudUpload className="mx-auto h-6 w-6 md:h-8 md:w-8 lg:h-10 lg:w-10 text-gray-400 mb-2" />
|
||||||
<p className="text-sm text-gray-500 mb-1">Click to upload or drag and drop</p>
|
<p className="text-xs md:text-sm text-gray-500 mb-1">Click to upload or drag and drop</p>
|
||||||
<p className="text-xs text-gray-400">PNG, JPG, GIF up to 10MB</p>
|
<p className="text-xs text-gray-400">PNG, JPG, GIF up to 10MB</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -410,16 +666,17 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload 2 */}
|
{/* Upload 2 - Initials */}
|
||||||
<div className="col-span-4">
|
<div className="lg:col-span-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
|
<label className="block text-xs md:text-sm font-medium text-gray-700 mb-2">Initials</label>
|
||||||
<label htmlFor="upload2" className=" border-2 border-dashed border-gray-300 rounded-lg h-62 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
<label htmlFor="upload2" className="block border-2 border-dashed border-gray-300 rounded-lg h-32 md:h-40 lg:h-48 xl:h-56 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors">
|
||||||
{preview2 ? (
|
{preview2 ? (
|
||||||
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
|
<img src={preview2} alt="Initials Preview" className="max-h-full max-w-full object-contain p-2" />
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center px-2">
|
||||||
<MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
|
<MdCloudUpload className="mx-auto h-5 w-5 md:h-6 md:w-6 lg:h-8 lg:w-8 text-gray-400 mb-2" />
|
||||||
<p className="text-xs text-gray-500">Upload initials</p>
|
<p className="text-xs md:text-sm text-gray-500 mb-1">Upload initials</p>
|
||||||
|
<p className="text-xs text-gray-400 hidden md:block">PNG, JPG, GIF</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -434,8 +691,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'SAVED':
|
case 'SAVED':
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col ">
|
<div className="h-full flex flex-col ">
|
||||||
|
|
@ -457,14 +714,14 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
{/* Signature Image or Fallback Text */}
|
{/* Signature Image or Fallback Text */}
|
||||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
||||||
{signature?.signatureBlob ? (
|
{signature?.signatureType !== 'SELECT' ? (
|
||||||
<img
|
<img
|
||||||
src={`data:${signature?.signatureMimeType || "image/png"};base64,${signature?.signatureBlob}`}
|
src={`data:${signature?.signatureMimeType};base64,${signature?.signatureBlob}`}
|
||||||
className="h-[50px] w-[250px] object-contain"
|
className="h-[50px] w-[250px] object-contain"
|
||||||
alt="Signature"
|
alt="Signature"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-400 italic text-sm">No Signature</div>
|
renderSignatureText(signature?.signatureType, signature?.font, signature?.firstName, signature?.lastName, 'name')
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -484,14 +741,14 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
|
|
||||||
{/* Initials Image */}
|
{/* Initials Image */}
|
||||||
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
<div className="bg-white rounded-lg p-1 flex items-center justify-center">
|
||||||
{signature?.initialsBlob ? (
|
{signature?.signatureType !== 'SELECT' ? (
|
||||||
<img
|
<img
|
||||||
src={`data:image/png;base64,${signature?.initialsBlob}`}
|
src={`data:${signature.initialsMimeType};base64,${signature.initialsBlob}`}
|
||||||
className="h-[50px] object-contain"
|
className="h-[50px] object-contain"
|
||||||
alt="Initials"
|
alt="Initials"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-400 italic text-sm">No Initials</div>
|
renderSignatureText(signature?.signatureType, signature?.font, signature?.firstName, signature?.lastName, 'initials')
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -513,7 +770,6 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserDetails = async () => {
|
const getUserDetails = async () => {
|
||||||
try {
|
try {
|
||||||
// let res = await axios.get(`/api/user/details`, {
|
// let res = await axios.get(`/api/user/details`, {
|
||||||
|
|
@ -536,24 +792,143 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 bg-black/30 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 bg-black/30 flex items-center justify-center z-50 p-2 md:p-4">
|
||||||
<div className="bg-white rounded-md shadow-xl w-full max-w-[80vw] h-[80vh] flex relative">
|
<div className="bg-white rounded-md shadow-xl w-full h-full md:max-w-[95vw] lg:max-w-[85vw] xl:max-w-[80vw] md:max-h-[95vh] lg:max-h-[90vh] flex flex-col md:flex-row relative overflow-hidden ">
|
||||||
|
|
||||||
{/* Sidebar Navigation */}
|
{/* Mobile Navigation - Fixed at top on mobile only */}
|
||||||
<div className="w-64 bg-gray-50 rounded-l-lg border-r border-gray-200 flex flex-col">
|
<div className="md:hidden flex-1 flex flex-col min-h-0">
|
||||||
{/* Header */}
|
|
||||||
<div className="px-4 py-4 border-b border-gray-200">
|
{/* Scrollable area (mobile: inputs + nav + content) */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex-1 min-h-0 overflow-y-auto no-scrollbar">
|
||||||
<div className='px-1 py-1'>
|
<div className="px-3 lg:px-6 py-2 border-b border-gray-200 flex-shrink-0">
|
||||||
<h3 className="text-md font-semibold text-gray-900">Add Signature Sign</h3>
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
<p className="text-xs text-gray-500">Create your digital signature</p>
|
Add Signature
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{/* Mobile Inputs */}
|
||||||
|
<div className="md:hidden w-full bg-white border-b border-gray-200 flex-shrink-0 px-3 py-2">
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div className='col-span-2'>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-1'>
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Initials
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={initials}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Inputs */}
|
{/* Mobile Navigation - Sticky inside scroll */}
|
||||||
<div className="px-6 py-2.5 border-b border-gray-200">
|
<div className="md:hidden w-full bg-gray-50 border-b border-gray-200 sticky top-0 z-20">
|
||||||
<div className="space-y-2">
|
<div className="px-3 py-1.5">
|
||||||
|
<nav className="flex gap-1">
|
||||||
|
{navigationItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
|
||||||
|
className={`flex-1 flex items-center justify-center space-x-1 px-2 py-2 text-xs font-medium rounded-lg transition-all duration-200 whitespace-nowrap ${activeTab === item.id
|
||||||
|
? 'bg-blue-50 text-blue-700 border border-blue-200 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={activeTab === item.id ? 'text-blue-600' : 'text-gray-400'}>
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
|
<span className="hidden sm:block">{item.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="md:hidden p-3 md:p-4 lg:py-5 lg:px-5">
|
||||||
|
<div className="h-full min-h-[300px] md:min-h-[400px]">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checkbox - Fixed */}
|
||||||
|
{/* <div className="px-3 md:px-4 lg:px-6 py-2 md:py-3 text-gray-700 border-t border-gray-100 flex-shrink-0">
|
||||||
|
<label className="inline-flex items-center cursor-pointer text-xs md:text-sm hover:text-gray-900 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={toggleCheckbox}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 md:w-4 md:h-4 border rounded flex items-center justify-center mr-2 md:mr-3 transition-colors flex-shrink-0 ${checked
|
||||||
|
? 'bg-[var(--modalSubmitBG)] border-[var(--modalSubmitBG)]'
|
||||||
|
: 'bg-white border-gray-400 hover:border-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{checked && <FiCheck className="text-white text-xs" />}
|
||||||
|
</div>
|
||||||
|
<span>Signature will be used by default</span>
|
||||||
|
</label>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Footer Buttons - Fixed */}
|
||||||
|
<div className="p-3 md:p-4 lg:p-3 border-t border-gray-200 flex sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
className="w-full sm:w-auto px-3 md:px-4 lg:px-6 py-2 md:py-3 text-xs md:text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
|
onClick={() => setOpenSign?.(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full sm:w-auto px-3 md:px-4 lg:px-6 py-2 md:py-3 text-xs md:text-sm font-medium rounded-md bg-gradient-to-br from-[var(--primary)] to-blue-600 text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<FaSpinner className="animate-spin w-3 h-3 md:w-4 md:h-4" />
|
||||||
|
<span>CREATING...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>CREATE SIGNATURE</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Desktop Sidebar Navigation */}
|
||||||
|
<div className="hidden md:flex md:w-64 lg:w-72 xl:w-80 bg-gray-50 md:border-r border-gray-200 flex-col">
|
||||||
|
{/* Header - Fixed */}
|
||||||
|
<div className="px-4 lg:px-6 py-4 border-b border-gray-200 flex-shrink-0">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
|
Add Signature
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Create your digital signature
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Inputs - Fixed */}
|
||||||
|
<div className="px-4 lg:px-6 py-4 border-b border-gray-200 flex-shrink-0">
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Full Name
|
Full Name
|
||||||
|
|
@ -563,10 +938,9 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
value={fullName}
|
value={fullName}
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
placeholder="Enter full name"
|
placeholder="Enter full name"
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Initials
|
Initials
|
||||||
|
|
@ -576,79 +950,89 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
|
||||||
value={initials}
|
value={initials}
|
||||||
onChange={(e) => setInitials(e.target.value)}
|
onChange={(e) => setInitials(e.target.value)}
|
||||||
placeholder="Enter initials"
|
placeholder="Enter initials"
|
||||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Menu */}
|
{/* Navigation Menu - Fixed */}
|
||||||
<div className="flex-1 px-6 py-2.5">
|
<div className="px-4 lg:px-6 py-4 flex-shrink-0">
|
||||||
<nav className="space-y-2">
|
<nav className="flex flex-col gap-2">
|
||||||
{navigationItems.map((item) => (
|
{navigationItems.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => setActiveTab(item.id as 'TYPED' | 'DRAW' | 'UPLOAD' | 'SAVED')}
|
onClick={() => setActiveTab(item.id as 'SELECT' | 'DRAW' | 'UPLOAD')}
|
||||||
className={`w-full flex items-center space-x-3 px-2 py-2 text-sm font-medium rounded-lg cursor-pointer transition-colors ${activeTab === item.id
|
className={`w-full flex items-center justify-start space-x-2 px-3 py-3 text-sm font-medium rounded-lg transition-all duration-200 whitespace-nowrap ${activeTab === item.id
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
? 'bg-blue-50 text-blue-700 border border-blue-200 shadow-sm'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className={activeTab === item.id ? 'text-blue-600' : 'text-gray-400'}>
|
<span className={activeTab === item.id ? 'text-blue-600' : 'text-gray-400'}>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer for desktop */}
|
||||||
|
<div className="flex-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex-1 flex flex-col ">
|
<div className="hidden flex-1 md:flex flex-col min-h-0">
|
||||||
{/* Header with close button */}
|
{/* Content - Scrollable */}
|
||||||
|
<div className="flex-1 min-h-0 p-3 md:p-4 lg:py-5 lg:px-5 overflow-y-auto no-scrollbar">
|
||||||
{/* Content */}
|
<div className="h-full min-h-[300px] md:min-h-[400px]">
|
||||||
<div className="flex-1 py-5 px-5 overflow-hidden ">
|
|
||||||
{/* <button
|
|
||||||
onClick={() => setShowAddSignature?.(false)}
|
|
||||||
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20"
|
|
||||||
>
|
|
||||||
<AiOutlineClose className="w-5 h-5 text-gray-500" />
|
|
||||||
</button> */}
|
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Footer Buttons in Main Area */}
|
{/* Checkbox - Fixed */}
|
||||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
|
{/* <div className="px-3 md:px-4 lg:px-6 py-2 md:py-3 text-gray-700 border-t border-gray-100 flex-shrink-0">
|
||||||
|
<label className="inline-flex items-center cursor-pointer text-xs md:text-sm hover:text-gray-900 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={toggleCheckbox}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 md:w-4 md:h-4 border rounded flex items-center justify-center mr-2 md:mr-3 transition-colors flex-shrink-0 ${checked ? 'bg-[var(--modalSubmitBG)] border-[var(--modalSubmitBG)]' : 'bg-white border-gray-400 hover:border-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{checked && <FiCheck className="text-white text-xs" />}
|
||||||
|
</div>
|
||||||
|
<span>Signature will be used by default</span>
|
||||||
|
</label>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Footer Buttons - Fixed */}
|
||||||
|
<div className="p-3 md:p-4 lg:p-3 border-t border-gray-200 flex sm:flex-row justify-end gap-2 md:gap-3 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
className="w-full sm:w-auto px-3 md:px-4 lg:px-6 py-2 md:py-3 text-xs md:text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 hover:border-gray-400 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
onClick={() => setOpenSign?.(false)}
|
onClick={() => setOpenSign?.(false)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
CANCEL
|
CANCEL
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
className="w-full sm:w-auto px-3 md:px-4 lg:px-6 py-2 md:py-3 text-xs md:text-sm font-medium rounded-md bg-gradient-to-br from-[var(--primary)] to-blue-600 text-white hover:opacity-90 hover:shadow-lg flex items-center justify-center gap-1 md:gap-2 disabled:opacity-60 disabled:cursor-not-allowed transition-all duration-200"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<FaSpinner className="animate-spin" />
|
<FaSpinner className="animate-spin w-3 h-3 md:w-4 md:h-4" />
|
||||||
{/* {isEditMode ? 'UPDATING...' : 'CREATING...'} */}
|
<span>CREATING...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<span>CREATE SIGNATURE</span>
|
||||||
{activeTab === "SAVED" ? "CHANGE SIGNATURE" : "CREATE SIGNATURE"}
|
|
||||||
{/* isEditMode ? 'UPDATE SIGNATURE' : 'CREATE SIGNATURE' */}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue