Common Nav

This commit is contained in:
=bhagwanSingh 2025-07-11 16:27:40 +05:30
parent 2f2b4f3aca
commit 71ef8913d7
7 changed files with 520 additions and 51 deletions

View File

@ -14,18 +14,10 @@ import SignDocument from "./components/client/SignDocument";
import Transactions from "./components/transactions/Transactions";
import ProtectedRoute from "./components/routes_helper/ProtectedRoute";
import { HashRouter } from 'react-router-dom';
import { useDispatch, useSelector } from "react-redux";
import { fetchUserDetails } from "./store/slices/userSlice";
import { AppDispatch, RootState } from "./store/store";
import { useEffect, useLayoutEffect } from "react";
import Setting from "./components/setting/Setting";
import Accounts from "./components/accounts/Accounts";
import Account from "./components/accounts/Account";
import TransactionView from "./components/transactions/TransactionView";
import AccountTransactionWrapper from "./components/AccountTransactionWrapper";
import SigningOrderDiagram from "./SigningOrderDiagram ";
import Templates from "./components/templates/Templates";
const App = () => {
@ -42,16 +34,15 @@ const App = () => {
</ProtectedRoute>
}>
<Route path="/" element={<Home />} />
<Route path="/templates" element={<Templates />} />
{/* <Route path="/s/settings" element={<Setting />} /> */}
<Route element={<AccountTransactionWrapper />}>
{/* <Route element={<AccountTransactionWrapper />}> */}
<Route path="/accounts" element={<Accounts />} />
<Route path="/accounts/:accountId/transactions" element={<Account />} />
<Route path="/transactions" element={<Account />} />
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
<Route path="/transactions/:filterParam" element={<Transactions />} />
</Route>
<Route path="/templates" element={<Templates />} />
{/* </Route> */}
<Route path="/s/:page1" element={<Setting />} />
</Route>

View File

@ -1,13 +1,14 @@
import { Outlet } from 'react-router-dom'
import AccountNav from './accounts/AccountNav'
// import AccountNav from './accounts/AccountNav'
import CommonNav from './CommonNav'
const AccountTransactionWrapper = () => {
return (
<>
<AccountNav />
<CommonNav />
<Outlet />
</>
)

View File

@ -0,0 +1,363 @@
import React, { useEffect, useRef, useState } from 'react';
// import { useSelector } from 'react-redux';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser, FaBuilding, FaChevronDown, FaCheck } from "react-icons/fa";
import api from '../config/api';
// import { FaUser, FaSignOutAlt, FaBuilding, FaChevronDown, FaCheck } from 'react-icons/fa';
// import { RootState } from '../../store/store';
// import { UserState } from '../../store/slices/userSlice';
interface CommonNavProps {
accountName?: string | null;
transactionName?: string | null;
}
// 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 AccountDataArray {
accountId: number, accountName: string, role: string, status: boolean
}
const CommonNav: React.FC<CommonNavProps> = ({ accountName = null, transactionName = null }) => {
// const user: UserState = useSelector((state: RootState) => state.user);
const [open, setOpen] = useState(false);
const [accountsOpen, setAccountsOpen] = useState(false);
const [loadingAccounts, setLoadingAccounts] = useState(false);
const [accounts, setAccounts] = useState<AccountDataArray[]>([]);
const [hasModalBeenClosed, setHasModalBeenClosed] = useState(false); // Track if modal was closed
const dropdownRef = useRef(null);
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
const getAccountDetails = async () => {
try {
let res = await api.get('/api/user/accounts')
let items = res?.data
console.log('items', items)
// Extract only the fields you want
const cleanedData: AccountDataArray[] = items.map((item: any) => ({
accountId: item.accountId,
accountName: item.accountName,
role: item.role,
status: item.status,
}));
setAccounts(cleanedData);
} catch (error) {
console.log(error)
}
}
const handleAccountsClick = async () => {
setAccountsOpen(!accountsOpen);
if (!accountsOpen) {
setLoadingAccounts(true);
// Fetch API if:
// 1. No accounts data exists (first time), OR
// 2. Modal was previously closed (need fresh data)
if (accounts.length === 0 || hasModalBeenClosed) {
await getAccountDetails();
setHasModalBeenClosed(false); // Reset the flag after fetching
}
setLoadingAccounts(false);
}
};
const handleAccountSwitch = (account) => {
setUser(prev => ({
...prev,
account: { name: account.name, id: account.id }
}));
// Update accounts active status
setAccounts(prevAccounts =>
prevAccounts.map(acc => ({
...acc,
isActive: acc.id === account.id
}))
);
setAccountsOpen(false);
setOpen(false);
};
const navigate = useNavigate()
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()
}, [])
const location = useLocation();
const pathnames = location.pathname
.split("/")
.filter((x) => x && isNaN(Number(x))); // remove numeric IDs
const labelMap: Record<string, string> = {
accounts: "Accounts",
transactions: "Transactions",
templates: "Templates"
// Add more routes here if needed
};
// const [open, setOpen] = useState(false);
// const dropdownRef = useRef<HTMLDivElement>(null);
// Close dropdown when clicked outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
if (open) {
setHasModalBeenClosed(true); // Mark modal as closed when clicking outside
}
setOpen(false);
setAccountsOpen(false); // Also close accounts dropdown
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
const handleLogout = async () => {
try {
let res = await api.post('/api/auth/logout')
if (res.status === 200) {
setOpen(false);
navigate("/login")
}
} catch (error) {
console.log(error)
}
};
// Handle main dropdown toggle
const handleMainDropdownToggle = () => {
if (open) {
setHasModalBeenClosed(true); // Mark as closed when manually closing
}
setOpen(!open);
if (!open) {
setAccountsOpen(false); // Close accounts dropdown when opening main dropdown
}
};
const formatSegment = (segment: string) =>
segment
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
const ShimmerRow = () => (
<div className="animate-pulse px-4 py-3 border-b border-gray-100 last:border-b-0">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gray-200 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
<div className="w-16 h-6 bg-gray-200 rounded"></div>
</div>
</div>
);
return (
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
<nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
{pathnames.length === 0 ? (
<span className="text-gray-900 font-semibold">Dashboard</span>
) : (
pathnames.map((segment, index) => {
const label = labelMap[segment] || formatSegment(segment);
const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1;
return (
<span key={to} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
{!isLast ? (
<Link
to={to}
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
>
{label}
</Link>
) : (
<span className="text-gray-900 font-semibold">{label}</span>
)}
</span>
);
})
)}
</nav>
<div className="flex items-center space-x-2">
{/* User Dropdown */}
<div className="relative" ref={dropdownRef}>
{/* Avatar Button */}
<button
className="flex items-center space-x-2 p-1 cursor-pointer rounded-lg transition-colors"
onClick={handleMainDropdownToggle}
aria-expanded={open}
aria-haspopup="true"
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold text-sm shadow-md">
{user ? (
<span
className="w-full h-full object-cover p-1.5 font-bold"
>{(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
) : (
<FaUser className="text-lg" />
)}
</div>
</button>
{/* Enhanced Dropdown */}
{open && (
<div className="absolute right-0 mt-2 w-72 top-8 rounded-xl shadow-xl bg-white border border-gray-200 z-50 overflow-hidden">
{/* User Info Section */}
<div className="px-4 py-4 bg-gradient-to-r from-blue-50 to-purple-50 border-b border-gray-100">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full overflow-hidden bg-gradient-to-br from-green-500 to-blue-600 flex items-center justify-center text-white font-semibold shadow-md">
{(user) ? (
<span
className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold"
>
{(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
) : (
<FaUser className="text-lg" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 truncate">{user.firstName + ' ' + user.lastName || '-'}</div>
<div className="text-sm text-gray-600 truncate">{user.email || '-'}</div>
<div className="text-xs text-gray-500 mt-1">
Account #{user?.account?.name || '-'}
</div>
</div>
</div>
</div>
{/* Account List Section */}
<div className="border-b border-gray-100">
<button
onClick={handleAccountsClick}
className="w-full flex items-center justify-between px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors cursor-pointer"
>
<div className="flex items-center">
<FaBuilding className="mr-3 text-gray-500" />
<span>Switch Account</span>
</div>
<FaChevronDown className={`text-gray-400 transition-transform ${accountsOpen ? 'rotate-180' : ''}`} />
</button>
{/* Accounts Dropdown */}
{accountsOpen && (
<div className="bg-gray-50 max-h-64 overflow-y-auto">
{loadingAccounts ? (
<div className="space-y-0">
<ShimmerRow />
<ShimmerRow />
</div>
) : (
<div className="divide-y divide-gray-200">
{accounts.length > 0 ? (accounts?.map((account) => (
<button
key={account.accountId}
// onClick={() => handleAccountSwitch(account)}
className="w-full px-4 py-3 hover:bg-white transition-colors cursor-pointer text-left"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-gray-200 to-gray-400 flex items-center justify-center text-white font-semibold text-xs">
{account.accountName.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<span className="font-medium text-gray-900 text-sm">{account.accountName}</span>
{account.status && (
<FaCheck className="text-green-500 text-xs" />
)}
</div>
<div className="text-xs text-gray-600 truncate">{account.company}</div>
</div>
</div>
<div className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
{account.role}
</div>
</div>
</button>
))) : <span className='text-gray-400 font-medium text-[12px] flex justify-center p-3'>No Account...</span>}
</div>
)}
</div>
)}
</div>
{/* Logout Section */}
<div className="border-t border-gray-100 ">
<button
onClick={handleLogout}
className="w-full flex items-center px-4 py-3 text-sm text-red-600 hover:bg-red-50 transition-colors cursor-pointer"
>
<FaSignOutAlt className="mr-3" />
<span>Log Out</span>
</button>
</div>
</div>
)}
</div>
</div>
</header>
);
};
export default CommonNav;

View File

@ -17,6 +17,7 @@ import confetti from 'canvas-confetti'
import { useWindowSize } from 'react-use'
import { FaCheckCircle } from 'react-icons/fa'
import { IoClose } from 'react-icons/io5'
import CommonNav from '../CommonNav'
function Dashboard() {
const [showSideBar, setShowSideBar] = React.useState(true)
@ -103,31 +104,31 @@ function Dashboard() {
useEffect(() => {
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
if (showConfetti) {
const duration = 3000;
const end = Date.now() + duration;
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
const interval = setInterval(() => {
if (Date.now() > end) {
clearInterval(interval);
return;
}
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
confetti({
particleCount: 80, // more particles
angle: Math.random() * 360, // fire in random direction
spread: 220, // wider spread
startVelocity: 15, // more power
scalar: 1.2, // bigger pieces (default is 1)
origin: {
x: Math.random(),
y: Math.random() * 0.6,
},
zIndex: 9999,
});
}, 350); // fire every 150ms
}
}, [showConfetti]);
@ -173,6 +174,7 @@ function Dashboard() {
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
{/* Main Content */}
<div className='px-2 py-1.5'>
<CommonNav />
<Outlet />
</div>
</div>

View File

@ -176,7 +176,6 @@ const Home = () => {
return (
<>
<AccountNav />
<Cards
onDateRangeChange={handleDateRangeChange}
dashboardData={dashboardData}

View File

@ -14,11 +14,9 @@ import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
import { fetchDocuments } from '../../../store/slices/documentSlice';
import { FaQuestionCircle } from 'react-icons/fa';
import typeToIconMap from '../../../helper/iconMap';
<<<<<<< HEAD
import { IoAdd } from 'react-icons/io5';
=======
import Loader from '../../dashboard/Loader';
>>>>>>> merge2
export interface DroppedItemsObj {
[key: string]: DroppedItem[]
@ -216,20 +214,16 @@ const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
"type": dropTag.type.toUpperCase(),
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
// validation: {
<<<<<<< HEAD
"minLength": 0,
"maxLength": 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
"regex": "regex" in dropTag ? dropTag.regex : [],
// },
=======
"minLength": "minLength" in dropTag ? dropTag.minLength : 0,
"maxLength": "maxLength" in dropTag ? dropTag.maxLength : 0,
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
"regex": "regex" in dropTag ? dropTag.regex : []
// }
,
>>>>>>> merge2
"documentId": dropTag.documentId,
"options": ("options" in dropTag) ? dropTag.options : [],
"pageIndex": dropTag.pageIndex,

View File

@ -1,9 +1,128 @@
import React from 'react'
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
import { FaCheck, FaDatabase } from "react-icons/fa";
import AddAccountModal from "../accounts/AddAccountModal";
import { Bounce, toast } from "react-toastify";
import api from "../../config/api";
import { useState } from "react";
const Templates = () => {
return (
<div>Templates</div>
)
}
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
export default Templates
const handleAction = async (accountId: string, action: 'accept' | 'reject') => {
try {
let response = await api.post(`/api/accounts/${accountId}/accept/invite`)
console.log(response)
if (response.data.status === 202) {
toast.success("Login Successful", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
} catch (error) {
console.error('Error posting data:', error);
} finally {
setOpenMenuId(null); // close menu
}
};
return (
<>
<div className="overflow-x-auto py-2">
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<tr>
<th className="px-4 py-3 w-12"></th>
<th className="px-4 py-3 text-left">Name</th>
<th className="px-4 py-3 text-center">Owner</th>
<th className="px-4 py-3 text-center">Created Date</th>
<th className="px-4 py-3 text-center">Last Change</th>
<th className="px-4 py-3 text-center"></th>
<th className="px-4 py-3 text-center"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr
className="hover:bg-gray-50 cursor-pointer"
>
<td className="px-4 py-3">
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center">
{/* {selectedTransactions.includes(tx.id) && (
<FaCheck className="text-purple-600 text-sm" />
)} */}
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<span>{"test"}</span>
</div>
</td>
<td className="px-4 py-3 text-center">{"1"}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium`}>
{"role"}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium`}>
{"text"}
</span>
</td>
<td>
</td>
<td className="px-4 py-3 text-center relative">
<button
className="mx-auto"
aria-label="Actions menu"
>
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black" />
</button>
{/* Dropdown menu */}
{/* {openMenuId === "1" && (
<div
className="absolute right-13 top-6 mt-1 w-18 bg-white border text-[13px] font-bold border-gray-300 rounded-md shadow-lg z-10"
onClick={(e) => e.stopPropagation()} // prevent row click closing
>
<button
className="w-full px-3 py-1 text-left text-green-700 hover:bg-green-100 rounded-t-md cursor-pointer"
onClick={() => handleAction(1, 'accept')}
>
Accept
</button>
<button
className="w-full px-3 py-1 text-left text-red-700 hover:bg-red-100 rounded-b-md cursor-pointer"
onClick={() => handleAction(1, 'reject')}
>
Reject
</button>
</div>
)} */}
</td>
</tr>
</tbody>
</table>
</div>
</>
);
};
export default Templates;