feat: Implement admin dashboard, login, user management, authentication, and API utilities with port configuration.

This commit is contained in:
pradeepkumar
2025-12-20 19:22:20 +05:30
parent 2e99d8617b
commit aa97e36cfd
10 changed files with 1192 additions and 64 deletions

View File

@@ -0,0 +1,258 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { api, type User } from '@/lib/api';
export default function UsersPage() {
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [totalUsers, setTotalUsers] = useState(0);
const limit = 10;
useEffect(() => {
fetchUsers();
}, [currentPage]);
const fetchUsers = async () => {
setIsLoading(true);
setError('');
try {
const response = await api.getUsers(currentPage, limit);
setUsers(response.users || []);
setTotalUsers(response.total || 0);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
if (err instanceof Error && err.message.includes('Unauthorized')) {
router.push('/login');
}
} finally {
setIsLoading(false);
}
};
const totalPages = Math.ceil(totalUsers / limit);
return (
<div>
{/* Page Title */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Users</h1>
<p className="text-gray-500">Manage all registered users</p>
</div>
{/* Users Table */}
<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">All Users</h2>
<button
onClick={fetchUsers}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors flex items-center"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Refresh
</button>
</div>
</div>
{error && (
<div className="px-6 py-4 bg-red-50 border-b border-red-200">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
<div className="overflow-x-auto">
{isLoading ? (
<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>
) : users.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">
Provider
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Verified
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Joined
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Login
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.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">
{user.authProvider === 'LOCAL' ? 'Email' : user.authProvider}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{user.emailVerified ? (
<svg
className="w-5 h-5 text-green-500"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
) : (
<svg
className="w-5 h-5 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user.lastLoginAt
? new Date(user.lastLoginAt).toLocaleDateString()
: 'Never'}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
<p className="text-sm text-gray-500">
Showing {(currentPage - 1) * limit + 1} to{' '}
{Math.min(currentPage * limit, totalUsers)} of {totalUsers} users
</p>
<div className="flex space-x-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
)}
</div>
</div>
);
}