260 lines
8.6 KiB
TypeScript
260 lines
8.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
||
|
|
import { Search, UserPlus, Loader2 } from "lucide-react";
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from "@/components/ui/table";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
import { RoleBadge } from "./role-badge";
|
||
|
|
import { UserActions } from "./user-actions";
|
||
|
|
import { useSession } from "@/lib/auth-client";
|
||
|
|
|
||
|
|
interface User {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
email: string;
|
||
|
|
email_verified: boolean;
|
||
|
|
role: string;
|
||
|
|
banned: boolean;
|
||
|
|
created_at: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Pagination {
|
||
|
|
page: number;
|
||
|
|
limit: number;
|
||
|
|
total: number;
|
||
|
|
totalPages: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function UserTable() {
|
||
|
|
const router = useRouter();
|
||
|
|
const searchParams = useSearchParams();
|
||
|
|
const { data: session } = useSession();
|
||
|
|
|
||
|
|
const [users, setUsers] = useState<User[]>([]);
|
||
|
|
const [pagination, setPagination] = useState<Pagination | null>(null);
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
const [search, setSearch] = useState(searchParams.get("search") || "");
|
||
|
|
const [roleFilter, setRoleFilter] = useState(searchParams.get("role") || "all");
|
||
|
|
const [statusFilter, setStatusFilter] = useState(searchParams.get("status") || "all");
|
||
|
|
|
||
|
|
async function fetchUsers() {
|
||
|
|
setIsLoading(true);
|
||
|
|
try {
|
||
|
|
const params = new URLSearchParams();
|
||
|
|
if (search) params.set("search", search);
|
||
|
|
if (roleFilter && roleFilter !== "all") params.set("role", roleFilter);
|
||
|
|
if (statusFilter && statusFilter !== "all") params.set("status", statusFilter);
|
||
|
|
params.set("page", searchParams.get("page") || "1");
|
||
|
|
|
||
|
|
const response = await fetch(`/api/admin/users?${params.toString()}`);
|
||
|
|
if (!response.ok) throw new Error("Failed to fetch users");
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
setUsers(data.users);
|
||
|
|
setPagination(data.pagination);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error fetching users:", error);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchUsers();
|
||
|
|
}, [searchParams]);
|
||
|
|
|
||
|
|
function handleSearch(e: React.FormEvent) {
|
||
|
|
e.preventDefault();
|
||
|
|
const params = new URLSearchParams(searchParams.toString());
|
||
|
|
if (search) {
|
||
|
|
params.set("search", search);
|
||
|
|
} else {
|
||
|
|
params.delete("search");
|
||
|
|
}
|
||
|
|
params.set("page", "1");
|
||
|
|
router.push(`/admin/users?${params.toString()}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleFilterChange(type: "role" | "status", value: string) {
|
||
|
|
const params = new URLSearchParams(searchParams.toString());
|
||
|
|
if (value && value !== "all") {
|
||
|
|
params.set(type, value);
|
||
|
|
} else {
|
||
|
|
params.delete(type);
|
||
|
|
}
|
||
|
|
params.set("page", "1");
|
||
|
|
|
||
|
|
if (type === "role") setRoleFilter(value);
|
||
|
|
if (type === "status") setStatusFilter(value);
|
||
|
|
|
||
|
|
router.push(`/admin/users?${params.toString()}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* Filters */}
|
||
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
||
|
|
<form onSubmit={handleSearch} className="flex-1">
|
||
|
|
<div className="relative">
|
||
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||
|
|
<Input
|
||
|
|
placeholder="Search by name or email..."
|
||
|
|
value={search}
|
||
|
|
onChange={(e) => setSearch(e.target.value)}
|
||
|
|
className="pl-10"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Select value={roleFilter} onValueChange={(v) => handleFilterChange("role", v)}>
|
||
|
|
<SelectTrigger className="w-[140px]">
|
||
|
|
<SelectValue placeholder="Role" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="all">All Roles</SelectItem>
|
||
|
|
<SelectItem value="super-admin">Super Admin</SelectItem>
|
||
|
|
<SelectItem value="admin">Admin</SelectItem>
|
||
|
|
<SelectItem value="user">User</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
<Select value={statusFilter} onValueChange={(v) => handleFilterChange("status", v)}>
|
||
|
|
<SelectTrigger className="w-[140px]">
|
||
|
|
<SelectValue placeholder="Status" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="all">All Status</SelectItem>
|
||
|
|
<SelectItem value="active">Active</SelectItem>
|
||
|
|
<SelectItem value="banned">Banned</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
<Button onClick={() => router.push("/admin/users/invite")}>
|
||
|
|
<UserPlus className="mr-2 h-4 w-4" />
|
||
|
|
Invite
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Table */}
|
||
|
|
<div className="rounded-md border">
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead>Name</TableHead>
|
||
|
|
<TableHead>Email</TableHead>
|
||
|
|
<TableHead>Role</TableHead>
|
||
|
|
<TableHead>Status</TableHead>
|
||
|
|
<TableHead>Created</TableHead>
|
||
|
|
<TableHead className="w-[70px]"></TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{isLoading ? (
|
||
|
|
<TableRow>
|
||
|
|
<TableCell colSpan={6} className="text-center py-8">
|
||
|
|
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
) : users.length === 0 ? (
|
||
|
|
<TableRow>
|
||
|
|
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||
|
|
No users found
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
) : (
|
||
|
|
users.map((user) => (
|
||
|
|
<TableRow key={user.id}>
|
||
|
|
<TableCell className="font-medium">{user.name}</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
{user.email}
|
||
|
|
{user.email_verified && (
|
||
|
|
<Badge variant="outline" className="text-xs">Verified</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<RoleBadge role={user.role} />
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
{user.banned ? (
|
||
|
|
<Badge variant="destructive">Banned</Badge>
|
||
|
|
) : (
|
||
|
|
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
|
||
|
|
Active
|
||
|
|
</Badge>
|
||
|
|
)}
|
||
|
|
</TableCell>
|
||
|
|
<TableCell className="text-muted-foreground">
|
||
|
|
{new Date(user.created_at).toLocaleDateString()}
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<UserActions
|
||
|
|
user={user}
|
||
|
|
currentUserId={session?.user?.id}
|
||
|
|
onRefresh={fetchUsers}
|
||
|
|
/>
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Pagination */}
|
||
|
|
{pagination && pagination.totalPages > 1 && (
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
Showing {(pagination.page - 1) * pagination.limit + 1} to{" "}
|
||
|
|
{Math.min(pagination.page * pagination.limit, pagination.total)} of{" "}
|
||
|
|
{pagination.total} users
|
||
|
|
</p>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
disabled={pagination.page === 1}
|
||
|
|
onClick={() => {
|
||
|
|
const params = new URLSearchParams(searchParams.toString());
|
||
|
|
params.set("page", String(pagination.page - 1));
|
||
|
|
router.push(`/admin/users?${params.toString()}`);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Previous
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
size="sm"
|
||
|
|
disabled={pagination.page === pagination.totalPages}
|
||
|
|
onClick={() => {
|
||
|
|
const params = new URLSearchParams(searchParams.toString());
|
||
|
|
params.set("page", String(pagination.page + 1));
|
||
|
|
router.push(`/admin/users?${params.toString()}`);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Next
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|