first commit
This commit is contained in:
191
src/components/DataTable.jsx
Normal file
191
src/components/DataTable.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
|
||||
|
||||
const DataTable = ({
|
||||
data = [],
|
||||
columns = [],
|
||||
loading = false,
|
||||
searchable = false,
|
||||
onSearch,
|
||||
className = ''
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(10);
|
||||
|
||||
const filteredData = data.filter(item => {
|
||||
if (!searchTerm) return true;
|
||||
return Object.values(item).some(value =>
|
||||
String(value).toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(filteredData.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentData = filteredData.slice(startIndex, endIndex);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const value = e.target.value;
|
||||
setSearchTerm(value);
|
||||
if (onSearch) {
|
||||
onSearch(value);
|
||||
}
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const statusClasses = {
|
||||
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
pending: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClasses[status] || statusClasses.pending}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount, currency = 'USD') => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card p-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
|
||||
<div className="space-y-3">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`card ${className}`}>
|
||||
{searchable && (
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{currentData.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
currentData.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{column.render ? (
|
||||
column.render(row[column.key], row)
|
||||
) : (
|
||||
row[column.key]
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="px-6 py-3 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredData.length)} of {filteredData.length} results
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex space-x-1">
|
||||
{[...Array(totalPages)].map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handlePageChange(i + 1)}
|
||||
className={`px-3 py-1 text-sm rounded-md ${
|
||||
currentPage === i + 1
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
113
src/components/Navbar.jsx
Normal file
113
src/components/Navbar.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Menu, Sun, Moon, User, LogOut, ChevronDown } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const Navbar = ({ onSidebarToggle }) => {
|
||||
const { user, logout } = useAuth();
|
||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||
return localStorage.getItem('theme') === 'dark' ||
|
||||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
});
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
|
||||
// Apply theme to document
|
||||
React.useEffect(() => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setIsDarkMode(!isDarkMode);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setIsUserMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 h-16 flex items-center justify-between px-4 lg:px-6 relative z-20">
|
||||
{/* Left side */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={onSidebarToggle}
|
||||
className="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<h1 className="text-lg font-semibold text-gray-900 dark:text-white lg:hidden">
|
||||
Payment Admin
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* User menu */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
|
||||
className="flex items-center space-x-2 p-2 rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors duration-200"
|
||||
>
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<span className="hidden sm:block text-sm font-medium">
|
||||
{user?.name || 'Admin'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{/* Dropdown menu */}
|
||||
{isUserMenuOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{user?.name || 'Admin User'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{user?.email || 'admin@example.com'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors duration-200"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Click outside to close user menu */}
|
||||
{isUserMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsUserMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
81
src/components/Sidebar.jsx
Normal file
81
src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
CreditCard,
|
||||
Settings,
|
||||
Menu,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
|
||||
const Sidebar = ({ isOpen, onToggle }) => {
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||
{ name: 'Transactions', href: '/transactions', icon: CreditCard },
|
||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
||||
onClick={onToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className={`
|
||||
fixed inset-y-0 left-0 z-30 w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 transform transition-transform duration-300 ease-in-out
|
||||
${isOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
lg:translate-x-0 lg:static lg:inset-0 lg:z-auto
|
||||
`}>
|
||||
<div className="flex items-center justify-between h-16 px-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Payment Admin
|
||||
</h1>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="mt-8 px-4">
|
||||
<ul className="space-y-2">
|
||||
{navigation.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<li key={item.name}>
|
||||
<NavLink
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`group flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
|
||||
isActive
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`
|
||||
}
|
||||
onClick={() => {
|
||||
// Close sidebar on mobile when navigating
|
||||
if (window.innerWidth < 1024) {
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon className="mr-3 h-5 w-5" />
|
||||
{item.name}
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
Reference in New Issue
Block a user