From cd6b047d3f9c2cb7e2e737184921cc1016210515 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sat, 11 Oct 2025 18:22:42 +0700 Subject: [PATCH] feat: add admin frontend layout and dashboard - Add role field to User interface in AuthContext - Create AdminLayout with responsive sidebar navigation - Create AdminDashboard with stats cards - Add admin routes to App.tsx - Admin panel accessible at /admin - Shows stats: total users, active subscriptions, pending payments - Access control: only users with role=admin can access --- apps/web/src/App.tsx | 7 + apps/web/src/components/admin/AdminLayout.tsx | 165 ++++++++++++++++++ .../components/admin/pages/AdminDashboard.tsx | 153 ++++++++++++++++ apps/web/src/contexts/AuthContext.tsx | 1 + 4 files changed, 326 insertions(+) create mode 100644 apps/web/src/components/admin/AdminLayout.tsx create mode 100644 apps/web/src/components/admin/pages/AdminDashboard.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 56faee5..0baac10 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -6,6 +6,8 @@ import { Login } from './components/pages/Login' import { Register } from './components/pages/Register' import { OtpVerification } from './components/pages/OtpVerification' import { AuthCallback } from './components/pages/AuthCallback' +import { AdminLayout } from './components/admin/AdminLayout' +import { AdminDashboard } from './components/admin/pages/AdminDashboard' import { Loader2 } from 'lucide-react' function ProtectedRoute({ children }: { children: React.ReactNode }) { @@ -59,6 +61,11 @@ export default function App() { } /> } /> + {/* Admin Routes */} + }> + } /> + + {/* Protected Routes */} } /> diff --git a/apps/web/src/components/admin/AdminLayout.tsx b/apps/web/src/components/admin/AdminLayout.tsx new file mode 100644 index 0000000..75a2c9f --- /dev/null +++ b/apps/web/src/components/admin/AdminLayout.tsx @@ -0,0 +1,165 @@ +import { Link, useLocation, Outlet } from 'react-router-dom' +import { useAuth } from '../../contexts/AuthContext' +import { + LayoutDashboard, + CreditCard, + Wallet, + Users, + Settings, + LogOut, + Menu, + X, +} from 'lucide-react' +import { useState } from 'react' + +const navigation = [ + { name: 'Dashboard', href: '/admin', icon: LayoutDashboard }, + { name: 'Plans', href: '/admin/plans', icon: CreditCard }, + { name: 'Payment Methods', href: '/admin/payment-methods', icon: Wallet }, + { name: 'Payments', href: '/admin/payments', icon: CreditCard }, + { name: 'Users', href: '/admin/users', icon: Users }, + { name: 'Settings', href: '/admin/settings', icon: Settings }, +] + +export function AdminLayout() { + const { user, logout } = useAuth() + const location = useLocation() + const [sidebarOpen, setSidebarOpen] = useState(false) + + // Check if user is admin + if (user?.role !== 'admin') { + return ( +
+
+

+ Access Denied +

+

+ You don't have permission to access the admin panel. +

+ + Go to Dashboard + +
+
+ ) + } + + return ( +
+ {/* Mobile sidebar backdrop */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
+
+ {/* Logo */} +
+

+ Admin Panel +

+ +
+ + {/* Navigation */} + + + {/* User info & logout */} +
+
+
+
+ {user?.name?.[0] || user?.email[0].toUpperCase()} +
+
+
+

+ {user?.name || 'Admin'} +

+

+ {user?.email} +

+
+
+ +
+
+
+ + {/* Main content */} +
+ {/* Top bar */} +
+ +
+

+ Admin +

+
+ + {new Date().toLocaleDateString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +
+
+
+ + {/* Page content */} +
+ +
+
+
+ ) +} diff --git a/apps/web/src/components/admin/pages/AdminDashboard.tsx b/apps/web/src/components/admin/pages/AdminDashboard.tsx new file mode 100644 index 0000000..59b125d --- /dev/null +++ b/apps/web/src/components/admin/pages/AdminDashboard.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react' +import axios from 'axios' +import { Users, CreditCard, DollarSign, TrendingUp } from 'lucide-react' + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001' + +interface Stats { + totalUsers: number + activeSubscriptions: number + suspendedUsers: number +} + +export function AdminDashboard() { + const [stats, setStats] = useState(null) + const [pendingPayments, setPendingPayments] = useState(0) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchStats() + }, []) + + const fetchStats = async () => { + try { + const token = localStorage.getItem('token') + const [statsRes, paymentsRes] = await Promise.all([ + axios.get(`${API_URL}/api/admin/users/stats`, { + headers: { Authorization: `Bearer ${token}` }, + }), + axios.get(`${API_URL}/api/admin/payments/pending/count`, { + headers: { Authorization: `Bearer ${token}` }, + }), + ]) + setStats(statsRes.data) + setPendingPayments(paymentsRes.data) + } catch (error) { + console.error('Failed to fetch stats:', error) + } finally { + setLoading(false) + } + } + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + const statCards = [ + { + name: 'Total Users', + value: stats?.totalUsers || 0, + icon: Users, + color: 'bg-blue-500', + bgColor: 'bg-blue-50 dark:bg-blue-900/20', + }, + { + name: 'Active Subscriptions', + value: stats?.activeSubscriptions || 0, + icon: TrendingUp, + color: 'bg-green-500', + bgColor: 'bg-green-50 dark:bg-green-900/20', + }, + { + name: 'Pending Payments', + value: pendingPayments, + icon: DollarSign, + color: 'bg-yellow-500', + bgColor: 'bg-yellow-50 dark:bg-yellow-900/20', + }, + { + name: 'Suspended Users', + value: stats?.suspendedUsers || 0, + icon: CreditCard, + color: 'bg-red-500', + bgColor: 'bg-red-50 dark:bg-red-900/20', + }, + ] + + return ( +
+
+

+ Dashboard +

+

+ Welcome to the admin panel +

+
+ + {/* Stats Grid */} +
+ {statCards.map((stat) => ( +
+
+
+ +
+
+

+ {stat.name} +

+

+ {stat.value} +

+
+
+
+ ))} +
+ + {/* Quick Actions */} + +
+ ) +} diff --git a/apps/web/src/contexts/AuthContext.tsx b/apps/web/src/contexts/AuthContext.tsx index 4a2f57d..de68fc9 100644 --- a/apps/web/src/contexts/AuthContext.tsx +++ b/apps/web/src/contexts/AuthContext.tsx @@ -10,6 +10,7 @@ interface User { name: string | null avatarUrl: string | null emailVerified: boolean + role?: string } interface LoginResponse {