first commit

This commit is contained in:
ghazall-ag
2025-10-23 21:52:07 +03:30
commit d63a5b8d99
15218 changed files with 1639961 additions and 0 deletions

109
src/App.jsx Normal file
View File

@@ -0,0 +1,109 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import Sidebar from './components/Sidebar';
import Navbar from './components/Navbar';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Transactions from './pages/Transactions';
import Settings from './pages/Settings';
// Protected Route Component
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
return isAuthenticated ? children : <Navigate to="/login" />;
};
// Main Layout Component
const Layout = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = React.useState(false);
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Sidebar */}
<Sidebar isOpen={sidebarOpen} onToggle={toggleSidebar} />
{/* Main Content Area */}
<div className="flex flex-col flex-1 min-h-screen">
{/* Navbar */}
<div className="sticky top-0 z-20 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<Navbar onSidebarToggle={toggleSidebar} />
</div>
{/* Page Content */}
<main className="flex-1 py-6 px-4 sm:px-6 lg:px-8 overflow-y-auto">
{children}
</main>
</div>
</div>
);
};
// App Routes Component
const AppRoutes = () => {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<Layout>
<Dashboard />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/transactions"
element={
<ProtectedRoute>
<Layout>
<Transactions />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Layout>
<Settings />
</Layout>
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
);
};
// Main App Component
const App = () => {
return (
<AuthProvider>
<Router>
<div className="App">
<AppRoutes />
</div>
</Router>
</AuthProvider>
);
};
export default App;

View 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
View 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;

View 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;

110
src/context/AuthContext.jsx Normal file
View File

@@ -0,0 +1,110 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing token on app load
const savedToken = localStorage.getItem('token');
const savedUser = localStorage.getItem('user');
if (savedToken && savedUser) {
setToken(savedToken);
setUser(JSON.parse(savedUser));
}
setLoading(false);
}, []);
const login = async (email, password) => {
try {
// Mock API call - replace with actual API endpoint
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
// For demo purposes, use mock data if API fails
const mockData = {
token: 'mock-jwt-token-' + Date.now(),
user: {
id: 1,
email: email,
name: 'Admin User',
role: 'admin'
}
};
const result = response.ok ? data : mockData;
setToken(result.token);
setUser(result.user);
localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(result.user));
return { success: true };
} catch (error) {
// Fallback to mock login for demo
const mockData = {
token: 'mock-jwt-token-' + Date.now(),
user: {
id: 1,
email: email,
name: 'Admin User',
role: 'admin'
}
};
setToken(mockData.token);
setUser(mockData.user);
localStorage.setItem('token', mockData.token);
localStorage.setItem('user', JSON.stringify(mockData.user));
return { success: true };
}
};
const logout = () => {
setToken(null);
setUser(null);
localStorage.removeItem('token');
localStorage.removeItem('user');
};
const value = {
user,
token,
login,
logout,
loading,
isAuthenticated: !!token
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

27
src/index.css Normal file
View File

@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: 'Inter', system-ui, sans-serif;
}
}
@layer components {
.btn-primary {
@apply bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200;
}
.btn-secondary {
@apply bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-gray-100 font-medium py-2 px-4 rounded-lg transition-colors duration-200;
}
.card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700;
}
.input-field {
@apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100;
}
}

38
src/layout.css Normal file
View File

@@ -0,0 +1,38 @@
/* Layout specific styles */
.layout-container {
position: relative;
min-height: 100vh;
}
.sidebar-fixed {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 30;
width: 16rem; /* 256px */
}
.main-content {
margin-left: 0;
transition: margin-left 0.3s ease-in-out;
}
@media (min-width: 1024px) {
.main-content {
margin-left: 16rem; /* 256px */
}
}
.navbar-fixed {
position: sticky;
top: 0;
z-index: 20;
background: white;
}
.dark .navbar-fixed {
background: #111827;
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

273
src/pages/Dashboard.jsx Normal file
View File

@@ -0,0 +1,273 @@
import React, { useState, useEffect } from 'react';
import {
DollarSign,
CreditCard,
TrendingUp,
TrendingDown,
ArrowUpRight,
ArrowDownRight
} from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { paymentsAPI } from '../services/api';
const Dashboard = () => {
const [stats, setStats] = useState(null);
const [chartData, setChartData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
try {
setLoading(true);
const [statsData, chartDataResponse] = await Promise.all([
paymentsAPI.getStats(),
paymentsAPI.getChartData()
]);
setStats(statsData);
setChartData(chartDataResponse);
} catch (err) {
setError('Failed to fetch dashboard data');
console.error('Dashboard data fetch error:', err);
} finally {
setLoading(false);
}
};
const StatCard = ({ title, value, icon: Icon, change, changeType, color = 'primary' }) => {
const colorClasses = {
primary: 'text-primary-600 dark:text-primary-400',
green: 'text-green-600 dark:text-green-400',
red: 'text-red-600 dark:text-red-400',
blue: 'text-blue-600 dark:text-blue-400',
};
const bgColorClasses = {
primary: 'bg-primary-100 dark:bg-primary-900',
green: 'bg-green-100 dark:bg-green-900',
red: 'bg-red-100 dark:bg-red-900',
blue: 'bg-blue-100 dark:bg-blue-900',
};
return (
<div className="card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{title}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{value}</p>
{change && (
<div className="flex items-center mt-2">
{changeType === 'increase' ? (
<ArrowUpRight className="h-4 w-4 text-green-500 mr-1" />
) : (
<ArrowDownRight className="h-4 w-4 text-red-500 mr-1" />
)}
<span className={`text-sm ${changeType === 'increase' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{change}
</span>
</div>
)}
</div>
<div className={`p-3 rounded-lg ${bgColorClasses[color]}`}>
<Icon className={`h-6 w-6 ${colorClasses[color]}`} />
</div>
</div>
</div>
);
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
};
const formatNumber = (num) => {
return new Intl.NumberFormat('en-US').format(num);
};
if (loading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-6"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{[...Array(3)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
<div className="card p-6">
<div className="h-80 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="card p-6 text-center">
<p className="text-red-600 dark:text-red-400">{error}</p>
<button
onClick={fetchDashboardData}
className="btn-primary mt-4"
>
Retry
</button>
</div>
</div>
);
}
const successRate = stats ? ((stats.success / stats.total) * 100).toFixed(1) : 0;
const failedRate = stats ? ((stats.failed / stats.total) * 100).toFixed(1) : 0;
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Dashboard</h1>
<p className="text-gray-600 dark:text-gray-400">Overview of your payment system</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<StatCard
title="Total Payments"
value={formatNumber(stats?.total || 0)}
icon={CreditCard}
change="+12.5%"
changeType="increase"
color="primary"
/>
<StatCard
title="Successful Payments"
value={formatNumber(stats?.success || 0)}
icon={TrendingUp}
change={`${successRate}%`}
changeType="increase"
color="green"
/>
<StatCard
title="Failed Payments"
value={formatNumber(stats?.failed || 0)}
icon={TrendingDown}
change={`${failedRate}%`}
changeType="increase"
color="red"
/>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Line Chart */}
<div className="card p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Payment Trends</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">Daily payment amounts over time</p>
</div>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="date"
stroke="#6B7280"
fontSize={12}
tickFormatter={(value) => new Date(value).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
/>
<YAxis
stroke="#6B7280"
fontSize={12}
tickFormatter={(value) => `$${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'var(--tw-bg-white)',
border: '1px solid var(--tw-border-gray-200)',
borderRadius: '8px',
color: 'var(--tw-text-gray-900)'
}}
labelStyle={{ color: 'var(--tw-text-gray-700)' }}
formatter={(value) => [formatCurrency(value), 'Amount']}
labelFormatter={(value) => `Date: ${new Date(value).toLocaleDateString()}`}
/>
<Line
type="monotone"
dataKey="amount"
stroke="#3B82F6"
strokeWidth={2}
dot={{ fill: '#3B82F6', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#3B82F6', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Bar Chart */}
<div className="card p-6">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Payment Status</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">Distribution of payment statuses</p>
</div>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={[
{ name: 'Success', value: stats?.success || 0, fill: '#10B981' },
{ name: 'Failed', value: stats?.failed || 0, fill: '#EF4444' },
]}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="name" stroke="#6B7280" fontSize={12} />
<YAxis stroke="#6B7280" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: 'var(--tw-bg-white)',
border: '1px solid var(--tw-border-gray-200)',
borderRadius: '8px',
color: 'var(--tw-text-gray-900)'
}}
formatter={(value) => [formatNumber(value), 'Count']}
/>
<Bar dataKey="value" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
{successRate}%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Success Rate</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-red-600 dark:text-red-400">
{failedRate}%
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Failure Rate</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{formatCurrency((stats?.success || 0) * 150)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Revenue (Est.)</div>
</div>
<div className="card p-4 text-center">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{Math.round((stats?.total || 0) / 7)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Daily Average</div>
</div>
</div>
</div>
);
};
export default Dashboard;

153
src/pages/Login.jsx Normal file
View File

@@ -0,0 +1,153 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { CreditCard, Eye, EyeOff } from 'lucide-react';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const result = await login(email, password);
if (result.success) {
navigate('/');
} else {
setError('Invalid email or password');
}
} catch (err) {
setError('Login failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="flex justify-center">
<div className="bg-primary-600 p-3 rounded-full">
<CreditCard className="h-8 w-8 text-white" />
</div>
</div>
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900 dark:text-white">
Payment Admin Panel
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Sign in to your account
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10 border border-gray-200 dark:border-gray-700">
<form className="space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-md text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input-field"
placeholder="admin@example.com"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Password
</label>
<div className="mt-1 relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-field pr-10"
placeholder="Enter your password"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400" />
) : (
<Eye className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{loading ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Signing in...
</>
) : (
'Sign in'
)}
</button>
</div>
</form>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
Demo Credentials
</span>
</div>
</div>
<div className="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
<p>Email: admin@example.com</p>
<p>Password: password</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;

352
src/pages/Settings.jsx Normal file
View File

@@ -0,0 +1,352 @@
import React, { useState, useEffect } from 'react';
import { Save, Key, Globe, DollarSign, Bell, Shield, Database } from 'lucide-react';
const Settings = () => {
const [settings, setSettings] = useState({
paymentApiKey: '',
webhookUrl: '',
currency: 'USD',
notifications: {
email: true,
sms: false,
webhook: true
},
security: {
twoFactor: false,
sessionTimeout: 30
},
data: {
retentionPeriod: 365,
autoBackup: true
}
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [activeTab, setActiveTab] = useState('payment');
useEffect(() => {
loadSettings();
}, []);
const loadSettings = () => {
const savedSettings = localStorage.getItem('adminSettings');
if (savedSettings) {
setSettings(JSON.parse(savedSettings));
}
};
const handleInputChange = (section, field, value) => {
setSettings(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}));
};
const handleDirectChange = (field, value) => {
setSettings(prev => ({
...prev,
[field]: value
}));
};
const handleSave = async () => {
setLoading(true);
setMessage('');
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
localStorage.setItem('adminSettings', JSON.stringify(settings));
setMessage('Settings saved successfully!');
setTimeout(() => setMessage(''), 3000);
} catch (error) {
setMessage('Failed to save settings');
} finally {
setLoading(false);
}
};
const tabs = [
{ id: 'payment', label: 'Payment', icon: DollarSign },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'security', label: 'Security', icon: Shield },
{ id: 'data', label: 'Data', icon: Database }
];
const currencies = [
{ value: 'USD', label: 'US Dollar (USD)' },
{ value: 'EUR', label: 'Euro (EUR)' },
{ value: 'GBP', label: 'British Pound (GBP)' },
{ value: 'CAD', label: 'Canadian Dollar (CAD)' },
{ value: 'AUD', label: 'Australian Dollar (AUD)' },
{ value: 'JPY', label: 'Japanese Yen (JPY)' }
];
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
<p className="text-gray-600 dark:text-gray-400">Configure your payment system preferences</p>
</div>
{/* Message */}
{message && (
<div className={`mb-6 p-4 rounded-lg ${
message.includes('success')
? 'bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-700'
: 'bg-red-50 dark:bg-red-900 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-700'
}`}>
{message}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar */}
<div className="lg:col-span-1">
<nav className="space-y-1">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
activeTab === tab.id
? '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'
}`}
>
<Icon className="mr-3 h-5 w-5" />
{tab.label}
</button>
);
})}
</nav>
</div>
{/* Content */}
<div className="lg:col-span-3">
<div className="card p-6">
{/* Payment Settings */}
{activeTab === 'payment' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Payment Configuration</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Key className="inline h-4 w-4 mr-1" />
Payment API Key
</label>
<input
type="password"
value={settings.paymentApiKey}
onChange={(e) => handleDirectChange('paymentApiKey', e.target.value)}
className="input-field"
placeholder="Enter your payment API key"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Your secure API key for payment processing
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<Globe className="inline h-4 w-4 mr-1" />
Webhook URL
</label>
<input
type="url"
value={settings.webhookUrl}
onChange={(e) => handleDirectChange('webhookUrl', e.target.value)}
className="input-field"
placeholder="https://your-domain.com/webhook"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
URL to receive payment notifications
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<DollarSign className="inline h-4 w-4 mr-1" />
Default Currency
</label>
<select
value={settings.currency}
onChange={(e) => handleDirectChange('currency', e.target.value)}
className="input-field"
>
{currencies.map(currency => (
<option key={currency.value} value={currency.value}>
{currency.label}
</option>
))}
</select>
</div>
</div>
</div>
)}
{/* Notification Settings */}
{activeTab === 'notifications' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Notification Preferences</h3>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Email Notifications</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Receive notifications via email</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.notifications.email}
onChange={(e) => handleInputChange('notifications', 'email', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</label>
</div>
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">SMS Notifications</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Receive notifications via SMS</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.notifications.sms}
onChange={(e) => handleInputChange('notifications', 'sms', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</label>
</div>
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Webhook Notifications</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Send notifications to webhook URL</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.notifications.webhook}
onChange={(e) => handleInputChange('notifications', 'webhook', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</label>
</div>
</div>
</div>
)}
{/* Security Settings */}
{activeTab === 'security' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Security Settings</h3>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Two-Factor Authentication</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Enable 2FA for enhanced security</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.security.twoFactor}
onChange={(e) => handleInputChange('security', 'twoFactor', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Session Timeout (minutes)
</label>
<input
type="number"
min="5"
max="480"
value={settings.security.sessionTimeout}
onChange={(e) => handleInputChange('security', 'sessionTimeout', parseInt(e.target.value))}
className="input-field w-32"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Automatically log out after inactivity
</p>
</div>
</div>
</div>
)}
{/* Data Settings */}
{activeTab === 'data' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Data Management</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Data Retention Period (days)
</label>
<input
type="number"
min="30"
max="2555"
value={settings.data.retentionPeriod}
onChange={(e) => handleInputChange('data', 'retentionPeriod', parseInt(e.target.value))}
className="input-field w-32"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
How long to keep transaction data
</p>
</div>
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Automatic Backup</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Automatically backup data daily</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={settings.data.autoBackup}
onChange={(e) => handleInputChange('data', 'autoBackup', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
</label>
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleSave}
disabled={loading}
className="btn-primary flex items-center"
>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Saving...' : 'Save Settings'}
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default Settings;

277
src/pages/Transactions.jsx Normal file
View File

@@ -0,0 +1,277 @@
import React, { useState, useEffect } from 'react';
import { RefreshCw, Download, Filter } from 'lucide-react';
import DataTable from '../components/DataTable';
import { paymentsAPI } from '../services/api';
const Transactions = () => {
const [payments, setPayments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [filter, setFilter] = useState('all');
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
fetchPayments();
}, []);
const fetchPayments = async () => {
try {
setLoading(true);
setError('');
const response = await paymentsAPI.getPayments();
setPayments(response.data || []);
} catch (err) {
setError('Failed to fetch transactions');
console.error('Transactions fetch error:', err);
} finally {
setLoading(false);
}
};
const handleRefresh = async () => {
setRefreshing(true);
await fetchPayments();
setRefreshing(false);
};
const handleFilter = (status) => {
setFilter(status);
};
const filteredPayments = payments.filter(payment => {
if (filter === 'all') return true;
return payment.status === filter;
});
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',
});
};
const exportToCSV = () => {
const csvContent = [
['ID', 'User', 'Amount', 'Status', 'Date', 'Currency'],
...filteredPayments.map(payment => [
payment.id,
payment.user,
payment.amount,
payment.status,
formatDate(payment.date),
payment.currency
])
].map(row => row.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `transactions-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
};
const columns = [
{
key: 'id',
header: 'Transaction ID',
render: (value) => (
<span className="font-mono text-sm font-medium text-gray-900 dark:text-white">
{value}
</span>
)
},
{
key: 'user',
header: 'User',
render: (value) => (
<div className="flex items-center">
<div className="w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center mr-3">
<span className="text-xs font-medium text-primary-600 dark:text-primary-400">
{value.charAt(0).toUpperCase()}
</span>
</div>
<span className="text-sm font-medium text-gray-900 dark:text-white">{value}</span>
</div>
)
},
{
key: 'amount',
header: 'Amount',
render: (value, row) => (
<span className="text-sm font-semibold text-gray-900 dark:text-white">
{formatCurrency(value, row.currency)}
</span>
)
},
{
key: 'status',
header: 'Status',
render: (value) => getStatusBadge(value)
},
{
key: 'date',
header: 'Date',
render: (value) => (
<span className="text-sm text-gray-600 dark:text-gray-400">
{formatDate(value)}
</span>
)
}
];
return (
<div className="p-6">
<div className="mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Transactions</h1>
<p className="text-gray-600 dark:text-gray-400">View and manage all payment transactions</p>
</div>
<div className="mt-4 sm:mt-0 flex space-x-3">
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn-secondary flex items-center"
>
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
</button>
<button
onClick={exportToCSV}
className="btn-primary flex items-center"
>
<Download className="h-4 w-4 mr-2" />
Export CSV
</button>
</div>
</div>
</div>
{/* Filters */}
<div className="mb-6">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Filter className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Filter by status:</span>
</div>
<div className="flex space-x-2">
{[
{ key: 'all', label: 'All', count: payments.length },
{ key: 'success', label: 'Success', count: payments.filter(p => p.status === 'success').length },
{ key: 'pending', label: 'Pending', count: payments.filter(p => p.status === 'pending').length },
{ key: 'failed', label: 'Failed', count: payments.filter(p => p.status === 'failed').length },
].map(({ key, label, count }) => (
<button
key={key}
onClick={() => handleFilter(key)}
className={`px-3 py-1 text-sm rounded-full transition-colors duration-200 ${
filter === key
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
}`}
>
{label} ({count})
</button>
))}
</div>
</div>
</div>
{/* Error State */}
{error && (
<div className="card p-6 text-center mb-6">
<p className="text-red-600 dark:text-red-400 mb-4">{error}</p>
<button onClick={fetchPayments} className="btn-primary">
Try Again
</button>
</div>
)}
{/* Transactions Table */}
<DataTable
data={filteredPayments}
columns={columns}
loading={loading}
searchable={true}
className="shadow-sm"
/>
{/* Summary Stats */}
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Transactions</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{filteredPayments.length}
</p>
</div>
<div className="p-3 bg-primary-100 dark:bg-primary-900 rounded-lg">
<div className="w-6 h-6 bg-primary-600 dark:bg-primary-400 rounded"></div>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Amount</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{formatCurrency(
filteredPayments.reduce((sum, payment) => sum + payment.amount, 0)
)}
</p>
</div>
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
<div className="w-6 h-6 bg-green-600 dark:bg-green-400 rounded"></div>
</div>
</div>
</div>
<div className="card p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Success Rate</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{filteredPayments.length > 0
? ((filteredPayments.filter(p => p.status === 'success').length / filteredPayments.length) * 100).toFixed(1)
: 0}%
</p>
</div>
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-400 rounded"></div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Transactions;

160
src/services/api.js Normal file
View File

@@ -0,0 +1,160 @@
import axios from 'axios';
// Create axios instance
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add JWT token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor to handle token expiration
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// Mock data for development
const mockData = {
stats: {
total: 1247,
success: 1189,
failed: 58
},
payments: [
{
id: 'TXN-001',
user: 'John Doe',
amount: 299.99,
status: 'success',
date: '2024-01-15T10:30:00Z',
currency: 'USD'
},
{
id: 'TXN-002',
user: 'Jane Smith',
amount: 150.00,
status: 'pending',
date: '2024-01-15T11:45:00Z',
currency: 'USD'
},
{
id: 'TXN-003',
user: 'Bob Johnson',
amount: 75.50,
status: 'failed',
date: '2024-01-15T12:15:00Z',
currency: 'USD'
},
{
id: 'TXN-004',
user: 'Alice Brown',
amount: 450.00,
status: 'success',
date: '2024-01-15T13:20:00Z',
currency: 'USD'
},
{
id: 'TXN-005',
user: 'Charlie Wilson',
amount: 89.99,
status: 'success',
date: '2024-01-15T14:30:00Z',
currency: 'USD'
}
],
chartData: [
{ date: '2024-01-09', amount: 1200 },
{ date: '2024-01-10', amount: 1900 },
{ date: '2024-01-11', amount: 3000 },
{ date: '2024-01-12', amount: 2800 },
{ date: '2024-01-13', amount: 1890 },
{ date: '2024-01-14', amount: 2390 },
{ date: '2024-01-15', amount: 3490 }
]
};
// API functions
export const authAPI = {
login: async (email, password) => {
try {
const response = await api.post('/auth/login', { email, password });
return response.data;
} catch (error) {
// Return mock data for demo
return {
token: 'mock-jwt-token-' + Date.now(),
user: {
id: 1,
email: email,
name: 'Admin User',
role: 'admin'
}
};
}
}
};
export const paymentsAPI = {
getStats: async () => {
try {
const response = await api.get('/payments/stats');
return response.data;
} catch (error) {
// Return mock data for demo
return mockData.stats;
}
},
getPayments: async (page = 1, limit = 10) => {
try {
const response = await api.get(`/payments?page=${page}&limit=${limit}`);
return response.data;
} catch (error) {
// Return mock data for demo
return {
data: mockData.payments,
total: mockData.payments.length,
page,
limit
};
}
},
getChartData: async () => {
try {
const response = await api.get('/payments/chart-data');
return response.data;
} catch (error) {
// Return mock data for demo
return mockData.chartData;
}
}
};
export default api;