jai #1

Merged
jaipandey merged 6 commits from jai into master 2025-07-07 06:15:27 +00:00
4 changed files with 559 additions and 294 deletions
Showing only changes of commit 79b121c25b - Show all commits

View File

@ -1,63 +1,119 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
export type ActionType = 'create' | 'invite'; export type ActionType = 'create' | 'invite';
type CreateData = { accountName: string }; type CreateData = { accountName: string };
type InviteData = { i_accountName: string; email: string; role: string }; type InviteData = { i_accountName: number; email: string; role: string };
type FormDataType = CreateData | InviteData; export type FormDataType = CreateData | InviteData;
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
interface Props { interface Props {
onSubmit: (data: FormDataType, action: ActionType) => void; onSubmit: (data: FormDataType, action: ActionType) => void;
createAndInviteLoader: boolean;
accountOptions: AccountOption[];
accountOptionsLoading: boolean;
} }
const CreateAndInvite = ({ onSubmit }: Props) => { const CreateAndInvite = ({
const [isOpen, setIsOpen] = useState(false); onSubmit,
const dropdownRef = useRef<HTMLDivElement>(null); createAndInviteLoader,
accountOptions,
accountOptionsLoading
}: Props) => {
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
const roleDropdownRef = useRef<HTMLDivElement>(null);
const accountDropdownRef = useRef<HTMLDivElement>(null);
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({ const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
accountName: '', accountName: '',
}); });
const [accountSearchTerm, setAccountSearchTerm] = useState('');
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
const [inviteFormData, setInviteFormData] = useState<{ const [inviteFormData, setInviteFormData] = useState<{
i_accountName: string; i_accountName: number;
email: string; email: string;
role: string; role: string;
}>({ }>({
i_accountName: '', i_accountName: 0,
email: '', email: '',
role: '', role: '',
}); });
const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [errors, setErrors] = useState<{ [key: string]: string }>({});
const roles = ['ADMIN', 'EDITOR', 'VIEWER'];
const roles = ['Owner', 'Client']; // Handle role dropdown
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
const handleFocus = () => setIsOpen(true); const handleRoleDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => { setTimeout(() => {
const activeElement = document.activeElement as HTMLElement; const activeElement = document.activeElement as HTMLElement;
if (dropdownRef.current && !dropdownRef.current.contains(activeElement)) { if (roleDropdownRef.current && !roleDropdownRef.current.contains(activeElement)) {
setIsOpen(false); setIsRoleDropdownOpen(false);
} }
}, 100); }, 100);
}; };
const handleSelect = (role: string) => { const handleRoleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role })); setInviteFormData((prev) => ({ ...prev, role }));
setIsOpen(false); setIsRoleDropdownOpen(false);
}; };
// Handle account dropdown
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
const handleAccountDropdownBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement;
if (accountDropdownRef.current && !accountDropdownRef.current.contains(activeElement)) {
setIsAccountDropdownOpen(false);
}
}, 100);
};
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setAccountSearchTerm(value);
// Clear selected account if user is typing
if (selectedAccount && value !== selectedAccount.accountName) {
setSelectedAccount(null);
setInviteFormData((prev) => ({ ...prev, i_accountName: 0 }));
}
};
const handleAccountSelect = (option: AccountOption) => {
setSelectedAccount(option);
setAccountSearchTerm(option.accountName);
setInviteFormData((prev) => ({ ...prev, i_accountName: option.accountId }));
setIsAccountDropdownOpen(false);
// Clear any existing account name error
if (errors.i_accountName) {
setErrors(prev => ({ ...prev, i_accountName: '' }));
}
};
// Handle create form
const handleCreateChange = (value: string) => { const handleCreateChange = (value: string) => {
setCreateFormData({ accountName: value }); setCreateFormData({ accountName: value });
}; // Clear error when user starts typing
if (errors.accountName) {
const handleInviteChange = ( setErrors(prev => ({ ...prev, accountName: '' }));
field: keyof typeof inviteFormData, }
value: string
) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
}; };
const handleCreateSubmit = () => { const handleCreateSubmit = () => {
@ -69,11 +125,20 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
onSubmit(createFormData, 'create'); onSubmit(createFormData, 'create');
}; };
// Handle invite form
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
const handleInviteSubmit = () => { const handleInviteSubmit = () => {
const { i_accountName, email, role } = inviteFormData; const { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {}; const errs: { [key: string]: string } = {};
if (!i_accountName.trim()) errs.i_accountName = 'Account name is required'; if (!i_accountName) errs.i_accountName = 'Account name is required';
if (!email.trim()) errs.email = 'Email is required'; if (!email.trim()) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email'; else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
if (!role.trim()) errs.role = 'Role is required'; if (!role.trim()) errs.role = 'Role is required';
@ -87,9 +152,14 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
onSubmit(inviteFormData, 'invite'); onSubmit(inviteFormData, 'invite');
}; };
// Filter account options based on search term
const filteredAccountOptions = accountOptions.filter((option) =>
option.accountName?.toLowerCase().includes(accountSearchTerm.toLowerCase())
);
return ( return (
<div className="mt-3 space-y-3"> <div className="ms-2 space-y-3">
{/* Create */} {/* Create Section */}
<section className="border-gray-200 border-b"> <section className="border-gray-200 border-b">
<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">
@ -107,9 +177,17 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
<button <button
type="button" type="button"
onClick={handleCreateSubmit} onClick={handleCreateSubmit}
className="ms-1 text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer" className="ms-1 flex items-center text-[14px] px-3 py-[1.1] bg-gray-300 text-slate-600 font-medium rounded-md border border-gray-300 hover:bg-opacity-90 transition cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed"
disabled={createAndInviteLoader}
> >
Create {createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
CREATING...
</>
) : (
'CREATE'
)}
</button> </button>
</div> </div>
{errors.accountName && ( {errors.accountName && (
@ -118,22 +196,62 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
</div> </div>
</section> </section>
{/* Invite */} {/* Invite Section */}
<section className="border-gray-300"> <section className="border-gray-300">
<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 */} {/* Account Name Dropdown */}
<div className="flex flex-col"> <div className="flex flex-col relative">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1"> <label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name Account Name
</label> </label>
<input <div
type="text" className="relative w-full"
placeholder="Enter account name" onBlur={handleAccountDropdownBlur}
value={inviteFormData.i_accountName} ref={accountDropdownRef}
onChange={(e) => handleInviteChange('i_accountName', e.target.value)} tabIndex={-1}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" >
/> <input
type="text"
placeholder={accountOptionsLoading ? "Loading accounts..." : "Select or search account"}
value={accountSearchTerm}
onChange={handleAccountSearchChange}
onFocus={handleAccountDropdownFocus}
disabled={accountOptionsLoading}
className="w-full px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)] disabled:opacity-60 disabled:cursor-not-allowed"
/>
{/* Loading indicator */}
{accountOptionsLoading && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<FaSpinner className="animate-spin text-gray-400 text-sm" />
</div>
)}
{/* Account Dropdown */}
{isAccountDropdownOpen && !accountOptionsLoading && (
<div className="absolute w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg z-10">
<ul className="max-h-40 overflow-y-auto">
{filteredAccountOptions.length > 0 ? (
filteredAccountOptions.map((option) => (
<li
key={option.accountId}
onMouseDown={() => handleAccountSelect(option)}
className="px-3 py-2 text-[13px] text-gray-600 hover:bg-gray-100 cursor-pointer"
>
<div className="flex justify-between items-center">
<span>{option.accountName}</span>
</div>
</li>
))
) : (
<li className="px-3 py-2 text-gray-500">No results found</li>
)}
</ul>
</div>
)}
</div>
{errors.i_accountName && ( {errors.i_accountName && (
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span> <span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
)} )}
@ -163,8 +281,8 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
</label> </label>
<div <div
className="relative w-full" className="relative w-full"
onBlur={handleBlur} onBlur={handleRoleDropdownBlur}
ref={dropdownRef} ref={roleDropdownRef}
tabIndex={-1} tabIndex={-1}
> >
<input <input
@ -172,30 +290,30 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
placeholder="Select role" placeholder="Select role"
value={inviteFormData.role} value={inviteFormData.role}
readOnly readOnly
onFocus={handleFocus} onFocus={handleRoleDropdownFocus}
className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition" className="w-full px-3 py-1.5 border text-gray-600 border-gray-400 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[var(--primary)] transition cursor-pointer"
/> />
<div <div
className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${ className={`absolute z-10 w-full bg-white text-slate-500 font-semibold border border-gray-200 rounded-md shadow-lg mt-1 transition-all duration-200 ease-in-out ${
isOpen isRoleDropdownOpen
? 'opacity-100 scale-100' ? 'opacity-100 scale-100'
: 'opacity-0 scale-95 pointer-events-none' : 'opacity-0 scale-95 pointer-events-none'
}`} }`}
> >
{roles.map((r) => ( {roles.map((role) => (
<div <div
key={r} key={role}
onMouseDown={() => handleSelect(r)} onMouseDown={() => handleRoleSelect(role)}
className="px-4 py-2 text-[13px] hover:bg-gray-100 cursor-pointer" className="px-4 py-1 text-[13px] hover:bg-gray-100 cursor-pointer"
> >
{r} {role}
</div> </div>
))} ))}
</div> </div>
{errors.role && (
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
)}
</div> </div>
{errors.role && (
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
)}
</div> </div>
</div> </div>
@ -203,9 +321,17 @@ const CreateAndInvite = ({ onSubmit }: Props) => {
<button <button
type="button" type="button"
onClick={handleInviteSubmit} onClick={handleInviteSubmit}
className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600" className="px-4 py-2 text-sm font-medium rounded-md cursor-pointer hover:bg-opacity-90 transition text-[14px] bg-gray-300 text-slate-600 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={createAndInviteLoader}
> >
Invite {createAndInviteLoader ? (
<>
<FaSpinner className="animate-spin me-1" />
INVITING...
</>
) : (
'INVITE'
)}
</button> </button>
</div> </div>
</section> </section>

View File

@ -7,6 +7,7 @@ import Language from "./Language";
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite' import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection"; import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature"; import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
// Define the shape of state object // Define the shape of state object
interface Account { interface Account {
@ -30,6 +31,15 @@ interface SubSection {
component?: React.ReactNode; component?: React.ReactNode;
} }
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
const Setting = () => { const Setting = () => {
const { page1 } = useParams<{ page1: string }>(); const { page1 } = useParams<{ page1: string }>();
const location = useLocation(); const location = useLocation();
@ -54,6 +64,12 @@ const Setting = () => {
}, },
}); });
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
// Dropdown data states
const [accountOptions, setAccountOptions] = useState<AccountOption[]>([]);
const [accountOptionsLoading, setAccountOptionsLoading] = useState(false);
// Define subsections for each main tab // Define subsections for each main tab
const getSubSections = (tabId: string): SubSection[] => { const getSubSections = (tabId: string): SubSection[] => {
switch (tabId) { switch (tabId) {
@ -62,16 +78,6 @@ const Setting = () => {
{ id: "profile-info", label: "Profile Information" }, { id: "profile-info", label: "Profile Information" },
{ id: "signature", label: "Signature" }, { id: "signature", label: "Signature" },
]; ];
// case "create and invite":
// return [
// { id: "create-account", label: "Create Account" },
// { id: "invite-users", label: "Invite Users" },
// { id: "manage-invitations", label: "Manage Invitations" }
// ];
// return [
// { id: "share-options", label: "Share Options" },
// { id: "privacy-settings", label: "Privacy Settings" }
// ];
default: default:
return []; return [];
} }
@ -143,6 +149,21 @@ const Setting = () => {
} }
}, [location.hash, activeTab]); }, [location.hash, activeTab]);
// Fetch account list for dropdown
const getAccountList = async () => {
setAccountOptionsLoading(true);
try {
const res = await api.get('/api/user/accounts');
if (res?.data) {
setAccountOptions(res.data);
}
} catch (error) {
console.error('Error fetching account options:', error);
} finally {
setAccountOptionsLoading(false);
}
};
// Render subsection content based on active tab and subsection // Render subsection content based on active tab and subsection
const renderSubSectionContent = (subSectionId: string) => { const renderSubSectionContent = (subSectionId: string) => {
switch (activeTab) { switch (activeTab) {
@ -152,7 +173,7 @@ const Setting = () => {
return <UserProfile userData={user} />; return <UserProfile userData={user} />;
case "signature": case "signature":
return ( return (
<Signature/> <Signature />
); );
default: default:
return; return;
@ -178,7 +199,14 @@ const Setting = () => {
case "language": case "language":
return <Language userData={user} />; return <Language userData={user} />;
case "create and invite": case "create and invite":
return <CreateAndInvite onSubmit={handleSubmit} />; return (
<CreateAndInvite
onSubmit={handleSubmit}
createAndInviteLoader={createAndInviteLoader}
accountOptions={accountOptions}
accountOptionsLoading={accountOptionsLoading}
/>
);
default: default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>; return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
} }
@ -221,6 +249,8 @@ const Setting = () => {
useEffect(() => { useEffect(() => {
getUserDetails() getUserDetails()
// Fetch account list when component mounts
getAccountList()
}, []) }, [])
// Set initial subsection when component mounts or activeTab changes // Set initial subsection when component mounts or activeTab changes
@ -255,14 +285,51 @@ const Setting = () => {
}, [activeSubSection]); }, [activeSubSection]);
const handleSubmit = async (data: FormDataType, action: ActionType) => { const handleSubmit = async (data: FormDataType, action: ActionType) => {
setCreateAndInviteLoader(true)
try { try {
let res;
let toastMesaage ;
if (action === 'create') { if (action === 'create') {
console.log('Creating account:', data); console.log('Creating account:', data);
const payloadAccount = {
name: data.accountName
}
res = await api.post(`/api/user/accounts`, payloadAccount)
console.log("resAccountName", res)
if (res.status === 201) {
toastMesaage = `${action.toUpperCase()} Created Successfull...!`
}
} else if (action === 'invite') { } else if (action === 'invite') {
console.log('Inviting user:', data); console.log('Inviting user:', data);
const payloadInvite = {
email: data.email,
role: data.role
}
res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
console.log("InviteNAme",res)
toastMesaage = "Invite Successfull...!"
}
if (res?.data) {
toast.success(toastMesaage, {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
// Refetch account list after successful submission
await getAccountList();
} }
} catch (err) { } catch (err) {
console.error('Submission error:', err); console.error('Submission error:', err);
} finally {
setCreateAndInviteLoader(false)
} }
}; };
@ -279,11 +346,10 @@ const Setting = () => {
key={tab.id} key={tab.id}
onClick={() => handleMainTabClick(tab.id)} onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200 className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${ ${activeTab === tab.id
activeTab === tab.id
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600" ? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
: "text-gray-600 hover:text-blue-500" : "text-gray-600 hover:text-blue-500"
}`} }`}
> >
{tab.label} {tab.label}
</button> </button>
@ -296,17 +362,17 @@ const Setting = () => {
<div className="flex mt-3 px-8 flex-1 min-h-0"> <div className="flex mt-3 px-8 flex-1 min-h-0">
{/* Fixed Aside Navigation - Shows subsections of active tab */} {/* Fixed Aside Navigation - Shows subsections of active tab */}
{activeTab === 'userProfile' && {activeTab === 'userProfile' &&
<section className="flex-shrink-0 mr-6 w-50 relative"> <section className="flex-shrink-0 mr-6 w-50 relative">
<div className="sticky top-6 z-20"> <div className="sticky top-6 z-20">
<AsideSection <AsideSection
activeSubSection={activeSubSection} activeSubSection={activeSubSection}
onSubSectionClick={handleSubSectionClick} onSubSectionClick={handleSubSectionClick}
subSections={getSubSections(activeTab)} subSections={getSubSections(activeTab)}
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""} mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
/> />
</div> </div>
</section> </section>
} }
{/* Scrollable Main Content */} {/* Scrollable Main Content */}
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}

View File

@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
// import { X, Upload, Edit3, Image, Cloud, HardDrive, Droplets } from 'lucide-react';
import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai'; import { AiOutlineClose, AiOutlineUpload, AiOutlineEdit } from 'react-icons/ai';
import { MdImage, MdCloudUpload } from 'react-icons/md'; import { MdImage, MdCloudUpload } from 'react-icons/md';
import api from '../../../config/api'; import api from '../../../config/api';
import DrawSignatureCanvas from './DrawSignatureCanvas'; import DrawSignatureCanvas from './DrawSignatureCanvas';
import { FaSpinner } from "react-icons/fa";
import { GrCheckboxSelected } from "react-icons/gr";
interface AddSignatureModalProps { interface AddSignatureModalProps {
setShowAddSignature?: (value: boolean) => void; setShowAddSignature?: (value: boolean) => void;
getAllSignatures: () => Promise<void>;
} }
interface Account { interface Account {
id: number; id: number;
@ -24,23 +25,19 @@ interface UserData {
account: Account; account: Account;
} }
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature }) => { interface SelectStyle {
const [activeTab, setActiveTab] = useState<'choose' | 'draw' | 'upload'>('choose'); id: number,
style: string
}
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature , getAllSignatures }) => {
const [activeTab, setActiveTab] = useState<'SELECT' | 'DRAW' | 'UPLOAD'>('SELECT');
const [fullName, setFullName] = useState(''); const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState(''); const [initials, setInitials] = useState('');
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [signatures, setSignatures] = useState<{ const [signatures, setSignatures] = useState<{
fullSignature: string | null; fullSignature: string | null;
initials: string | null; initials: string | null;
}>({ fullSignature: null, initials: null }); }>({ fullSignature: null, initials: null });
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
setSignatures(data);
};
// Sample signature options for the CHOOSE tab
const signatureOptions = [ const signatureOptions = [
{ id: 1, name: 'print', initials: 'LA', style: 'print' }, { id: 1, name: 'print', initials: 'LA', style: 'print' },
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' }, { id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
@ -49,10 +46,9 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' }, { id: 5, name: 'modern', initials: 'LA', style: 'modern' },
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' }, { id: 6, name: 'classic', initials: 'LA', style: 'classic' },
]; ];
const [selectedSignature, setSelectedSignature] = useState<SelectStyle>(
const [selectedSignature, setSelectedSignature] = useState<number>(1); signatureOptions.length > 0 ? { id: signatureOptions[0].id, style: signatureOptions[0].style } : { id: 0, style: "" }
console.log(selectedSignature); );
// Store the actual file // Store the actual file
const [file1, setFile1] = useState<File | null>(null); const [file1, setFile1] = useState<File | null>(null);
@ -75,13 +71,22 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
name: "", name: "",
}, },
}); });
// Navigation items // Navigation items
const navigationItems = [ const navigationItems = [
{ id: 'choose', label: 'Choose', icon: <MdImage className="w-5 h-5" /> }, { 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" /> },
]; ];
console.log("signatures", signatures)
const handleSignatureChange = (data: { fullSignature: string | null; initials: string | null }) => {
setSignatures(data);
};
console.log("activeTab", activeTab)
console.log("selectedSignature", selectedSignature);
console.log('file1', file1)
console.log("file1", file2)
// Cleanup preview URLs when component unmounts or image changes // Cleanup preview URLs when component unmounts or image changes
useEffect(() => { useEffect(() => {
@ -91,55 +96,73 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
}; };
}, [preview1, preview2]); }, [preview1, preview2]);
const handleImageChange = ( const handleImageChange = async (
e: React.ChangeEvent<HTMLInputElement>, e: React.ChangeEvent<HTMLInputElement>,
setFile: React.Dispatch<React.SetStateAction<File | null>>, setFile: React.Dispatch<React.SetStateAction<File | null>>,
setPreview: React.Dispatch<React.SetStateAction<string | null>> setPreview: React.Dispatch<React.SetStateAction<string | null>>,
type: "fullSignature" | "initials"
) => { ) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
setFile(file); setFile(file);
const previewUrl = URL.createObjectURL(file); const previewUrl = URL.createObjectURL(file);
setPreview(previewUrl); setPreview(previewUrl);
// Convert to base64
const base64 = await convertToBase64(file);
// Update the appropriate signature
setSignatures((prev) => ({
...prev,
[type]: base64,
}));
} }
}; };
const convertToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSubmitting(true)
if (!signatures.fullSignature || !signatures.initials) { if (!signatures.fullSignature || !signatures.initials) {
alert('Please provide both signature and initials'); alert('Please provide both signature and initials');
return; return;
} }
// Example: Posting the base64 data
try { try {
const payload = { const payload = {
signatureType: "DRAW", signatureType: activeTab,
// "signatureId": 0, this work on only the put req. // "signatureId": 0, this work on only the put req.
signatureBase64: signatures.fullSignature, signatureBase64: signatures.fullSignature,
initialsBase64: signatures.initials, initialsBase64: signatures.initials,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
font: "" font: selectedSignature.style
}; };
console.log('Submitting Signature:', payload); console.log('Submitting Signature:', payload);
let res = await api.post(`/api/user/signatures`,payload) let res = await api.post(`/api/user/signatures`, payload)
console.log("handleSubmit", res) console.log("handleSubmit", res)
if (res?.data) {
getAllSignatures()
setShowAddSignature?.(false)
}
} catch (error) { } catch (error) {
console.error("Error fetching user details:", error); console.error("Error fetching user details:", error);
} finally {
setIsSubmitting(false)
} }
// fetch('/api/signature', { method: 'POST', body: JSON.stringify(payload) })
}; };
const renderContent = () => { const renderContent = () => {
switch (activeTab) { switch (activeTab) {
case 'choose': 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-6">
@ -155,8 +178,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="radio" type="radio"
id={`signature-${option.id}`} id={`signature-${option.id}`}
name="signature" name="signature"
checked={selectedSignature === option.id} checked={selectedSignature.id === option.id}
onChange={() => setSelectedSignature(option.id)} onChange={() => setSelectedSignature({ id: option.id, style: option.style })}
className="w-4 h-4 text-blue-600 focus:ring-blue-500" className="w-4 h-4 text-blue-600 focus:ring-blue-500"
/> />
<div className="flex-1 grid grid-cols-2 gap-2"> <div className="flex-1 grid grid-cols-2 gap-2">
@ -193,12 +216,12 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
</div> </div>
); );
case 'draw': case 'DRAW':
return ( return (
<DrawSignatureCanvas onSignatureChange={handleSignatureChange} /> <DrawSignatureCanvas onSignatureChange={handleSignatureChange} />
); );
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-6">
@ -210,7 +233,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{/* Upload 1 */} {/* Upload 1 */}
<div className="col-span-8"> <div className="col-span-8">
<label className="block 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=" border-2 border-dashed border-gray-300 rounded-lg h-48 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-44 object-contain" />
) : ( ) : (
@ -226,19 +249,19 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => handleImageChange(e, setFile1, setPreview1)} onChange={(e) => handleImageChange(e, setFile1, setPreview1, "fullSignature")}
/> />
</div> </div>
{/* Upload 2 */} {/* Upload 2 */}
<div className="col-span-4"> <div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label> <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-48 flex items-center justify-center bg-gray-50 hover:bg-gray-100 cursor-pointer transition-colors"> <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 ? ( {preview2 ? (
<img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" /> <img src={preview2} alt="Initials Preview" className="max-h-44 object-contain" />
) : ( ) : (
<div className="text-center"> <div className="text-center">
<MdImage className="mx-auto h-8 w-8 text-gray-400 mb-2" /> <MdCloudUpload className="mx-auto h-8 w-8 text-gray-400 mb-2" />
<p className="text-xs text-gray-500">Upload initials</p> <p className="text-xs text-gray-500">Upload initials</p>
</div> </div>
)} )}
@ -248,7 +271,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => handleImageChange(e, setFile2, setPreview2)} onChange={(e) => handleImageChange(e, setFile2, setPreview2, "initials")}
/> />
</div> </div>
</div> </div>
@ -332,8 +355,8 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{navigationItems.map((item) => ( {navigationItems.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => setActiveTab(item.id as 'choose' | 'draw' | 'upload')} 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 transition-colors ${activeTab === item.id 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
? 'bg-blue-50 text-blue-700 border border-blue-200' ? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`} }`}
@ -356,25 +379,37 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignatu
{/* Content */} {/* Content */}
<div className="flex-1 py-5 px-5 overflow-hidden "> <div className="flex-1 py-5 px-5 overflow-hidden ">
<button {/* <button
onClick={() => setShowAddSignature?.(false)} onClick={() => setShowAddSignature?.(false)}
className="absolute top-2 right-3 p-2 hover:bg-gray-100 rounded-full transition-colors z-20" 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" /> <AiOutlineClose className="w-5 h-5 text-gray-500" />
</button> </button> */}
{renderContent()} {renderContent()}
</div> </div>
{/* Footer Buttons in Main Area */} {/* Footer Buttons in Main Area */}
<div className="p-4 border-t border-gray-200 flex justify-end gap-3"> <div className="p-4 border-t border-gray-200 flex justify-end gap-3">
<button <button
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors" 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"
onClick={() => setShowAddSignature?.(false)} onClick={() => setShowAddSignature?.(false)}
disabled={isSubmitting}
> >
CANCEL CANCEL
</button> </button>
<button className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors" onClick={handleSubmit}> <button
CREATE SIGNATURE className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<FaSpinner className="animate-spin" />
CREATING...
</>
) : (
"CREATE SIGNATURE"
)}
</button> </button>
</div> </div>

View File

@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi'; import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
import AddSignatureModal from './AddSignatureModal'; import AddSignatureModal from './AddSignatureModal';
import api from '../../../config/api'; import api from '../../../config/api';
import { Bounce, toast } from "react-toastify";
import { FaSpinner } from 'react-icons/fa';
interface Signature { interface Signature {
id: string; id: string;
@ -10,39 +12,23 @@ interface Signature {
lastName: string; lastName: string;
firstName: string firstName: string
font: string; font: string;
initialsBlob:string; initialsBlob: string;
initialsStampedBlob:string; initialsStampedBlob: string;
initialsHash:string; initialsHash: string;
signatureHash:string; signatureHash: string;
signatureBlob:string; signatureBlob: string;
signatureStampedBlob:string; signatureStampedBlob: string;
default:boolean default: boolean
} }
const SignatureManager: React.FC = () => { const SignatureManager: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false)
const [signatures, setSignatures] = useState<Signature[]>([]); const [signatures, setSignatures] = useState<Signature[]>([]);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null); const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const [showAddSignature, setShowAddSignature] = useState<boolean>(false) const [showAddSignature, setShowAddSignature] = useState<boolean>(false)
const handleEdit = (id: string) => {
console.log('Edit signature:', id);
setActiveDropdown(null);
};
const handleDelete = (id: string) => {
setSignatures(signatures.filter(sig => sig.id !== id));
setActiveDropdown(null);
};
const toggleDropdown = (id: string) => {
setActiveDropdown(activeDropdown === id ? null : id);
};
const getAllSignatures = async () => { const getAllSignatures = async () => {
setLoading(true)
try { try {
let res = await api.get("/api/user/signatures") let res = await api.get("/api/user/signatures")
console.log("getAllSignatures", res.data) console.log("getAllSignatures", res.data)
@ -50,6 +36,8 @@ const SignatureManager: React.FC = () => {
setSignatures(signatureData) setSignatures(signatureData)
} catch (error) { } catch (error) {
console.log("getAllSignatures", error) console.log("getAllSignatures", error)
} finally {
setLoading(false)
} }
} }
@ -57,148 +45,198 @@ const SignatureManager: React.FC = () => {
getAllSignatures() getAllSignatures()
}, []) }, [])
const handleEdit = (id: string) => {
console.log('Edit signature:', id);
setActiveDropdown(null);
};
const handleDelete = async (id: string) => {
if (!id) return;
setLoading(true)
try {
let res = await api.delete(`/api/user/signatures/${id}`)
console.log(res.data)
toast.success("Signature Delete Successfull", {
position: "top-center",
autoClose: 500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await getAllSignatures();
} catch (error) {
console.log("delete signature", error)
} finally {
setActiveDropdown(null);
setLoading(false)
}
};
const toggleDropdown = (id: string) => {
setActiveDropdown(activeDropdown === id ? null : id);
};
return ( return (
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6"> <>
<div className="max-w-[84vw] mx-auto"> {loading ? <div className=' flex items-center justify-center'>
{/* Header */} <div className="p-12 text-center mt-8">
<div className="flex items-center justify-between mb-8"> <div className="text-gray-400 mb-4">
<div> <FaSpinner className="w-20 h-20 mx-auto animate-spin" />
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
</div> </div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={() => setShowAddSignature((prev) => !prev)}> <h3 className="text-xl font-semibold text-gray-700 mb-2">Loading...</h3>
<FiPlus className="" />
ADD
</button>
</div> </div>
</div> :
<div className="min-h-screen bg-white p-3 rounded-md px-4 mb-6">
<div className="max-w-[84vw] mx-auto">
<>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-[17px] font-medium text-gray-500 mb-2">Signatures</h1>
<p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
</div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={() => setShowAddSignature((prev) => !prev)}>
<FiPlus className="" />
ADD
</button>
</div>
{/* Table Card */} {/* Table Card */}
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100"> <div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
{/* Table Header */} {/* Table Header */}
<div className="bg-gray-50 border-b border-gray-200"> <div className="bg-gray-50 border-b border-gray-200">
<div className="grid grid-cols-12 gap-4 px-6 py-4"> <div className="grid grid-cols-12 gap-4 px-6 py-4">
<div className="col-span-5"> <div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3> <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Signature</h3>
</div>
<div className="col-span-5">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
</div>
<div className="col-span-2">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
</div>
</div>
</div>
{/* Table Body */}
<div className="divide-y divide-gray-100">
{signatures.map((signature, index) => (
<div key={signature.id} className="grid grid-cols-12 gap-4 px-6 py-6 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">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Signed by:
</div> </div>
<div className="col-span-5">
{/* Signature Image */} <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider">Initial</h3>
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img
src={`data:image/png;base64,${signature.signatureBlob}`}
className="h-[50px] w-[250px] object-contain"
alt="Signature"
/>
</div> </div>
<div className="col-span-2">
{/* Signature ID */} <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider text-right">Actions</h3>
<div className="text-xs text-gray-500 truncate">
{signature.signatureHash}
</div> </div>
</div> </div>
</div> </div>
{/* Table Body */}
<div className="divide-y divide-gray-100">
{signatures.map((signature, index) => (
<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">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
Signed by:
</div>
{/* Initial Column */} {/* Signature Image */}
<div className="col-span-5"> <div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<div className="flex flex-col space-y-2 relative ps-6 border-l-3 border-green-600 rounded-bl-2xl rounded-tl-2xl"> <img
{/* Label */} src={`data:image/png;base64,${signature.signatureBlob}`}
<div className="text-sm text-gray-600 font-medium mb-1"> className="h-[50px] w-[250px] object-contain"
DS alt="Signature"
</div> />
</div>
{/* Signature Image */} {/* Signature ID */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center "> <div className="text-xs text-gray-500 truncate">
<img {signature.signatureHash}
src={`data:image/png;base64,${signature.initialsBlob}`} </div>
className="h-[50px] object-contain" </div>
alt="DS Signature"
/>
</div>
</div>
</div>
{/* Actions Column */}
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
<div className="relative ">
<button
onClick={() => toggleDropdown(signature.id)}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
>
ACTIONS
<FiMoreVertical />
</button>
{/* Dropdown Menu */}
{activeDropdown === signature.id && (
<div className="absolute right-0 mt-0.5 w-28 bg-white rounded-lg shadow-xl border border-gray-200 z-10 py-1">
<button
onClick={() => handleEdit(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiEdit3 />
Edit
</button>
<button
onClick={() => handleDelete(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiTrash2 />
Delete
</button>
</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">
{/* Label */}
<div className="text-sm text-gray-600 font-medium mb-1">
DS
</div>
{/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img
src={`data:image/png;base64,${signature.initialsBlob}`}
className="h-[50px] object-contain"
alt="DS Signature"
/>
</div>
</div>
</div>
{/* Actions Column */}
<div className="col-span-2 flex justify-between items-start pt-9 ps-8 ">
<div className="relative ">
<button
onClick={() => toggleDropdown(signature.id)}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-1.5 py-1.5 text-[13px] rounded-lg font-semibold flex items-center transition-all duration-200 border border-gray-300 "
>
ACTIONS
<FiMoreVertical />
</button>
{/* Dropdown Menu */}
{activeDropdown === signature.id && (
<div className="absolute right-0 mt-0.5 w-28 bg-white rounded-lg shadow-xl border border-gray-200 z-10 py-1">
<button
onClick={() => handleEdit(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-blue-50 hover:text-blue-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiEdit3 />
Edit
</button>
<button
onClick={() => handleDelete(signature.id)}
className="w-full px-4 py-1 text-left text-gray-500 hover:bg-red-50 hover:text-red-700 flex items-center gap-3 text-sm transition-colors duration-150"
>
<FiTrash2 />
Delete
</button>
</div>
)}
</div>
</div>
</div>
))}
</div> </div>
</div> </div>
))}
{/* Empty State (if no signatures) */}
{signatures.length === 0 && (
<div className="bg-white rounded-xl shadow-lg p-12 text-center mt-8">
<div className="text-gray-400 mb-4">
<FiEdit3 className="w-16 h-16 mx-auto" />
</div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3>
<p className="text-gray-500 mb-6">Create your first signature to get started.</p>
</div>
)}
</>
</div> </div>
{/* Click outside to close dropdown */}
{activeDropdown && (
<div
className="fixed inset-0 z-0"
onClick={() => setActiveDropdown(null)}
/>
)}
{showAddSignature && (
<AddSignatureModal setShowAddSignature={setShowAddSignature} getAllSignatures={getAllSignatures} />
)}
</div> </div>
}</>
{/* Empty State (if no signatures) */}
{signatures.length === 0 && (
<div className="bg-white rounded-xl shadow-lg p-12 text-center mt-8">
<div className="text-gray-400 mb-4">
<FiEdit3 className="w-16 h-16 mx-auto" />
</div>
<h3 className="text-xl font-semibold text-gray-700 mb-2">No signatures yet</h3>
<p className="text-gray-500 mb-6">Create your first signature to get started.</p>
</div>
)}
</div>
{/* Click outside to close dropdown */}
{activeDropdown && (
<div
className="fixed inset-0 z-0"
onClick={() => setActiveDropdown(null)}
/>
)}
{showAddSignature && (
<AddSignatureModal setShowAddSignature={setShowAddSignature} />
)}
</div>
); );
}; };