Change the UI of Cards

This commit is contained in:
JaiPandey425 2025-07-07 18:29:00 +05:30
parent 2bb54b2043
commit 73273865f4
12 changed files with 576 additions and 165 deletions

View File

@ -42,6 +42,7 @@ const App = () => {
}> }>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/templates" element={<Home />} /> <Route path="/templates" element={<Home />} />
{/* <Route path="/s/settings" element={<Setting />} /> */}
<Route element={<AccountTransactionWrapper />}> <Route element={<AccountTransactionWrapper />}>
<Route path="/accounts" element={<Accounts />} /> <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 { FaCheck } from "react-icons/fa";
import Cards from "../home/Cards"; import Cards from "../home/Cards";
import AddInviteModal from "./AddInviteModal"; import AddInviteModal from "./AddInviteModal";
import { Bounce, toast } from "react-toastify";
interface Transaction { interface Transaction {
id: number; id: number;
@ -24,6 +25,8 @@ const Account = () => {
const { accountId } = useParams<{ accountId: string }>(); const { accountId } = useParams<{ accountId: string }>();
const [transactions, setTransactions] = useState<Transaction[]>([]); const [transactions, setTransactions] = useState<Transaction[]>([]);
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]); const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
const [accountsData, setAccountsData] = useState([])
const [inviteLoading, setInviteLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@ -44,22 +47,62 @@ const Account = () => {
if (accountId) fetchTransactions(); if (accountId) fetchTransactions();
}, [accountId]); }, [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 navigate = useNavigate();
const handleTransactionClick = (id: number) => { const handleTransactionClick = (id: number) => {
navigate(`/accounts/${accountId}/transactions/${id}`); navigate(`/accounts/${accountId}/transactions/${id}`);
}; };
const handleAddAccount = (accountName: string) => { const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
console.log(accountId) 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); setIsModalOpen(false);
}
} catch (err) {
console.error('Failed to invite', err);
} finally {
setInviteLoading(false);
}
}; };
return ( return (
<> <>
<Cards type="account" onOpenModal={handleOpenModal} /> <Cards type="transactions" onOpenModal={handleOpenModal} />
{transactions.length > 0 && ( {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"> <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"> <thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
<tr> <tr>
@ -116,8 +159,8 @@ const Account = () => {
</table> </table>
</div> </div>
)} )}
{isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} {isModalOpen && <AddInviteModal onClose={() => setIsModalOpen(false)} onInvite={handleInvite} inviteLoading={inviteLoading}
onAddAccount={handleAddAccount} />} accountOptions={accountsData} />}
</> </>
); );
}; };

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
// import { useSelector } from 'react-redux'; // import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import { FaSignOutAlt,FaUser } from "react-icons/fa"; import { FaSignOutAlt, FaUser } from "react-icons/fa";
import api from '../../config/api'; import api from '../../config/api';
// import { RootState } from '../../store/store'; // import { RootState } from '../../store/store';
@ -44,6 +44,8 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
}, },
}); });
const navigate = useNavigate()
const getUserDetails = async () => { const getUserDetails = async () => {
try { try {
let res = await api.get(`/api/user/details`) let res = await api.get(`/api/user/details`)
@ -68,6 +70,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
accounts: "Accounts", accounts: "Accounts",
transactions: "Transactions", transactions: "Transactions",
templates: "Templates"
// Add more routes here if needed // Add more routes here if needed
}; };
@ -86,42 +89,58 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
const handleLogout = () => { const handleLogout = async() => {
// Add logout logic here try {
console.log('Logging out...'); let res = await api.post('/api/auth/logout')
if (res.status === 200) {
setOpen(false); setOpen(false);
navigate("/login")
}
} catch (error) {
console.log(error)
}
}; };
const formatSegment = (segment: string) =>
segment
.replace(/[-_]/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
return ( return (
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200"> <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"> <nav className="text-md font-medium text-gray-800 capitalize flex items-center flex-wrap">
{pathnames.map((segment, index) => { {pathnames.length === 0 ? (
const label = labelMap[segment] || segment; <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 to = "/" + pathnames.slice(0, index + 1).join("/");
const isLast = index === pathnames.length - 1; const isLast = index === pathnames.length - 1;
return ( return (
<span key={to}> <span key={to} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-400">{">"}</span>}
{!isLast ? ( {!isLast ? (
<>
<Link <Link
to={to} to={to}
className="text-blue-600 hover:text-blue-700 hover:underline transition-colors" className="text-blue-600 hover:text-blue-700 hover:underline transition-colors"
> >
{label} {label}
</Link> </Link>
<span className="mx-2 text-gray-400">{">"}</span>
</>
) : ( ) : (
<span className="text-gray-900 font-semibold">{label}</span> <span className="text-gray-900 font-semibold">{label}</span>
)} )}
</span> </span>
); );
})} })
)}
</nav> </nav>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{/* User Dropdown */} {/* User Dropdown */}
@ -137,7 +156,7 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
{user ? ( {user ? (
<span <span
className="w-full h-full object-cover p-1.5 font-bold" 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" /> <FaUser className="text-lg" />
)} )}
@ -154,13 +173,14 @@ const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transaction
{(user) ? ( {(user) ? (
<span <span
className="w-full h-full object-cover px-3 text-xl py-2.5 font-bold" 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> >
{(user?.firstName + " " + user?.lastName).split(' ').map((item) => item.slice(0, 1)).join("").toUpperCase()}</span>
) : ( ) : (
<FaUser className="text-lg" /> <FaUser className="text-lg" />
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <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-sm text-gray-600 truncate">{user.email || '-'}</div>
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
Account #{user?.account?.name || '-'} Account #{user?.account?.name || '-'}

View File

@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom";
import Cards from "../home/Cards"; import Cards from "../home/Cards";
import AddAccountModal from "./AddAccountModal"; import AddAccountModal from "./AddAccountModal";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import Loader from "../../helper/Loader";
// import { UserRole, AccountStatus, AccountData } from "../../types/account"; // import { UserRole, AccountStatus, AccountData } from "../../types/account";
@ -63,9 +64,31 @@ const Accounts = () => {
navigate(`/accounts/${id}/transactions`); navigate(`/accounts/${id}/transactions`);
}; };
const handleAddAccount = (accountName: string) => { const handleAddAccount = async (accountName: string) => {
setAccounts((prev) => [...prev, accountName]); 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); setIsModalOpen(false);
}
} catch (error) {
console.log(error)
} finally {
}
}; };
const handleMenuToggle = (accountId: string) => { 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 { interface AccountOption {
onClose: () => void; userId: number;
onAddAccount: (accountName: string) => void; userEmail: string;
accountId: number;
accountName: string;
role: string;
status: string;
} }
const AddInviteModal : React.FC<AddAccountModalProps> = ({ onClose, onAddAccount }) => {
const [inputValue, setInputValue] = useState('');
const handleAddClick = () => { interface AddInviteModalProps {
if (inputValue.trim()) { onClose: () => void;
onAddAccount(inputValue.trim()); onInvite: (data: { i_accountName: number; email: string; role: string }) => void;
setInputValue(''); // clear input after submit 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 ( return (
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50"> <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> <h3 className="text-lg font-medium text-gray-800 mb-4">Send Invite</h3>
{/* 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 <input
type="text" type="text"
placeholder="Enter account name" placeholder={accountOptionsLoading ? "Loading..." : "Select or search account"}
value={inputValue} value={accountSearchTerm}
onChange={(e) => setInputValue(e.target.value)} onChange={handleAccountSearchChange}
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)]" 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"> <div className="flex justify-end gap-3">
<button <button
onClick={() => { onClick={onClose}
setInputValue(''); className="px-4 py-1.5 text-[13px] font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
onClose();
}}
className="px-4 py-1.5 text-[13px] cursor-pointer font-semibold text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleAddClick} onClick={handleSubmit}
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" 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={!inputValue.trim()} disabled={inviteLoading}
> >
Add Account {inviteLoading && <FaSpinner className="animate-spin text-sm" />}
Invite
</button> </button>
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default AddInviteModal export default AddInviteModal;

View File

@ -1,18 +1,31 @@
import React, { useEffect, useState } from 'react'; 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 // Define a type for card data
interface CardData { interface CardData {
title: string; title: string;
count: number; count: number;
icon: JSX.Element; icon: JSX.Element;
ring: string; gradient: string;
bg: string; shadowColor: string;
iconBg: string;
textColor: string;
percentage?: number;
trend?: 'up' | 'down' | 'stable';
} }
interface CardsProps { interface CardsProps {
type?: 'dashboard'; type?: 'dashboard' | 'account' | 'transactions';
// onCardClick?: (card: CardData) => void;
onOpenModal?: () => void; onOpenModal?: () => void;
} }
@ -20,130 +33,267 @@ const cards: CardData[] = [
{ {
title: 'Action Required', title: 'Action Required',
count: 12, count: 12,
icon: <FaTasks size={20} className="text-orange-500" />, icon: <FaTasks size={18} className="text-indigo-600" />,
ring: 'ring-orange-400/20', gradient: 'bg-gradient-to-br from-indigo-50 to-indigo-100',
bg: 'bg-orange-50', shadowColor: 'shadow-indigo-200/50',
}, iconBg: 'bg-indigo-100 border-indigo-200',
{ textColor: 'text-indigo-700',
title: 'Waiting for Others', trend: 'up'
count: 8,
icon: <FaClock size={20} className="text-yellow-500" />,
ring: 'ring-yellow-400/20',
bg: 'bg-yellow-50',
}, },
{ {
title: 'Expiring Soon', title: 'Expiring Soon',
count: 5, count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />, icon: <FaHourglassHalf size={18} className="text-rose-600" />,
ring: 'ring-red-400/20', gradient: 'bg-gradient-to-br from-rose-50 to-rose-100',
bg: 'bg-red-50', shadowColor: 'shadow-rose-200/50',
iconBg: 'bg-rose-100 border-rose-200',
textColor: 'text-rose-700',
trend: 'up'
}, },
{ {
title: 'Completed', title: 'Completed',
count: 27, count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />, icon: <FaCheckCircle size={18} className="text-teal-600" />,
ring: 'ring-green-400/20', gradient: 'bg-gradient-to-br from-teal-50 to-teal-100',
bg: 'bg-green-50', 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[] = [ const AccountData: CardData[] = [
{ {
title: 'Transaction', title: 'Active Accounts',
count: 12, count: 12,
icon: <FaTasks size={20} className="text-orange-500" />, icon: <FaTasks size={18} className="text-blue-600" />,
ring: 'ring-orange-400/20', gradient: 'bg-gradient-to-br from-blue-50 to-blue-100',
bg: 'bg-orange-50', shadowColor: 'shadow-blue-200/50',
iconBg: 'bg-blue-100 border-blue-200',
textColor: 'text-blue-700',
trend: 'up'
}, },
{ {
title: 'In-Progress', title: 'In-Progress',
count: 8, count: 8,
icon: <FaClock size={20} className="text-yellow-500" />, icon: <FaClock size={18} className="text-purple-600" />,
ring: 'ring-yellow-400/20', gradient: 'bg-gradient-to-br from-purple-50 to-purple-100',
bg: 'bg-yellow-50', shadowColor: 'shadow-purple-200/50',
}, iconBg: 'bg-purple-100 border-purple-200',
{ textColor: 'text-purple-700',
title: 'Deactive', trend: 'stable'
count: 5,
icon: <FaHourglassHalf size={20} className="text-red-500" />,
ring: 'ring-red-400/20',
bg: 'bg-red-50',
}, },
{ {
title: 'Completed', title: 'Completed',
count: 27, count: 27,
icon: <FaCheckCircle size={20} className="text-green-500" />, icon: <FaCheckCircle size={18} className="text-green-600" />,
ring: 'ring-green-400/20', gradient: 'bg-gradient-to-br from-green-50 to-green-100',
bg: 'bg-green-50', 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 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 [selectedFilter, setSelectedFilter] = useState<string>('Last 7 Days');
const [selectedAccount, setSelectedAccount] = useState<string>('All Accounts');
const [showCards, setShowCards] = useState<CardData[]>([]); const [showCards, setShowCards] = useState<CardData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
setIsLoading(true);
setTimeout(() => {
if (type === 'account') { if (type === 'account') {
setShowCards(AccountData); setShowCards(AccountData);
} else { } else {
setShowCards(cards); setShowCards(cards);
} }
setIsLoading(false);
}, 500);
}, [type]); }, [type]);
return ( const getButtonConfig = () => {
<div className="w-full px-4 py-3"> switch (type) {
{/* Header */} case 'account':
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3"> return {
<h2 className="text-lg font-medium text-gray-700">Overview</h2> 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 */} {/* Account Selector */}
<div className="relative">
<select <select
value={'All'} value={selectedAccount}
onChange={() => { }} onChange={(e) => setSelectedAccount(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" 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]"
> >
<option>All</option> {accounts.map((account) => (
<option>Account 1</option> <option key={account} value={account}>{account}</option>
<option>Account 2</option> ))}
<option>Account 3</option>
</select> </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 */} {/* Date Filter */}
<div className="relative">
<select <select
value={selectedFilter} value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)} 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" 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((f) => ( {filters.map((filter) => (
<option key={f}>{f}</option> <option key={filter} value={filter}>{filter}</option>
))} ))}
</select> </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' && {/* Action Button */}
<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>} {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>
</div> </div>
{/* Cards */} {/* Cards */}
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6">
{showCards.map((card) => ( {showCards.map((card, index) => (
<div <div
key={card.title} 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"> {/* Background Pattern */}
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}> <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} {card.icon}
</div> </div>
<div> {renderTrendIndicator(card.trend, card.percentage)}
<p className="text-sm text-gray-500">{card.title}</p> </div>
<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> </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> </div>
</div> </div>

View File

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

View File

@ -33,7 +33,7 @@ const HomeTopNav = () => {
</button> </button>
<Link <Link
to="/s/settings" to="settings"
aria-label="Settings" aria-label="Settings"
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600" 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 AsideSection from "./AsideSection";
import Signature from "./Signature/Signature"; import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify"; import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
// Define the shape of state object // Define the shape of state object
interface Account { interface Account {
@ -256,6 +257,7 @@ const Setting = () => {
// Set initial subsection when component mounts or activeTab changes // Set initial subsection when component mounts or activeTab changes
useEffect(() => { useEffect(() => {
const subSections = getSubSections(activeTab); const subSections = getSubSections(activeTab);
console.log("subSections",subSections)
const hash = location.hash.replace('#', ''); const hash = location.hash.replace('#', '');
if (subSections.length > 0) { if (subSections.length > 0) {
@ -336,6 +338,7 @@ const Setting = () => {
return ( return (
<> <>
<SettingNav /> <SettingNav />
{/* <AccountNav/> */}
<div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2"> <div className="bg-[#f9fafa] h-[calc(90vh-20px)] flex flex-col mt-2">
{/* Main Navigation Tabs */} {/* Main Navigation Tabs */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5"> <div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-5">

View File

@ -505,7 +505,7 @@ const AddSignatureModal: React.FC<AddSignatureModalProps> = ({
CANCEL CANCEL
</button> </button>
<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} onClick={handleSubmit}
disabled={isSubmitting} 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> <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> <p className="text-gray-300 text-sm">Add or update your name and signature styles.</p>
</div> </div>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-semibold text-sm text-[14px] flex items-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5" onClick={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="" /> <FiPlus className="" />
ADD ADD
</button> </button>

View File

@ -89,3 +89,9 @@ canvas {
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
} }
/* Loader CSS */