This commit is contained in:
= 2025-07-02 17:34:05 +05:30
commit e2369a4548
15 changed files with 2341 additions and 781 deletions

1728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,8 +74,6 @@ const App = () => {
<Route path="/sign/:recipientId" element={<SignDocument />} />
<Route path="/view" element={<DocView />} />
<Route path="/SigningOrderDiagram" element={<SigningOrderDiagram />} />
{/* Links work wothout Login */}
</Routes>

View File

@ -69,7 +69,7 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
) || -1;
return (
<div className="relative p-8 bg-white max-w-max">
<div className="relative p-8 bg-white rounded-b-lg max-w-max max-h-[60vh] overflow-auto">
{/* Single mail icon on top */}
<div className="flex justify-center items-center gap-4">
@ -85,7 +85,7 @@ const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
<div className="w-16 border-1 border-dotted border-slate-200 "></div>
</div>
<div className="flex items-center justify-center space-x-6">
<div className="flex items-center justify-center space-x-6 ">
{sortedOrders.map((order, index) => (
<React.Fragment key={order}>
<div className="flex flex-col items-center space-y-4">

View File

@ -1,6 +1,6 @@
import React, { use, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { Link, useLocation, useParams } from 'react-router-dom';
import { FaCog } from 'react-icons/fa';
import { IoIosNotifications } from 'react-icons/io';
import { RootState } from '../../store/store';
@ -60,14 +60,48 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
// }
// }, [accountId, transactionId]);
const location = useLocation();
const pathnames = location.pathname
.split("/")
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
const labelMap = {
accounts: "Accounts",
transactions: "Transactions",
// Add more routes here if needed
};
return (
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
<h1 className="text-md font-medium text-gray-800 capitalize">
Account
{accountName ? ` > ${accountName}` : ''}
{transactionName ? ` > ${transactionName}` : ''}
</h1>
<nav className="text-md font-medium text-gray-800 capitalize">
{pathnames.map((segment, index) => {
const label = labelMap[segment] || segment;
const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1;
return (
<span key={to}>
{!isLast ? (
<>
<Link
to={to}
className="text-blue-600 hover:underline"
>
{label}
</Link>
<span className="mx-1">{">"}</span>
</>
) : (
<span>{label}</span>
)}
</span>
);
})}
</nav>
{/* <h1 className="text-md font-medium text-gray-800 capitalize">Account</h1> */}
@ -82,7 +116,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
</button>
<Link
to="/settings"
to="/s/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>

View File

@ -33,7 +33,7 @@ const HomeTopNav = () => {
</button>
<Link
to="/settings"
to="/s/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>

View File

@ -0,0 +1,54 @@
interface SubSection {
id: string;
label: string;
}
interface AsideSectionProps {
activeSubSection: string;
onSubSectionClick: (subSectionId: string) => void;
subSections: SubSection[];
mainTabTitle: string;
}
const AsideSection = ({
activeSubSection,
onSubSectionClick,
subSections,
mainTabTitle
}: AsideSectionProps) => {
// If no subsections, don't render the aside navigation
if (subSections.length === 0) {
return null;
}
return (
<div className="w-[16vw] p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-800">{mainTabTitle}</h3>
<p className="text-sm text-gray-500 mt-1">Navigate through sections</p>
</div>
<nav className="space-y-2 max-h-96 overflow-y-auto">
{subSections.map((subSection) => (
<a
key={subSection.id}
href={`#${subSection.id}`}
onClick={(e) => {
e.preventDefault(); // Prevent default anchor behavior
onSubSectionClick(subSection.id);
}}
className={`block w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 hover:bg-gray-50 cursor-pointer no-underline ${
activeSubSection === subSection.id
? "bg-blue-50 text-blue-600 border-l-4 border-blue-600 shadow-sm"
: "text-gray-600 hover:text-gray-800 hover:bg-gray-50"
}`}
>
<span className="block">{subSection.label}</span>
</a>
))}
</nav>
</div>
);
};
export default AsideSection;

View File

@ -0,0 +1,216 @@
import React, { useRef, useState } from 'react';
export type ActionType = 'create' | 'invite';
type CreateData = { accountName: string };
type InviteData = { i_accountName: string; email: string; role: string };
type FormDataType = CreateData | InviteData;
interface Props {
onSubmit: (data: FormDataType, action: ActionType) => void;
}
const CreateAndInvite = ({ onSubmit }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const [createFormData, setCreateFormData] = useState<{ accountName: string }>({
accountName: '',
});
const [inviteFormData, setInviteFormData] = useState<{
i_accountName: string;
email: string;
role: string;
}>({
i_accountName: '',
email: '',
role: '',
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const roles = ['Owner', 'Client'];
const handleFocus = () => setIsOpen(true);
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement;
if (dropdownRef.current && !dropdownRef.current.contains(activeElement)) {
setIsOpen(false);
}
}, 100);
};
const handleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role }));
setIsOpen(false);
};
const handleCreateChange = (value: string) => {
setCreateFormData({ accountName: value });
};
const handleInviteChange = (
field: keyof typeof inviteFormData,
value: string
) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
};
const handleCreateSubmit = () => {
if (!createFormData.accountName.trim()) {
setErrors({ accountName: 'Account name is required' });
return;
}
setErrors({});
onSubmit(createFormData, 'create');
};
const handleInviteSubmit = () => {
const { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {};
if (!i_accountName.trim()) errs.i_accountName = 'Account name is required';
if (!email.trim()) errs.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(email)) errs.email = 'Invalid email';
if (!role.trim()) errs.role = 'Role is required';
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
setErrors({});
onSubmit(inviteFormData, 'invite');
};
return (
<div className="mt-3 space-y-3">
{/* Create */}
<section className="border-gray-200 border-b">
<h2 className="text-lg font-medium mb-2 text-slate-500">Create Account</h2>
<div className="flex flex-col mb-6">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name
</label>
<div className="flex w-[40vw]">
<input
type="text"
placeholder="Enter account name"
value={createFormData.accountName}
onChange={(e) => handleCreateChange(e.target.value)}
className="flex-1 px-3 py-1.5 text-gray-600 rounded-md border border-gray-400 focus:outline-none focus:ring-1 focus:ring-[var(--primary)]"
/>
<button
type="button"
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"
>
Create
</button>
</div>
{errors.accountName && (
<span className="text-sm text-red-500 mt-1">{errors.accountName}</span>
)}
</div>
</section>
{/* Invite */}
<section className="border-gray-300">
<h2 className="text-lg font-medium mb-2 text-slate-500">Invite</h2>
<div className="grid grid-cols-2 gap-4 mb-6">
{/* Account Name */}
<div className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Account Name
</label>
<input
type="text"
placeholder="Enter account name"
value={inviteFormData.i_accountName}
onChange={(e) => handleInviteChange('i_accountName', e.target.value)}
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)]"
/>
{errors.i_accountName && (
<span className="text-sm text-red-500 mt-1">{errors.i_accountName}</span>
)}
</div>
{/* Email */}
<div className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Email
</label>
<input
type="email"
placeholder="Enter email address"
value={inviteFormData.email}
onChange={(e) => handleInviteChange('email', e.target.value)}
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)]"
/>
{errors.email && (
<span className="text-sm text-red-500 mt-1">{errors.email}</span>
)}
</div>
{/* Role Dropdown */}
<div className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">
Role
</label>
<div
className="relative w-full"
onBlur={handleBlur}
ref={dropdownRef}
tabIndex={-1}
>
<input
type="text"
placeholder="Select role"
value={inviteFormData.role}
readOnly
onFocus={handleFocus}
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"
/>
<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 ${
isOpen
? 'opacity-100 scale-100'
: 'opacity-0 scale-95 pointer-events-none'
}`}
>
{roles.map((r) => (
<div
key={r}
onMouseDown={() => handleSelect(r)}
className="px-4 py-2 text-[13px] hover:bg-gray-100 cursor-pointer"
>
{r}
</div>
))}
</div>
{errors.role && (
<span className="text-sm text-red-500 mt-1">{errors.role}</span>
)}
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
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"
>
Invite
</button>
</div>
</section>
</div>
);
};
export default CreateAndInvite;

View File

@ -0,0 +1,38 @@
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface UserProfileProps {
userData: UserData;
}
const Language : React.FC<UserProfileProps> = ({ userData }) => {
return (
<div className="max-w-[100vw] bg-white shadow-sm p-5 max-h-screen overflow-hidden">
<div className="px-3 ms-3 w-1/2 border border-gray-200 rounded-md space-y-3 ">
{/* Language */}
<div className="flex items-center justify-between py-3 border-b border-gray-100">
<span className="text-gray-700 font-medium">Language</span>
<span className="text-gray-500">{userData.language}</span>
</div>
</div>
</div>
)
}
export default Language

View File

@ -1,67 +1,321 @@
import { useState } from "react";
import { useEffect, useState, useRef } from "react";
import SettingNav from "./SettingNav";
import { useParams } from "react-router-dom";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import api from "../../config/api";
import UserProfile from './UserProfile';
import Language from "./Language";
import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
// Define the shape of state object
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface SubSection {
id: string;
label: string;
component?: React.ReactNode;
}
const Setting = () => {
const { page1 } = useParams<{ page1: string }>();
const renderContent = () => {
const location = useLocation();
const navigate = useNavigate();
// Refs for subsections within each main tab
const subSectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState("userProfile");
const [activeSubSection, setActiveSubSection] = useState<string>("");
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
// Define subsections for each main tab
const getSubSections = (tabId: string): SubSection[] => {
switch (tabId) {
case "userProfile":
return [
{ id: "profile-info", label: "Profile Information" },
{ 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:
return [];
}
};
// Function to scroll to specific subsection
const scrollToSubSection = (subSectionId: string) => {
const targetRef = subSectionRefs.current[subSectionId];
if (targetRef && scrollContainerRef?.current) {
const container = scrollContainerRef.current;
const target = targetRef;
// Calculate the position relative to the scroll container
const containerRect = container.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
// Calculate scroll position (accounting for some padding)
const scrollTop = container.scrollTop + targetRect.top - containerRect.top - 20;
// Smooth scroll to the target
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
};
// Handle subsection click with hash update
const handleSubSectionClick = (subSectionId: string) => {
setActiveSubSection(subSectionId);
// Update URL hash without causing page reload
navigate(`${location.pathname}#${subSectionId}`, { replace: true });
scrollToSubSection(subSectionId);
};
// Handle main tab click (independent of aside navigation)
const handleMainTabClick = (tabId: string) => {
setActiveTab(tabId);
// Reset subsection when switching main tabs
const subSections = getSubSections(tabId);
if (subSections.length > 0) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
} else {
setActiveSubSection("");
// Clear hash
navigate(location.pathname, { replace: true });
}
};
// Handle browser back/forward and direct URL navigation
useEffect(() => {
const hash = location.hash.replace('#', '');
if (hash) {
// Check if the hash corresponds to a valid subsection
const allSubSections = getSubSections(activeTab);
const validSubSection = allSubSections.find(section => section.id === hash);
if (validSubSection) {
setActiveSubSection(hash);
// Delay scroll to ensure component is rendered
setTimeout(() => {
scrollToSubSection(hash);
}, 100);
}
}
}, [location.hash, activeTab]);
// Render subsection content based on active tab and subsection
const renderSubSectionContent = (subSectionId: string) => {
switch (activeTab) {
case "userProfile":
return <div>User Profile content goes here...</div>;
case "language":
return <div>Language settings content goes here...</div>;
case "contactAll":
return <div>All Contacts content goes here...</div>;
case "contactAdd":
return <div>Add Contact form goes here...</div>;
case "contactRemove":
return <div>Remove Contact UI goes here...</div>;
case "contactShare":
return <div>Share Contact options go here...</div>;
switch (subSectionId) {
case "profile-info":
return <UserProfile userData={user} />;
case "signature":
return (
<Signature/>
);
default:
return;
}
default:
return <div>Coming Soon...</div>;
return (
<div className="bg-white p-6 rounded-lg shadow-sm">
<h3 className="text-lg font-semibold mb-4">Coming Soon</h3>
<p>This section is under development.</p>
</div>
);
}
};
const renderContent = () => {
const currentSubSections = getSubSections(activeTab);
if (currentSubSections.length === 0) {
// If no subsections, render the main content directly
switch (activeTab) {
case "userProfile":
return <UserProfile userData={user} />;
case "language":
return <Language userData={user} />;
case "create and invite":
return <CreateAndInvite onSubmit={handleSubmit} />;
default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
}
}
return (
<div className="space-y-8">
{currentSubSections.map((subSection) => (
<div
key={subSection.id}
ref={(el) => (subSectionRefs.current[subSection.id] = el)}
id={subSection.id} // This creates the anchor for hash navigation
className="scroll-mt-6"
>
{renderSubSectionContent(subSection.id)}
</div>
))}
</div>
);
};
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
{ id: "create and invite", label: "Create and invite", type: "settings" },
{ id: "language", label: "Language", type: "settings" },
{ id: "contactAll", label: "All Contacts", type: "contact" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
{ id: "contactShare", label: "Share Contact", type: "contact" },
];
const [activeTab, setActiveTab] = useState("userProfile");
const getUserDetails = async () => {
try {
let res = await api.get(`/api/user/details`)
console.log("getuser", res)
setUser(res?.data)
} catch (error) {
console.error("Error fetching user details:", error);
}
}
useEffect(() => {
getUserDetails()
}, [])
// Set initial subsection when component mounts or activeTab changes
useEffect(() => {
const subSections = getSubSections(activeTab);
const hash = location.hash.replace('#', '');
if (subSections.length > 0) {
// If there's a hash and it's valid for current tab, use it
if (hash && subSections.find(s => s.id === hash)) {
setActiveSubSection(hash);
}
// If no valid hash or no current activeSubSection, use first subsection
else if (!activeSubSection || !subSections.find(s => s.id === activeSubSection)) {
const firstSubSectionId = subSections[0].id;
setActiveSubSection(firstSubSectionId);
// Update URL hash if no hash exists
if (!hash) {
navigate(`${location.pathname}#${firstSubSectionId}`, { replace: true });
}
}
}
}, [activeTab]);
// Auto-scroll to subsection when activeSubSection changes
useEffect(() => {
if (activeSubSection) {
setTimeout(() => {
scrollToSubSection(activeSubSection);
}, 100);
}
}, [activeSubSection]);
const handleSubmit = async (data: FormDataType, action: ActionType) => {
try {
if (action === 'create') {
console.log('Creating account:', data);
} else if (action === 'invite') {
console.log('Inviting user:', data);
}
} catch (err) {
console.error('Submission error:', err);
}
};
return (
<>
<SettingNav />
<div className="min-h-screen bg-[#f9fafa]">
{/* Top Nav Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-4">
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
{/* Main Navigation Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">
<div className="flex gap-6">
{tabs.map((tab) => (
tab.type === page1 && <button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${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-gray-600 hover:text-blue-500"
{tabs.map((tab) =>
tab.type === page1 && (
<button
key={tab.id}
onClick={() => handleMainTabClick(tab.id)}
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
${
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-gray-600 hover:text-blue-500"
}`}
>
{tab.label}
</button>
))}
>
{tab.label}
</button>
)
)}
</div>
</div>
{/* Main Content Area */}
<div className="px-8 py-6">{renderContent()}</div>
{/* Content with Aside Navigation */}
<div className="flex mt-3 px-8 flex-1 min-h-0">
{/* Fixed Aside Navigation - Shows subsections of active tab */}
{activeTab === 'userProfile' &&
<section className="flex-shrink-0 mr-6 w-50 relative">
<div className="sticky top-6 z-20">
<AsideSection
activeSubSection={activeSubSection}
onSubSectionClick={handleSubSectionClick}
subSections={getSubSections(activeTab)}
mainTabTitle={tabs.find(t => t.id === activeTab)?.label || ""}
/>
</div>
</section>
}
{/* Scrollable Main Content */}
<div
ref={scrollContainerRef}
className="flex-1 min-w-0 overflow-y-auto no-scrollbar"
>
{renderContent()}
</div>
</div>
</div>
</>
)
}

View File

@ -34,7 +34,7 @@ const SettingNav = () => {
</button>
<Link
to="/settings"
to="/s/settings"
aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
>

View File

@ -0,0 +1,382 @@
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 { MdImage, MdCloudUpload, MdStorage } from 'react-icons/md';
import api from '../../../config/api';
interface AddSignatureModalProps {
setShowAddSignature?: (value: boolean) => void;
}
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
const AddSignatureModal: React.FC<AddSignatureModalProps> = ({ setShowAddSignature }) => {
const [activeTab, setActiveTab] = useState<'choose' | 'draw' | 'upload'>('choose');
const [fullName, setFullName] = useState('');
const [initials, setInitials] = useState('');
// Sample signature options for the CHOOSE tab
const signatureOptions = [
{ id: 1, name: 'print', initials: 'LA', style: 'print' },
{ id: 2, name: 'cursive', initials: 'LA', style: 'cursive' },
{ id: 3, name: 'script', initials: 'LA', style: 'script' },
{ id: 4, name: 'elegant', initials: 'LA', style: 'elegant' },
{ id: 5, name: 'modern', initials: 'LA', style: 'modern' },
{ id: 6, name: 'classic', initials: 'LA', style: 'classic' },
];
const [selectedSignature, setSelectedSignature] = useState<number>(1);
// Store the actual file
const [file1, setFile1] = useState<File | null>(null);
const [file2, setFile2] = useState<File | null>(null);
// Store the preview URL
const [preview1, setPreview1] = useState<string | null>(null);
const [preview2, setPreview2] = useState<string | null>(null);
//user
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
// Navigation items
const navigationItems = [
{ id: 'choose', label: 'Choose', icon: <MdImage 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" /> },
];
// Cleanup preview URLs when component unmounts or image changes
useEffect(() => {
return () => {
if (preview1) URL.revokeObjectURL(preview1);
if (preview2) URL.revokeObjectURL(preview2);
};
}, [preview1, preview2]);
const handleImageChange = (
e: React.ChangeEvent<HTMLInputElement>,
setFile: React.Dispatch<React.SetStateAction<File | null>>,
setPreview: React.Dispatch<React.SetStateAction<string | null>>
) => {
const file = e.target.files?.[0];
if (file) {
setFile(file);
const previewUrl = URL.createObjectURL(file);
setPreview(previewUrl);
}
};
const renderContent = () => {
switch (activeTab) {
case 'choose':
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 === option.id}
onChange={() => setSelectedSignature(option.id)}
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 (
<div className="h-full flex flex-col">
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-900 mb-2">Draw Signature</h2>
<p className="text-sm text-gray-600">Create your signature by drawing directly on the canvas</p>
</div>
<div className="flex-1 flex flex-col">
<div className="grid grid-cols-12 gap-6 mb-6">
{/* Draw area 1 */}
<div className="col-span-8">
<label className="block text-sm font-medium text-gray-700 mb-2">Full Signature</label>
<div 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">
<div className="text-center">
{/* <Edit3 className="mx-auto h-12 w-12 text-gray-400 mb-3" /> */}
<p className="text-sm text-gray-500">Click and drag to draw your signature</p>
<p className="text-xs text-gray-400 mt-1">Use mouse or touch to create your signature</p>
</div>
</div>
</div>
{/* Draw area 2 */}
<div className="col-span-4">
<label className="block text-sm font-medium text-gray-700 mb-2">Initials</label>
<div 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">
<div className="text-center">
{/* <Edit3 className="mx-auto h-8 w-8 text-gray-400 mb-2" /> */}
<p className="text-xs text-gray-500">Draw initials</p>
</div>
</div>
</div>
</div>
<div className="flex gap-3">
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors flex items-center gap-2">
{/* <X className="w-4 h-4" /> */}
Clear
</button>
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
Undo
</button>
<button className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
Redo
</button>
</div>
</div>
</div>
);
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-48 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)}
/>
</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-48 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">
<MdImage 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)}
/>
</div>
</div>
</div>
</div>
</div>
);
default:
return null;
}
};
const getUserDetails = async () => {
try {
let res = await api.get(`/api/user/details`)
console.log("getuser", res)
setUser(res?.data)
} catch (error) {
console.error("Error fetching user details:", error);
}
}
useEffect(() => {
getUserDetails()
}, [])
return (
<div className="fixed inset-0 backdrop-blur-sm bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-md shadow-xl w-full max-w-[80vw] h-[80vh] flex relative">
{/* Sidebar Navigation */}
<div className="w-64 bg-gray-50 rounded-l-lg border-r border-gray-200 flex flex-col">
{/* Header */}
<div className="px-4 py-4 border-b border-gray-200">
<div className="flex items-center space-x-3">
<div className='px-1 py-1'>
<h3 className="text-md font-semibold text-gray-900">Add Signature</h3>
<p className="text-xs text-gray-500">Create your digital signature</p>
</div>
</div>
</div>
{/* Form Inputs */}
<div className="px-6 py-2.5 border-b border-gray-200">
<div className="space-y-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Initials
</label>
<input
type="text"
value={initials}
onChange={(e) => setInitials(e.target.value)}
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"
/>
</div>
</div>
</div>
{/* Navigation Menu */}
<div className="flex-1 px-6 py-2.5">
<nav className="space-y-2">
{navigationItems.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as 'choose' | '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
? 'bg-blue-50 text-blue-700 border border-blue-200'
: '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>{item.label}</span>
</button>
))}
</nav>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col ">
{/* Header with close button */}
{/* Content */}
<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()}
</div>
{/* Footer Buttons in Main Area */}
<div className="p-4 border-t border-gray-200 flex justify-end gap-3">
<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"
onClick={() => setShowAddSignature?.(false)}
>
CANCEL
</button>
<button className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors">
CREATE SIGNATURE
</button>
</div>
</div>
</div>
</div>
);
};
export default AddSignatureModal;

View File

@ -0,0 +1,200 @@
import React, { useState } from 'react';
import { FiEdit3, FiTrash2, FiPlus, FiMoreVertical } from 'react-icons/fi';
import AddSignatureModal from './AddSignatureModal';
interface Signature {
id: string;
signature: string;
initial: string;
createdAt: string;
}
const SignatureManager: React.FC = () => {
const [signatures, setSignatures] = useState<Signature[]>([
{
id: '1',
signature: 'https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg',
initial: 'AB',
createdAt: 'dca3e646-62cd-4cc...'
},
{
id: '2',
signature: '∼∼⌒∼∼⌒∼',
initial: 'AB',
createdAt: 'dca3e646-62cd-4cc...'
},
{
id: '3',
signature: 'Digital Signature',
initial: 'DS',
createdAt: 'dca3e646-62cd-4cc...'
}
]);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
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);
};
return (
<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 */}
<div className="bg-white rounded-md shadow-md overflow-hidden border border-gray-100">
{/* Table Header */}
<div className="bg-gray-50 border-b border-gray-200">
<div className="grid grid-cols-12 gap-4 px-6 py-4">
<div className="col-span-5">
<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>
{/* Signature Image */}
<div className="bg-white rounded-lg p-1 flex items-center justify-center ">
<img
src="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg"
className="h-[50px] w-[250px] object-contain"
alt="Signature"
/>
</div>
{/* Signature ID */}
<div className="text-xs text-gray-500 truncate">
{signature.createdAt}
</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="https://www.signwell.com/assets/vip-signatures/muhammad-ali-signature-3f9237f6fc48c3a04ba083117948e16ee7968aae521ae4ccebdfb8f22596ad22.svg"
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>
{/* 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>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold flex items-center gap-2 mx-auto transition-all duration-200">
<FiPlus className="w-5 h-5" />
ADD SIGNATURE
</button>
</div>
)}
</div>
{/* Click outside to close dropdown */}
{activeDropdown && (
<div
className="fixed inset-0 z-0"
onClick={() => setActiveDropdown(null)}
/>
)}
{showAddSignature && (
<AddSignatureModal setShowAddSignature={setShowAddSignature}/>
)}
</div>
);
};
export default SignatureManager;

View File

@ -0,0 +1,63 @@
import { FiX, FiEdit2 } from 'react-icons/fi';
interface Account {
id: number;
isDefault: boolean;
name: string;
}
interface UserData {
id: string;
firstName: string;
lastName: string;
email: string;
language: string;
account: Account;
}
interface UserProfileProps {
userData: UserData;
}
const UserProfile: React.FC<UserProfileProps> = ({ userData }) => {
const getInitials = (firstName: string, lastName: string) => {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
};
return (
<div className="bg-white rounded-md shadow-sm flex w-full p-6">
{/* Profile Picture */}
<div className="flex-shrink-0 text-center">
<div className="w-20 h-20 bg-green-800 text-white font-bold text-2xl rounded-full flex items-center justify-center shadow-lg">
{getInitials(userData.firstName, userData.lastName)}
</div>
</div>
{/* Info Fields */}
<div className="flex flex-col justify-center flex-1 ms-10">
{/* Row: Name */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.firstName} {userData.lastName}
</div>
</div>
{/* Row: Email */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Email</div>
<div className="text-gray-400 font-medium text-[13px]">{userData.email}</div>
</div>
{/* Row: Account Name */}
<div className="flex py-1">
<div className="text-gray-500 font-medium text-[13px] w-32">Account name</div>
<div className="text-gray-400 font-medium text-[13px]">
{userData.account.name}
</div>
</div>
</div>
</div>
);
};
export default UserProfile

View File

@ -1,9 +1,12 @@
import { AiFillSignature } from 'react-icons/ai';
import { useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { LuGitGraph } from 'react-icons/lu';
import AccountNav from '../accounts/AccountNav';
import { useEffect, useState } from 'react';
import api from '../../config/api';
import SigningOrderDiagram from '../../SigningOrderDiagram ';
import { IoCloseOutline } from 'react-icons/io5';
import { MdEditDocument } from "react-icons/md";
// Sample recipient data — you can replace this with actual API data later
const recipients = [
@ -27,6 +30,8 @@ const recipients = [
const TransactionView = () => {
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
console.log("account" , accountId , transactionId)
if (!accountId || !transactionId) {
window.location.href = "/";
@ -34,6 +39,8 @@ const TransactionView = () => {
}
const [transactionDetails, setTransactionDetails] = useState(null);
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
console.log(transactionDetails, "line 37");
const fetchData = async () => {
try {
@ -57,7 +64,7 @@ const TransactionView = () => {
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
{/* Header */}
<h1 className='text-lg font-semibold text-gray-800'>
Complete with Forms Made Easy: <span className='text-slate-600'>{transactionDetails.transaction.name ? transactionDetails.transaction.name : "Untitled"}</span>
Complete with Forms Made Easy: <span className='text-slate-600'>{ transactionDetails.transaction.name ?? "Untitled"}</span>
</h1>
<div className='mt-2'>
@ -77,18 +84,23 @@ const TransactionView = () => {
>
Sign
</button> */}
<button className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100'>
<button className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold'>
More
</button>
<Link className='cursor-pointer border text-green-700 border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100 font-bold' to={`/process-step-two/${accountId}/${transactionId}`}>
Edit
</Link>
</div>
<hr className='my-6 border-slate-200' />
{/* Recipients Section */}
{transactionDetails.recipients.length > 0 &&
<div>
<div className='flex justify-between items-center text-md'>
<h2 className='text-lg font-semibold text-gray-800 cursor-pointer'>Signers</h2>
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer'>
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer' onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}>
<LuGitGraph className='text-[0.9rem] text-[var(--primary)]' />
Signing Graph
</p>
@ -122,6 +134,7 @@ const TransactionView = () => {
</div>
))}
</div>
}
<div>
@ -137,7 +150,11 @@ const TransactionView = () => {
<div className='border-3 rounded-sm border-slate-300 relative text-xs'>
<img style={{ width: "200px", height: "160px" }} src="https://images.unsplash.com/photo-1746469535771-71a672e8719f?q=80&w=870&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="" />
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
<p className='text-[0.7rem] text-slate-500'>1 of 10</p>
<p className='text-[0.7rem] text-slate-500 flex justify-between items-center'>
<span> 1 of 10</span>
<span className='cursor-pointer'><Link to={`/process-step-two/${accountId}/${transactionId}`}><MdEditDocument size={16}/></Link></span>
</p>
</div>
</div>
@ -146,6 +163,24 @@ const TransactionView = () => {
</div>
</div>
{signingOrderModal.open &&
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center overflow-hidden">
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full ">
<div className="pb-2 text-white flex justify-between items-center bg-[var(--primary)] px-2 py-3 rounded-t-lg">
<p className="font-semibold">Signing Order</p>
<div
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: false })}
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
<IoCloseOutline />
</div>
</div>
<span className='overflow-y-auto'>
<SigningOrderDiagram signers={transactionDetails.recipients} />
</span>
</div>
</div>
}
</>
}
</>

View File

@ -74,3 +74,15 @@ canvas {
.animate-text {
animation: textColorChange 2s infinite ease-in-out;
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}