ajinkya-mhetre-mern
Version:
A MERN starter with frontend and backend folders
576 lines (535 loc) • 27.2 kB
JSX
import React, { useEffect, useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { Search, Filter, Eye, Edit, Trash2, CheckCircle, XCircle, MapPin, Phone, Mail, Calendar } from 'lucide-react';
const baseUrl = import.meta.env.VITE_API_URL;
function Users() {
const { token } = useAuth();
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const [verificationFilter, setVerificationFilter] = useState('all');
const [currentPage, setCurrentPage] = useState(1);
const [selectedUser, setSelectedUser] = useState(null);
const [showUserModal, setShowUserModal] = useState(false);
const usersPerPage = 10;
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
filterUsers();
}, [users, searchTerm, roleFilter, verificationFilter]);
const fetchUsers = async () => {
try {
const res = await fetch(`${baseUrl}/admin/users`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
const data = await res.json();
if (data.success) {
setUsers(data.users);
} else {
setError('Failed to fetch users.');
}
} catch (err) {
setError('Error fetching users.');
} finally {
setLoading(false);
}
};
const filterUsers = () => {
let filtered = users.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.phone.includes(searchTerm);
const matchesRole = roleFilter === 'all' || user.role === roleFilter;
const matchesVerification = verificationFilter === 'all' ||
(verificationFilter === 'verified' && user.isVerified) ||
(verificationFilter === 'unverified' && !user.isVerified);
return matchesSearch && matchesRole && matchesVerification;
});
setFilteredUsers(filtered);
setCurrentPage(1);
};
const getRoleBadgeColor = (role) => {
switch (role) {
case 'admin': return 'bg-purple-100 text-purple-800 border-purple-200';
case 'farmer': return 'bg-green-100 text-green-800 border-green-200';
case 'customer': return 'bg-blue-100 text-blue-800 border-blue-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getCertificationBadges = (certifications) => {
if (!certifications || certifications.length === 0) {
return <span className="text-gray-400 text-sm">No certifications</span>;
}
return (
<div className="flex flex-wrap gap-1">
{certifications.slice(0, 2).map((cert, index) => (
<span key={index} className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full border border-yellow-200">
{cert}
</span>
))}
{certifications.length > 2 && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">
+{certifications.length - 2} more
</span>
)}
</div>
);
};
const openUserModal = (user) => {
setSelectedUser(user);
setShowUserModal(true);
};
// Pagination logic
const indexOfLastUser = currentPage * usersPerPage;
const indexOfFirstUser = indexOfLastUser - usersPerPage;
const currentUsers = filteredUsers.slice(indexOfFirstUser, indexOfLastUser);
const totalPages = Math.ceil(filteredUsers.length / usersPerPage);
if (loading) {
return (
<div className="w-full min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading users...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="w-full min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center text-red-600">
<XCircle className="h-16 w-16 mx-auto mb-4" />
<p className="text-xl font-semibold">{error}</p>
</div>
</div>
);
}
return (
<div className="w-full min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">User Management</h1>
<p className="text-gray-600">Manage and monitor all platform users</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-sm p-6 border border-gray-200">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg">
<Eye className="h-6 w-6 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Users</p>
<p className="text-2xl font-bold text-gray-900">{users.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
<div className="flex items-center">
<div className="p-2 bg-green-100 rounded-lg">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Farmers</p>
<p className="text-2xl font-bold text-gray-900">
{users.filter(u => u.role === 'farmer').length}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
<div className="flex items-center">
<div className="p-2 bg-purple-100 rounded-lg">
<XCircle className="h-6 w-6 text-purple-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Customers</p>
<p className="text-2xl font-bold text-gray-900">
{users.filter(u => u.role === 'customer').length}
</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm p-6 border border-gray-200">
<div className="flex items-center">
<div className="p-2 bg-yellow-100 rounded-lg">
<XCircle className="h-6 w-6 text-yellow-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Unverified</p>
<p className="text-2xl font-bold text-gray-900">
{users.filter(u => !u.isVerified).length}
</p>
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
<input
type="text"
placeholder="Search by name, email, or phone..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="flex gap-4">
<select
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
>
<option value="all">All Roles</option>
<option value="admin">Admin</option>
<option value="farmer">Farmer</option>
<option value="customer">Customer</option>
</select>
<select
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={verificationFilter}
onChange={(e) => setVerificationFilter(e.target.value)}
>
<option value="all">All Status</option>
<option value="verified">Verified</option>
<option value="unverified">Unverified</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<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 Info
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Contact
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role & Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Farm Details
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Joined Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentUsers.map((user) => (
<tr key={user._id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-600 flex items-center justify-center">
<span className="text-white font-medium text-sm">
{user.name.charAt(0).toUpperCase()}
</span>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{user.name}</div>
<div className="text-sm text-gray-500">ID: {user._id.slice(-8)}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-900">
<Mail className="h-4 w-4 text-gray-400 mr-2" />
{user.email}
</div>
<div className="flex items-center text-sm text-gray-500">
<Phone className="h-4 w-4 text-gray-400 mr-2" />
{user.phone}
</div>
<div className="flex items-center text-sm text-gray-500">
<MapPin className="h-4 w-4 text-gray-400 mr-2" />
{user.address.city}, {user.address.state}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full border ${getRoleBadgeColor(user.role)}`}>
{user.role.charAt(0).toUpperCase() + user.role.slice(1)}
</span>
<div className="flex items-center">
{user.isVerified ? (
<CheckCircle className="h-4 w-4 text-green-500 mr-1" />
) : (
<XCircle className="h-4 w-4 text-red-500 mr-1" />
)}
<span className={`text-xs ${user.isVerified ? 'text-green-600' : 'text-red-600'}`}>
{user.isVerified ? 'Verified' : 'Unverified'}
</span>
</div>
</div>
</td>
<td className="px-6 py-4">
{user.role === 'farmer' && user.farmDetails ? (
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{user.farmDetails.farmName || 'No farm name'}
</div>
<div className="text-sm text-gray-500">
Size: {user.farmDetails.farmSize || 'N/A'}
</div>
<div className="text-sm text-gray-500">
{user.farmDetails.farmLocation || 'No location'}
</div>
{getCertificationBadges(user.farmDetails.certifications)}
</div>
) : (
<span className="text-sm text-gray-400">No farm details</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-500">
<Calendar className="h-4 w-4 text-gray-400 mr-2" />
{formatDate(user.createdAt)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button
onClick={() => openUserModal(user)}
className="text-blue-600 hover:text-blue-900 transition-colors"
>
<Eye className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900 transition-colors">
<Edit className="h-4 w-4" />
</button>
<button className="text-red-600 hover:text-red-900 transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{indexOfFirstUser + 1}</span> to{' '}
<span className="font-medium">
{Math.min(indexOfLastUser, filteredUsers.length)}
</span>{' '}
of <span className="font-medium">{filteredUsers.length}</span> results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const pageNum = i + 1;
return (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
currentPage === pageNum
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
</div>
{/* User Detail Modal */}
{showUserModal && selectedUser && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4">
<div className="relative bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-900">User Details</h3>
<button
onClick={() => setShowUserModal(false)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<XCircle className="h-6 w-6" />
</button>
</div>
<div className="p-6 space-y-6">
{/* Personal Information */}
<div>
<h4 className="text-md font-semibold text-gray-900 mb-3">Personal Information</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600">Name</label>
<p className="text-sm text-gray-900">{selectedUser.name}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Email</label>
<p className="text-sm text-gray-900">{selectedUser.email}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Phone</label>
<p className="text-sm text-gray-900">{selectedUser.phone}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Role</label>
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full border ${getRoleBadgeColor(selectedUser.role)}`}>
{selectedUser.role.charAt(0).toUpperCase() + selectedUser.role.slice(1)}
</span>
</div>
</div>
</div>
{/* Address */}
<div>
<h4 className="text-md font-semibold text-gray-900 mb-3">Address</h4>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-sm text-gray-900">
{selectedUser.address.street}<br />
{selectedUser.address.city}, {selectedUser.address.state} {selectedUser.address.zipCode}
</p>
</div>
</div>
{/* Farm Details (if farmer) */}
{selectedUser.role === 'farmer' && selectedUser.farmDetails && (
<div>
<h4 className="text-md font-semibold text-gray-900 mb-3">Farm Details</h4>
<div className="bg-green-50 p-4 rounded-lg space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600">Farm Name</label>
<p className="text-sm text-gray-900">{selectedUser.farmDetails.farmName || 'Not specified'}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Farm Size</label>
<p className="text-sm text-gray-900">{selectedUser.farmDetails.farmSize || 'Not specified'}</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Location</label>
<p className="text-sm text-gray-900">{selectedUser.farmDetails.farmLocation || 'Not specified'}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Certifications</label>
<div className="mt-1">
{selectedUser.farmDetails.certifications && selectedUser.farmDetails.certifications.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedUser.farmDetails.certifications.map((cert, index) => (
<span key={index} className="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded-full border border-yellow-200">
{cert}
</span>
))}
</div>
) : (
<p className="text-sm text-gray-500">No certifications</p>
)}
</div>
</div>
</div>
</div>
)}
{/* Account Status */}
<div>
<h4 className="text-md font-semibold text-gray-900 mb-3">Account Status</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-600">Verification Status</label>
<div className="flex items-center mt-1">
{selectedUser.isVerified ? (
<CheckCircle className="h-4 w-4 text-green-500 mr-1" />
) : (
<XCircle className="h-4 w-4 text-red-500 mr-1" />
)}
<span className={`text-sm ${selectedUser.isVerified ? 'text-green-600' : 'text-red-600'}`}>
{selectedUser.isVerified ? 'Verified' : 'Unverified'}
</span>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-600">Member Since</label>
<p className="text-sm text-gray-900">{formatDate(selectedUser.createdAt)}</p>
</div>
</div>
</div>
</div>
<div className="sticky bottom-0 bg-gray-50 px-6 py-4 flex justify-end space-x-3 border-t border-gray-200">
<button
onClick={() => setShowUserModal(false)}
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors"
>
Close
</button>
<button className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 transition-colors">
Edit User
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
export default Users;