185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
|
|
import { AppSidebar } from '@/components/app-sidebar';
|
|
import { SiteHeader } from '@/components/site-header';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { Eye } from 'lucide-react';
|
|
|
|
type User = {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
image: string | null;
|
|
role: string;
|
|
createdAt: Date;
|
|
enrollments: Array<{
|
|
course: {
|
|
id: string;
|
|
title: string;
|
|
};
|
|
}>;
|
|
lastActivity: Date | null;
|
|
};
|
|
|
|
function UsersAdminClientUI() {
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
const fetchUsers = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await fetch('/api/admin/users');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setUsers(data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch users:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchUsers();
|
|
}, []);
|
|
|
|
const getRoleBadgeVariant = (role: string) => {
|
|
switch (role) {
|
|
case 'superadmin':
|
|
return 'default';
|
|
case 'admin':
|
|
return 'secondary';
|
|
default:
|
|
return 'outline';
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="p-4 text-sm text-muted-foreground">Loading users...</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 py-4 md:gap-6 md:py-6 px-4 lg:px-16">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Total: {users.length} users</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
<TableHead>Courses</TableHead>
|
|
<TableHead>Last Activity</TableHead>
|
|
<TableHead>Joined</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} 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.email}</TableCell>
|
|
<TableCell>{user.name || '-'}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={getRoleBadgeVariant(user.role)}>
|
|
{user.role}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
{user.enrollments.length > 0 ? (
|
|
<div className="flex gap-1 flex-wrap">
|
|
{user.enrollments.map((enrollment, idx) => (
|
|
<Badge key={idx} variant="outline" className="text-xs">
|
|
{enrollment.course.title}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-muted-foreground text-sm">-</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{user.lastActivity ? (
|
|
formatDistanceToNow(new Date(user.lastActivity), { addSuffix: true })
|
|
) : (
|
|
<span className="text-muted-foreground text-sm">No activity</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{formatDistanceToNow(new Date(user.createdAt), { addSuffix: true })}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => router.push(`/admin/users/${user.id}`)}
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
View
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
}
|
|
export default function sersAdminClient() {
|
|
return (
|
|
<SidebarProvider
|
|
style={
|
|
{
|
|
'--sidebar-width': 'calc(var(--spacing) * 72)',
|
|
'--header-height': 'calc(var(--spacing) * 12)',
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<AppSidebar variant="inset" />
|
|
<SidebarInset>
|
|
<SiteHeader />
|
|
<div className="flex flex-1 flex-col">
|
|
<div className="@container/main flex flex-1 flex-col gap-2">
|
|
<UsersAdminClientUI />
|
|
</div>
|
|
</div>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
);
|
|
} |