feat: Implement admin dashboard, login, user management, authentication, and API utilities with port configuration.
This commit is contained in:
269
src/app/dashboard/page.tsx
Normal file
269
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, type User } from '@/lib/api';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
agents: 0,
|
||||
pendingUsers: 0,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [recentUsers, setRecentUsers] = useState<User[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.getUsers(1, 100);
|
||||
const users = response.users || [];
|
||||
setStats({
|
||||
totalUsers: response.total || 0,
|
||||
activeUsers: users.filter((u) => u.status === 'ACTIVE').length,
|
||||
agents: users.filter((u) => u.role === 'AGENT').length,
|
||||
pendingUsers: users.filter((u) => u.status === 'PENDING_VERIFICATION').length,
|
||||
});
|
||||
setRecentUsers(users.slice(0, 5));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch dashboard data:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Title */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-gray-500">Welcome to Re-Quest Admin Panel</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-blue-100 rounded-lg">
|
||||
<svg
|
||||
className="w-6 h-6 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Users</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-green-100 rounded-lg">
|
||||
<svg
|
||||
className="w-6 h-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Active</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.activeUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-purple-100 rounded-lg">
|
||||
<svg
|
||||
className="w-6 h-6 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Agents</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.agents}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 bg-yellow-100 rounded-lg">
|
||||
<svg
|
||||
className="w-6 h-6 text-yellow-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Pending</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.pendingUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Users */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Recent Users</h2>
|
||||
<Link
|
||||
href="/dashboard/users"
|
||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
{recentUsers.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 text-gray-500">No users found</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
User
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Role
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Joined
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{recentUsers.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
{user.profile?.avatar ? (
|
||||
<img
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
src={user.profile.avatar}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<span className="text-gray-500 font-medium text-sm">
|
||||
{user.profile?.firstName?.[0] || user.email[0].toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{user.profile?.firstName} {user.profile?.lastName}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">{user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.role === 'ADMIN'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: user.role === 'AGENT'
|
||||
? 'bg-purple-100 text-purple-800'
|
||||
: 'bg-blue-100 text-blue-800'
|
||||
}`}
|
||||
>
|
||||
{user.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.status === 'ACTIVE'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: user.status === 'PENDING_VERIFICATION'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: user.status === 'SUSPENDED'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{user.status === 'PENDING_VERIFICATION' ? 'PENDING' : user.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user