Compare commits

...

2 Commits

12 changed files with 576 additions and 165 deletions

View File

@ -42,6 +42,7 @@ const App = () => {
}>
<Route path="/" element={<Home />} />
<Route path="/templates" element={<Home />} />
{/* <Route path="/s/settings" element={<Setting />} /> */}
<Route element={<AccountTransactionWrapper />}>
<Route path="/accounts" element={<Accounts />} />

View File

@ -6,6 +6,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { FaCheck } from "react-icons/fa";
import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal";
import { Bounce, toast } from "react-toastify";
interface Transaction {
id: number;
@ -24,6 +25,8 @@ const Account = () => {
const { accountId } = useParams<{ accountId: string }>();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [accountsData, setAccountsData] = useState([])
const [inviteLoading, setInviteLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -44,22 +47,62 @@ const Account = () => {
if (accountId) fetchTransactions();
}, [accountId]);
const getAccountDetails = async () => {
try {
let res = await api.get('/api/user/accounts')
let items = res?.data
setAccountsData(items)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getAccountDetails()
}, [])
const navigate = useNavigate();
const handleTransactionClick = (id: number) => {
navigate(`/accounts/${accountId}/transactions/${id}`);
};
const handleAddAccount = (accountName: string) => {
console.log(accountId)
setIsModalOpen(false);
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
setInviteLoading(true);
try {
const payloadInvite = {
email: data.email,
role: data.role
}
let res = await api.post(`/api/accounts/${data.i_accountName}/invite`, payloadInvite)
if (res?.data) {
toast.success("Invite Sent Successfully...!", {
position: "top-center",
autoClose: 1500,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await fetchTransactions()
setIsModalOpen(false);
}
} catch (err) {
console.error('Failed to invite', err);
} finally {
setInviteLoading(false);
}
};
return (
<>
<Cards type="account" onOpenModal={handleOpenModal} />
<Cards type="transactions" onOpenModal={handleOpenModal} />
{transactions.length > 0 && (
<div className="overflow-x-auto py-4">
<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>
@ -116,8 +159,8 @@ const Account = () => {
</table>
</div>
)}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)}
onAddAccount={handleAddAccount} />}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
accountOptions={accountsData} />}
</>
);
};

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
// import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { FaSignOutAlt,FaUser } from "react-icons/fa";
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt, FaUser } from "react-icons/fa";
import api from '../../config/api';
// import { RootState } from '../../store/store';
@ -32,17 +32,19 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
// const user: UserState = useSelector((state: RootState) => state.user);
const [user, setUser] = useState<UserData>({
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
id: "",
firstName: "",
lastName: "",
email: "",
language: "",
account: {
id: 0,
isDefault: false,
name: "",
},
});
const navigate = useNavigate()
const getUserDetails = async () => {
try {
@ -68,6 +70,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
const labelMap: Record<string, string> = {
accounts: "Accounts",
transactions: "Transactions",
templates: "Templates"
// Add more routes here if needed
};
@ -86,42 +89,58 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleLogout = () => {
// Add logout logic here
console.log('Logging out...');
setOpen(false);
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)
}
};
const formatSegment = (segment: string) =>
segment
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
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">
{pathnames.map((segment, index) => {
const label = labelMap[segment] || segment;
const to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1;
<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}>
{!isLast ? (
<>
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="mx-2 text-gray-400">{">"}</span>
</>
) : (
<span className="text-gray-900 font-semibold">{label}</span>
)}
</span>
);
})}
) : (
<span className="text-gray-900 font-semibold">{label}</span>
)}
</span>
);
})
)}
</nav>
<div className="flex items-center space-x-2">
{/* User Dropdown */}
@ -137,7 +156,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
{user ? (
<span
className="w-full h-full object-cover p-1.5 font-bold"
>{(user.firstName.slice(0,1) + user.lastName.slice(0,1)).toUpperCase()}</span>
>{(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
) : (
<FaUser className="text-lg" />
)}
@ -153,14 +172,15 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
<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.slice(0,1) + user.lastName.slice(0,1)).toUpperCase()}</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="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 || '-'}

View File

@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom";
import Cards from "../home/Cards";
import AddAccountModal from "./AddAccountModal";
import { Bounce, toast } from "react-toastify";
import Loader from "../../helper/Loader";
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
@ -63,9 +64,31 @@ const Accounts = () => {
navigate(`/accounts/${id}/transactions`);
};
const handleAddAccount = (accountName: string) => {
setAccounts((prev) => [...prev, accountName]);
setIsModalOpen(false);
const handleAddAccount = async (accountName: string) => {
try {
const payloadAccount = {
name: accountName
}
let res = await api.post(`/api/user/accounts`, payloadAccount)
if (res?.data) {
toast.success("Account Add Successfully...!", {
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
await fetchAccounts()
setIsModalOpen(false);
}
} catch (error) {
console.log(error)
} finally {
}
};
const handleMenuToggle = (accountId: string) => {

View File

@ -1,51 +1,214 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { FaSpinner } from "react-icons/fa";
import Loader from "../../helper/Loader";
interface AddAccountModalProps {
onClose: () => void;
onAddAccount: (accountName: string) => void;
interface AccountOption {
userId: number;
userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
}
const AddInviteModal : React.FC<AddAccountModalProps> = ({ onClose, onAddAccount }) => {
const [inputValue, setInputValue] = useState('');
const handleAddClick = () => {
if (inputValue.trim()) {
onAddAccount(inputValue.trim());
setInputValue(''); // clear input after submit
interface AddInviteModalProps {
onClose: () => void;
onInvite: (data: { i_accountName: number; email: string; role: string }) => void;
accountOptions: AccountOption[];
accountOptionsLoading: boolean;
inviteLoading: boolean;
}
const AddInviteModal: React.FC<AddInviteModalProps> = ({
onClose,
onInvite,
accountOptions,
accountOptionsLoading,
inviteLoading,
}) => {
const [inviteFormData, setInviteFormData] = useState({
i_accountName: 0,
email: "",
role: "",
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [accountSearchTerm, setAccountSearchTerm] = useState("");
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(null);
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
const [isRoleDropdownOpen, setIsRoleDropdownOpen] = useState(false);
const accountDropdownRef = useRef<HTMLDivElement>(null);
const roleDropdownRef = useRef<HTMLDivElement>(null);
const roles = ["ADMIN", "EDITOR", "VIEWER"];
// Handlers
const handleInviteChange = (field: keyof typeof inviteFormData, value: string) => {
setInviteFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" }));
}
};
const handleAccountDropdownFocus = () => setIsAccountDropdownOpen(true);
const handleRoleDropdownFocus = () => setIsRoleDropdownOpen(true);
const handleDropdownBlur = (
e: React.FocusEvent<HTMLDivElement>,
ref: React.RefObject<HTMLDivElement>,
setOpen: (open: boolean) => void
) => {
setTimeout(() => {
const active = document.activeElement as HTMLElement;
if (ref.current && !ref.current.contains(active)) {
setOpen(false);
}
}, 100);
};
const handleAccountSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setAccountSearchTerm(val);
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);
setErrors((prev) => ({ ...prev, i_accountName: "" }));
};
const handleRoleSelect = (role: string) => {
setInviteFormData((prev) => ({ ...prev, role }));
setIsRoleDropdownOpen(false);
setErrors((prev) => ({ ...prev, role: "" }));
};
const handleSubmit = () => {
const { i_accountName, email, role } = inviteFormData;
const errs: { [key: string]: string } = {};
if (!i_accountName) errs.i_accountName = "Account 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;
}
onInvite(inviteFormData);
};
const filteredOptions = accountOptions.filter((option) =>
option.accountName.toLowerCase().includes(accountSearchTerm.toLowerCase())
);
return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
<div className="bg-white rounded-lg shadow-xl p-6 w-[26rem]">
<h3 className="text-lg font-medium text-gray-800 mb-4">Send Invite</h3>
<input
type="text"
placeholder="Enter account name"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="w-full mb-4 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
/>
{/* Account Name Dropdown */}
<div className="mb-3 relative" onBlur={(e) => handleDropdownBlur(e, accountDropdownRef, setIsAccountDropdownOpen)}>
<label className="text-sm font-medium text-gray-600 mb-1 block">Account</label>
<div ref={accountDropdownRef}>
<input
type="text"
placeholder={accountOptionsLoading ? "Loading..." : "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"
/>
{isAccountDropdownOpen && !accountOptionsLoading && (
<ul className="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg max-h-40 overflow-auto">
{filteredOptions.length > 0 ? (
filteredOptions.map((opt) => (
<li
key={opt.accountId}
className="px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onMouseDown={() => handleAccountSelect(opt)}
>
{opt.accountName}
</li>
))
) : (
<li className="px-3 py-2 text-gray-500 text-sm">No results found</li>
)}
</ul>
)}
</div>
{errors.i_accountName && <p className="text-sm text-red-500 mt-1">{errors.i_accountName}</p>}
</div>
{/* Email Field */}
<div className="mb-3">
<label className="text-sm font-medium text-gray-600 mb-1 block">Email</label>
<input
type="email"
placeholder="Enter email"
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)] disabled:opacity-60 disabled:cursor-not-allowed"
/>
{errors.email && <p className="text-sm text-red-500 mt-1">{errors.email}</p>}
</div>
{/* Role Dropdown */}
<div className="mb-4 relative" onBlur={(e) => handleDropdownBlur(e, roleDropdownRef, setIsRoleDropdownOpen)}>
<label className="text-sm font-medium text-gray-600 mb-1 block">Role</label>
<div ref={roleDropdownRef}>
<input
type="text"
placeholder="Select role"
value={inviteFormData.role}
readOnly
onFocus={handleRoleDropdownFocus}
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"
/>
{isRoleDropdownOpen && (
<div className="absolute z-10 w-full bg-white border border-gray-300 rounded-md mt-1 shadow-lg">
{roles.map((role) => (
<div
key={role}
className="px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onMouseDown={() => handleRoleSelect(role)}
>
{role}
</div>
))}
</div>
)}
</div>
{errors.role && <p className="text-sm text-red-500 mt-1">{errors.role}</p>}
</div>
{/* Buttons */}
<div className="flex justify-end gap-3">
<button
onClick={() => {
setInputValue('');
onClose();
}}
className="px-4 py-1.5 text-[13px] cursor-pointer font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
onClick={onClose}
className="px-4 py-1.5 text-[13px] font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
>
Cancel
</button>
<button
onClick={handleAddClick}
className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--modalSubmitBG)] text-[13px] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors"
disabled={!inputValue.trim()}
onClick={handleSubmit}
className="px-4 py-1.5 text-[13px] font-semibold bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] rounded-md transition-colors flex items-center gap-1"
disabled={inviteLoading}
>
Add Account
{inviteLoading && <FaSpinner className="animate-spin text-sm" />}
Invite
</button>
</div>
</div>
</div>
)
}
);
};
export default AddInviteModal
export default AddInviteModal;

View File

@ -1,18 +1,31 @@
import React, { useEffect, useState } from 'react';
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa';
import {
FaCheckCircle,
FaClock,
FaHourglassHalf,
FaTasks,
FaChevronDown,
FaPlus,
FaUserPlus,
FaFilter,
FaCalendarAlt
} from 'react-icons/fa';
// Define a type for card data
interface CardData {
title: string;
count: number;
icon: JSX.Element;
ring: string;
bg: string;
gradient: string;
shadowColor: string;
iconBg: string;
textColor: string;
percentage?: number;
trend?: 'up' | 'down' | 'stable';
}
interface CardsProps {
type?: 'dashboard';
// onCardClick?: (card: CardData) => void;
type?: 'dashboard' | 'account' | 'transactions';
onOpenModal?: () => void;
}
@ -20,129 +33,266 @@ const cards: CardData[] = [
{
title: 'Action Required',
count: 12,
icon: <FaTasks size={20} className="text-orange-500" />,
ring: 'ring-orange-400/20',
bg: 'bg-orange-50',
},
{
title: 'Waiting for Others',
count: 8,
icon: <FaClock size={20} className="text-yellow-500" />,
ring: 'ring-yellow-400/20',
bg: 'bg-yellow-50',
icon: <FaTasks size={18} className="text-indigo-600" />,
gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
shadowColor: 'shadow-indigo-200/50',
iconBg: 'bg-indigo-100 border-indigo-200',
textColor: 'text-indigo-700',
trend: 'up'
},
{
title: 'Expiring Soon',
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
icon: <FaHourglassHalf size={18} className="text-rose-600" />,
gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
shadowColor: 'shadow-rose-200/50',
iconBg: 'bg-rose-100 border-rose-200',
textColor: 'text-rose-700',
trend: 'up'
},
{
title: 'Completed',
count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />,
ring: 'ring-green-400/20',
bg: 'bg-green-50',
icon: <FaCheckCircle size={18} className="text-teal-600" />,
gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
shadowColor: 'shadow-teal-200/50',
iconBg: 'bg-teal-100 border-teal-200',
textColor: 'text-teal-700',
trend: 'up'
},
{
title: 'Waiting for Others',
count: 8,
icon: <FaClock size={18} className="text-stone-600" />,
gradient: 'bg-gradient-to-br from-stone-50 to-stone-100',
shadowColor: 'shadow-stone-200/50',
iconBg: 'bg-stone-100 border-stone-200',
textColor: 'text-stone-700',
trend: 'down'
},
];
const AccountData: CardData[] = [
{
title: 'Transaction',
title: 'Active Accounts',
count: 12,
icon: <FaTasks size={20} className="text-orange-500" />,
ring: 'ring-orange-400/20',
bg: 'bg-orange-50',
icon: <FaTasks size={18} className="text-blue-600" />,
gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
shadowColor: 'shadow-blue-200/50',
iconBg: 'bg-blue-100 border-blue-200',
textColor: 'text-blue-700',
trend: 'up'
},
{
title: 'In-Progress',
count: 8,
icon: <FaClock size={20} className="text-yellow-500" />,
ring: 'ring-yellow-400/20',
bg: 'bg-yellow-50',
},
{
title: 'Deactive',
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
icon: <FaClock size={18} className="text-purple-600" />,
gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
shadowColor: 'shadow-purple-200/50',
iconBg: 'bg-purple-100 border-purple-200',
textColor: 'text-purple-700',
trend: 'stable'
},
{
title: 'Completed',
count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />,
ring: 'ring-green-400/20',
bg: 'bg-green-50',
icon: <FaCheckCircle size={18} className="text-green-600" />,
gradient: 'bg-gradient-to-br from-green-50 to-green-100',
shadowColor: 'shadow-green-200/50',
iconBg: 'bg-green-100 border-green-200',
textColor: 'text-green-700',
trend: 'up'
},
{
title: 'Inactive',
count: 5,
icon: <FaHourglassHalf size={18} className="text-gray-600" />,
gradient: 'bg-gradient-to-br from-gray-50 to-gray-100',
shadowColor: 'shadow-gray-200/50',
iconBg: 'bg-gray-100 border-gray-200',
textColor: 'text-gray-700',
trend: 'down'
},
];
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
const accounts = ['All Accounts', 'Account 1', 'Account 2', 'Account 3'];
const Cards: React.FC<CardsProps> = ({ type, onOpenModal }) => {
const Cards: React.FC<CardsProps> = ({ type = 'dashboard', onOpenModal }) => {
const [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
const [selectedAccount, setSelectedAccount] = useState<string>('All Accounts');
const [showCards, setShowCards] = useState<CardData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
if (type === 'account') {
setShowCards(AccountData);
} else {
setShowCards(cards);
}
setIsLoading(true);
setTimeout(() => {
if (type === 'account') {
setShowCards(AccountData);
} else {
setShowCards(cards);
}
setIsLoading(false);
}, 500);
}, [type]);
return (
<div className="w-full px-4 py-3">
{/* Header */}
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
const getButtonConfig = () => {
switch (type) {
case 'account':
return {
text: 'Add Account',
icon: <FaPlus size={14} />,
gradient: 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800'
};
case 'transactions':
return {
text: 'Send Invite',
icon: <FaUserPlus size={14} />,
gradient: 'bg-gradient-to-r from-emerald-600 to-emerald-700 hover:from-emerald-700 hover:to-emerald-800'
};
default:
return null;
}
};
<div className="flex gap-2 w-full md:w-auto">
const buttonConfig = getButtonConfig();
const renderTrendIndicator = (trend: string | undefined, percentage: number | undefined) => {
if (!trend || !percentage) return null;
const trendColors = {
up: 'text-emerald-500',
down: 'text-red-500',
stable: 'text-gray-500'
};
const trendSymbols = {
up: '↗',
down: '↘',
stable: '→'
};
return (
<div className={`flex items-center text-xs ${trendColors[trend as keyof typeof trendColors]} mt-1`}>
<span className="mr-1">{trendSymbols[trend as keyof typeof trendSymbols]}</span>
{/* <span>{percentage}%</span> */}
</div>
);
};
if (isLoading) {
return (
<div className="w-full px-6 py-8">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-32 mb-8"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-32 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="w-full px-6 py-6 bg-gradient-to-br from-slate-50 to-slate-100 mt-2 rounded-t-md">
{/* Header */}
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between mb-8 gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
<FaFilter className="text-white text-lg" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800">Overview</h2>
<p className="text-gray-600 text-sm">Monitor performance metrics</p>
</div>
</div>
<div className="flex flex-wrap gap-3 w-full lg:w-auto">
{/* Account Selector */}
<select
value={'All'}
onChange={() => { }}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
<option>All</option>
<option>Account 1</option>
<option>Account 2</option>
<option>Account 3</option>
</select>
<div className="relative">
<select
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{accounts.map((account) => (
<option key={account} value={account}>{account}</option>
))}
</select>
<FaChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
{/* Date Filter */}
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
>
{filters.map((f) => (
<option key={f}>{f}</option>
))}
</select>
<div className="relative">
<select
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
className="appearance-none text-sm text-gray-700 border border-gray-300 rounded-xl px-4 py-3 pr-10 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white shadow-sm hover:shadow-md transition-all cursor-pointer min-w-[140px]"
>
{filters.map((filter) => (
<option key={filter} value={filter}>{filter}</option>
))}
</select>
<FaCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs pointer-events-none" />
</div>
{type === 'account' &&
<button className="px-2 py-1.5 cursor-pointer font-semibold bg-[var(--primary)] text-[13px] text-white hover:bg-[var(--primaryLight2)] rounded-md transition-colors" onClick={onOpenModal}>Add Account</button>}
{/* Action Button */}
{buttonConfig && (
<button
className={`flex items-center gap-2 px-6 py-3 text-sm font-semibold text-white rounded-xl transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95 cursor-pointer ${buttonConfig.gradient}`}
onClick={onOpenModal}
>
{buttonConfig.icon}
{buttonConfig.text}
</button>
)}
</div>
</div>
{/* Cards */}
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{showCards.map((card) => (
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6">
{showCards.map((card, index) => (
<div
key={card.title}
className={`rounded-md bg-white/60 backdrop-blur-md border border-gray-200 p-5 transition-transform shadow-md hover:scale-[1.02] hover:shadow-lg`}
className={`group relative overflow-hidden rounded-2xl ${card.gradient} border border-white/50 p-6 transition-all duration-300 hover:scale-105 hover:shadow-xl ${card.shadowColor} cursor-pointer backdrop-blur-sm`}
style={{
animationDelay: `${index * 100}ms`
}}
>
<div className="flex items-center space-x-4">
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}>
{card.icon}
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute -top-10 -right-10 w-20 h-20 rounded-full bg-current"></div>
<div className="absolute -bottom-10 -left-10 w-16 h-16 rounded-full bg-current"></div>
</div>
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-4">
<div className={`p-3 rounded-xl border ${card.iconBg} group-hover:scale-110 transition-transform duration-200`}>
{card.icon}
</div>
{renderTrendIndicator(card.trend, card.percentage)}
</div>
<div>
<p className="text-sm text-gray-500">{card.title}</p>
<p className="text-xl font-bold text-gray-800">{card.count}</p>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-600 group-hover:text-gray-700 transition-colors">
{card.title}
</h3>
<div className="flex items-baseline gap-2">
<p className={`text-3xl font-bold ${card.textColor} group-hover:scale-105 transition-transform duration-200`}>
{card.count}
</p>
<span className="text-xs text-gray-500">items</span>
</div>
</div>
</div>
{/* Hover Effect */}
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
))}
</div>
@ -150,4 +300,4 @@ const Cards: React.FC<CardsProps> = ({ type, onOpenModal }) => {
);
};
export default Cards;
export default Cards;

View File

@ -5,11 +5,13 @@ import FilterSort from './FilterSort'
import ShowDocs from './ShowDocs'
import Cards from './Cards'
import StartSection from './StartSection'
import AccountNav from '../accounts/AccountNav'
const Home = () => {
return (
<>
<HomeTopNav />
{/* <HomeTopNav /> */}
<AccountNav/>
<Cards />
<StartSection />
{/* <UploadDoc /> */}

View File

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

View File

@ -8,6 +8,7 @@ import CreateAndInvite, { ActionType, FormDataType } from './CreateAndInvite'
import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
// Define the shape of state object
interface Account {
@ -256,6 +257,7 @@ const Setting = () => {
// Set initial subsection when component mounts or activeTab changes
useEffect(() => {
const subSections = getSubSections(activeTab);
console.log("subSections",subSections)
const hash = location.hash.replace('#', '');
if (subSections.length > 0) {
@ -336,6 +338,7 @@ const Setting = () => {
return (
<>
<SettingNav />
{/* <AccountNav/> */}
<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">
@ -359,7 +362,7 @@ const Setting = () => {
</div>
{/* Content with Aside Navigation */}
<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 */}
{activeTab === 'userProfile' &&
<section className="flex-shrink-0 mr-6 w-50 relative">

View File

@ -505,7 +505,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
CANCEL
</button>
<button
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"
className="cursor-pointer px-4 py-2 text-sm font-medium rounded-md bg-[var(--modalSubmitBG)] text-white hover:bg-[var(--modalSubmitBGHover)] transition-colors flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
onClick={handleSubmit}
disabled={isSubmitting}
>

View File

@ -117,7 +117,7 @@ const SignatureManager: React.FC = () => {
<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={handleAddNew}>
<button className="bg-[var(--modalSubmitBG)] hover:bg-[var(--modalSubmitBGHover)] 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={handleAddNew}>
<FiPlus className="" />
ADD
</button>

View File

@ -88,4 +88,10 @@ canvas {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
}
/* Loader CSS */