first commit
This commit is contained in:
109
src/App.jsx
Normal file
109
src/App.jsx
Normal 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;
|
||||
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;
|
||||
110
src/context/AuthContext.jsx
Normal file
110
src/context/AuthContext.jsx
Normal 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
27
src/index.css
Normal 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
38
src/layout.css
Normal 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
10
src/main.jsx
Normal 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
273
src/pages/Dashboard.jsx
Normal 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
153
src/pages/Login.jsx
Normal 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
352
src/pages/Settings.jsx
Normal 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
277
src/pages/Transactions.jsx
Normal 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
160
src/services/api.js
Normal 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;
|
||||
Reference in New Issue
Block a user